Ver Fonte

feat(baidu): 实现百度网盘隐式授权和播放优化

- 替换设备码授权流程为隐式授权方式,提升用户体验
- 新增百度网盘dlink缓存机制,减少重复请求
- 实现百度网盘文件预取功能,提高播放响应速度
- 优化百度网盘播放请求头设置,增强兼容性
- 调整百度网盘播放器配置参数,改善播放稳定性
- 完善百度网盘授权过期处理逻辑
- 移除旧的设备码相关接口和状态管理代码
chendeben há 8 meses atrás
pai
commit
ff83dc463c

+ 4 - 3
entry/src/main/ets/common/constants/BaiduConstants.ets

@@ -4,10 +4,11 @@ export class BaiduConstants {
   static readonly SECRET_KEY: string = 'I1P0fp2k84zE2AIjn9pa6HH0AoY5pT2G';
   static readonly SIGN_KEY: string = '1~ml7QjPapdU$jSLR0Bop2rC3V9TyV5v';
   static readonly USER_AGENT: string = 'pan.baidu.com';
-  static readonly DEVICE_CODE_URL: string = 'https://openapi.baidu.com/oauth/2.0/device/code';
+  static readonly AUTHORIZE_URL: string = 'https://openapi.baidu.com/oauth/2.0/authorize';
   static readonly TOKEN_URL: string = 'https://openapi.baidu.com/oauth/2.0/token';
   static readonly OPENAPI_BASE: string = 'https://openapi.baidu.com';
   static readonly PAN_BASE: string = 'https://pan.baidu.com';
-  static readonly DEVICE_AUTH_SCOPE: string = 'basic,netdisk';
-  static readonly DEFAULT_POLL_INTERVAL: number = 5; // seconds
+  static readonly AUTH_SCOPE: string = 'basic,netdisk';
+  static readonly OOB_REDIRECT_PARAM: string = 'oob';
+  static readonly LOGIN_SUCCESS_URL: string = 'http://openapi.baidu.com/oauth/2.0/login_success';
 }

+ 0 - 33
entry/src/main/ets/common/network/BaiduPanClient.ets

@@ -56,15 +56,6 @@ async function httpGet(url: string, params: QueryParamEntry[]): Promise<string>
   }
 }
 
-export interface BaiduDeviceCodeResponse {
-  device_code: string;
-  user_code: string;
-  verification_url: string;
-  qrcode_url: string;
-  expires_in: number;
-  interval: number;
-}
-
 export interface BaiduTokenResponse {
   access_token: string;
   refresh_token: string;
@@ -116,30 +107,6 @@ async function parseJson<T>(payload: string): Promise<T> {
   }
 }
 
