Răsfoiți Sursa

feat(emby): 添加 Emby 服务器支持

- 实现了 EmbyApi 的 getAllSongs 方法获取所有音频文件
- 在 JellyfinApi 中添加了相同的 getAllSongs 方法
- 修改 RemoteDriveManager 使根目录直接显示所有歌曲而非艺术家列表
- 更新 WebDavMainPage 支持显示歌曲列表
- 在 LocalMusic 中添加 Emby 类型支持和播放链接构建
- 修复了进度对话框的显示和更新逻辑
- 添加了 Emby 类型的错误处理和清理逻辑
chendeben 7 luni în urmă
părinte
comite
3fc85831a8

+ 21 - 0
entry/src/main/ets/common/network/EmbyApi.ets

@@ -239,6 +239,27 @@ export class EmbyApi {
     return songs;
   }
 
+  async getAllSongs(account: WebDavAccount): Promise<EmbySong[]> {
+    const auth = await this.ensureAuth(account);
+    const params: Array<QueryParam> = [
+      new QueryParam('IncludeItemTypes', 'Audio'),
+      new QueryParam('Recursive', 'true'),
+      new QueryParam('SortBy', 'SortName'),
+      new QueryParam('SortOrder', 'Ascending'),
+      new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
+    ];
+    const response = await this.get<EmbyItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
+    const items = response.Items ?? [];
+    const songs: EmbySong[] = [];
+    for (let i = 0; i < items.length; i++) {
+      const song = this.toSong(items[i]);
+      if (song) {
+        songs.push(song);
+      }
+    }
+    return songs;
+  }
+
   async buildStreamUrl(account: WebDavAccount, itemId: string): Promise<string> {
     const baseUrl = this.buildBaseUrl(account);
     return `${baseUrl}/Audio/${encodeURIComponent(itemId)}/stream?static=true`;

+ 21 - 0
entry/src/main/ets/common/network/JellyfinApi.ets

@@ -206,6 +206,27 @@ export class JellyfinApi {
     return songs;
   }
 
+  async getAllSongs(account: WebDavAccount): Promise<JellyfinSong[]> {
+    const auth = await this.ensureAuth(account);
+    const params: Array<QueryParam> = [
+      new QueryParam('IncludeItemTypes', 'Audio'),
+      new QueryParam('Recursive', 'true'),
+      new QueryParam('SortBy', 'SortName'),
+      new QueryParam('SortOrder', 'Ascending'),
+      new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
+    ];
+    const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
+    const items = response.Items ?? [];
+    const songs: JellyfinSong[] = [];
+    for (let i = 0; i < items.length; i++) {
+      const song = this.toSong(items[i]);
+      if (song) {
+        songs.push(song);
+      }
+    }
+    return songs;
+  }
+
   async buildStreamUrl(account: WebDavAccount, itemId: string): Promise<string> {
     const auth = await this.ensureAuth(account);
     const baseUrl = this.buildBaseUrl(account);

+ 10 - 18
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -1730,15 +1730,11 @@ export class RemoteDriveManager {
     void ServerLogUtil.info(TAG, `浏览 Jellyfin 路径: ${normalized} account=${ServerLogUtil.sanitizeAccount(account)}`);
 
     if (normalized === '/' || segments.length === 0) {
-      const artists: JellyfinArtist[] = await this.jellyfinApi.getArtists(account);
-      artists.sort((a, b) => a.name.localeCompare(b.name));
-      this.webDavFiles = artists.map(artist => {
-        const info = this.createJellyfinDirectory(artist.name, `/artist/${artist.id}`);
-        this.registerPathLabel(info.href, artist.name);
-        return info;
-      });
-      this.webDavSongs = [];
-      void ServerLogUtil.info(TAG, `获取 Jellyfin 艺术家列表,数量: ${artists.length}`);
+      const songs = await this.jellyfinApi.getAllSongs(account);
+      this.webDavFiles = [];
+      this.webDavSongs = songs.map(song => this.buildJellyfinVideoItem(song, account, null, undefined));
+      await this.enrichSongsWithDatabase(this.webDavSongs);
+      void ServerLogUtil.info(TAG, `进入 Jellyfin 根目录,歌曲数: ${songs.length}`);
       return;
     }
 
@@ -1788,15 +1784,11 @@ export class RemoteDriveManager {
     void ServerLogUtil.info(TAG, `浏览 Emby 路径: ${normalized} account=${ServerLogUtil.sanitizeAccount(account)}`);
 
     if (normalized === '/' || segments.length === 0) {
-      const artists: EmbyArtist[] = await this.embyApi.getArtists(account);
-      artists.sort((a, b) => a.name.localeCompare(b.name));
-      this.webDavFiles = artists.map(artist => {
-        const info = this.createEmbyDirectory(artist.name, `/artist/${artist.id}`);
-        this.registerPathLabel(info.href, artist.name);
-        return info;
-      });
-      this.webDavSongs = [];
-      void ServerLogUtil.info(TAG, `获取 Emby 艺术家列表,数量: ${artists.length}`);
+      const songs = await this.embyApi.getAllSongs(account);
+      this.webDavFiles = [];
+      this.webDavSongs = songs.map(song => this.buildEmbyVideoItem(song, account, null, undefined));
+      await this.enrichSongsWithDatabase(this.webDavSongs);
+      void ServerLogUtil.info(TAG, `进入 Emby 根目录,歌曲数: ${songs.length}`);
       return;
     }
 

+ 1 - 1
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -1317,7 +1317,7 @@ export struct WebDavMainPage {
 
 
       // 文件列表(文件夹 + 歌曲)
-      if (this.webDavFiles.length > 0) {
+      if (this.webDavFiles.length > 0 || this.songs.length > 0) {
         List({ scroller: this.listScroller ,space: 0 }) {
           // 显示文件夹 - 只显示当前目录下的直接子文件夹
           ForEach(this.isSearchMode ? this.filteredFolderList : this.visibleFoldersState, (folder: FileInfo) => {

+ 39 - 9
entry/src/main/ets/view/LocalMusic.ets

@@ -90,6 +90,7 @@ import { RemoteCacheType, resolveCacheFilePath } from '../common/network/RemoteS
 import { findWebDavCacheIfExists, triggerWebDavCacheDownload } from '../common/network/WebDavFileCache';
 import { navidromeApi } from '../common/network/NavidromeApi';
 import { jellyfinApi } from '../common/network/JellyfinApi';
+import { embyApi } from '../common/network/EmbyApi';
 import { ServerLogUtil } from '../common/util/ServerLogUtil';
 import { ensureSmbFileStreaming, SmbStreamingMeta } from '../common/network/SmbFileCache';
 import { ensureBaiduFileCached } from '../common/network/BaiduFileCache';
@@ -176,6 +177,7 @@ function getTypeOrder(type: number) {
     case CommonConstants.TYPE_FTP:
     case CommonConstants.TYPE_BAIDU:
     case CommonConstants.TYPE_JELLYFIN:
+    case CommonConstants.TYPE_EMBY:
       return 3;
     default:
       return 4; // Unknown types, if any, go last
@@ -206,8 +208,12 @@ function isJellyfinType(type: number): boolean {
   return type === CommonConstants.TYPE_JELLYFIN;
 }
 
+function isEmbyType(type: number): boolean {
+  return type === CommonConstants.TYPE_EMBY;
+}
+
 function isRemoteCloudType(type: number): boolean {
-  return isWebDavType(type) || isSmbType(type) || isNavidromeType(type) || isFtpType(type) || isBaiduType(type) || isJellyfinType(type);
+  return isWebDavType(type) || isSmbType(type) || isNavidromeType(type) || isFtpType(type) || isBaiduType(type) || isJellyfinType(type) || isEmbyType(type);
 }
 
 function getShareNameFromFilePath(filePath?: string): string | undefined {
@@ -591,6 +597,25 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
     }
   }
 
+  if (isEmbyType(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('Emby账号不可用');
+      }
+      const embySongId = song.remote_rel_path || song.id || song.filePath;
+      const streamUrl = await embyApi.buildStreamUrl(account, embySongId);
+      void ServerLogUtil.info(TAG, `Emby 流地址构建成功: ${streamUrl}`);
+      void ServerLogUtil.debug(TAG, `播放songId=${embySongId}, account=${ServerLogUtil.sanitizeAccount(account)}`);
+      return sanitizePlaybackUrl(streamUrl);
+    } catch (error) {
+      const err = error as Error;
+      void ServerLogUtil.error(TAG, `Emby URL构建失败: ${err.message}`);
+      throw new Error(err.message);
+    }
+  }
+
   if (isSmbType(song.type) && song.webdav_account_id) {
     try {
       const manager = RemoteDriveManager.getInstance();
@@ -738,6 +763,9 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
   if (isJellyfinType(song.type)) {
     throw new Error('Jellyfin歌曲缺少webdav_account_id,无法构建播放链接');
   }
+  if (isEmbyType(song.type)) {
+    throw new Error('Emby歌曲缺少webdav_account_id,无法构建播放链接');
+  }
   Logger.info(TAG, `setVideoUrlForSong fallback直接返回原始路径: ${song.filePath}`);
   return song.filePath;
 }
@@ -1856,7 +1884,10 @@ export struct LocalMusic {
   }
 
   private closeLoadingProgressDialog(): void {
-    DialogHelper.closeLoading();
+    if (!this.loadingProgressDialogId) {
+      return;
+    }
+    DialogHelper.closeDialog(this.loadingProgressDialogId);
     this.loadingProgressDialogId = '';
   }
 
@@ -2928,14 +2959,13 @@ export struct LocalMusic {
 
     // 初始化进度条
     this.progress  = 0;
-    DialogHelper.showLoadingProgress({
+    this.loadingProgressDialogId = DialogHelper.showLoadingProgress({
       progress: this.progress,
       backCancel: false,
       autoCancel: false,
       loadColor: $r('app.color.title_bar_bg'),
       fontColor: $r('app.color.title_bar_bg')
     });
-    this.loadingProgressDialogId = 'dialog_progress';
 
     // 计算处理总数用于进度计算
     const totalItems = uris.length;
@@ -2963,14 +2993,14 @@ export struct LocalMusic {
         // 更新进度
         processedItems++;
         this.progress  = Math.floor((processedItems  / totalItems) * 100);
-        DialogHelper.updateLoading(` 正在处理 ${this.progress}%`, this.progress);
+        DialogHelper.updateLoading(this.loadingProgressDialogId, ` 正在处理 ${this.progress}%`, this.progress);
 
       } catch (error) {
         Logger.error(TAG,  'saveVideoDatas failed with err: ' + JSON.stringify(error));
         // 即使出错也更新进度
         processedItems++;
         this.progress  = Math.floor((processedItems  / totalItems) * 100);
-        DialogHelper.updateLoading(` 正在处理 ${this.progress}%`, this.progress);
+        DialogHelper.updateLoading(this.loadingProgressDialogId, ` 正在处理 ${this.progress}%`, this.progress);
       }
     }
     // 关闭进度条
@@ -2991,14 +3021,13 @@ export struct LocalMusic {
 
     let newUris: string[] = [];
     this.progress = 0
-    DialogHelper.showLoadingProgress({
+    this.loadingProgressDialogId = DialogHelper.showLoadingProgress({
       progress: this.progress,
       backCancel: false,
       autoCancel: false,
       loadColor: this.themeColor,
       fontColor: this.themeColor
     });
-    this.loadingProgressDialogId = 'dialog_progress';
     // 计算所有文件的总大小
     let totalSize = 0;
     for (let uri of uris) {
@@ -3030,7 +3059,7 @@ export struct LocalMusic {
 
           // 计算总进度
           this.progress = Math.floor((totalRead / totalSize) * 100);
-          DialogHelper.updateLoading(`正在导入 ${this.progress}%`, this.progress);
+          DialogHelper.updateLoading(this.loadingProgressDialogId, `正在导入 ${this.progress}%`, this.progress);
 
           len = await fileIo.read(sourceFile.fd, buffer);
         }
@@ -7169,6 +7198,7 @@ export struct LocalMusic {
       case CommonConstants.TYPE_FTP:
       case CommonConstants.TYPE_BAIDU:
       case CommonConstants.TYPE_JELLYFIN:
+      case CommonConstants.TYPE_EMBY:
         // 处理网络音频播放(WebDAV/SMB)
         Logger.info(`heanup 处理云端音频播放: ${item.name}, URL: ${item.filePath}`)