|
@@ -1,10 +1,10 @@
|
|
|
-// WebdavManager - WebDAV管理器(简化版)
|
|
|
|
|
|
|
+// RemoteDriveManager - WebDAV管理器(简化版)
|
|
|
import { VideoItem } from '../../viewmodel/VideoItem';
|
|
import { VideoItem } from '../../viewmodel/VideoItem';
|
|
|
import { common } from '@kit.AbilityKit';
|
|
import { common } from '@kit.AbilityKit';
|
|
|
import { FileInfo } from '../../viewmodel/FileInfo';
|
|
import { FileInfo } from '../../viewmodel/FileInfo';
|
|
|
import FileManager, { merge2paths } from './FileManager';
|
|
import FileManager, { merge2paths } from './FileManager';
|
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
-import { WebdavManagerStates } from '../enums/WebdavManagerStates';
|
|
|
|
|
|
|
+import { RemoteDriveManagerStates } from '../enums/RemoteDriveManagerStates';
|
|
|
import { RcpSocket } from './RcpSocketUtil';
|
|
import { RcpSocket } from './RcpSocketUtil';
|
|
|
import { DataBaseUtil } from './DataBaseUtil';
|
|
import { DataBaseUtil } from './DataBaseUtil';
|
|
|
import { WebDavAccount } from '../../viewmodel/WebDavAccount';
|
|
import { WebDavAccount } from '../../viewmodel/WebDavAccount';
|
|
@@ -15,8 +15,12 @@ import { buffer } from '@kit.ArkTS';
|
|
|
import { CommonConstants } from '../constants/CommonConstants';
|
|
import { CommonConstants } from '../constants/CommonConstants';
|
|
|
import { GlobalContext } from '@pura/harmony-utils';
|
|
import { GlobalContext } from '@pura/harmony-utils';
|
|
|
import { MusicInfo, parseMusicFileName, Utility } from './Utility';
|
|
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 WebdavManager';
|
|
|
|
|
|
|
+const TAG = 'heanup RemoteDriveManager';
|
|
|
|
|
|
|
|
// WebDAV认证信息接口
|
|
// WebDAV认证信息接口
|
|
|
export interface WebDavAuthInfo {
|
|
export interface WebDavAuthInfo {
|
|
@@ -37,11 +41,11 @@ export interface TransferTask {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Observed
|
|
@Observed
|
|
|
-export class WebdavManager {
|
|
|
|
|
|
|
+export class RemoteDriveManager {
|
|
|
public rcpSocket: RcpSocket = RcpSocket.getInstance();
|
|
public rcpSocket: RcpSocket = RcpSocket.getInstance();
|
|
|
private dataBaseUtil = DataBaseUtil.getInstance();
|
|
private dataBaseUtil = DataBaseUtil.getInstance();
|
|
|
public observers: Array<(event: string) => void> = [];
|
|
public observers: Array<(event: string) => void> = [];
|
|
|
- public static instance: WebdavManager;
|
|
|
|
|
|
|
+ public static instance: RemoteDriveManager;
|
|
|
public context: common.Context | undefined;
|
|
public context: common.Context | undefined;
|
|
|
public DownloadDirectoryFilePath: string = '';
|
|
public DownloadDirectoryFilePath: string = '';
|
|
|
public audioExtensions = Constants.AUDIO_EXTENSIONS;
|
|
public audioExtensions = Constants.AUDIO_EXTENSIONS;
|
|
@@ -76,6 +80,10 @@ export class WebdavManager {
|
|
|
// 上传队列
|
|
// 上传队列
|
|
|
public uploadQueue: TransferTask[] = [];
|
|
public uploadQueue: TransferTask[] = [];
|
|
|
public finishUploadQueue: TransferTask[] = [];
|
|
public finishUploadQueue: TransferTask[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ private navidromeApi: NavidromeApi = new NavidromeApi();
|
|
|
|
|
+ private pathDisplayNames: Map<string, string> = new Map();
|
|
|
|
|
+ private lastAccountId: number | null = null;
|
|
|
public isProcessingUploadQueue: boolean = false;
|
|
public isProcessingUploadQueue: boolean = false;
|
|
|
public currentUploadTask: TransferTask | null = null;
|
|
public currentUploadTask: TransferTask | null = null;
|
|
|
public isPauseUpload: boolean = true;
|
|
public isPauseUpload: boolean = true;
|
|
@@ -88,11 +96,11 @@ export class WebdavManager {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public static getInstance(): WebdavManager {
|
|
|
|
|
- if (!WebdavManager.instance) {
|
|
|
|
|
- WebdavManager.instance = new WebdavManager();
|
|
|
|
|
|
|
+ public static getInstance(): RemoteDriveManager {
|
|
|
|
|
+ if (!RemoteDriveManager.instance) {
|
|
|
|
|
+ RemoteDriveManager.instance = new RemoteDriveManager();
|
|
|
}
|
|
}
|
|
|
- return WebdavManager.instance;
|
|
|
|
|
|
|
+ return RemoteDriveManager.instance;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public setContext(context: common.Context): void {
|
|
public setContext(context: common.Context): void {
|
|
@@ -160,6 +168,21 @@ export class WebdavManager {
|
|
|
account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
|
|
account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
|
|
|
account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
|
|
account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
|
|
|
account.webType = resultSet.getLong(resultSet.getColumnIndex('webType'));
|
|
account.webType = resultSet.getLong(resultSet.getColumnIndex('webType'));
|
|
|
|
|
+ const smbShareIndex = resultSet.getColumnIndex('smbShare');
|
|
|
|
|
+ const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
|
|
|
|
|
+ const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
|
|
|
|
|
+ if (smbShareIndex >= 0) {
|
|
|
|
|
+ account.smbShare = resultSet.getString(smbShareIndex) ?? '';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (smbDomainIndex >= 0) {
|
|
|
|
|
+ account.smbDomain = resultSet.getString(smbDomainIndex) ?? '';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (navBaseIndex >= 0) {
|
|
|
|
|
+ const stored = resultSet.getString(navBaseIndex);
|
|
|
|
|
+ account.navidromeBasePath = stored && stored.length > 0 ? stored : '/rest';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ account.navidromeBasePath = '/rest';
|
|
|
|
|
+ }
|
|
|
account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
|
|
account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
|
|
|
|
|
|
|
|
resultSet.close();
|
|
resultSet.close();
|
|
@@ -290,7 +313,7 @@ export class WebdavManager {
|
|
|
// 查询所有TYPE_WEBDAV且webdav_account_id为空或null的记录
|
|
// 查询所有TYPE_WEBDAV且webdav_account_id为空或null的记录
|
|
|
const querySql = `
|
|
const querySql = `
|
|
|
SELECT id, filePath FROM mediaTable
|
|
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 = '')
|
|
AND (webdav_account_id IS NULL OR webdav_account_id = '')
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
@@ -304,7 +327,7 @@ export class WebdavManager {
|
|
|
const updateSql = `
|
|
const updateSql = `
|
|
|
UPDATE mediaTable
|
|
UPDATE mediaTable
|
|
|
SET webdav_account_id = ?
|
|
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 = '')
|
|
AND (webdav_account_id IS NULL OR webdav_account_id = '')
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
@@ -377,6 +400,14 @@ export class WebdavManager {
|
|
|
account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
|
|
account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
|
|
|
account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
|
|
account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
|
|
|
account.webType = resultSet.getLong(resultSet.getColumnIndex('webType'));
|
|
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;
|
|
account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
|
|
|
|
|
|
|
|
resultSet.close();
|
|
resultSet.close();
|
|
@@ -410,7 +441,10 @@ export class WebdavManager {
|
|
|
password TEXT,
|
|
password TEXT,
|
|
|
enableHttps INTEGER DEFAULT 0,
|
|
enableHttps INTEGER DEFAULT 0,
|
|
|
coverPath TEXT,
|
|
coverPath TEXT,
|
|
|
- webType INTEGER DEFAULT 0
|
|
|
|
|
|
|
+ webType INTEGER DEFAULT 0,
|
|
|
|
|
+ smbShare TEXT,
|
|
|
|
|
+ smbDomain TEXT,
|
|
|
|
|
+ navidromeBasePath TEXT
|
|
|
)`;
|
|
)`;
|
|
|
|
|
|
|
|
return this.dataBaseUtil.executeSql(createTableSql)
|
|
return this.dataBaseUtil.executeSql(createTableSql)
|
|
@@ -439,7 +473,7 @@ export class WebdavManager {
|
|
|
// 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
|
|
// 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
|
|
|
Logger.info(TAG, 'coverPath字段可能已存在或添加失败,继续正常运行');
|
|
Logger.info(TAG, 'coverPath字段可能已存在或添加失败,继续正常运行');
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
// 添加webType字段,用于标识WebDAV账户类型
|
|
// 添加webType字段,用于标识WebDAV账户类型
|
|
|
Logger.info(TAG, '检查数据库表结构,尝试添加webType字段...');
|
|
Logger.info(TAG, '检查数据库表结构,尝试添加webType字段...');
|
|
@@ -451,6 +485,33 @@ export class WebdavManager {
|
|
|
// 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
|
|
// 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
|
|
|
Logger.info(TAG, 'webType字段可能已存在或添加失败,继续正常运行');
|
|
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字段可能已存在或添加失败,继续运行');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ Logger.info(TAG, '尝试添加navidromeBasePath字段...');
|
|
|
|
|
+ const addNavBaseSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN navidromeBasePath TEXT`;
|
|
|
|
|
+ await this.dataBaseUtil.executeSql(addNavBaseSql);
|
|
|
|
|
+ Logger.info(TAG, 'navidromeBasePath字段添加成功');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Logger.info(TAG, 'navidromeBasePath字段可能已存在或添加失败,继续运行');
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 从数据库查询所有账户
|
|
// 从数据库查询所有账户
|
|
@@ -463,7 +524,7 @@ export class WebdavManager {
|
|
|
// 先查询基础字段(确保这些字段在旧版本中存在)
|
|
// 先查询基础字段(确保这些字段在旧版本中存在)
|
|
|
const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
|
|
const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
|
|
|
'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
|
|
'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
|
|
|
- 'account', 'password', 'enableHttps'];
|
|
|
|
|
|
|
+ 'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain', 'navidromeBasePath'];
|
|
|
|
|
|
|
|
const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, baseColumns, predicates);
|
|
const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, baseColumns, predicates);
|
|
|
|
|
|
|
@@ -484,48 +545,39 @@ export class WebdavManager {
|
|
|
account.account = resultSet.getString(resultSet.getColumnIndex('account'));
|
|
account.account = resultSet.getString(resultSet.getColumnIndex('account'));
|
|
|
account.password = resultSet.getString(resultSet.getColumnIndex('password'));
|
|
account.password = resultSet.getString(resultSet.getColumnIndex('password'));
|
|
|
account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
|
|
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) ?? '';
|
|
|
|
|
+ }
|
|
|
|
|
+ const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
|
|
|
|
|
+ if (navBaseIndex >= 0) {
|
|
|
|
|
+ const stored = resultSet.getString(navBaseIndex);
|
|
|
|
|
+ account.navidromeBasePath = stored && stored.length > 0 ? stored : '/rest';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ account.navidromeBasePath = '/rest';
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
this.webDavAccounts.push(account);
|
|
this.webDavAccounts.push(account);
|
|
|
}
|
|
}
|
|
|
resultSet.close();
|
|
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账户');
|
|
Logger.info(TAG, '从数据库加载了', this.webDavAccounts.length.toString(), '个WebDAV账户');
|
|
|
- this.notifyObservers(WebdavManagerStates.QueryAccountsSucceed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.QueryAccountsSucceed);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
const error = err as Error;
|
|
const error = err as Error;
|
|
|
Logger.error(TAG, '查询WebDAV账户失败:', error.message);
|
|
Logger.error(TAG, '查询WebDAV账户失败:', error.message);
|
|
|
- this.notifyObservers(WebdavManagerStates.QueryAccountsFailed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.QueryAccountsFailed);
|
|
|
throw error;
|
|
throw error;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -545,7 +597,10 @@ export class WebdavManager {
|
|
|
password: string,
|
|
password: string,
|
|
|
enableHttps: boolean,
|
|
enableHttps: boolean,
|
|
|
coverPath?: string,
|
|
coverPath?: string,
|
|
|
- webType: number = 0
|
|
|
|
|
|
|
+ webType: number = 0,
|
|
|
|
|
+ smbShare: string = '',
|
|
|
|
|
+ smbDomain: string = '',
|
|
|
|
|
+ navidromeBasePath: string = '/rest'
|
|
|
): Promise<void> {
|
|
): Promise<void> {
|
|
|
try {
|
|
try {
|
|
|
const values: relationalStore.ValuesBucket = {
|
|
const values: relationalStore.ValuesBucket = {
|
|
@@ -563,18 +618,21 @@ export class WebdavManager {
|
|
|
'password': password,
|
|
'password': password,
|
|
|
'enableHttps': enableHttps ? 1 : 0,
|
|
'enableHttps': enableHttps ? 1 : 0,
|
|
|
'coverPath': coverPath || null,
|
|
'coverPath': coverPath || null,
|
|
|
- 'webType': webType
|
|
|
|
|
|
|
+ 'webType': webType,
|
|
|
|
|
+ 'smbShare': smbShare,
|
|
|
|
|
+ 'smbDomain': smbDomain,
|
|
|
|
|
+ 'navidromeBasePath': navidromeBasePath
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
await this.dataBaseUtil.insertData(this.webDavTable, values);
|
|
await this.dataBaseUtil.insertData(this.webDavTable, values);
|
|
|
Logger.info(TAG, '插入WebDAV账户成功:', name);
|
|
Logger.info(TAG, '插入WebDAV账户成功:', name);
|
|
|
|
|
|
|
|
await this.queryWebDavAccountsFromDB();
|
|
await this.queryWebDavAccountsFromDB();
|
|
|
- this.notifyObservers(WebdavManagerStates.InsertAccountSucceed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.InsertAccountSucceed);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
const error = err as Error;
|
|
const error = err as Error;
|
|
|
Logger.error(TAG, '插入WebDAV账户失败:', error.message);
|
|
Logger.error(TAG, '插入WebDAV账户失败:', error.message);
|
|
|
- this.notifyObservers(WebdavManagerStates.InsertAccountFailed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.InsertAccountFailed);
|
|
|
throw error;
|
|
throw error;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -597,7 +655,10 @@ export class WebdavManager {
|
|
|
'password': account.password,
|
|
'password': account.password,
|
|
|
'enableHttps': account.enableHttps ? 1 : 0,
|
|
'enableHttps': account.enableHttps ? 1 : 0,
|
|
|
'coverPath': account.coverPath || null,
|
|
'coverPath': account.coverPath || null,
|
|
|
- 'webType': account.webType
|
|
|
|
|
|
|
+ 'webType': account.webType,
|
|
|
|
|
+ 'smbShare': account.smbShare,
|
|
|
|
|
+ 'smbDomain': account.smbDomain,
|
|
|
|
|
+ 'navidromeBasePath': account.navidromeBasePath
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const predicates = new relationalStore.RdbPredicates(this.webDavTable);
|
|
const predicates = new relationalStore.RdbPredicates(this.webDavTable);
|
|
@@ -607,11 +668,11 @@ export class WebdavManager {
|
|
|
Logger.info(TAG, '更新WebDAV账户成功:', account.name);
|
|
Logger.info(TAG, '更新WebDAV账户成功:', account.name);
|
|
|
|
|
|
|
|
await this.queryWebDavAccountsFromDB();
|
|
await this.queryWebDavAccountsFromDB();
|
|
|
- this.notifyObservers(WebdavManagerStates.EditAccountSucceed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.EditAccountSucceed);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
const error = err as Error;
|
|
const error = err as Error;
|
|
|
Logger.error(TAG, '更新WebDAV账户失败:', error.message);
|
|
Logger.error(TAG, '更新WebDAV账户失败:', error.message);
|
|
|
- this.notifyObservers(WebdavManagerStates.EditAccountFailed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.EditAccountFailed);
|
|
|
throw error;
|
|
throw error;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -626,11 +687,11 @@ export class WebdavManager {
|
|
|
Logger.info(TAG, '删除WebDAV账户成功:', account.name);
|
|
Logger.info(TAG, '删除WebDAV账户成功:', account.name);
|
|
|
|
|
|
|
|
await this.queryWebDavAccountsFromDB();
|
|
await this.queryWebDavAccountsFromDB();
|
|
|
- this.notifyObservers(WebdavManagerStates.RemoveAccountSucceed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.RemoveAccountSucceed);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
const error = err as Error;
|
|
const error = err as Error;
|
|
|
Logger.error(TAG, '删除WebDAV账户失败:', error.message);
|
|
Logger.error(TAG, '删除WebDAV账户失败:', error.message);
|
|
|
- this.notifyObservers(WebdavManagerStates.RemoveAccountFailed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.RemoveAccountFailed);
|
|
|
throw error;
|
|
throw error;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -657,144 +718,205 @@ export class WebdavManager {
|
|
|
|
|
|
|
|
// 从WebDAV加载文件列表
|
|
// 从WebDAV加载文件列表
|
|
|
public async loadFilesInfoFromWebdav(customPath?: string): Promise<void> {
|
|
public async loadFilesInfoFromWebdav(customPath?: string): Promise<void> {
|
|
|
-
|
|
|
|
|
- const account = this.getActivatedWebDavAccount();
|
|
|
|
|
|
|
+ const account = await this.getActiveWebDavAccount();
|
|
|
if (!account) {
|
|
if (!account) {
|
|
|
Logger.error(TAG, '没有激活的WebDAV账户');
|
|
Logger.error(TAG, '没有激活的WebDAV账户');
|
|
|
- this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoFailed);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
+ await this.loadFilesInfoFromAccount(account, customPath);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- 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);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ public async loadFilesInfoFromAccount(account: WebDavAccount,customPath?: string): Promise<void> {
|
|
|
|
|
+ this.currentAccount = account;
|
|
|
|
|
+ if (this.lastAccountId !== account.id) {
|
|
|
|
|
+ this.pathHistory = [];
|
|
|
|
|
+ this.pathDisplayNames.clear();
|
|
|
|
|
+ this.registerPathLabel('/', '根目录');
|
|
|
|
|
+ }
|
|
|
|
|
+ this.lastAccountId = account.id ?? null;
|
|
|
|
|
|
|
|
- // 调试:输出获取到的文件总数
|
|
|
|
|
- Logger.info(TAG, '从WebDAV获取到 ' + files.length + ' 个文件/文件夹');
|
|
|
|
|
|
|
+ // 更新现有WebDAV歌曲的webdav_account_id字段
|
|
|
|
|
+ if (account && account.id) {
|
|
|
|
|
+ await this.updateWebDavSongsAccountId(account.id.toString());
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 分别统计文件夹和音频文件
|
|
|
|
|
- let folderCount = 0;
|
|
|
|
|
- let audioCount = 0;
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoStart);
|
|
|
|
|
|
|
|
- // 过滤音频文件
|
|
|
|
|
- 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);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 使用自定义路径或账户默认路径
|
|
|
|
|
+ const normalizedFullPath = this.normalizeFullPath(customPath !== undefined ? customPath : account.filepath);
|
|
|
|
|
+ this.currentPath = normalizedFullPath;
|
|
|
|
|
+
|
|
|
|
|
+ if (account.webType === RemoteDriveType.Smb) {
|
|
|
|
|
+ await this.loadSmbFiles(account, normalizedFullPath);
|
|
|
|
|
+ } else if (account.webType === RemoteDriveType.Navidrome) {
|
|
|
|
|
+ await this.loadNavidromeFiles(account, normalizedFullPath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await this.loadWebDavFiles(account, normalizedFullPath);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.notifyObservers(WebdavManagerStates.LoadFilesInfoSucceed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoSucceed);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
this.ErrorMessage = error as BusinessError;
|
|
this.ErrorMessage = error as BusinessError;
|
|
|
- this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoFailed);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ 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 || '/';
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- public async loadFilesInfoFromAccount(account: WebDavAccount,customPath?: string): Promise<void> {
|
|
|
|
|
- this.currentAccount = account;
|
|
|
|
|
|
|
+ // 将 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);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 更新现有WebDAV歌曲的webdav_account_id字段
|
|
|
|
|
- if (account && account.id) {
|
|
|
|
|
- await this.updateWebDavSongsAccountId(account.id.toString());
|
|
|
|
|
|
|
+ private toRelativePath(fullPath: string): string {
|
|
|
|
|
+ if (!fullPath || fullPath === '/') {
|
|
|
|
|
+ return '';
|
|
|
}
|
|
}
|
|
|
|
|
+ return fullPath.replace(/^\/+/, '');
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- try {
|
|
|
|
|
- this.notifyObservers(WebdavManagerStates.LoadFilesInfoStart);
|
|
|
|
|
|
|
+ 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
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
- // 使用自定义路径或账户默认路径
|
|
|
|
|
- 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);
|
|
|
|
|
|
|
+ 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} 个文件/文件夹`);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 调试:输出获取到的文件总数
|
|
|
|
|
- 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
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- // 分别统计文件夹和音频文件
|
|
|
|
|
- let folderCount = 0;
|
|
|
|
|
- let audioCount = 0;
|
|
|
|
|
|
|
+ this.webDavFiles = [];
|
|
|
|
|
+ this.webDavSongs = [];
|
|
|
|
|
|
|
|
- // 过滤音频文件
|
|
|
|
|
- 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);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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} 个文件/文件夹`);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- this.notifyObservers(WebdavManagerStates.LoadFilesInfoSucceed);
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- this.ErrorMessage = error as BusinessError;
|
|
|
|
|
- this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
|
|
|
|
|
|
|
+ private async loadNavidromeFiles(account: WebDavAccount, fullPath: string): Promise<void> {
|
|
|
|
|
+ const normalized = this.normalizeFullPath(fullPath);
|
|
|
|
|
+ this.registerPathLabel('/', '根目录');
|
|
|
|
|
+ const segments = normalized.split('/').filter(part => part.length > 0);
|
|
|
|
|
+
|
|
|
|
|
+ if (normalized === '/' || segments.length === 0) {
|
|
|
|
|
+ const artists: NavidromeArtist[] = await this.navidromeApi.getArtists(account);
|
|
|
|
|
+ artists.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
+ this.webDavFiles = artists.map(artist => {
|
|
|
|
|
+ const info = this.createNavDirectory(artist.name, `/artist/${artist.id}`);
|
|
|
|
|
+ this.registerPathLabel(info.href, artist.name);
|
|
|
|
|
+ return info;
|
|
|
|
|
+ });
|
|
|
|
|
+ this.webDavSongs = [];
|
|
|
|
|
+ Logger.info(TAG, `从Navidrome获取到 ${artists.length} 位艺术家`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (segments[0] === 'artist' && segments[1]) {
|
|
|
|
|
+ const artistId = segments[1];
|
|
|
|
|
+ const detail: NavidromeArtistDetail = await this.navidromeApi.getArtist(account, artistId);
|
|
|
|
|
+ this.registerPathLabel(`/artist/${artistId}`, detail.name);
|
|
|
|
|
+ this.webDavFiles = detail.albums.map(album => {
|
|
|
|
|
+ const info = this.createNavDirectory(album.name, `/album/${album.id}`);
|
|
|
|
|
+ this.registerPathLabel(info.href, album.name);
|
|
|
|
|
+ return info;
|
|
|
|
|
+ });
|
|
|
|
|
+ this.webDavSongs = [];
|
|
|
|
|
+ Logger.info(TAG, `Navidrome 艺术家 ${detail.name} 包含 ${detail.albums.length} 张专辑`);
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ if (segments[0] === 'album' && segments[1]) {
|
|
|
|
|
+ const albumId = segments[1];
|
|
|
|
|
+ const detail: NavidromeAlbumDetail = await this.navidromeApi.getAlbum(account, albumId);
|
|
|
|
|
+ this.registerPathLabel(`/album/${albumId}`, detail.name);
|
|
|
|
|
+ this.webDavFiles = detail.songs.map(song => this.createNavSongFileInfo(song, albumId));
|
|
|
|
|
+ this.webDavSongs = detail.songs.map(song => this.buildNavidromeVideoItem(song, account, detail));
|
|
|
|
|
+ Logger.info(TAG, `Navidrome 专辑 ${detail.name} 包含 ${detail.songs.length} 首歌曲`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ throw new Error(`不支持的Navidrome路径: ${fullPath}`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 将FileInfo转换为VideoItem
|
|
// 将FileInfo转换为VideoItem
|
|
|
private fileInfoToVideoItem(fileInfo: FileInfo, account: WebDavAccount): VideoItem {
|
|
private fileInfoToVideoItem(fileInfo: FileInfo, account: WebDavAccount): VideoItem {
|
|
|
|
|
+ if (account.webType === RemoteDriveType.Smb) {
|
|
|
|
|
+ return this.buildSmbVideoItem(fileInfo, account);
|
|
|
|
|
+ }
|
|
|
// 构建安全的WebDAV URL(不包含认证信息)
|
|
// 构建安全的WebDAV URL(不包含认证信息)
|
|
|
const protocol = account.enableHttps ? 'https' : 'http';
|
|
const protocol = account.enableHttps ? 'https' : 'http';
|
|
|
const host = account.isUseLocalHost ? account.localHost : account.host;
|
|
const host = account.isUseLocalHost ? account.localHost : account.host;
|
|
|
const port = account.port;
|
|
const port = account.port;
|
|
|
|
|
|
|
|
// 构建基础URL,确保路径重新编码(目录/文件名中的中文与空格)
|
|
// 构建基础URL,确保路径重新编码(目录/文件名中的中文与空格)
|
|
|
- const encodedHref = encodeURI(fileInfo.href); // 仅编码非保留字符,已编码的百分号不重复编码
|
|
|
|
|
|
|
+ const safeHref = this.normalizeRemoteHref(fileInfo.href);
|
|
|
|
|
+ const encodedHref = encodeURI(safeHref); // 仅编码非保留字符,已编码的百分号不重复编码
|
|
|
const absoluteUrl = `${protocol}://${host}:${port}${encodedHref}`;
|
|
const absoluteUrl = `${protocol}://${host}:${port}${encodedHref}`;
|
|
|
|
|
|
|
|
// 创建VideoItem对象
|
|
// 创建VideoItem对象
|
|
@@ -840,6 +962,160 @@ export class WebdavManager {
|
|
|
return videoItem;
|
|
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 createNavDirectory(name: string, href: string): FileInfo {
|
|
|
|
|
+ const info = new FileInfo('', name, 0, Date.now());
|
|
|
|
|
+ info.fileName = name;
|
|
|
|
|
+ info.href = this.normalizeFullPath(href);
|
|
|
|
|
+ info.isDirectory = true;
|
|
|
|
|
+ return info;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private createNavSongFileInfo(song: NavidromeSong, albumId: string): FileInfo {
|
|
|
|
|
+ const displayName = song.title ?? '未知曲目';
|
|
|
|
|
+ const info = new FileInfo('', displayName, song.size ?? 0, Date.now());
|
|
|
|
|
+ info.fileName = `${displayName}${song.suffix ? '.' + song.suffix : ''}`;
|
|
|
|
|
+ info.href = this.normalizeFullPath(`/album/${albumId}/${song.id}`);
|
|
|
|
|
+ info.isDirectory = false;
|
|
|
|
|
+ info.contentLength = song.size ?? 0;
|
|
|
|
|
+ return info;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private buildNavidromeVideoItem(song: NavidromeSong, account: WebDavAccount, album?: NavidromeAlbumDetail): VideoItem {
|
|
|
|
|
+ const title = song.title ?? '未知曲目';
|
|
|
|
|
+ const fileName = `${title}${song.suffix ? '.' + song.suffix : ''}`;
|
|
|
|
|
+ const videoItem = new VideoItem(
|
|
|
|
|
+ title,
|
|
|
|
|
+ song.id,
|
|
|
|
|
+ `navidrome://${account.id ?? 0}/${song.id}`,
|
|
|
|
|
+ CommonConstants.TYPE_NAVIDROME,
|
|
|
|
|
+ song.size ?? 0,
|
|
|
|
|
+ Utility.getFormatDateStr(Date.now(), 'yyyy-MM-dd HH:mm'),
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ Utility.formatFSize(song.size ?? 0),
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ song.artist ?? album?.artist ?? Constants.UNKNOWN_ARTIST,
|
|
|
|
|
+ album?.name ?? song.album,
|
|
|
|
|
+ fileName
|
|
|
|
|
+ );
|
|
|
|
|
+ videoItem.size = Utility.formatFSize(song.size ?? 0);
|
|
|
|
|
+ videoItem.webdav_account_id = account.id?.toString();
|
|
|
|
|
+ videoItem.remote_rel_path = song.id;
|
|
|
|
|
+ videoItem.bit_rate = song.bitRate ? song.bitRate.toString() : undefined;
|
|
|
|
|
+ videoItem.mimeType = song.contentType;
|
|
|
|
|
+ return videoItem;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private registerPathLabel(path: string, label: string): void {
|
|
|
|
|
+ if (!path || !label) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const normalized = this.normalizeFullPath(path);
|
|
|
|
|
+ this.pathDisplayNames.set(normalized, label);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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 {
|
|
private isAudioFile(fileName: string): boolean {
|
|
|
const ext = this.getFileExtension(fileName).toLowerCase();
|
|
const ext = this.getFileExtension(fileName).toLowerCase();
|
|
@@ -881,11 +1157,12 @@ export class WebdavManager {
|
|
|
Logger.info(TAG, '目标路径:', folder.href);
|
|
Logger.info(TAG, '目标路径:', folder.href);
|
|
|
|
|
|
|
|
// 保存当前路径到历史记录
|
|
// 保存当前路径到历史记录
|
|
|
|
|
+ this.registerPathLabel(folder.href, folder.fileName);
|
|
|
this.pathHistory.push(this.currentPath);
|
|
this.pathHistory.push(this.currentPath);
|
|
|
Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
|
|
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> {
|
|
public async enterFolderFromPath(path: string): Promise<void> {
|
|
@@ -896,10 +1173,10 @@ export class WebdavManager {
|
|
|
this.pathHistory.push(this.currentPath);
|
|
this.pathHistory.push(this.currentPath);
|
|
|
Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
|
|
Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
|
|
|
// 加载文件夹内容
|
|
// 加载文件夹内容
|
|
|
- await this.loadFilesInfoFromWebdav(path);
|
|
|
|
|
|
|
+ await this.loadFilesInfoFromAccount(this.currentAccount, path);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 在 WebdavManager 类中添加以下方法
|
|
|
|
|
|
|
+ // 在 RemoteDriveManager 类中添加以下方法
|
|
|
navigateToBreadcrumb(breadcrumbIndex: number): Promise<string> {
|
|
navigateToBreadcrumb(breadcrumbIndex: number): Promise<string> {
|
|
|
return new Promise((resolve, reject) => {
|
|
return new Promise((resolve, reject) => {
|
|
|
try {
|
|
try {
|
|
@@ -942,7 +1219,7 @@ export class WebdavManager {
|
|
|
// 从历史记录中取出上一级路径
|
|
// 从历史记录中取出上一级路径
|
|
|
const previousPath = this.pathHistory.pop();
|
|
const previousPath = this.pathHistory.pop();
|
|
|
if (previousPath !== undefined) {
|
|
if (previousPath !== undefined) {
|
|
|
- await this.loadFilesInfoFromWebdav(previousPath);
|
|
|
|
|
|
|
+ await this.loadFilesInfoFromAccount(this.currentAccount, previousPath);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -955,8 +1232,11 @@ export class WebdavManager {
|
|
|
const parts = this.currentPath.split('/').filter(part => part !== '');
|
|
const parts = this.currentPath.split('/').filter(part => part !== '');
|
|
|
const breadcrumbs = ['根目录'];
|
|
const breadcrumbs = ['根目录'];
|
|
|
|
|
|
|
|
|
|
+ let cumulative = '';
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
for (let i = 0; i < parts.length; i++) {
|
|
|
- breadcrumbs.push(parts[i]);
|
|
|
|
|
|
|
+ cumulative += `/${parts[i]}`;
|
|
|
|
|
+ const label = this.pathDisplayNames.get(cumulative) ?? parts[i];
|
|
|
|
|
+ breadcrumbs.push(label);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return breadcrumbs;
|
|
return breadcrumbs;
|
|
@@ -998,7 +1278,7 @@ export class WebdavManager {
|
|
|
const task: TransferTask = { song, account };
|
|
const task: TransferTask = { song, account };
|
|
|
this.downloadQueue.push(task);
|
|
this.downloadQueue.push(task);
|
|
|
Logger.info(TAG, '添加到下载队列:', song.name);
|
|
Logger.info(TAG, '添加到下载队列:', song.name);
|
|
|
- this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.DownloadQueueChanged);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 从下载队列移除
|
|
// 从下载队列移除
|
|
@@ -1007,7 +1287,7 @@ export class WebdavManager {
|
|
|
const task = this.downloadQueue[index];
|
|
const task = this.downloadQueue[index];
|
|
|
this.downloadQueue.splice(index, 1);
|
|
this.downloadQueue.splice(index, 1);
|
|
|
Logger.info(TAG, '从下载队列移除:', task.song.name);
|
|
Logger.info(TAG, '从下载队列移除:', task.song.name);
|
|
|
- this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.DownloadQueueChanged);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1015,7 +1295,7 @@ export class WebdavManager {
|
|
|
public clearDownloadQueue(): void {
|
|
public clearDownloadQueue(): void {
|
|
|
this.downloadQueue = [];
|
|
this.downloadQueue = [];
|
|
|
Logger.info(TAG, '清空下载队列');
|
|
Logger.info(TAG, '清空下载队列');
|
|
|
- this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.DownloadQueueChanged);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==================== 上传队列管理 ====================
|
|
// ==================== 上传队列管理 ====================
|
|
@@ -1025,7 +1305,7 @@ export class WebdavManager {
|
|
|
const task: TransferTask = { song, account };
|
|
const task: TransferTask = { song, account };
|
|
|
this.uploadQueue.push(task);
|
|
this.uploadQueue.push(task);
|
|
|
Logger.info(TAG, '添加到上传队列:', song.name);
|
|
Logger.info(TAG, '添加到上传队列:', song.name);
|
|
|
- this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 从上传队列移除
|
|
// 从上传队列移除
|
|
@@ -1034,7 +1314,7 @@ export class WebdavManager {
|
|
|
const task = this.uploadQueue[index];
|
|
const task = this.uploadQueue[index];
|
|
|
this.uploadQueue.splice(index, 1);
|
|
this.uploadQueue.splice(index, 1);
|
|
|
Logger.info(TAG, '从上传队列移除:', task.song.name);
|
|
Logger.info(TAG, '从上传队列移除:', task.song.name);
|
|
|
- this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1042,7 +1322,7 @@ export class WebdavManager {
|
|
|
public clearUploadQueue(): void {
|
|
public clearUploadQueue(): void {
|
|
|
this.uploadQueue = [];
|
|
this.uploadQueue = [];
|
|
|
Logger.info(TAG, '清空上传队列');
|
|
Logger.info(TAG, '清空上传队列');
|
|
|
- this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
|
|
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==================== 安全认证方法 ====================
|
|
// ==================== 安全认证方法 ====================
|
|
@@ -1134,8 +1414,8 @@ export async function buildHttpHeadersWithWebDav(
|
|
|
try {
|
|
try {
|
|
|
if (webDavAuthItem && webDavAuthItem.accountId) {
|
|
if (webDavAuthItem && webDavAuthItem.accountId) {
|
|
|
Logger.info(`heanup 找到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
|
|
Logger.info(`heanup 找到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
|
|
|
- // 使用WebdavManager获取认证头
|
|
|
|
|
- const webdavManager = WebdavManager.getInstance();
|
|
|
|
|
|
|
+ // 使用RemoteDriveManager获取认证头
|
|
|
|
|
+ const webdavManager = RemoteDriveManager.getInstance();
|
|
|
const authHeaders = await webdavManager.getWebDavAuthHeaders(webDavAuthItem.accountId.toString());
|
|
const authHeaders = await webdavManager.getWebDavAuthHeaders(webDavAuthItem.accountId.toString());
|
|
|
|
|
|
|
|
if (authHeaders) {
|
|
if (authHeaders) {
|
|
@@ -1171,7 +1451,7 @@ export async function buildHttpHeadersWithWebDav(
|
|
|
console.log(`heanup ${key}: ${value}`);
|
|
console.log(`heanup ${key}: ${value}`);
|
|
|
headerEntry = headerIterator.next();
|
|
headerEntry = headerIterator.next();
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
return headers;
|
|
return headers;
|
|
|
}
|
|
}
|
|
|
|
|
|