import { VideoItem } from '../viewmodel/VideoItem'; import { LengthMetrics, SegmentButton, SegmentButtonItemTuple, SegmentButtonOptions, SymbolGlyphModifier } from '@kit.ArkUI'; import { PreferencesUtil, ToastUtil, StrUtil } from '@pura/harmony-utils'; import { ButtonFancyModifier, SymbolGlyphFancyModifier, ShadowModifier } from '../common/util/AttributeModifierUtil'; import { CommonConstants } from '../common/constants/CommonConstants'; import { WebDavAccount } from '../viewmodel/WebDavAccount'; import Logger from '../common/util/Logger'; import { RemoteDriveType } from '../common/enums/RemoteDriveType'; import { navidromeRestApi, NavidromeRestAlbum, NavidromeRestArtist, NavidromeRestSong, NavidromeRestPlaylist } from '../common/network/NavidromeRestApi'; import { Utility } from '../common/util/Utility'; import { Constants } from '../Constants'; import { EventConstants } from '../common/constants/EventConstants'; import { emitter } from '@kit.BasicServicesKit'; import { setNavidromePlaylist, appendToNavidromePlaylist, setNavidromeTotalCount } from '../common/util/NavidromePlaylistStore'; import { registerLoadMoreCallback, unregisterLoadMoreCallback } from '../common/util/NavidromeRandomLoader'; import { navidromeApi, NavidromeSong } from '../common/network/NavidromeApi'; import { ServerLogUtil } from '../common/util/ServerLogUtil'; import { RemoteDriveManager } from '../common/util/RemoteDriveManager'; import { SettingPage } from '../pages/SettingPage'; import { jellyfinApi, JellyfinAlbum, JellyfinArtist, JellyfinSong } from '../common/network/JellyfinApi'; import { embyApi, EmbyAlbum, EmbyArtist, EmbySong } from '../common/network/EmbyApi'; import { audioStationApi, AudioStationAlbum, AudioStationArtist, AudioStationPlaylist, AudioStationSong } from '../common/network/AudioStationApi'; import { plexApi, PlexAlbum, PlexArtist, PlexPlaylist, PlexSong } from '../common/network/PlexApi'; import { daoLiYuApi, DaoLiYuAlbum, DaoLiYuArtist, DaoLiYuTrack, DaoLiYuPlaylist } from '../common/network/DaoLiYuApi'; 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; interface PlaylistEventData { playlistId: string; playlistName: string; songCount: number; startIndex: number; isJump: boolean; songFilePaths: string[]; } enum NavFilterType { None = 0, Artist = 1, Album = 2, Playlist = 3 } interface CachePreviewInfo { cacheRoot: string; examplePath: string; } interface LibraryInfo { type: number; scheme: string; } class AudioStationAlbumKey { name: string; artist: string; constructor(name: string, artist: string) { this.name = name; this.artist = artist; } } /** * taskpool 任务结果接口 * 返回带封面的完整数据 */ interface TaskResult { songs: VideoItem[]; artists: NavidromeRestArtist[]; albums: NavidromeRestAlbum[]; playlists: NavidromeRestPlaylist[]; } /** * Navidrome API 分页响应接口 */ interface PagedResponse { 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 RemoteMusicPage { @State isShowTitleBar: boolean = true //是否显示分类导航条 private scroller: Scroller = new Scroller() private prevOffsetY: number = 0; // 记录上一次的Y轴偏移量 @State autoHideTitle: boolean = true //滚动自动隐藏标题栏 @State isCustomizeBg: boolean = false //自定义背景界面 @StorageProp('isLandscape') isLandscape: boolean = false; @State blurValue: number = 0 //背景模糊 @State bgBrightness: number = 0 //背景亮度 @State customizeBgPath: string | undefined = ''; @Link mType: number; @Link offsetX: number; @Link isShowDrawer: boolean; @State isNoJumpToHome: boolean = false //网盘播放不跳转首页 @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0; @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount; @StorageProp('currentTheme') currentTheme: number = 0; @State selectedTab: number = 0; // 0: 全部, 1: 艺术家, 2: 专辑 @State allVideos: VideoItem[] = []; @State artists: NavidromeRestArtist[] = []; @State albums: NavidromeRestAlbum[] = []; @State playlists: NavidromeRestPlaylist[] = []; @State loading: boolean = false; @State songNextStart: number | null = 0; @State artistNextStart: number | null = 0; @State albumNextStart: number | null = 0; @State playlistNextStart: number | null = 0; @State isSongPageLoading: boolean = false; @State isArtistPageLoading: boolean = false; @State isAlbumPageLoading: boolean = false; @State isPlaylistPageLoading: boolean = false; @State serverTotalSongCount: number = 0; // 服务端返回的歌曲总数 // 新增:详情视图状态 @Link isDetailView: boolean; // 是否在艺术家/专辑详情视图 @State previousTab: number = 0; // 进入详情视图前的标签页索引 @StorageProp('isDarkMode') isDarkMode: boolean = false; @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined; @StorageProp('topSafeHeight') topSafeHeight: number = 0; @State @Watch('onTabSelectedIndexesChanged') tabSelectedIndexes: number[] = [0]; @StorageProp('themeColor') themeColor: string = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR); private tabs: string[] = ['全部', '艺术家', '专辑', '歌单']; private loadTicket: number = 0; @State filterType: NavFilterType = NavFilterType.None; @State filterLabel: string = ''; @State filterId: string = ''; @State filterSongs: VideoItem[] = []; @State isFilterLoading: boolean = false; private coverUrlCache: Map = new Map(); private albumCoverLookup: Map = new Map(); private searchTicket: number = 0; private readonly REMOTE_PAGE_SIZE: number = 200; private jellyfinSongIdSeen: Set = new Set(); private embySongIdSeen: Set = new Set(); private jellyfinSongKeySeen: Set = new Set(); private embySongKeySeen: Set = new Set(); private listCache = NavidromeListCache.getInstance(); private isNavidromeAccount(account: WebDavAccount): boolean { return account.webType === RemoteDriveType.Navidrome; } private isJellyfinAccount(account: WebDavAccount): boolean { return account.webType === RemoteDriveType.Jellyfin; } private isEmbyAccount(account: WebDavAccount): boolean { return account.webType === RemoteDriveType.Emby; } private isAudioStationAccount(account: WebDavAccount): boolean { return account.webType === RemoteDriveType.AudioStation; } private isPlexAccount(account: WebDavAccount): boolean { return account.webType === RemoteDriveType.Plex; } private isDaoLiYuAccount(account: WebDavAccount): boolean { return account.webType === RemoteDriveType.DaoLiYu; } private createTabOptions(): SegmentButtonOptions { const buttons = [ { text: '全部' }, { 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 }); } 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('RemoteMusicPage', `获取缓存预览路径失败: ${(error as Error).message}`); return null; } } // 搜索和排序相关状态 @State isSearchMode: boolean = false; @State searchText: string = ''; // 用户输入内容 @State filteredList: Array = []; // 过滤后的歌曲结果 @State sortType: number = 0; // 排序类型 0:名称升序 1:名称降序 2:时间升序 3:时间降序 4:大小升序 5:大小降序 @State isSearchLoading: boolean = false; private readonly REMOTE_SEARCH_LIMIT: number = 500; // SegmentButton选项 @State tabOptions: SegmentButtonOptions = this.createTabOptions(); //当胶囊按钮的选择发生变化时调用此函数 onTabSelectedIndexesChanged() { this.selectedTab = this.tabSelectedIndexes[0]; console.info('heanup', `Selected tab: ${this.tabs[this.selectedTab]}`); // 如果在详情视图模式下切换标签页,退出详情视图并清除筛选 if (this.isDetailView) { this.isDetailView = false; this.clearFilter(); } // 从艺术家或专辑切换回全部时,清除筛选状态 if (this.selectedTab === 0 && this.filterType !== NavFilterType.None) { // 不清除筛选,保持筛选状态 } else if (this.selectedTab !== 0) { // 切换到艺术家或专辑标签页时,清除筛选和搜索状态 this.clearFilter(); this.isSearchMode = false; this.searchText = ''; this.filteredList = []; this.isSearchLoading = false; this.searchTicket++; } } //切换不同的RemoteMusicPage async onSwitchAccount() { await this.refreshNavidromeData(true); } initSetting(){ this.isCustomizeBg = PreferencesUtil.getBooleanSync(SettingPage.IS_CUSTOMIZE_BG, false) this.customizeBgPath = PreferencesUtil.getStringSync(SettingPage.IS_CUSTOMIZE_BG_PATH, '') this.blurValue = PreferencesUtil.getNumberSync(SettingPage.CUSTOMIZE_BG_BLUR, 0) this.bgBrightness = PreferencesUtil.getNumberSync(SettingPage.BG_BRIGHTNESS, 0) this.isNoJumpToHome = PreferencesUtil.getBooleanSync('isNoJumpToHome', true) this.sortType = PreferencesUtil.getNumberSync('navidromeSortType', 0); this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true) this.autoHideTitle = PreferencesUtil.getBooleanSync('autoHideTitle', true) } aboutToAppear() { this.initSetting() let eventSetting: emitter.InnerEvent = { eventId: 333 } // 监听广播事件(通用设置配置更新) emitter.on(eventSetting, (eventData: emitter.EventData) => { this.initSetting() }); // 监听手势返回事件 let eventBackSwipeBack: emitter.InnerEvent = { eventId: EventConstants.EVENT_SWIPE_BACK_NAVID } emitter.on(eventBackSwipeBack, (eventData: emitter.EventData) => { console.info('heanup', 'RemoteMusicPage 收到 EVENT_SWIPE_BACK_NAVID 事件'); // 如果在详情视图模式,退出详情视图 if (this.isDetailView) { this.getUIContext().animateTo({ duration: 555 }, () => { this.isDetailView = false; this.clearFilter(); }) } }); // 注册加载更多页回调(用于渐进式随机播放) this.registerRandomLoadCallback(); this.refreshNavidromeData(); } /** * 注册加载更多页回调函数(供LocalMusic的randomPlay调用) */ private registerRandomLoadCallback(): void { registerLoadMoreCallback( // 加载一页数据并返回新增歌曲 async (): Promise => { const account = this.resolveActiveAccount(); if (!account) { return []; } // 记录加载前的数量 const beforeCount = this.allVideos.length; // 根据账户类型调用对应的加载方法 await this.loadNextSongPageByAccountType(account); // 获取新增的歌曲 const newItems = this.allVideos.slice(beforeCount); // 同步到播放列表存储 if (newItems.length > 0) { appendToNavidromePlaylist(newItems); void ServerLogUtil.info('NavidromeRandom', `加载更多: 新增${newItems.length}首,总数${this.allVideos.length}`); } return newItems; }, // 检查是否还有更多数据 (): boolean => { return this.songNextStart !== null && !this.isSongPageLoading; } ); } /** * 根据账户类型调用对应的歌曲加载方法 */ private async loadNextSongPageByAccountType(account: WebDavAccount): Promise { if (this.isJellyfinAccount(account)) { await this.loadNextJellyfinSongPage(account); } else if (this.isEmbyAccount(account)) { await this.loadNextEmbySongPage(account); } else if (this.isAudioStationAccount(account)) { await this.loadNextAudioStationSongPage(account); } else if (this.isPlexAccount(account)) { await this.loadNextPlexSongPage(account); } else if (this.isDaoLiYuAccount(account)) { await this.loadNextDaoLiYuSongPage(account); } else { // 默认使用 Navidrome await this.loadNextSongPage(account); } } private async refreshNavidromeData(showToastWhenMissing: boolean = false): Promise { const account = this.resolveActiveAccount(); if (!account) { this.resetData(); if (showToastWhenMissing) { ToastUtil.showToast('请先选择媒体库账号'); } return; } // 记录日志开关状态 void ServerLogUtil.info('NavidromeLoad', `日志状态: ${ServerLogUtil.getLogUploadStatus()}`); // 记录账号信息和缓存路径 await this.logAccountCacheInfo(account); await this.loadMediaLibrary(account); this.doSortType(this.sortType) } private resolveActiveAccount(): WebDavAccount | undefined { if (!this.selectedAccount) { return undefined; } if (!this.isNavidromeAccount(this.selectedAccount) && !this.isJellyfinAccount(this.selectedAccount) && !this.isEmbyAccount(this.selectedAccount) && !this.isAudioStationAccount(this.selectedAccount) && !this.isPlexAccount(this.selectedAccount) && !this.isDaoLiYuAccount(this.selectedAccount)) { return undefined; } if (!this.selectedAccount.host || this.selectedAccount.host.length === 0) { return undefined; } return this.selectedAccount; } private resetData(): void { this.allVideos = []; this.artists = []; this.albums = []; this.playlists = []; this.jellyfinSongIdSeen.clear(); this.embySongIdSeen.clear(); this.jellyfinSongKeySeen.clear(); this.embySongKeySeen.clear(); this.clearFilter(); this.coverUrlCache.clear(); this.albumCoverLookup.clear(); this.songNextStart = 0; this.artistNextStart = 0; this.albumNextStart = 0; this.playlistNextStart = 0; this.isSongPageLoading = false; this.isArtistPageLoading = false; this.isAlbumPageLoading = false; this.isPlaylistPageLoading = false; this.serverTotalSongCount = 0; } private async loadMediaLibrary(account: WebDavAccount): Promise { if (this.isNavidromeAccount(account)) { await this.loadNavidromeLibrary(account); return; } if (this.isJellyfinAccount(account)) { await this.loadJellyfinLibrary(account); return; } if (this.isEmbyAccount(account)) { await this.loadEmbyLibrary(account); return; } if (this.isAudioStationAccount(account)) { await this.loadAudioStationLibrary(account); return; } if (this.isPlexAccount(account)) { await this.loadPlexLibrary(account); return; } if (this.isDaoLiYuAccount(account)) { await this.loadDaoLiYuLibrary(account); } } private async loadNavidromeLibrary(account: WebDavAccount): Promise { const ticket = ++this.loadTicket; this.loading = true; this.resetData(); void ServerLogUtil.info('NavidromeLoad', '开始加载 Navidrome 数据库'); void ServerLogUtil.info('NavidromeLoad', `服务器: ${ServerLogUtil.sanitizeAccount(account)}`); void ServerLogUtil.info('NavidromeLoad', `封面URL缓存已清空,当前缓存数量: ${this.coverUrlCache.size}`); 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), this.loadNextAlbumPage(account, ticket), this.loadNextPlaylistPage(account, ticket) ]); if (ticket !== this.loadTicket) { return; } // 保存缓存(前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) { 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', '加载流程结束'); } } } private async loadJellyfinLibrary(account: WebDavAccount): Promise { const ticket = ++this.loadTicket; this.loading = true; this.resetData(); void ServerLogUtil.info('NavidromeLoad', '开始加载 Jellyfin 媒体库'); void ServerLogUtil.info('NavidromeLoad', `服务器: ${ServerLogUtil.sanitizeAccount(account)}`); try { this.songNextStart = 0; this.artistNextStart = 0; this.albumNextStart = 0; this.playlistNextStart = null; await Promise.all([ this.loadNextJellyfinSongPage(account, ticket), this.loadNextJellyfinArtistPage(account, ticket), this.loadNextJellyfinAlbumPage(account, ticket) ]); if (ticket === this.loadTicket) { void ServerLogUtil.info('NavidromeLoad', `Jellyfin 首屏完成: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length}`); } } catch (error) { if (ticket === this.loadTicket) { void ServerLogUtil.error('NavidromeLoad', `Jellyfin 数据加载失败: ${(error as Error).message}`); ToastUtil.showToast((error as Error).message ?? 'Jellyfin 数据加载失败'); } } finally { if (ticket === this.loadTicket) { this.loading = false; } } } private async loadEmbyLibrary(account: WebDavAccount): Promise { const ticket = ++this.loadTicket; this.loading = true; this.resetData(); void ServerLogUtil.info('NavidromeLoad', '开始加载 Emby 媒体库'); void ServerLogUtil.info('NavidromeLoad', `服务器: ${ServerLogUtil.sanitizeAccount(account)}`); try { this.songNextStart = 0; this.artistNextStart = 0; this.albumNextStart = 0; this.playlistNextStart = null; await Promise.all([ this.loadNextEmbySongPage(account, ticket), this.loadNextEmbyArtistPage(account, ticket), this.loadNextEmbyAlbumPage(account, ticket) ]); if (ticket === this.loadTicket) { void ServerLogUtil.info('NavidromeLoad', `Emby 首屏完成: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length}`); } } catch (error) { if (ticket === this.loadTicket) { void ServerLogUtil.error('NavidromeLoad', `Emby 数据加载失败: ${(error as Error).message}`); ToastUtil.showToast((error as Error).message ?? 'Emby 数据加载失败'); } } finally { if (ticket === this.loadTicket) { this.loading = false; } } } private async loadAudioStationLibrary(account: WebDavAccount): Promise { const ticket = ++this.loadTicket; this.loading = true; this.resetData(); void ServerLogUtil.info('NavidromeLoad', '开始加载 AudioStation 媒体库'); void ServerLogUtil.info('NavidromeLoad', `服务器: ${ServerLogUtil.sanitizeAccount(account)}`); try { this.songNextStart = 0; this.artistNextStart = 0; this.albumNextStart = 0; this.playlistNextStart = 0; await Promise.all([ this.loadNextAudioStationSongPage(account, ticket), this.loadNextAudioStationArtistPage(account, ticket), this.loadNextAudioStationAlbumPage(account, ticket), this.loadNextAudioStationPlaylistPage(account, ticket) ]); if (ticket === this.loadTicket) { void ServerLogUtil.info('NavidromeLoad', `AudioStation 首屏完成: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length} / 歌单 ${this.playlists.length}`); } } catch (error) { if (ticket === this.loadTicket) { void ServerLogUtil.error('NavidromeLoad', `AudioStation 数据加载失败: ${(error as Error).message}`); ToastUtil.showToast((error as Error).message ?? 'AudioStation 数据加载失败'); } } finally { if (ticket === this.loadTicket) { this.loading = false; } } } private async loadPlexLibrary(account: WebDavAccount): Promise { const ticket = ++this.loadTicket; this.loading = true; this.resetData(); void ServerLogUtil.info('NavidromeLoad', '开始加载 Plex 媒体库'); void ServerLogUtil.info('NavidromeLoad', `服务器: ${ServerLogUtil.sanitizeAccount(account)}`); try { this.songNextStart = 0; this.artistNextStart = 0; this.albumNextStart = 0; this.playlistNextStart = 0; await Promise.all([ this.loadNextPlexSongPage(account, ticket), this.loadNextPlexArtistPage(account, ticket), this.loadNextPlexAlbumPage(account, ticket), this.loadNextPlexPlaylistPage(account, ticket) ]); if (ticket === this.loadTicket) { void ServerLogUtil.info('NavidromeLoad', `Plex 首屏完成: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length} / 歌单 ${this.playlists.length}`); } } catch (error) { if (ticket === this.loadTicket) { void ServerLogUtil.error('NavidromeLoad', `Plex 数据加载失败: ${(error as Error).message}`); ToastUtil.showToast((error as Error).message ?? 'Plex 数据加载失败'); } } finally { if (ticket === this.loadTicket) { this.loading = false; } } } private async loadDaoLiYuLibrary(account: WebDavAccount): Promise { const ticket = ++this.loadTicket; this.loading = true; this.resetData(); void ServerLogUtil.info('NavidromeLoad', '开始加载 道理鱼 媒体库'); void ServerLogUtil.info('NavidromeLoad', `服务器: ${ServerLogUtil.sanitizeAccount(account)}`); try { this.songNextStart = 0; this.artistNextStart = 0; this.albumNextStart = 0; this.playlistNextStart = 0; await Promise.all([ this.loadNextDaoLiYuSongPage(account, ticket), this.loadNextDaoLiYuArtistPage(account, ticket), this.loadNextDaoLiYuAlbumPage(account, ticket), this.loadNextDaoLiYuPlaylistPage(account, ticket) ]); if (ticket === this.loadTicket) { void ServerLogUtil.info('NavidromeLoad', `道理鱼 首屏完成: 歌曲 ${this.allVideos.length} / 艺术家 ${this.artists.length} / 专辑 ${this.albums.length}`); } } catch (error) { if (ticket === this.loadTicket) { void ServerLogUtil.error('NavidromeLoad', `道理鱼 数据加载失败: ${(error as Error).message}`); ToastUtil.showToast((error as Error).message ?? '道理鱼 数据加载失败'); } } finally { if (ticket === this.loadTicket) { this.loading = false; } } } private async loadNextSongPage(account: WebDavAccount, ticket?: number): Promise { 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; } const chunk = response.data ?? []; if (chunk.length === 0) { this.songNextStart = null; return; } const videos = await this.convertSongsToVideoItems(chunk, account); if (currentTicket !== this.loadTicket) { 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 { this.isSongPageLoading = false; } } private async loadNextArtistPage(account: WebDavAccount, ticket?: number): Promise { 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.artists = [...this.artists, ...processed]; // 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 // })); // } this.artistNextStart = response.nextStart; void ServerLogUtil.info('ArtistCover', `艺术家列表追加: 本次 ${processed.length} 位, 总数 ${this.artists.length}`); } finally { this.isArtistPageLoading = false; } } private async loadNextAlbumPage(account: WebDavAccount, ticket?: number): Promise { 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; } 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 loadNextJellyfinSongPage(account: WebDavAccount, ticket?: number): Promise { if (this.songNextStart === null || this.isSongPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; const isFirstPage = this.songNextStart === 0; this.isSongPageLoading = true; try { const response = await jellyfinApi.getSongsPage(account, this.songNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } // 首次加载时保存服务端总数 if (isFirstPage && response.total !== undefined && response.total > 0) { this.serverTotalSongCount = response.total; void ServerLogUtil.info('NavidromeLoad', `Jellyfin 服务端歌曲总数: ${this.serverTotalSongCount}`); } const uniqueItems: JellyfinSong[] = []; for (let i = 0; i < response.items.length; i++) { const item = response.items[i]; if (!item.id) { continue; } const key = this.buildSongDedupKey(item.title, item.artist, item.album, item.durationSeconds); if (this.jellyfinSongIdSeen.has(item.id) || this.jellyfinSongKeySeen.has(key)) { continue; } this.jellyfinSongIdSeen.add(item.id); this.jellyfinSongKeySeen.add(key); uniqueItems.push(item); } const restSongs = this.convertJellyfinSongsToRestSongs(uniqueItems); const videoItems = await this.convertSongsToVideoItems(restSongs, account); if (currentTicket !== this.loadTicket) { return; } this.allVideos = this.dedupRemoteSongsByKey([...this.allVideos, ...videoItems]); this.songNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `Jellyfin 歌曲追加: 本次 ${videoItems.length} 首, 已加载 ${this.allVideos.length}, 服务端总数 ${this.serverTotalSongCount}`); } finally { this.isSongPageLoading = false; } } private async loadNextJellyfinArtistPage(account: WebDavAccount, ticket?: number): Promise { if (this.artistNextStart === null || this.isArtistPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isArtistPageLoading = true; try { const response = await jellyfinApi.getArtistsPage(account, this.artistNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = await this.convertJellyfinArtistsToRest(response.items, account); if (currentTicket !== this.loadTicket) { return; } this.artists = [...this.artists, ...processed]; this.artistNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `Jellyfin 艺术家追加: 本次 ${processed.length} 位, 总数 ${this.artists.length}`); } finally { this.isArtistPageLoading = false; } } private async loadNextJellyfinAlbumPage(account: WebDavAccount, ticket?: number): Promise { if (this.albumNextStart === null || this.isAlbumPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isAlbumPageLoading = true; try { const response = await jellyfinApi.getAlbumsPage(account, this.albumNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = await this.convertJellyfinAlbumsToRest(response.items, account); if (currentTicket !== this.loadTicket) { return; } this.albums = [...this.albums, ...processed]; this.albumNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `Jellyfin 专辑追加: 本次 ${processed.length} 张, 总数 ${this.albums.length}`); } finally { this.isAlbumPageLoading = false; } } private async loadNextEmbySongPage(account: WebDavAccount, ticket?: number): Promise { if (this.songNextStart === null || this.isSongPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; const isFirstPage = this.songNextStart === 0; this.isSongPageLoading = true; try { const response = await embyApi.getSongsPage(account, this.songNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } // 首次加载时保存服务端总数 if (isFirstPage && response.total !== undefined && response.total > 0) { this.serverTotalSongCount = response.total; void ServerLogUtil.info('NavidromeLoad', `Emby 服务端歌曲总数: ${this.serverTotalSongCount}`); } const uniqueItems: EmbySong[] = []; for (let i = 0; i < response.items.length; i++) { const item = response.items[i]; if (!item.id) { continue; } const key = this.buildSongDedupKey(item.title, item.artist, item.album, item.durationSeconds); if (this.embySongIdSeen.has(item.id) || this.embySongKeySeen.has(key)) { continue; } this.embySongIdSeen.add(item.id); this.embySongKeySeen.add(key); uniqueItems.push(item); } const restSongs = this.convertEmbySongsToRestSongs(uniqueItems); const videoItems = await this.convertSongsToVideoItems(restSongs, account); if (currentTicket !== this.loadTicket) { return; } this.allVideos = this.dedupRemoteSongsByKey([...this.allVideos, ...videoItems]); this.songNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `Emby 歌曲追加: 本次 ${videoItems.length} 首, 已加载 ${this.allVideos.length}, 服务端总数 ${this.serverTotalSongCount}`); } finally { this.isSongPageLoading = false; } } private async loadNextEmbyArtistPage(account: WebDavAccount, ticket?: number): Promise { if (this.artistNextStart === null || this.isArtistPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isArtistPageLoading = true; try { const response = await embyApi.getArtistsPage(account, this.artistNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = await this.convertEmbyArtistsToRest(response.items, account); if (currentTicket !== this.loadTicket) { return; } this.artists = [...this.artists, ...processed]; this.artistNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `Emby 艺术家追加: 本次 ${processed.length} 位, 总数 ${this.artists.length}`); } finally { this.isArtistPageLoading = false; } } private async loadNextEmbyAlbumPage(account: WebDavAccount, ticket?: number): Promise { if (this.albumNextStart === null || this.isAlbumPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isAlbumPageLoading = true; try { const response = await embyApi.getAlbumsPage(account, this.albumNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = await this.convertEmbyAlbumsToRest(response.items, account); if (currentTicket !== this.loadTicket) { return; } this.albums = [...this.albums, ...processed]; this.albumNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `Emby 专辑追加: 本次 ${processed.length} 张, 总数 ${this.albums.length}`); } finally { this.isAlbumPageLoading = false; } } private async loadNextAudioStationSongPage(account: WebDavAccount, ticket?: number): Promise { if (this.songNextStart === null || this.isSongPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; const isFirstPage = this.songNextStart === 0; // 在调用前保存是否为首页 this.isSongPageLoading = true; try { const response = await audioStationApi.getSongsPage(account, this.songNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } // 首次加载时保存服务端总数 if (isFirstPage && response.total !== undefined && response.total > 0) { this.serverTotalSongCount = response.total; void ServerLogUtil.info('NavidromeLoad', `AudioStation 服务端歌曲总数: ${this.serverTotalSongCount}`); } const restSongs = this.convertAudioStationSongsToRestSongs(response.items); const videoItems = await this.convertSongsToVideoItems(restSongs, account); if (currentTicket !== this.loadTicket) { return; } this.allVideos = this.dedupRemoteSongsByKey([...this.allVideos, ...videoItems]); this.songNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `AudioStation 歌曲追加: 本次 ${videoItems.length} 首, 已加载 ${this.allVideos.length}, 服务端总数 ${this.serverTotalSongCount}`); } finally { this.isSongPageLoading = false; } } private async loadNextAudioStationArtistPage(account: WebDavAccount, ticket?: number): Promise { if (this.artistNextStart === null || this.isArtistPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isArtistPageLoading = true; try { const response = await audioStationApi.getArtistsPage(account, this.artistNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = this.convertAudioStationArtistsToRest(response.items); if (currentTicket !== this.loadTicket) { return; } this.artists = [...this.artists, ...processed]; this.artistNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `AudioStation 艺术家追加: 本次 ${processed.length} 位, 总数 ${this.artists.length}`); } finally { this.isArtistPageLoading = false; } } private async loadNextAudioStationAlbumPage(account: WebDavAccount, ticket?: number): Promise { if (this.albumNextStart === null || this.isAlbumPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isAlbumPageLoading = true; try { const response = await audioStationApi.getAlbumsPage(account, undefined, this.albumNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = await this.convertAudioStationAlbumsToRest(response.items, account); if (currentTicket !== this.loadTicket) { return; } this.albums = [...this.albums, ...processed]; this.albumNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `AudioStation 专辑追加: 本次 ${processed.length} 张, 总数 ${this.albums.length}`); } finally { this.isAlbumPageLoading = false; } } private async loadNextAudioStationPlaylistPage(account: WebDavAccount, ticket?: number): Promise { if (this.playlistNextStart === null || this.isPlaylistPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isPlaylistPageLoading = true; try { const response = await audioStationApi.getPlaylistsPage(account, this.playlistNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = this.convertAudioStationPlaylistsToRest(response.items); if (currentTicket !== this.loadTicket) { return; } this.playlists = [...this.playlists, ...processed]; this.playlistNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `AudioStation 歌单追加: 本次 ${processed.length} 个, 总数 ${this.playlists.length}`); } finally { this.isPlaylistPageLoading = false; } } private async loadNextPlexSongPage(account: WebDavAccount, ticket?: number): Promise { if (this.songNextStart === null || this.isSongPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; const isFirstPage = this.songNextStart === 0; this.isSongPageLoading = true; try { const response = await plexApi.getSongsPage(account, this.songNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } // 首次加载时保存服务端总数 if (isFirstPage && response.total !== undefined && response.total > 0) { this.serverTotalSongCount = response.total; void ServerLogUtil.info('NavidromeLoad', `Plex 服务端歌曲总数: ${this.serverTotalSongCount}`); } const restSongs = this.convertPlexSongsToRestSongs(response.items); const videoItems = await this.convertSongsToVideoItems(restSongs, account); if (currentTicket !== this.loadTicket) { return; } this.allVideos = this.dedupRemoteSongsByKey([...this.allVideos, ...videoItems]); this.songNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `Plex 歌曲追加: 本次 ${videoItems.length} 首, 已加载 ${this.allVideos.length}, 服务端总数 ${this.serverTotalSongCount}`); } finally { this.isSongPageLoading = false; } } private async loadNextPlexArtistPage(account: WebDavAccount, ticket?: number): Promise { if (this.artistNextStart === null || this.isArtistPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isArtistPageLoading = true; try { const response = await plexApi.getArtistsPage(account, this.artistNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = await this.convertPlexArtistsToRest(response.items, account); if (currentTicket !== this.loadTicket) { return; } this.artists = [...this.artists, ...processed]; this.artistNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `Plex 艺术家追加: 本次 ${processed.length} 位, 总数 ${this.artists.length}`); } finally { this.isArtistPageLoading = false; } } private async loadNextPlexAlbumPage(account: WebDavAccount, ticket?: number): Promise { if (this.albumNextStart === null || this.isAlbumPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isAlbumPageLoading = true; try { const response = await plexApi.getAlbumsPage(account, this.albumNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = await this.convertPlexAlbumsToRest(response.items, account); if (currentTicket !== this.loadTicket) { return; } this.albums = [...this.albums, ...processed]; this.albumNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `Plex 专辑追加: 本次 ${processed.length} 张, 总数 ${this.albums.length}`); } finally { this.isAlbumPageLoading = false; } } private async loadNextPlexPlaylistPage(account: WebDavAccount, ticket?: number): Promise { if (this.playlistNextStart === null || this.isPlaylistPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isPlaylistPageLoading = true; try { const response = await plexApi.getPlaylistsPage(account, this.playlistNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = this.convertPlexPlaylistsToRest(response.items); if (currentTicket !== this.loadTicket) { return; } this.playlists = [...this.playlists, ...processed]; this.playlistNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `Plex 歌单追加: 本次 ${processed.length} 个, 总数 ${this.playlists.length}`); } finally { this.isPlaylistPageLoading = false; } } private async loadNextDaoLiYuSongPage(account: WebDavAccount, ticket?: number): Promise { if (this.songNextStart === null || this.isSongPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; const isFirstPage = this.songNextStart === 0; this.isSongPageLoading = true; try { const response = await daoLiYuApi.getTracksPage(account, this.songNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } // 首次加载时保存服务端总数 if (isFirstPage && response.total !== undefined && response.total > 0) { this.serverTotalSongCount = response.total; void ServerLogUtil.info('NavidromeLoad', `道理鱼 服务端歌曲总数: ${this.serverTotalSongCount}`); } const restSongs = this.convertDaoLiYuSongsToRestSongs(response.items); const videoItems = await this.convertSongsToVideoItems(restSongs, account); if (currentTicket !== this.loadTicket) { return; } this.allVideos = [...this.allVideos, ...videoItems]; this.songNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `道理鱼 歌曲追加: 本次 ${videoItems.length} 首, 已加载 ${this.allVideos.length}, 服务端总数 ${this.serverTotalSongCount}`); } finally { this.isSongPageLoading = false; } } private async loadNextDaoLiYuArtistPage(account: WebDavAccount, ticket?: number): Promise { if (this.artistNextStart === null || this.isArtistPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isArtistPageLoading = true; try { const response = await daoLiYuApi.getArtistsPage(account, this.artistNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = await this.convertDaoLiYuArtistsToRest(response.items, account); if (currentTicket !== this.loadTicket) { return; } this.artists = [...this.artists, ...processed]; this.artistNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `道理鱼 艺术家追加: 本次 ${processed.length} 位, 总数 ${this.artists.length}`); } finally { this.isArtistPageLoading = false; } } private async loadNextDaoLiYuAlbumPage(account: WebDavAccount, ticket?: number): Promise { if (this.albumNextStart === null || this.isAlbumPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isAlbumPageLoading = true; try { const response = await daoLiYuApi.getAlbumsPage(account, this.albumNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = await this.convertDaoLiYuAlbumsToRest(response.items, account); if (currentTicket !== this.loadTicket) { return; } this.albums = [...this.albums, ...processed]; this.albumNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `道理鱼 专辑追加: 本次 ${processed.length} 张, 总数 ${this.albums.length}`); } finally { this.isAlbumPageLoading = false; } } private async loadNextDaoLiYuPlaylistPage(account: WebDavAccount, ticket?: number): Promise { if (this.playlistNextStart === null || this.isPlaylistPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isPlaylistPageLoading = true; try { const response = await daoLiYuApi.getPlaylistsPage(account, this.playlistNextStart, this.REMOTE_PAGE_SIZE); if (currentTicket !== this.loadTicket) { return; } const processed = await this.convertDaoLiYuPlaylistsToRest(response.items, account); if (currentTicket !== this.loadTicket) { return; } this.playlists = [...this.playlists, ...processed]; this.playlistNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `道理鱼 歌单追加: 本次 ${processed.length} 个, 总数 ${this.playlists.length}`); } finally { this.isPlaylistPageLoading = false; } } private async loadNextPlaylistPage(account: WebDavAccount, ticket?: number): Promise { if (this.playlistNextStart === null || this.isPlaylistPageLoading) { return; } const currentTicket = ticket ?? this.loadTicket; this.isPlaylistPageLoading = true; try { const response = await navidromeRestApi.fetchPlaylistPage(account, this.playlistNextStart); if (currentTicket !== this.loadTicket) { return; } const chunk = response.data ?? []; if (chunk.length === 0) { this.playlistNextStart = null; return; } this.playlists = [...this.playlists, ...chunk]; this.playlistNextStart = response.nextStart; void ServerLogUtil.info('NavidromeLoad', `歌单列表追加: 本次 ${chunk.length} 个, 总数 ${this.playlists.length}`); } finally { this.isPlaylistPageLoading = false; } } private async handleReachEnd(): Promise { if (this.loading) { return; } if (this.selectedTab === 0 && this.filterType !== NavFilterType.None) { return; } const account = this.selectedAccount; if (!account) { return; } if (this.isNavidromeAccount(account)) { switch (this.selectedTab) { case 0: await this.loadNextSongPage(account); break; case 1: await this.loadNextArtistPage(account); break; case 2: await this.loadNextAlbumPage(account); break; case 3: await this.loadNextPlaylistPage(account); break; default: break; } return; } if (this.isJellyfinAccount(account)) { switch (this.selectedTab) { case 0: await this.loadNextJellyfinSongPage(account); break; case 1: await this.loadNextJellyfinArtistPage(account); break; case 2: await this.loadNextJellyfinAlbumPage(account); break; default: break; } return; } if (this.isEmbyAccount(account)) { switch (this.selectedTab) { case 0: await this.loadNextEmbySongPage(account); break; case 1: await this.loadNextEmbyArtistPage(account); break; case 2: await this.loadNextEmbyAlbumPage(account); break; default: break; } return; } if (this.isAudioStationAccount(account)) { switch (this.selectedTab) { case 0: await this.loadNextAudioStationSongPage(account); break; case 1: await this.loadNextAudioStationArtistPage(account); break; case 2: await this.loadNextAudioStationAlbumPage(account); break; case 3: await this.loadNextAudioStationPlaylistPage(account); break; default: break; } return; } if (this.isPlexAccount(account)) { switch (this.selectedTab) { case 0: await this.loadNextPlexSongPage(account); break; case 1: await this.loadNextPlexArtistPage(account); break; case 2: await this.loadNextPlexAlbumPage(account); break; case 3: await this.loadNextPlexPlaylistPage(account); break; default: break; } return; } if (this.isDaoLiYuAccount(account)) { switch (this.selectedTab) { case 0: await this.loadNextDaoLiYuSongPage(account); break; case 1: await this.loadNextDaoLiYuArtistPage(account); break; case 2: await this.loadNextDaoLiYuAlbumPage(account); break; case 3: await this.loadNextDaoLiYuPlaylistPage(account); break; default: break; } } } private async buildAlbumCoverMap(albums: NavidromeRestAlbum[], account: WebDavAccount): Promise> { const map = new Map(); const tasks: Promise[] = []; let directCoverCount = 0; let generatedCoverCount = 0; let failedCoverCount = 0; void ServerLogUtil.info('RemoteMusicPageCover', `📀 开始处理 ${albums.length} 张专辑的封面 - 账号: ${ServerLogUtil.sanitizeAccount(account)}`); for (let i = 0; i < albums.length; i++) { const album = albums[i]; void ServerLogUtil.debug('RemoteMusicPageCover', `处理专辑 [${i + 1}/${albums.length}] - id: ${album.id}, name: ${album.name}`); tasks.push((async () => { try { // 注意: embedArtPath 和 coverArtPath 可能指向音频文件而非封面图片 // 所以这里不能直接使用,需要通过 buildCoverUrl 生成正确的封面URL // const directUrl = this.resolveEmbedCover(account, album.embedArtPath ?? album.coverArtPath); // if (directUrl) { // map.set(album.id, directUrl); // directCoverCount++; // void ServerLogUtil.info('RemoteMusicPageCover', `✅ 专辑使用直接封面 - name: ${album.name}, url: ${directUrl}`); // return; // } // 生成封面URL const coverId = album.coverArt ?? album.coverArtId ?? (album.id ? `al-${album.id}` : undefined); void ServerLogUtil.debug('RemoteMusicPageCover', `专辑封面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.info('RemoteMusicPageCover', `✅ 专辑生成封面成功 - name: ${album.name}, coverId: ${coverId}, url: ${url}`); } else { failedCoverCount++; void ServerLogUtil.error('RemoteMusicPageCover', `❌ 专辑封面失败 - name: ${album.name}, coverId: ${coverId}, 原始数据: ${JSON.stringify({coverArt: album.coverArt, coverArtId: album.coverArtId, id: album.id})}`); } } catch (error) { failedCoverCount++; void ServerLogUtil.error('RemoteMusicPageCover', `❌ 专辑封面解析异常 - name: ${album.name}, error: ${(error as Error).message}`); } })()); } await Promise.all(tasks); void ServerLogUtil.info('RemoteMusicPageCover', `📊 专辑封面处理完成 - 总数: ${albums.length}, 直接嵌入: ${directCoverCount}, 生成URL: ${generatedCoverCount}, 失败: ${failedCoverCount}`); return map; } private async buildArtistCoverMap(artists: NavidromeRestArtist[], account: WebDavAccount): Promise> { const map = new Map(); const tasks: Promise[] = []; let directCoverCount = 0; let generatedCoverCount = 0; let failedCoverCount = 0; void ServerLogUtil.info('RemoteMusicPageCover', `🎤 开始处理 ${artists.length} 位艺术家的封面 - 账号: ${ServerLogUtil.sanitizeAccount(account)}`); for (let i = 0; i < artists.length; i++) { const artist = artists[i]; void ServerLogUtil.debug('RemoteMusicPageCover', `处理艺术家 [${i + 1}/${artists.length}] - id: ${artist.id}, name: ${artist.name}`); tasks.push((async () => { try { // 尝试获取已有图片URL const directUrl = artist.mediumImageUrl ?? artist.largeImageUrl ?? this.resolveEmbedCover(account, artist.coverArtPath); if (directUrl) { map.set(artist.id, directUrl); directCoverCount++; void ServerLogUtil.info('RemoteMusicPageCover', `✅ 艺术家使用已有图片 - name: ${artist.name}, url: ${directUrl}`); return; } // 生成封面URL const coverId = artist.coverArt ?? artist.coverArtId ?? (artist.id ? `ar-${artist.id}` : undefined); void ServerLogUtil.debug('RemoteMusicPageCover', `艺术家封面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.info('RemoteMusicPageCover', `✅ 艺术家生成封面成功 - name: ${artist.name}, coverId: ${coverId}, url: ${url}`); } else { failedCoverCount++; void ServerLogUtil.error('RemoteMusicPageCover', `❌ 艺术家封面失败 - name: ${artist.name}, coverId: ${coverId}, 原始数据: ${JSON.stringify({coverArt: artist.coverArt, coverArtId: artist.coverArtId, id: artist.id})}`); } } catch (error) { failedCoverCount++; void ServerLogUtil.error('RemoteMusicPageCover', `❌ 艺术家封面解析异常 - name: ${artist.name}, error: ${(error as Error).message}`); } })()); } await Promise.all(tasks); void ServerLogUtil.info('RemoteMusicPageCover', `📊 艺术家封面处理完成 - 总数: ${artists.length}, 直接链接: ${directCoverCount}, 生成URL: ${generatedCoverCount}, 失败: ${failedCoverCount}`); return map; } private async buildRemoteCoverMap(ids: string[], account: WebDavAccount): Promise> { const map = new Map(); const tasks = ids.map(async (id) => { if (!id) { return; } try { const url = await this.buildCoverUrl(account, id, 300); if (url) { map.set(id, url); } } catch (error) { void ServerLogUtil.warn('CoverCache', `远程封面生成失败: ${id} ${(error as Error).message}`); } }); await Promise.all(tasks); return map; } private buildAudioStationAlbumKey(name?: string, artist?: string): string { const safeName = name ?? ''; const safeArtist = artist ?? ''; return `as:${safeName}|||${safeArtist}`; } private parseAudioStationAlbumKey(value: string): AudioStationAlbumKey { let raw = value ?? ''; if (raw.startsWith('as:')) { raw = raw.slice(3); } const parts = raw.split('|||'); const name = parts.length > 0 ? parts[0] : ''; const artist = parts.length > 1 ? parts.slice(1).join('|||') : ''; return new AudioStationAlbumKey(name, artist); } private async convertJellyfinArtistsToRest(artists: JellyfinArtist[], account: WebDavAccount): Promise { const results: NavidromeRestArtist[] = []; for (let i = 0; i < artists.length; i++) { const artist = artists[i]; const coverUrl = await this.buildCoverUrl(account, artist.id, 300); const restArtist: NavidromeRestArtist = { id: artist.id, name: artist.name, albumCount: artist.albumCount, songCount: artist.songCount, coverUrl: coverUrl }; results.push(restArtist); } return results; } private async convertEmbyArtistsToRest(artists: EmbyArtist[], account: WebDavAccount): Promise { const results: NavidromeRestArtist[] = []; for (let i = 0; i < artists.length; i++) { const artist = artists[i]; const coverUrl = await this.buildCoverUrl(account, artist.id, 300); const restArtist: NavidromeRestArtist = { id: artist.id, name: artist.name, albumCount: artist.albumCount, songCount: artist.songCount, coverUrl: coverUrl }; results.push(restArtist); } return results; } private async convertJellyfinAlbumsToRest(albums: JellyfinAlbum[], account: WebDavAccount): Promise { const results: NavidromeRestAlbum[] = []; for (let i = 0; i < albums.length; i++) { const album = albums[i]; const coverUrl = await this.buildCoverUrl(account, album.id, 300); const restAlbum: NavidromeRestAlbum = { id: album.id, name: album.name, artist: album.artist, minYear: album.year, coverUrl: coverUrl }; results.push(restAlbum); } return results; } private async convertEmbyAlbumsToRest(albums: EmbyAlbum[], account: WebDavAccount): Promise { const results: NavidromeRestAlbum[] = []; for (let i = 0; i < albums.length; i++) { const album = albums[i]; const coverUrl = await this.buildCoverUrl(account, album.id, 300); const restAlbum: NavidromeRestAlbum = { id: album.id, name: album.name, artist: album.artist, minYear: album.year, coverUrl: coverUrl }; results.push(restAlbum); } return results; } private convertAudioStationArtistsToRest(artists: AudioStationArtist[]): NavidromeRestArtist[] { return artists.map(artist => { const name = artist.name ?? ''; return { id: name, name: name } as NavidromeRestArtist; }); } private async convertAudioStationAlbumsToRest(albums: AudioStationAlbum[], account: WebDavAccount): Promise { const results: NavidromeRestAlbum[] = []; for (let i = 0; i < albums.length; i++) { const album = albums[i]; const artistName = album.albumArtist ?? album.displayArtist ?? ''; const albumKey = this.buildAudioStationAlbumKey(album.name, artistName); const coverUrl = await this.buildCoverUrl(account, albumKey, 300); const restAlbum: NavidromeRestAlbum = { id: albumKey, name: album.name, artist: artistName, minYear: album.year, coverArtId: albumKey, coverUrl: coverUrl }; results.push(restAlbum); } return results; } private async convertPlexArtistsToRest(artists: PlexArtist[], account: WebDavAccount): Promise { const results: NavidromeRestArtist[] = []; for (let i = 0; i < artists.length; i++) { const artist = artists[i]; const coverId = artist.thumb ?? (artist.id ? `ar-${artist.id}` : ''); const coverUrl = coverId ? await this.buildCoverUrl(account, coverId, 300) : undefined; const restArtist: NavidromeRestArtist = { id: artist.id, name: artist.name, coverArtId: coverId, coverUrl: coverUrl }; results.push(restArtist); } return results; } private async convertPlexAlbumsToRest(albums: PlexAlbum[], account: WebDavAccount): Promise { const results: NavidromeRestAlbum[] = []; for (let i = 0; i < albums.length; i++) { const album = albums[i]; const coverId = album.thumb ?? (album.id ? `al-${album.id}` : ''); const coverUrl = coverId ? await this.buildCoverUrl(account, coverId, 300) : undefined; const restAlbum: NavidromeRestAlbum = { id: album.id, name: album.name, artist: album.artist, minYear: album.year, coverArtId: coverId, coverUrl: coverUrl }; results.push(restAlbum); } return results; } private async convertDaoLiYuArtistsToRest(artists: DaoLiYuArtist[], account: WebDavAccount): Promise { const results: NavidromeRestArtist[] = []; for (let i = 0; i < artists.length; i++) { const artist = artists[i]; const coverId = artist.coverArtUrl ?? ''; const coverUrl = coverId ? await this.buildCoverUrl(account, coverId, 300) : undefined; const restArtist: NavidromeRestArtist = { id: artist.id, name: artist.name, albumCount: artist.albumCount, songCount: artist.trackCount, coverArtId: coverId, coverUrl: coverUrl }; results.push(restArtist); } return results; } private async convertDaoLiYuAlbumsToRest(albums: DaoLiYuAlbum[], account: WebDavAccount): Promise { const results: NavidromeRestAlbum[] = []; for (let i = 0; i < albums.length; i++) { const album = albums[i]; const coverId = album.coverArtUrl ?? ''; const coverUrl = coverId ? await this.buildCoverUrl(account, coverId, 300) : undefined; const restAlbum: NavidromeRestAlbum = { id: album.id, name: album.title, artist: album.artist, minYear: album.year, coverArtId: coverId, coverUrl: coverUrl }; results.push(restAlbum); } return results; } private async convertDaoLiYuPlaylistsToRest(playlists: DaoLiYuPlaylist[], account: WebDavAccount): Promise { const results: NavidromeRestPlaylist[] = []; for (let i = 0; i < playlists.length; i++) { const playlist = playlists[i]; const coverId = playlist.coverArtUrl ?? ''; const coverUrl = coverId ? await this.buildCoverUrl(account, coverId, 300) : undefined; const restPlaylist: NavidromeRestPlaylist = { id: playlist.id, name: playlist.name, comment: playlist.description ?? undefined, songCount: playlist.songCount, coverArtId: coverId, coverUrl: coverUrl }; results.push(restPlaylist); } return results; } private convertJellyfinSongsToRestSongs(songs: JellyfinSong[]): NavidromeRestSong[] { return songs.map(song => { const restSong: NavidromeRestSong = { id: song.id, title: song.title, album: song.album, albumId: song.albumId, artist: song.artist, artistId: song.artistId, duration: song.durationSeconds, bitRate: song.bitRate, suffix: song.suffix, size: song.size, track: song.track, year: song.year, coverArt: song.albumId ?? song.id }; return restSong; }); } private convertEmbySongsToRestSongs(songs: EmbySong[]): NavidromeRestSong[] { return songs.map(song => { const restSong: NavidromeRestSong = { id: song.id, title: song.title, album: song.album, albumId: song.albumId, artist: song.artist, artistId: song.artistId, duration: song.durationSeconds, bitRate: song.bitRate, suffix: song.suffix, size: song.size, track: song.track, year: song.year, coverArt: song.albumId ?? song.id }; return restSong; }); } private convertAudioStationSongsToRestSongs(songs: AudioStationSong[]): NavidromeRestSong[] { return songs.map(song => { const artistName = song.artist ?? song.albumArtist ?? ''; const albumKey = this.buildAudioStationAlbumKey(song.album, song.albumArtist ?? song.artist); const songCoverId = song.id ? `as-song:${song.id}` : albumKey; const restSong: NavidromeRestSong = { id: song.id, title: song.title, album: song.album, albumId: albumKey, artist: artistName, artistId: artistName, duration: song.durationSeconds, bitRate: song.bitRate, suffix: song.container, size: song.size, track: song.track, year: song.year, contentType: song.container ? `audio/${song.container}` : undefined, coverArtId: songCoverId }; return restSong; }); } private convertPlexSongsToRestSongs(songs: PlexSong[]): NavidromeRestSong[] { return songs.map(song => { const coverId = song.albumThumb ?? song.thumb ?? song.albumId ?? song.id; const restSong: NavidromeRestSong = { id: song.id, title: song.title, album: song.album, albumId: song.albumId, artist: song.artist, artistId: song.artistId, duration: song.durationSeconds, bitRate: song.bitRate, suffix: song.suffix, size: song.size, track: song.track, year: song.year, contentType: song.mimeType, coverArtId: coverId }; return restSong; }); } private convertDaoLiYuSongsToRestSongs(songs: DaoLiYuTrack[]): NavidromeRestSong[] { return songs.map(song => { const coverId = song.coverArtUrl ?? song.albumId ?? song.id; const restSong: NavidromeRestSong = { id: song.id, title: song.title, album: song.album, albumId: song.albumId, artist: song.artist, artistId: song.artistId, duration: song.durationSeconds, bitRate: song.bitRate, suffix: song.suffix, size: song.size, track: song.track, year: song.year, contentType: song.mimeType, coverArtId: coverId, lyrics: song.lyrics, createdAt: song.createdAt }; return restSong; }); } private convertAudioStationPlaylistsToRest(playlists: AudioStationPlaylist[]): NavidromeRestPlaylist[] { return playlists.map(playlist => { const restPlaylist: NavidromeRestPlaylist = { id: playlist.id, name: playlist.name, path: playlist.path }; return restPlaylist; }); } private convertPlexPlaylistsToRest(playlists: PlexPlaylist[]): NavidromeRestPlaylist[] { return playlists.map(playlist => { const restPlaylist: NavidromeRestPlaylist = { id: playlist.id, name: playlist.title, comment: playlist.summary, duration: playlist.duration, songCount: playlist.leafCount }; return restPlaylist; }); } private async fetchAllJellyfinArtistSongs(account: WebDavAccount, artistId: string): Promise { const results: JellyfinSong[] = []; let startIndex = 0; while (true) { const response = await jellyfinApi.getArtistSongs(account, artistId, startIndex, this.REMOTE_PAGE_SIZE); results.push(...response.items); if (response.nextStart === null) { break; } startIndex = response.nextStart; } return results; } private async fetchAllEmbyArtistSongs(account: WebDavAccount, artistId: string): Promise { const results: EmbySong[] = []; let startIndex = 0; while (true) { const response = await embyApi.getArtistSongs(account, artistId, startIndex, this.REMOTE_PAGE_SIZE); results.push(...response.items); if (response.nextStart === null) { break; } startIndex = response.nextStart; } return results; } private async fetchAllAudioStationArtistSongs(account: WebDavAccount, artistName: string): Promise { if (!artistName || artistName.trim().length === 0) { return []; } const keyword = artistName.trim(); const songs = await audioStationApi.searchSongs(account, keyword, 0, this.REMOTE_SEARCH_LIMIT); return songs.filter(song => { const artist = song.artist ?? song.albumArtist ?? ''; return artist.trim() === keyword; }); } private async fetchAllAudioStationPlaylistSongs(account: WebDavAccount, playlistId: string): Promise { const results: AudioStationSong[] = []; let startIndex = 0; while (true) { const response = await audioStationApi.getPlaylistSongsPage(account, playlistId, startIndex, this.REMOTE_PAGE_SIZE); results.push(...response.items); if (response.nextStart === null) { break; } startIndex = response.nextStart; } return results; } private async fetchAllPlexArtistSongs(account: WebDavAccount, artistId: string): Promise { const results: PlexSong[] = []; const albums = await plexApi.getArtistAlbums(account, artistId); for (let i = 0; i < albums.length; i++) { const album = albums[i]; if (!album.id) { continue; } const songs = await plexApi.getAlbumSongs(account, album.id); results.push(...songs); } return results; } private async fetchAllPlexPlaylistSongs(account: WebDavAccount, playlistId: string): Promise { const results: PlexSong[] = []; let startIndex = 0; while (true) { const response = await plexApi.getPlaylistSongsPage(account, playlistId, startIndex, this.REMOTE_PAGE_SIZE); results.push(...response.items); if (response.nextStart === null) { break; } startIndex = response.nextStart; } return results; } private buildSongDedupKey(title?: string, artist?: string, album?: string, durationSeconds?: number): string { const safeTitle = title ?? ''; const safeArtist = artist ?? ''; const safeAlbum = album ?? ''; const safeDuration = durationSeconds !== undefined && durationSeconds !== null ? durationSeconds.toString() : ''; return `${safeTitle}__${safeArtist}__${safeAlbum}__${safeDuration}`.toLowerCase(); } private buildSongKeyFromItem(item: VideoItem): string { const safeTitle = item.name ?? ''; const safeArtist = item.artist ?? ''; const safeAlbum = item.album ?? ''; const safeDuration = item.duration ?? ''; return `${safeTitle}__${safeArtist}__${safeAlbum}__${safeDuration}`.toLowerCase(); } private dedupRemoteSongsByKey(items: VideoItem[]): VideoItem[] { const map = new Map(); for (let i = 0; i < items.length; i++) { const item = items[i]; const key = this.buildSongKeyFromItem(item); const existing = map.get(key); if (!existing) { map.set(key, item); continue; } const existingHasAlbumId = StrUtil.isNotEmpty(existing.navAlbumId); const currentHasAlbumId = StrUtil.isNotEmpty(item.navAlbumId); if (!existingHasAlbumId && currentHasAlbumId) { map.set(key, item); } } return Array.from(map.values()); } private convertApiSongsToRestSongs(songs: NavidromeSong[]): NavidromeRestSong[] { return songs.map((song: NavidromeSong): NavidromeRestSong => ({ id: song.id, title: song.title, artist: song.artist, artistId: song.artistId, album: song.album, albumId: song.albumId, duration: song.duration, bitRate: song.bitRate, suffix: song.suffix, size: song.size, createdAt: song.created, genre: song.genre, track: song.track, year: song.year, contentType: song.contentType, coverArt: song.coverArt, coverArtId: song.coverArt, lyrics: song.lyrics, })); } private async convertSongsToVideoItems(songs: NavidromeRestSong[], account: WebDavAccount): Promise { const tasks: Promise[] = []; let successCount = 0; let failedCount = 0; void ServerLogUtil.info('SongConvert', `开始转换 ${songs.length} 首歌曲为 VideoItem`); for (let i = 0; i < songs.length; i++) { const song = songs[i]; tasks.push((async () => { try { let coverUrl: string | undefined; try { coverUrl = await this.resolveSongCover(song, account); } catch (error) { void ServerLogUtil.warn('SongConvert', `歌曲 ${song.title} 封面解析失败: ${(error as Error).message}`); } const videoItem = this.convertSongToVideoItem(song, account, coverUrl); successCount++; // 记录前几首歌的详细信息用于调试 if (i < 3) { void ServerLogUtil.debug('SongConvert', `歌曲 ${i + 1}: ${videoItem.name}, 封面: ${coverUrl ? '有' : '无'}, 路径: ${videoItem.filePath}`); } return videoItem; } catch (error) { failedCount++; void ServerLogUtil.error('SongConvert', `歌曲 ${song.title} 转换失败: ${(error as Error).message}`); throw new Error(`歌曲 ${song.title} 转换失败: ${(error as Error).message}`); } })()); } const results = await Promise.all(tasks); void ServerLogUtil.info('SongConvert', `歌曲转换完成: 成功 ${successCount}, 失败 ${failedCount}`); return results; } private async resolveSongCover(song: NavidromeRestSong, account: WebDavAccount): Promise { void ServerLogUtil.debug('RemoteMusicPageCover', `🎵 解析歌曲封面 - 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('RemoteMusicPageCover', `✅ 歌曲使用专辑封面缓存 - title: ${song.title}, albumCover: ${albumCover}`); // return albumCover; // } // } if (!this.isNavidromeAccount(account)) { const fallbackId = song.coverArtId ?? song.albumId ?? song.id; void ServerLogUtil.debug('RemoteMusicPageCover', `歌曲使用非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('RemoteMusicPageCover', `✅ 歌曲使用直接封面 - title: ${song.title}, url: ${directUrl}`); return directUrl; } const coverId = song.coverArt ?? song.coverArtId ?? song.id; void ServerLogUtil.debug('RemoteMusicPageCover', `歌曲生成封面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 { void ServerLogUtil.info('RemoteMusicPageCover', `开始构建封面URL - coverId: ${coverId}, size: ${size}, 账号: ${ServerLogUtil.sanitizeAccount(account)}`); if (!coverId || coverId.trim().length === 0) { void ServerLogUtil.warn('RemoteMusicPageCover', `封面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.info('RemoteMusicPageCover', `✅ 缓存命中 - coverId: ${coverId}, size: ${size}, url: ${cached}`); return cached; } } void ServerLogUtil.info('RemoteMusicPageCover', `⚡ 调用API生成封面URL - coverId: ${coverId}, size: ${size}`); let url: string | undefined = undefined; try { if (this.isNavidromeAccount(account)) { void ServerLogUtil.debug('RemoteMusicPageCover', `[开始] 调用 navidromeApi.buildCoverArtUrl - coverId: ${normalizedId}, size: ${size}`); void ServerLogUtil.debug('RemoteMusicPageCover', `[账号信息] host=${account.host}, port=${account.port}, enableHttps=${account.enableHttps}`); void ServerLogUtil.debug('RemoteMusicPageCover', `[账号信息] basePath=${account.navidromeBasePath}`); url = await navidromeApi.buildCoverArtUrl(account, normalizedId, size); void ServerLogUtil.debug('RemoteMusicPageCover', `[完成] navidromeApi.buildCoverArtUrl 返回 - coverId: ${normalizedId}, 返回值: "${url}"`); if (url) { void ServerLogUtil.info('RemoteMusicPageCover', `✅ API返回URL成功 - coverId: ${coverId}, url: ${url}`); } else { void ServerLogUtil.warn('RemoteMusicPageCover', `⚠️ API返回空值 - coverId: ${coverId}`); } } else if (this.isJellyfinAccount(account)) { void ServerLogUtil.debug('RemoteMusicPageCover', `调用 jellyfinApi.buildPrimaryImageUrl - coverId: ${normalizedId}, size: ${size}`); url = await jellyfinApi.buildPrimaryImageUrl(account, normalizedId, size, size); } else if (this.isEmbyAccount(account)) { void ServerLogUtil.debug('RemoteMusicPageCover', `调用 embyApi.buildPrimaryImageUrl - coverId: ${normalizedId}, size: ${size}`); url = await embyApi.buildPrimaryImageUrl(account, normalizedId, size, size); } else if (this.isAudioStationAccount(account)) { if (this.isAudioStationSongCoverId(normalizedId)) { const songId = this.stripAudioStationSongCoverId(normalizedId); void ServerLogUtil.debug('RemoteMusicPageCover', `调用 audioStationApi.buildSongCoverUrl - songId=${songId}`); url = await audioStationApi.buildSongCoverUrl(account, songId); } else { const key = this.parseAudioStationAlbumKey(normalizedId); void ServerLogUtil.debug('RemoteMusicPageCover', `调用 audioStationApi.buildAlbumCoverUrl - album=${key.name}, artist=${key.artist}`); url = await audioStationApi.buildAlbumCoverUrl(account, key.name, key.artist); } } else if (this.isPlexAccount(account)) { void ServerLogUtil.debug('RemoteMusicPageCover', `调用 plexApi.buildImageUrl - coverId: ${normalizedId}`); url = plexApi.buildImageUrl(account, normalizedId); } else if (this.isDaoLiYuAccount(account)) { void ServerLogUtil.debug('RemoteMusicPageCover', `调用 daoLiYuApi.buildImageUrl - coverId: ${normalizedId}`); url = daoLiYuApi.buildImageUrl(account, normalizedId); } if (url && this.isNavidromeAccount(account)) { this.coverUrlCache.set(cacheKey, url); void ServerLogUtil.info('RemoteMusicPageCover', `✅ 封面URL生成成功并已缓存 - coverId: ${coverId}, url: ${url}`); void ServerLogUtil.debug('RemoteMusicPageCover', `缓存键值 - cacheKey: ${cacheKey}`); } else if (!url) { void ServerLogUtil.error('RemoteMusicPageCover', `❌ 封面URL生成失败(返回空) - coverId: ${coverId}, size: ${size}`); } else { void ServerLogUtil.info('RemoteMusicPageCover', `✅ 封面URL生成成功(非Navidrome账号) - coverId: ${coverId}, url: ${url}`); } } catch (error) { const err = error as Error; void ServerLogUtil.error('RemoteMusicPageCover', `❌ 封面URL生成异常 - coverId: ${coverId}, error: ${err.message}`); void ServerLogUtil.error('RemoteMusicPageCover', `错误类型: ${err.name || 'Unknown'}`); void ServerLogUtil.error('RemoteMusicPageCover', `错误堆栈: ${err.stack || '无'}`); } return url; } private isAudioStationSongCoverId(value: string): boolean { return value.startsWith('as-song:'); } private stripAudioStationSongCoverId(value: string): string { return value.replace(/^as-song:/, ''); } /** * 记录账号缓存信息 */ private async logAccountCacheInfo(account: WebDavAccount): Promise { try { void ServerLogUtil.info('AccountInfo', `账号信息: ${ServerLogUtil.sanitizeAccount(account)}`); // 记录缓存路径信息 const cachePreview = this.getCachePreviewInfo(account); if (cachePreview) { void ServerLogUtil.info('AccountInfo', `Navidrome缓存目录结构:`); void ServerLogUtil.info('AccountInfo', `- 缓存根目录: ${cachePreview.cacheRoot}`); void ServerLogUtil.info('AccountInfo', `- 示例缓存路径: ${cachePreview.examplePath}`); } else { void ServerLogUtil.warn('AccountInfo', '无法预览缓存目录:上下文或根目录不可用'); } // 记录封面缓存状态 void ServerLogUtil.info('AccountInfo', `当前封面URL缓存数量: ${this.coverUrlCache.size}`); if (this.coverUrlCache.size > 0) { const cacheKeys = Array.from(this.coverUrlCache.keys()).slice(0, 10); void ServerLogUtil.debug('AccountInfo', `封面缓存示例: ${cacheKeys.join(', ')}`); } } catch (error) { void ServerLogUtil.error('AccountInfo', `记录账号信息失败: ${(error as Error).message}`); } } /** * 记录缓存统计信息 */ private async logCacheStatistics(account: WebDavAccount): Promise { try { void ServerLogUtil.info('CacheStats', '=== Navidrome 缓存统计 ==='); void ServerLogUtil.info('CacheStats', `封面URL缓存: ${this.coverUrlCache.size} 个条目`); void ServerLogUtil.info('CacheStats', `已加载歌曲: ${this.allVideos.length} 首`); void ServerLogUtil.info('CacheStats', `已处理艺术家: ${this.artists.length} 位`); void ServerLogUtil.info('CacheStats', `已处理专辑: ${this.albums.length} 张`); // 计算封面统计 const songsWithCover = this.allVideos.filter(song => song.pixelMapPath && song.pixelMapPath.length > 0).length; const albumsWithCover = this.albums.filter(album => album.coverUrl && album.coverUrl.length > 0).length; const artistsWithCover = this.artists.filter(artist => artist.coverUrl && artist.coverUrl.length > 0).length; void ServerLogUtil.info('CacheStats', `封面统计:`); void ServerLogUtil.info('CacheStats', `- 歌曲有封面: ${songsWithCover}/${this.allVideos.length} (${Math.round(songsWithCover / this.allVideos.length * 100)}%)`); void ServerLogUtil.info('CacheStats', `- 专辑有封面: ${albumsWithCover}/${this.albums.length} (${Math.round(albumsWithCover / this.albums.length * 100)}%)`); void ServerLogUtil.info('CacheStats', `- 艺术家有封面: ${artistsWithCover}/${this.artists.length} (${Math.round(artistsWithCover / this.artists.length * 100)}%)`); } catch (error) { void ServerLogUtil.error('CacheStats', `记录缓存统计失败: ${(error as Error).message}`); } } private resolveEmbedCover(account: WebDavAccount, path?: string): string | undefined { if (!this.isNavidromeAccount(account)) { return undefined; } return navidromeRestApi.resolveResourceUrl(account, path); } private resolveLibraryInfo(account: WebDavAccount): LibraryInfo { if (this.isJellyfinAccount(account)) { return { type: CommonConstants.TYPE_JELLYFIN, scheme: 'jellyfin' } as LibraryInfo; } if (this.isEmbyAccount(account)) { return { type: CommonConstants.TYPE_EMBY, scheme: 'emby' } as LibraryInfo; } if (this.isAudioStationAccount(account)) { return { type: CommonConstants.TYPE_AUDIOSTATION, scheme: 'audiostation' } as LibraryInfo; } if (this.isPlexAccount(account)) { return { type: CommonConstants.TYPE_PLEX, scheme: 'plex' } as LibraryInfo; } if (this.isDaoLiYuAccount(account)) { return { type: CommonConstants.TYPE_DAOLIYU, scheme: 'daoliyu' } as LibraryInfo; } return { type: CommonConstants.TYPE_NAVIDROME, scheme: 'navidrome' } as LibraryInfo; } private convertSongToVideoItem(song: NavidromeRestSong, account: WebDavAccount, coverUrl?: string): VideoItem { const title = song.title ?? Constants.UNKNOWN_TITLE; const libraryInfo = this.resolveLibraryInfo(account); // 调试日志:记录从playlist或其他API获取的歌曲的id字段 void ServerLogUtil.debug('SongConvert', `转换歌曲: ${title}`); void ServerLogUtil.debug('SongConvert', `- song.id: ${song.id}`); void ServerLogUtil.debug('SongConvert', `- song.artistId: ${song.artistId}`); void ServerLogUtil.debug('SongConvert', `- song.albumId: ${song.albumId}`); void ServerLogUtil.debug('SongConvert', `- song.lyrics: ${song.lyrics}`); const videoItem = new VideoItem( title, song.id, `${libraryInfo.scheme}://${account.id ?? 0}/${song.id}`, libraryInfo.type, song.size ?? 0, song.createdAt ?? '', Utility.formatFSize(song.size ?? 0), undefined, song.artist ?? Constants.UNKNOWN_ARTIST, song.album ?? '', `${title}${song.suffix ? '.' + song.suffix : ''}` ); const durationStr = this.formatSongDuration(song.duration); if (durationStr) { videoItem.duration = durationStr; } 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.pixelMapPath = coverUrl; videoItem.lyricContent = song.lyrics; // 调试日志:检查歌曲的 albumId 和 artistId // if (this.allVideos.length < 3) { // Logger.info('heanup', `歌曲 ${title}: artistId=${song.artistId}, albumId=${song.albumId}, artist=${song.artist}, album=${song.album}`); // } 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; } // 记录最终生成的VideoItem路径 void ServerLogUtil.debug('SongConvert', `- VideoItem.filePath: ${videoItem.filePath}`); void ServerLogUtil.debug('SongConvert', `- VideoItem.remote_rel_path: ${videoItem.remote_rel_path}`); return videoItem; } private formatSongDuration(durationSeconds?: number): string | undefined { if (durationSeconds === undefined || durationSeconds === null || durationSeconds < 0) { return undefined; } const totalSeconds = Math.floor(durationSeconds); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; const pad = (value: number) => value.toString().padStart(2, '0'); return `${pad(minutes)}:${pad(seconds)}`; } @Builder topTitleBar() { Column() { Row({ space: 6 }) { // 标题或搜索框 if (!this.isSearchMode) { // 详情视图模式下显示返回按钮 if (this.isDetailView) { Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.chevron_left')) .attributeModifier(new SymbolGlyphFancyModifier(24, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .animation({ duration: 300, curve: Curve.Ease }) .onClick(() => { // 返回到之前的标签页 this.isDetailView = false; this.clearFilter(); }) .attributeModifier(new ShadowModifier()) .zIndex(0) Text(this.filterLabel) .margin({ left: 3, right: 10 }) .fontColor($r('app.color.text_color')) .fontSize(18) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE }) .layoutWeight(1) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .animation({ duration: 300, curve: Curve.Ease }) } else { // 正常模式:侧边栏按钮和账号名 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.sort')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .animation({ duration: 300, curve: Curve.Ease }) .onClick(() => { this.getUIContext().animateTo({ duration: 555 }, () => { // 动画闭包内控制Image组件的出现和消失 this.isShowDrawer = !this.isShowDrawer this.offsetX = 0 }) }) .attributeModifier(new ShadowModifier()) .zIndex(0) Text(this.selectedAccount.name) .margin({ left: 3, right: 10 }) .fontColor($r('app.color.text_color')) .fontSize(18) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE }) .layoutWeight(1) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .onClick(() => { // 可以添加标题点击事件 }) .animation({ duration: 300, curve: Curve.Ease }) } } else { Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.chevron_left')) .attributeModifier(new SymbolGlyphFancyModifier(24, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .animation({ duration: 300, curve: Curve.Ease }) .onClick(() => { this.isSearchMode = false; void this.onSearchInput(''); }) .attributeModifier(new ShadowModifier()) .zIndex(0) } // 搜索框 Search({ value: this.searchText, placeholder: '搜索标题、艺术家...' }) .searchButton('搜索', { fontColor: this.themeColor }) .searchIcon({ src: $r('sys.media.ohos_ic_public_search_filled') }) .cancelButton({ style: CancelButtonStyle.CONSTANT, icon: { src: $r('sys.media.ohos_ic_public_cancel_filled') } }) .layoutWeight(1) .height(35) .maxLength(20) .backgroundColor(this.isDarkMode?Color.Black:'#F5F5F5') .placeholderColor(Color.Grey) .placeholderFont({ size: 14, weight: 400 }) .textFont({ size: 14, weight: 400 }) .onSubmit((value: string) => { void this.onSearchInput(value); }) .onChange((value: string) => { void this.onSearchInput(value); }) .visibility(this.isSearchMode?Visibility.Visible:Visibility.None) .animation({ duration: 300, curve: Curve.Ease }) // 搜索/排序按钮 if (!this.isSearchMode) { // 搜索按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.magnifyingglass')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .animation({ duration: 300, curve: Curve.Ease }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .attributeModifier(new ShadowModifier()) .zIndex(0) .visibility(this.selectedTab==0?Visibility.Visible:Visibility.None) .onClick(() => { this.isSearchMode = true; if (!this.isSearchLoading && this.searchText.length === 0) { this.filteredList = []; } }) // 排序按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.list_number')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .animation({ duration: 300, curve: Curve.Ease }) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .bindMenu(this.SortMenuBuilder) .attributeModifier(new ShadowModifier()) .zIndex(0) } } .width('100%') .height(55) .padding({ left: 15, right: 15 }) .justifyContent(FlexAlign.SpaceBetween) .alignItems(VerticalAlign.Center) // 分段按钮 SegmentButton({ options: this.tabOptions, selectedIndexes: $tabSelectedIndexes }) .width('100%') .padding({ left: 25, right: 25, top: 5, bottom: 5 }) } .width('100%') .padding({ top: this.topSafeHeight + 5 }) // .backgroundColor($r('app.color.start_window_background')) } @Builder SortMenuBuilder() { Menu() { MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')), content: $r('app.string.sort_by_name') }) .onClick(async () => { this.doSortType(0); PreferencesUtil.put("navidromeSortType", 0); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')), content: '按名称降序' }) .onClick(async () => { this.doSortType(1); PreferencesUtil.put("navidromeSortType", 1); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')), content: '按艺术家升序' }) .onClick(async () => { this.doSortType(2); PreferencesUtil.put("navidromeSortType", 2); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.timer')), content: '按艺术家降序' }) .onClick(async () => { this.doSortType(3); PreferencesUtil.put("navidromeSortType", 3); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')), content: '按专辑升序' }) .onClick(async () => { this.doSortType(4); PreferencesUtil.put("navidromeSortType", 4); }) MenuItem({ symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')), content: '按专辑降序' }) .onClick(async () => { this.doSortType(5); PreferencesUtil.put("navidromeSortType", 5); }) } } doSortType(index: number) { this.sortType = index; const songs = this.isSearchMode && this.searchText.length > 0 ? this.filteredList : this.allVideos; const sortTypeNames = ['名称升序', '名称降序', '艺术家升序', '艺术家降序', '专辑升序', '专辑降序']; const sortTypeName = sortTypeNames[index] || '未知排序'; // 记录排序操作 void ServerLogUtil.info('NavidromeSort', `应用排序: ${sortTypeName} (${index})`); void ServerLogUtil.info('NavidromeSort', `排序范围: ${songs.length} 首歌曲 (${this.isSearchMode ? '搜索结果' : '全部歌曲'})`); const startTime = Date.now(); // 对歌曲列表进行排序 switch (index) { case 0: // 名称升序 Utility.doSortListAscending(songs,false) break; case 1: // 名称降序 Utility.doSortListDescending(songs,true) break; case 2: // 艺术家升序 songs.sort((a, b) => { // 处理艺术家可能为undefined的字符串比较 const artistA = a.artist?.trim() || ''; // 可选添加 trim() 处理空格 const artistB = b.artist?.trim() || ''; return artistA.localeCompare(artistB); }); break; case 3: // 艺术家降序 songs.sort((a, b) => { // 处理艺术家可能为undefined的字符串比较 const artistA = a.artist?.trim() || ''; // 可选添加 trim() 处理空格 const artistB = b.artist?.trim() || ''; return artistB.localeCompare(artistA); }); break; case 4: // 专辑升序 songs.sort((a, b) => { const albumA = a.album?.trim() || ''; // 可选添加 trim() 处理空格 const albumB = b.album?.trim() || ''; return albumA.localeCompare(albumB); }); break; case 5: // 专辑降序 songs.sort((a, b) => { const albumA = a.album?.trim() || ''; // 可选添加 trim() 处理空格 const albumB = b.album?.trim() || ''; return albumB.localeCompare(albumA); }); break; } const sortTime = Date.now() - startTime; // 更新显示列表 if (this.isSearchMode) { this.filteredList = [...songs]; } // 记录排序结果 void ServerLogUtil.info('NavidromeSort', `排序完成: ${sortTypeName}`); void ServerLogUtil.info('NavidromeSort', `- 排序耗时: ${sortTime}ms`); void ServerLogUtil.debug('NavidromeSort', `排序结果示例: ${songs.slice(0, 3).map(s => `${s.name} (${s.artist})`).join(', ')}`); } // 实时搜索逻辑(改为远程API搜索) private async onSearchInput(value: string): Promise { const keyword = value.trim(); this.searchText = keyword; const ticket = ++this.searchTicket; void ServerLogUtil.debug('NavidromeSearch', `搜索输入: "${value}" -> "${keyword}"`); if (keyword.length === 0) { this.filteredList = []; this.isSearchLoading = false; void ServerLogUtil.info('NavidromeSearch', '搜索已清空,显示所有歌曲'); return; } if (!this.isSearchMode) { this.isSearchMode = true; } const account = this.resolveActiveAccount(); if (!account) { this.filteredList = []; this.isSearchLoading = false; ToastUtil.showToast('媒体库账号不可用'); return; } this.isSearchLoading = true; this.filteredList = []; const startTime = Date.now(); void ServerLogUtil.info('NavidromeSearch', `开始远程搜索: "${keyword}"`); try { let restSongs: NavidromeRestSong[] = []; if (this.isNavidromeAccount(account)) { const songs = await navidromeApi.searchSongs(account, keyword, NAVIDROME_SEARCH_LIMIT); restSongs = this.convertApiSongsToRestSongs(songs); } else if (this.isJellyfinAccount(account)) { const response = await jellyfinApi.searchSongs(account, keyword, 0, this.REMOTE_SEARCH_LIMIT); restSongs = this.convertJellyfinSongsToRestSongs(response.items); } else if (this.isEmbyAccount(account)) { const response = await embyApi.searchSongs(account, keyword, 0, this.REMOTE_SEARCH_LIMIT); restSongs = this.convertEmbySongsToRestSongs(response.items); } else if (this.isAudioStationAccount(account)) { const songs = await audioStationApi.searchSongs(account, keyword, 0, this.REMOTE_SEARCH_LIMIT); restSongs = this.convertAudioStationSongsToRestSongs(songs); } else if (this.isPlexAccount(account)) { const response = await plexApi.searchSongs(account, keyword, 0, this.REMOTE_SEARCH_LIMIT); restSongs = this.convertPlexSongsToRestSongs(response.items); } else if (this.isDaoLiYuAccount(account)) { const tracks = await daoLiYuApi.searchTracks(account, keyword, this.REMOTE_SEARCH_LIMIT); restSongs = this.convertDaoLiYuSongsToRestSongs(tracks); } if (ticket !== this.searchTicket) { return; } const searchTime = Date.now() - startTime; if (!restSongs || restSongs.length === 0) { this.filteredList = []; void ServerLogUtil.warn('NavidromeSearch', `搜索无结果: "${keyword}"`); void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${searchTime}ms`); return; } const videoItems = await this.convertSongsToVideoItems(restSongs, account); if (ticket !== this.searchTicket) { return; } this.filteredList = videoItems; this.doSortType(this.sortType); void ServerLogUtil.info('NavidromeSearch', `搜索完成: "${keyword}"`); void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${searchTime}ms`); void ServerLogUtil.info('NavidromeSearch', `- 匹配结果: ${videoItems.length}`); void ServerLogUtil.debug('NavidromeSearch', `搜索结果示例: ${videoItems.slice(0, 3).map(s => `${s.name} (${s.artist})`).join(', ')}`); } catch (error) { if (ticket !== this.searchTicket) { return; } this.filteredList = []; const message = (error as Error).message ?? 'Navidrome 搜索失败'; ToastUtil.showToast(message); void ServerLogUtil.error('NavidromeSearch', `搜索失败: ${message}`); void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${Date.now() - startTime}ms`); } finally { if (ticket === this.searchTicket) { this.isSearchLoading = false; } } } private getCurrentCount(): number { switch (this.selectedTab) { case 1: return this.artists.length; case 2: return this.albums.length; case 3: return this.playlists.length; default: return this.getVisibleSongs().length; } } private getEmptyTitle(): string { switch (this.selectedTab) { case 1: return '暂无艺术家'; case 2: return '暂无专辑'; case 3: return '暂无歌单'; default: if (this.isSearchMode && this.searchText.length > 0) { return this.isSearchLoading ? '正在搜索远程歌曲' : '没有匹配的远程歌曲'; } if (this.filterType !== NavFilterType.None && this.isFilterLoading) { return '正在加载筛选结果'; } return this.filterType === NavFilterType.None ? '暂无音乐' : '该筛选下暂无歌曲'; } } private getEmptySubtitle(): string { switch (this.selectedTab) { case 1: return '当前筛选没有找到艺术家'; case 2: return '当前筛选没有找到专辑'; case 3: return '当前筛选没有找到歌单'; default: if (this.isSearchMode && this.searchText.length > 0) { return this.isSearchLoading ? '请稍候,正在通过 API 搜索' : '换个关键词再试试吧'; } if (this.filterType !== NavFilterType.None && this.isFilterLoading) { return '请稍候...'; } return this.filterType === NavFilterType.None ? '当前分类下没有找到音乐文件' : '请尝试调整筛选条件'; } } @Builder buildSongItem(song: VideoItem, index: number) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row({ space: 12 }) { // 歌曲封面 Image(song.pixelMapPath ? song.pixelMapPath : $r('app.media.nocover')) .width(52) .height(52) .borderRadius(9) .sourceSize({ width: 38, height: 38 }) .draggable(false) .interpolation(ImageInterpolation.High)// 用于重采样后的抗锯齿 .autoResize(true) // 重采样,可减少内存占用 .alt($r('app.media.nocover')) .fillColor(song.pixelMapPath ? undefined : this.themeColor) .objectFit(ImageFit.Cover) .margin({ left: 20 }) .animation({ duration: 500, curve: Curve.Friction // 可选动画曲线 }) .shadow({ radius: StrUtil.isEmpty(song.pixelMapPath) ?6:14, type: ShadowType.BLUR, color: 'on_primary' }) .onClick(() => { this.playSong(song, index, true); }) // 歌曲信息 Column({ space: 4 }) { Text(song.name) .fontSize(15) .fontColor(this.currentSong?.filePath == song.filePath ? this.themeColor : $r('app.color.index_tab_font_color')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row() { Text((song.artist ?? '') + " ") .fontSize(13) .fontColor(this.currentSong?.filePath == song.filePath ? this.themeColor : $r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .visibility(song.artist ? Visibility.Visible : Visibility.None) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.buildSongMetaLine(song)) .fontSize(13) .fontColor(this.currentSong?.filePath == song.filePath ? this.themeColor : $r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('90%') } .alignItems(HorizontalAlign.Start) .layoutWeight(1) .padding({ right: 20 }) Column() { ImageAnimator() .images(CommonConstants.IMAGE_FRAME_INFO)// 动画数组 .duration(1000)// 持续 .state(AnimationStatus.Running)// 动画状态 .fillMode(FillMode.Forwards) .visibility(this.currentSong?.filePath == song.filePath ? Visibility.Visible : Visibility.None) .width(18) .margin({ right: 12, top: 8, bottom: 8 }) .height(18) .iterations(-1) // 播放次数 } } } .width('100%') .padding(12) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 }) .backgroundColor(Color.Transparent) .onClick(() => { this.playSong(song, index); }) } @Builder buildArtistItem(artist: NavidromeRestArtist) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row({ space: 12 }) { Image(artist.coverUrl ?? $r('app.media.nocover')) .width(48) .height(48) .borderRadius(10) .draggable(false) .alt($r('app.media.nocover')) // .interpolation(ImageInterpolation.High)// 用于重采样后的抗锯齿 // .autoResize(true) // 重采样,可减少内存占用 .sourceSize({ width: 38, height: 38 }) .fillColor(artist.coverUrl ? undefined : this.themeColor) .objectFit(ImageFit.Cover) .margin({ left: 20 }) Column({ space: 4 }) { Text(artist.name ?? Constants.UNKNOWN_ARTIST) .fontSize(15) .fontColor($r('app.color.index_tab_font_color')) .maxLines(1) .textAlign(TextAlign.Start) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.buildArtistMetaLine(artist)) .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .visibility((artist.albumCount ?? 0) === 0 && (artist.songCount ?? 0) === 0 ? Visibility.None : Visibility.Visible) } .alignItems(HorizontalAlign.Start) .padding({ right: 20 }) } .width('100%') } .width('100%') .padding(12) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.85 }) .backgroundColor(Color.Transparent) .onClick(() => { this.onArtistSelected(artist); }) } @Builder buildAlbumItem(album: NavidromeRestAlbum) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row({ space: 12 }) { Image(album.coverUrl ?? $r('app.media.nocover')) .width(48) .height(48) .borderRadius(10) .clip(true) .alt($r('app.media.nocover')) .draggable(false) // .interpolation(ImageInterpolation.High)// 用于重采样后的抗锯齿 // .autoResize(true) // 重采样,可减少内存占用 .sourceSize({ width: 38, height: 38 }) .fillColor(album.coverUrl ? undefined : this.themeColor) .objectFit(ImageFit.Cover) .margin({ left: 20 }) Column({ space: 4 }) { Text(album.name ?? '未知专辑') .fontSize(15) .fontColor($r('app.color.index_tab_font_color')) .maxLines(1) .textAlign(TextAlign.Start) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row() { Text(album.artist ?? Constants.UNKNOWN_ARTIST) .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.buildAlbumMetaLine(album)) .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } } .alignItems(HorizontalAlign.Start) .padding({ right: 20 }) } .width('100%') } .width('100%') .padding(12) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.85 }) .backgroundColor(Color.Transparent) .onClick(() => { this.onAlbumSelected(album); }) } @Builder buildPlaylistItem(playlist: NavidromeRestPlaylist) { Button({ type: ButtonType.Capsule, stateEffect: true }) { Row({ space: 12 }) { Image(playlist.coverUrl ?? $r('app.media.nocover')) .width(48) .height(48) .borderRadius(10) .alt($r('app.media.nocover')) .draggable(false) .sourceSize({ width: 38, height: 38 }) .fillColor(playlist.coverUrl ? undefined : this.themeColor) .objectFit(ImageFit.Cover) .margin({ left: 20 }) Column({ space: 4 }) { Text(playlist.name ?? '未知歌单') .fontSize(15) .fontColor($r('app.color.index_tab_font_color')) .maxLines(1) .textAlign(TextAlign.Start) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.buildPlaylistMetaLine(playlist)) .fontSize(13) .fontColor($r('app.color.index_tab_font_color')) .opacity(0.6) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) // if (playlist.ownerName) { // Text(playlist.ownerName) // .fontSize(12) // .fontColor($r('app.color.index_tab_font_color')) // .opacity(0.5) // .maxLines(1) // .textOverflow({ overflow: TextOverflow.Ellipsis }) // } } .alignItems(HorizontalAlign.Start) .padding({ right: 20 }) } .width('100%') } .width('100%') .padding(12) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.85 }) .backgroundColor(Color.Transparent) .onClick(() => { this.onPlaylistSelected(playlist); }) } private buildSongMetaLine(song: VideoItem): string { const parts: string[] = []; if (song.duration) { parts.push(song.duration as string); } // if (song.size) { // parts.push(song.size as string); // } // if (parts.length === 0 && song.cTime) { // parts.push(song.cTime as string); // } return parts.join(' · '); } private buildArtistMetaLine(artist: NavidromeRestArtist): string { const albumCount = artist.albumCount ?? 0; const songCount = artist.songCount ?? 0; const playCount = artist.playCount ?? 0; // 如果专辑和歌曲数都为0,不显示元数据信息 if (albumCount === 0 && songCount === 0) { return ''; } return `专辑 ${albumCount} · 歌曲 ${songCount} `; } private buildAlbumMetaLine(album: NavidromeRestAlbum): string { const parts: string[] = []; if (album.duration !== undefined) { const duration = this.formatSongDuration(album.duration); if (duration) { parts.push(' '+duration); } } if (album.minYear) { parts.push(`发行 ${album.minYear}`); } return parts.join(' · '); } private buildPlaylistMetaLine(playlist: NavidromeRestPlaylist): string { const parts: string[] = []; if (playlist.songCount !== undefined) { parts.push(`${playlist.songCount} 首歌`); } if (playlist.duration !== undefined) { const duration = this.formatSongDuration(playlist.duration); if (duration) { parts.push(duration); } } return parts.join(' · '); } private getVisibleSongs(): VideoItem[] { if (this.isSearchMode) { return this.searchText.length > 0 ? this.filteredList : this.allVideos; } if (this.filterType !== NavFilterType.None) { return this.filterSongs; } return this.allVideos; } private onArtistSelected(artist: NavidromeRestArtist): void { if (!artist || !artist.id) { return; } this.isShowTitleBar = true; // 保存当前标签页,进入详情视图 this.previousTab = this.selectedTab; this.isDetailView = true; // 应用筛选但不切换标签页 void this.applyFilter(NavFilterType.Artist, artist.id, artist.name ?? Constants.UNKNOWN_ARTIST); } private onAlbumSelected(album: NavidromeRestAlbum): void { if (!album || !album.id) { return; } this.isShowTitleBar = true; // 保存当前标签页,进入详情视图 this.previousTab = this.selectedTab; this.isDetailView = true; // 应用筛选但不切换标签页 void this.applyFilter(NavFilterType.Album, album.id, album.name ?? '未知专辑'); } private onPlaylistSelected(playlist: NavidromeRestPlaylist): void { if (!playlist || !playlist.id) { return; } this.isShowTitleBar = true; // 保存当前标签页,进入详情视图 this.previousTab = this.selectedTab; this.isDetailView = true; // 应用筛选但不切换标签页 void this.applyFilter(NavFilterType.Playlist, playlist.id, playlist.name ?? '未知歌单'); } private async applyFilter(type: NavFilterType, id: string, label: string): Promise { // 记录筛选操作 void ServerLogUtil.info('NavidromeFilter', `应用筛选:`); const typeText = type === NavFilterType.Artist ? '艺术家' : type === NavFilterType.Album ? '专辑' : type === NavFilterType.Playlist ? '歌单' : '无'; void ServerLogUtil.info('NavidromeFilter', `- 类型: ${type} (${typeText})`); void ServerLogUtil.info('NavidromeFilter', `- ID: ${id}`); void ServerLogUtil.info('NavidromeFilter', `- 标签: ${label}`); void ServerLogUtil.info('NavidromeFilter', `- 筛选前总歌曲数: ${this.allVideos.length}`); // 先更新状态 this.filterType = type; this.filterId = id; this.filterLabel = label; // 退出搜索模式,确保显示筛选结果 this.isSearchMode = false; this.searchText = ''; this.filteredList = []; this.isSearchLoading = false; this.searchTicket++; this.filterSongs = []; this.isFilterLoading = true; // 调试日志:检查筛选结果 void this.loadSongsForFilter(label).catch((error: Error) => { void ServerLogUtil.error('NavidromeFilter', `筛选歌曲加载失败: ${error.message}`); ToastUtil.showToast(error.message || '筛选数据加载失败'); }); // 不再自动切换标签页,保持在详情视图模式 } private async loadSongsForFilter(label:string): Promise { if (this.filterType === NavFilterType.None || !this.filterId) { this.filterSongs = []; this.isFilterLoading = false; return; } const account = this.resolveActiveAccount(); if (!account) { this.filterSongs = []; this.isFilterLoading = false; throw new Error('媒体库账号不可用'); } if (!this.isNavidromeAccount(account)) { let restSongs: NavidromeRestSong[] = []; if (this.filterType === NavFilterType.Artist) { if (this.isJellyfinAccount(account)) { const songs = await this.fetchAllJellyfinArtistSongs(account, this.filterId); restSongs = this.convertJellyfinSongsToRestSongs(songs); } else if (this.isEmbyAccount(account)) { const songs = await this.fetchAllEmbyArtistSongs(account, this.filterId); restSongs = this.convertEmbySongsToRestSongs(songs); } else if (this.isAudioStationAccount(account)) { const songs = await this.fetchAllAudioStationArtistSongs(account, this.filterId); restSongs = this.convertAudioStationSongsToRestSongs(songs); } else if (this.isPlexAccount(account)) { const songs = await this.fetchAllPlexArtistSongs(account, this.filterId); restSongs = this.convertPlexSongsToRestSongs(songs); } else if (this.isDaoLiYuAccount(account)) { // 同时传递 artistId 和 artistName,支持按名称匹配作为后备 const songs = await daoLiYuApi.getTracksByArtistId(account, this.filterId, label); restSongs = this.convertDaoLiYuSongsToRestSongs(songs); } } else if (this.filterType === NavFilterType.Album) { if (this.isJellyfinAccount(account)) { const songs = await jellyfinApi.getAlbumSongs(account, this.filterId); restSongs = this.convertJellyfinSongsToRestSongs(songs); } else if (this.isEmbyAccount(account)) { const songs = await embyApi.getAlbumSongs(account, this.filterId); restSongs = this.convertEmbySongsToRestSongs(songs); } else if (this.isAudioStationAccount(account)) { const albumKey = this.parseAudioStationAlbumKey(this.filterId); const songs = await audioStationApi.getAlbumSongs(account, albumKey.name, albumKey.artist); restSongs = this.convertAudioStationSongsToRestSongs(songs); } else if (this.isPlexAccount(account)) { const songs = await plexApi.getAlbumSongs(account, this.filterId); restSongs = this.convertPlexSongsToRestSongs(songs); } else if (this.isDaoLiYuAccount(account)) { // 同时传递 albumId 和 albumName,支持按名称匹配作为后备 const songs = await daoLiYuApi.getTracksByAlbumId(account, this.filterId, label); restSongs = this.convertDaoLiYuSongsToRestSongs(songs); } } else if (this.filterType === NavFilterType.Playlist) { if (this.isAudioStationAccount(account)) { const songs = await this.fetchAllAudioStationPlaylistSongs(account, this.filterId); restSongs = this.convertAudioStationSongsToRestSongs(songs); } else if (this.isPlexAccount(account)) { const songs = await this.fetchAllPlexPlaylistSongs(account, this.filterId); restSongs = this.convertPlexSongsToRestSongs(songs); } else if (this.isDaoLiYuAccount(account)) { const songs = await daoLiYuApi.getPlaylistTracks(account, this.filterId); restSongs = this.convertDaoLiYuSongsToRestSongs(songs); } else { this.filterSongs = []; this.isFilterLoading = false; ToastUtil.showToast('当前媒体库不支持歌单'); return; } } const videoItems = await this.convertSongsToVideoItems(restSongs, account); this.filterSongs = videoItems; this.isFilterLoading = false; return; } const expectedFilterId = this.filterId; const expectedFilterType = this.filterType; try { let songs: NavidromeRestSong[] = []; if (this.filterType === NavFilterType.Artist) { songs = await navidromeRestApi.fetchSongsByArtist(account, this.filterId,label); } else if (this.filterType === NavFilterType.Album) { songs = await navidromeRestApi.fetchSongsByAlbum(account, this.filterId,label); } else if (this.filterType === NavFilterType.Playlist) { songs = await navidromeRestApi.fetchSongsByPlaylist(account, this.filterId); } const videoItems = await this.convertSongsToVideoItems(songs, account); if (expectedFilterId !== this.filterId || expectedFilterType !== this.filterType) { return; } this.filterSongs = videoItems; if (videoItems.length === 0) { void ServerLogUtil.warn('NavidromeFilter', '筛选下暂无歌曲'); } else { void ServerLogUtil.info('NavidromeFilter', `筛选歌曲加载完成: ${videoItems.length} 首`); } } finally { this.isFilterLoading = false; } } private clearFilter(): void { this.filterType = NavFilterType.None; this.filterId = ''; this.filterLabel = ''; this.filterSongs = []; this.isFilterLoading = false; // 退出详情视图 this.isDetailView = false; } private playSong(song: VideoItem, index: number, isJump: boolean = false): void { try { if (!this.allVideos || this.allVideos.length === 0) { ToastUtil.showToast('暂无可播放的歌曲'); return; } const account = this.resolveActiveAccount(); if (!account || !account.id) { ToastUtil.showToast('媒体库账号信息不完整,无法播放'); return; } // 记录播放详细信息 void ServerLogUtil.info('StreamingPlay', `开始播放歌曲: ${song.name} (#${index})`); void ServerLogUtil.info('StreamingPlay', `歌曲信息:`); void ServerLogUtil.info('StreamingPlay', `- ID: ${song.id}`); void ServerLogUtil.info('StreamingPlay', `- 艺术家: ${song.artist || '未知'}`); void ServerLogUtil.info('StreamingPlay', `- 专辑: ${song.album || '未知'}`); void ServerLogUtil.info('StreamingPlay', `- 文件大小: ${song.size || '未知'}`); void ServerLogUtil.info('StreamingPlay', `- 时长: ${song.duration || '未知'}`); void ServerLogUtil.info('StreamingPlay', `- 封面: ${song.pixelMapPath ? '有' : '无'}`); void ServerLogUtil.info('StreamingPlay', `- 播放路径: ${song.filePath}`); const playlistSource = this.getVisibleSongs().length > 0 ? this.getVisibleSongs() : this.allVideos; const targetIndex = playlistSource.findIndex(item => item.id === song.id); const startIndex = targetIndex >= 0 ? targetIndex : Math.min(index, Math.max(playlistSource.length - 1, 0)); void ServerLogUtil.info('StreamingPlay', `播放列表设置: 起始索引 ${startIndex}, 已加载 ${playlistSource.length}, 服务端总数 ${this.serverTotalSongCount}`); setNavidromePlaylist(playlistSource, startIndex); // 保存服务端总数到全局存储 if (this.serverTotalSongCount > 0) { setNavidromeTotalCount(this.serverTotalSongCount); } const playlistData: PlaylistEventData = { playlistId: NAVIDROME_PLAYLIST_ID, playlistName: `${getRemoteDriveDisplayLabel(account.webType)} - ${account.name ?? '未知账户'}`, songCount: this.serverTotalSongCount > 0 ? this.serverTotalSongCount : playlistSource.length, startIndex, isJump: isJump,//设置true会弹出播放页 songFilePaths: playlistSource.map(item => item.filePath) }; const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }; emitter.emit(eventPlaylistPlay, { data: playlistData }); void ServerLogUtil.info('StreamingPlay', `播放事件已发送`); void ServerLogUtil.debug('StreamingPlay', `播放事件详情: 列表ID=${playlistData.playlistId}, 列表名称=${playlistData.playlistName}, 歌曲数=${playlistData.songCount}, 起始索引=${playlistData.startIndex}, 是否跳转=${playlistData.isJump}`); if(!this.isNoJumpToHome){ void ServerLogUtil.debug('StreamingPlay', '跳转到首页播放器'); // 跳转到首页播放器 this.getUIContext()?.animateTo({ duration: 555 }, () => { this.mType = 0 }) } } catch (error) { const err = error as Error; void ServerLogUtil.error('StreamingPlay', `播放失败: ${err.message}`); void ServerLogUtil.error('StreamingPlay', `失败歌曲信息: ${song.name} (${song.id})`); ToastUtil.showToast('播放失败'); } } @Builder buildContentView(){ // 主内容区域 if (this.loading) { Column() { LoadingProgress() .width(50) .height(50) .color($r('app.color.title_bar_bg')) Text('加载中...') .margin({ top: 10 }) .fontSize(14) .fontColor($r('app.color.index_tab_unselected_font_color')) } .width('100%') .layoutWeight(1) .padding({top:this.topSafeHeight+98}) .justifyContent(FlexAlign.Center) } else { if (this.isSearchMode && this.selectedTab === 0 && this.searchText.length > 0 && this.isSearchLoading) { Column() { LoadingProgress() .width(40) .height(40) .color(this.themeColor) Text('正在通过 API 搜索歌曲...') .margin({ top: 12 }) .fontSize(14) .fontColor($r('app.color.index_tab_unselected_font_color')) } .width('100%') .layoutWeight(1) } else if (this.selectedTab === 0 && this.filterType !== NavFilterType.None && this.isFilterLoading) { Column() { LoadingProgress() .width(40) .height(40) .color(this.themeColor) Text('正在加载筛选歌曲...') .margin({ top: 12 }) .fontSize(14) .fontColor($r('app.color.index_tab_unselected_font_color')) } .width('100%') .layoutWeight(1) } else { List({scroller:this.scroller, space: 8 }) { // 详情视图模式下显示筛选后的歌曲,否则根据标签页显示对应内容 if (this.selectedTab === 0||this.isDetailView) { ForEach(this.getVisibleSongs(), (item: VideoItem, index: number) => { ListItem() { this.buildSongItem(item, index) } }, (item: VideoItem) => item.id) } else if (this.selectedTab === 1) { ForEach(this.artists, (artist: NavidromeRestArtist) => { ListItem() { this.buildArtistItem(artist) } }, (artist: NavidromeRestArtist) => artist.id) } else if (this.selectedTab === 2) { ForEach(this.albums, (album: NavidromeRestAlbum) => { ListItem() { this.buildAlbumItem(album) } }, (album: NavidromeRestAlbum) => album.id) } else if (this.selectedTab === 3) { ForEach(this.playlists, (playlist: NavidromeRestPlaylist) => { ListItem() { this.buildPlaylistItem(playlist) } }, (playlist: NavidromeRestPlaylist) => playlist.id) } } .onScrollFrameBegin((offset: number) => { // 获取当前滚动偏移量 if(this.autoHideTitle){ const currentOffsetY = this.scroller.currentOffset().yOffset; // 判断滚动方向 if (currentOffsetY > this.prevOffsetY) { this.isShowTitleBar = false } else if (currentOffsetY < this.prevOffsetY) { this.isShowTitleBar = true } // 更新前一次偏移量 this.prevOffsetY = currentOffsetY; } return { offsetRemain: offset }; }) .width('100%') .height('100%') .layoutWeight(1) .cachedCount(3) .padding({ top: 5, bottom: 5 }) .listDirection(Axis.Vertical) .contentEndOffset(this.bottomSafeHeight+70) .contentStartOffset(this.topSafeHeight + 98) .scrollBar(BarState.Auto) .edgeEffect(EdgeEffect.Spring) .onReachEnd(() => { void this.handleReachEnd(); }) } // 空状态 if (this.getCurrentCount() === 0 && !(this.selectedTab === 0 && this.filterType !== NavFilterType.None && this.isFilterLoading) && !(this.isSearchMode && this.searchText.length > 0 && this.isSearchLoading)) { Column() { SymbolGlyph($r('sys.symbol.music_fill')) .fontSize(50) .opacity(0.3) Text(this.getEmptyTitle()) .margin({ top: 16 }) .fontSize(16) .fontColor($r('app.color.index_tab_unselected_font_color')) Text(this.getEmptySubtitle()) .margin({ top: 8 }) .fontSize(14) .fontColor($r('app.color.index_tab_unselected_font_color')) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } } build() { Stack() { Column() { this.buildContentView() } .height('100%') Column() { this.topTitleBar() } .visibility(this.isShowTitleBar?Visibility.Visible: this.autoHideTitle?Visibility.None:Visibility.Visible) .animation({ duration: 500, curve: Curve.Friction // 可选动画曲线 }) } .alignContent(Alignment.Top) .width('100%') .height('100%') .backgroundImage(this.isCustomizeBg?this.customizeBgPath:$r('app.color.start_window_background')) .backgroundImageSize(this.isLandscape?{width:'100%'}:{ height: '100%'}) .backgroundImagePosition(Alignment.Center) .backdropBlur(this.blurValue) .backgroundBrightness({rate:this.isCustomizeBg?0.1:0,lightUpDegree:this.bgBrightness}) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) } } /** * Navidrome 数据加载任务 (使用 taskpool 后台执行) * 该函数在 worker 线程中运行,避免阻塞 UI 线程 */ @Concurrent async function loadNavidromeDataTask(accountData: AccountData, ticket: number): Promise { 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 = 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 = 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 = 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 = 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; }