Kaynağa Gözat

一键创建歌单

chendeben 9 ay önce
ebeveyn
işleme
d9d8d7cf9f

+ 92 - 2
entry/src/main/ets/common/util/MediaTable.ets

@@ -1,10 +1,12 @@
 import relationalStore from '@ohos.data.relationalStore';
 import { LogUtil, StrUtil } from '@pura/harmony-utils';
 import { VideoItem } from '../../viewmodel/VideoItem';
+import { WebDavAccount } from '../../viewmodel/WebDavAccount';
 import { CommonConstants } from '../constants/CommonConstants';
 import Logger from './Logger';
 import RdbUtils from './RdbUtils';
 import { AudioQuality, Utility } from './Utility';
+import { WebDavUrlUtil } from './WebDavUrlUtil';
 
 /**
  * 数据库字段常量接口定义
@@ -31,6 +33,7 @@ interface DBColumnsInterface {
   PLAY_COUNT: string;
   LYRIC_CONTENT: string;
   WEBDAV_ACCOUNT_ID: string;
+  REMOTE_REL_PATH: string;
 }
 
 /**
@@ -57,10 +60,11 @@ const DB_COLUMNS: DBColumnsInterface = {
   LAST_PLAYED_STR: 'lastPlayedStr',
   PLAY_COUNT: 'playCount',
   LYRIC_CONTENT: 'lyricContent',
-  WEBDAV_ACCOUNT_ID: 'webdav_account_id'
+  WEBDAV_ACCOUNT_ID: 'webdav_account_id',
+  REMOTE_REL_PATH: 'remote_rel_path'
 };
 
-export default class MediaTable {
+export default  class MediaTable {
   private accountTable = new RdbUtils(RdbUtils.MEDIA_TABLE.tableName, RdbUtils.MEDIA_TABLE.sqlCreate,
     RdbUtils.MEDIA_TABLE.columns);
 
@@ -69,6 +73,89 @@ export default class MediaTable {
     this.accountTable.getRdbStore(context,callback);
   }
 
+  /**
+   * 判断指定 filePath 是否已存在
+   */
+  private async existsByFilePath(filePath: string): Promise<boolean> {
+    return new Promise<boolean>((resolve, reject) => {
+      try {
+        const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
+        predicates.equalTo(DB_COLUMNS.FILE_PATH, filePath);
+        this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
+          try {
+            resolve(resultSet.rowCount > 0);
+          } catch (e) {
+            reject(e);
+          } finally {
+            resultSet.close();
+          }
+        });
+      } catch (err) {
+        reject(err);
+      }
+    });
+  }
+
+  /**
+   * WebDAV歌曲入库(若不存在则插入)
+   */
+  public async upsertWebDavVideoItem(item: VideoItem): Promise<boolean> {
+    try {
+      if (!item || !item.filePath) {
+        return false;
+      }
+
+      // 将完整URL转换为相对路径用于存储
+      const relativePath = WebDavUrlUtil.toStoragePath(item.filePath);
+      if (relativePath !== item.filePath) {
+        // 只有当转换成功时才设置remote_rel_path
+        item.remote_rel_path = relativePath;
+        // 更新filePath为相对路径
+        item.filePath = relativePath;
+        Logger.info(RdbUtils.RDB_TAG, `WebDAV URL转换: 原始URL -> 存储路径: ${relativePath}`);
+      }
+
+      const exists = await this.existsByFilePath(item.filePath);
+      if (exists) {
+        return true; // 已存在不再重复插入
+      }
+      return await this.insertWebDavItem(item);
+    } catch (e) {
+      Logger.error(RdbUtils.RDB_TAG, 'upsertWebDavVideoItem 失败: ' + (e as Error).message);
+      return false;
+    }
+  }
+
+  /**
+   * 插入WebDAV歌曲的最小字段集合
+   */
+  private async insertWebDavItem(item: VideoItem): Promise<boolean> {
+    return new Promise<boolean>((resolve) => {
+      try {
+        const bucket: relationalStore.ValuesBucket = generateBucket(item);
+        if (!bucket.id) { // 使用filePath作为主键/ID以避免为空
+          bucket.id = item.filePath;
+        }
+        if (!bucket.parentPath) {
+          // 以路径去掉文件名部分作为 parentPath(已经是相对路径)
+          const idx = item.filePath.lastIndexOf('/');
+          if (idx > 0) {
+            bucket.parentPath = item.filePath.substring(0, idx);
+          }
+        }
+        if (item.remote_rel_path) {
+          bucket.remote_rel_path = item.remote_rel_path;
+        }
+        this.accountTable.insertData(bucket, (success: boolean) => {
+          resolve(success);
+        });
+      } catch (e) {
+        Logger.error(RdbUtils.RDB_TAG, 'insertWebDavItem 出错: ' + (e as Error).message);
+        resolve(false);
+      }
+    });
+  }
+
   getRdbStore(context:Context,callback: Function = () => {
   }) {
     this.accountTable.getRdbStore(context,callback);
@@ -997,6 +1084,9 @@ function generateBucket(item: VideoItem): relationalStore.ValuesBucket {
   if(item.webdav_account_id){
     obj.webdav_account_id = item.webdav_account_id;
   }
+  if(item.remote_rel_path){
+    obj.remote_rel_path = item.remote_rel_path;
+  }
 
   return obj;
 }

+ 0 - 1
entry/src/main/ets/common/util/RcpSocketUtil.ets

@@ -309,7 +309,6 @@ export class RcpSocket {
       const encodedCredentials = buffer
         .from(`${account}:${password}`)
         .toString("base64");
-      console.info(UtilName, 'testTag', account, password)
 
 
       const headers: rcp.RequestHeaders = {

+ 4 - 1
entry/src/main/ets/common/util/RdbUtils.ets

@@ -81,6 +81,7 @@ export default class RdbUtils {
       '        COMMENT TEXT,\n' +
       '        disc TEXT,\n' +
       '        webdav_account_id TEXT,\n' +
+      '        remote_rel_path TEXT,\n' +
 
       '        mimeType TEXT' +
       ')',
@@ -90,6 +91,7 @@ export default class RdbUtils {
       'lyricContent','md5Str','extra_json','pyStr','bit_rate','probe_score','year','nb_streams','nb_programs',
       'genre','track',  'bits_per_raw_sample','channels',  'channel_layout','start_time',
       'ALBUMARTIST',  'COMPOSER','LYRICIST','COMMENT','COMMENT','disc','webdav_account_id',
+      'remote_rel_path',
       'mimeType']
   };
 
@@ -218,6 +220,7 @@ export default class RdbUtils {
             'COMMENT': 'TEXT',
             'disc': 'TEXT',
             'webdav_account_id': 'TEXT',
+            'remote_rel_path': 'TEXT',
           };
 
           // 逐个添加列,不依赖于检查结果
@@ -417,4 +420,4 @@ export default class RdbUtils {
     }
   }
 
-}
+}