-export async function requestDeviceCode(): Promise<BaiduDeviceCodeResponse> {
-  const payload = await httpGet(BaiduConstants.DEVICE_CODE_URL, [
-    { key: 'response_type', value: 'device_code' },
-    { key: 'client_id', value: BaiduConstants.APP_KEY },
-    { key: 'scope', value: BaiduConstants.DEVICE_AUTH_SCOPE }
-  ]);
-  return parseJson<BaiduDeviceCodeResponse>(payload);
-}
-
-export async function pollAccessToken(deviceCode: string): Promise<BaiduTokenResponse | BaiduTokenError> {
-  const payload = await httpGet(BaiduConstants.TOKEN_URL, [
-    { key: 'grant_type', value: 'device_token' },
-    { key: 'code', value: deviceCode },
-    { key: 'client_id', value: BaiduConstants.APP_KEY },
-    { key: 'client_secret', value: BaiduConstants.SECRET_KEY }
-  ]);
-  const result = await parseJson<BaiduTokenResponse | BaiduTokenError>(payload);
-  const errorResult = result as BaiduTokenError;
-  if (errorResult.error) {
-    return errorResult;
-  }
-  return result as BaiduTokenResponse;
-}
-
 export async function refreshAccessToken(refreshToken: string): Promise<BaiduTokenResponse> {
   const payload = await httpGet(BaiduConstants.TOKEN_URL, [
     { key: 'grant_type', value: 'refresh_token' },

+ 99 - 6
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -23,7 +23,7 @@ import { WebDavUrlUtil } from './WebDavUrlUtil';
 import MediaTable from './MediaTable';
 import { FtpClient, FileInfo as FtpEntryInfo, StringEncoding, AccessOptions } from '@liuzhosoft/ftp4h';
 import { BaiduConstants } from '../constants/BaiduConstants';
-import { appendAccessTokenToDlink, BaiduListEntry, fetchFileMetas as fetchBaiduFileMetas, listDirectory as listBaiduDirectory, refreshAccessToken as refreshBaiduAccessToken } from '../network/BaiduPanClient';
+import { appendAccessTokenToDlink, BaiduListEntry, BaiduFileMeta, fetchFileMetas as fetchBaiduFileMetas, listDirectory as listBaiduDirectory, refreshAccessToken as refreshBaiduAccessToken } from '../network/BaiduPanClient';
 import { ServerLogUtil } from './ServerLogUtil';
 
 const TAG = 'heanup RemoteDriveManager';
@@ -33,6 +33,12 @@ export interface BreadcrumbItem {
   path: string;
 }
 
+interface BaiduDlinkCacheEntry {
+  url: string;
+  token: string;
+  expireAt: number;
+}
+
 // WebDAV认证信息接口
 export interface WebDavAuthInfo {
   headers: Record<string, string>;
@@ -75,6 +81,8 @@ export class RemoteDriveManager {
   public webDavAccounts: WebDavAccount[] = [];
   public webDavSongs: VideoItem[] = [];
   public webDavFiles: FileInfo[] = [];  // 当前目录的所有文件(包括文件夹)
+  private static readonly BAIDU_DLINK_CACHE_TTL: number = 10 * 60 * 1000; // 10分钟
+  private baiduDlinkCache: Map<string, BaiduDlinkCacheEntry> = new Map();
 
   // 路径导航
   public currentPath: string = '';  // 当前浏览的路径
@@ -339,6 +347,22 @@ export class RemoteDriveManager {
       }
     }
 
+    if (currentSong.type === CommonConstants.TYPE_BAIDU) {
+      headers.set('User-Agent', BaiduConstants.USER_AGENT);
+      headers.set('Accept', '*/*');
+      headers.set('Connection', 'Keep-Alive');
+      headers.set('Referer', 'https://pan.baidu.com/disk/home');
+      headers.set('Range', 'bytes=0-');
+      headers.set('Accept-Encoding', 'identity');
+      headers.set('Pragma', 'no-cache');
+      headers.set('Cache-Control', 'no-cache');
+      const headerSnapshot: Record<string, string> = {};
+      headers.forEach((value, key) => {
+        headerSnapshot[key] = value;
+      });
+      Logger.info(TAG, `构建百度请求头: ${JSON.stringify(headerSnapshot)}`);
+    }
+
     return headers;
   }
 
@@ -1146,6 +1170,42 @@ export class RemoteDriveManager {
     }
     await this.enrichSongsWithDatabase(this.webDavSongs);
     Logger.info(TAG, `从百度网盘获取到 ${entries.length} 个文件/文件夹`);
+    this.scheduleBaiduPrefetch(account, accessToken, entries);
+    Logger.info(TAG, `百度预取任务已调度, account=${account.id}, 文件数=${entries.length}`);
+  }
+
+  private scheduleBaiduPrefetch(account: WebDavAccount, accessToken: string, entries: BaiduListEntry[]): void {
+    const fileEntries = entries.filter(entry => entry.isdir === 0 && this.isAudioFile(entry.server_filename));
+    if (fileEntries.length === 0) {
+      return;
+    }
+    const fsIds = fileEntries.map(entry => entry.fs_id.toString());
+    const chunkSize = 50;
+    const prefetch = async () => {
+      Logger.info(TAG, `开始预取百度dlink,文件数: ${fsIds.length}`);
+      Logger.info(TAG, `Baidu dlink prefetch start: account=${account.id} files=${fsIds.length}`);
+      for (let i = 0; i < fsIds.length; i += chunkSize) {
+        const chunk = fsIds.slice(i, i + chunkSize);
+        try {
+          const metas = await fetchBaiduFileMetas(accessToken, chunk);
+          metas.forEach((meta: BaiduFileMeta) => {
+            if (!meta.dlink) {
+              return;
+            }
+            const rawLink = meta.dlink.startsWith('https://') ? meta.dlink : meta.dlink.replace('http://', 'https://');
+            const finalUrl = appendAccessTokenToDlink(rawLink, accessToken);
+            this.cacheBaiduDlink(account, meta.fs_id.toString(), accessToken, finalUrl);
+          });
+          Logger.info(TAG, `Baidu dlink prefetch chunk success account=${account.id} size=${chunk.length}`);
+        } catch (error) {
+          const err = error as Error;
+          Logger.warn(TAG, `百度dlink预取失败: ${err.message}`);
+          break;
+        }
+      }
+      Logger.info(TAG, `Baidu dlink prefetch finished account=${account.id}`);
+    };
+    void prefetch();
   }
 
   private async loadNavidromeFiles(account: WebDavAccount, fullPath: string): Promise<void> {
@@ -1479,12 +1539,13 @@ export class RemoteDriveManager {
   }
 
   private async ensureBaiduAccessToken(account: WebDavAccount): Promise<string> {
-    if (account.baiduAccessToken && account.baiduTokenExpiresAt &&
-      (account.baiduTokenExpiresAt - Date.now()) > 60 * 1000) {
-      return account.baiduAccessToken;
+    if (account.baiduAccessToken) {
+      if (!account.baiduTokenExpiresAt || (account.baiduTokenExpiresAt - Date.now()) > 60 * 1000) {
+        return account.baiduAccessToken;
+      }
     }
     if (!account.baiduRefreshToken) {
-      throw new Error('百度网盘账户尚未授权');
+      throw new Error('百度网盘授权已过期,请在账户设置中重新登录百度网盘');
     }
     const token = await refreshBaiduAccessToken(account.baiduRefreshToken);
     account.baiduAccessToken = token.access_token;
@@ -1508,17 +1569,49 @@ export class RemoteDriveManager {
     await this.dataBaseUtil.updateData(this.webDavTable, values, predicates);
   }
 
+  private getBaiduCacheKey(account: WebDavAccount, fsId: string): string {
+    return `${account.id ?? '0'}_${fsId}`;
+  }
+
+  private cacheBaiduDlink(account: WebDavAccount, fsId: string, token: string, url: string): void {
+    const key = this.getBaiduCacheKey(account, fsId);
+    const expireAt = Date.now() + RemoteDriveManager.BAIDU_DLINK_CACHE_TTL;
+    this.baiduDlinkCache.set(key, { url, token, expireAt });
+  }
+
+  private getCachedBaiduDlink(account: WebDavAccount, fsId: string, token: string): string | null {
+    const key = this.getBaiduCacheKey(account, fsId);
+    const cached = this.baiduDlinkCache.get(key);
+    if (!cached) {
+      return null;
+    }
+    if (cached.token !== token || cached.expireAt <= Date.now()) {
+      this.baiduDlinkCache.delete(key);
+      return null;
+    }
+    return cached.url;
+  }
+
   public async getBaiduDownloadUrl(account: WebDavAccount, song: VideoItem): Promise<string> {
     const accessToken = await this.ensureBaiduAccessToken(account);
     const fsId = song.baiduFsId || song.id;
     if (!fsId) {
       throw new Error('缺少百度网盘文件fs_id');
     }
+    const cached = this.getCachedBaiduDlink(account, fsId, accessToken);
+    if (cached) {
+      Logger.info(TAG, `Baidu dlink cache hit, fsId=${fsId}`);
+      return cached;
+    }
     const metas = await fetchBaiduFileMetas(accessToken, [fsId]);
     if (!metas || metas.length === 0 || !metas[0].dlink) {
       throw new Error('无法获取下载链接');
     }
-    return appendAccessTokenToDlink(metas[0].dlink, accessToken);
+    const rawLink = metas[0].dlink.startsWith('https://') ? metas[0].dlink : metas[0].dlink.replace('http://', 'https://');
+    const urlWithToken = appendAccessTokenToDlink(rawLink, accessToken);
+    Logger.info(TAG, `Baidu dlink ready, fsId=${fsId} url=${urlWithToken}`);
+    this.cacheBaiduDlink(account, fsId, accessToken, urlWithToken);
+    return urlWithToken;
   }
 
   private smbEntryToFileInfo(entry: SmbDirectoryEntry, basePath: string): FileInfo {

+ 206 - 135
entry/src/main/ets/dialog/RemoteDriveAccountDialog.ets

@@ -6,7 +6,7 @@ import { ImagePickerUtil } from '../common/util/ImagePickerUtil';
 import Logger from '../common/util/Logger';
 import { ConfigurationConstant, Context } from '@kit.AbilityKit';
 import { BaiduConstants } from '../common/constants/BaiduConstants';
-import { BaiduTokenError, BaiduTokenResponse, pollAccessToken, requestDeviceCode } from '../common/network/BaiduPanClient';
+import { webview } from '@kit.ArkWeb';
 
 interface ParsedConnectionParts {
   protocol: string;
@@ -52,13 +52,13 @@ export struct RemoteDriveAccountDialog {
   @State coverPath: string = '';
   @State isDarkMode: boolean = false;
   @State ftpEncoding: string = 'utf-8';
-  @State baiduUserCode: string = '';
-  @State baiduQrUrl: string = '';
-  @State baiduVerificationUrl: string = '';
   @State baiduAuthStatus: string = '';
   @State baiduAccessToken: string = '';
   @State baiduRefreshToken: string = '';
   @State baiduTokenExpiresAt: number = 0;
+  @State showBaiduAuthDialog: boolean = false;
+  @State baiduAuthorizeUrl: string = '';
+  @State baiduAuthProgress: number = 0;
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
   @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
     ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
@@ -67,9 +67,8 @@ export struct RemoteDriveAccountDialog {
   private nameCustomized: boolean = false;
   private portCustomized: boolean = false;
   private navBasePathCustomized: boolean = false;
-  private baiduDeviceCode?: string;
-  private baiduDeviceCodeExpireAt: number = 0;
-  private baiduPollingTimer?: number;
+  private baiduAuthStateToken: string = '';
+  private baiduWebController: webview.WebviewController = new webview.WebviewController();
 
   onColorModeChange() {
     this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
@@ -113,17 +112,24 @@ export struct RemoteDriveAccountDialog {
   }
 
   aboutToDisappear(): void {
-    this.stopBaiduPolling();
+    this.closeBaiduAuthDialog();
   }
 
   build() {
-    Scroll(){
-      this.contentBuilder()
+    Stack() {
+      Scroll() {
+        this.contentBuilder();
+      }
+      .height('100%')
+      .width('100%')
+      .scrollBar(BarState.Off);
+
+      if (this.showBaiduAuthDialog) {
+        this.buildBaiduAuthDialogOverlay();
+      }
     }
     .height('100%')
-    .width('100%')
-    .scrollBar(BarState.Off)
-
+    .width('100%');
   }
 
   @Builder
@@ -355,53 +361,54 @@ export struct RemoteDriveAccountDialog {
       .visibility(this.driveType === RemoteDriveType.Navidrome? Visibility.None:Visibility.Visible)
       // this.buildHelperText('从共享根开始的路径,例如 /music 或 /音乐/歌单1')
 
-      if (this.driveType !== RemoteDriveType.Baidu) {
-        Row({ space: 8 }) {
-          Text('用户名')
-            .fontSize(14)
-            .fontColor($r('app.color.index_tab_font_color'));
-          TextInput({ placeholder: '请输入用户名', text: this.username })
-            .layoutWeight(1)
-            .maxLines(1)
-            .onChange((value: string) => {
-              this.username = value;
-            });
-        }
-        .alignItems(VerticalAlign.Center);
+      Row({ space: 8 }) {
+        Text('用户名')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'));
+        TextInput({ placeholder: '请输入用户名', text: this.username })
+          .layoutWeight(1)
+          .maxLines(1)
+          .onChange((value: string) => {
+            this.username = value;
+          });
+      }
+      .alignItems(VerticalAlign.Center);
 
-        Row({ space: 8 }) {
-          Text('密码')
+      Row({ space: 8 }) {
+        Text('密码')
+          .fontSize(14)
+          .fontColor($r('app.color.index_tab_font_color'));
+        TextInput({ placeholder: '请输入密码', text: this.password })
+          .type(InputType.Password)
+          .layoutWeight(1)
+          .maxLines(2)
+          .onChange((value: string) => {
+            this.password = value;
+          });
+      }
+      .alignItems(VerticalAlign.Center);
+
+      if (this.driveType === RemoteDriveType.WebDav) {
+        Row({ space: 12 }) {
+          Text('启用HTTPS')
             .fontSize(14)
             .fontColor($r('app.color.index_tab_font_color'));
-          TextInput({ placeholder: '请输入密码', text: this.password })
-            .type(InputType.Password)
-            .layoutWeight(1)
-            .maxLines(2)
-            .onChange((value: string) => {
-              this.password = value;
+          Toggle({ type: ToggleType.Switch, isOn: this.enableHttps })
+            .selectedColor(this.themeColor)
+            .onChange((isOn: boolean) => {
+              this.enableHttps = isOn;
+              if (!this.portCustomized && this.driveType === RemoteDriveType.WebDav) {
+                this.updatePortState(this.getDefaultPort(), false);
+              }
             });
         }
+        .width('100%')
+        .justifyContent(FlexAlign.SpaceBetween)
         .alignItems(VerticalAlign.Center);
-
-        if (this.driveType === RemoteDriveType.WebDav) {
-          Row() {
-            Text('启用HTTPS')
-              .fontSize(14)
-              .fontColor($r('app.color.index_tab_font_color'));
-            Blank();
-            Toggle({ type: ToggleType.Switch, isOn: this.enableHttps })
-              .selectedColor(this.themeColor)
-              .onChange((isOn: boolean) => {
-                this.enableHttps = isOn;
-                if (!this.portCustomized && this.driveType === RemoteDriveType.WebDav) {
-                  this.updatePortState(this.getDefaultPort(), false);
-                }
-              });
-          }
-          .width('100%');
-        }
       }
 
+      } // end non-baidu section
+
       // 按钮
       Row({ space: 12 }) {
         Button('取消', { type: ButtonType.Capsule })
@@ -461,7 +468,7 @@ export struct RemoteDriveAccountDialog {
     }
     .padding(24)
     .width('90%')
-    .borderRadius(16)
+    .borderRadius(16);
   }
 
   @Builder
@@ -501,7 +508,7 @@ export struct RemoteDriveAccountDialog {
       return;
     }
     if (this.driveType === RemoteDriveType.Baidu && type !== RemoteDriveType.Baidu) {
-      this.stopBaiduPolling();
+      this.closeBaiduAuthDialog();
     }
     this.driveType = type;
     if (type === RemoteDriveType.Smb || type === RemoteDriveType.Ftp) {
@@ -800,12 +807,12 @@ export struct RemoteDriveAccountDialog {
         .alignSelf(ItemAlign.Start);
 
       Row({ space: 8 }) {
-        Button(this.baiduDeviceCode ? '刷新二维码' : '生成授权二维码')
+        Button('打开百度授权页')
           .type(ButtonType.Capsule)
           .backgroundColor(this.themeColor)
           .fontColor(Color.White)
           .onClick(() => {
-            this.handleBaiduDeviceCodeRequest();
+            this.handleBaiduImplicitAuthorize();
           });
         if (this.baiduAccessToken) {
           Button('清除授权')
@@ -817,33 +824,15 @@ export struct RemoteDriveAccountDialog {
               this.baiduRefreshToken = '';
               this.baiduTokenExpiresAt = 0;
               this.baiduAuthStatus = '已清除授权信息';
-              this.stopBaiduPolling();
+              this.closeBaiduAuthDialog();
             });
         }
       }
 
-      if (this.baiduQrUrl) {
-        Image(this.baiduQrUrl)
-          .width(180)
-          .height(180)
-          .objectFit(ImageFit.Contain)
-          .backgroundColor(this.isDarkMode ? '#1C1C1E' : '#F2F2F7')
-          .borderRadius(12);
-      }
-
-      if (this.baiduUserCode) {
-        Text(`用户码: ${this.baiduUserCode}`)
-          .fontSize(18)
-          .fontWeight(FontWeight.Bold)
-          .fontColor(this.themeColor);
-      }
-
-      if (this.baiduVerificationUrl) {
-        Text(`也可访问 ${this.baiduVerificationUrl} 输入用户码完成授权`)
-          .fontSize(13)
-          .fontColor($r('app.color.index_tab_font_color'))
-          .maxLines(2);
-      }
+      Text('系统将打开百度网盘授权网页,请登录并允许访问网盘文件。')
+        .fontSize(13)
+        .fontColor(this.isDarkMode ? '#C7C7CC' : '#666666')
+        .maxLines(2);
 
       if (this.baiduAuthStatus) {
         Text(this.baiduAuthStatus)
@@ -861,6 +850,73 @@ export struct RemoteDriveAccountDialog {
     .width('100%');
   }
 
+  @Builder
+  private buildBaiduAuthDialogOverlay() {
+    Stack() {
+      Column()
+        .width('100%')
+        .height('100%')
+        .backgroundColor('rgba(0,0,0,0.5)')
+        .onClick(() => {
+          this.closeBaiduAuthDialog();
+        });
+
+      Column({ space: 8 }) {
+        Row() {
+          Text('百度网盘授权')
+            .fontSize(16)
+            .fontWeight(FontWeight.Medium)
+            .fontColor(this.isDarkMode ? Color.White : Color.Black);
+          Text('关闭')
+            .fontSize(14)
+            .fontColor(this.themeColor)
+            .onClick(() => {
+              this.closeBaiduAuthDialog();
+            });
+        }
+        .width('100%')
+        .justifyContent(FlexAlign.SpaceBetween)
+        .alignItems(VerticalAlign.Center)
+        .padding({ bottom: 4 });
+
+        Divider()
+          .color(this.isDarkMode ? '#3A3A3C' : '#E5E5EA');
+
+        if (this.baiduAuthProgress > 0 && this.baiduAuthProgress < 100) {
+          Text(`页面加载中 ${this.baiduAuthProgress}%`)
+            .fontSize(12)
+            .fontColor(this.isDarkMode ? '#C7C7CC' : '#666666');
+        }
+
+        Web({
+          src: this.baiduAuthorizeUrl,
+          controller: this.baiduWebController
+        })
+          .layoutWeight(1)
+          .width('100%')
+          .javaScriptAccess(true)
+          .domStorageAccess(true)
+          .onPageEnd((event) => {
+            if (event && event.url) {
+              this.handleBaiduAuthNavigation(event.url);
+            }
+          })
+          .onProgressChange((event) => {
+            if (event) {
+              this.baiduAuthProgress = event.newProgress;
+            }
+          });
+      }
+      .width('92%')
+      .height('85%')
+      .padding(16)
+      .backgroundColor(this.isDarkMode ? '#1C1C1E' : Color.White)
+      .borderRadius(16);
+    }
+    .width('100%')
+    .height('100%');
+  }
+
   private getBaiduExpireText(): string {
     if (!this.baiduTokenExpiresAt) {
       return '';
@@ -871,72 +927,87 @@ export struct RemoteDriveAccountDialog {
     return hours > 0 ? `${hours}小时${minutes}分后` : `${minutes}分钟后`;
   }
 
-  private async handleBaiduDeviceCodeRequest(): Promise<void> {
-    try {
-      this.baiduAuthStatus = '正在获取设备码...';
-      const response = await requestDeviceCode();
-      this.baiduDeviceCode = response.device_code;
-      this.baiduUserCode = response.user_code;
-      this.baiduVerificationUrl = response.verification_url;
-      this.baiduQrUrl = response.qrcode_url;
-      this.baiduDeviceCodeExpireAt = Date.now() + response.expires_in * 1000;
-      this.baiduAuthStatus = '请使用百度网盘或百度APP扫码授权';
-      this.startBaiduPolling(response.device_code, response.interval);
-    } catch (error) {
-      this.baiduAuthStatus = `获取设备码失败: ${(error as Error).message}`;
-    }
+  private handleBaiduImplicitAuthorize(): void {
+    this.baiduAuthStatus = '正在打开百度授权页面...';
+    this.baiduAuthStateToken = `${Date.now()}`;
+    this.baiduAuthProgress = 0;
+    this.baiduAuthorizeUrl = this.buildBaiduAuthorizeUrl(this.baiduAuthStateToken);
+    this.showBaiduAuthDialog = true;
   }
 
-  private startBaiduPolling(deviceCode: string, intervalSeconds: number): void {
-    this.stopBaiduPolling();
-    const interval = Math.max(intervalSeconds || BaiduConstants.DEFAULT_POLL_INTERVAL, BaiduConstants.DEFAULT_POLL_INTERVAL) * 1000;
-    const timerId = setInterval(() => {
-      this.pollBaiduAuthorization(deviceCode).catch((err: Error) => {
-        Logger.error('RemoteDriveAccountDialog', `poll baidu auth failed: ${err.message}`);
-      });
-    }, interval);
-    this.baiduPollingTimer = timerId as number;
+  private closeBaiduAuthDialog(): void {
+    this.showBaiduAuthDialog = false;
+    this.baiduAuthorizeUrl = '';
+    this.baiduAuthProgress = 0;
+    this.baiduAuthStateToken = '';
   }
 
-  private stopBaiduPolling(): void {
-    if (this.baiduPollingTimer !== undefined) {
-      clearInterval(this.baiduPollingTimer);
-      this.baiduPollingTimer = undefined;
-    }
+  private buildBaiduAuthorizeUrl(state: string): string {
+    const params = [
+      `response_type=token`,
+      `client_id=${encodeURIComponent(BaiduConstants.APP_KEY)}`,
+      `redirect_uri=${encodeURIComponent(BaiduConstants.OOB_REDIRECT_PARAM)}`,
+      `scope=${encodeURIComponent(BaiduConstants.AUTH_SCOPE)}`,
+      `display=mobile`,
+      `state=${encodeURIComponent(state)}`
+    ];
+    return `${BaiduConstants.AUTHORIZE_URL}?${params.join('&')}`;
   }
 
-  private async pollBaiduAuthorization(deviceCode: string): Promise<void> {
-    if (!this.baiduDeviceCode || Date.now() > this.baiduDeviceCodeExpireAt) {
-      this.stopBaiduPolling();
-      this.baiduAuthStatus = '二维码已过期,请重新获取';
+  private handleBaiduAuthNavigation(url: string): void {
+    if (!url) {
       return;
     }
-    const result = await pollAccessToken(deviceCode);
-    if ((result as BaiduTokenError).error) {
-      const errorResult = result as BaiduTokenError;
-      switch (errorResult.error) {
-        case 'authorization_pending':
-          this.baiduAuthStatus = '等待用户确认授权...';
-          return;
-        case 'slow_down':
-          this.baiduAuthStatus = '授权中,请稍候...';
-          return;
-        case 'expired_token':
-          this.stopBaiduPolling();
-          this.baiduAuthStatus = '设备码已过期,请重新获取';
-          return;
-        default:
-          this.stopBaiduPolling();
-          this.baiduAuthStatus = `授权失败: ${errorResult.error}`;
-          return;
-      }
+    const loginSuccessHttps = BaiduConstants.LOGIN_SUCCESS_URL.replace('http://', 'https://');
+    if (!url.startsWith(BaiduConstants.LOGIN_SUCCESS_URL) && !url.startsWith(loginSuccessHttps)) {
+      return;
     }
-    const token = result as BaiduTokenResponse;
-    this.baiduAccessToken = token.access_token;
-    this.baiduRefreshToken = token.refresh_token ?? this.baiduRefreshToken;
-    this.baiduTokenExpiresAt = token.expires_in ? Date.now() + token.expires_in * 1000 : 0;
+    const fragmentIndex = url.indexOf('#');
+    const fragment = fragmentIndex >= 0 ? url.substring(fragmentIndex + 1) : '';
+    if (!fragment) {
+      return;
+    }
+    const params = this.parseAuthFragment(fragment);
+    const responseState = params.get('state') ?? '';
+    if (this.baiduAuthStateToken && responseState && responseState !== this.baiduAuthStateToken) {
+      this.baiduAuthStatus = '授权状态校验失败,请重试';
+      this.closeBaiduAuthDialog();
+      return;
+    }
+    const error = params.get('error');
+    if (error) {
+      const description = params.get('error_description') ?? error;
+      this.baiduAuthStatus = `授权失败: ${description}`;
+      this.closeBaiduAuthDialog();
+      return;
+    }
+    const accessToken = params.get('access_token');
+    if (!accessToken) {
+      return;
+    }
+    const expiresIn = Number(params.get('expires_in') ?? '0');
+    this.baiduAccessToken = accessToken;
+    this.baiduRefreshToken = '';
+    this.baiduTokenExpiresAt = expiresIn > 0 ? Date.now() + expiresIn * 1000 : 0;
     this.baiduAuthStatus = '授权成功,可保存账户';
-    this.stopBaiduPolling();
+    ToastUtil.showToast('百度授权成功');
+    this.closeBaiduAuthDialog();
+  }
+
+  private parseAuthFragment(fragment: string): Map<string, string> {
+    const params = new Map<string, string>();
+    const pairs = fragment.split('&');
+    for (let i = 0; i < pairs.length; i++) {
+      const pair = pairs[i];
+      if (!pair || pair.length === 0) {
+        continue;
+      }
+      const separator = pair.indexOf('=');
+      const key = separator >= 0 ? pair.substring(0, separator) : pair;
+      const value = separator >= 0 ? pair.substring(separator + 1) : '';
+      params.set(decodeURIComponent(key), decodeURIComponent(value));
+    }
+    return params;
   }
 
   /**

+ 35 - 3
entry/src/main/ets/view/LocalMusic.ets

@@ -27,6 +27,7 @@ import {
 import { imagePathToPixelMap } from '../common/util/CommUtils';
 import { unifiedDataChannel, uniformDataStruct, uniformTypeDescriptor } from '@kit.ArkData';
 import { CommonConstants } from '../common/constants/CommonConstants';
+import { BaiduConstants } from '../common/constants/BaiduConstants';
 import { EventConstants } from '../common/constants/EventConstants';
 import Logger from '../common/util/Logger';
 import { photoAccessHelper } from '@kit.MediaLibraryKit';
@@ -492,6 +493,7 @@ function cloneVideoItem(item: VideoItem): VideoItem {
  */
 async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionOptions): Promise<string> {
   const metadataOptions = options ?? {};
+  Logger.info(TAG, `setVideoUrlForSong start -> name=${song.name}, type=${song.type}, path=${song.filePath}, fsId=${song.baiduFsId || song.id}`);
   if (isWebDavType(song.type) && song.webdav_account_id) {
     try {
       Logger.info(TAG, `WebDAV 路径信息 remote_rel_path: ${song.remote_rel_path}`);
@@ -619,8 +621,10 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
       if (!account) {
         throw new Error('百度网盘账户不可用');
       }
+      Logger.info(TAG, `Baidu播放准备 - accountId: ${account.id}, fsId: ${song.baiduFsId || song.id}`);
       const downloadUrl = await manager.getBaiduDownloadUrl(account, song);
-      return sanitizePlaybackUrl(downloadUrl);
+      Logger.info(TAG, `Baidu播放URL已经构建完成: ${downloadUrl}`);
+      return downloadUrl;
     } catch (error) {
       const err = error as Error;
       Logger.error(TAG, `百度网盘获取播放链接失败: ${err.message}`);
@@ -646,6 +650,7 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
   if (isNavidromeType(song.type)) {
     throw new Error('Navidrome歌曲缺少webdav_account_id,无法构建播放链接');
   }
+  Logger.info(TAG, `setVideoUrlForSong fallback直接返回原始路径: ${song.filePath}`);
   return song.filePath;
 }
 
@@ -7106,6 +7111,7 @@ export struct LocalMusic {
       case CommonConstants.TYPE_SMB:
       case CommonConstants.TYPE_NAVIDROME:
       case CommonConstants.TYPE_FTP:
+      case CommonConstants.TYPE_BAIDU:
         // 处理网络音频播放(WebDAV/SMB)
         Logger.info(`heanup 处理云端音频播放: ${item.name}, URL: ${item.filePath}`)
 
@@ -13407,6 +13413,7 @@ export struct LocalMusic {
     }
 
     //设置视频源
+    Logger.info(TAG, `IjkPlayer setDataSource url=${url}`);
     this.mIjkMediaPlayer.setDataSource(url);
     // 构建规范的HTTP请求头(统一一次性设置,避免未带认证提前发起连接)
     const headers = new Map<string, string>();
@@ -13419,7 +13426,7 @@ export struct LocalMusic {
     }
 
     // 如果是WebDAV网络音频,使用webdav_account_id获取认证信息(等待完成后再设置一次性头部)
-    if (this.currentSong && isRemoteCloudType(this.currentSong.type)) {
+    if (this.currentSong && isRemoteCloudType(this.currentSong.type) && !isBaiduType(this.currentSong.type)) {
       Logger.info(`heanup WebDAV歌曲认证 - 歌曲名: ${this.currentSong.name}, webdav_account_id: ${this.currentSong.webdav_account_id || '未设置'}`);
       if (this.currentSong.webdav_account_id) {
         try {
@@ -13440,8 +13447,21 @@ export struct LocalMusic {
       }
     }
     // 统一设置带认证的请求头
+    // 百度类型也需要特殊请求头
+    if (this.currentSong && isBaiduType(this.currentSong.type)) {
+      try {
+        const webdavManager = RemoteDriveManager.getInstance();
+        const baiduHeaders = await webdavManager.buildHttpHeadersWithAccountId(this.currentSong, this.videoUrl);
+        baiduHeaders.forEach((value, key) => headers.set(key, value));
+        const headerSnapshot: Record<string, string> = {};
+        headers.forEach((value, key) => headerSnapshot[key] = value);
+        Logger.info(TAG, `Baidu播放请求头: ${JSON.stringify(headerSnapshot)}`);
+      } catch (error) {
+        Logger.error(`heanup 构建百度请求头失败: ${(error as Error).message}`);
+      }
+    }
     this.mIjkMediaPlayer.setDataSourceHeader(headers);
-    if (this.currentSong && isRemoteCloudType(this.currentSong.type)) {
+    if (this.currentSong && isRemoteCloudType(this.currentSong.type) && !isBaiduType(this.currentSong.type)) {
       console.log(`heanup 为WebDAV播放设置IjkPlayer选项`);
       // 网络超时设置(单位:微秒,文档显示应该是字符串格式)
       this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "30000000"); // 30秒
@@ -13465,6 +13485,18 @@ export struct LocalMusic {
       this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", "3"); // 重连3次
 
       console.log(`heanup WebDAV IjkPlayer选项设置完成`);
+    } else if (this.currentSong && isBaiduType(this.currentSong.type)) {
+      console.log(`heanup 为百度网盘播放设置IjkPlayer选项`);
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "15000000");
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "connect_timeout", "15000000");
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http_redirect", "1");
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "1");
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", "2097152");
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "25");
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "user_agent", BaiduConstants.USER_AGENT);
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", "3");
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", "1");
+      console.log(`heanup 百度网盘 IjkPlayer 选项设置完成`);
     }
 
     // if(PreferencesUtil.getBooleanSync(SettingPage.IS_MIDIACODEC_OPEN,false)){