Selaa lähdekoodia

修复href中无法跟随协议的问题;修复特殊字符(如 # 和空格)在 URL 中的编码问题

chendeben 9 kuukautta sitten
vanhempi
sitoutus
71eccb5e7e

+ 25 - 3
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -18,6 +18,7 @@ import { MusicInfo, parseMusicFileName, Utility } from './Utility';
 import { RemoteDriveType } from '../enums/RemoteDriveType';
 import { listSmbDirectory, SmbDirectoryEntry } from '../network/SmbBridge';
 import { NavidromeApi, NavidromeAlbumDetail, NavidromeArtist, NavidromeArtistDetail, NavidromeSong } from '../network/NavidromeApi';
+import { WebDavUrlUtil } from './WebDavUrlUtil';
 
 const TAG = 'heanup RemoteDriveManager';
 
@@ -472,7 +473,7 @@ export class RemoteDriveManager {
       // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
       Logger.info(TAG, 'coverPath字段可能已存在或添加失败,继续正常运行');
     }
-    
+
     try {
       // 添加webType字段,用于标识WebDAV账户类型
       Logger.info(TAG, '检查数据库表结构,尝试添加webType字段...');
@@ -778,6 +779,26 @@ export class RemoteDriveManager {
     return normalized || '/';
   }
 
+  // 将 WebDAV 响应中的 href 统一转换为以 / 开头的相对路径
+  private normalizeRemoteHref(rawHref: string): string {
+    if (!rawHref || rawHref.trim().length === 0) {
+      return '/';
+    }
+    const trimmed = rawHref.trim();
+    const relativeFromHttp = WebDavUrlUtil.extractRelativePath(trimmed);
+    if (relativeFromHttp) {
+      return this.normalizeFullPath(relativeFromHttp);
+    }
+    if (trimmed.startsWith('//')) {
+      const hostEnd = trimmed.indexOf('/', 2);
+      if (hostEnd > 1) {
+        return this.normalizeFullPath(trimmed.substring(hostEnd));
+      }
+      return '/';
+    }
+    return this.normalizeFullPath(trimmed);
+  }
+
   private toRelativePath(fullPath: string): string {
     if (!fullPath || fullPath === '/') {
       return '';
@@ -894,7 +915,8 @@ export class RemoteDriveManager {
     const port = account.port;
 
     // 构建基础URL,确保路径重新编码(目录/文件名中的中文与空格)
-    const encodedHref = encodeURI(fileInfo.href); // 仅编码非保留字符,已编码的百分号不重复编码
+    const safeHref = this.normalizeRemoteHref(fileInfo.href);
+    const encodedHref = encodeURI(safeHref); // 仅编码非保留字符,已编码的百分号不重复编码
     const absoluteUrl = `${protocol}://${host}:${port}${encodedHref}`;
 
     // 创建VideoItem对象
@@ -1429,7 +1451,7 @@ export async function buildHttpHeadersWithWebDav(
     console.log(`heanup ${key}: ${value}`);
     headerEntry = headerIterator.next();
   }
-  
+
   return headers;
 }
 

+ 22 - 6
entry/src/main/ets/view/LocalMusic.ets

@@ -232,6 +232,22 @@ function extractSmbRelativePath(song: VideoItem, shareNameOverride?: string): st
   return remainder;
 }
 
+function sanitizePlaybackUrl(rawUrl: string | null | undefined): string {
+  if (!rawUrl) {
+    return '';
+  }
+  try {
+    const decoded = decodeURI(rawUrl);
+    const encoded = encodeURI(decoded);
+    // encodeURI 不会处理 #,手动转义避免播放器把剩余内容当作片段
+    return encoded.replace(/#/g, '%23');
+  } catch (error) {
+    const err = error as Error;
+    Logger.warn(TAG, `URL 编码失败,使用降级方案: ${err?.message}`);
+    return rawUrl.replace(/ /g, '%20').replace(/#/g, '%23');
+  }
+}
+
 /**
  * 异步设置videoUrl的辅助方法,处理WebDAV URL的构建
  * @param song 歌曲对象
@@ -252,14 +268,14 @@ async function setVideoUrlForSong(song: VideoItem): Promise<string> {
       const fullUrl = await WebDavUrlUtil.buildFullUrlByAccountId(song.webdav_account_id, relativePath);
       if (fullUrl) {
         Logger.info(TAG, `WebDAV完整URL构建成功: ${fullUrl}`);
-        return fullUrl.replace(/ /g, '%20');
+        return sanitizePlaybackUrl(fullUrl);
       }
       Logger.error(TAG, `WebDAV URL构建失败,使用原始路径: ${relativePath}`);
-      return relativePath.replace(/ /g, '%20');
+      return sanitizePlaybackUrl(relativePath);
     } catch (error) {
       Logger.error(TAG, `WebDAV URL构建出错: ${(error as Error).message}`);
       const fallbackPath = song.remote_rel_path || song.filePath;
-      return fallbackPath.replace(/ /g, '%20');
+      return sanitizePlaybackUrl(fallbackPath);
     }
   }
 
@@ -273,7 +289,7 @@ async function setVideoUrlForSong(song: VideoItem): Promise<string> {
       const navSongId = song.remote_rel_path || song.id || song.filePath;
       const streamUrl = await navidromeApi.buildStreamUrl(account, navSongId);
       Logger.info(TAG, `Navidrome 流地址构建成功: ${streamUrl}`);
-      return streamUrl.replace(/ /g, '%20');
+      return sanitizePlaybackUrl(streamUrl);
     } catch (error) {
       const err = error as Error;
       Logger.error(TAG, `Navidrome URL构建失败: ${err.message}`);
@@ -301,7 +317,7 @@ async function setVideoUrlForSong(song: VideoItem): Promise<string> {
 
   if (isWebDavType(song.type)) {
     Logger.warn(TAG, `WebDAV歌曲缺少webdav_account_id,使用原始路径: ${song.filePath}`);
-    return song.filePath.replace(/ /g, '%20');
+    return sanitizePlaybackUrl(song.filePath);
   }
   if (isSmbType(song.type)) {
     Logger.warn(TAG, `SMB歌曲缺少webdav_account_id,使用原始路径: ${song.filePath}`);
@@ -6209,7 +6225,7 @@ export struct LocalMusic {
           this.songList = globalVideoList
           this.sonDataSource.pushArrayData(this.songList)
           if(this.currentSong && isRemoteCloudType(this.currentSong.type!)){
-            this.videoUrl = this.currentSong.filePath.replace(/ /g, '%20');
+            this.videoUrl = sanitizePlaybackUrl(this.currentSong.filePath);
           }else{
             this.videoUrl =  this.currentSong.filePath
           }