+ 169 - 0
entry/src/main/ets/common/util/WebDavUrlUtil.ets

@@ -0,0 +1,169 @@
+// WebDavUrlUtil - WebDAV URL处理工具类
+import { WebDavAccount } from '../../viewmodel/WebDavAccount';
+import Logger from './Logger';
+import { DataBaseUtil } from './DataBaseUtil';
+import { relationalStore } from '@kit.ArkData';
+
+const TAG = 'heanup WebDavUrlUtil';
+
+/**
+ * WebDAV URL处理工具类
+ */
+export class WebDavUrlUtil {
+
+  /**
+   * 从完整URL中提取相对路径(去掉协议、主机和端口)
+   * @param fullUrl 完整的URL,例如: https://example.com:8080/music/song.mp3
+   * @returns 相对路径,例如: /music/song.mp3,如果不是HTTP/HTTPS URL则返回null
+   */
+  public static extractRelativePath(fullUrl: string): string | null {
+    if (!fullUrl) return null;
+    const match = fullUrl.match(/^https?:\/\/[^\/]+(?::\d+)?(\/.*)$/i);
+    return match ? match[1] : null;
+  }
+
+  /**
+   * 根据WebDAV账号配置和相对路径拼接完整URL
+   * @param account WebDAV账号配置
+   * @param relativePath 相对路径,例如: /music/song.mp3 或 music/song.mp3
+   * @returns 完整的URL,例如: https://example.com:8080/music/song.mp3
+   */
+  public static buildFullUrl(account: WebDavAccount, relativePath: string): string {
+    if (!account || !relativePath) {
+      Logger.error(TAG, 'buildFullUrl: 账号或相对路径为空');
+      return '';
+    }
+
+    // 确保relativePath以/开头
+    const normalizedPath = relativePath.startsWith('/') ? relativePath : '/' + relativePath;
+
+    // 选择使用的主机地址
+    const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+
+    // 构建基础URL(协议+主机+端口)
+    const protocol = account.enableHttps ? 'https' : 'http';
+    const port = account.port !== 80 && account.port !== 443 ? `:${account.port}` : '';
+    const baseUrl = `${protocol}://${host}${port}`;
+
+    // 拼接完整URL
+    const fullUrl = baseUrl + normalizedPath;
+
+    Logger.info(TAG, `构建完整URL: ${fullUrl}`);
+    return fullUrl;
+  }
+
+  /**
+   * 根据webdav_account_id和相对路径拼接完整URL
+   * @param accountId WebDAV账号ID
+   * @param relativePath 相对路径
+   * @returns 完整的URL,如果账号不存在则返回空字符串
+   */
+  public static async buildFullUrlByAccountId(accountId: string, relativePath: string): Promise<string> {
+    if (!accountId || !relativePath) {
+      Logger.error(TAG, 'buildFullUrlByAccountId: 账号ID或相对路径为空');
+      return '';
+    }
+
+    try {
+      const account = await WebDavUrlUtil.getAccountById(accountId);
+      if (!account) {
+        Logger.error(TAG, `buildFullUrlByAccountId: 找不到ID为${accountId}的账号`);
+        return '';
+      }
+
+      return WebDavUrlUtil.buildFullUrl(account, relativePath);
+    } catch (error) {
+      Logger.error(TAG, `buildFullUrlByAccountId: 查询账号失败 - ${(error as Error).message}`);
+      return '';
+    }
+  }
+
+  /**
+   * 检查URL是否为HTTP/HTTPS URL
+   * @param url 要检查的URL
+   * @returns true如果是HTTP/HTTPS URL
+   */
+  public static isHttpUrl(url: string): boolean {
+    if (!url) return false;
+    return /^https?:\/\//i.test(url);
+  }
+
+  /**
+   * 从数据库查询WebDAV账号信息
+   * @param accountId 账号ID
+   * @returns WebDAV账号信息,如果不存在则返回null
+   */
+  private static async getAccountById(accountId: string): Promise<WebDavAccount | null> {
+    try {
+      const dataBaseUtil = DataBaseUtil.getInstance();
+      const webDavTable = 'WebDavAccount';
+
+      const predicates = new relationalStore.RdbPredicates(webDavTable);
+      predicates.equalTo('id', accountId);
+
+      const columns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
+        'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
+        'account', 'password', 'enableHttps'];
+
+      const resultSet = await dataBaseUtil.queryData(webDavTable, columns, predicates);
+
+      if (resultSet.rowCount === 0) {
+        resultSet.close();
+        Logger.warn(TAG, `getAccountById: 找不到ID为${accountId}的账号`);
+        return null;
+      }
+
+      resultSet.goToFirstRow();
+      const account = new WebDavAccount();
+      account.id = resultSet.getLong(resultSet.getColumnIndex('id'));
+      account.name = resultSet.getString(resultSet.getColumnIndex('name'));
+      account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
+      account.host = resultSet.getString(resultSet.getColumnIndex('host'));
+      account.localHost = resultSet.getString(resultSet.getColumnIndex('localHost'));
+      account.isUseLocalHost = resultSet.getLong(resultSet.getColumnIndex('isUseLocalHost')) === 1;
+      account.port = resultSet.getLong(resultSet.getColumnIndex('port'));
+      account.filepath = resultSet.getString(resultSet.getColumnIndex('filepath'));
+      account.imageFilePath = resultSet.getString(resultSet.getColumnIndex('imageFilePath'));
+      account.lyricFilePath = resultSet.getString(resultSet.getColumnIndex('lyricFilePath'));
+      account.uploadFilePath = resultSet.getString(resultSet.getColumnIndex('uploadFilePath'));
+      account.account = resultSet.getString(resultSet.getColumnIndex('account'));
+      account.password = resultSet.getString(resultSet.getColumnIndex('password'));
+      account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
+
+      resultSet.close();
+
+      Logger.info(TAG, `getAccountById: 成功获取账号信息 - ${account.name} (${account.host})`);
+      return account;
+
+    } catch (error) {
+      Logger.error(TAG, `getAccountById: 查询账号失败 - ${(error as Error).message}`);
+      return null;
+    }
+  }
+
+  /**
+   * 将完整的URL转换为相对路径用于存储
+   * @param fullUrl 完整URL
+   * @returns 相对路径,如果不是HTTP/HTTPS URL则返回原始URL
+   */
+  public static toStoragePath(fullUrl: string): string {
+    const relativePath = WebDavUrlUtil.extractRelativePath(fullUrl);
+    return relativePath || fullUrl;
+  }
+
+  /**
+   * 将存储的路径转换为完整URL用于访问
+   * @param storagePath 存储的路径(可能是相对路径或完整URL)
+   * @param account WebDAV账号配置
+   * @returns 完整的URL
+   */
+  public static toAccessUrl(storagePath: string, account: WebDavAccount): string {
+    // 如果存储的路径已经是完整URL,直接返回
+    if (WebDavUrlUtil.isHttpUrl(storagePath)) {
+      return storagePath;
+    }
+
+    // 如果是相对路径,需要拼接完整URL
+    return WebDavUrlUtil.buildFullUrl(account, storagePath);
+  }
+}

