Sfoglia il codice sorgente

集成smb账户播放

chendeben 9 mesi fa
parent
commit
563266d6bd

BIN
entry/src/main/cpp/libs/arm64-v8a/libsmb2.so.1


+ 132 - 1
entry/src/main/cpp/napi_init.cpp

@@ -4,7 +4,11 @@
 #include <atomic>
 #include <cctype>
 #include <cstdint>
+#include <cstdio>
 #include <cstring>
+#include <filesystem>
+#include <fcntl.h>
+#include <system_error>
 #include <memory>
 #include <mutex>
 #include <stdexcept>
@@ -555,6 +559,132 @@ napi_value ReadDirectory(napi_env env, napi_callback_info info)
         return nullptr;
     }
 }
+
+napi_value DownloadSmbFile(napi_env env, napi_callback_info info)
+{
+    try {
+        size_t argc = 7;
+        napi_value args[7] = {nullptr};
+        NapiCheck(napi_get_cb_info(env, info, &argc, args, nullptr, nullptr), "Failed to get arguments for downloadSmbFile");
+        if (argc < 7) {
+            throw std::runtime_error("downloadSmbFile requires host, share, username, password, domain, remotePath and localPath");
+        }
+
+        std::string host = ReadString(env, args[0], "host", true);
+        std::string share = ReadString(env, args[1], "share", true);
+        std::string username = ReadString(env, args[2], "username", true);
+        std::string password = ReadString(env, args[3], "password", true);
+        std::string domain = ReadString(env, args[4], "domain", false);
+        std::string remotePath = ReadString(env, args[5], "remotePath", true);
+        std::string localPath = ReadString(env, args[6], "localPath", true);
+
+        if (share.empty()) {
+            throw std::runtime_error("share must not be empty");
+        }
+        if (localPath.empty()) {
+            throw std::runtime_error("localPath must not be empty");
+        }
+
+        std::string normalizedRemotePath = NormalizeRemotePath(remotePath);
+        if (normalizedRemotePath.empty()) {
+            throw std::runtime_error("remotePath must not be empty");
+        }
+
+        std::error_code fsError;
+        std::filesystem::path targetPath(localPath);
+        auto parent = targetPath.parent_path();
+        if (!parent.empty()) {
+            std::filesystem::create_directories(parent, fsError);
+            if (fsError) {
+                throw std::runtime_error("Failed to prepare cache directory: " + fsError.message());
+            }
+        }
+
+        FILE *output = std::fopen(localPath.c_str(), "wb");
+        if (output == nullptr) {
+            throw std::runtime_error("Unable to open local file for writing");
+        }
+
+        smb2_context *ctx = smb2_init_context();
+        if (ctx == nullptr) {
+            std::fclose(output);
+            throw std::runtime_error("Unable to initialize libsmb2 context");
+        }
+
+        auto cleanupContext = [&ctx]() {
+            if (ctx != nullptr) {
+                smb2_disconnect_share(ctx);
+                smb2_destroy_context(ctx);
+                ctx = nullptr;
+            }
+        };
+
+        auto cleanupFile = [&output]() {
+            if (output != nullptr) {
+                std::fclose(output);
+                output = nullptr;
+            }
+        };
+
+        auto removeCacheFile = [&localPath]() {
+            std::remove(localPath.c_str());
+        };
+
+        smb2_set_timeout(ctx, 30);
+        smb2_set_authentication(ctx, SMB2_SEC_NTLMSSP);
+        smb2_set_user(ctx, username.c_str());
+        smb2_set_password(ctx, password.c_str());
+        smb2_set_domain(ctx, domain.empty() ? nullptr : domain.c_str());
+
+        int rc = smb2_connect_share(ctx, host.c_str(), share.c_str(), username.c_str());
+        if (rc != 0) {
+            cleanupFile();
+            cleanupContext();
+            removeCacheFile();
+            throw std::runtime_error(BuildSmbErrorMessage(ctx, "Failed to connect to share"));
+        }
+
+        smb2fh *fileHandle = smb2_open(ctx, normalizedRemotePath.c_str(), O_RDONLY);
+        if (fileHandle == nullptr) {
+            cleanupFile();
+            cleanupContext();
+            removeCacheFile();
+            throw std::runtime_error(BuildSmbErrorMessage(ctx, "Failed to open remote file"));
+        }
+
+        const size_t BUFFER_SIZE = 64 * 1024;
+        std::vector<uint8_t> buffer(BUFFER_SIZE);
+        int bytesRead = 0;
+        while ((bytesRead = smb2_read(ctx, fileHandle, buffer.data(), static_cast<uint32_t>(buffer.size()))) > 0) {
+            size_t written = std::fwrite(buffer.data(), 1, static_cast<size_t>(bytesRead), output);
+            if (written != static_cast<size_t>(bytesRead)) {
+                smb2_close(ctx, fileHandle);
+                cleanupFile();
+                cleanupContext();
+                removeCacheFile();
+                throw std::runtime_error("Failed to write to local file");
+            }
+        }
+
+        if (bytesRead < 0) {
+            std::string message = BuildSmbErrorMessage(ctx, "Failed to read remote file");
+            smb2_close(ctx, fileHandle);
+            cleanupFile();
+            cleanupContext();
+            removeCacheFile();
+            throw std::runtime_error(message);
+        }
+
+        smb2_close(ctx, fileHandle);
+        cleanupFile();
+        cleanupContext();
+
+        return CreateUndefined(env);
+    } catch (const std::exception &error) {
+        napi_throw_error(env, nullptr, error.what());
+        return nullptr;
+    }
+}
 }
 
 EXTERN_C_START
@@ -567,7 +697,8 @@ static napi_value Init(napi_env env, napi_value exports)
         {"disconnectSession", nullptr, DisconnectSession, nullptr, nullptr, nullptr, napi_default, nullptr},
         {"connectTree", nullptr, ConnectTree, nullptr, nullptr, nullptr, napi_default, nullptr},
         {"disconnectTree", nullptr, DisconnectTree, nullptr, nullptr, nullptr, napi_default, nullptr},
-        {"readDirectory", nullptr, ReadDirectory, nullptr, nullptr, nullptr, napi_default, nullptr}
+        {"readDirectory", nullptr, ReadDirectory, nullptr, nullptr, nullptr, napi_default, nullptr},
+        {"downloadSmbFile", nullptr, DownloadSmbFile, nullptr, nullptr, nullptr, napi_default, nullptr}
     };
     NapiCheck(napi_define_properties(env, exports, sizeof(descriptors) / sizeof(descriptors[0]), descriptors), "Failed to define native exports");
     return exports;

+ 1 - 0
entry/src/main/cpp/types/libentry/Index.d.ts

@@ -14,6 +14,7 @@ export interface NativeModule {
   connectTree(sessionId: number, share: string): number;
   disconnectTree(treeId: number): void;
   readDirectory(treeId: number, path: string): NativeDirectoryEntry[];
+  downloadSmbFile(host: string, share: string, username: string, password: string, domain: string | undefined, remotePath: string, localPath: string): void;
 }
 
 declare const libentry: NativeModule;

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

