Эх сурвалжийг харах

Merge branch 'refs/heads/feature/baiduyun'

# Conflicts:
#	entry/src/main/ets/common/util/RemoteDriveManager.ets
#	entry/src/main/ets/dialog/RemoteDriveAccountDialog.ets
#	entry/src/main/ets/viewmodel/VideoItem.ets
chendeben 8 сар өмнө
parent
commit
62dede8b6d

+ 13 - 0
entry/src/main/ets/common/constants/BaiduConstants.ets

@@ -0,0 +1,13 @@
+export class BaiduConstants {
+  static readonly APP_ID: string = '120875124';
+  static readonly APP_KEY: string = 'UPU5I5IAI9Ns36n1eWRQ9WorfgTTKJLD';
+  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 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
+}

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

@@ -210,6 +210,7 @@ export class CommonConstants {
   static readonly TYPE_SMB: number = 4;//SMB文件
   static readonly TYPE_NAVIDROME: number = 5;//Navidrome流媒体文件
   static readonly TYPE_FTP: number = 6;//FTP文件
+  static readonly TYPE_BAIDU: number = 7;//百度网盘文件
 
   static readonly TYPE_LOCK: number = 200;//私密视频
   static readonly TYPE_LIKE: number = 201;//我收藏的视频

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

@@ -2,5 +2,6 @@ export enum RemoteDriveType {
   WebDav = 0,
   Smb = 1,
   Navidrome = 2,
-  Ftp = 3
+  Ftp = 3,
+  Baidu = 4
 }

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

@@ -0,0 +1,208 @@
+import { http } from '@kit.NetworkKit';
+import { JSON, util } from '@kit.ArkTS';
+import { BaiduConstants } from '../constants/BaiduConstants';
+import Logger from '../util/Logger';
+
+const TAG = 'BaiduPanClient';
+
+interface QueryParamEntry {
+  key: string;
+  value?: string | number | boolean;
+}
+
+function encodeQuery(params: QueryParamEntry[]): string {
+  const entries: string[] = [];
+  for (let i = 0; i < params.length; i++) {
+    const param = params[i];
+    if (param.value === undefined || param.value === null) {
+      continue;
+    }
+    entries.push(`${encodeURIComponent(param.key)}=${encodeURIComponent(String(param.value))}`);
+  }
+  return entries.join('&');
+}
+
+function parseResponseBody(response: http.HttpResponse): string {
+  if (typeof response.result === 'string') {
+    return response.result;
+  }
+  if (response.result instanceof ArrayBuffer) {
+    const decoder = new util.TextDecoder('utf-8');
+    const buffer = new Uint8Array(response.result);
+    return decoder.decode(buffer);
+  }
+  return JSON.stringify(response.result);
+}
+
+async function httpGet(url: string, params: QueryParamEntry[]): Promise<string> {
+  const query = encodeQuery(params);
+  const requestUrl = query.length > 0 ? `${url}?${query}` : url;
+  const httpRequest = http.createHttp();
+  const headers: Record<string, string> = {
+    'User-Agent': BaiduConstants.USER_AGENT
+  };
+  const options: http.HttpRequestOptions = {
+    method: http.RequestMethod.GET,
+    readTimeout: 10000,
+    connectTimeout: 8000,
+    expectDataType: http.HttpDataType.STRING,
+    header: headers
+  };
+  try {
+    const response = await httpRequest.request(requestUrl, options);
+    return parseResponseBody(response);
+  } finally {
+    httpRequest.destroy();
+  }
+}
+
+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;
+  expires_in: number;
+  scope?: string;
+}
+
+export interface BaiduTokenError {
+  error: string;
+  error_description?: string;
+}
+
+export interface BaiduListEntry {
+  fs_id: number;
+  path: string;
+  server_filename: string;
+  size: number;
+  isdir: number;
+  category: number;
+  md5?: string;
+  server_mtime?: number;
+}
+
+export interface BaiduListResponse {
+  errno: number;
+  list: BaiduListEntry[];
+}
+
+export interface BaiduFileMeta {
+  fs_id: number;
+  filename: string;
+  size: number;
+  dlink?: string;
+  md5?: string;
+}
+
+export interface BaiduFileMetaResponse {
+  errno: number;
+  list: BaiduFileMeta[];
+}
+
+async function parseJson<T>(payload: string): Promise<T> {
+  try {
+    return JSON.parse(payload) as T;
+  } catch (error) {
+    const err = error as Error;
+    Logger.error(TAG, `解析百度网盘响应失败: ${err.message} payload=${payload}`);
+    throw new Error(err.message);
+  }
+}
+
+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' },
+    { key: 'refresh_token', value: refreshToken },
+    { 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) {
+    throw new Error(errorResult.error_description || errorResult.error);
+  }
+  return result as BaiduTokenResponse;
+}
+
+export async function listDirectory(
+  accessToken: string,
+  dir: string,
+  start: number = 0,
+  limit: number = 1000
+): Promise<BaiduListEntry[]> {
+  const payload = await httpGet(`${BaiduConstants.PAN_BASE}/rest/2.0/xpan/file`, [
+    { key: 'method', value: 'list' },
+    { key: 'access_token', value: accessToken },
+    { key: 'dir', value: dir },
+    { key: 'start', value: start },
+    { key: 'limit', value: limit },
+    { key: 'order', value: 'name' },
+    { key: 'desc', value: 0 },
+    { key: 'web', value: 1 },
+    { key: 'folder', value: 0 }
+  ]);
+  const parsed = await parseJson<BaiduListResponse>(payload);
+  if (parsed.errno !== 0) {
+    throw new Error(`列表查询失败 errno=${parsed.errno}`);
+  }
+  return parsed.list ?? [];
+}
+
+export async function fetchFileMetas(accessToken: string, fsIds: string[]): Promise<BaiduFileMeta[]> {
+  if (fsIds.length === 0) {
+    return [];
+  }
+  const payload = await httpGet(`${BaiduConstants.PAN_BASE}/rest/2.0/xpan/multimedia`, [
+    { key: 'method', value: 'filemetas' },
+    { key: 'access_token', value: accessToken },
+    { key: 'fsids', value: JSON.stringify(fsIds.map(id => Number(id))) },
+    { key: 'dlink', value: 1 }
+  ]);
+  const parsed = await parseJson<BaiduFileMetaResponse>(payload);
+  if (parsed.errno !== 0) {
+    throw new Error(`filemetas 查询失败 errno=${parsed.errno}`);
+  }
+  return parsed.list ?? [];
+}
+
+export function appendAccessTokenToDlink(dlink: string, accessToken: string): string {
+  if (!dlink) {
+    return '';
+  }
+  if (dlink.includes('access_token=')) {
+    return dlink;
+  }
+  const separator = dlink.includes('?') ? '&' : '?';
+  return `${dlink}${separator}access_token=${encodeURIComponent(accessToken)}`;
+}

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

