|
|
@@ -23,6 +23,9 @@ 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 { getRemoteDriveDisplayLabel } from '../common/util/RemoteDriveLabel';
|
|
|
|
|
|
const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
|
|
|
const NAVIDROME_SEARCH_LIMIT = 500;
|
|
|
@@ -47,6 +50,11 @@ interface CachePreviewInfo {
|
|
|
examplePath: string;
|
|
|
}
|
|
|
|
|
|
+interface LibraryInfo {
|
|
|
+ type: number;
|
|
|
+ scheme: string;
|
|
|
+}
|
|
|
+
|
|
|
@Component
|
|
|
export struct NavidromePage {
|
|
|
@State isShowTitleBar: boolean = true //是否显示分类导航条
|
|
|
@@ -92,6 +100,19 @@ export struct NavidromePage {
|
|
|
private coverUrlCache: Map<string, string> = new Map();
|
|
|
private albumCoverLookup: Map<string, string> = new Map();
|
|
|
private searchTicket: number = 0;
|
|
|
+ private readonly REMOTE_PAGE_SIZE: number = 200;
|
|
|
+
|
|
|
+ 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 createTabOptions(): SegmentButtonOptions {
|
|
|
const buttons = [
|
|
|
@@ -141,6 +162,7 @@ export struct NavidromePage {
|
|
|
@State filteredList: Array<VideoItem> = []; // 过滤后的歌曲结果
|
|
|
@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();
|
|
|
@@ -196,7 +218,7 @@ export struct NavidromePage {
|
|
|
if (!account) {
|
|
|
this.resetData();
|
|
|
if (showToastWhenMissing) {
|
|
|
- ToastUtil.showToast('请先选择 Navidrome 账号');
|
|
|
+ ToastUtil.showToast('请先选择媒体库账号');
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
@@ -207,7 +229,7 @@ export struct NavidromePage {
|
|
|
// 记录账号信息和缓存路径
|
|
|
await this.logAccountCacheInfo(account);
|
|
|
|
|
|
- await this.loadNavidromeLibrary(account);
|
|
|
+ await this.loadMediaLibrary(account);
|
|
|
this.doSortType(this.sortType)
|
|
|
}
|
|
|
|
|
|
@@ -215,7 +237,9 @@ export struct NavidromePage {
|
|
|
if (!this.selectedAccount) {
|
|
|
return undefined;
|
|
|
}
|
|
|
- if (this.selectedAccount.webType !== RemoteDriveType.Navidrome) {
|
|
|
+ if (!this.isNavidromeAccount(this.selectedAccount)
|
|
|
+ && !this.isJellyfinAccount(this.selectedAccount)
|
|
|
+ && !this.isEmbyAccount(this.selectedAccount)) {
|
|
|
return undefined;
|
|
|
}
|
|
|
if (!this.selectedAccount.host || this.selectedAccount.host.length === 0) {
|
|
|
@@ -239,6 +263,20 @@ export struct NavidromePage {
|
|
|
this.isAlbumPageLoading = false;
|
|
|
}
|
|
|
|
|
|
+ private async loadMediaLibrary(account: WebDavAccount): Promise<void> {
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private async loadNavidromeLibrary(account: WebDavAccount): Promise<void> {
|
|
|
const ticket = ++this.loadTicket;
|
|
|
this.loading = true;
|
|
|
@@ -273,6 +311,66 @@ export struct NavidromePage {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private async loadJellyfinLibrary(account: WebDavAccount): Promise<void> {
|
|
|
+ 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;
|
|
|
+ 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<void> {
|
|
|
+ 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;
|
|
|
+ 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 loadNextSongPage(account: WebDavAccount, ticket?: number): Promise<void> {
|
|
|
if (this.songNextStart === null || this.isSongPageLoading) {
|
|
|
return;
|
|
|
@@ -370,6 +468,146 @@ export struct NavidromePage {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private async loadNextJellyfinSongPage(account: WebDavAccount, ticket?: number): Promise<void> {
|
|
|
+ if (this.songNextStart === null || this.isSongPageLoading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentTicket = ticket ?? this.loadTicket;
|
|
|
+ this.isSongPageLoading = true;
|
|
|
+ try {
|
|
|
+ const response = await jellyfinApi.getSongsPage(account, this.songNextStart, this.REMOTE_PAGE_SIZE);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const restSongs = this.convertJellyfinSongsToRestSongs(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', `Jellyfin 歌曲追加: 本次 ${videoItems.length} 首, 总数 ${this.allVideos.length}`);
|
|
|
+ } finally {
|
|
|
+ this.isSongPageLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async loadNextJellyfinArtistPage(account: WebDavAccount, ticket?: number): Promise<void> {
|
|
|
+ if (this.artistNextStart === null || this.isArtistPageLoading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentTicket = ticket ?? this.loadTicket;
|
|
|
+ this.isArtistPageLoading = true;
|
|
|
+ try {
|
|
|
+ const response = await 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<void> {
|
|
|
+ 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<void> {
|
|
|
+ if (this.songNextStart === null || this.isSongPageLoading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentTicket = ticket ?? this.loadTicket;
|
|
|
+ this.isSongPageLoading = true;
|
|
|
+ try {
|
|
|
+ const response = await embyApi.getSongsPage(account, this.songNextStart, this.REMOTE_PAGE_SIZE);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const restSongs = this.convertEmbySongsToRestSongs(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', `Emby 歌曲追加: 本次 ${videoItems.length} 首, 总数 ${this.allVideos.length}`);
|
|
|
+ } finally {
|
|
|
+ this.isSongPageLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async loadNextEmbyArtistPage(account: WebDavAccount, ticket?: number): Promise<void> {
|
|
|
+ if (this.artistNextStart === null || this.isArtistPageLoading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentTicket = ticket ?? this.loadTicket;
|
|
|
+ this.isArtistPageLoading = true;
|
|
|
+ try {
|
|
|
+ const response = await 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<void> {
|
|
|
+ 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 handleReachEnd(): Promise<void> {
|
|
|
if (this.loading) {
|
|
|
return;
|
|
|
@@ -381,18 +619,52 @@ export struct NavidromePage {
|
|
|
if (!account) {
|
|
|
return;
|
|
|
}
|
|
|
- switch (this.selectedTab) {
|
|
|
- case 0:
|
|
|
- await this.loadNextSongPage(account);
|
|
|
- break;
|
|
|
- case 1:
|
|
|
- await this.loadNextArtistPage(account);
|
|
|
- break;
|
|
|
- case 2:
|
|
|
- await this.loadNextAlbumPage(account);
|
|
|
- break;
|
|
|
- default:
|
|
|
- break;
|
|
|
+ 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;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -488,6 +760,159 @@ export struct NavidromePage {
|
|
|
return map;
|
|
|
}
|
|
|
|
|
|
+ private async buildRemoteCoverMap(ids: string[], account: WebDavAccount): Promise<Map<string, string>> {
|
|
|
+ const map = new Map<string, string>();
|
|
|
+ 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 async convertJellyfinArtistsToRest(artists: JellyfinArtist[], account: WebDavAccount): Promise<NavidromeRestArtist[]> {
|
|
|
+ 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,
|
|
|
+ coverUrl: coverUrl
|
|
|
+ };
|
|
|
+ results.push(restArtist);
|
|
|
+ }
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async convertEmbyArtistsToRest(artists: EmbyArtist[], account: WebDavAccount): Promise<NavidromeRestArtist[]> {
|
|
|
+ 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,
|
|
|
+ coverUrl: coverUrl
|
|
|
+ };
|
|
|
+ results.push(restArtist);
|
|
|
+ }
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async convertJellyfinAlbumsToRest(albums: JellyfinAlbum[], account: WebDavAccount): Promise<NavidromeRestAlbum[]> {
|
|
|
+ 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<NavidromeRestAlbum[]> {
|
|
|
+ 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 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 async fetchAllJellyfinArtistSongs(account: WebDavAccount, artistId: string): Promise<JellyfinSong[]> {
|
|
|
+ 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<EmbySong[]> {
|
|
|
+ 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 convertApiSongsToRestSongs(songs: NavidromeSong[]): NavidromeRestSong[] {
|
|
|
return songs.map((song: NavidromeSong): NavidromeRestSong => ({
|
|
|
id: song.id,
|
|
|
@@ -551,12 +976,16 @@ export struct NavidromePage {
|
|
|
}
|
|
|
|
|
|
private async resolveSongCover(song: NavidromeRestSong, account: WebDavAccount): Promise<string | undefined> {
|
|
|
- if (song.albumId) {
|
|
|
+ if (this.isNavidromeAccount(account) && song.albumId) {
|
|
|
const albumCover = song.albumId ? this.albumCoverLookup.get(song.albumId) : undefined;
|
|
|
if (albumCover) {
|
|
|
return albumCover;
|
|
|
}
|
|
|
}
|
|
|
+ if (!this.isNavidromeAccount(account)) {
|
|
|
+ const fallbackId = song.albumId ?? song.id;
|
|
|
+ return this.buildCoverUrl(account, fallbackId);
|
|
|
+ }
|
|
|
const directUrl = this.resolveEmbedCover(account, song.embedArtPath ?? song.coverArtPath);
|
|
|
if (directUrl) {
|
|
|
return directUrl;
|
|
|
@@ -571,15 +1000,24 @@ export struct NavidromePage {
|
|
|
}
|
|
|
const normalizedId = coverId.trim();
|
|
|
const cacheKey = `${normalizedId}_${size}`;
|
|
|
- const cached = this.coverUrlCache.get(cacheKey);
|
|
|
- if (cached) {
|
|
|
- void ServerLogUtil.debug('CoverCache', `封面URL缓存命中: ${cacheKey} -> ${cached}`);
|
|
|
- return cached;
|
|
|
+ if (this.isNavidromeAccount(account)) {
|
|
|
+ const cached = this.coverUrlCache.get(cacheKey);
|
|
|
+ if (cached) {
|
|
|
+ void ServerLogUtil.debug('CoverCache', `封面URL缓存命中: ${cacheKey} -> ${cached}`);
|
|
|
+ return cached;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
void ServerLogUtil.debug('CoverCache', `生成封面URL: ${coverId} (尺寸: ${size})`);
|
|
|
- const url = await navidromeApi.buildCoverArtUrl(account, normalizedId, size);
|
|
|
- if (url) {
|
|
|
+ let url: string | undefined = undefined;
|
|
|
+ if (this.isNavidromeAccount(account)) {
|
|
|
+ url = await navidromeApi.buildCoverArtUrl(account, normalizedId, size);
|
|
|
+ } else if (this.isJellyfinAccount(account)) {
|
|
|
+ url = await jellyfinApi.buildPrimaryImageUrl(account, normalizedId, size, size);
|
|
|
+ } else if (this.isEmbyAccount(account)) {
|
|
|
+ url = await embyApi.buildPrimaryImageUrl(account, normalizedId, size, size);
|
|
|
+ }
|
|
|
+ if (url && this.isNavidromeAccount(account)) {
|
|
|
this.coverUrlCache.set(cacheKey, url);
|
|
|
void ServerLogUtil.debug('CoverCache', `封面URL已缓存: ${cacheKey} -> ${url}`);
|
|
|
} else {
|
|
|
@@ -644,16 +1082,30 @@ export struct NavidromePage {
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
+ }
|
|
|
+ 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);
|
|
|
const videoItem = new VideoItem(
|
|
|
title,
|
|
|
song.id,
|
|
|
- `navidrome://${account.id ?? 0}/${song.id}`,
|
|
|
- CommonConstants.TYPE_NAVIDROME,
|
|
|
+ `${libraryInfo.scheme}://${account.id ?? 0}/${song.id}`,
|
|
|
+ libraryInfo.type,
|
|
|
song.size ?? 0,
|
|
|
song.createdAt ?? '',
|
|
|
Utility.formatFSize(song.size ?? 0),
|
|
|
@@ -978,7 +1430,7 @@ export struct NavidromePage {
|
|
|
if (!account) {
|
|
|
this.filteredList = [];
|
|
|
this.isSearchLoading = false;
|
|
|
- ToastUtil.showToast('Navidrome账号不可用');
|
|
|
+ ToastUtil.showToast('媒体库账号不可用');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -988,18 +1440,27 @@ export struct NavidromePage {
|
|
|
void ServerLogUtil.info('NavidromeSearch', `开始远程搜索: "${keyword}"`);
|
|
|
|
|
|
try {
|
|
|
- const songs = await navidromeApi.searchSongs(account, keyword, NAVIDROME_SEARCH_LIMIT);
|
|
|
+ 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);
|
|
|
+ }
|
|
|
if (ticket !== this.searchTicket) {
|
|
|
return;
|
|
|
}
|
|
|
const searchTime = Date.now() - startTime;
|
|
|
- if (!songs || songs.length === 0) {
|
|
|
+ if (!restSongs || restSongs.length === 0) {
|
|
|
this.filteredList = [];
|
|
|
void ServerLogUtil.warn('NavidromeSearch', `搜索无结果: "${keyword}"`);
|
|
|
void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${searchTime}ms`);
|
|
|
return;
|
|
|
}
|
|
|
- const restSongs = this.convertApiSongsToRestSongs(songs);
|
|
|
const videoItems = await this.convertSongsToVideoItems(restSongs, account);
|
|
|
if (ticket !== this.searchTicket) {
|
|
|
return;
|
|
|
@@ -1355,7 +1816,31 @@ export struct NavidromePage {
|
|
|
if (!account) {
|
|
|
this.filterSongs = [];
|
|
|
this.isFilterLoading = false;
|
|
|
- throw new Error('Navidrome账号不可用');
|
|
|
+ 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.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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const videoItems = await this.convertSongsToVideoItems(restSongs, account);
|
|
|
+ this.filterSongs = videoItems;
|
|
|
+ this.isFilterLoading = false;
|
|
|
+ return;
|
|
|
}
|
|
|
const expectedFilterId = this.filterId;
|
|
|
const expectedFilterType = this.filterType;
|
|
|
@@ -1397,7 +1882,7 @@ export struct NavidromePage {
|
|
|
}
|
|
|
const account = this.resolveActiveAccount();
|
|
|
if (!account || !account.id) {
|
|
|
- ToastUtil.showToast('Navidrome账号信息不完整,无法播放');
|
|
|
+ ToastUtil.showToast('媒体库账号信息不完整,无法播放');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -1422,7 +1907,7 @@ export struct NavidromePage {
|
|
|
|
|
|
const playlistData: PlaylistEventData = {
|
|
|
playlistId: NAVIDROME_PLAYLIST_ID,
|
|
|
- playlistName: `Navidrome - ${account.name ?? '未知账户'}`,
|
|
|
+ playlistName: `${getRemoteDriveDisplayLabel(account.webType)} - ${account.name ?? '未知账户'}`,
|
|
|
songCount: playlistSource.length,
|
|
|
startIndex,
|
|
|
isJump: isJump,//设置true会弹出播放页
|