@@ -200,6 +200,7 @@ export class CommonConstants {
    */
   static readonly TYPE_INTERNET: number = 1;//网络视频
   static readonly TYPE_WEBDAV: number = 3;//webdav文件
+  static readonly TYPE_SMB: number = 4;//SMB文件
 
   static readonly TYPE_LOCK: number = 200;//私密视频
   static readonly TYPE_LIKE: number = 201;//我收藏的视频

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

@@ -0,0 +1,4 @@
+export enum RemoteDriveType {
+  WebDav = 0,
+  Smb = 1
+}

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

@@ -1,5 +1,6 @@
 // 歌曲类型枚举
 export enum SongType {
   Local = 0,    // 本地歌曲
-  WebDav = 1    // WebDAV云端歌曲
+  WebDav = 1,   // WebDAV云端歌曲
+  Smb = 2       // SMB云端歌曲
 }

+ 146 - 0
entry/src/main/ets/common/network/SmbBridge.ets

@@ -0,0 +1,146 @@
+import libentry, { NativeDirectoryEntry as LibNativeDirectoryEntry, NativeModule as LibNativeModule } from 'libentry.so';
+
+export interface SmbConnectionOptions {
+  host: string;
+  share: string;
+  username: string;
+  password: string;
+  domain?: string;
+}
+
+export interface SmbListOptions extends SmbConnectionOptions {
+  path?: string;
+}
+
+export interface SmbDirectoryEntry {
+  name: string;
+  isDirectory: boolean;
+  isFile: boolean;
+  size: number;
+}
+
+export type NativeDirectoryEntry = LibNativeDirectoryEntry;
+
+const bridge: LibNativeModule = libentry as LibNativeModule;
+
+export class NativeSambaTree {
+  private closed = false;
+  private readonly treeId: number;
+
+  constructor(treeId: number) {
+    this.treeId = treeId;
+  }
+
+  private normalizePath(input?: string): string {
+    if (!input) {
+      return '';
+    }
+    const cleaned = input.replace(/^[\\/]+/, '').replace(/\\/g, '/');
+    return cleaned;
+  }
+
+  private ensureOpen(): void {
+    if (this.closed) {
+      throw new Error('SMB tree has been closed');
+    }
+  }
+
+  async readDirectory(path?: string): Promise<NativeDirectoryEntry[]> {
+    this.ensureOpen();
+    const normalized = this.normalizePath(path);
+    return bridge.readDirectory(this.treeId, normalized) ?? [];
+  }
+
+  async close(): Promise<void> {
+    if (this.closed) {
+      return;
+    }
+    bridge.disconnectTree(this.treeId);
+    this.closed = true;
+  }
+}
+
+export class NativeSambaSession {
+  private closed = false;
+  private readonly sessionId: number;
+
+  constructor(sessionId: number) {
+    this.sessionId = sessionId;
+  }
+
+  private ensureOpen(): void {
+    if (this.closed) {
+      throw new Error('SMB session has been closed');
+    }
+  }
+
+  async connectTree(share: string): Promise<NativeSambaTree> {
+    this.ensureOpen();
+    const treeId = Number(bridge.connectTree(this.sessionId, share));
+    return new NativeSambaTree(treeId);
+  }
+
+  async disconnect(): Promise<void> {
+    if (this.closed) {
+      return;
+    }
+    bridge.disconnectSession(this.sessionId);
+    this.closed = true;
+  }
+}
+
+export class NativeSambaClient {
+  private readonly clientId: number;
+  private closed = false;
+
+  constructor(host: string) {
+    this.clientId = bridge.createClient(host);
+  }
+
+  private ensureOpen(): void {
+    if (this.closed) {
+      throw new Error('SMB client has been closed');
+    }
+  }
+
+  async authenticate(options: SmbConnectionOptions): Promise<NativeSambaSession> {
+    this.ensureOpen();
+    const sessionId = Number(
+      bridge.authenticate(this.clientId, options.username, options.password, options.domain)
+    );
+    return new NativeSambaSession(sessionId);
+  }
+
+  async close(): Promise<void> {
+    if (this.closed) {
+      return;
+    }
+    bridge.destroyClient(this.clientId);
+    this.closed = true;
+  }
+}
+
+async function withSmbTree<T>(options: SmbConnectionOptions, handler: (tree: NativeSambaTree) => Promise<T>): Promise<T> {
+  const client = new NativeSambaClient(options.host);
+  let session: NativeSambaSession | undefined;
+  let tree: NativeSambaTree | undefined;
+  try {
+    session = await client.authenticate(options);
+    tree = await session.connectTree(options.share);
+    return await handler(tree);
+  } finally {
+    await tree?.close();
+    await session?.disconnect();
+    await client.close();
+  }
+}
+
+export async function listSmbDirectory(options: SmbListOptions): Promise<SmbDirectoryEntry[]> {
+  const entries = await withSmbTree(options, (tree: NativeSambaTree) => tree.readDirectory(options.path));
+  return entries.map((entry: NativeDirectoryEntry): SmbDirectoryEntry => ({
+    name: entry.name ?? entry.fileName ?? 'unknown',
+    isDirectory: Boolean(entry.isDirectory),
+    isFile: Boolean(entry.isFile),
+    size: entry.size ?? 0
+  }));
+}

+ 67 - 0
entry/src/main/ets/common/network/SmbFileCache.ets

@@ -0,0 +1,67 @@
+import { MD5 } from '@pura/harmony-utils';
+import { WebdavManager } from '../util/WebdavManager';
+import FileManager, { merge2paths } from '../util/FileManager';
+import { WebDavAccount } from '../../viewmodel/WebDavAccount';
+import nativeBridge from 'libentry.so';
+
+interface SmbDownloadBinding {
+  downloadSmbFile(host: string, share: string, username: string, password: string, domain: string | undefined, remotePath: string, localPath: string): void;
+}
+
+const smbBinding: SmbDownloadBinding = nativeBridge as SmbDownloadBinding;
+
+async function buildCacheFileName(relativePath: string): Promise<string> {
+  const hash = await MD5.digestSync(relativePath ?? '');
+  const lastSlash = relativePath.lastIndexOf('/');
+  const originalName = lastSlash >= 0 ? relativePath.substring(lastSlash + 1) : relativePath;
+  const safeName = originalName ? originalName.replace(/[^a-zA-Z0-9_.-]/g, '_') : 'remote';
+  return `${hash}_${safeName}`;
+}
+
+export async function ensureSmbFileCached(account: WebDavAccount, relativePath: string): Promise<string> {
+  const manager = WebdavManager.getInstance();
+  if (!manager.context) {
+    throw new Error('App context is not initialized');
+  }
+  if (!account.smbShare) {
+    throw new Error('SMB account missing share name');
+  }
+  const baseDir = manager.context.filesDir ?? manager.context.cacheDir;
+  if (!baseDir) {
+    throw new Error('Cache directory unavailable');
+  }
+  const cacheRoot = merge2paths(baseDir, 'smb_cache');
+  await FileManager.createDir(cacheRoot);
+  const accountDir = merge2paths(cacheRoot, account.id?.toString() ?? 'default');
+  await FileManager.createDir(accountDir);
+  const normalizedRelative = relativePath?.startsWith('/') ? relativePath : `/${relativePath}`;
+  const cacheFileName = await buildCacheFileName(normalizedRelative);
+  const localPath = merge2paths(accountDir, cacheFileName);
+  let exists = await FileManager.isExist(localPath);
+  if (exists) {
+    const currentSize = await FileManager.getFileSize(localPath);
+    if (currentSize <= 0) {
+      await FileManager.deleteFile(localPath);
+      exists = false;
+    }
+  }
+  if (!exists) {
+    const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+    try {
+      smbBinding.downloadSmbFile(
+        host,
+        account.smbShare,
+        account.account,
+        account.password,
+        account.smbDomain,
+        normalizedRelative,
+        localPath
+      );
+    } catch (error) {
+      await FileManager.deleteFile(localPath);
+      const err = error as Error;
+      throw err;
+    }
+  }
+  return localPath;
+}

+ 258 - 140
entry/src/main/ets/common/util/WebdavManager.ets

@@ -15,6 +15,8 @@ import { buffer } from '@kit.ArkTS';
 import { CommonConstants } from '../constants/CommonConstants';
 import { GlobalContext } from '@pura/harmony-utils';
 import { MusicInfo, parseMusicFileName, Utility } from './Utility';
+import { RemoteDriveType } from '../enums/RemoteDriveType';
+import { listSmbDirectory, SmbDirectoryEntry } from '../network/SmbBridge';
 
 const TAG = 'heanup WebdavManager';
 
@@ -160,6 +162,14 @@ export class WebdavManager {
         account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
         account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
         account.webType = resultSet.getLong(resultSet.getColumnIndex('webType'));
+        const smbShareIndex = resultSet.getColumnIndex('smbShare');
+        const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
+        if (smbShareIndex >= 0) {
+          account.smbShare = resultSet.getString(smbShareIndex) ?? '';
+        }
+        if (smbDomainIndex >= 0) {
+          account.smbDomain = resultSet.getString(smbDomainIndex) ?? '';
+        }
         account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
 
         resultSet.close();
@@ -290,7 +300,7 @@ export class WebdavManager {
       // 查询所有TYPE_WEBDAV且webdav_account_id为空或null的记录
       const querySql = `
         SELECT id, filePath FROM mediaTable
-        WHERE mtype = ${CommonConstants.TYPE_WEBDAV}
+        WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB})
         AND (webdav_account_id IS NULL OR webdav_account_id = '')
       `;
 
@@ -304,7 +314,7 @@ export class WebdavManager {
         const updateSql = `
           UPDATE mediaTable
           SET webdav_account_id = ?
-          WHERE mtype = ${CommonConstants.TYPE_WEBDAV}
+          WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB})
           AND (webdav_account_id IS NULL OR webdav_account_id = '')
         `;
 
@@ -377,6 +387,14 @@ export class WebdavManager {
         account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
         account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
         account.webType = resultSet.getLong(resultSet.getColumnIndex('webType'));
+        const smbShareIndex = resultSet.getColumnIndex('smbShare');
+        const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
+        if (smbShareIndex >= 0) {
+          account.smbShare = resultSet.getString(smbShareIndex) ?? '';
+        }
+        if (smbDomainIndex >= 0) {
+          account.smbDomain = resultSet.getString(smbDomainIndex) ?? '';
+        }
         account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
 
         resultSet.close();
@@ -410,7 +428,9 @@ export class WebdavManager {
       password TEXT,
       enableHttps INTEGER DEFAULT 0,
       coverPath TEXT,
-      webType INTEGER DEFAULT 0
+      webType INTEGER DEFAULT 0,
+      smbShare TEXT,
+      smbDomain TEXT
     )`;
 
     return this.dataBaseUtil.executeSql(createTableSql)
@@ -451,6 +471,24 @@ export class WebdavManager {
       // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
       Logger.info(TAG, 'webType字段可能已存在或添加失败,继续正常运行');
     }
+
+    try {
+      Logger.info(TAG, '尝试添加smbShare字段...');
+      const addSmbShareSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN smbShare TEXT`;
+      await this.dataBaseUtil.executeSql(addSmbShareSql);
+      Logger.info(TAG, 'smbShare字段添加成功');
+    } catch (error) {
+      Logger.info(TAG, 'smbShare字段可能已存在或添加失败,继续运行');
+    }
+
+    try {
+      Logger.info(TAG, '尝试添加smbDomain字段...');
+      const addSmbDomainSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN smbDomain TEXT`;
+      await this.dataBaseUtil.executeSql(addSmbDomainSql);
+      Logger.info(TAG, 'smbDomain字段添加成功');
+    } catch (error) {
+      Logger.info(TAG, 'smbDomain字段可能已存在或添加失败,继续运行');
+    }
   }
 
   // 从数据库查询所有账户
@@ -463,7 +501,7 @@ export class WebdavManager {
       // 先查询基础字段(确保这些字段在旧版本中存在)
       const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
         'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
-        'account', 'password', 'enableHttps'];
+        'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain'];
 
       const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, baseColumns, predicates);
 
@@ -484,42 +522,26 @@ export class WebdavManager {
         account.account = resultSet.getString(resultSet.getColumnIndex('account'));
         account.password = resultSet.getString(resultSet.getColumnIndex('password'));
         account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
-        // 设置coverPath和webType为默认值undefined,稍后会尝试更新
-        account.coverPath = undefined;
-        account.webType = 0;
+
+        const coverPathIndex = resultSet.getColumnIndex('coverPath');
+        if (coverPathIndex >= 0) {
+          account.coverPath = resultSet.getString(coverPathIndex) ?? undefined;
+        }
+        const webTypeIndex = resultSet.getColumnIndex('webType');
+        account.webType = webTypeIndex >= 0 ? resultSet.getLong(webTypeIndex) : 0;
+        const smbShareIndex = resultSet.getColumnIndex('smbShare');
+        if (smbShareIndex >= 0) {
+          account.smbShare = resultSet.getString(smbShareIndex) ?? '';
+        }
+        const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
+        if (smbDomainIndex >= 0) {
+          account.smbDomain = resultSet.getString(smbDomainIndex) ?? '';
+        }
 
         this.webDavAccounts.push(account);
       }
       resultSet.close();
 
-      // 尝试查询coverPath字段(如果升级成功)
-      try {
-        const coverPathResultSet = await this.dataBaseUtil.queryData(this.webDavTable, ['id', 'coverPath'], predicates);
-        if (coverPathResultSet.goToFirstRow()) {
-          // 创建一个映射来存储coverPath
-          const coverPathMap = new Map<number, string>();
-          do {
-            const accountId = coverPathResultSet.getLong(coverPathResultSet.getColumnIndex('id'));
-            const coverPathIndex = coverPathResultSet.getColumnIndex('coverPath');
-            const coverPath = coverPathIndex >= 0 ? coverPathResultSet.getString(coverPathIndex) : undefined;
-            if (coverPath) {
-              coverPathMap.set(accountId, coverPath);
-            }
-          } while (coverPathResultSet.goToNextRow());
-
-          // 将coverPath值赋给对应的账户
-          for (const account of this.webDavAccounts) {
-            if (coverPathMap.has(account.id)) {
-              account.coverPath = coverPathMap.get(account.id);
-            }
-          }
-        }
-        coverPathResultSet.close();
-      } catch (error) {
-        // 如果查询coverPath失败,说明字段可能不存在,忽略错误
-        Logger.info(TAG, 'coverPath字段不存在或查询失败,使用默认值');
-      }
-
       Logger.info(TAG, '从数据库加载了', this.webDavAccounts.length.toString(), '个WebDAV账户');
       this.notifyObservers(WebdavManagerStates.QueryAccountsSucceed);
     } catch (err) {
@@ -545,7 +567,9 @@ export class WebdavManager {
     password: string,
     enableHttps: boolean,
     coverPath?: string,
-    webType: number = 0
+    webType: number = 0,
+    smbShare: string = '',
+    smbDomain: string = ''
   ): Promise<void> {
     try {
       const values: relationalStore.ValuesBucket = {
@@ -563,7 +587,9 @@ export class WebdavManager {
         'password': password,
         'enableHttps': enableHttps ? 1 : 0,
         'coverPath': coverPath || null,
-        'webType': webType
+        'webType': webType,
+        'smbShare': smbShare,
+        'smbDomain': smbDomain
       };
 
       await this.dataBaseUtil.insertData(this.webDavTable, values);
@@ -597,7 +623,9 @@ export class WebdavManager {
         'password': account.password,
         'enableHttps': account.enableHttps ? 1 : 0,
         'coverPath': account.coverPath || null,
-        'webType': account.webType
+        'webType': account.webType,
+        'smbShare': account.smbShare,
+        'smbDomain': account.smbDomain
       };
 
       const predicates = new relationalStore.RdbPredicates(this.webDavTable);
@@ -657,68 +685,13 @@ export class WebdavManager {
 
   // 从WebDAV加载文件列表
   public async loadFilesInfoFromWebdav(customPath?: string): Promise<void> {
-
-    const account = this.getActivatedWebDavAccount();
+    const account = await this.getActiveWebDavAccount();
     if (!account) {
       Logger.error(TAG, '没有激活的WebDAV账户');
       this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
       return;
     }
-
-    try {
-      this.notifyObservers(WebdavManagerStates.LoadFilesInfoStart);
-
-      // 使用自定义路径或账户默认路径
-      const path = customPath !== undefined ? customPath : account.filepath;
-      this.currentPath = path;
-
-      const files = await this.rcpSocket.getFileList(
-        account.host,
-        account.localHost,
-        account.isUseLocalHost,
-        account.port,
-        path,
-        account.account,
-        account.password,
-        account.enableHttps
-      );
-
-      // 保存所有文件(包括文件夹)
-      // 直接使用从RcpSocketUtil返回的FileInfo对象
-      this.webDavFiles = [];
-      for (let i = 0; i < files.length; i++) {
-        const file = files[i];
-        // 直接添加原始文件对象
-        this.webDavFiles.push(file);
-      }
-
-      // 调试:输出获取到的文件总数
-      Logger.info(TAG, '从WebDAV获取到 ' + files.length + ' 个文件/文件夹');
-
-      // 分别统计文件夹和音频文件
-      let folderCount = 0;
-      let audioCount = 0;
-
-      // 过滤音频文件
-      this.webDavSongs = [];
-      for (let i = 0; i < files.length; i++) {
-        const file = files[i];
-        const fileName = file.fileName;
-
-        if (file.isDirectory) {
-          folderCount++;
-        } else if (this.isAudioFile(fileName)) {
-          audioCount++;
-          const song = this.fileInfoToVideoItem(file, account);
-          this.webDavSongs.push(song);
-        }
-      }
-
-      this.notifyObservers(WebdavManagerStates.LoadFilesInfoSucceed);
-    } catch (error) {
-      this.ErrorMessage = error as BusinessError;
-      this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
-    }
+    await this.loadFilesInfoFromAccount(account, customPath);
   }
 
 
@@ -734,49 +707,13 @@ export class WebdavManager {
       this.notifyObservers(WebdavManagerStates.LoadFilesInfoStart);
 
       // 使用自定义路径或账户默认路径
-      const path = customPath !== undefined ? customPath : account.filepath;
-      this.currentPath = path;
-
-      const files = await this.rcpSocket.getFileList(
-        account.host,
-        account.localHost,
-        account.isUseLocalHost,
-        account.port,
-        path,
-        account.account,
-        account.password,
-        account.enableHttps
-      );
-
-      // 保存所有文件(包括文件夹)
-      // 直接使用从RcpSocketUtil返回的FileInfo对象
-      this.webDavFiles = [];
-      for (let i = 0; i < files.length; i++) {
-        const file = files[i];
-        // 直接添加原始文件对象
-        this.webDavFiles.push(file);
-      }
+      const normalizedFullPath = this.normalizeFullPath(customPath !== undefined ? customPath : account.filepath);
+      this.currentPath = normalizedFullPath;
 
-      // 调试:输出获取到的文件总数
-      Logger.info(TAG, '从WebDAV获取到 ' + files.length + ' 个文件/文件夹');
-
-      // 分别统计文件夹和音频文件
-      let folderCount = 0;
-      let audioCount = 0;
-
-      // 过滤音频文件
-      this.webDavSongs = [];
-      for (let i = 0; i < files.length; i++) {
-        const file = files[i];
-        const fileName = file.fileName;
-
-        if (file.isDirectory) {
-          folderCount++;
-        } else if (this.isAudioFile(fileName)) {
-          audioCount++;
-          const song = this.fileInfoToVideoItem(file, account);
-          this.webDavSongs.push(song);
-        }
+      if (account.webType === RemoteDriveType.Smb) {
+        await this.loadSmbFiles(account, normalizedFullPath);
+      } else {
+        await this.loadWebDavFiles(account, normalizedFullPath);
       }
 
       this.notifyObservers(WebdavManagerStates.LoadFilesInfoSucceed);
@@ -786,8 +723,86 @@ export class WebdavManager {
     }
   }
 
+  private normalizeFullPath(path?: string): string {
+    if (!path || path.trim().length === 0) {
+      return '/';
+    }
+    let normalized = path.trim().replace(/\\/g, '/');
+    if (!normalized.startsWith('/')) {
+      normalized = `/${normalized}`;
+    }
+    normalized = normalized.replace(/\/+/g, '/');
+    if (normalized.length > 1 && normalized.endsWith('/')) {
+      normalized = normalized.slice(0, -1);
+    }
+    return normalized || '/';
+  }
+
+  private toRelativePath(fullPath: string): string {
+    if (!fullPath || fullPath === '/') {
+      return '';
+    }
+    return fullPath.replace(/^\/+/, '');
+  }
+
+  private async loadWebDavFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+    const files = await this.rcpSocket.getFileList(
+      account.host,
+      account.localHost,
+      account.isUseLocalHost,
+      account.port,
+      fullPath,
+      account.account,
+      account.password,
+      account.enableHttps
+    );
+
+    this.webDavFiles = files;
+    this.webDavSongs = [];
+
+    for (let i = 0; i < files.length; i++) {
+      const file = files[i];
+      if (!file.isDirectory && this.isAudioFile(file.fileName)) {
+        this.webDavSongs.push(this.fileInfoToVideoItem(file, account));
+      }
+    }
+    Logger.info(TAG, `从WebDAV获取到 ${files.length} 个文件/文件夹`);
+  }
+
+  private async loadSmbFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+    if (!account.smbShare) {
+      throw new Error('SMB账户缺少共享名称');
+    }
+    const relativePath = this.toRelativePath(fullPath);
+    const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+    const entries = await listSmbDirectory({
+      host,
+      share: account.smbShare,
+      username: account.account,
+      password: account.password,
+      domain: account.smbDomain,
+      path: relativePath
+    });
+
+    this.webDavFiles = [];
+    this.webDavSongs = [];
+
+    for (let i = 0; i < entries.length; i++) {
+      const entry = entries[i];
+      const fileInfo = this.smbEntryToFileInfo(entry, relativePath);
+      this.webDavFiles.push(fileInfo);
+      if (!entry.isDirectory && this.isAudioFile(entry.name)) {
+        this.webDavSongs.push(this.fileInfoToVideoItem(fileInfo, account));
+      }
+    }
+    Logger.info(TAG, `从SMB获取到 ${entries.length} 个文件/文件夹`);
+  }
+
   // 将FileInfo转换为VideoItem
   private fileInfoToVideoItem(fileInfo: FileInfo, account: WebDavAccount): VideoItem {
+    if (account.webType === RemoteDriveType.Smb) {
+      return this.buildSmbVideoItem(fileInfo, account);
+    }
     // 构建安全的WebDAV URL(不包含认证信息)
     const protocol = account.enableHttps ? 'https' : 'http';
     const host = account.isUseLocalHost ? account.localHost : account.host;
@@ -840,6 +855,109 @@ export class WebdavManager {
     return videoItem;
   }
 
+  private buildSmbVideoItem(fileInfo: FileInfo, account: WebDavAccount): VideoItem {
+    const relativePath = fileInfo.href && fileInfo.href.length > 0 ? fileInfo.href : `/${fileInfo.fileName}`;
+    const normalizedRelative = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
+    const sanitizedRelative = this.normalizeSmbRelativePath(normalizedRelative, account.smbShare);
+    const videoItem = new VideoItem(
+      this.getFileNameWithoutExtension(fileInfo.fileName),
+      '',
+      sanitizedRelative,
+      CommonConstants.TYPE_SMB,
+      fileInfo.contentLength,
+      Utility.getFormatDateStr(fileInfo.time, 'yyyy-MM-dd HH:mm'),
+      undefined,
+      undefined,
+      undefined,
+      Constants.UNKNOWN_ARTIST,
+      undefined,
+      fileInfo.fileName
+    );
+    videoItem.size = Utility.formatFSize(fileInfo.contentLength);
+    if (account && account.id) {
+      videoItem.webdav_account_id = account.id.toString();
+    }
+    videoItem.remote_rel_path = sanitizedRelative;
+    const hostForDisplay = account.host && account.host.trim().length > 0
+      ? account.host.trim()
+      : (account.id?.toString() ?? '');
+    const cleanedShare = account.smbShare
+      ? account.smbShare.replace(/^\/+|\/+$/g, '')
+      : '';
+    const relativeForDisplay = sanitizedRelative === '/' ? '' : sanitizedRelative;
+    if (hostForDisplay.length > 0 && cleanedShare.length > 0) {
+      videoItem.filePath = `smb://${hostForDisplay}/${cleanedShare}${relativeForDisplay}`;
+    } else if (hostForDisplay.length > 0) {
+      videoItem.filePath = `smb://${hostForDisplay}${relativeForDisplay}`;
+    } else {
+      videoItem.filePath = `smb://${account.id}${relativeForDisplay}`;
+    }
+    const musicData: MusicInfo = parseMusicFileName(fileInfo.fileName);
+    if (musicData.isValid) {
+      videoItem.artist = musicData.artist;
+      videoItem.name = musicData.title;
+    }
+    return videoItem;
+  }
+
+  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());
+    info.fileName = entry.name;
+    info.href = normalizedPath;
+    info.contentLength = entry.size ?? 0;
+    info.isDirectory = entry.isDirectory;
+    return info;
+  }
+
+  private combineRemotePath(basePath: string, name: string, isDirectory: boolean): string {
+    const segments: string[] = [];
+    const trimmedBase = basePath ? basePath.replace(/^\/+/, '').replace(/\/+/g, '/') : '';
+    if (trimmedBase.length > 0) {
+      segments.push(trimmedBase);
+    }
+    const cleanedName = name.replace(/^\/+/, '').replace(/\/+/g, '/');
+    if (cleanedName.length > 0) {
+      segments.push(cleanedName);
+    }
+    let fullPath = `/${segments.filter(part => part.length > 0).join('/')}`;
+    if (isDirectory && !fullPath.endsWith('/')) {
+      fullPath = `${fullPath}/`;
+    }
+    if (!isDirectory && fullPath.endsWith('/')) {
+      fullPath = fullPath.slice(0, -1);
+    }
+    return fullPath;
+  }
+
+  private normalizeSmbRelativePath(path: string, shareName?: string): string {
+    if (!path || path.trim().length === 0) {
+      return '/';
+    }
+    let normalized = path.replace(/\/+/g, '/');
+    if (!normalized.startsWith('/')) {
+      normalized = `/${normalized}`;
+    }
+    if (normalized.length > 1 && normalized.endsWith('/')) {
+      normalized = normalized.slice(0, -1);
+    }
+    if (shareName) {
+      const cleanedShare = shareName.replace(/^\/+|\/+$/g, '');
+      if (cleanedShare.length > 0) {
+        const prefix = `/${cleanedShare}`;
+        if (normalized === prefix) {
+          normalized = '/';
+        } else if (normalized.startsWith(`${prefix}/`)) {
+          normalized = normalized.substring(prefix.length);
+          if (!normalized.startsWith('/')) {
+            normalized = `/${normalized}`;
+          }
+        }
+      }
+    }
+    return normalized || '/';
+  }
+
   // 判断是否为音频文件
   private isAudioFile(fileName: string): boolean {
     const ext = this.getFileExtension(fileName).toLowerCase();
@@ -885,7 +1003,7 @@ export class WebdavManager {
     Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
 
     // 加载文件夹内容
-    await this.loadFilesInfoFromWebdav(folder.href);
+    await this.loadFilesInfoFromAccount(this.currentAccount, folder.href);
   }
 
   public async enterFolderFromPath(path: string): Promise<void> {
@@ -896,7 +1014,7 @@ export class WebdavManager {
     this.pathHistory.push(this.currentPath);
     Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
     // 加载文件夹内容
-    await this.loadFilesInfoFromWebdav(path);
+    await this.loadFilesInfoFromAccount(this.currentAccount, path);
   }
 
   // 在 WebdavManager 类中添加以下方法
@@ -942,7 +1060,7 @@ export class WebdavManager {
     // 从历史记录中取出上一级路径
     const previousPath = this.pathHistory.pop();
     if (previousPath !== undefined) {
-      await this.loadFilesInfoFromWebdav(previousPath);
+      await this.loadFilesInfoFromAccount(this.currentAccount, previousPath);
     }
   }
 

+ 108 - 17
entry/src/main/ets/dialog/WebDavAccountDialog.ets

@@ -1,5 +1,6 @@
 import { StrUtil, ToastUtil } from '@pura/harmony-utils';
 import { CommonConstants } from '../common/constants/CommonConstants';
+import { RemoteDriveType } from '../common/enums/RemoteDriveType';
 import { WebDavAccount } from '../viewmodel/WebDavAccount';
 import { ImagePickerUtil } from '../common/util/ImagePickerUtil';
 import Logger from '../common/util/Logger';
@@ -32,6 +33,9 @@ export struct WebDavAccountDialog {
   @State username: string = '';
   @State password: string = '';
   @State enableHttps: boolean = false;
+  @State driveType: RemoteDriveType = RemoteDriveType.WebDav;
+  @State shareName: string = '';
+  @State domain: string = '';
   @State coverPath: string = '';
   @State isDarkMode: boolean = false;
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
@@ -54,6 +58,9 @@ export struct WebDavAccountDialog {
       this.username = this.account.account;
       this.password = this.account.password;
       this.enableHttps = this.account.enableHttps;
+      this.driveType = this.account.webType ?? RemoteDriveType.WebDav;
+      this.shareName = this.account.smbShare ?? '';
+      this.domain = this.account.smbDomain ?? '';
       this.coverPath = this.account.coverPath || '';
       Logger.info('heanup WebDavAccountDialog', `编辑模式加载账户: ${this.accountName}, 封面路径: ${this.coverPath}`);
       if (this.coverPath) {
@@ -81,6 +88,9 @@ export struct WebDavAccountDialog {
         .fontWeight(FontWeight.Bold)
         .fontColor($r('app.color.index_tab_font_color'));
 
+      // 类型选择 + 封面
+      this.buildTypeSelector();
+
       // 账户封面选择
       Column({ space: 8 }) {
         Text('账户封面(可选)')
@@ -184,20 +194,50 @@ export struct WebDavAccountDialog {
         Text('端口')
           .fontSize(14)
           .fontColor($r('app.color.index_tab_font_color'));
-        TextInput({ placeholder: '默认: 80', text: this.port.toString() })
+        TextInput({ placeholder: this.getPortPlaceholder(), text: this.port.toString() })
           .layoutWeight(1)
           .maxLines(1)
           .type(InputType.Number)
           .onChange((value: string) => {
-            if(this.enableHttps){
-              this.port = parseInt(value) || 443;
-            }else{
-              this.port = parseInt(value) || 80;
+            const parsed = parseInt(value);
+            if (Number.isNaN(parsed)) {
+              this.port = this.driveType === RemoteDriveType.Smb ? 445 : (this.enableHttps ? 443 : 80);
+            } else {
+              this.port = parsed;
             }
           });
       }
       .alignItems(VerticalAlign.Center);
 
+      if (this.driveType === RemoteDriveType.Smb) {
+        // SMB 共享名称
+        Row({ space: 8 }) {
+          Text('共享名称')
+            .fontSize(14)
+            .fontColor($r('app.color.index_tab_font_color'));
+          TextInput({ placeholder: '例如: music', text: this.shareName })
+            .layoutWeight(1)
+            .maxLines(1)
+            .onChange((value: string) => {
+              this.shareName = value;
+            });
+        }
+        .alignItems(VerticalAlign.Center);
+
+        Row({ space: 8 }) {
+          Text('域/工作组')
+            .fontSize(14)
+            .fontColor($r('app.color.index_tab_font_color'));
+          TextInput({ placeholder: '可选,例如: WORKGROUP', text: this.domain })
+            .layoutWeight(1)
+            .maxLines(1)
+            .onChange((value: string) => {
+              this.domain = value;
+            });
+        }
+        .alignItems(VerticalAlign.Center);
+      }
+
       // 文件目录
       Row({ space: 8 }) {
         Text('文件目录')
@@ -242,18 +282,21 @@ export struct WebDavAccountDialog {
       .alignItems(VerticalAlign.Center);
 
       // HTTPS开关
-      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.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;
+              this.port = isOn ? 443 : 80;
+            });
+        }
+        .width('100%');
       }
-      .width('100%');
 
       // 按钮
       Row({ space: 12 }) {
@@ -292,6 +335,9 @@ export struct WebDavAccountDialog {
             updatedAccount.lyricFilePath = '';
             updatedAccount.uploadFilePath = '';
             updatedAccount.imageFilePath = '';
+            updatedAccount.webType = this.driveType;
+            updatedAccount.smbShare = this.shareName;
+            updatedAccount.smbDomain = this.domain;
             this.onConfirm?.(updatedAccount);
 
           });
@@ -304,6 +350,51 @@ export struct WebDavAccountDialog {
     .borderRadius(16)
   }
 
+  @Builder
+  private buildTypeSelector() {
+    Column({ space: 8 }) {
+      Text('协议类型')
+        .fontSize(14)
+        .fontColor($r('app.color.index_tab_font_color'))
+        .fontWeight(FontWeight.Medium)
+        .alignSelf(ItemAlign.Start);
+      Row({ space: 12 }) {
+        this.buildTypeButton('WebDAV', RemoteDriveType.WebDav);
+        this.buildTypeButton('SMB', RemoteDriveType.Smb);
+      }
+      .width('100%');
+    }
+    .width('100%');
+  }
+
+  @Builder
+  private buildTypeButton(label: string, type: RemoteDriveType) {
+    Button(label)
+      .type(ButtonType.Capsule)
+      .backgroundColor(this.driveType === type ? this.themeColor : (this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background')))
+      .fontColor(this.driveType === type ? Color.White : $r('app.color.index_tab_font_color'))
+      .onClick(() => {
+        this.driveType = type;
+        if (type === RemoteDriveType.Smb) {
+          this.enableHttps = false;
+          if (!this.port || this.port === 80 || this.port === 0) {
+            this.port = 445;
+          }
+        } else {
+          if (!this.port || this.port === 445) {
+            this.port = this.enableHttps ? 443 : 80;
+          }
+        }
+      });
+  }
+
+  private getPortPlaceholder(): string {
+    if (this.driveType === RemoteDriveType.Smb) {
+      return '默认: 445';
+    }
+    return this.enableHttps ? '默认: 443' : '默认: 80';
+  }
+
   /**
    * 处理选择封面
    */
@@ -348,4 +439,4 @@ export struct WebDavAccountDialog {
       Logger.info('heanup WebDavAccountDialog', '移除封面成功')
     }
   }
-}
+}

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

@@ -1660,21 +1660,24 @@ struct NewIndex {
            })
          } else {
            // 添加模式:创建新账户
-           this.webdavManager.insertAccount(
-             account.name,
-             account.host,
-             account.localHost,
-             account.isUseLocalHost,
-             account.port,
-             account.filepath,
-             account.lyricFilePath,
-             account.uploadFilePath,
-             account.imageFilePath,
-             account.account,
-             account.password,
-             account.enableHttps,
-             account.coverPath
-           ).then(() => {
+          this.webdavManager.insertAccount(
+            account.name,
+            account.host,
+            account.localHost,
+            account.isUseLocalHost,
+            account.port,
+            account.filepath,
+            account.lyricFilePath,
+            account.uploadFilePath,
+            account.imageFilePath,
+            account.account,
+            account.password,
+            account.enableHttps,
+            account.coverPath,
+            account.webType,
+            account.smbShare,
+            account.smbDomain
+          ).then(() => {
              ToastUtil.showToast('添加成功')
              // 重新加载WebDAV账户列表 观察着模式去更新了,所以这里不需要更新,先注释掉
              // this.loadWebDavAccounts()

+ 563 - 0
entry/src/main/ets/pages/SmbTestPage.ets

@@ -0,0 +1,563 @@
+import { NativeSambaClient, NativeSambaSession as BridgeSession, NativeSambaTree as BridgeTree, NativeDirectoryEntry } from '../common/network/SmbBridge';
+import { router } from '@kit.ArkUI';
+import { ToastUtil } from '@pura/harmony-utils';
+import { CommonConstants } from '../common/constants/CommonConstants';
+import Logger from '../common/util/Logger';
+
+type ErrorPrimitive = string | number | boolean | null | undefined;
+
+type ErrorLike = Error | ErrorPrimitive | object;
+
+interface SambaAuthOptions {
+  domain?: string;
+  username: string;
+  password: string;
+}
+
+interface SambaSession {
+  connectTree: (share: string) => Promise<SambaTree>;
+  disconnect?: () => Promise<void> | void;
+}
+
+interface SambaTree {
+  readDirectory: (path: string) => Promise<SambaDirectoryEntry[]>;
+  close?: () => Promise<void> | void;
+}
+
+interface SambaDirectoryEntry {
+  name?: string;
+  fileName?: string;
+  isDirectory?: boolean;
+  isFile?: boolean;
+  size?: number;
+}
+
+interface ParsedSharePath {
+  host: string;
+  share: string;
+  basePath: string;
+}
+
+interface SambaClientInstance {
+  authenticate: (options: SambaAuthOptions) => Promise<SambaSession>;
+  close?: () => Promise<void> | void;
+}
+
+class NativeSambaTreeAdapter implements SambaTree {
+  private closed: boolean = false;
+  private readonly tree: BridgeTree;
+
+  constructor(tree: BridgeTree) {
+    this.tree = tree;
+  }
+
+  private ensureUsable(): void {
+    if (this.closed) {
+      throw new Error('SMB tree has been closed');
+    }
+  }
+
+  async readDirectory(path: string): Promise<SambaDirectoryEntry[]> {
+    this.ensureUsable();
+    const entries: NativeDirectoryEntry[] = await this.tree.readDirectory(path ?? '');
+    return entries.map((entry: NativeDirectoryEntry, index: number): SambaDirectoryEntry => ({
+      name: entry.name ?? entry.fileName ?? `entry-${index}`,
+      fileName: entry.fileName ?? entry.name,
+      isDirectory: Boolean(entry.isDirectory),
+      isFile: Boolean(entry.isFile),
+      size: entry.size
+    }));
+  }
+
+  async close(): Promise<void> {
+    if (this.closed) {
+      return;
+    }
+    await this.tree.close();
+    this.closed = true;
+  }
+}
+
+class NativeSambaSessionAdapter implements SambaSession {
+  private closed: boolean = false;
+  private readonly session: BridgeSession;
+
+  constructor(session: BridgeSession) {
+    this.session = session;
+  }
+
+  private ensureUsable(): void {
+    if (this.closed) {
+      throw new Error('SMB session has been closed');
+    }
+  }
+
+  async connectTree(share: string): Promise<SambaTree> {
+    this.ensureUsable();
+    const tree = await this.session.connectTree(share);
+    return new NativeSambaTreeAdapter(tree);
+  }
+
+  async disconnect(): Promise<void> {
+    if (this.closed) {
+      return;
+    }
+    await this.session.disconnect();
+    this.closed = true;
+  }
+}
+
+class NativeSambaClientAdapter implements SambaClientInstance {
+  private closed: boolean = false;
+  private readonly client: NativeSambaClient;
+  private readonly host: string;
+
+  constructor(host: string) {
+    this.host = host;
+    this.client = new NativeSambaClient(host);
+  }
+
+  private ensureUsable(): void {
+    if (this.closed) {
+      throw new Error('SMB client has been closed');
+    }
+  }
+
+  async authenticate(options: SambaAuthOptions): Promise<SambaSession> {
+    this.ensureUsable();
+    const session = await this.client.authenticate({
+      host: this.host,
+      share: '',
+      username: options.username,
+      password: options.password,
+      domain: options.domain
+    });
+    return new NativeSambaSessionAdapter(session);
+  }
+
+  async close(): Promise<void> {
+    if (this.closed) {
+      return;
+    }
+    await this.client.close();
+    this.closed = true;
+  }
+}
+
+@Entry
+@Component
+export struct SmbTestPage {
+  @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  @State sharePath: string = '\\\\192.168.11.170\\share';
+  @State domain: string = 'WORKGROUP';
+  @State username: string = 'chendeben';
+  @State password: string = 'chen384626WYT';
+  @State targetPath: string = '';
+  @State directoryEntries: string[] = [];
+  @State logMessages: string[] = [];
+  @State isBusy: boolean = false;
+
+  build() {
+    Column() {
+      this.buildTopBar();
+      Scroll() {
+        Column({ space: 16 }) {
+          this.buildDescription();
+          this.buildConnectionForm();
+          this.buildListAction();
+          this.buildDirectoryResult();
+          this.buildLogSection();
+        }
+        .padding({ left: 20, right: 20, bottom: 24 })
+      }
+      .scrollBar(BarState.Off)
+    }
+    .backgroundColor($r('app.color.index_background'))
+    .width('100%')
+    .height('100%')
+  }
+
+  @Builder
+  private buildTopBar() {
+    Row() {
+      Button('返回')
+        .type(ButtonType.Capsule)
+        .backgroundColor(this.themeColor)
+        .fontColor(Color.White)
+        .fontSize(15)
+        .padding({ left: 18, right: 18, top: 8, bottom: 8 })
+        .onClick(() => {
+          router.back();
+        })
+      Text('SMB 测试工具')
+        .fontSize(18)
+        .fontWeight(FontWeight.Medium)
+        .margin({ left: 12 })
+        .fontColor($r('app.color.text_color'))
+      Blank()
+    }
+    .padding({ left: 20, right: 20, top: 24, bottom: 12 })
+    .width('100%')
+  }
+
+  @Builder
+  private buildDescription() {
+    Column({ space: 6 }) {
+      Text('依赖 libsmb2 native 库,通过 SMB2 协议连接 NAS 并列出目录。')
+        .fontSize(14)
+        .fontColor($r('app.color.text_color'))
+      Text('共享路径示例: \\\\192.168.1.100\\music\\albums')
+        .fontSize(12)
+        .fontColor(Color.Gray)
+    }
+  }
+
+  @Builder
+  private buildConnectionForm() {
+    Column({ space: 14 }) {
+      this.buildInputItem('共享路径', '例如:\\\\192.168.1.100\\music', this.sharePath, (value: string) => {
+        this.sharePath = value;
+      })
+      this.buildInputItem('域(可选)', '例如:WORKGROUP', this.domain, (value: string) => {
+        this.domain = value;
+      })
+      this.buildInputItem('用户名', '请输入用户名', this.username, (value: string) => {
+        this.username = value;
+      })
+      this.buildPasswordItem()
+    }
+  }
+
+  @Builder
+  private buildListAction() {
+    Column({ space: 12 }) {
+      this.buildInputItem('目录路径', '留空表示共享根目录', this.targetPath, (value: string) => {
+        this.targetPath = value;
+      })
+      Button('列出目录')
+        .type(ButtonType.Capsule)
+        .backgroundColor(this.themeColor)
+        .fontColor(Color.White)
+        .enabled(!this.isBusy)
+        .onClick(() => {
+          void this.listDirectory();
+        })
+    }
+  }
+
+  @Builder
+  private buildDirectoryResult() {
+    Column({ space: 8 }) {
+      Text('目录结果')
+        .fontSize(15)
+        .fontWeight(FontWeight.Medium)
+        .fontColor($r('app.color.text_color'))
+      if (this.directoryEntries.length === 0) {
+        Text('暂无数据,点击“列出目录”开始测试。')
+          .fontSize(12)
+          .fontColor(Color.Gray)
+      } else {
+        Column({ space: 4 }) {
+          ForEach(this.directoryEntries, (line: string) => {
+            Text(line)
+              .fontSize(13)
+              .fontColor(Color.Gray)
+          })
+        }
+        .backgroundColor($r('app.color.input_background'))
+        .borderRadius(10)
+        .padding({ left: 12, right: 12, top: 10, bottom: 10 })
+      }
+    }
+  }
+
+  @Builder
+  private buildLogSection() {
+    Column({ space: 8 }) {
+      Text('调试输出')
+        .fontSize(15)
+        .fontWeight(FontWeight.Medium)
+        .fontColor($r('app.color.text_color'))
+      Scroll() {
+        Column({ space: 6 }) {
+          if (this.logMessages.length === 0) {
+            Text('暂无日志,执行操作后查看输出。')
+              .fontSize(12)
+              .fontColor(Color.Gray)
+          } else {
+            ForEach(this.logMessages, (line: string) => {
+              Text(line)
+                .fontSize(12)
+                .fontColor(Color.Gray)
+            })
+          }
+        }
+        .padding({ top: 6, bottom: 6 })
+      }
+      .height(220)
+      .backgroundColor($r('app.color.input_background'))
+      .borderRadius(12)
+    }
+  }
+
+  @Builder
+  private buildInputItem(label: string, placeholder: string, value: string, onChange: (value: string) => void) {
+    Column({ space: 6 }) {
+      Text(label)
+        .fontSize(14)
+        .fontColor($r('app.color.text_color'))
+      TextInput({
+        text: value,
+        placeholder: placeholder
+      })
+        .width('100%')
+        .height(36)
+        .fontSize(14)
+        .backgroundColor($r('app.color.input_background'))
+        .borderRadius(8)
+        .padding({ left: 10, right: 10 })
+        .enabled(!this.isBusy)
+        .onChange((val: string) => {
+          onChange(val);
+        })
+    }
+  }
+
+  @Builder
+  private buildPasswordItem() {
+    Column({ space: 6 }) {
+      Text('密码')
+        .fontSize(14)
+        .fontColor($r('app.color.text_color'))
+      TextInput({
+        text: this.password,
+        placeholder: '请输入密码'
+      })
+        .width('100%')
+        .height(36)
+        .type(InputType.Password)
+        .fontSize(14)
+        .backgroundColor($r('app.color.input_background'))
+        .borderRadius(8)
+        .padding({ left: 10, right: 10 })
+        .enabled(!this.isBusy)
+        .onChange((val: string) => {
+          this.password = val;
+        })
+    }
+  }
+
+  private async listDirectory(): Promise<void> {
+    const normalizedPath = this.normalizeRemotePath(this.targetPath);
+    this.appendLog(`准备列出目录,用户输入: ${normalizedPath || '[root]'}`);
+    const parsedShare = this.parseSharePath(this.sharePath);
+    if (!parsedShare) {
+      ToastUtil.showToast('共享路径格式不正确,请检查');
+      return;
+    }
+    await this.runWithConnection(parsedShare, async (tree: SambaTree, basePath: string) => {
+      const shareRelativePath = this.combineShareRelativePath(parsedShare.basePath, normalizedPath);
+      const remotePath = this.combineRemotePath(basePath, normalizedPath);
+      const displayPath = remotePath === '/' ? '[root]' : remotePath;
+      this.appendLog(`开始列出目录: ${displayPath}`);
+      Logger.info('heanup SMB', `开始列出目录: ${displayPath}`);
+      Logger.info('heanup SMB', `readDirectory inputs => base:${parsedShare.basePath || '[root]'} relative:${normalizedPath || '[root]'} effective:${shareRelativePath || '[root]'}`);
+      const entries = await tree.readDirectory(shareRelativePath);
+      Logger.info('heanup SMB', `目录返回 ${entries.length} 项`);
+      const formatted = entries.map((entry: SambaDirectoryEntry, index: number) => {
+        const label = this.describeEntry(entry, index);
+        Logger.info('heanup SMB', `目录项: ${label}`);
+        return label;
+      });
+      this.directoryEntries = formatted;
+      if (formatted.length === 0) {
+        this.appendLog('目录为空');
+      } else {
+        this.appendLog(`目录列出完成,数量: ${formatted.length}`);
+      }
+    });
+  }
+
+  private async runWithConnection(parsedShare: ParsedSharePath, task: (tree: SambaTree, basePath: string) => Promise<void>): Promise<void> {
+    if (!this.validateCredentials()) {
+      return;
+    }
+    let client: SambaClientInstance | undefined;
+    let session: SambaSession | undefined;
+    this.isBusy = true;
+    this.appendLog(`连接 NAS: ${parsedShare.host}/${parsedShare.share}`);
+    try {
+      client = this.createClient(parsedShare.host);
+      const authOptions = this.buildAuthOptions();
+      session = await client.authenticate(authOptions);
+      if (!session) {
+        throw new Error('SMB 认证失败,session 不存在');
+      }
+      const tree = await session.connectTree(parsedShare.share);
+      this.appendLog('连接成功,开始执行 SMB 操作');
+      await task(tree, parsedShare.basePath);
+      await this.safeClose(() => tree.close?.());
+      this.appendLog('操作完成,准备释放连接');
+    } catch (error) {
+      const message = this.errorMessage(error as ErrorLike);
+      this.appendLog(`操作失败: ${message}`);
+      Logger.error('heanup SMB', `操作失败: ${message}`);
+      ToastUtil.showToast(`操作失败: ${message}`);
+    } finally {
+      await this.safeClose((): Promise<void> | void => session?.disconnect?.());
+      await this.safeClose((): Promise<void> | void => client?.close?.());
+      this.isBusy = false;
+      this.appendLog('操作流程结束,恢复可操作状态');
+    }
+  }
+
+  private createClient(host: string): SambaClientInstance {
+    return new NativeSambaClientAdapter(host);
+  }
+
+  private buildAuthOptions(): SambaAuthOptions {
+    const credentials: SambaAuthOptions = {
+      username: this.username.trim(),
+      password: this.password
+    };
+    const domainValue = this.domain.trim();
+    if (domainValue.length > 0) {
+      credentials.domain = domainValue;
+    }
+    return credentials;
+  }
+
+  private parseSharePath(rawValue: string): ParsedSharePath | undefined {
+    const trimmed = rawValue.trim();
+    if (!trimmed) {
+      return undefined;
+    }
+    const cleaned = trimmed.replace(/^\\\\+/, '').replace(/^\/+/, '');
+    const parts = cleaned.split(/\\|\//).filter(part => part.length > 0);
+    if (parts.length < 2) {
+      return undefined;
+    }
+    const host = parts[0];
+    const share = parts[1];
+    const rest = parts.slice(2);
+    return {
+      host,
+      share,
+      basePath: rest.join('/')
+    };
+  }
+
+  private combineRemotePath(basePath: string, relativePath: string): string {
+    const segments: string[] = [];
+    if (basePath) {
+      segments.push(basePath);
+    }
+    if (relativePath) {
+      segments.push(relativePath);
+    }
+    if (segments.length === 0) {
+      return '/';
+    }
+    return `/${segments.join('/')}`;
+  }
+
+  private combineShareRelativePath(basePath: string, relativePath: string): string {
+    const segments: string[] = [];
+    if (basePath) {
+      segments.push(basePath);
+    }
+    if (relativePath) {
+      segments.push(relativePath);
+    }
+    return segments.join('/');
+  }
+
+  private normalizeRemotePath(input: string | undefined): string {
+    if (!input) {
+      return '';
+    }
+    const trimmed = input.trim();
+    if (!trimmed || trimmed === '/' || trimmed === '\\') {
+      return '';
+    }
+    const cleaned = trimmed.replace(/^[\\/]+/, '').replace(/\\/g, '/');
+    return cleaned.replace(/\/{2,}/g, '/');
+  }
+
+  private describeEntry(entry: SambaDirectoryEntry, index: number): string {
+    const name = entry.name ?? entry.fileName ?? `未命名-${index}`;
+    const typeLabel = entry.isDirectory ? '[目录]' : '[文件]';
+    const size = typeof entry.size === 'number' ? ` (${entry.size}B)` : '';
+    return `${typeLabel} ${name}${size}`;
+  }
+
+  private validateCredentials(): boolean {
+    if (!this.username.trim()) {
+      ToastUtil.showToast('请填写用户名');
+      return false;
+    }
+    if (!this.password) {
+      ToastUtil.showToast('请填写密码');
+      return false;
+    }
+    return true;
+  }
+
+  private appendLog(message: string): void {
+    const time = this.formatTime(new Date());
+    const line = `[${time}] ${message}`;
+    const updated = [...this.logMessages, line];
+    const limit = 200;
+    if (updated.length > limit) {
+      updated.splice(0, updated.length - limit);
+    }
+    this.logMessages = updated;
+  }
+
+  private formatTime(date: Date): string {
+    const hours = date.getHours().toString().padStart(2, '0');
+    const minutes = date.getMinutes().toString().padStart(2, '0');
+    const seconds = date.getSeconds().toString().padStart(2, '0');
+    return `${hours}:${minutes}:${seconds}`;
+  }
+
+  private async safeClose(action?: () => Promise<void> | void): Promise<void> {
+    if (!action) {
+      return;
+    }
+    try {
+      const result = action();
+      if (this.isPromise(result)) {
+        await result;
+      }
+    } catch (error) {
+      Logger.warn('heanup SMB', `资源释放时出现异常: ${this.errorMessage(error as ErrorLike)}`);
+    }
+  }
+
+  private isPromise(value: Promise<void> | void): boolean {
+    return typeof value === 'object' && value !== null && typeof (value as Promise<void>).then === 'function';
+  }
+
+  private errorMessage(error: ErrorLike): string {
+    if (error instanceof Error) {
+      return error.message;
+    }
+    if (typeof error === 'string') {
+      return error;
+    }
+    if (typeof error === 'number' || typeof error === 'boolean') {
+      return String(error);
+    }
+    if (error && typeof error === 'object') {
+      try {
+        return JSON.stringify(error);
+      } catch (serializationError) {
+        const message = serializationError instanceof Error ? serializationError.message : '未知错误';
+        Logger.warn('heanup SMB', `序列化错误: ${message}`);
+        return '未知错误';
+      }
+    }
+    return '未知错误';
+  }
+}

+ 103 - 20
entry/src/main/ets/view/LocalMusic.ets

@@ -79,6 +79,7 @@ import { ringtone } from '@kit.RingtoneKit';
 import { BreakpointType, BreakpointTypeEnum } from '../common/util/BreakpointSystem';
 import { resourceManager } from '@kit.LocalizationKit';
 import { deviceInfo } from '@kit.BasicServicesKit';
+import { ensureSmbFileCached } from '../common/network/SmbFileCache';
 
 import { HSBColorPicker, HSBColorPickerLayout } from '@keke/color-picker'
 import app from '@system.app';
@@ -147,11 +148,74 @@ function getTypeOrder(type: number) {
       return 2; // Middle
     case CommonConstants.TYPE_LOCAL:
       return 3; // Last
+    case CommonConstants.TYPE_WEBDAV:
+    case CommonConstants.TYPE_SMB:
+      return 3;
     default:
       return 4; // Unknown types, if any, go last
   }
 }
 
+function isWebDavType(type: number): boolean {
+  return type === CommonConstants.TYPE_WEBDAV;
+}
+
+function isSmbType(type: number): boolean {
+  return type === CommonConstants.TYPE_SMB;
+}
+
+function isRemoteCloudType(type: number): boolean {
+  return isWebDavType(type) || isSmbType(type);
+}
+
+function getShareNameFromFilePath(filePath?: string): string | undefined {
+  if (!filePath) {
+    return undefined;
+  }
+  const match = filePath.match(/^smb:\/\/[^/]+\/([^/]+)/i);
+  return match && match[1] ? match[1] : undefined;
+}
+
+function removeSharePrefixFromPath(path: string, shareName?: string): string {
+  if (!shareName) {
+    return path;
+  }
+  const cleanedShare = shareName.replace(/^\/+|\/+$/g, '');
+  if (cleanedShare.length === 0) {
+    return path;
+  }
+  const lowerPath = path.toLowerCase();
+  const lowerShare = cleanedShare.toLowerCase();
+  if (lowerPath === lowerShare) {
+    return '';
+  }
+  const prefix = `${lowerShare}/`;
+  if (lowerPath.startsWith(prefix)) {
+    return path.substring(cleanedShare.length + 1);
+  }
+  return path;
+}
+
+function extractSmbRelativePath(song: VideoItem): string {
+  const shareName = getShareNameFromFilePath(song.filePath);
+  if (song.remote_rel_path && song.remote_rel_path.length > 0) {
+    const cleaned = song.remote_rel_path.replace(/^\/+/, '');
+    return removeSharePrefixFromPath(cleaned, shareName);
+  }
+  if (!song.filePath) {
+    return '';
+  }
+  const match = song.filePath.match(/^smb:\/\/[^/]+\/([^/]+)(\/.*)?$/i);
+  if (!match) {
+    return song.filePath.replace(/^\/+/, '');
+  }
+  const remainder = match[2] ? match[2].replace(/^\/+/, '') : '';
+  if (remainder.length === 0) {
+    return '';
+  }
+  return remainder;
+}
+
 /**
  * 异步设置videoUrl的辅助方法,处理WebDAV URL的构建
  * @param song 歌曲对象
@@ -164,34 +228,52 @@ function getTypeOrder(type: number) {
  * @returns Promise<string> 完整的URL
  */
 async function setVideoUrlForSong(song: VideoItem): Promise<string> {
-  if (song.type === CommonConstants.TYPE_WEBDAV && song.webdav_account_id) {
+  if (isWebDavType(song.type) && song.webdav_account_id) {
     try {
       Logger.info(TAG, `WebDAV完整URL构建成功remote_rel_path: ${song.remote_rel_path}`);
       Logger.info(TAG, `WebDAV完整URL构建成功song.filePath: ${song.filePath}`);
-      // 优先使用远端相对路径,其次回退到 filePath
       const relativePath = song.remote_rel_path || song.filePath;
       const fullUrl = await WebDavUrlUtil.buildFullUrlByAccountId(song.webdav_account_id, relativePath);
       if (fullUrl) {
         Logger.info(TAG, `WebDAV完整URL构建成功: ${fullUrl}`);
         return fullUrl.replace(/ /g, '%20');
-      } else {
-        Logger.error(TAG, `WebDAV URL构建失败,使用原始路径: ${relativePath}`);
-        return relativePath.replace(/ /g, '%20');
       }
+      Logger.error(TAG, `WebDAV URL构建失败,使用原始路径: ${relativePath}`);
+      return relativePath.replace(/ /g, '%20');
     } catch (error) {
       Logger.error(TAG, `WebDAV URL构建出错: ${(error as Error).message}`);
       const fallbackPath = song.remote_rel_path || song.filePath;
       return fallbackPath.replace(/ /g, '%20');
     }
-  } else {
-    // 非WebDAV文件或缺少webdav_account_id,直接使用原始路径
-    if (song.type === CommonConstants.TYPE_WEBDAV) {
-      Logger.warn(TAG, `WebDAV歌曲缺少webdav_account_id,使用原始路径: ${song.filePath}`);
+  }
+
+  if (isSmbType(song.type) && song.webdav_account_id) {
+    try {
+      const manager = WebdavManager.getInstance();
+      const account = await manager.getWebDavAccountById(song.webdav_account_id);
+      if (!account) {
+        throw new Error('SMB账号不可用');
+      }
+      const relativePath = extractSmbRelativePath(song);
+      const cachedPath = await ensureSmbFileCached(account, relativePath);
+      Logger.info(TAG, `SMB 缓存路径: ${cachedPath}`);
+      return cachedPath;
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `SMB 缓存失败: ${err.message}`);
+      throw new Error(err.message);
     }
-    return song.type === CommonConstants.TYPE_WEBDAV ?
-      song.filePath.replace(/ /g, '%20') :
-      song.filePath;
   }
+
+  if (isWebDavType(song.type)) {
+    Logger.warn(TAG, `WebDAV歌曲缺少webdav_account_id,使用原始路径: ${song.filePath}`);
+    return song.filePath.replace(/ /g, '%20');
+  }
+  if (isSmbType(song.type)) {
+    Logger.warn(TAG, `SMB歌曲缺少webdav_account_id,使用原始路径: ${song.filePath}`);
+    return song.filePath;
+  }
+  return song.filePath;
 }
 
 const workerInstance = new worker.ThreadWorker("entry/ets/workers/Worker.ets");
@@ -5983,8 +6065,9 @@ export struct LocalMusic {
         break;
 
       case CommonConstants.TYPE_WEBDAV:
-        // 处理网络音频播放(WebDAV)
-        Logger.info(`heanup 处理网络音频播放: ${item.name}, URL: ${item.filePath}`)
+      case CommonConstants.TYPE_SMB:
+        // 处理网络音频播放(WebDAV/SMB)
+        Logger.info(`heanup 处理云端音频播放: ${item.name}, URL: ${item.filePath}`)
 
         if (this.CONTROL_PlayStatus !== PlayStatus.INIT) {
           this.stop();
@@ -6008,7 +6091,7 @@ export struct LocalMusic {
           Logger.info(`heanup 网络音频 isFromSonPlayList分支 - 当前播放列表长度: ${this.songList.length}, 当前索引: ${this.curIndex}`)
         } else {
           // 从全局网络音频列表中查找
-          let globalVideoList = this.videoLocalList.filter(video => video.type === CommonConstants.TYPE_WEBDAV) as VideoItem[];
+          let globalVideoList = this.videoLocalList.filter(video => isRemoteCloudType(video.type)) as VideoItem[];
           this.curIndex = globalVideoList.findIndex(video => video.filePath === item.filePath);
           if (this.curIndex === -1 && index !== undefined) {
             this.curIndex = index;
@@ -6087,7 +6170,7 @@ export struct LocalMusic {
           }
           this.songList = globalVideoList
           this.sonDataSource.pushArrayData(this.songList)
-          if(this.currentSong.type==CommonConstants.TYPE_WEBDAV){
+          if(this.currentSong && isRemoteCloudType(this.currentSong.type!)){
             this.videoUrl = this.currentSong.filePath.replace(/ /g, '%20');
           }else{
             this.videoUrl =  this.currentSong.filePath
@@ -12035,7 +12118,7 @@ export struct LocalMusic {
 
   updateLastPlayTimeStr(filePath: string) {
     // 检查是否为网络音频(WebDAV),如果是则不更新本地数据库
-    if (this.currentSong && this.currentSong.type === CommonConstants.TYPE_WEBDAV) {
+    if (this.currentSong && isRemoteCloudType(this.currentSong.type)) {
       Logger.info(`heanup 检测到网络音频播放,跳过数据库更新: ${filePath}`)
       return;
     }
@@ -12243,7 +12326,7 @@ export struct LocalMusic {
     }
 
     // 如果是WebDAV网络音频,使用webdav_account_id获取认证信息(等待完成后再设置一次性头部)
-    if (this.currentSong && this.currentSong.type === CommonConstants.TYPE_WEBDAV) {
+    if (this.currentSong && isRemoteCloudType(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 {
@@ -12265,7 +12348,7 @@ export struct LocalMusic {
     }
     // 统一设置带认证的请求头
     this.mIjkMediaPlayer.setDataSourceHeader(headers);
-    if (this.currentSong&&this.currentSong.type === CommonConstants.TYPE_WEBDAV) {
+    if (this.currentSong && isRemoteCloudType(this.currentSong.type)) {
       console.log(`heanup 为WebDAV播放设置IjkPlayer选项`);
       // 网络超时设置(单位:微秒,文档显示应该是字符串格式)
       this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "30000000"); // 30秒
@@ -12588,7 +12671,7 @@ export struct LocalMusic {
         LogUtils.getInstance().LOGI('heanup 播放错误,歌曲详情:' + JSON.stringify(this.currentSong))
         // 检查是否为WebDAV播放错误
         let isWebDavError = false;
-        if (this.currentSong && this.currentSong.type === CommonConstants.TYPE_WEBDAV && this.currentSong.filePath) {
+        if (this.currentSong && isRemoteCloudType(this.currentSong.type) && this.currentSong.filePath) {
           try {
             const globalContext = GlobalContext.getContext();
             const webDavAuthInfo = globalContext.getObject('webDavAuthInfo') as WebDavAuthItem;

+ 4 - 4
entry/src/main/ets/viewmodel/Song.ets

@@ -8,8 +8,8 @@ export class Song{
   // 歌曲索引
   public id: number = -1
   // 云盘歌曲还是本地歌曲
-  public type: SongType = SongType.WebDav
-  public songType: SongType = SongType.WebDav
+  public type: SongType = SongType.Local
+  public songType: SongType = SongType.Local
   // WebDav所属账号
   public WebDavAccountId: number = -1
   public webDavAccountId: number = -1
@@ -61,7 +61,7 @@ export class Song{
   public static  equal(song1: Song,song2: Song):boolean{
     if(song1.type === song2.type && song1.type === SongType.Local){
       return song1.localFilePath === song2.localFilePath && song1.name === song2.name
-    }else if(song1.type === song2.type && song2.type === SongType.WebDav){
+    }else if(song1.type === song2.type && (song2.type === SongType.WebDav || song2.type === SongType.Smb)){
       return song1.WebDavAccountId === song2.WebDavAccountId && song1.name === song2.name && song1.webFilePath === song2.webFilePath
     }else{
       return false
@@ -90,4 +90,4 @@ export class Song{
     return clonedSong;
   }
 
-}
+}

+ 5 - 2
entry/src/main/ets/viewmodel/WebDavAccount.ets

@@ -1,4 +1,5 @@
 import { Constants } from "../Constants";
+import { RemoteDriveType } from "../common/enums/RemoteDriveType";
 import { FileInfo } from "./FileInfo";
 
 @Observed
@@ -23,9 +24,11 @@ export class WebDavAccount{
   public imageFilePaths: string[] = []
   // 自定义封面路径
   public coverPath?: string
-  public webType : number = 0
+  public webType : number = RemoteDriveType.WebDav
+  public smbShare: string = ''
+  public smbDomain: string = ''
 
   public setIsUseLocalHost(isuse: boolean){
     this.isUseLocalHost = isuse
   }
-}
+}