|
|
@@ -27,6 +27,7 @@ import { jellyfinApi, JellyfinAlbum, JellyfinArtist, JellyfinSong } from '../com
|
|
|
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';
|
|
|
@@ -185,6 +186,10 @@ export struct NavidromePage {
|
|
|
return account.webType === RemoteDriveType.Plex;
|
|
|
}
|
|
|
|
|
|
+ private isDaoLiYuAccount(account: WebDavAccount): boolean {
|
|
|
+ return account.webType === RemoteDriveType.DaoLiYu;
|
|
|
+ }
|
|
|
+
|
|
|
private createTabOptions(): SegmentButtonOptions {
|
|
|
const buttons = [
|
|
|
{ text: '全部' },
|
|
|
@@ -332,7 +337,8 @@ export struct NavidromePage {
|
|
|
&& !this.isJellyfinAccount(this.selectedAccount)
|
|
|
&& !this.isEmbyAccount(this.selectedAccount)
|
|
|
&& !this.isAudioStationAccount(this.selectedAccount)
|
|
|
- && !this.isPlexAccount(this.selectedAccount)) {
|
|
|
+ && !this.isPlexAccount(this.selectedAccount)
|
|
|
+ && !this.isDaoLiYuAccount(this.selectedAccount)) {
|
|
|
return undefined;
|
|
|
}
|
|
|
if (!this.selectedAccount.host || this.selectedAccount.host.length === 0) {
|
|
|
@@ -382,6 +388,10 @@ export struct NavidromePage {
|
|
|
}
|
|
|
if (this.isPlexAccount(account)) {
|
|
|
await this.loadPlexLibrary(account);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.isDaoLiYuAccount(account)) {
|
|
|
+ await this.loadDaoLiYuLibrary(account);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -639,6 +649,38 @@ export struct NavidromePage {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private async loadDaoLiYuLibrary(account: WebDavAccount): Promise<void> {
|
|
|
+ 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<void> {
|
|
|
if (this.songNextStart === null || this.isSongPageLoading) {
|
|
|
return;
|
|
|
@@ -1115,6 +1157,99 @@ export struct NavidromePage {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private async loadNextDaoLiYuSongPage(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 daoLiYuApi.getTracksPage(account, this.songNextStart, this.REMOTE_PAGE_SIZE);
|
|
|
+ if (currentTicket !== this.loadTicket) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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}`);
|
|
|
+ } finally {
|
|
|
+ this.isSongPageLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async loadNextDaoLiYuArtistPage(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 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<void> {
|
|
|
+ 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<void> {
|
|
|
+ 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<void> {
|
|
|
if (this.playlistNextStart === null || this.isPlaylistPageLoading) {
|
|
|
return;
|
|
|
@@ -1237,6 +1372,25 @@ export struct NavidromePage {
|
|
|
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;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1511,6 +1665,63 @@ export struct NavidromePage {
|
|
|
return results;
|
|
|
}
|
|
|
|
|
|
+ private async convertDaoLiYuArtistsToRest(artists: DaoLiYuArtist[], account: WebDavAccount): Promise<NavidromeRestArtist[]> {
|
|
|
+ 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<NavidromeRestAlbum[]> {
|
|
|
+ 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<NavidromeRestPlaylist[]> {
|
|
|
+ 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 = {
|
|
|
@@ -1601,6 +1812,31 @@ export struct NavidromePage {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ 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 = {
|
|
|
@@ -1890,6 +2126,9 @@ export struct NavidromePage {
|
|
|
} else if (this.isPlexAccount(account)) {
|
|
|
void ServerLogUtil.debug('NavidromePageCover', `调用 plexApi.buildImageUrl - coverId: ${normalizedId}`);
|
|
|
url = plexApi.buildImageUrl(account, normalizedId);
|
|
|
+ } else if (this.isDaoLiYuAccount(account)) {
|
|
|
+ void ServerLogUtil.debug('NavidromePageCover', `调用 daoLiYuApi.buildImageUrl - coverId: ${normalizedId}`);
|
|
|
+ url = daoLiYuApi.buildImageUrl(account, normalizedId);
|
|
|
}
|
|
|
|
|
|
if (url && this.isNavidromeAccount(account)) {
|
|
|
@@ -1994,6 +2233,9 @@ export struct NavidromePage {
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
@@ -2396,6 +2638,9 @@ export struct NavidromePage {
|
|
|
} 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;
|
|
|
@@ -2675,14 +2920,14 @@ export struct NavidromePage {
|
|
|
buildPlaylistItem(playlist: NavidromeRestPlaylist) {
|
|
|
Button({ type: ButtonType.Capsule, stateEffect: true }) {
|
|
|
Row({ space: 12 }) {
|
|
|
- Image($r('app.media.nocover'))
|
|
|
+ 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(this.themeColor)
|
|
|
+ .fillColor(playlist.coverUrl ? undefined : this.themeColor)
|
|
|
.objectFit(ImageFit.Cover)
|
|
|
.margin({ left: 20 })
|
|
|
|
|
|
@@ -2884,6 +3129,9 @@ export struct NavidromePage {
|
|
|
} else if (this.isPlexAccount(account)) {
|
|
|
const songs = await this.fetchAllPlexArtistSongs(account, this.filterId);
|
|
|
restSongs = this.convertPlexSongsToRestSongs(songs);
|
|
|
+ } else if (this.isDaoLiYuAccount(account)) {
|
|
|
+ const songs = await daoLiYuApi.getTracksByArtistId(account, this.filterId);
|
|
|
+ restSongs = this.convertDaoLiYuSongsToRestSongs(songs);
|
|
|
}
|
|
|
} else if (this.filterType === NavFilterType.Album) {
|
|
|
if (this.isJellyfinAccount(account)) {
|
|
|
@@ -2899,6 +3147,9 @@ export struct NavidromePage {
|
|
|
} else if (this.isPlexAccount(account)) {
|
|
|
const songs = await plexApi.getAlbumSongs(account, this.filterId);
|
|
|
restSongs = this.convertPlexSongsToRestSongs(songs);
|
|
|
+ } else if (this.isDaoLiYuAccount(account)) {
|
|
|
+ const songs = await daoLiYuApi.getTracksByAlbumId(account, this.filterId);
|
|
|
+ restSongs = this.convertDaoLiYuSongsToRestSongs(songs);
|
|
|
}
|
|
|
} else if (this.filterType === NavFilterType.Playlist) {
|
|
|
if (this.isAudioStationAccount(account)) {
|
|
|
@@ -2907,6 +3158,9 @@ export struct NavidromePage {
|
|
|
} 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;
|
|
|
@@ -3412,4 +3666,4 @@ async function loadNavidromeDataTask(accountData: AccountData, ticket: number):
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
-}
|
|
|
+}
|