Explorar o código

增加道理鱼支持

chendeben hai 6 meses
pai
achega
6cc303f815

+ 1 - 0
entry/src/main/ets/common/constants/CommonConstants.ets

@@ -203,6 +203,7 @@ export class CommonConstants {
   static readonly TYPE_EMBY: number = 9;//Emby流媒体文件
   static readonly TYPE_AUDIOSTATION: number = 10;//AudioStation流媒体文件
   static readonly TYPE_PLEX: number = 11;//Plex流媒体文件
+  static readonly TYPE_DAOLIYU: number = 12;//道理鱼流媒体文件
 
   static readonly TYPE_LOCK: number = 200;//私密视频
   static readonly TYPE_LIKE: number = 201;//我收藏的视频

+ 1 - 0
entry/src/main/ets/common/enums/RemoteDriveType.ets

@@ -11,4 +11,5 @@ export enum RemoteDriveType {
   Emby = 9,
   AudioStation = 10,
   Plex = 11,
+  DaoLiYu = 12,
 }

+ 2 - 0
entry/src/main/ets/common/network/NavidromeRestApi.ets

@@ -91,6 +91,8 @@ export interface NavidromeRestPlaylist {
   duration?: number;
   size?: number;
   songCount?: number;
+  coverArtId?: string;
+  coverUrl?: string;
   ownerName?: string;
   ownerId?: string;
   public?: boolean;

+ 2 - 0
entry/src/main/ets/common/util/RemoteDriveLabel.ets

@@ -24,6 +24,8 @@ export function getRemoteDriveProtocolLabel(type?: number): string {
       return 'AudioStation';
     case RemoteDriveType.Plex:
       return 'Plex';
+    case RemoteDriveType.DaoLiYu:
+      return '道理鱼';
     case RemoteDriveType.Ftp:
       return 'FTP';
     case RemoteDriveType.Baidu:

+ 31 - 2
entry/src/main/ets/common/util/RemotePlayerUtil.ets

@@ -10,6 +10,7 @@ import { jellyfinApi } from '../network/JellyfinApi';
 import { embyApi } from '../network/EmbyApi';
 import { audioStationApi } from '../network/AudioStationApi';
 import { plexApi } from '../network/PlexApi';
+import { daoLiYuApi } from '../network/DaoLiYuApi';
 import { ensureNavidromeFileCached } from '../network/NavidromeFileCache';
 import { ensureJellyfinFileCached } from '../network/JellyfinFileCache';
 import { ensureEmbyFileCached } from '../network/EmbyFileCache';
@@ -829,6 +830,26 @@ export async function setVideoUrlForSong(
     }
   }
 
+  // DaoLiYu 类型处理
+  if (isDaoLiYuType(song.type) && song.webdav_account_id) {
+    try {
+      const manager = RemoteDriveManager.getInstance();
+      const account = await manager.getWebDavAccountById(song.webdav_account_id);
+      if (!account) {
+        throw new Error('道理鱼账号不可用');
+      }
+      const trackId = song.remote_rel_path || song.id || song.filePath;
+      const streamUrl = await daoLiYuApi.buildStreamUrl(account, trackId);
+      void ServerLogUtil.info(TAG, `道理鱼流地址构建成功: ${streamUrl}`);
+      void ServerLogUtil.debug(TAG, `播放songId=${trackId}, account=${ServerLogUtil.sanitizeAccount(account)}`);
+      return sanitizePlaybackUrl(streamUrl);
+    } catch (error) {
+      const err = error as Error;
+      void ServerLogUtil.error(TAG, `道理鱼 URL构建失败: ${err.message}`);
+      throw new Error(err.message);
+    }
+  }
+
   // SMB 类型处理
   if (isSmbType(song.type) && song.webdav_account_id) {
     try {
@@ -995,6 +1016,9 @@ export async function setVideoUrlForSong(
   if (isPlexType(song.type)) {
     throw new Error('Plex歌曲缺少webdav_account_id,无法构建播放链接');
   }
+  if (isDaoLiYuType(song.type)) {
+    throw new Error('道理鱼歌曲缺少webdav_account_id,无法构建播放链接');
+  }
 
   Logger.info(TAG, `setVideoUrlForSong fallback直接返回原始路径: ${song.filePath}`);
   return song.filePath;
@@ -1032,7 +1056,7 @@ function resolveRemoteStoragePathForMetadata(item: VideoItem): string | null {
     return relative && relative.length > 0 ? relative : null;
   }
   if (isNavidromeType(item.type) || isJellyfinType(item.type) || isEmbyType(item.type) || isAudioStationType(item.type)
-    || isPlexType(item.type) || isBaiduType(item.type)) {
+    || isPlexType(item.type) || isDaoLiYuType(item.type) || isBaiduType(item.type)) {
     return item.remote_rel_path || item.id || item.filePath || null;
   }
   return item.filePath || null;
@@ -1057,6 +1081,7 @@ export function getTypeOrder(type: number) {
     case CommonConstants.TYPE_EMBY:
     case CommonConstants.TYPE_AUDIOSTATION:
     case CommonConstants.TYPE_PLEX:
+    case CommonConstants.TYPE_DAOLIYU:
       return 3;
     default:
       return 4; // Unknown types, if any, go last
@@ -1101,9 +1126,13 @@ export function isPlexType(type: number | undefined): boolean {
   return type !== undefined && type === CommonConstants.TYPE_PLEX;
 }
 
+export function isDaoLiYuType(type: number | undefined): boolean {
+  return type !== undefined && type === CommonConstants.TYPE_DAOLIYU;
+}
+
 export function isRemoteCloudType(type: number): boolean {
   return isWebDavType(type) || isSmbType(type) || isNavidromeType(type) || isFtpType(type) || isBaiduType(type)
-    || isJellyfinType(type) || isEmbyType(type) || isAudioStationType(type) || isPlexType(type);
+    || isJellyfinType(type) || isEmbyType(type) || isAudioStationType(type) || isPlexType(type) || isDaoLiYuType(type);
 }
 
 export function getShareNameFromFilePath(filePath?: string): string | undefined {

+ 45 - 2
entry/src/main/ets/dialog/RemoteDriveAccountDialog.ets

@@ -459,7 +459,8 @@ export struct RemoteDriveAccountDialog {
         || this.driveType === RemoteDriveType.Jellyfin
         || this.driveType === RemoteDriveType.Emby
         || this.driveType === RemoteDriveType.AudioStation
-        || this.driveType === RemoteDriveType.Plex) {
+        || this.driveType === RemoteDriveType.Plex
+        || this.driveType === RemoteDriveType.DaoLiYu) {
         Row({ space: 12 }) {
           Text('启用HTTPS')
             .fontSize(14)
@@ -473,7 +474,8 @@ export struct RemoteDriveAccountDialog {
                   || this.driveType === RemoteDriveType.Jellyfin
                   || this.driveType === RemoteDriveType.Emby
                   || this.driveType === RemoteDriveType.AudioStation
-                  || this.driveType === RemoteDriveType.Plex)) {
+                  || this.driveType === RemoteDriveType.Plex
+                  || this.driveType === RemoteDriveType.DaoLiYu)) {
                 this.updatePortState(this.getDefaultPort(), false);
               }
             });
@@ -565,6 +567,7 @@ export struct RemoteDriveAccountDialog {
         this.buildTypeButton('Emby', RemoteDriveType.Emby);
         this.buildTypeButton('AudioStation', RemoteDriveType.AudioStation);
         this.buildTypeButton('Plex', RemoteDriveType.Plex);
+        this.buildTypeButton('道理鱼', RemoteDriveType.DaoLiYu);
         this.buildTypeButton('FTP', RemoteDriveType.Ftp);
         this.buildTypeButton('百度网盘', RemoteDriveType.Baidu);
       }
@@ -653,6 +656,8 @@ export struct RemoteDriveAccountDialog {
         return '新建AudioStation';
       case RemoteDriveType.Plex:
         return '新建Plex';
+      case RemoteDriveType.DaoLiYu:
+        return '新建道理鱼';
       case RemoteDriveType.Ftp:
         return '新建FTP';
       case RemoteDriveType.Baidu:
@@ -680,6 +685,8 @@ export struct RemoteDriveAccountDialog {
         return '可粘贴 AudioStation 连接(如 https://user:pass@host:5001),自动填充参数';
       case RemoteDriveType.Plex:
         return '可粘贴 Plex 连接(如 http://user:pass@host:32400),自动填充参数';
+      case RemoteDriveType.DaoLiYu:
+        return '可粘贴道理鱼连接(如 http://email:password@host:5173),自动填充参数';
       case RemoteDriveType.Ftp:
         return '支持 ftp://user:pass@host:21/path 输入,自动填充账户和目录';
       case RemoteDriveType.Baidu:
@@ -712,6 +719,9 @@ export struct RemoteDriveAccountDialog {
     if (this.driveType === RemoteDriveType.Plex) {
       return 32400;
     }
+    if (this.driveType === RemoteDriveType.DaoLiYu) {
+      return this.enableHttps ? 443 : 5173;
+    }
     if (this.driveType === RemoteDriveType.Ftp) {
       return 21;
     }
@@ -771,6 +781,11 @@ export struct RemoteDriveAccountDialog {
       return true;
     }
     if (protocol === 'http' || protocol === 'https') {
+      if (Number(this.driveType) === RemoteDriveType.DaoLiYu) {
+        this.applyParsedDaoLiYu(parsed);
+        ToastUtil.showToast('已解析道理鱼连接');
+        return true;
+      }
       if (Number(this.driveType) === RemoteDriveType.Jellyfin) {
         this.applyParsedJellyfin(parsed);
         ToastUtil.showToast('已解析 Jellyfin 连接');
@@ -813,6 +828,15 @@ export struct RemoteDriveAccountDialog {
         ToastUtil.showToast('已解析 Emby 连接');
         return true;
       }
+      const looksDaoLiYu = parsed.port === 5173 || parsed.path?.includes('/api');
+      if (looksDaoLiYu) {
+        if (Number(this.driveType) !== RemoteDriveType.DaoLiYu) {
+          this.handleDriveTypeChange(RemoteDriveType.DaoLiYu);
+        }
+        this.applyParsedDaoLiYu(parsed);
+        ToastUtil.showToast('已解析道理鱼连接');
+        return true;
+      }
       if (this.driveType !== RemoteDriveType.WebDav) {
         this.handleDriveTypeChange(RemoteDriveType.WebDav);
       }
@@ -848,6 +872,23 @@ export struct RemoteDriveAccountDialog {
     this.filepath = parsed.path?.length ? parsed.path : '/';
   }
 
+  private applyParsedDaoLiYu(parsed: ParsedConnectionParts): void {
+    this.enableHttps = parsed.protocol === 'https';
+    this.host = parsed.host;
+    if (parsed.port) {
+      this.updatePortState(parsed.port, true);
+    } else {
+      this.updatePortForType(RemoteDriveType.DaoLiYu, true);
+    }
+    if (parsed.username) {
+      this.username = parsed.username;
+    }
+    if (parsed.password) {
+      this.password = parsed.password;
+    }
+    this.filepath = '/';
+  }
+
   private applyParsedSmb(parsed: ParsedConnectionParts): void {
     this.host = parsed.host;
     if (parsed.port) {
@@ -1544,6 +1585,8 @@ export function getCloudDiskIcon(type: number): ResourceStr {
       return $r('app.media.audio_Station');
     case RemoteDriveType.Plex:
       return $r('app.media.plex');
+    case RemoteDriveType.DaoLiYu:
+      return $r('app.media.music_group');
     case RemoteDriveType.Baidu:
       return $r('app.media.baiduwp');
     case RemoteDriveType.ALi:

+ 15 - 16
entry/src/main/ets/pages/NewIndex.ets

@@ -165,7 +165,8 @@ struct NewIndex {
       // 网盘选项卡
       if(this.webDavAccounts.length>0&&(this.webDavAccounts[0].webType===RemoteDriveType.Navidrome||
         this.webDavAccounts[0].webType===RemoteDriveType.Jellyfin||
-        this.webDavAccounts[0].webType===RemoteDriveType.Emby)){
+        this.webDavAccounts[0].webType===RemoteDriveType.Emby||
+        this.webDavAccounts[0].webType===RemoteDriveType.DaoLiYu)){
         this.mType = 7
       }else{
         this.mType = 6
@@ -360,7 +361,8 @@ struct NewIndex {
         console.info('onecold 网盘 默认首页2 this.webDavAccounts[0].webType='+this.webDavAccounts[0].webType )
         if(this.webDavAccounts[0].webType===RemoteDriveType.Navidrome||
           this.webDavAccounts[0].webType===RemoteDriveType.Jellyfin||
-          this.webDavAccounts[0].webType===RemoteDriveType.Emby){
+          this.webDavAccounts[0].webType===RemoteDriveType.Emby||
+          this.webDavAccounts[0].webType===RemoteDriveType.DaoLiYu){
           this.mType = 7
           this.selectedAccount = new WebDavAccount()
           this.selectWebDavAccount(this.webDavAccounts[0])
@@ -1653,6 +1655,13 @@ struct NewIndex {
         .onClick(async () => {
           this.showRemoteDriveAccountDialog(false, undefined, RemoteDriveType.Plex)
         })
+      MenuItem({
+        startIcon: $r('app.media.music_group'),
+        content: '道理鱼'
+      })
+        .onClick(async () => {
+          this.showRemoteDriveAccountDialog(false, undefined, RemoteDriveType.DaoLiYu)
+        })
       MenuItem({
         startIcon: $r('app.media.baiduwp'),
         content: '百度网盘'
@@ -1705,18 +1714,7 @@ struct NewIndex {
         content: '置顶'
       })
         .onClick(async() => {
-          try {
-            await this.webdavManager.pinAccount(account)
-            // 重新加载网盘账户列表以更新UI排序
-            await this.loadWebDavAccounts()
-            ToastUtil.showToast('置顶成功')
-            LogUtil.info('heanup NewIndex', '网盘账户置顶成功:', account.name)
-            this.logAccount('info', `网盘账户置顶成功: ${account.name}`)
-          } catch (error) {
-            LogUtil.error('heanup NewIndex', `置顶网盘账户失败: ${(error as Error).message}`)
-            ToastUtil.showToast('置顶失败')
-            this.logAccount('error', `置顶网盘账户失败: ${(error as Error).message}`)
-          }
+          ToastUtil.showToast('当前版本暂不支持置顶')
         })
 
       MenuItem({
@@ -2017,9 +2015,10 @@ struct NewIndex {
         || account.webType === RemoteDriveType.Jellyfin
         || account.webType === RemoteDriveType.Emby
         || account.webType === RemoteDriveType.AudioStation
-        || account.webType === RemoteDriveType.Plex) {
+        || account.webType === RemoteDriveType.Plex
+        || account.webType === RemoteDriveType.DaoLiYu) {
         this.mType = 7
-        LogUtil.info('heanup NewIndex', `切换到 Navidrome/Jellyfin/Emby/AudioStation/Plex 页面,webType=${account.webType}`)
+        LogUtil.info('heanup NewIndex', `切换到 Navidrome/Jellyfin/Emby/AudioStation/Plex/道理鱼 页面,webType=${account.webType}`)
       } else {
         this.mType = 6
         LogUtil.info('heanup NewIndex', `切换到 WebDAV 页面,webType=${account.webType}`)

+ 26 - 2
entry/src/main/ets/view/LocalMusic.ets

@@ -6989,7 +6989,8 @@ export struct LocalMusic {
       case CommonConstants.TYPE_EMBY:
       case CommonConstants.TYPE_AUDIOSTATION:
       case CommonConstants.TYPE_PLEX:
-        // 处理网络音频播放(WebDAV/SMB/Navidrome/Jellyfin/Emby/AudioStation/Plex/Baidu)
+      case CommonConstants.TYPE_DAOLIYU:
+        // 处理网络音频播放(WebDAV/SMB/Navidrome/Jellyfin/Emby/AudioStation/Plex/道理鱼/Baidu)
         Logger.info(`heanup 处理云端音频播放: ${item.name}, URL: ${item.filePath}`)
 
         if (this.CONTROL_PlayStatus !== PlayStatus.INIT) {
@@ -13561,6 +13562,23 @@ export struct LocalMusic {
     }
   }
 
+  private parseDurationToMs(duration?: string): number {
+    if (!duration) {
+      return 0;
+    }
+    const parts = duration.split(':').map((item) => Number(item));
+    if (parts.some((item) => Number.isNaN(item))) {
+      return 0;
+    }
+    if (parts.length === 2) {
+      return Math.floor((parts[0] * 60 + parts[1]) * 1000);
+    }
+    if (parts.length === 3) {
+      return Math.floor((parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000);
+    }
+    return 0;
+  }
+
   private stringForTime(timeMs: number): string {
     let totalSeconds: number | string = (timeMs / 1000);
     let newSeconds: number | string = totalSeconds % 60;
@@ -14003,7 +14021,13 @@ export struct LocalMusic {
               }
             }
           } else {
-            fetchDurationFallback();
+            const metaDurationMs = this.parseDurationToMs(this.currentSong?.duration);
+            if (metaDurationMs > 0) {
+              Logger.info(TAG, `heanup 使用元数据时长: ${metaDurationMs}ms`);
+              applyDuration(metaDurationMs);
+            } else {
+              fetchDurationFallback();
+            }
           }
 
           setTimeout(() => {

+ 258 - 4
entry/src/main/ets/view/NavidromePage.ets

@@ -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;
-}
+}