+ 11 - 4
entry/src/main/ets/common/util/WebdavManager.ets

@@ -776,8 +776,7 @@ export class WebdavManager {
 
     // 构建基础URL,确保路径重新编码(目录/文件名中的中文与空格)
     const encodedHref = encodeURI(fileInfo.href); // 仅编码非保留字符,已编码的百分号不重复编码
-    const filePath = `${protocol}://${host}:${port}${encodedHref}`;
-    Logger.info(TAG, '编码后的WebDAV URL: ' + filePath);
+    const absoluteUrl = `${protocol}://${host}:${port}${encodedHref}`;
 
     // 创建VideoItem对象
     // 构造函数签名: (name, id, filePath, type, videoSize, cTime, pixelMap?, size?, pixelMapPath?, artist?, album?, fileName?, lastPlayed?)
@@ -785,7 +784,7 @@ export class WebdavManager {
     const videoItem = new VideoItem(
       this.getFileNameWithoutExtension(fileInfo.fileName), // name: 歌曲名
       '', // id: 空字符串,WebDAV文件无本地ID
-      filePath, // filePath: 文件路径
+      absoluteUrl, // filePath: 绝对路径
       CommonConstants.TYPE_WEBDAV, // type: WebDAV类型
       fileInfo.contentLength, // videoSize: 文件大小
       Utility.getFormatDateStr(fileInfo.time,'yyyy-MM-dd HH:mm'), // cTime: 修改时间
@@ -808,7 +807,15 @@ export class WebdavManager {
     // 设置WebDAV账号ID,用于后续认证信息查询
     if (account && account.id) {
       videoItem.webdav_account_id = account.id.toString();
-      Logger.info(TAG, `设置WebDAV歌曲 ${videoItem.name} 的账号ID: ${videoItem.webdav_account_id}`);
+    }
+    // 计算并保存相对路径
+    try {
+      const relMatch = absoluteUrl.match(/^https?:\/\/[^\/]+(?::\d+)?(\/.*)$/i);
+      if (relMatch && relMatch[1]) {
+        videoItem.remote_rel_path = relMatch[1];
+      }
+    } catch (e) {
+      Logger.warn(TAG, '计算相对路径失败: ' + (e as Error).message);
     }
 
     return videoItem;

+ 1 - 1
entry/src/main/ets/dialog/AddSongsToPlaylistDialog.ets

@@ -1,6 +1,6 @@
 import { DialogHelper } from '@pura/harmony-dialog';
 import { Playlist } from '../viewmodel/Playlist';
-import { ToastUtil, LogUtil, ArrayUtil, StrUtil } from '@pura/harmony-utils';
+import { ToastUtil, LogUtil, StrUtil } from '@pura/harmony-utils';
 import { VideoItem } from '../viewmodel/VideoItem';
 import MediaTable from '../common/util/MediaTable';
 import PlaylistTable from '../common/util/PlaylistTable';

+ 2 - 6
entry/src/main/ets/pages/PlaylistDetailPage.ets

@@ -1,17 +1,15 @@
 import { Playlist, PlaylistSong } from '../viewmodel/Playlist';
 import { VideoItem } from '../viewmodel/VideoItem';
 import PlaylistTable from '../common/util/PlaylistTable';
-import MediaTable from '../common/util/MediaTable';
+import  MediaTable from '../common/util/MediaTable';
 import { emitter } from '@kit.BasicServicesKit';
-import { ToastUtil, AppUtil, LogUtil, PreferencesUtil, StrUtil } from '@pura/harmony-utils';
+import { ToastUtil, LogUtil, PreferencesUtil, StrUtil } from '@pura/harmony-utils';
 import { showEditPlaylistDialog } from '../dialog/PlaylistDialog';
 import { showAddSongsToPlaylistDialog } from '../dialog/AddSongsToPlaylistDialog';
 import { curves, router, SymbolGlyphModifier } from '@kit.ArkUI';
-import { GlobalContext } from '../common/util/GlobalContext';
 import { CommonConstants } from '../common/constants/CommonConstants';
 import { EventConstants } from '../common/constants/EventConstants';
 import { common } from '@kit.AbilityKit';
-import { ImagePickerUtil } from '../common/util/ImagePickerUtil';
 import { LazyDataSource } from '../common/util/LazyDataSource';
 import { ConfigurationConstant } from '@kit.AbilityKit';
 
@@ -1437,13 +1435,11 @@ export async function convertPlaylistSongsToVideoItems(context: Context,playlist
 
   for (const playlistSong of playlistSongs) {
     try {
-      LogUtil.info(`heanup 查询歌曲: ${playlistSong.songFilePath}`)
 
       // 从数据库查询完整的歌曲信息
       const videoItem = await mediaTable.queryVideoByFilePath(playlistSong.songFilePath)
 
       if (videoItem) {
-        LogUtil.info(`heanup 找到歌曲: ${videoItem.name}`)
         videoItems.push(videoItem)
       } else {
         LogUtil.warn(`heanup 数据库中未找到歌曲,已被删除,开始清理: ${playlistSong.songFilePath}`)

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

@@ -349,6 +349,125 @@ export struct WebDavMainPage {
     }
   }
 
+  /**
+   * 一键创建歌单:将当前WebDAV歌曲全部加入新歌单
+   */
+  private async createPlaylistFromCurrentWebDav(): Promise<void> {
+    try {
+      Logger.info(TAG, 'heanup 一键创建歌单开始');
+      if (!this.selectedAccount || !this.selectedAccount.id) {
+        this.getUIContext().getPromptAction().showToast({ message: '请先选择WebDAV账户' });
+        return;
+      }
+      if (!this.songs || this.songs.length === 0) {
+        // 回退到manager内的歌曲(可能还未复制到页面state)
+        this.songs = this.webdavManager.webDavSongs;
+        Logger.info(TAG, `heanup 页面songs为空,回退webdavManager.webDavSongs,长度=${this.songs.length}`);
+        if (this.songs.length === 0) {
+          this.getUIContext().getPromptAction().showToast({ message: '当前目录没有可添加的歌曲' });
+          return;
+        }
+      }
+      Logger.info(TAG, `heanup 歌单创建前歌曲数量: ${this.songs.length}`);
+
+      // 确保每首歌都有webdav_account_id
+      for (let i = 0; i < this.songs.length; i++) {
+        if (!this.songs[i].webdav_account_id) {
+          this.songs[i].webdav_account_id = this.selectedAccount.id.toString();
+        }
+      }
+
+      // 构建歌单名称:账户名 + 当前路径(简化)
+      const rawPath = this.webdavManager.currentPath || '/';
+      const shortPath = rawPath === '/' ? '根目录' : rawPath.replace(/\/$/, '').split('/').slice(-1)[0];
+      const playlistName = `WebDAV-${this.selectedAccount.name}-${shortPath}`;
+
+      // 创建歌单
+      // 安全获取HostContext
+      const uiContext = this.getUIContext();
+      const hostCtx = uiContext ? uiContext.getHostContext() : undefined;
+      if (!hostCtx) {
+        this.getUIContext().getPromptAction().showToast({ message: '无法获取上下文' });
+        return;
+      }
+      const playlistModule = await import('../common/util/PlaylistTable');
+      const mediaModule = await import('../common/util/MediaTable');
+      const playlistTable = new playlistModule.default(hostCtx);
+      const mediaTable = new mediaModule.default(hostCtx);
+
+      // 等待MediaTable底层RDB初始化完成
+      await new Promise<void>((resolve) => {
+        mediaTable.getRdbStore(hostCtx, () => {
+          Logger.info(TAG, 'heanup mediaTable RDB 初始化完成');
+          resolve();
+        });
+      });
+
+      // 先将WebDAV歌曲入库(若不存在)
+      let upsertSuccess = 0;
+      for (let i = 0; i < this.songs.length; i++) {
+        const v = this.songs[i];
+        if (!v.id || v.id === '') {
+          // 使用filePath作为唯一ID
+            v.id = v.filePath;
+        }
+        if (!v.parentPath) {
+          const idxp = v.filePath.lastIndexOf('/');
+          if (idxp > 0) {
+            v.parentPath = v.filePath.substring(0, idxp);
+          }
+        }
+        const ok = await mediaTable.upsertWebDavVideoItem(v);
+        Logger.info(TAG, `heanup upsert 第${i+1}/${this.songs.length}首: ${v.filePath} => ${ok}`);
+        if (ok) {
+          upsertSuccess++;
+        }
+      }
+      Logger.info(TAG, `heanup WebDAV歌曲入库完成: 成功 ${upsertSuccess}/${this.songs.length}`);
+      // 先查询是否已有同名歌单,避免重复创建导致混淆
+      const existing = (await playlistTable.queryAllPlaylists()).find(p => p.name === playlistName);
+      if (existing) {
+        this.getUIContext().getPromptAction().showToast({ message: '歌单已存在,直接追加歌曲' });
+        const filePathsExist: string[] = this.songs.map(s => s.filePath);
+        await playlistTable.addSongsToPlaylist(existing.id, filePathsExist);
+        router.pushUrl({ url: 'pages/PlaylistDetailPage', params: { playlist: existing } });
+        return;
+      }
+      Logger.info(TAG, `heanup 准备创建歌单: ${playlistName}`);
+      const created = await playlistTable.createPlaylist(playlistName, `来自WebDAV账户: ${this.selectedAccount.name} 路径: ${rawPath}`);
+      if (!created) {
+        this.getUIContext().getPromptAction().showToast({ message: '歌单创建失败' });
+        return;
+      }
+
+      // 查询刚创建的歌单ID
+      const playlists = await playlistTable.queryAllPlaylists();
+      const target = playlists.reverse().find(p => p.name === playlistName); // 取最近创建的同名歌单
+      if (!target) {
+        this.getUIContext().getPromptAction().showToast({ message: '无法找到新建歌单' });
+        return;
+      }
+
+      // 批量添加歌曲
+      const filePaths: string[] = this.songs.map(s => s.filePath);
+      Logger.info(TAG, `heanup 开始批量添加歌曲到歌单: ${target.id}`);
+      const addResult = await playlistTable.addSongsToPlaylist(target.id, filePaths);
+      Logger.info(TAG, `heanup 批量添加结果: ${addResult}`);
+      if (!addResult) {
+        Logger.warn(TAG, '批量添加歌曲返回false,可能全部已存在或写入失败');
+      }
+
+      this.getUIContext().getPromptAction().showToast({ message: `歌单创建成功: ${playlistName}` });
+      Logger.info(TAG, `heanup 一键创建歌单成功: ${playlistName}, 添加 ${filePaths.length} 首歌曲`);
+
+      // 跳转到歌单详情页面
+      router.pushUrl({ url: 'pages/PlaylistDetailPage', params: { playlist: target } });
+    } catch (error) {
+      Logger.error(TAG, '一键创建歌单失败: ' + (error as Error).message);
+      this.getUIContext().getPromptAction().showToast({ message: '一键创建歌单失败' });
+    }
+  }
+
 
   // 导航到指定层级的面包屑路径
   private navigateToBreadcrumb(breadcrumbIndex: number): void {
@@ -538,7 +657,7 @@ export struct WebDavMainPage {
         }
 
         // 统计信息
-        if (this.webDavFiles.length > 0) {
+      if (this.webDavFiles.length > 0) {
           Row() {
             Text(this.visibleFoldersState.length + ' 个文件夹, ' + this.songs.length + ' 首歌曲')
               .fontSize(13)
@@ -547,6 +666,18 @@ export struct WebDavMainPage {
               .layoutWeight(1)
               .textAlign(TextAlign.Start)
             Blank()
+            Button('一键创建歌单')
+              .type(ButtonType.Normal)
+              .fontSize(13)
+              .backgroundColor(this.themeColor)
+              .fontColor(Color.White)
+              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
+              .borderRadius(16)
+              .visibility(this.songs.length > 0 ? Visibility.Visible : Visibility.None)
+              .onClick(() => {
+                Logger.info(TAG, 'heanup 点击一键创建歌单按钮');
+                this.createPlaylistFromCurrentWebDav();
+              })
           }
           .padding({ left: 4, right: 4 })
         }

+ 59 - 28
entry/src/main/ets/view/LocalMusic.ets

@@ -1,18 +1,16 @@
 import TitleBar from './TitleBar'
 import { curves, display, PiPWindow, promptAction, router, SymbolGlyphModifier, window } from '@kit.ArkUI';
 import { VideoItem } from '../viewmodel/VideoItem';
-import {  LengthMetrics, SegmentButton,borderRadiuses, SegmentButtonOptions } from '@kit.ArkUI';
-import { getWebdavVideoItems, getWebdavCurrentPlayIndex, clearWebdavVideoItems } from '../pages/WebDavMainPage';
+import {  LengthMetrics, SegmentButton,SegmentButtonOptions } from '@kit.ArkUI';
+import { getWebdavVideoItems, getWebdavCurrentPlayIndex } from '../pages/WebDavMainPage';
 import {
   AppUtil,
   ArrayUtil,
   Base64Util,
   DateUtil,
   DeviceUtil,
-  DisplayUtil,
   FileUtil,
   GlobalContext,
-  ImageUtil,
   LogUtil,
   MD5,
   PreferencesUtil,
@@ -33,7 +31,6 @@ import { common, ConfigurationConstant } from '@kit.AbilityKit';
 import { AnimationHelper, DialogAction, DialogHelper } from '@pura/harmony-dialog';
 import { fileIo, fileUri, picker } from '@kit.CoreFileKit';
 import { MessageEvents, util, worker, ErrorEvent } from '@kit.ArkTS';
-import Base64 from '@ohos.util';
 import { Verify } from './Verify';
 import { taskpool } from '@kit.ArkTS';
 import { RotatingCover } from './RotatingCover';
@@ -45,7 +42,6 @@ import { AvSessionController } from '../controller/AvSessionController';
 import { repairAudioMetadata, convertDsfToWav, getApiLyric,changeMusicCover} from '../common/util/MusicTagUtils';
 import { FFMpegTags,Utility } from '../common/util/Utility';
 import { TagsContentCover } from '../view/TagsContentCover';
-import { ImageFancyModifier } from '../common/util/AttributeModifierUtil'
 import {
   // DeviceChangeReason,
   IjkMediaPlayer,
@@ -71,7 +67,7 @@ import { secondToTime,getTransverterText } from '../common/util/CommUtils';
 import { CommonConstants2 } from '../common/util/CommonConstants2';
 import { hilog } from '@kit.PerformanceAnalysisKit';
 import { LazyDataSource } from '../common/util/LazyDataSource';
-import MediaTable from '../common/util/MediaTable';
+import  MediaTable  from '../common/util/MediaTable';
 import NetAxiosUtil from '../common/util/NetAxiosUtil';
 import ImageUtils from '../common/util/ImageUtils';
 import { ringtone } from '@kit.RingtoneKit';
@@ -95,11 +91,8 @@ import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog';
 import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
 import { convertPlaylistSongsToVideoItems, emptyView } from '../pages/PlaylistDetailPage';
 import { Playlist, PlaylistSong } from '../viewmodel/Playlist';
-import { Song } from '../viewmodel/Song';
-import { SongType } from '../common/enums/SongType';
-import { WebdavManager, WebDavAuthInfo as WebDavManagerAuthInfo,
-  buildHttpHeadersWithWebDav,
-  WebDavAuthItem} from '../common/util/WebdavManager';
+import { WebdavManager, WebDavAuthItem} from '../common/util/WebdavManager';
+import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
 const TAG = 'LocalMusic';
 
 /**
@@ -134,6 +127,43 @@ function getTypeOrder(type: number) {
   }
 }
 
+/**
+ * 异步设置videoUrl的辅助方法,处理WebDAV URL的构建
+ * @param song 歌曲对象
+ * @returns Promise<string> 完整的URL
+ */
+
+/**
+ * 设置videoUrl,通过webdav_account_id设置完整的 URL
+ * @param song 歌曲对象
+ * @returns Promise<string> 完整的URL
+ */
+async function setVideoUrlForSong(song: VideoItem): Promise<string> {
+  if (song.type === CommonConstants.TYPE_WEBDAV && song.webdav_account_id) {
+    try {
+      const fullUrl = await WebDavUrlUtil.buildFullUrlByAccountId(song.webdav_account_id, song.filePath);
+      if (fullUrl) {
+        Logger.info(TAG, `WebDAV完整URL构建成功: ${fullUrl}`);
+        return fullUrl.replace(/ /g, '%20');
+      } else {
+        Logger.error(TAG, `WebDAV URL构建失败,使用原始路径: ${song.filePath}`);
+        return song.filePath.replace(/ /g, '%20');
+      }
+    } catch (error) {
+      Logger.error(TAG, `WebDAV URL构建出错: ${(error as Error).message}`);
+      return song.filePath.replace(/ /g, '%20');
+    }
+  } else {
+    // 非WebDAV文件或缺少webdav_account_id,直接使用原始路径
+    if (song.type === CommonConstants.TYPE_WEBDAV) {
+      Logger.warn(TAG, `WebDAV歌曲缺少webdav_account_id,使用原始路径: ${song.filePath}`);
+    }
+    return song.type === CommonConstants.TYPE_WEBDAV ?
+      song.filePath.replace(/ /g, '%20') :
+      song.filePath;
+  }
+}
+
 const workerInstance = new worker.ThreadWorker("entry/ets/workers/Worker.ets");
 // 定义接口
 interface HiCarAspectRatio {
@@ -1095,7 +1125,7 @@ export struct LocalMusic {
       FileUtil.mkdirSync(this.historyPath)
     }
 
-    this.getSortedFiles(this.rootPath).then(() => {
+    this.getSortedFiles(this.rootPath).then(async () => {
       this.isFavMusic = false
       //穿山甲
       // this.loadBannerAd(CSJUtil.getBannerID())
@@ -1114,11 +1144,9 @@ export struct LocalMusic {
           this.isFirstStartPlay = false
           this.currentSong = this.songList[0]
         }
-        if(this.currentSong.type==CommonConstants.TYPE_WEBDAV){
-          this.videoUrl = this.currentSong.filePath.replace(/ /g, '%20');
-        }else{
-          this.videoUrl =  this.currentSong.filePath
-        }
+        // 使用辅助方法设置videoUrl,等待WebDAV URL构建完成
+        this.videoUrl = await setVideoUrlForSong(this.currentSong);
+        Logger.info(TAG, `WebDAV URL同步构建完成: ${this.videoUrl}`);
         this.name = this.currentSong.name
         this.cover = this.currentSong.pixelMapPath
         AppStorage.setOrCreate('currentSong',this.currentSong) ;
@@ -5548,13 +5576,16 @@ export struct LocalMusic {
         }
 
         AppStorage.setOrCreate('currentSong', this.currentSong);
-        // 对WebDAV URL进行编码处理,确保空格等特殊字符被正确编码
-        this.videoUrl = this.currentSong.filePath.replace(/ /g, '%20');
+
+        // 使用辅助方法设置videoUrl,等待WebDAV URL构建完成
+        this.videoUrl = await setVideoUrlForSong(this.currentSong);
+        Logger.info(TAG, `WebDAV URL同步构建完成(分支2): ${this.videoUrl}`);
+
         this.name = this.currentSong.name
         this.cover = this.currentSong.pixelMapPath
         this.artist = this.currentSong.artist
 
-        Logger.info(`heanup 准备播放网络音频 - 歌名: ${this.name}, 艺术家: ${this.artist}, 编码后URL: ${this.videoUrl}`)
+        Logger.info(`heanup 准备播放网络音频 - 歌名: ${this.name}, 艺术家: ${this.artist}, 完整URL: ${this.videoUrl}`)
 
         this.startPlayOrResumePlay()
         break;
@@ -12074,7 +12105,7 @@ export struct LocalMusic {
       onError: (what: number, extra: number) => {
         this.stopProgressTask();
         LogUtils.getInstance().LOGI("OnErrorListener-->go:" + what + "===" + extra + " this.videoUrl=" + this.videoUrl)
-
+        LogUtils.getInstance().LOGI('heanup 播放错误,歌曲详情:' + JSON.stringify(this.currentSong))
         // 检查是否为WebDAV播放错误
         let isWebDavError = false;
         if (this.currentSong && this.currentSong.type === CommonConstants.TYPE_WEBDAV && this.currentSong.filePath) {
@@ -13404,27 +13435,27 @@ export struct LocalMusic {
       for (let i = 0; i < songFilePaths.length; i++) {
         const filePath = songFilePaths[i]
         const index = i
-        Logger.info(`heanup 正在查询歌曲[${index}]: ${filePath}`)
+        // Logger.info(`heanup 正在查询歌曲[${index}]: ${filePath}`)
 
         // 使用已初始化的MediaTable实例从数据库查询歌曲信息
         const queryPromise = this.table.queryVideoByFilePath(filePath)
-        Logger.info(`heanup 创建了查询Promise,开始等待结果[${index}]: ${filePath}`)
+        // Logger.info(`heanup 创建了查询Promise,开始等待结果[${index}]: ${filePath}`)
 
         queryPromise.then((videoItem) => {
           completedQueries++
-          Logger.info(`heanup 查询Promise返回结果[${index}]: ${filePath}, 找到歌曲: ${videoItem ? videoItem.name : 'null'}`)
+          // Logger.info(`heanup 查询Promise返回结果[${index}]: ${filePath}, 找到歌曲: ${videoItem ? videoItem.name : 'null'}`)
 
           if (videoItem) {
             // 使用索引作为key保存到Map中,保持原始顺序
             songMap.set(index, videoItem)
-            Logger.info(`heanup 从数据库找到歌曲[${index}]: ${videoItem.name} (${completedQueries}/${songFilePaths.length})`)
+            // Logger.info(`heanup 从数据库找到歌曲[${index}]: ${videoItem.name} (${completedQueries}/${songFilePaths.length})`)
           } else {
             Logger.warn(`heanup 数据库中未找到歌曲[${index}]: ${filePath} (${completedQueries}/${songFilePaths.length})`)
           }
 
           // 检查是否所有歌曲都已加载完成
           if (completedQueries === songFilePaths.length) {
-            Logger.info(`heanup 所有数据库查询完成,共找到 ${songMap.size} 首歌曲`)
+            // Logger.info(`heanup 所有数据库查询完成,共找到 ${songMap.size} 首歌曲`)
 
             // 按照索引顺序重建歌曲数组
             const songs: VideoItem[] = []
@@ -13432,7 +13463,7 @@ export struct LocalMusic {
               const song = songMap.get(j)
               if (song) {
                 songs.push(song)
-                Logger.info(`heanup 按顺序添加歌曲[${j}]: ${song.name}`)
+                // Logger.info(`heanup 按顺序添加歌曲[${j}]: ${song.name}`)
               }
             }
 

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

@@ -69,6 +69,7 @@ export class VideoItem  {
   disc?:string//碟号
 
   webdav_account_id?: string// WebDAV账号ID,用于获取认证信息
+  remote_rel_path?: string // 远程相对路径(去掉协议+host+端口),便于重构URL
 
   constructor(name: string, id: string, filePath: string, type:number,videoSize:number,cTime: string,pixelMap?: image.PixelMap,
     size?: string,pixelMapPath?: string,artist?: string,album?: string,fileName?: string, lastPlayed?:Date) {
@@ -101,4 +102,4 @@ export class VideoItem  {
     this.extra_json = ''
     this.pyStr = ''
   }
-}
+}