|
|
@@ -25,6 +25,8 @@ 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 { getRemoteDriveDisplayLabel } from '../common/util/RemoteDriveLabel';
|
|
|
import NavidromeListCache, { AllCacheData } from '../common/util/NavidromeListCache';
|
|
|
import { taskpool } from '@kit.ArkTS';
|
|
|
@@ -58,6 +60,16 @@ interface LibraryInfo {
|
|
|
scheme: string;
|
|
|
}
|
|
|
|
|
|
+class AudioStationAlbumKey {
|
|
|
+ name: string;
|
|
|
+ artist: string;
|
|
|
+
|
|
|
+ constructor(name: string, artist: string) {
|
|
|
+ this.name = name;
|
|
|
+ this.artist = artist;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* taskpool 任务结果接口
|
|
|
* 返回带封面的完整数据
|
|
|
@@ -165,6 +177,14 @@ export struct NavidromePage {
|
|
|
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 createTabOptions(): SegmentButtonOptions {
|
|
|
const buttons = [
|
|
|
{ text: '全部' },
|
|
|
@@ -310,7 +330,9 @@ export struct NavidromePage {
|
|
|
}
|
|
|
if (!this.isNavidromeAccount(this.selectedAccount)
|
|
|
&& !this.isJellyfinAccount(this.selectedAccount)
|
|
|
- && !this.isEmbyAccount(this.selectedAccount)) {
|
|
|
+ && !this.isEmbyAccount(this.selectedAccount)
|
|
|
+ && !this.isAudioStationAccount(this.selectedAccount)
|
|
|
+ && !this.isPlexAccount(this.selectedAccount)) {
|
|
|
return undefined;
|
|
|
}
|
|
|
if (!this.selectedAccount.host || this.selectedAccount.host.length === 0) {
|
|
|
@@ -352,6 +374,14 @@ export struct NavidromePage {
|
|
|
}
|
|
|
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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -545,6 +575,70 @@ export struct NavidromePage {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private async loadAudioStationLibrary(account: WebDavAccount): Promise<void> {
|
|
|
+ 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<void> {
|
|
|
+ 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 loadNextSongPage(account: WebDavAccount, ticket?: number): Promise<void> {
|
|
|
if (this.songNextStart === null || this.isSongPageLoading) {
|
|
|
return;
|
|
|
@@ -835,6 +929,192 @@ export struct NavidromePage {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private async loadNextAudioStationSongPage(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 audioStationApi.getSongsPage(account, this.songNextStart, this.REMOTE_PAGE_SIZE);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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}`);
|
|
|
+ } finally {
|
|
|
+ this.isSongPageLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async loadNextAudioStationArtistPage(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 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<void> {
|
|
|
+ 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<void> {
|
|
|
+ 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<void> {
|
|
|
+ if (this.songNextStart === null || this.isSongPageLoading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentTicket = ticket ?? this.loadTicket;
|
|
|
+ this.isSongPageLoading = true;
|
|
|
+ try {
|
|
|
+ const response = await plexApi.getSongsPage(account, this.songNextStart, this.REMOTE_PAGE_SIZE);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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}`);
|
|
|
+ } finally {
|
|
|
+ this.isSongPageLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async loadNextPlexArtistPage(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 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<void> {
|
|
|
+ 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<void> {
|
|
|
+ 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 loadNextPlaylistPage(account: WebDavAccount, ticket?: number): Promise<void> {
|
|
|
if (this.playlistNextStart === null || this.isPlaylistPageLoading) {
|
|
|
return;
|
|
|
@@ -919,6 +1199,44 @@ export struct NavidromePage {
|
|
|
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;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1042,6 +1360,23 @@ export struct NavidromePage {
|
|
|
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<NavidromeRestArtist[]> {
|
|
|
const results: NavidromeRestArtist[] = [];
|
|
|
for (let i = 0; i < artists.length; i++) {
|
|
|
@@ -1110,6 +1445,72 @@ export struct NavidromePage {
|
|
|
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<NavidromeRestAlbum[]> {
|
|
|
+ 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<NavidromeRestArtist[]> {
|
|
|
+ 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<NavidromeRestAlbum[]> {
|
|
|
+ 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 convertJellyfinSongsToRestSongs(songs: JellyfinSong[]): NavidromeRestSong[] {
|
|
|
return songs.map(song => {
|
|
|
const restSong: NavidromeRestSong = {
|
|
|
@@ -1152,6 +1553,78 @@ export struct NavidromePage {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ 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 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<JellyfinSong[]> {
|
|
|
const results: JellyfinSong[] = [];
|
|
|
let startIndex = 0;
|
|
|
@@ -1180,6 +1653,60 @@ export struct NavidromePage {
|
|
|
return results;
|
|
|
}
|
|
|
|
|
|
+ private async fetchAllAudioStationArtistSongs(account: WebDavAccount, artistName: string): Promise<AudioStationSong[]> {
|
|
|
+ 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<AudioStationSong[]> {
|
|
|
+ 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<PlexSong[]> {
|
|
|
+ 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<PlexSong[]> {
|
|
|
+ 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 ?? '';
|
|
|
@@ -1291,7 +1818,7 @@ export struct NavidromePage {
|
|
|
// }
|
|
|
|
|
|
if (!this.isNavidromeAccount(account)) {
|
|
|
- const fallbackId = song.albumId ?? song.id;
|
|
|
+ const fallbackId = song.coverArtId ?? song.albumId ?? song.id;
|
|
|
void ServerLogUtil.debug('NavidromePageCover', `歌曲使用非Navidrome账号封面 - title: ${song.title}, fallbackId: ${fallbackId}`);
|
|
|
return this.buildCoverUrl(account, fallbackId);
|
|
|
}
|
|
|
@@ -1350,6 +1877,19 @@ export struct NavidromePage {
|
|
|
} else if (this.isEmbyAccount(account)) {
|
|
|
void ServerLogUtil.debug('NavidromePageCover', `调用 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('NavidromePageCover', `调用 audioStationApi.buildSongCoverUrl - songId=${songId}`);
|
|
|
+ url = await audioStationApi.buildSongCoverUrl(account, songId);
|
|
|
+ } else {
|
|
|
+ const key = this.parseAudioStationAlbumKey(normalizedId);
|
|
|
+ void ServerLogUtil.debug('NavidromePageCover', `调用 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('NavidromePageCover', `调用 plexApi.buildImageUrl - coverId: ${normalizedId}`);
|
|
|
+ url = plexApi.buildImageUrl(account, normalizedId);
|
|
|
}
|
|
|
|
|
|
if (url && this.isNavidromeAccount(account)) {
|
|
|
@@ -1371,6 +1911,14 @@ export struct NavidromePage {
|
|
|
return url;
|
|
|
}
|
|
|
|
|
|
+ private isAudioStationSongCoverId(value: string): boolean {
|
|
|
+ return value.startsWith('as-song:');
|
|
|
+ }
|
|
|
+
|
|
|
+ private stripAudioStationSongCoverId(value: string): string {
|
|
|
+ return value.replace(/^as-song:/, '');
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 记录账号缓存信息
|
|
|
*/
|
|
|
@@ -1440,6 +1988,12 @@ export struct NavidromePage {
|
|
|
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;
|
|
|
+ }
|
|
|
return { type: CommonConstants.TYPE_NAVIDROME, scheme: 'navidrome' } as LibraryInfo;
|
|
|
}
|
|
|
|
|
|
@@ -1836,6 +2390,12 @@ export struct NavidromePage {
|
|
|
} 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);
|
|
|
}
|
|
|
if (ticket !== this.searchTicket) {
|
|
|
return;
|
|
|
@@ -2310,12 +2870,6 @@ export struct NavidromePage {
|
|
|
throw new Error('媒体库账号不可用');
|
|
|
}
|
|
|
if (!this.isNavidromeAccount(account)) {
|
|
|
- if (this.filterType === NavFilterType.Playlist) {
|
|
|
- this.filterSongs = [];
|
|
|
- this.isFilterLoading = false;
|
|
|
- ToastUtil.showToast('当前媒体库不支持歌单');
|
|
|
- return;
|
|
|
- }
|
|
|
let restSongs: NavidromeRestSong[] = [];
|
|
|
if (this.filterType === NavFilterType.Artist) {
|
|
|
if (this.isJellyfinAccount(account)) {
|
|
|
@@ -2324,6 +2878,12 @@ export struct NavidromePage {
|
|
|
} 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.filterType === NavFilterType.Album) {
|
|
|
if (this.isJellyfinAccount(account)) {
|
|
|
@@ -2332,6 +2892,26 @@ export struct NavidromePage {
|
|
|
} 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.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 {
|
|
|
+ this.filterSongs = [];
|
|
|
+ this.isFilterLoading = false;
|
|
|
+ ToastUtil.showToast('当前媒体库不支持歌单');
|
|
|
+ return;
|
|
|
}
|
|
|
}
|
|
|
const videoItems = await this.convertSongsToVideoItems(restSongs, account);
|