@@ -18,6 +18,8 @@ export function getRemoteDriveProtocolLabel(type?: number): string {
       return 'Navidrome';
     case RemoteDriveType.Ftp:
       return 'FTP';
+    case RemoteDriveType.Baidu:
+      return 'Baidu';
     case RemoteDriveType.WebDav:
     default:
       return 'WebDAV';

+ 176 - 7
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -22,6 +22,8 @@ import { NavidromeApi, NavidromeAlbumDetail, NavidromeArtist, NavidromeArtistDet
 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 { ServerLogUtil } from './ServerLogUtil';
 
 const TAG = 'heanup RemoteDriveManager';
@@ -192,6 +194,9 @@ export class RemoteDriveManager {
         const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
         const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
         const ftpEncodingIndex = resultSet.getColumnIndex('ftpEncoding');
+        const baiduAccessIndex = resultSet.getColumnIndex('baiduAccessToken');
+        const baiduRefreshIndex = resultSet.getColumnIndex('baiduRefreshToken');
+        const baiduExpireIndex = resultSet.getColumnIndex('baiduTokenExpiresAt');
         if (smbShareIndex >= 0) {
           account.smbShare = resultSet.getString(smbShareIndex) ?? '';
         }
@@ -204,6 +209,21 @@ export class RemoteDriveManager {
         } else {
           account.navidromeBasePath = '/rest';
         }
+        if (ftpEncodingIndex >= 0) {
+          const encodingValue = resultSet.getString(ftpEncodingIndex);
+          account.ftpEncoding = encodingValue && encodingValue.length > 0 ? encodingValue : 'utf-8';
+        } else {
+          account.ftpEncoding = 'utf-8';
+        }
+        if (baiduAccessIndex >= 0) {
+          account.baiduAccessToken = resultSet.getString(baiduAccessIndex) ?? '';
+        }
+        if (baiduRefreshIndex >= 0) {
+          account.baiduRefreshToken = resultSet.getString(baiduRefreshIndex) ?? '';
+        }
+        if (baiduExpireIndex >= 0) {
+          account.baiduTokenExpiresAt = resultSet.getLong(baiduExpireIndex);
+        }
         account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
 
         resultSet.close();
@@ -334,7 +354,7 @@ export class RemoteDriveManager {
       // 查询所有TYPE_WEBDAV且webdav_account_id为空或null的记录
       const querySql = `
         SELECT id, filePath FROM mediaTable
-        WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB}, ${CommonConstants.TYPE_FTP})
+        WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB}, ${CommonConstants.TYPE_FTP}, ${CommonConstants.TYPE_BAIDU})
         AND (webdav_account_id IS NULL OR webdav_account_id = '')
       `;
 
@@ -348,7 +368,7 @@ export class RemoteDriveManager {
         const updateSql = `
           UPDATE mediaTable
           SET webdav_account_id = ?
-          WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB}, ${CommonConstants.TYPE_FTP})
+          WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB}, ${CommonConstants.TYPE_FTP}, ${CommonConstants.TYPE_BAIDU})
           AND (webdav_account_id IS NULL OR webdav_account_id = '')
         `;
 
@@ -424,6 +444,9 @@ export class RemoteDriveManager {
         const smbShareIndex = resultSet.getColumnIndex('smbShare');
         const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
         const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
+        const baiduAccessIndex = resultSet.getColumnIndex('baiduAccessToken');
+        const baiduRefreshIndex = resultSet.getColumnIndex('baiduRefreshToken');
+        const baiduExpireIndex = resultSet.getColumnIndex('baiduTokenExpiresAt');
         if (smbShareIndex >= 0) {
           account.smbShare = resultSet.getString(smbShareIndex) ?? '';
         }
@@ -443,6 +466,15 @@ export class RemoteDriveManager {
         } else {
           account.ftpEncoding = 'utf-8';
         }
+        if (baiduAccessIndex >= 0) {
+          account.baiduAccessToken = resultSet.getString(baiduAccessIndex) ?? '';
+        }
+        if (baiduRefreshIndex >= 0) {
+          account.baiduRefreshToken = resultSet.getString(baiduRefreshIndex) ?? '';
+        }
+        if (baiduExpireIndex >= 0) {
+          account.baiduTokenExpiresAt = resultSet.getLong(baiduExpireIndex);
+        }
         account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
 
         resultSet.close();
@@ -551,6 +583,15 @@ export class RemoteDriveManager {
       Logger.info(TAG, 'ftpEncoding字段添加成功');
     } catch (error) {
     }
+
+    try {
+      Logger.info(TAG, '尝试添加百度授权字段...');
+      await this.dataBaseUtil.executeSql(`ALTER TABLE ${this.webDavTable} ADD COLUMN baiduAccessToken TEXT`);
+      await this.dataBaseUtil.executeSql(`ALTER TABLE ${this.webDavTable} ADD COLUMN baiduRefreshToken TEXT`);
+      await this.dataBaseUtil.executeSql(`ALTER TABLE ${this.webDavTable} ADD COLUMN baiduTokenExpiresAt INTEGER`);
+      Logger.info(TAG, '百度授权字段添加成功');
+    } catch (error) {
+    }
   }
 
   // 从数据库查询所有账户
@@ -563,7 +604,8 @@ export class RemoteDriveManager {
       // 先查询基础字段(确保这些字段在旧版本中存在)
       const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
         'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
-        'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain', 'navidromeBasePath', 'ftpEncoding'];
+        'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain', 'navidromeBasePath', 'ftpEncoding',
+        'baiduAccessToken', 'baiduRefreshToken', 'baiduTokenExpiresAt'];
 
       const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, baseColumns, predicates);
 
@@ -613,6 +655,18 @@ export class RemoteDriveManager {
         } else {
           account.ftpEncoding = 'utf-8';
         }
+        const baiduAccessIndex = resultSet.getColumnIndex('baiduAccessToken');
+        if (baiduAccessIndex >= 0) {
+          account.baiduAccessToken = resultSet.getString(baiduAccessIndex) ?? '';
+        }
+        const baiduRefreshIndex = resultSet.getColumnIndex('baiduRefreshToken');
+        if (baiduRefreshIndex >= 0) {
+          account.baiduRefreshToken = resultSet.getString(baiduRefreshIndex) ?? '';
+        }
+        const baiduExpireIndex = resultSet.getColumnIndex('baiduTokenExpiresAt');
+        if (baiduExpireIndex >= 0) {
+          account.baiduTokenExpiresAt = resultSet.getLong(baiduExpireIndex);
+        }
 
         this.webDavAccounts.push(account);
       }
@@ -647,7 +701,10 @@ export class RemoteDriveManager {
     smbShare: string = '',
     smbDomain: string = '',
     navidromeBasePath: string = '/rest',
-    ftpEncoding: string = 'utf-8'
+    ftpEncoding: string = 'utf-8',
+    baiduAccessToken: string = '',
+    baiduRefreshToken: string = '',
+    baiduTokenExpiresAt: number = 0
   ): Promise<void> {
     try {
       const values: relationalStore.ValuesBucket = {
@@ -669,7 +726,10 @@ export class RemoteDriveManager {
         'smbShare': smbShare,
         'smbDomain': smbDomain,
         'navidromeBasePath': navidromeBasePath,
-        'ftpEncoding': ftpEncoding
+        'ftpEncoding': ftpEncoding,
+        'baiduAccessToken': baiduAccessToken,
+        'baiduRefreshToken': baiduRefreshToken,
+        'baiduTokenExpiresAt': baiduTokenExpiresAt
       };
 
       await this.dataBaseUtil.insertData(this.webDavTable, values);
@@ -707,7 +767,10 @@ export class RemoteDriveManager {
         'smbShare': account.smbShare,
         'smbDomain': account.smbDomain,
         'navidromeBasePath': account.navidromeBasePath,
-        'ftpEncoding': account.ftpEncoding
+        'ftpEncoding': account.ftpEncoding,
+        'baiduAccessToken': account.baiduAccessToken,
+        'baiduRefreshToken': account.baiduRefreshToken,
+        'baiduTokenExpiresAt': account.baiduTokenExpiresAt
       };
 
       const predicates = new relationalStore.RdbPredicates(this.webDavTable);
@@ -804,6 +867,8 @@ export class RemoteDriveManager {
         await this.loadNavidromeFiles(account, normalizedFullPath);
       } else if (account.webType === RemoteDriveType.Ftp) {
         await this.loadFtpFiles(account, normalizedFullPath);
+      } else if (account.webType === RemoteDriveType.Baidu) {
+        await this.loadBaiduFiles(account, normalizedFullPath);
       } else {
         await this.loadWebDavFiles(account, normalizedFullPath);
       }
@@ -1063,6 +1128,26 @@ export class RemoteDriveManager {
     }
   }
 
+  private async loadBaiduFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+    const accessToken = await this.ensureBaiduAccessToken(account);
+    const directory = fullPath && fullPath.length > 0 ? fullPath : '/';
+    const entries = await listBaiduDirectory(accessToken, directory);
+    this.webDavFiles = [];
+    this.webDavSongs = [];
+    for (let i = 0; i < entries.length; i++) {
+      const entry = entries[i];
+      const info = this.baiduEntryToFileInfo(entry);
+      this.webDavFiles.push(info);
+      if (info.isDirectory) {
+        this.registerPathLabel(info.href, info.fileName);
+      } else if (this.isAudioFile(info.fileName)) {
+        this.webDavSongs.push(this.buildBaiduVideoItem(entry, account));
+      }
+    }
+    await this.enrichSongsWithDatabase(this.webDavSongs);
+    Logger.info(TAG, `从百度网盘获取到 ${entries.length} 个文件/文件夹`);
+  }
+
   private async loadNavidromeFiles(account: WebDavAccount, fullPath: string): Promise<void> {
     const normalized = this.normalizeFullPath(fullPath);
     this.registerPathLabel('/', '根目录');
@@ -1170,7 +1255,8 @@ export class RemoteDriveManager {
       }
       return null;
     }
-    if (item.type === CommonConstants.TYPE_SMB || item.type === CommonConstants.TYPE_NAVIDROME || item.type === CommonConstants.TYPE_FTP) {
+    if (item.type === CommonConstants.TYPE_SMB || item.type === CommonConstants.TYPE_NAVIDROME ||
+      item.type === CommonConstants.TYPE_FTP || item.type === CommonConstants.TYPE_BAIDU) {
       if (item.remote_rel_path) {
         return item.remote_rel_path;
       }
@@ -1352,6 +1438,89 @@ export class RemoteDriveManager {
     return videoItem;
   }
 
+  private baiduEntryToFileInfo(entry: BaiduListEntry): FileInfo {
+    const timestamp = (entry.server_mtime ?? Date.now()) * 1000;
+    const info = new FileInfo('', entry.server_filename, entry.size, timestamp);
+    info.fileName = entry.server_filename;
+    info.href = entry.path;
+    info.isDirectory = entry.isdir === 1;
+    info.contentLength = entry.size;
+    info.time = timestamp;
+    return info;
+  }
+
+  private buildBaiduVideoItem(entry: BaiduListEntry, account: WebDavAccount): VideoItem {
+    const timestamp = (entry.server_mtime ?? Date.now()) * 1000;
+    const videoItem = new VideoItem(
+      this.getFileNameWithoutExtension(entry.server_filename),
+      entry.fs_id.toString(),
+      `baidupan://${account.id ?? 0}${entry.path}`,
+      CommonConstants.TYPE_BAIDU,
+      entry.size,
+      Utility.getFormatDateStr(timestamp, 'yyyy-MM-dd HH:mm'),
+      Utility.formatFSize(entry.size)
+    );
+    videoItem.size = Utility.formatFSize(entry.size);
+    videoItem.album = '';
+    videoItem.remote_rel_path = entry.path;
+    videoItem.baiduFsId = entry.fs_id.toString();
+    videoItem.md5Str = entry.md5;
+    if (account && account.id) {
+      videoItem.webdav_account_id = account.id.toString();
+    }
+    const musicData: MusicInfo = parseMusicFileName(entry.server_filename);
+    if (musicData.isValid) {
+      videoItem.artist = musicData.artist;
+      videoItem.name = musicData.title;
+    } else {
+      videoItem.artist = Constants.UNKNOWN_ARTIST;
+    }
+    return videoItem;
+  }
+
+  private async ensureBaiduAccessToken(account: WebDavAccount): Promise<string> {
+    if (account.baiduAccessToken && account.baiduTokenExpiresAt &&
+      (account.baiduTokenExpiresAt - Date.now()) > 60 * 1000) {
+      return account.baiduAccessToken;
+    }
+    if (!account.baiduRefreshToken) {
+      throw new Error('百度网盘账户尚未授权');
+    }
+    const token = await refreshBaiduAccessToken(account.baiduRefreshToken);
+    account.baiduAccessToken = token.access_token;
+    account.baiduRefreshToken = token.refresh_token ?? account.baiduRefreshToken;
+    account.baiduTokenExpiresAt = token.expires_in ? Date.now() + token.expires_in * 1000 : 0;
+    await this.persistBaiduTokens(account);
+    return account.baiduAccessToken;
+  }
+
+  private async persistBaiduTokens(account: WebDavAccount): Promise<void> {
+    if (!account.id) {
+      return;
+    }
+    const predicates = new relationalStore.RdbPredicates(this.webDavTable);
+    predicates.equalTo('id', account.id);
+    const values: relationalStore.ValuesBucket = {
+      baiduAccessToken: account.baiduAccessToken,
+      baiduRefreshToken: account.baiduRefreshToken,
+      baiduTokenExpiresAt: account.baiduTokenExpiresAt
+    };
+    await this.dataBaseUtil.updateData(this.webDavTable, values, predicates);
+  }
+
+  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 metas = await fetchBaiduFileMetas(accessToken, [fsId]);
+    if (!metas || metas.length === 0 || !metas[0].dlink) {
+      throw new Error('无法获取下载链接');
+    }
+    return appendAccessTokenToDlink(metas[0].dlink, accessToken);
+  }
+
   private smbEntryToFileInfo(entry: SmbDirectoryEntry, basePath: string): FileInfo {
     const normalizedPath = this.combineRemotePath(basePath, entry.name, entry.isDirectory);
     const info = new FileInfo('', entry.name, entry.size, Date.now());

+ 277 - 59
entry/src/main/ets/dialog/RemoteDriveAccountDialog.ets

@@ -5,6 +5,8 @@ import { WebDavAccount } from '../viewmodel/WebDavAccount';
 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';
 
 interface ParsedConnectionParts {
   protocol: string;
@@ -42,13 +44,21 @@ export struct RemoteDriveAccountDialog {
   @State username: string = '';
   @State password: string = '';
   @State enableHttps: boolean = false;
-  @Prop driveType: RemoteDriveType = RemoteDriveType.WebDav;
+  @Prop initialDriveType: RemoteDriveType = RemoteDriveType.WebDav;
+  @State driveType: RemoteDriveType = RemoteDriveType.WebDav;
   @State shareName: string = '';
   @State domain: string = '';
   @State navidromeBasePath: string = '/rest';
   @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;
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
   @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
     ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
@@ -57,16 +67,18 @@ 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;
 
   onColorModeChange() {
     this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
   }
 
   aboutToAppear(): void {
-    console.info('heanup driveType = ' +this.driveType)
-    // 初始化深色模式状态
-    this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
-    if(this.isEditMode){
+    console.info('heanup driveType = ' + this.driveType);
+    this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
+    if (this.isEditMode) {
       this.accountName = this.account.name;
       this.host = this.account.host;
       this.updatePortState(this.account.port, true);
@@ -79,6 +91,9 @@ export struct RemoteDriveAccountDialog {
       this.domain = this.account.smbDomain ?? '';
       this.coverPath = this.account.coverPath || '';
       this.ftpEncoding = this.account.ftpEncoding ?? 'utf-8';
+      this.baiduAccessToken = this.account.baiduAccessToken ?? '';
+      this.baiduRefreshToken = this.account.baiduRefreshToken ?? '';
+      this.baiduTokenExpiresAt = this.account.baiduTokenExpiresAt ?? 0;
       this.nameCustomized = true;
       if (this.driveType === RemoteDriveType.Navidrome) {
         this.navidromeBasePath = this.normalizeNavidromeBasePath(this.account.navidromeBasePath ?? '/rest');
@@ -89,11 +104,18 @@ export struct RemoteDriveAccountDialog {
         Logger.info('heanup RemoteDriveAccountDialog', `封面文件是否存在: ${ImagePickerUtil.imageExists(this.coverPath)}`);
       }
     } else {
-      // this.applyTypeDefaults(this.driveType);
+      this.driveType = this.initialDriveType ?? RemoteDriveType.WebDav;
+      this.baiduAccessToken = '';
+      this.baiduRefreshToken = '';
+      this.baiduTokenExpiresAt = 0;
       this.handleDriveTypeChange(this.driveType, true);
     }
   }
 
+  aboutToDisappear(): void {
+    this.stopBaiduPolling();
+  }
+
   build() {
     Scroll(){
       this.contentBuilder()
@@ -201,6 +223,9 @@ export struct RemoteDriveAccountDialog {
       .width('100%')
       .alignItems(VerticalAlign.Center);
 
+      if (this.driveType === RemoteDriveType.Baidu) {
+        this.buildBaiduAuthSection();
+      } else {
       // 服务器地址
       Row({ space: 8 }) {
         Text(this.getServerLabel())
@@ -330,52 +355,51 @@ export struct RemoteDriveAccountDialog {
       .visibility(this.driveType === RemoteDriveType.Navidrome? Visibility.None:Visibility.Visible)
       // this.buildHelperText('从共享根开始的路径,例如 /music 或 /音乐/歌单1')
 
-      // 用户名
-      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.password })
-          .type(InputType.Password)
-          .layoutWeight(1)
-          .maxLines(2)
-          .onChange((value: string) => {
-            this.password = value;
-          });
-      }
-      .alignItems(VerticalAlign.Center);
+      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);
 
-      // HTTPS开关
-      if (this.driveType === RemoteDriveType.WebDav) {
-        Row() {
-          Text('启用HTTPS')
+        Row({ space: 8 }) {
+          Text('密码')
             .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);
-              }
+          TextInput({ placeholder: '请输入密码', text: this.password })
+            .type(InputType.Password)
+            .layoutWeight(1)
+            .maxLines(2)
+            .onChange((value: string) => {
+              this.password = value;
             });
         }
-        .width('100%');
+        .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%');
+        }
       }
 
       // 按钮
@@ -392,21 +416,26 @@ export struct RemoteDriveAccountDialog {
           .backgroundColor(this.themeColor)
           .layoutWeight(1)
           .onClick(() => {
-            if (!this.accountName || !this.host) {
+            const needHost = this.driveType !== RemoteDriveType.Baidu;
+            if (!this.accountName || (needHost && !this.host)) {
               ToastUtil.showToast('请填写账户名称和服务器地址');
               return;
             }
+            if (this.driveType === RemoteDriveType.Baidu && !this.baiduAccessToken) {
+              ToastUtil.showToast('请先完成百度网盘授权');
+              return;
+            }
 
             const updatedAccount = new WebDavAccount();
             if (this.isEditMode) {
               updatedAccount.id = this.account.id;
             }
             updatedAccount.name = this.accountName;
-            updatedAccount.host = this.host;
-            updatedAccount.port = this.port;
+            updatedAccount.host = this.driveType === RemoteDriveType.Baidu ? 'pan.baidu.com' : this.host;
+            updatedAccount.port = this.driveType === RemoteDriveType.Baidu ? 443 : this.port;
             updatedAccount.filepath = this.filepath;
-            updatedAccount.account = this.username;
-            updatedAccount.password = this.password;
+            updatedAccount.account = this.driveType === RemoteDriveType.Baidu ? '' : this.username;
+            updatedAccount.password = this.driveType === RemoteDriveType.Baidu ? '' : this.password;
             updatedAccount.enableHttps = this.enableHttps;
             updatedAccount.coverPath = this.coverPath;
             updatedAccount.isActivate = true;
@@ -420,6 +449,9 @@ export struct RemoteDriveAccountDialog {
             updatedAccount.smbDomain = this.domain;
             updatedAccount.navidromeBasePath = this.normalizeNavidromeBasePath(this.navidromeBasePath);
             updatedAccount.ftpEncoding = this.ftpEncoding && this.ftpEncoding.length > 0 ? this.ftpEncoding : 'utf-8';
+            updatedAccount.baiduAccessToken = this.baiduAccessToken;
+            updatedAccount.baiduRefreshToken = this.baiduRefreshToken;
+            updatedAccount.baiduTokenExpiresAt = this.baiduTokenExpiresAt;
             this.onConfirm?.(updatedAccount);
 
           });
@@ -445,6 +477,7 @@ export struct RemoteDriveAccountDialog {
         this.buildTypeButton('SMB', RemoteDriveType.Smb);
         this.buildTypeButton('Navidrome', RemoteDriveType.Navidrome);
         this.buildTypeButton('FTP', RemoteDriveType.Ftp);
+        this.buildTypeButton('百度网盘', RemoteDriveType.Baidu);
       }
       .width('100%');
     }
@@ -464,15 +497,17 @@ export struct RemoteDriveAccountDialog {
 
   private handleDriveTypeChange(type: RemoteDriveType, forceApply: boolean = false) {
     const changed = this.driveType !== type;
-    if (changed) {
-      this.driveType = type;
-      if (type === RemoteDriveType.Smb || type === RemoteDriveType.Ftp) {
-        this.enableHttps = false;
-      }
+    if (!changed && !forceApply) {
+      return;
+    }
+    if (this.driveType === RemoteDriveType.Baidu && type !== RemoteDriveType.Baidu) {
+      this.stopBaiduPolling();
     }
-    if (changed || forceApply) {
-      this.applyTypeDefaults(type, true);
+    this.driveType = type;
+    if (type === RemoteDriveType.Smb || type === RemoteDriveType.Ftp) {
+      this.enableHttps = false;
     }
+    this.applyTypeDefaults(type, true);
   }
 
   private applyTypeDefaults(type: RemoteDriveType, forcePort: boolean = false) {
@@ -485,6 +520,17 @@ export struct RemoteDriveAccountDialog {
     if (type === RemoteDriveType.Ftp && (!this.ftpEncoding || this.ftpEncoding.length === 0)) {
       this.ftpEncoding = 'utf-8';
     }
+    if (type === RemoteDriveType.Baidu) {
+      this.enableHttps = true;
+      this.host = 'pan.baidu.com';
+      this.username = '';
+      this.password = '';
+      this.shareName = '';
+      this.domain = '';
+      if (!this.filepath || this.filepath.length === 0) {
+        this.filepath = '/';
+      }
+    }
     this.updatePortForType(type, forcePort);
   }
 
@@ -503,6 +549,8 @@ export struct RemoteDriveAccountDialog {
         return '新建Navidrome';
       case RemoteDriveType.Ftp:
         return '新建FTP';
+      case RemoteDriveType.Baidu:
+        return '百度网盘';
       default:
         return '新建WebDAV';
     }
@@ -520,6 +568,8 @@ export struct RemoteDriveAccountDialog {
         return '可粘贴 Navidrome/Subsonic 连接(如 https://user:pass@host:4533/rest),自动填充参数';
       case RemoteDriveType.Ftp:
         return '支持 ftp://user:pass@host:21/path 输入,自动填充账户和目录';
+      case RemoteDriveType.Baidu:
+        return '百度网盘无需服务器地址,请使用下方按钮完成授权';
       default:
         return '支持 https://user:pass@host:port/path WebDAV 连接串,自动填充账户、端口和目录';
     }
@@ -539,6 +589,9 @@ export struct RemoteDriveAccountDialog {
     if (this.driveType === RemoteDriveType.Ftp) {
       return 21;
     }
+    if (this.driveType === RemoteDriveType.Baidu) {
+      return 443;
+    }
     return this.enableHttps ? 443 : 5005;
   }
 
@@ -561,6 +614,22 @@ export struct RemoteDriveAccountDialog {
     }
   }
 
+  @Builder
+  private buildFileDirectoryRow() {
+    Row({ space: 8 }) {
+      Text('文件目录')
+        .fontSize(14)
+        .fontColor($r('app.color.index_tab_font_color'));
+      TextInput({ placeholder: '远程目录', text: this.filepath })
+        .layoutWeight(1)
+        .maxLines(1)
+        .onChange((value: string) => {
+          this.filepath = value;
+        });
+    }
+    .alignItems(VerticalAlign.Center);
+  }
+
   private tryParseConnectionString(input: string): boolean {
     const parsed = this.parseConnectionString(input);
     if (!parsed) {
@@ -721,6 +790,155 @@ export struct RemoteDriveAccountDialog {
     }
   }
 
+  @Builder
+  private buildBaiduAuthSection() {
+    Column({ space: 12 }) {
+      Text('百度账号授权')
+        .fontSize(14)
+        .fontWeight(FontWeight.Medium)
+        .fontColor($r('app.color.index_tab_font_color'))
+        .alignSelf(ItemAlign.Start);
+
+      Row({ space: 8 }) {
+        Button(this.baiduDeviceCode ? '刷新二维码' : '生成授权二维码')
+          .type(ButtonType.Capsule)
+          .backgroundColor(this.themeColor)
+          .fontColor(Color.White)
+          .onClick(() => {
+            this.handleBaiduDeviceCodeRequest();
+          });
+        if (this.baiduAccessToken) {
+          Button('清除授权')
+            .type(ButtonType.Capsule)
+            .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))
+            .fontColor($r('app.color.index_tab_font_color'))
+            .onClick(() => {
+              this.baiduAccessToken = '';
+              this.baiduRefreshToken = '';
+              this.baiduTokenExpiresAt = 0;
+              this.baiduAuthStatus = '已清除授权信息';
+              this.stopBaiduPolling();
+            });
+        }
+      }
+
+      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);
+      }
+
+      if (this.baiduAuthStatus) {
+        Text(this.baiduAuthStatus)
+          .fontSize(13)
+          .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666')
+          .maxLines(2);
+      }
+
+      if (this.baiduAccessToken) {
+        Text(`已获取 Access Token,${this.baiduTokenExpiresAt > 0 ? `将在 ${this.getBaiduExpireText()} 过期` : '有效期未知'}`)
+          .fontSize(13)
+          .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666');
+      }
+    }
+    .width('100%');
+  }
+
+  private getBaiduExpireText(): string {
+    if (!this.baiduTokenExpiresAt) {
+      return '';
+    }
+    const remain = Math.max(0, this.baiduTokenExpiresAt - Date.now());
+    const hours = Math.floor(remain / 3600000);
+    const minutes = Math.floor((remain % 3600000) / 60000);
+    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 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 stopBaiduPolling(): void {
+    if (this.baiduPollingTimer !== undefined) {
+      clearInterval(this.baiduPollingTimer);
+      this.baiduPollingTimer = undefined;
+    }
+  }
+
+  private async pollBaiduAuthorization(deviceCode: string): Promise<void> {
+    if (!this.baiduDeviceCode || Date.now() > this.baiduDeviceCodeExpireAt) {
+      this.stopBaiduPolling();
+      this.baiduAuthStatus = '二维码已过期,请重新获取';
+      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 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;
+    this.baiduAuthStatus = '授权成功,可保存账户';
+    this.stopBaiduPolling();
+  }
+
   /**
    * 处理选择封面
    */

+ 12 - 2
entry/src/main/ets/pages/NewIndex.ets

@@ -1591,6 +1591,13 @@ struct NewIndex {
         .onClick(async () => {
           this.showRemoteDriveAccountDialog(false,undefined,RemoteDriveType.Navidrome)
         })
+      MenuItem({
+        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.smallcircle_filled_circle')),
+        content: '百度网盘'
+      })
+        .onClick(async () => {
+          this.showRemoteDriveAccountDialog(false, undefined, RemoteDriveType.Baidu)
+        })
 
     }.attributeModifier(new MenuModifier())
   }
@@ -1959,7 +1966,7 @@ struct NewIndex {
      RemoteDriveAccountDialog({
        isEditMode: isEditMode,
        account: account,
-       driveType: driveType,
+       initialDriveType: driveType,
        onCancel: () => {
          // 取消添加
          this.getUIContext().getPromptAction().closeCustomDialog(this.addDavDialogId)
@@ -2000,7 +2007,10 @@ struct NewIndex {
             account.smbShare,
             account.smbDomain,
             account.navidromeBasePath,
-            account.ftpEncoding
+            account.ftpEncoding,
+            account.baiduAccessToken,
+            account.baiduRefreshToken,
+            account.baiduTokenExpiresAt
           ).then(() => {
              ToastUtil.showToast('添加成功')
              // 重新加载网盘账户列表 观察着模式去更新了,所以这里不需要更新,先注释掉

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

@@ -167,6 +167,8 @@ function getTypeOrder(type: number) {
     case CommonConstants.TYPE_WEBDAV:
     case CommonConstants.TYPE_SMB:
     case CommonConstants.TYPE_NAVIDROME:
+    case CommonConstants.TYPE_FTP:
+    case CommonConstants.TYPE_BAIDU:
       return 3;
     default:
       return 4; // Unknown types, if any, go last
@@ -189,8 +191,12 @@ function isFtpType(type: number): boolean {
   return type === CommonConstants.TYPE_FTP;
 }
 
+function isBaiduType(type: number): boolean {
+  return type === CommonConstants.TYPE_BAIDU;
+}
+
 function isRemoteCloudType(type: number): boolean {
-  return isWebDavType(type) || isSmbType(type) || isNavidromeType(type) || isFtpType(type);
+  return isWebDavType(type) || isSmbType(type) || isNavidromeType(type) || isFtpType(type) || isBaiduType(type);
 }
 
 function getShareNameFromFilePath(filePath?: string): string | undefined {
@@ -606,6 +612,22 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
     }
   }
 
+  if (isBaiduType(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 downloadUrl = await manager.getBaiduDownloadUrl(account, song);
+      return sanitizePlaybackUrl(downloadUrl);
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `百度网盘获取播放链接失败: ${err.message}`);
+      throw err;
+    }
+  }
+
   if (isWebDavType(song.type)) {
     Logger.warn(TAG, `WebDAV歌曲缺少webdav_account_id,使用原始路径: ${song.filePath}`);
     return sanitizePlaybackUrl(song.filePath);
@@ -618,6 +640,9 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
     Logger.warn(TAG, `FTP歌曲缺少webdav_account_id,使用原始路径: ${song.filePath}`);
     return song.filePath;
   }
+  if (isBaiduType(song.type)) {
+    throw new Error('百度网盘歌曲缺少webdav_account_id,无法构建播放链接');
+  }
   if (isNavidromeType(song.type)) {
     throw new Error('Navidrome歌曲缺少webdav_account_id,无法构建播放链接');
   }

+ 1 - 0
entry/src/main/ets/viewmodel/VideoItem.ets

@@ -70,6 +70,7 @@ export class VideoItem  {
   remote_rel_path?: string // 远程相对路径(去掉协议+host+端口),便于重构URL
   navArtistId?: string;
   navAlbumId?: string;
+  baiduFsId?: string // 百度网盘 fs_id
 
   constructor(name: string, id: string, filePath: string, type:number,videoSize:number,cTime: string,
     size?: string,pixelMapPath?: string,artist?: string,album?: string,fileName?: string, lastPlayed?:Date) {

+ 3 - 0
entry/src/main/ets/viewmodel/WebDavAccount.ets

@@ -28,6 +28,9 @@ export class WebDavAccount{
   public smbShare: string = ''
   public smbDomain: string = ''
   public navidromeBasePath: string = '/rest'
+  public baiduAccessToken: string = ''
+  public baiduRefreshToken: string = ''
+  public baiduTokenExpiresAt: number = 0
   public ftpEncoding: string = 'utf-8'
 
   public setIsUseLocalHost(isuse: boolean){