| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624 |
- // RemoteDriveManager - WebDAV管理器(简化版)
- import { VideoItem } from '../../viewmodel/VideoItem';
- import { common } from '@kit.AbilityKit';
- import { FileInfo } from '../../viewmodel/FileInfo';
- import FileManager, { merge2paths } from './FileManager';
- import { BusinessError } from '@kit.BasicServicesKit';
- import { RemoteDriveManagerStates } from '../enums/RemoteDriveManagerStates';
- import { RcpSocket } from './RcpSocketUtil';
- import { DataBaseUtil } from './DataBaseUtil';
- import { WebDavAccount } from '../../viewmodel/WebDavAccount';
- import { relationalStore } from '@kit.ArkData';
- import { Constants } from '../../Constants';
- import Logger from './Logger';
- 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';
- import { NavidromeApi, NavidromeAlbumDetail, NavidromeArtist, NavidromeArtistDetail, NavidromeSong } from '../network/NavidromeApi';
- import { WebDavUrlUtil } from './WebDavUrlUtil';
- import MediaTable from './MediaTable';
- const TAG = 'heanup RemoteDriveManager';
- export interface BreadcrumbItem {
- label: string;
- path: string;
- }
- // WebDAV认证信息接口
- export interface WebDavAuthInfo {
- headers: Record<string, string>;
- url: string;
- accountId?: string; // 添加账号ID字段,用于标识认证信息对应的账号
- }
- // 流媒体认证信息接口
- export interface StreamAuthInfo {
- url: string;
- headers: Record<string, string>;
- }
- export interface TransferTask {
- song: VideoItem;
- account: WebDavAccount;
- }
- @Observed
- export class RemoteDriveManager {
- public rcpSocket: RcpSocket = RcpSocket.getInstance();
- private dataBaseUtil = DataBaseUtil.getInstance();
- public observers: Array<(event: string) => void> = [];
- public static instance: RemoteDriveManager;
- public context: common.Context | undefined;
- public DownloadDirectoryFilePath: string = '';
- public audioExtensions = Constants.AUDIO_EXTENSIONS;
- public lyricExtensions = Constants.LYRIC_EXTENSIONS;
- public imageExtensions = Constants.IMAGE_EXTENSIONS;
- // 数据表名称
- public preferenceName = Constants.WEBDAV_PREFERENCE_NAME;
- public webDavTable = 'WebDavAccount';
- public receivedSize: number = 0;
- public totalSize: number = 0;
- public webDavAccounts: WebDavAccount[] = [];
- public webDavSongs: VideoItem[] = [];
- public webDavFiles: FileInfo[] = []; // 当前目录的所有文件(包括文件夹)
- // 路径导航
- public currentPath: string = ''; // 当前浏览的路径
- public pathHistory: string[] = []; // 路径历史记录
- // 错误信息
- public ErrorMessage: string | BusinessError = '';
- // 下载队列
- public downloadQueue: TransferTask[] = [];
- public finishDownloadQueue: TransferTask[] = [];
- public isProcessingQueue: boolean = false;
- public currentDownloadTask: TransferTask | null = null;
- public isPauseDownload: boolean = true;
- // 上传队列
- public uploadQueue: 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 currentUploadTask: TransferTask | null = null;
- public isPauseUpload: boolean = true;
- public currentAccount:WebDavAccount = new WebDavAccount();
- public constructor() {
- this.rcpSocket.subscribeHTTPDataTransfer((event: string) => {
- this.notifyObservers(event);
- });
- }
- public static getInstance(): RemoteDriveManager {
- if (!RemoteDriveManager.instance) {
- RemoteDriveManager.instance = new RemoteDriveManager();
- }
- return RemoteDriveManager.instance;
- }
- public setContext(context: common.Context): void {
- this.context = context;
- this.DownloadDirectoryFilePath = merge2paths(context.filesDir, 'Download');
- }
- // ==================== 观察者模式 ====================
- public subscribe(callback: (event: string) => void): void {
- this.observers.push(callback);
- }
- public unsubscribe(callback: (event: string) => void): void {
- const index = this.observers.indexOf(callback);
- if (index > -1) {
- this.observers.splice(index, 1);
- }
- }
- public notifyObservers(event: string): void {
- Logger.info(TAG, '通知观察者:', event);
- Logger.info(TAG, '观察者数量:', this.observers.length.toString());
- for (let i = 0; i < this.observers.length; i++) {
- const observer = this.observers[i];
- Logger.info(TAG, '调用观察者', i.toString(), ',事件:', event);
- observer(event);
- }
- }
- // ==================== 数据库操作 ====================
- /**
- * 根据webdav_account_id查询WebDAV账号信息
- * @param accountId WebDAV账号ID
- * @returns Promise<WebDavAccount | null> 返回WebDAV账号信息或null
- */
- public async getWebDavAccountById(accountId: string): Promise<WebDavAccount | null> {
- if (!accountId) {
- Logger.warn(TAG, 'webdav_account_id为空,无法查询账号信息');
- return null;
- }
- try {
- const db = await this.dataBaseUtil.getDB();
- const predicates = new relationalStore.RdbPredicates(this.webDavTable);
- predicates.equalTo('id', accountId);
- const resultSet = await db.query(predicates);
- if (resultSet.rowCount > 0) {
- resultSet.goToFirstRow();
- const account = new WebDavAccount();
- account.id = resultSet.getLong(resultSet.getColumnIndex('id'));
- account.name = resultSet.getString(resultSet.getColumnIndex('name'));
- 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;
- account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
- 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;
- resultSet.close();
- Logger.info(TAG, `成功查询到WebDAV账号: ${account.name}, ID: ${account.id}`);
- return account;
- } else {
- Logger.warn(TAG, `未找到ID为 ${accountId} 的WebDAV账号`);
- resultSet.close();
- return null;
- }
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `查询WebDAV账号失败: ${err.message}`);
- return null;
- }
- }
- /**
- * 根据WebDAV账号构建认证信息
- * @param accountId WebDAV账号ID
- * @returns Promise<WebDavAuthInfo | null> 返回认证信息或null
- */
- public async buildAuthInfoByAccountId(accountId: string): Promise<WebDavAuthInfo | null> {
- const account = await this.getWebDavAccountById(accountId);
- if (!account) {
- Logger.warn(TAG, `无法构建认证信息,账号ID ${accountId} 对应的账号不存在`);
- return null;
- }
- try {
- // 构建基础URL
- const protocol = account.enableHttps ? 'https' : 'http';
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- const port = account.port && account.port !== 80 && account.port !== 443 ? `:${account.port}` : '';
- const baseUrl = `${protocol}://${host}${port}`;
- // 构建认证头
- const headers: Record<string, string> = {};
- if (account.account && account.password) {
- const credentials = `${account.account}:${account.password}`;
- // 简化base64编码实现,避免复杂的类型转换
- try {
- const bufferObj = buffer.from(credentials, 'utf-8');
- const base64Credentials = bufferObj.toString('base64');
- headers['Authorization'] = `Basic ${base64Credentials}`;
- } catch (error) {
- const err = error as Error;
- Logger.warn(TAG, `base64编码失败,使用原始凭据: ${err.message}`);
- headers['Authorization'] = `Basic ${credentials}`;
- }
- }
- Logger.info(TAG, `成功构建WebDAV认证信息,账号: ${account.name}, URL: ${baseUrl}`);
- return {
- headers: headers,
- url: baseUrl,
- accountId: accountId
- };
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `构建WebDAV认证信息失败: ${err.message}`);
- return null;
- }
- }
- /**
- * 构建HTTP请求头,通过webdav_account_id查询认证信息
- * @param currentSong - 当前播放的歌曲信息
- * @param videoUrl - 当前歌曲的URL
- * @returns Promise<Map<string, string>> HTTP请求头
- */
- public async buildHttpHeadersWithAccountId(
- currentSong: VideoItem | undefined,
- videoUrl: string
- ): Promise<Map<string, string>> {
- const headers = new Map<string, string>();
- if (!currentSong) {
- return headers;
- }
- Logger.info(`heanup 当前歌曲信息 - name: ${currentSong.name}, type: ${currentSong.type}, filePath: ${currentSong.filePath}`);
- // 检查是否为WebDAV歌曲
- if (currentSong.type === CommonConstants.TYPE_WEBDAV) {
- // 优先使用webdav_account_id查询认证信息
- if (currentSong.webdav_account_id) {
- Logger.info(`heanup 使用webdav_account_id查询认证信息,账号ID: ${currentSong.webdav_account_id}`);
- try {
- const authInfo = await this.buildAuthInfoByAccountId(currentSong.webdav_account_id);
- if (authInfo) {
- // 添加WebDAV认证头
- Object.keys(authInfo.headers).forEach(key => {
- headers.set(key, authInfo.headers[key]);
- });
- Logger.info(`heanup 成功通过webdav_account_id获取WebDAV认证信息`);
- } else {
- Logger.warn(`heanup 无法通过webdav_account_id ${currentSong.webdav_account_id} 获取认证信息`);
- }
- } catch (error) {
- const err = error as Error;
- Logger.error(`heanup 查询webdav_account_id认证信息时出错: ${err.message}`);
- }
- } else {
- Logger.warn(`heanup WebDAV歌曲缺少webdav_account_id字段,无法查询认证信息`);
- }
- // 如果没有认证信息,记录警告
- if (headers.size === 0) {
- Logger.warn(`heanup WebDAV歌曲缺少认证信息,可能需要手动配置WebDAV账户`);
- }
- }
- return headers;
- }
- /**
- * 更新现有WebDAV歌曲的webdav_account_id字段
- * 用于修复旧版本数据库中缺少webdav_account_id的记录
- * @param accountId WebDAV账号ID
- */
- public async updateWebDavSongsAccountId(accountId: string): Promise<void> {
- try {
- const db = await this.dataBaseUtil.getDB();
- // 查询所有TYPE_WEBDAV且webdav_account_id为空或null的记录
- const querySql = `
- SELECT id, filePath FROM mediaTable
- WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB})
- AND (webdav_account_id IS NULL OR webdav_account_id = '')
- `;
- const resultSet = await db.querySql(querySql);
- const updateCount = resultSet.rowCount;
- if (updateCount > 0) {
- Logger.info(TAG, `发现 ${updateCount} 个WebDAV歌曲缺少webdav_account_id,开始更新`);
- // 更新这些记录的webdav_account_id
- const updateSql = `
- UPDATE mediaTable
- SET webdav_account_id = ?
- WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB})
- AND (webdav_account_id IS NULL OR webdav_account_id = '')
- `;
- await db.executeSql(updateSql, [accountId]);
- Logger.info(TAG, `成功更新 ${updateCount} 个WebDAV歌曲的webdav_account_id为: ${accountId}`);
- } else {
- Logger.info(TAG, `所有WebDAV歌曲都已有webdav_account_id,无需更新`);
- }
- resultSet.close();
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `更新WebDAV歌曲webdav_account_id失败: ${err.message}`);
- }
- }
- /**
- * 修复所有WebDAV歌曲的webdav_account_id字段
- * 用于全局修复旧版本数据库中缺少webdav_account_id的记录
- * @param accountId WebDAV账号ID(可选,如果不提供则使用当前激活的账号)
- */
- public async fixAllWebDavSongsAccountId(accountId?: string): Promise<void> {
- try {
- // 如果没有提供accountId,尝试使用当前激活的账号
- if (!accountId) {
- const activeAccount = await this.getActiveWebDavAccount();
- if (activeAccount && activeAccount.id) {
- accountId = activeAccount.id.toString();
- } else {
- Logger.warn(TAG, `无法找到激活的WebDAV账号来修复歌曲记录`);
- return;
- }
- }
- Logger.info(TAG, `开始修复所有WebDAV歌曲的webdav_account_id,使用账号ID: ${accountId}`);
- await this.updateWebDavSongsAccountId(accountId);
- Logger.info(TAG, `WebDAV歌曲webdav_account_id修复完成`);
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `修复WebDAV歌曲webdav_account_id失败: ${err.message}`);
- }
- }
- /**
- * 获取当前激活的WebDAV账号
- */
- private async getActiveWebDavAccount(): Promise<WebDavAccount | null> {
- try {
- const db = await this.dataBaseUtil.getDB();
- const predicates = new relationalStore.RdbPredicates(this.webDavTable);
- predicates.equalTo('isActivate', 1);
- predicates.limitAs(1);
- const resultSet = await db.query(predicates);
- if (resultSet.rowCount > 0) {
- resultSet.goToFirstRow();
- const account = new WebDavAccount();
- account.id = resultSet.getLong(resultSet.getColumnIndex('id'));
- account.name = resultSet.getString(resultSet.getColumnIndex('name'));
- 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;
- 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();
- return account;
- } else {
- resultSet.close();
- return null;
- }
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `获取激活的WebDAV账号失败: ${err.message}`);
- return null;
- }
- }
- // 创建WebDAV账户表
- public createWebDavTableInDB(): Promise<void> {
- const createTableSql = `CREATE TABLE IF NOT EXISTS ${this.webDavTable} (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT,
- isActivate INTEGER DEFAULT 1,
- host TEXT,
- localHost TEXT,
- isUseLocalHost INTEGER DEFAULT 0,
- port INTEGER,
- filepath TEXT,
- imageFilePath TEXT,
- lyricFilePath TEXT,
- uploadFilePath TEXT,
- account TEXT,
- password TEXT,
- enableHttps INTEGER DEFAULT 0,
- coverPath TEXT,
- webType INTEGER DEFAULT 0,
- smbShare TEXT,
- smbDomain TEXT,
- navidromeBasePath TEXT
- )`;
- return this.dataBaseUtil.executeSql(createTableSql)
- .then(() => {
- Logger.info(TAG, 'WebDAV账户表创建成功');
- // 检查并添加新字段(用于数据库升级)
- return this.upgradeWebDavTable();
- })
- .catch((err: Error) => {
- Logger.error(TAG, '创建WebDAV账户表失败:', err.message);
- throw err;
- });
- }
- // 升级WebDAV表结构
- private async upgradeWebDavTable(): Promise<void> {
- try {
- // 直接尝试添加coverPath字段,如果字段已存在会失败但不影响应用运行
- Logger.info(TAG, '检查数据库表结构,尝试添加coverPath字段...');
- const addColumnSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN coverPath TEXT`;
- await this.dataBaseUtil.executeSql(addColumnSql);
- Logger.info(TAG, 'coverPath字段添加成功,数据库升级完成');
- } catch (error) {
- // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
- Logger.info(TAG, 'coverPath字段可能已存在或添加失败,继续正常运行');
- }
- try {
- // 添加webType字段,用于标识WebDAV账户类型
- Logger.info(TAG, '检查数据库表结构,尝试添加webType字段...');
- const addWebTypeColumnSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN webType INTEGER DEFAULT 0`;
- await this.dataBaseUtil.executeSql(addWebTypeColumnSql);
- Logger.info(TAG, 'webType字段添加成功,数据库升级完成');
- } catch (error) {
- // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
- 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字段可能已存在或添加失败,继续运行');
- }
- }
- // 从数据库查询所有账户
- public async queryWebDavAccountsFromDB(): Promise<void> {
- try {
- // 确保表结构是最新的
- await this.upgradeWebDavTable();
- const predicates = new relationalStore.RdbPredicates(this.webDavTable);
- // 先查询基础字段(确保这些字段在旧版本中存在)
- const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
- 'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
- 'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain', 'navidromeBasePath'];
- const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, baseColumns, predicates);
- this.webDavAccounts = [];
- while (resultSet.goToNextRow()) {
- 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;
- 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);
- }
- resultSet.close();
- Logger.info(TAG, '从数据库加载了', this.webDavAccounts.length.toString(), '个WebDAV账户');
- this.notifyObservers(RemoteDriveManagerStates.QueryAccountsSucceed);
- } catch (err) {
- const error = err as Error;
- Logger.error(TAG, '查询WebDAV账户失败:', error.message);
- this.notifyObservers(RemoteDriveManagerStates.QueryAccountsFailed);
- throw error;
- }
- }
- // 插入新账户
- public async insertAccount(
- name: string,
- host: string,
- localHost: string,
- isUseLocalHost: boolean,
- port: number,
- filepath: string,
- lyricFilePath: string,
- uploadFilePath: string,
- imageFilePath: string,
- account: string,
- password: string,
- enableHttps: boolean,
- coverPath?: string,
- webType: number = 0,
- smbShare: string = '',
- smbDomain: string = '',
- navidromeBasePath: string = '/rest'
- ): Promise<void> {
- try {
- const values: relationalStore.ValuesBucket = {
- 'name': name,
- 'isActivate': 1,
- 'host': host,
- 'localHost': localHost,
- 'isUseLocalHost': isUseLocalHost ? 1 : 0,
- 'port': port,
- 'filepath': filepath,
- 'imageFilePath': imageFilePath,
- 'lyricFilePath': lyricFilePath,
- 'uploadFilePath': uploadFilePath,
- 'account': account,
- 'password': password,
- 'enableHttps': enableHttps ? 1 : 0,
- 'coverPath': coverPath || null,
- 'webType': webType,
- 'smbShare': smbShare,
- 'smbDomain': smbDomain,
- 'navidromeBasePath': navidromeBasePath
- };
- await this.dataBaseUtil.insertData(this.webDavTable, values);
- Logger.info(TAG, '插入WebDAV账户成功:', name);
- await this.queryWebDavAccountsFromDB();
- this.notifyObservers(RemoteDriveManagerStates.InsertAccountSucceed);
- } catch (err) {
- const error = err as Error;
- Logger.error(TAG, '插入WebDAV账户失败:', error.message);
- this.notifyObservers(RemoteDriveManagerStates.InsertAccountFailed);
- throw error;
- }
- }
- // 编辑账户
- public async editAccount(account: WebDavAccount): Promise<void> {
- try {
- const values: relationalStore.ValuesBucket = {
- 'name': account.name,
- 'isActivate': account.isActivate ? 1 : 0,
- 'host': account.host,
- 'localHost': account.localHost,
- 'isUseLocalHost': account.isUseLocalHost ? 1 : 0,
- 'port': account.port,
- 'filepath': account.filepath,
- 'imageFilePath': account.imageFilePath,
- 'lyricFilePath': account.lyricFilePath,
- 'uploadFilePath': account.uploadFilePath,
- 'account': account.account,
- 'password': account.password,
- 'enableHttps': account.enableHttps ? 1 : 0,
- 'coverPath': account.coverPath || null,
- 'webType': account.webType,
- 'smbShare': account.smbShare,
- 'smbDomain': account.smbDomain,
- 'navidromeBasePath': account.navidromeBasePath
- };
- const predicates = new relationalStore.RdbPredicates(this.webDavTable);
- predicates.equalTo('id', account.id);
- await this.dataBaseUtil.updateData(this.webDavTable, values, predicates);
- Logger.info(TAG, '更新WebDAV账户成功:', account.name);
- await this.queryWebDavAccountsFromDB();
- this.notifyObservers(RemoteDriveManagerStates.EditAccountSucceed);
- } catch (err) {
- const error = err as Error;
- Logger.error(TAG, '更新WebDAV账户失败:', error.message);
- this.notifyObservers(RemoteDriveManagerStates.EditAccountFailed);
- throw error;
- }
- }
- // 删除账户
- public async removeAccount(account: WebDavAccount): Promise<void> {
- try {
- const predicates = new relationalStore.RdbPredicates(this.webDavTable);
- predicates.equalTo('id', account.id);
- await this.dataBaseUtil.deleteData(predicates);
- Logger.info(TAG, '删除WebDAV账户成功:', account.name);
- await this.queryWebDavAccountsFromDB();
- this.notifyObservers(RemoteDriveManagerStates.RemoveAccountSucceed);
- } catch (err) {
- const error = err as Error;
- Logger.error(TAG, '删除WebDAV账户失败:', error.message);
- this.notifyObservers(RemoteDriveManagerStates.RemoveAccountFailed);
- throw error;
- }
- }
- // 获取所有账户
- public getAllWebDavAccounts(): WebDavAccount[] {
- return this.webDavAccounts;
- }
- // 获取激活的账户
- public getActivatedWebDavAccount(): WebDavAccount | null {
- if(this.currentAccount)
- return this.currentAccount;
- for (let i = 0; i < this.webDavAccounts.length; i++) {
- const account = this.webDavAccounts[i];
- if (account.isActivate) {
- return account;
- }
- }
- return null;
- }
- // ==================== 文件操作 ====================
- // 从WebDAV加载文件列表
- public async loadFilesInfoFromWebdav(customPath?: string): Promise<void> {
- const account = await this.getActiveWebDavAccount();
- if (!account) {
- Logger.error(TAG, '没有激活的WebDAV账户');
- this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoFailed);
- return;
- }
- await this.loadFilesInfoFromAccount(account, customPath);
- }
- 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;
- // 更新现有WebDAV歌曲的webdav_account_id字段
- if (account && account.id) {
- await this.updateWebDavSongsAccountId(account.id.toString());
- }
- try {
- this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoStart);
- // 使用自定义路径或账户默认路径
- 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(RemoteDriveManagerStates.LoadFilesInfoSucceed);
- } catch (error) {
- this.ErrorMessage = error as BusinessError;
- 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 || '/';
- }
- // 将 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 '';
- }
- 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));
- }
- }
- await this.enrichSongsWithDatabase(this.webDavSongs);
- 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));
- }
- }
- await this.enrichSongsWithDatabase(this.webDavSongs);
- Logger.info(TAG, `从SMB获取到 ${entries.length} 个文件/文件夹`);
- }
- 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));
- await this.enrichSongsWithDatabase(this.webDavSongs);
- Logger.info(TAG, `Navidrome 专辑 ${detail.name} 包含 ${detail.songs.length} 首歌曲`);
- return;
- }
- throw new Error(`不支持的Navidrome路径: ${fullPath}`);
- }
- private async enrichSongsWithDatabase(videoItems: VideoItem[]): Promise<void> {
- if (!this.context || !videoItems || videoItems.length === 0) {
- return;
- }
- try {
- const mediaTable = new MediaTable(this.context);
- await new Promise<void>((resolve) => {
- mediaTable.getRdbStore(this.context as common.Context, () => resolve());
- });
- const pathMap: Map<string, VideoItem[]> = new Map();
- for (let i = 0; i < videoItems.length; i++) {
- const item = videoItems[i];
- const storagePath = this.resolveStoragePathForItem(item);
- if (!storagePath) {
- continue;
- }
- if (!pathMap.has(storagePath)) {
- pathMap.set(storagePath, []);
- }
- pathMap.get(storagePath)?.push(item);
- }
- if (pathMap.size === 0) {
- return;
- }
- const tasks: Promise<void>[] = [];
- pathMap.forEach((targets, storagePath) => {
- tasks.push((async () => {
- try {
- const cached = await mediaTable.queryVideoByFilePath(storagePath);
- if (cached) {
- targets.forEach(target => this.mergeCachedMetadata(target, cached, storagePath));
- }
- } catch (error) {
- Logger.error(TAG, `查询缓存元数据失败: ${storagePath} - ${(error as Error).message}`);
- }
- })());
- });
- await Promise.all(tasks);
- } catch (error) {
- Logger.error(TAG, `补全云端歌曲元数据失败: ${(error as Error).message}`);
- }
- }
- private resolveStoragePathForItem(item: VideoItem): string | null {
- if (!item) {
- return null;
- }
- if (item.type === CommonConstants.TYPE_WEBDAV) {
- const relative = item.remote_rel_path || WebDavUrlUtil.toStoragePath(item.filePath);
- if (relative) {
- item.remote_rel_path = relative;
- return relative;
- }
- return null;
- }
- if (item.type === CommonConstants.TYPE_SMB || item.type === CommonConstants.TYPE_NAVIDROME) {
- if (item.remote_rel_path) {
- return item.remote_rel_path;
- }
- return null;
- }
- return null;
- }
- private mergeCachedMetadata(target: VideoItem, cached: VideoItem, storagePath: string): void {
- target.id = cached.id || target.id || storagePath;
- target.name = cached.name || target.name;
- target.artist = cached.artist || target.artist;
- target.album = cached.album || target.album;
- target.duration = cached.duration || target.duration;
- target.size = cached.size || target.size;
- target.pixelMapPath = cached.pixelMapPath || target.pixelMapPath;
- target.pixelMap = cached.pixelMap || target.pixelMap;
- target.lyricContent = cached.lyricContent || target.lyricContent;
- target.md5Str = cached.md5Str || target.md5Str;
- target.bit_rate = cached.bit_rate || target.bit_rate;
- target.sampleRate = cached.sampleRate || target.sampleRate;
- target.trackCount = cached.trackCount || target.trackCount;
- target.mimeType = cached.mimeType || target.mimeType;
- target.pyStr = cached.pyStr || target.pyStr;
- target.genre = cached.genre || target.genre;
- target.track = cached.track || target.track;
- target.year = cached.year || target.year;
- target.webdav_account_id = target.webdav_account_id || cached.webdav_account_id;
- target.remote_rel_path = target.remote_rel_path || cached.remote_rel_path || storagePath;
- }
- // 将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;
- const port = account.port;
- // 构建基础URL,确保路径重新编码(目录/文件名中的中文与空格)
- const safeHref = this.normalizeRemoteHref(fileInfo.href);
- const encodedHref = encodeURI(safeHref); // 仅编码非保留字符,已编码的百分号不重复编码
- const absoluteUrl = `${protocol}://${host}:${port}${encodedHref}`;
- // 创建VideoItem对象
- // 构造函数签名: (name, id, filePath, type, videoSize, cTime, pixelMap?, size?, pixelMapPath?, artist?, album?, fileName?, lastPlayed?)
- const videoItem = new VideoItem(
- this.getFileNameWithoutExtension(fileInfo.fileName), // name: 歌曲名
- '', // id: 空字符串,WebDAV文件无本地ID
- absoluteUrl, // filePath: 绝对路径
- CommonConstants.TYPE_WEBDAV, // type: WebDAV类型
- fileInfo.contentLength, // videoSize: 文件大小
- Utility.getFormatDateStr(fileInfo.time,'yyyy-MM-dd HH:mm'), // cTime: 修改时间
- undefined, // pixelMap
- undefined, // size
- undefined, // pixelMapPath: 对于WebDAV歌曲不设置图片路径
- Constants.UNKNOWN_ARTIST, // artist: 艺术家
- undefined, // album: 专辑
- fileInfo.fileName // fileName: 真实文件名
- );
- videoItem.size = Utility.formatFSize(fileInfo.contentLength)
- //根据文件名解析艺术家
- const musicData:MusicInfo = parseMusicFileName(fileInfo.fileName);
- if (musicData.isValid) {
- console.log('onecold parseMusicFileName artist:', musicData.artist)
- videoItem.artist = musicData.artist
- videoItem.name = musicData.title
- }
- videoItem.album = ''
- // 设置WebDAV账号ID,用于后续认证信息查询
- if (account && account.id) {
- videoItem.webdav_account_id = account.id.toString();
- }
- // 计算并保存相对路径
- 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;
- }
- 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.album = ''
- 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 === undefined) {
- return;
- }
- const normalized = this.normalizeFullPath(path);
- const sanitizedLabel = this.sanitizeLabel(label);
- this.pathDisplayNames.set(normalized, sanitizedLabel);
- }
- private sanitizeLabel(label: string): string {
- if (!label) {
- return '';
- }
- if (label === '根目录') {
- return label;
- }
- let sanitized = label.trim();
- sanitized = sanitized.replace(/\/+$/, '');
- if (sanitized.length === 0) {
- sanitized = '/';
- }
- try {
- sanitized = decodeURIComponent(sanitized);
- } catch (error) {
- // ignore decode failure, keep original label
- }
- return sanitized;
- }
- private getBasePath(account: WebDavAccount | null = this.currentAccount): string {
- if (account && account.filepath) {
- return this.normalizeFullPath(account.filepath);
- }
- return '/';
- }
- private getRelativePath(fullPath: string, basePath: string): string {
- const normalizedFull = this.normalizeFullPath(fullPath);
- const normalizedBase = this.normalizeFullPath(basePath);
- if (normalizedFull === normalizedBase) {
- return '';
- }
- if (normalizedFull.startsWith(normalizedBase) && normalizedBase !== '/') {
- return normalizedFull.substring(normalizedBase.length).replace(/^\/+/, '');
- }
- return normalizedFull.replace(/^\/+/, '');
- }
- 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();
- for (let i = 0; i < this.audioExtensions.length; i++) {
- if (ext === this.audioExtensions[i]) {
- return true;
- }
- }
- return false;
- }
- // 获取文件扩展名
- private getFileExtension(fileName: string): string {
- const lastDotIndex = fileName.lastIndexOf('.');
- if (lastDotIndex === -1) {
- return '';
- }
- return fileName.substring(lastDotIndex);
- }
- // 获取不带扩展名的文件名
- private getFileNameWithoutExtension(fileName: string): string {
- const lastDotIndex = fileName.lastIndexOf('.');
- if (lastDotIndex === -1) {
- return fileName;
- }
- return fileName.substring(0, lastDotIndex);
- }
- // 进入文件夹
- public async enterFolder(folder: FileInfo): Promise<void> {
- if (!folder.isDirectory) {
- Logger.error(TAG, '不是文件夹,无法进入');
- return;
- }
- Logger.info(TAG, '准备进入文件夹:', folder.fileName);
- Logger.info(TAG, '当前路径:', this.currentPath);
- Logger.info(TAG, '目标路径:', folder.href);
- // 保存当前路径到历史记录
- this.registerPathLabel(folder.href, folder.fileName);
- this.pathHistory.push(this.currentPath);
- Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
- // 加载文件夹内容
- await this.loadFilesInfoFromAccount(this.currentAccount, folder.href);
- }
- public async enterFolderFromPath(path: string): Promise<void> {
- const normalizedTarget = this.normalizeFullPath(path);
- Logger.info(TAG, '当前路径:', this.currentPath);
- Logger.info(TAG, '目标路径:', normalizedTarget);
- this.rebuildPathHistoryForTarget(normalizedTarget);
- Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
- await this.loadFilesInfoFromAccount(this.currentAccount, normalizedTarget);
- }
- private rebuildPathHistoryForTarget(targetPath: string): void {
- if (!targetPath) {
- this.pathHistory = [];
- return;
- }
- const normalizedTarget = this.normalizeFullPath(targetPath);
- const basePath = this.getBasePath();
- if (normalizedTarget === basePath) {
- this.pathHistory = [];
- return;
- }
- const relativePath = this.getRelativePath(normalizedTarget, basePath);
- if (!relativePath) {
- this.pathHistory = [];
- return;
- }
- const parts = relativePath.split('/').filter(part => part.length > 0);
- const rebuiltHistory: string[] = [];
- // 把账户根路径作为栈底,以便可以返回
- rebuiltHistory.push(basePath);
- let cumulative = basePath;
- for (let i = 0; i < parts.length - 1; i++) {
- const segment = parts[i];
- cumulative = this.normalizeFullPath(`${cumulative === '/' ? '' : cumulative}/${segment}`);
- rebuiltHistory.push(cumulative);
- }
- this.pathHistory = rebuiltHistory;
- }
- // 返回上级目录
- public async goBack(): Promise<void> {
- if (this.pathHistory.length === 0) {
- Logger.info(TAG, '已经在根目录,无法返回');
- return;
- }
- // 从历史记录中取出上一级路径
- const previousPath = this.pathHistory.pop();
- if (previousPath !== undefined) {
- await this.loadFilesInfoFromAccount(this.currentAccount, previousPath);
- }
- }
- // 获取面包屑路径数组
- public getBreadcrumbs(): BreadcrumbItem[] {
- const basePath = this.getBasePath();
- const breadcrumbs: BreadcrumbItem[] = [{
- label: '根目录',
- path: basePath
- }];
- if (!this.currentPath || this.normalizeFullPath(this.currentPath) === basePath) {
- return breadcrumbs;
- }
- const relativePath = this.getRelativePath(this.currentPath, basePath);
- if (!relativePath) {
- return breadcrumbs;
- }
- const parts = relativePath.split('/').filter(part => part.length > 0);
- let cumulative = basePath;
- for (let i = 0; i < parts.length; i++) {
- const segment = parts[i];
- const targetPath = this.normalizeFullPath(`${cumulative === '/' ? '' : cumulative}/${segment}`);
- const label = this.pathDisplayNames.get(targetPath) ?? segment;
- breadcrumbs.push({
- label: this.sanitizeLabel(label),
- path: targetPath
- });
- cumulative = targetPath;
- }
- return breadcrumbs;
- }
- // 是否可以返回上级
- public canGoBack(): boolean {
- if(this.currentPath==='/')
- return false
- return this.pathHistory.length > 0;
- }
- // ==================== Preferences操作 ====================
- // 加载历史数据(从Preferences迁移)
- public async loadInfo(): Promise<void> {
- try {
- // 这里可以添加从Preferences加载历史配置的逻辑
- Logger.info(TAG, '从Preferences加载配置');
- } catch (error) {
- Logger.error(TAG, '从Preferences加载配置失败:', error.toString());
- }
- }
- // 保存配置到Preferences
- public async saveInfo(): Promise<void> {
- try {
- // 这里可以添加保存配置到Preferences的逻辑
- Logger.info(TAG, '保存配置到Preferences');
- } catch (error) {
- Logger.error(TAG, '保存配置到Preferences失败:', error.toString());
- }
- }
- // ==================== 下载队列管理 ====================
- // 添加到下载队列
- public addToDownloadQueue(song: VideoItem, account: WebDavAccount): void {
- const task: TransferTask = { song, account };
- this.downloadQueue.push(task);
- Logger.info(TAG, '添加到下载队列:', song.name);
- this.notifyObservers(RemoteDriveManagerStates.DownloadQueueChanged);
- }
- // 从下载队列移除
- public removeFromDownloadQueue(index: number): void {
- if (index >= 0 && index < this.downloadQueue.length) {
- const task = this.downloadQueue[index];
- this.downloadQueue.splice(index, 1);
- Logger.info(TAG, '从下载队列移除:', task.song.name);
- this.notifyObservers(RemoteDriveManagerStates.DownloadQueueChanged);
- }
- }
- // 清空下载队列
- public clearDownloadQueue(): void {
- this.downloadQueue = [];
- Logger.info(TAG, '清空下载队列');
- this.notifyObservers(RemoteDriveManagerStates.DownloadQueueChanged);
- }
- // ==================== 上传队列管理 ====================
- // 添加到上传队列
- public addToUploadQueue(song: VideoItem, account: WebDavAccount): void {
- const task: TransferTask = { song, account };
- this.uploadQueue.push(task);
- Logger.info(TAG, '添加到上传队列:', song.name);
- this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
- }
- // 从上传队列移除
- public removeFromUploadQueue(index: number): void {
- if (index >= 0 && index < this.uploadQueue.length) {
- const task = this.uploadQueue[index];
- this.uploadQueue.splice(index, 1);
- Logger.info(TAG, '从上传队列移除:', task.song.name);
- this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
- }
- }
- // 清空上传队列
- public clearUploadQueue(): void {
- this.uploadQueue = [];
- Logger.info(TAG, '清空上传队列');
- this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
- }
- // ==================== 安全认证方法 ====================
- // 获取WebDAV认证信息(用于播放器)
- public async getWebDavAuthHeaders(accountId: string): Promise<WebDavAuthInfo | null> {
- const account = await this.getWebDavAccountById(accountId);
- if (!account || !account.account || !account.password) {
- Logger.error(TAG, '无效的WebDAV账户或缺少认证信息');
- return null;
- }
- // 构建基础URL
- const protocol: string = account.enableHttps ? 'https' : 'http';
- const host: string = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- const port: number = account.port;
- // 构建认证头
- const credentials = buffer
- .from(`${account.account}:${account.password}`)
- .toString("base64");
- const authInfo: WebDavAuthInfo = {
- headers: {
- 'Authorization': `Basic ${credentials}`,
- 'User-Agent': 'TTMusic/1.0'
- },
- url: `${protocol}://${host}:${port}`,
- accountId: accountId
- };
- return authInfo;
- }
- }
- /**
- * 构建HTTP请求头,特别处理WebDAV认证
- * @param currentSong - 当前播放的歌曲信息
- * @param videoUrl - 当前歌曲的URL
- * @param webDavAuthItem - 当前WebDAV认证信息(实例变量)
- * @returns Promise<Map<string, string>> HTTP请求头
- */
- export async function buildHttpHeadersWithWebDav(
- currentSong: VideoItem | undefined,
- videoUrl: string,
- webDavAuthItem: WebDavAuthItem
- ): Promise<Map<string, string>> {
- const headers = new Map<string, string>();
- let isWebDavSong = false;
- if (currentSong) {
- Logger.info(`heanup 当前歌曲信息 - name: ${currentSong.name}, type: ${currentSong.type}, filePath: ${currentSong.filePath}`);
- // 检查是否为WebDAV歌曲
- if (currentSong.type === CommonConstants.TYPE_WEBDAV) {
- isWebDavSong = true;
- // 优先从实例变量获取认证信息
- if (webDavAuthItem) {
- Logger.info(`heanup 从实例变量获取到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
- } else {
- // 回退到全局上下文
- try {
- const globalContext = GlobalContext.getContext();
- webDavAuthItem = globalContext.getObject('webDavAuthInfo') as WebDavAuthItem;
- if (webDavAuthItem) {
- Logger.info(`heanup 从全局上下文获取到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
- } else {
- Logger.warn(`heanup 全局上下文中没有WebDAV认证信息`);
- }
- } catch (error) {
- Logger.error(`heanup 获取全局上下文失败:`, error.toString());
- }
- }
- // 如果识别为WebDAV但没有认证信息,记录警告
- if (!webDavAuthItem) {
- Logger.warn(`heanup 识别为WebDAV播放但缺少认证信息,可能需要手动配置WebDAV账户`);
- }
- }
- if (isWebDavSong && webDavAuthItem) {
- Logger.info(`heanup 检测到WebDAV播放,添加安全认证头`);
- Logger.info(`heanup 歌曲URL: ${videoUrl}`);
- try {
- if (webDavAuthItem && webDavAuthItem.accountId) {
- Logger.info(`heanup 找到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
- // 使用RemoteDriveManager获取认证头
- const webdavManager = RemoteDriveManager.getInstance();
- const authHeaders = await webdavManager.getWebDavAuthHeaders(webDavAuthItem.accountId.toString());
- if (authHeaders) {
- // 添加Basic认证头(使用规范字段名)
- headers.set("Authorization", authHeaders.headers.Authorization);
- } else {
- Logger.error(`heanup 无法获取WebDAV认证头`);
- }
- } else {
- Logger.error(`heanup WebDAV认证信息无效或缺少accountId`);
- }
- } catch (error) {
- Logger.error(`heanup 获取WebDAV认证信息失败:`, error.toString());
- }
- // 添加标准的WebDAV请求头
- headers.set("User-Agent", "TTMusic-WebDAV/1.0");
- headers.set("Accept", "*/*");
- // 请求首段数据,触发服务器返回 206(部分内容)以适配流式播放
- headers.set("Range", "bytes=0-");
- Logger.info(`heanup WebDAV安全认证头设置完成`);
- }
- }
- // 输出所有设置的头部信息用于调试
- console.log(`heanup 设置的HTTP头部信息:`);
- const headerIterator = headers.entries();
- let headerEntry = headerIterator.next();
- while (!headerEntry.done) {
- const key = headerEntry.value[0];
- const value = headerEntry.value[1];
- console.log(`heanup ${key}: ${value}`);
- headerEntry = headerIterator.next();
- }
- return headers;
- }
- /**
- * WebDAV认证信息(LocalMusic专用)
- */
- export interface WebDavAuthItem {
- accountId: number;
- host: string;
- port: number;
- account: string;
- password: string;
- enableHttps: boolean;
- }
|