RemoteDriveManager.ets 60 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624
  1. // RemoteDriveManager - WebDAV管理器(简化版)
  2. import { VideoItem } from '../../viewmodel/VideoItem';
  3. import { common } from '@kit.AbilityKit';
  4. import { FileInfo } from '../../viewmodel/FileInfo';
  5. import FileManager, { merge2paths } from './FileManager';
  6. import { BusinessError } from '@kit.BasicServicesKit';
  7. import { RemoteDriveManagerStates } from '../enums/RemoteDriveManagerStates';
  8. import { RcpSocket } from './RcpSocketUtil';
  9. import { DataBaseUtil } from './DataBaseUtil';
  10. import { WebDavAccount } from '../../viewmodel/WebDavAccount';
  11. import { relationalStore } from '@kit.ArkData';
  12. import { Constants } from '../../Constants';
  13. import Logger from './Logger';
  14. import { buffer } from '@kit.ArkTS';
  15. import { CommonConstants } from '../constants/CommonConstants';
  16. import { GlobalContext } from '@pura/harmony-utils';
  17. import { MusicInfo, parseMusicFileName, Utility } from './Utility';
  18. import { RemoteDriveType } from '../enums/RemoteDriveType';
  19. import { listSmbDirectory, SmbDirectoryEntry } from '../network/SmbBridge';
  20. import { NavidromeApi, NavidromeAlbumDetail, NavidromeArtist, NavidromeArtistDetail, NavidromeSong } from '../network/NavidromeApi';
  21. import { WebDavUrlUtil } from './WebDavUrlUtil';
  22. import MediaTable from './MediaTable';
  23. const TAG = 'heanup RemoteDriveManager';
  24. export interface BreadcrumbItem {
  25. label: string;
  26. path: string;
  27. }
  28. // WebDAV认证信息接口
  29. export interface WebDavAuthInfo {
  30. headers: Record<string, string>;
  31. url: string;
  32. accountId?: string; // 添加账号ID字段,用于标识认证信息对应的账号
  33. }
  34. // 流媒体认证信息接口
  35. export interface StreamAuthInfo {
  36. url: string;
  37. headers: Record<string, string>;
  38. }
  39. export interface TransferTask {
  40. song: VideoItem;
  41. account: WebDavAccount;
  42. }
  43. @Observed
  44. export class RemoteDriveManager {
  45. public rcpSocket: RcpSocket = RcpSocket.getInstance();
  46. private dataBaseUtil = DataBaseUtil.getInstance();
  47. public observers: Array<(event: string) => void> = [];
  48. public static instance: RemoteDriveManager;
  49. public context: common.Context | undefined;
  50. public DownloadDirectoryFilePath: string = '';
  51. public audioExtensions = Constants.AUDIO_EXTENSIONS;
  52. public lyricExtensions = Constants.LYRIC_EXTENSIONS;
  53. public imageExtensions = Constants.IMAGE_EXTENSIONS;
  54. // 数据表名称
  55. public preferenceName = Constants.WEBDAV_PREFERENCE_NAME;
  56. public webDavTable = 'WebDavAccount';
  57. public receivedSize: number = 0;
  58. public totalSize: number = 0;
  59. public webDavAccounts: WebDavAccount[] = [];
  60. public webDavSongs: VideoItem[] = [];
  61. public webDavFiles: FileInfo[] = []; // 当前目录的所有文件(包括文件夹)
  62. // 路径导航
  63. public currentPath: string = ''; // 当前浏览的路径
  64. public pathHistory: string[] = []; // 路径历史记录
  65. // 错误信息
  66. public ErrorMessage: string | BusinessError = '';
  67. // 下载队列
  68. public downloadQueue: TransferTask[] = [];
  69. public finishDownloadQueue: TransferTask[] = [];
  70. public isProcessingQueue: boolean = false;
  71. public currentDownloadTask: TransferTask | null = null;
  72. public isPauseDownload: boolean = true;
  73. // 上传队列
  74. public uploadQueue: TransferTask[] = [];
  75. public finishUploadQueue: TransferTask[] = [];
  76. private navidromeApi: NavidromeApi = new NavidromeApi();
  77. private pathDisplayNames: Map<string, string> = new Map();
  78. private lastAccountId: number | null = null;
  79. public isProcessingUploadQueue: boolean = false;
  80. public currentUploadTask: TransferTask | null = null;
  81. public isPauseUpload: boolean = true;
  82. public currentAccount:WebDavAccount = new WebDavAccount();
  83. public constructor() {
  84. this.rcpSocket.subscribeHTTPDataTransfer((event: string) => {
  85. this.notifyObservers(event);
  86. });
  87. }
  88. public static getInstance(): RemoteDriveManager {
  89. if (!RemoteDriveManager.instance) {
  90. RemoteDriveManager.instance = new RemoteDriveManager();
  91. }
  92. return RemoteDriveManager.instance;
  93. }
  94. public setContext(context: common.Context): void {
  95. this.context = context;
  96. this.DownloadDirectoryFilePath = merge2paths(context.filesDir, 'Download');
  97. }
  98. // ==================== 观察者模式 ====================
  99. public subscribe(callback: (event: string) => void): void {
  100. this.observers.push(callback);
  101. }
  102. public unsubscribe(callback: (event: string) => void): void {
  103. const index = this.observers.indexOf(callback);
  104. if (index > -1) {
  105. this.observers.splice(index, 1);
  106. }
  107. }
  108. public notifyObservers(event: string): void {
  109. Logger.info(TAG, '通知观察者:', event);
  110. Logger.info(TAG, '观察者数量:', this.observers.length.toString());
  111. for (let i = 0; i < this.observers.length; i++) {
  112. const observer = this.observers[i];
  113. Logger.info(TAG, '调用观察者', i.toString(), ',事件:', event);
  114. observer(event);
  115. }
  116. }
  117. // ==================== 数据库操作 ====================
  118. /**
  119. * 根据webdav_account_id查询WebDAV账号信息
  120. * @param accountId WebDAV账号ID
  121. * @returns Promise<WebDavAccount | null> 返回WebDAV账号信息或null
  122. */
  123. public async getWebDavAccountById(accountId: string): Promise<WebDavAccount | null> {
  124. if (!accountId) {
  125. Logger.warn(TAG, 'webdav_account_id为空,无法查询账号信息');
  126. return null;
  127. }
  128. try {
  129. const db = await this.dataBaseUtil.getDB();
  130. const predicates = new relationalStore.RdbPredicates(this.webDavTable);
  131. predicates.equalTo('id', accountId);
  132. const resultSet = await db.query(predicates);
  133. if (resultSet.rowCount > 0) {
  134. resultSet.goToFirstRow();
  135. const account = new WebDavAccount();
  136. account.id = resultSet.getLong(resultSet.getColumnIndex('id'));
  137. account.name = resultSet.getString(resultSet.getColumnIndex('name'));
  138. account.host = resultSet.getString(resultSet.getColumnIndex('host'));
  139. account.localHost = resultSet.getString(resultSet.getColumnIndex('localHost'));
  140. account.isUseLocalHost = resultSet.getLong(resultSet.getColumnIndex('isUseLocalHost')) === 1;
  141. account.port = resultSet.getLong(resultSet.getColumnIndex('port'));
  142. account.filepath = resultSet.getString(resultSet.getColumnIndex('filepath'));
  143. account.imageFilePath = resultSet.getString(resultSet.getColumnIndex('imageFilePath'));
  144. account.lyricFilePath = resultSet.getString(resultSet.getColumnIndex('lyricFilePath'));
  145. account.uploadFilePath = resultSet.getString(resultSet.getColumnIndex('uploadFilePath'));
  146. account.account = resultSet.getString(resultSet.getColumnIndex('account'));
  147. account.password = resultSet.getString(resultSet.getColumnIndex('password'));
  148. account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
  149. account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
  150. account.webType = resultSet.getLong(resultSet.getColumnIndex('webType'));
  151. const smbShareIndex = resultSet.getColumnIndex('smbShare');
  152. const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
  153. const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
  154. if (smbShareIndex >= 0) {
  155. account.smbShare = resultSet.getString(smbShareIndex) ?? '';
  156. }
  157. if (smbDomainIndex >= 0) {
  158. account.smbDomain = resultSet.getString(smbDomainIndex) ?? '';
  159. }
  160. if (navBaseIndex >= 0) {
  161. const stored = resultSet.getString(navBaseIndex);
  162. account.navidromeBasePath = stored && stored.length > 0 ? stored : '/rest';
  163. } else {
  164. account.navidromeBasePath = '/rest';
  165. }
  166. account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
  167. resultSet.close();
  168. Logger.info(TAG, `成功查询到WebDAV账号: ${account.name}, ID: ${account.id}`);
  169. return account;
  170. } else {
  171. Logger.warn(TAG, `未找到ID为 ${accountId} 的WebDAV账号`);
  172. resultSet.close();
  173. return null;
  174. }
  175. } catch (error) {
  176. const err = error as Error;
  177. Logger.error(TAG, `查询WebDAV账号失败: ${err.message}`);
  178. return null;
  179. }
  180. }
  181. /**
  182. * 根据WebDAV账号构建认证信息
  183. * @param accountId WebDAV账号ID
  184. * @returns Promise<WebDavAuthInfo | null> 返回认证信息或null
  185. */
  186. public async buildAuthInfoByAccountId(accountId: string): Promise<WebDavAuthInfo | null> {
  187. const account = await this.getWebDavAccountById(accountId);
  188. if (!account) {
  189. Logger.warn(TAG, `无法构建认证信息,账号ID ${accountId} 对应的账号不存在`);
  190. return null;
  191. }
  192. try {
  193. // 构建基础URL
  194. const protocol = account.enableHttps ? 'https' : 'http';
  195. const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
  196. const port = account.port && account.port !== 80 && account.port !== 443 ? `:${account.port}` : '';
  197. const baseUrl = `${protocol}://${host}${port}`;
  198. // 构建认证头
  199. const headers: Record<string, string> = {};
  200. if (account.account && account.password) {
  201. const credentials = `${account.account}:${account.password}`;
  202. // 简化base64编码实现,避免复杂的类型转换
  203. try {
  204. const bufferObj = buffer.from(credentials, 'utf-8');
  205. const base64Credentials = bufferObj.toString('base64');
  206. headers['Authorization'] = `Basic ${base64Credentials}`;
  207. } catch (error) {
  208. const err = error as Error;
  209. Logger.warn(TAG, `base64编码失败,使用原始凭据: ${err.message}`);
  210. headers['Authorization'] = `Basic ${credentials}`;
  211. }
  212. }
  213. Logger.info(TAG, `成功构建WebDAV认证信息,账号: ${account.name}, URL: ${baseUrl}`);
  214. return {
  215. headers: headers,
  216. url: baseUrl,
  217. accountId: accountId
  218. };
  219. } catch (error) {
  220. const err = error as Error;
  221. Logger.error(TAG, `构建WebDAV认证信息失败: ${err.message}`);
  222. return null;
  223. }
  224. }
  225. /**
  226. * 构建HTTP请求头,通过webdav_account_id查询认证信息
  227. * @param currentSong - 当前播放的歌曲信息
  228. * @param videoUrl - 当前歌曲的URL
  229. * @returns Promise<Map<string, string>> HTTP请求头
  230. */
  231. public async buildHttpHeadersWithAccountId(
  232. currentSong: VideoItem | undefined,
  233. videoUrl: string
  234. ): Promise<Map<string, string>> {
  235. const headers = new Map<string, string>();
  236. if (!currentSong) {
  237. return headers;
  238. }
  239. Logger.info(`heanup 当前歌曲信息 - name: ${currentSong.name}, type: ${currentSong.type}, filePath: ${currentSong.filePath}`);
  240. // 检查是否为WebDAV歌曲
  241. if (currentSong.type === CommonConstants.TYPE_WEBDAV) {
  242. // 优先使用webdav_account_id查询认证信息
  243. if (currentSong.webdav_account_id) {
  244. Logger.info(`heanup 使用webdav_account_id查询认证信息,账号ID: ${currentSong.webdav_account_id}`);
  245. try {
  246. const authInfo = await this.buildAuthInfoByAccountId(currentSong.webdav_account_id);
  247. if (authInfo) {
  248. // 添加WebDAV认证头
  249. Object.keys(authInfo.headers).forEach(key => {
  250. headers.set(key, authInfo.headers[key]);
  251. });
  252. Logger.info(`heanup 成功通过webdav_account_id获取WebDAV认证信息`);
  253. } else {
  254. Logger.warn(`heanup 无法通过webdav_account_id ${currentSong.webdav_account_id} 获取认证信息`);
  255. }
  256. } catch (error) {
  257. const err = error as Error;
  258. Logger.error(`heanup 查询webdav_account_id认证信息时出错: ${err.message}`);
  259. }
  260. } else {
  261. Logger.warn(`heanup WebDAV歌曲缺少webdav_account_id字段,无法查询认证信息`);
  262. }
  263. // 如果没有认证信息,记录警告
  264. if (headers.size === 0) {
  265. Logger.warn(`heanup WebDAV歌曲缺少认证信息,可能需要手动配置WebDAV账户`);
  266. }
  267. }
  268. return headers;
  269. }
  270. /**
  271. * 更新现有WebDAV歌曲的webdav_account_id字段
  272. * 用于修复旧版本数据库中缺少webdav_account_id的记录
  273. * @param accountId WebDAV账号ID
  274. */
  275. public async updateWebDavSongsAccountId(accountId: string): Promise<void> {
  276. try {
  277. const db = await this.dataBaseUtil.getDB();
  278. // 查询所有TYPE_WEBDAV且webdav_account_id为空或null的记录
  279. const querySql = `
  280. SELECT id, filePath FROM mediaTable
  281. WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB})
  282. AND (webdav_account_id IS NULL OR webdav_account_id = '')
  283. `;
  284. const resultSet = await db.querySql(querySql);
  285. const updateCount = resultSet.rowCount;
  286. if (updateCount > 0) {
  287. Logger.info(TAG, `发现 ${updateCount} 个WebDAV歌曲缺少webdav_account_id,开始更新`);
  288. // 更新这些记录的webdav_account_id
  289. const updateSql = `
  290. UPDATE mediaTable
  291. SET webdav_account_id = ?
  292. WHERE mtype IN (${CommonConstants.TYPE_WEBDAV}, ${CommonConstants.TYPE_SMB})
  293. AND (webdav_account_id IS NULL OR webdav_account_id = '')
  294. `;
  295. await db.executeSql(updateSql, [accountId]);
  296. Logger.info(TAG, `成功更新 ${updateCount} 个WebDAV歌曲的webdav_account_id为: ${accountId}`);
  297. } else {
  298. Logger.info(TAG, `所有WebDAV歌曲都已有webdav_account_id,无需更新`);
  299. }
  300. resultSet.close();
  301. } catch (error) {
  302. const err = error as Error;
  303. Logger.error(TAG, `更新WebDAV歌曲webdav_account_id失败: ${err.message}`);
  304. }
  305. }
  306. /**
  307. * 修复所有WebDAV歌曲的webdav_account_id字段
  308. * 用于全局修复旧版本数据库中缺少webdav_account_id的记录
  309. * @param accountId WebDAV账号ID(可选,如果不提供则使用当前激活的账号)
  310. */
  311. public async fixAllWebDavSongsAccountId(accountId?: string): Promise<void> {
  312. try {
  313. // 如果没有提供accountId,尝试使用当前激活的账号
  314. if (!accountId) {
  315. const activeAccount = await this.getActiveWebDavAccount();
  316. if (activeAccount && activeAccount.id) {
  317. accountId = activeAccount.id.toString();
  318. } else {
  319. Logger.warn(TAG, `无法找到激活的WebDAV账号来修复歌曲记录`);
  320. return;
  321. }
  322. }
  323. Logger.info(TAG, `开始修复所有WebDAV歌曲的webdav_account_id,使用账号ID: ${accountId}`);
  324. await this.updateWebDavSongsAccountId(accountId);
  325. Logger.info(TAG, `WebDAV歌曲webdav_account_id修复完成`);
  326. } catch (error) {
  327. const err = error as Error;
  328. Logger.error(TAG, `修复WebDAV歌曲webdav_account_id失败: ${err.message}`);
  329. }
  330. }
  331. /**
  332. * 获取当前激活的WebDAV账号
  333. */
  334. private async getActiveWebDavAccount(): Promise<WebDavAccount | null> {
  335. try {
  336. const db = await this.dataBaseUtil.getDB();
  337. const predicates = new relationalStore.RdbPredicates(this.webDavTable);
  338. predicates.equalTo('isActivate', 1);
  339. predicates.limitAs(1);
  340. const resultSet = await db.query(predicates);
  341. if (resultSet.rowCount > 0) {
  342. resultSet.goToFirstRow();
  343. const account = new WebDavAccount();
  344. account.id = resultSet.getLong(resultSet.getColumnIndex('id'));
  345. account.name = resultSet.getString(resultSet.getColumnIndex('name'));
  346. account.host = resultSet.getString(resultSet.getColumnIndex('host'));
  347. account.localHost = resultSet.getString(resultSet.getColumnIndex('localHost'));
  348. account.isUseLocalHost = resultSet.getLong(resultSet.getColumnIndex('isUseLocalHost')) === 1;
  349. account.port = resultSet.getLong(resultSet.getColumnIndex('port'));
  350. account.filepath = resultSet.getString(resultSet.getColumnIndex('filepath'));
  351. account.imageFilePath = resultSet.getString(resultSet.getColumnIndex('imageFilePath'));
  352. account.lyricFilePath = resultSet.getString(resultSet.getColumnIndex('lyricFilePath'));
  353. account.uploadFilePath = resultSet.getString(resultSet.getColumnIndex('uploadFilePath'));
  354. account.account = resultSet.getString(resultSet.getColumnIndex('account'));
  355. account.password = resultSet.getString(resultSet.getColumnIndex('password'));
  356. account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
  357. account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
  358. account.webType = resultSet.getLong(resultSet.getColumnIndex('webType'));
  359. const smbShareIndex = resultSet.getColumnIndex('smbShare');
  360. const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
  361. if (smbShareIndex >= 0) {
  362. account.smbShare = resultSet.getString(smbShareIndex) ?? '';
  363. }
  364. if (smbDomainIndex >= 0) {
  365. account.smbDomain = resultSet.getString(smbDomainIndex) ?? '';
  366. }
  367. account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
  368. resultSet.close();
  369. return account;
  370. } else {
  371. resultSet.close();
  372. return null;
  373. }
  374. } catch (error) {
  375. const err = error as Error;
  376. Logger.error(TAG, `获取激活的WebDAV账号失败: ${err.message}`);
  377. return null;
  378. }
  379. }
  380. // 创建WebDAV账户表
  381. public createWebDavTableInDB(): Promise<void> {
  382. const createTableSql = `CREATE TABLE IF NOT EXISTS ${this.webDavTable} (
  383. id INTEGER PRIMARY KEY AUTOINCREMENT,
  384. name TEXT,
  385. isActivate INTEGER DEFAULT 1,
  386. host TEXT,
  387. localHost TEXT,
  388. isUseLocalHost INTEGER DEFAULT 0,
  389. port INTEGER,
  390. filepath TEXT,
  391. imageFilePath TEXT,
  392. lyricFilePath TEXT,
  393. uploadFilePath TEXT,
  394. account TEXT,
  395. password TEXT,
  396. enableHttps INTEGER DEFAULT 0,
  397. coverPath TEXT,
  398. webType INTEGER DEFAULT 0,
  399. smbShare TEXT,
  400. smbDomain TEXT,
  401. navidromeBasePath TEXT
  402. )`;
  403. return this.dataBaseUtil.executeSql(createTableSql)
  404. .then(() => {
  405. Logger.info(TAG, 'WebDAV账户表创建成功');
  406. // 检查并添加新字段(用于数据库升级)
  407. return this.upgradeWebDavTable();
  408. })
  409. .catch((err: Error) => {
  410. Logger.error(TAG, '创建WebDAV账户表失败:', err.message);
  411. throw err;
  412. });
  413. }
  414. // 升级WebDAV表结构
  415. private async upgradeWebDavTable(): Promise<void> {
  416. try {
  417. // 直接尝试添加coverPath字段,如果字段已存在会失败但不影响应用运行
  418. Logger.info(TAG, '检查数据库表结构,尝试添加coverPath字段...');
  419. const addColumnSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN coverPath TEXT`;
  420. await this.dataBaseUtil.executeSql(addColumnSql);
  421. Logger.info(TAG, 'coverPath字段添加成功,数据库升级完成');
  422. } catch (error) {
  423. // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
  424. Logger.info(TAG, 'coverPath字段可能已存在或添加失败,继续正常运行');
  425. }
  426. try {
  427. // 添加webType字段,用于标识WebDAV账户类型
  428. Logger.info(TAG, '检查数据库表结构,尝试添加webType字段...');
  429. const addWebTypeColumnSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN webType INTEGER DEFAULT 0`;
  430. await this.dataBaseUtil.executeSql(addWebTypeColumnSql);
  431. Logger.info(TAG, 'webType字段添加成功,数据库升级完成');
  432. } catch (error) {
  433. // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
  434. Logger.info(TAG, 'webType字段可能已存在或添加失败,继续正常运行');
  435. }
  436. try {
  437. Logger.info(TAG, '尝试添加smbShare字段...');
  438. const addSmbShareSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN smbShare TEXT`;
  439. await this.dataBaseUtil.executeSql(addSmbShareSql);
  440. Logger.info(TAG, 'smbShare字段添加成功');
  441. } catch (error) {
  442. Logger.info(TAG, 'smbShare字段可能已存在或添加失败,继续运行');
  443. }
  444. try {
  445. Logger.info(TAG, '尝试添加smbDomain字段...');
  446. const addSmbDomainSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN smbDomain TEXT`;
  447. await this.dataBaseUtil.executeSql(addSmbDomainSql);
  448. Logger.info(TAG, 'smbDomain字段添加成功');
  449. } catch (error) {
  450. Logger.info(TAG, 'smbDomain字段可能已存在或添加失败,继续运行');
  451. }
  452. try {
  453. Logger.info(TAG, '尝试添加navidromeBasePath字段...');
  454. const addNavBaseSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN navidromeBasePath TEXT`;
  455. await this.dataBaseUtil.executeSql(addNavBaseSql);
  456. Logger.info(TAG, 'navidromeBasePath字段添加成功');
  457. } catch (error) {
  458. Logger.info(TAG, 'navidromeBasePath字段可能已存在或添加失败,继续运行');
  459. }
  460. }
  461. // 从数据库查询所有账户
  462. public async queryWebDavAccountsFromDB(): Promise<void> {
  463. try {
  464. // 确保表结构是最新的
  465. await this.upgradeWebDavTable();
  466. const predicates = new relationalStore.RdbPredicates(this.webDavTable);
  467. // 先查询基础字段(确保这些字段在旧版本中存在)
  468. const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
  469. 'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
  470. 'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain', 'navidromeBasePath'];
  471. const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, baseColumns, predicates);
  472. this.webDavAccounts = [];
  473. while (resultSet.goToNextRow()) {
  474. const account = new WebDavAccount();
  475. account.id = resultSet.getLong(resultSet.getColumnIndex('id'));
  476. account.name = resultSet.getString(resultSet.getColumnIndex('name'));
  477. account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
  478. account.host = resultSet.getString(resultSet.getColumnIndex('host'));
  479. account.localHost = resultSet.getString(resultSet.getColumnIndex('localHost'));
  480. account.isUseLocalHost = resultSet.getLong(resultSet.getColumnIndex('isUseLocalHost')) === 1;
  481. account.port = resultSet.getLong(resultSet.getColumnIndex('port'));
  482. account.filepath = resultSet.getString(resultSet.getColumnIndex('filepath'));
  483. account.imageFilePath = resultSet.getString(resultSet.getColumnIndex('imageFilePath'));
  484. account.lyricFilePath = resultSet.getString(resultSet.getColumnIndex('lyricFilePath'));
  485. account.uploadFilePath = resultSet.getString(resultSet.getColumnIndex('uploadFilePath'));
  486. account.account = resultSet.getString(resultSet.getColumnIndex('account'));
  487. account.password = resultSet.getString(resultSet.getColumnIndex('password'));
  488. account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
  489. const coverPathIndex = resultSet.getColumnIndex('coverPath');
  490. if (coverPathIndex >= 0) {
  491. account.coverPath = resultSet.getString(coverPathIndex) ?? undefined;
  492. }
  493. const webTypeIndex = resultSet.getColumnIndex('webType');
  494. account.webType = webTypeIndex >= 0 ? resultSet.getLong(webTypeIndex) : 0;
  495. const smbShareIndex = resultSet.getColumnIndex('smbShare');
  496. if (smbShareIndex >= 0) {
  497. account.smbShare = resultSet.getString(smbShareIndex) ?? '';
  498. }
  499. const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
  500. if (smbDomainIndex >= 0) {
  501. account.smbDomain = resultSet.getString(smbDomainIndex) ?? '';
  502. }
  503. const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
  504. if (navBaseIndex >= 0) {
  505. const stored = resultSet.getString(navBaseIndex);
  506. account.navidromeBasePath = stored && stored.length > 0 ? stored : '/rest';
  507. } else {
  508. account.navidromeBasePath = '/rest';
  509. }
  510. this.webDavAccounts.push(account);
  511. }
  512. resultSet.close();
  513. Logger.info(TAG, '从数据库加载了', this.webDavAccounts.length.toString(), '个WebDAV账户');
  514. this.notifyObservers(RemoteDriveManagerStates.QueryAccountsSucceed);
  515. } catch (err) {
  516. const error = err as Error;
  517. Logger.error(TAG, '查询WebDAV账户失败:', error.message);
  518. this.notifyObservers(RemoteDriveManagerStates.QueryAccountsFailed);
  519. throw error;
  520. }
  521. }
  522. // 插入新账户
  523. public async insertAccount(
  524. name: string,
  525. host: string,
  526. localHost: string,
  527. isUseLocalHost: boolean,
  528. port: number,
  529. filepath: string,
  530. lyricFilePath: string,
  531. uploadFilePath: string,
  532. imageFilePath: string,
  533. account: string,
  534. password: string,
  535. enableHttps: boolean,
  536. coverPath?: string,
  537. webType: number = 0,
  538. smbShare: string = '',
  539. smbDomain: string = '',
  540. navidromeBasePath: string = '/rest'
  541. ): Promise<void> {
  542. try {
  543. const values: relationalStore.ValuesBucket = {
  544. 'name': name,
  545. 'isActivate': 1,
  546. 'host': host,
  547. 'localHost': localHost,
  548. 'isUseLocalHost': isUseLocalHost ? 1 : 0,
  549. 'port': port,
  550. 'filepath': filepath,
  551. 'imageFilePath': imageFilePath,
  552. 'lyricFilePath': lyricFilePath,
  553. 'uploadFilePath': uploadFilePath,
  554. 'account': account,
  555. 'password': password,
  556. 'enableHttps': enableHttps ? 1 : 0,
  557. 'coverPath': coverPath || null,
  558. 'webType': webType,
  559. 'smbShare': smbShare,
  560. 'smbDomain': smbDomain,
  561. 'navidromeBasePath': navidromeBasePath
  562. };
  563. await this.dataBaseUtil.insertData(this.webDavTable, values);
  564. Logger.info(TAG, '插入WebDAV账户成功:', name);
  565. await this.queryWebDavAccountsFromDB();
  566. this.notifyObservers(RemoteDriveManagerStates.InsertAccountSucceed);
  567. } catch (err) {
  568. const error = err as Error;
  569. Logger.error(TAG, '插入WebDAV账户失败:', error.message);
  570. this.notifyObservers(RemoteDriveManagerStates.InsertAccountFailed);
  571. throw error;
  572. }
  573. }
  574. // 编辑账户
  575. public async editAccount(account: WebDavAccount): Promise<void> {
  576. try {
  577. const values: relationalStore.ValuesBucket = {
  578. 'name': account.name,
  579. 'isActivate': account.isActivate ? 1 : 0,
  580. 'host': account.host,
  581. 'localHost': account.localHost,
  582. 'isUseLocalHost': account.isUseLocalHost ? 1 : 0,
  583. 'port': account.port,
  584. 'filepath': account.filepath,
  585. 'imageFilePath': account.imageFilePath,
  586. 'lyricFilePath': account.lyricFilePath,
  587. 'uploadFilePath': account.uploadFilePath,
  588. 'account': account.account,
  589. 'password': account.password,
  590. 'enableHttps': account.enableHttps ? 1 : 0,
  591. 'coverPath': account.coverPath || null,
  592. 'webType': account.webType,
  593. 'smbShare': account.smbShare,
  594. 'smbDomain': account.smbDomain,
  595. 'navidromeBasePath': account.navidromeBasePath
  596. };
  597. const predicates = new relationalStore.RdbPredicates(this.webDavTable);
  598. predicates.equalTo('id', account.id);
  599. await this.dataBaseUtil.updateData(this.webDavTable, values, predicates);
  600. Logger.info(TAG, '更新WebDAV账户成功:', account.name);
  601. await this.queryWebDavAccountsFromDB();
  602. this.notifyObservers(RemoteDriveManagerStates.EditAccountSucceed);
  603. } catch (err) {
  604. const error = err as Error;
  605. Logger.error(TAG, '更新WebDAV账户失败:', error.message);
  606. this.notifyObservers(RemoteDriveManagerStates.EditAccountFailed);
  607. throw error;
  608. }
  609. }
  610. // 删除账户
  611. public async removeAccount(account: WebDavAccount): Promise<void> {
  612. try {
  613. const predicates = new relationalStore.RdbPredicates(this.webDavTable);
  614. predicates.equalTo('id', account.id);
  615. await this.dataBaseUtil.deleteData(predicates);
  616. Logger.info(TAG, '删除WebDAV账户成功:', account.name);
  617. await this.queryWebDavAccountsFromDB();
  618. this.notifyObservers(RemoteDriveManagerStates.RemoveAccountSucceed);
  619. } catch (err) {
  620. const error = err as Error;
  621. Logger.error(TAG, '删除WebDAV账户失败:', error.message);
  622. this.notifyObservers(RemoteDriveManagerStates.RemoveAccountFailed);
  623. throw error;
  624. }
  625. }
  626. // 获取所有账户
  627. public getAllWebDavAccounts(): WebDavAccount[] {
  628. return this.webDavAccounts;
  629. }
  630. // 获取激活的账户
  631. public getActivatedWebDavAccount(): WebDavAccount | null {
  632. if(this.currentAccount)
  633. return this.currentAccount;
  634. for (let i = 0; i < this.webDavAccounts.length; i++) {
  635. const account = this.webDavAccounts[i];
  636. if (account.isActivate) {
  637. return account;
  638. }
  639. }
  640. return null;
  641. }
  642. // ==================== 文件操作 ====================
  643. // 从WebDAV加载文件列表
  644. public async loadFilesInfoFromWebdav(customPath?: string): Promise<void> {
  645. const account = await this.getActiveWebDavAccount();
  646. if (!account) {
  647. Logger.error(TAG, '没有激活的WebDAV账户');
  648. this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoFailed);
  649. return;
  650. }
  651. await this.loadFilesInfoFromAccount(account, customPath);
  652. }
  653. public async loadFilesInfoFromAccount(account: WebDavAccount,customPath?: string): Promise<void> {
  654. this.currentAccount = account;
  655. if (this.lastAccountId !== account.id) {
  656. this.pathHistory = [];
  657. this.pathDisplayNames.clear();
  658. this.registerPathLabel('/', '根目录');
  659. }
  660. this.lastAccountId = account.id ?? null;
  661. // 更新现有WebDAV歌曲的webdav_account_id字段
  662. if (account && account.id) {
  663. await this.updateWebDavSongsAccountId(account.id.toString());
  664. }
  665. try {
  666. this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoStart);
  667. // 使用自定义路径或账户默认路径
  668. const normalizedFullPath = this.normalizeFullPath(customPath !== undefined ? customPath : account.filepath);
  669. this.currentPath = normalizedFullPath;
  670. if (account.webType === RemoteDriveType.Smb) {
  671. await this.loadSmbFiles(account, normalizedFullPath);
  672. } else if (account.webType === RemoteDriveType.Navidrome) {
  673. await this.loadNavidromeFiles(account, normalizedFullPath);
  674. } else {
  675. await this.loadWebDavFiles(account, normalizedFullPath);
  676. }
  677. this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoSucceed);
  678. } catch (error) {
  679. this.ErrorMessage = error as BusinessError;
  680. this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoFailed);
  681. }
  682. }
  683. private normalizeFullPath(path?: string): string {
  684. if (!path || path.trim().length === 0) {
  685. return '/';
  686. }
  687. let normalized = path.trim().replace(/\\/g, '/');
  688. if (!normalized.startsWith('/')) {
  689. normalized = `/${normalized}`;
  690. }
  691. normalized = normalized.replace(/\/+/g, '/');
  692. if (normalized.length > 1 && normalized.endsWith('/')) {
  693. normalized = normalized.slice(0, -1);
  694. }
  695. return normalized || '/';
  696. }
  697. // 将 WebDAV 响应中的 href 统一转换为以 / 开头的相对路径
  698. private normalizeRemoteHref(rawHref: string): string {
  699. if (!rawHref || rawHref.trim().length === 0) {
  700. return '/';
  701. }
  702. const trimmed = rawHref.trim();
  703. const relativeFromHttp = WebDavUrlUtil.extractRelativePath(trimmed);
  704. if (relativeFromHttp) {
  705. return this.normalizeFullPath(relativeFromHttp);
  706. }
  707. if (trimmed.startsWith('//')) {
  708. const hostEnd = trimmed.indexOf('/', 2);
  709. if (hostEnd > 1) {
  710. return this.normalizeFullPath(trimmed.substring(hostEnd));
  711. }
  712. return '/';
  713. }
  714. return this.normalizeFullPath(trimmed);
  715. }
  716. private toRelativePath(fullPath: string): string {
  717. if (!fullPath || fullPath === '/') {
  718. return '';
  719. }
  720. return fullPath.replace(/^\/+/, '');
  721. }
  722. private async loadWebDavFiles(account: WebDavAccount, fullPath: string): Promise<void> {
  723. const files = await this.rcpSocket.getFileList(
  724. account.host,
  725. account.localHost,
  726. account.isUseLocalHost,
  727. account.port,
  728. fullPath,
  729. account.account,
  730. account.password,
  731. account.enableHttps
  732. );
  733. this.webDavFiles = files;
  734. this.webDavSongs = [];
  735. for (let i = 0; i < files.length; i++) {
  736. const file = files[i];
  737. if (!file.isDirectory && this.isAudioFile(file.fileName)) {
  738. this.webDavSongs.push(this.fileInfoToVideoItem(file, account));
  739. }
  740. }
  741. await this.enrichSongsWithDatabase(this.webDavSongs);
  742. Logger.info(TAG, `从WebDAV获取到 ${files.length} 个文件/文件夹`);
  743. }
  744. private async loadSmbFiles(account: WebDavAccount, fullPath: string): Promise<void> {
  745. if (!account.smbShare) {
  746. throw new Error('SMB账户缺少共享名称');
  747. }
  748. const relativePath = this.toRelativePath(fullPath);
  749. const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
  750. const entries = await listSmbDirectory({
  751. host,
  752. share: account.smbShare,
  753. username: account.account,
  754. password: account.password,
  755. domain: account.smbDomain,
  756. path: relativePath
  757. });
  758. this.webDavFiles = [];
  759. this.webDavSongs = [];
  760. for (let i = 0; i < entries.length; i++) {
  761. const entry = entries[i];
  762. const fileInfo = this.smbEntryToFileInfo(entry, relativePath);
  763. this.webDavFiles.push(fileInfo);
  764. if (!entry.isDirectory && this.isAudioFile(entry.name)) {
  765. this.webDavSongs.push(this.fileInfoToVideoItem(fileInfo, account));
  766. }
  767. }
  768. await this.enrichSongsWithDatabase(this.webDavSongs);
  769. Logger.info(TAG, `从SMB获取到 ${entries.length} 个文件/文件夹`);
  770. }
  771. private async loadNavidromeFiles(account: WebDavAccount, fullPath: string): Promise<void> {
  772. const normalized = this.normalizeFullPath(fullPath);
  773. this.registerPathLabel('/', '根目录');
  774. const segments = normalized.split('/').filter(part => part.length > 0);
  775. if (normalized === '/' || segments.length === 0) {
  776. const artists: NavidromeArtist[] = await this.navidromeApi.getArtists(account);
  777. artists.sort((a, b) => a.name.localeCompare(b.name));
  778. this.webDavFiles = artists.map(artist => {
  779. const info = this.createNavDirectory(artist.name, `/artist/${artist.id}`);
  780. this.registerPathLabel(info.href, artist.name);
  781. return info;
  782. });
  783. this.webDavSongs = [];
  784. Logger.info(TAG, `从Navidrome获取到 ${artists.length} 位艺术家`);
  785. return;
  786. }
  787. if (segments[0] === 'artist' && segments[1]) {
  788. const artistId = segments[1];
  789. const detail: NavidromeArtistDetail = await this.navidromeApi.getArtist(account, artistId);
  790. this.registerPathLabel(`/artist/${artistId}`, detail.name);
  791. this.webDavFiles = detail.albums.map(album => {
  792. const info = this.createNavDirectory(album.name, `/album/${album.id}`);
  793. this.registerPathLabel(info.href, album.name);
  794. return info;
  795. });
  796. this.webDavSongs = [];
  797. Logger.info(TAG, `Navidrome 艺术家 ${detail.name} 包含 ${detail.albums.length} 张专辑`);
  798. return;
  799. }
  800. if (segments[0] === 'album' && segments[1]) {
  801. const albumId = segments[1];
  802. const detail: NavidromeAlbumDetail = await this.navidromeApi.getAlbum(account, albumId);
  803. this.registerPathLabel(`/album/${albumId}`, detail.name);
  804. this.webDavFiles = detail.songs.map(song => this.createNavSongFileInfo(song, albumId));
  805. this.webDavSongs = detail.songs.map(song => this.buildNavidromeVideoItem(song, account, detail));
  806. await this.enrichSongsWithDatabase(this.webDavSongs);
  807. Logger.info(TAG, `Navidrome 专辑 ${detail.name} 包含 ${detail.songs.length} 首歌曲`);
  808. return;
  809. }
  810. throw new Error(`不支持的Navidrome路径: ${fullPath}`);
  811. }
  812. private async enrichSongsWithDatabase(videoItems: VideoItem[]): Promise<void> {
  813. if (!this.context || !videoItems || videoItems.length === 0) {
  814. return;
  815. }
  816. try {
  817. const mediaTable = new MediaTable(this.context);
  818. await new Promise<void>((resolve) => {
  819. mediaTable.getRdbStore(this.context as common.Context, () => resolve());
  820. });
  821. const pathMap: Map<string, VideoItem[]> = new Map();
  822. for (let i = 0; i < videoItems.length; i++) {
  823. const item = videoItems[i];
  824. const storagePath = this.resolveStoragePathForItem(item);
  825. if (!storagePath) {
  826. continue;
  827. }
  828. if (!pathMap.has(storagePath)) {
  829. pathMap.set(storagePath, []);
  830. }
  831. pathMap.get(storagePath)?.push(item);
  832. }
  833. if (pathMap.size === 0) {
  834. return;
  835. }
  836. const tasks: Promise<void>[] = [];
  837. pathMap.forEach((targets, storagePath) => {
  838. tasks.push((async () => {
  839. try {
  840. const cached = await mediaTable.queryVideoByFilePath(storagePath);
  841. if (cached) {
  842. targets.forEach(target => this.mergeCachedMetadata(target, cached, storagePath));
  843. }
  844. } catch (error) {
  845. Logger.error(TAG, `查询缓存元数据失败: ${storagePath} - ${(error as Error).message}`);
  846. }
  847. })());
  848. });
  849. await Promise.all(tasks);
  850. } catch (error) {
  851. Logger.error(TAG, `补全云端歌曲元数据失败: ${(error as Error).message}`);
  852. }
  853. }
  854. private resolveStoragePathForItem(item: VideoItem): string | null {
  855. if (!item) {
  856. return null;
  857. }
  858. if (item.type === CommonConstants.TYPE_WEBDAV) {
  859. const relative = item.remote_rel_path || WebDavUrlUtil.toStoragePath(item.filePath);
  860. if (relative) {
  861. item.remote_rel_path = relative;
  862. return relative;
  863. }
  864. return null;
  865. }
  866. if (item.type === CommonConstants.TYPE_SMB || item.type === CommonConstants.TYPE_NAVIDROME) {
  867. if (item.remote_rel_path) {
  868. return item.remote_rel_path;
  869. }
  870. return null;
  871. }
  872. return null;
  873. }
  874. private mergeCachedMetadata(target: VideoItem, cached: VideoItem, storagePath: string): void {
  875. target.id = cached.id || target.id || storagePath;
  876. target.name = cached.name || target.name;
  877. target.artist = cached.artist || target.artist;
  878. target.album = cached.album || target.album;
  879. target.duration = cached.duration || target.duration;
  880. target.size = cached.size || target.size;
  881. target.pixelMapPath = cached.pixelMapPath || target.pixelMapPath;
  882. target.pixelMap = cached.pixelMap || target.pixelMap;
  883. target.lyricContent = cached.lyricContent || target.lyricContent;
  884. target.md5Str = cached.md5Str || target.md5Str;
  885. target.bit_rate = cached.bit_rate || target.bit_rate;
  886. target.sampleRate = cached.sampleRate || target.sampleRate;
  887. target.trackCount = cached.trackCount || target.trackCount;
  888. target.mimeType = cached.mimeType || target.mimeType;
  889. target.pyStr = cached.pyStr || target.pyStr;
  890. target.genre = cached.genre || target.genre;
  891. target.track = cached.track || target.track;
  892. target.year = cached.year || target.year;
  893. target.webdav_account_id = target.webdav_account_id || cached.webdav_account_id;
  894. target.remote_rel_path = target.remote_rel_path || cached.remote_rel_path || storagePath;
  895. }
  896. // 将FileInfo转换为VideoItem
  897. private fileInfoToVideoItem(fileInfo: FileInfo, account: WebDavAccount): VideoItem {
  898. if (account.webType === RemoteDriveType.Smb) {
  899. return this.buildSmbVideoItem(fileInfo, account);
  900. }
  901. // 构建安全的WebDAV URL(不包含认证信息)
  902. const protocol = account.enableHttps ? 'https' : 'http';
  903. const host = account.isUseLocalHost ? account.localHost : account.host;
  904. const port = account.port;
  905. // 构建基础URL,确保路径重新编码(目录/文件名中的中文与空格)
  906. const safeHref = this.normalizeRemoteHref(fileInfo.href);
  907. const encodedHref = encodeURI(safeHref); // 仅编码非保留字符,已编码的百分号不重复编码
  908. const absoluteUrl = `${protocol}://${host}:${port}${encodedHref}`;
  909. // 创建VideoItem对象
  910. // 构造函数签名: (name, id, filePath, type, videoSize, cTime, pixelMap?, size?, pixelMapPath?, artist?, album?, fileName?, lastPlayed?)
  911. const videoItem = new VideoItem(
  912. this.getFileNameWithoutExtension(fileInfo.fileName), // name: 歌曲名
  913. '', // id: 空字符串,WebDAV文件无本地ID
  914. absoluteUrl, // filePath: 绝对路径
  915. CommonConstants.TYPE_WEBDAV, // type: WebDAV类型
  916. fileInfo.contentLength, // videoSize: 文件大小
  917. Utility.getFormatDateStr(fileInfo.time,'yyyy-MM-dd HH:mm'), // cTime: 修改时间
  918. undefined, // pixelMap
  919. undefined, // size
  920. undefined, // pixelMapPath: 对于WebDAV歌曲不设置图片路径
  921. Constants.UNKNOWN_ARTIST, // artist: 艺术家
  922. undefined, // album: 专辑
  923. fileInfo.fileName // fileName: 真实文件名
  924. );
  925. videoItem.size = Utility.formatFSize(fileInfo.contentLength)
  926. //根据文件名解析艺术家
  927. const musicData:MusicInfo = parseMusicFileName(fileInfo.fileName);
  928. if (musicData.isValid) {
  929. console.log('onecold parseMusicFileName artist:', musicData.artist)
  930. videoItem.artist = musicData.artist
  931. videoItem.name = musicData.title
  932. }
  933. videoItem.album = ''
  934. // 设置WebDAV账号ID,用于后续认证信息查询
  935. if (account && account.id) {
  936. videoItem.webdav_account_id = account.id.toString();
  937. }
  938. // 计算并保存相对路径
  939. try {
  940. const relMatch = absoluteUrl.match(/^https?:\/\/[^\/]+(?::\d+)?(\/.*)$/i);
  941. if (relMatch && relMatch[1]) {
  942. videoItem.remote_rel_path = relMatch[1];
  943. }
  944. } catch (e) {
  945. Logger.warn(TAG, '计算相对路径失败: ' + (e as Error).message);
  946. }
  947. return videoItem;
  948. }
  949. private buildSmbVideoItem(fileInfo: FileInfo, account: WebDavAccount): VideoItem {
  950. const relativePath = fileInfo.href && fileInfo.href.length > 0 ? fileInfo.href : `/${fileInfo.fileName}`;
  951. const normalizedRelative = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
  952. const sanitizedRelative = this.normalizeSmbRelativePath(normalizedRelative, account.smbShare);
  953. const videoItem = new VideoItem(
  954. this.getFileNameWithoutExtension(fileInfo.fileName),
  955. '',
  956. sanitizedRelative,
  957. CommonConstants.TYPE_SMB,
  958. fileInfo.contentLength,
  959. Utility.getFormatDateStr(fileInfo.time, 'yyyy-MM-dd HH:mm'),
  960. undefined,
  961. undefined,
  962. undefined,
  963. Constants.UNKNOWN_ARTIST,
  964. undefined,
  965. fileInfo.fileName
  966. );
  967. videoItem.album = ''
  968. videoItem.size = Utility.formatFSize(fileInfo.contentLength);
  969. if (account && account.id) {
  970. videoItem.webdav_account_id = account.id.toString();
  971. }
  972. videoItem.remote_rel_path = sanitizedRelative;
  973. const hostForDisplay = account.host && account.host.trim().length > 0
  974. ? account.host.trim()
  975. : (account.id?.toString() ?? '');
  976. const cleanedShare = account.smbShare
  977. ? account.smbShare.replace(/^\/+|\/+$/g, '')
  978. : '';
  979. const relativeForDisplay = sanitizedRelative === '/' ? '' : sanitizedRelative;
  980. if (hostForDisplay.length > 0 && cleanedShare.length > 0) {
  981. videoItem.filePath = `smb://${hostForDisplay}/${cleanedShare}${relativeForDisplay}`;
  982. } else if (hostForDisplay.length > 0) {
  983. videoItem.filePath = `smb://${hostForDisplay}${relativeForDisplay}`;
  984. } else {
  985. videoItem.filePath = `smb://${account.id}${relativeForDisplay}`;
  986. }
  987. const musicData: MusicInfo = parseMusicFileName(fileInfo.fileName);
  988. if (musicData.isValid) {
  989. videoItem.artist = musicData.artist;
  990. videoItem.name = musicData.title;
  991. }
  992. return videoItem;
  993. }
  994. private smbEntryToFileInfo(entry: SmbDirectoryEntry, basePath: string): FileInfo {
  995. const normalizedPath = this.combineRemotePath(basePath, entry.name, entry.isDirectory);
  996. const info = new FileInfo('', entry.name, entry.size, Date.now());
  997. info.fileName = entry.name;
  998. info.href = normalizedPath;
  999. info.contentLength = entry.size ?? 0;
  1000. info.isDirectory = entry.isDirectory;
  1001. return info;
  1002. }
  1003. private combineRemotePath(basePath: string, name: string, isDirectory: boolean): string {
  1004. const segments: string[] = [];
  1005. const trimmedBase = basePath ? basePath.replace(/^\/+/, '').replace(/\/+/g, '/') : '';
  1006. if (trimmedBase.length > 0) {
  1007. segments.push(trimmedBase);
  1008. }
  1009. const cleanedName = name.replace(/^\/+/, '').replace(/\/+/g, '/');
  1010. if (cleanedName.length > 0) {
  1011. segments.push(cleanedName);
  1012. }
  1013. let fullPath = `/${segments.filter(part => part.length > 0).join('/')}`;
  1014. if (isDirectory && !fullPath.endsWith('/')) {
  1015. fullPath = `${fullPath}/`;
  1016. }
  1017. if (!isDirectory && fullPath.endsWith('/')) {
  1018. fullPath = fullPath.slice(0, -1);
  1019. }
  1020. return fullPath;
  1021. }
  1022. private createNavDirectory(name: string, href: string): FileInfo {
  1023. const info = new FileInfo('', name, 0, Date.now());
  1024. info.fileName = name;
  1025. info.href = this.normalizeFullPath(href);
  1026. info.isDirectory = true;
  1027. return info;
  1028. }
  1029. private createNavSongFileInfo(song: NavidromeSong, albumId: string): FileInfo {
  1030. const displayName = song.title ?? '未知曲目';
  1031. const info = new FileInfo('', displayName, song.size ?? 0, Date.now());
  1032. info.fileName = `${displayName}${song.suffix ? '.' + song.suffix : ''}`;
  1033. info.href = this.normalizeFullPath(`/album/${albumId}/${song.id}`);
  1034. info.isDirectory = false;
  1035. info.contentLength = song.size ?? 0;
  1036. return info;
  1037. }
  1038. private buildNavidromeVideoItem(song: NavidromeSong, account: WebDavAccount, album?: NavidromeAlbumDetail): VideoItem {
  1039. const title = song.title ?? '未知曲目';
  1040. const fileName = `${title}${song.suffix ? '.' + song.suffix : ''}`;
  1041. const videoItem = new VideoItem(
  1042. title,
  1043. song.id,
  1044. `navidrome://${account.id ?? 0}/${song.id}`,
  1045. CommonConstants.TYPE_NAVIDROME,
  1046. song.size ?? 0,
  1047. Utility.getFormatDateStr(Date.now(), 'yyyy-MM-dd HH:mm'),
  1048. undefined,
  1049. Utility.formatFSize(song.size ?? 0),
  1050. undefined,
  1051. song.artist ?? album?.artist ?? Constants.UNKNOWN_ARTIST,
  1052. album?.name ?? song.album,
  1053. fileName
  1054. );
  1055. videoItem.size = Utility.formatFSize(song.size ?? 0);
  1056. videoItem.webdav_account_id = account.id?.toString();
  1057. videoItem.remote_rel_path = song.id;
  1058. videoItem.bit_rate = song.bitRate ? song.bitRate.toString() : undefined;
  1059. videoItem.mimeType = song.contentType;
  1060. return videoItem;
  1061. }
  1062. private registerPathLabel(path: string, label: string): void {
  1063. if (!path || label === undefined) {
  1064. return;
  1065. }
  1066. const normalized = this.normalizeFullPath(path);
  1067. const sanitizedLabel = this.sanitizeLabel(label);
  1068. this.pathDisplayNames.set(normalized, sanitizedLabel);
  1069. }
  1070. private sanitizeLabel(label: string): string {
  1071. if (!label) {
  1072. return '';
  1073. }
  1074. if (label === '根目录') {
  1075. return label;
  1076. }
  1077. let sanitized = label.trim();
  1078. sanitized = sanitized.replace(/\/+$/, '');
  1079. if (sanitized.length === 0) {
  1080. sanitized = '/';
  1081. }
  1082. try {
  1083. sanitized = decodeURIComponent(sanitized);
  1084. } catch (error) {
  1085. // ignore decode failure, keep original label
  1086. }
  1087. return sanitized;
  1088. }
  1089. private getBasePath(account: WebDavAccount | null = this.currentAccount): string {
  1090. if (account && account.filepath) {
  1091. return this.normalizeFullPath(account.filepath);
  1092. }
  1093. return '/';
  1094. }
  1095. private getRelativePath(fullPath: string, basePath: string): string {
  1096. const normalizedFull = this.normalizeFullPath(fullPath);
  1097. const normalizedBase = this.normalizeFullPath(basePath);
  1098. if (normalizedFull === normalizedBase) {
  1099. return '';
  1100. }
  1101. if (normalizedFull.startsWith(normalizedBase) && normalizedBase !== '/') {
  1102. return normalizedFull.substring(normalizedBase.length).replace(/^\/+/, '');
  1103. }
  1104. return normalizedFull.replace(/^\/+/, '');
  1105. }
  1106. private normalizeSmbRelativePath(path: string, shareName?: string): string {
  1107. if (!path || path.trim().length === 0) {
  1108. return '/';
  1109. }
  1110. let normalized = path.replace(/\/+/g, '/');
  1111. if (!normalized.startsWith('/')) {
  1112. normalized = `/${normalized}`;
  1113. }
  1114. if (normalized.length > 1 && normalized.endsWith('/')) {
  1115. normalized = normalized.slice(0, -1);
  1116. }
  1117. if (shareName) {
  1118. const cleanedShare = shareName.replace(/^\/+|\/+$/g, '');
  1119. if (cleanedShare.length > 0) {
  1120. const prefix = `/${cleanedShare}`;
  1121. if (normalized === prefix) {
  1122. normalized = '/';
  1123. } else if (normalized.startsWith(`${prefix}/`)) {
  1124. normalized = normalized.substring(prefix.length);
  1125. if (!normalized.startsWith('/')) {
  1126. normalized = `/${normalized}`;
  1127. }
  1128. }
  1129. }
  1130. }
  1131. return normalized || '/';
  1132. }
  1133. // 判断是否为音频文件
  1134. private isAudioFile(fileName: string): boolean {
  1135. const ext = this.getFileExtension(fileName).toLowerCase();
  1136. for (let i = 0; i < this.audioExtensions.length; i++) {
  1137. if (ext === this.audioExtensions[i]) {
  1138. return true;
  1139. }
  1140. }
  1141. return false;
  1142. }
  1143. // 获取文件扩展名
  1144. private getFileExtension(fileName: string): string {
  1145. const lastDotIndex = fileName.lastIndexOf('.');
  1146. if (lastDotIndex === -1) {
  1147. return '';
  1148. }
  1149. return fileName.substring(lastDotIndex);
  1150. }
  1151. // 获取不带扩展名的文件名
  1152. private getFileNameWithoutExtension(fileName: string): string {
  1153. const lastDotIndex = fileName.lastIndexOf('.');
  1154. if (lastDotIndex === -1) {
  1155. return fileName;
  1156. }
  1157. return fileName.substring(0, lastDotIndex);
  1158. }
  1159. // 进入文件夹
  1160. public async enterFolder(folder: FileInfo): Promise<void> {
  1161. if (!folder.isDirectory) {
  1162. Logger.error(TAG, '不是文件夹,无法进入');
  1163. return;
  1164. }
  1165. Logger.info(TAG, '准备进入文件夹:', folder.fileName);
  1166. Logger.info(TAG, '当前路径:', this.currentPath);
  1167. Logger.info(TAG, '目标路径:', folder.href);
  1168. // 保存当前路径到历史记录
  1169. this.registerPathLabel(folder.href, folder.fileName);
  1170. this.pathHistory.push(this.currentPath);
  1171. Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
  1172. // 加载文件夹内容
  1173. await this.loadFilesInfoFromAccount(this.currentAccount, folder.href);
  1174. }
  1175. public async enterFolderFromPath(path: string): Promise<void> {
  1176. const normalizedTarget = this.normalizeFullPath(path);
  1177. Logger.info(TAG, '当前路径:', this.currentPath);
  1178. Logger.info(TAG, '目标路径:', normalizedTarget);
  1179. this.rebuildPathHistoryForTarget(normalizedTarget);
  1180. Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
  1181. await this.loadFilesInfoFromAccount(this.currentAccount, normalizedTarget);
  1182. }
  1183. private rebuildPathHistoryForTarget(targetPath: string): void {
  1184. if (!targetPath) {
  1185. this.pathHistory = [];
  1186. return;
  1187. }
  1188. const normalizedTarget = this.normalizeFullPath(targetPath);
  1189. const basePath = this.getBasePath();
  1190. if (normalizedTarget === basePath) {
  1191. this.pathHistory = [];
  1192. return;
  1193. }
  1194. const relativePath = this.getRelativePath(normalizedTarget, basePath);
  1195. if (!relativePath) {
  1196. this.pathHistory = [];
  1197. return;
  1198. }
  1199. const parts = relativePath.split('/').filter(part => part.length > 0);
  1200. const rebuiltHistory: string[] = [];
  1201. // 把账户根路径作为栈底,以便可以返回
  1202. rebuiltHistory.push(basePath);
  1203. let cumulative = basePath;
  1204. for (let i = 0; i < parts.length - 1; i++) {
  1205. const segment = parts[i];
  1206. cumulative = this.normalizeFullPath(`${cumulative === '/' ? '' : cumulative}/${segment}`);
  1207. rebuiltHistory.push(cumulative);
  1208. }
  1209. this.pathHistory = rebuiltHistory;
  1210. }
  1211. // 返回上级目录
  1212. public async goBack(): Promise<void> {
  1213. if (this.pathHistory.length === 0) {
  1214. Logger.info(TAG, '已经在根目录,无法返回');
  1215. return;
  1216. }
  1217. // 从历史记录中取出上一级路径
  1218. const previousPath = this.pathHistory.pop();
  1219. if (previousPath !== undefined) {
  1220. await this.loadFilesInfoFromAccount(this.currentAccount, previousPath);
  1221. }
  1222. }
  1223. // 获取面包屑路径数组
  1224. public getBreadcrumbs(): BreadcrumbItem[] {
  1225. const basePath = this.getBasePath();
  1226. const breadcrumbs: BreadcrumbItem[] = [{
  1227. label: '根目录',
  1228. path: basePath
  1229. }];
  1230. if (!this.currentPath || this.normalizeFullPath(this.currentPath) === basePath) {
  1231. return breadcrumbs;
  1232. }
  1233. const relativePath = this.getRelativePath(this.currentPath, basePath);
  1234. if (!relativePath) {
  1235. return breadcrumbs;
  1236. }
  1237. const parts = relativePath.split('/').filter(part => part.length > 0);
  1238. let cumulative = basePath;
  1239. for (let i = 0; i < parts.length; i++) {
  1240. const segment = parts[i];
  1241. const targetPath = this.normalizeFullPath(`${cumulative === '/' ? '' : cumulative}/${segment}`);
  1242. const label = this.pathDisplayNames.get(targetPath) ?? segment;
  1243. breadcrumbs.push({
  1244. label: this.sanitizeLabel(label),
  1245. path: targetPath
  1246. });
  1247. cumulative = targetPath;
  1248. }
  1249. return breadcrumbs;
  1250. }
  1251. // 是否可以返回上级
  1252. public canGoBack(): boolean {
  1253. if(this.currentPath==='/')
  1254. return false
  1255. return this.pathHistory.length > 0;
  1256. }
  1257. // ==================== Preferences操作 ====================
  1258. // 加载历史数据(从Preferences迁移)
  1259. public async loadInfo(): Promise<void> {
  1260. try {
  1261. // 这里可以添加从Preferences加载历史配置的逻辑
  1262. Logger.info(TAG, '从Preferences加载配置');
  1263. } catch (error) {
  1264. Logger.error(TAG, '从Preferences加载配置失败:', error.toString());
  1265. }
  1266. }
  1267. // 保存配置到Preferences
  1268. public async saveInfo(): Promise<void> {
  1269. try {
  1270. // 这里可以添加保存配置到Preferences的逻辑
  1271. Logger.info(TAG, '保存配置到Preferences');
  1272. } catch (error) {
  1273. Logger.error(TAG, '保存配置到Preferences失败:', error.toString());
  1274. }
  1275. }
  1276. // ==================== 下载队列管理 ====================
  1277. // 添加到下载队列
  1278. public addToDownloadQueue(song: VideoItem, account: WebDavAccount): void {
  1279. const task: TransferTask = { song, account };
  1280. this.downloadQueue.push(task);
  1281. Logger.info(TAG, '添加到下载队列:', song.name);
  1282. this.notifyObservers(RemoteDriveManagerStates.DownloadQueueChanged);
  1283. }
  1284. // 从下载队列移除
  1285. public removeFromDownloadQueue(index: number): void {
  1286. if (index >= 0 && index < this.downloadQueue.length) {
  1287. const task = this.downloadQueue[index];
  1288. this.downloadQueue.splice(index, 1);
  1289. Logger.info(TAG, '从下载队列移除:', task.song.name);
  1290. this.notifyObservers(RemoteDriveManagerStates.DownloadQueueChanged);
  1291. }
  1292. }
  1293. // 清空下载队列
  1294. public clearDownloadQueue(): void {
  1295. this.downloadQueue = [];
  1296. Logger.info(TAG, '清空下载队列');
  1297. this.notifyObservers(RemoteDriveManagerStates.DownloadQueueChanged);
  1298. }
  1299. // ==================== 上传队列管理 ====================
  1300. // 添加到上传队列
  1301. public addToUploadQueue(song: VideoItem, account: WebDavAccount): void {
  1302. const task: TransferTask = { song, account };
  1303. this.uploadQueue.push(task);
  1304. Logger.info(TAG, '添加到上传队列:', song.name);
  1305. this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
  1306. }
  1307. // 从上传队列移除
  1308. public removeFromUploadQueue(index: number): void {
  1309. if (index >= 0 && index < this.uploadQueue.length) {
  1310. const task = this.uploadQueue[index];
  1311. this.uploadQueue.splice(index, 1);
  1312. Logger.info(TAG, '从上传队列移除:', task.song.name);
  1313. this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
  1314. }
  1315. }
  1316. // 清空上传队列
  1317. public clearUploadQueue(): void {
  1318. this.uploadQueue = [];
  1319. Logger.info(TAG, '清空上传队列');
  1320. this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
  1321. }
  1322. // ==================== 安全认证方法 ====================
  1323. // 获取WebDAV认证信息(用于播放器)
  1324. public async getWebDavAuthHeaders(accountId: string): Promise<WebDavAuthInfo | null> {
  1325. const account = await this.getWebDavAccountById(accountId);
  1326. if (!account || !account.account || !account.password) {
  1327. Logger.error(TAG, '无效的WebDAV账户或缺少认证信息');
  1328. return null;
  1329. }
  1330. // 构建基础URL
  1331. const protocol: string = account.enableHttps ? 'https' : 'http';
  1332. const host: string = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
  1333. const port: number = account.port;
  1334. // 构建认证头
  1335. const credentials = buffer
  1336. .from(`${account.account}:${account.password}`)
  1337. .toString("base64");
  1338. const authInfo: WebDavAuthInfo = {
  1339. headers: {
  1340. 'Authorization': `Basic ${credentials}`,
  1341. 'User-Agent': 'TTMusic/1.0'
  1342. },
  1343. url: `${protocol}://${host}:${port}`,
  1344. accountId: accountId
  1345. };
  1346. return authInfo;
  1347. }
  1348. }
  1349. /**
  1350. * 构建HTTP请求头,特别处理WebDAV认证
  1351. * @param currentSong - 当前播放的歌曲信息
  1352. * @param videoUrl - 当前歌曲的URL
  1353. * @param webDavAuthItem - 当前WebDAV认证信息(实例变量)
  1354. * @returns Promise<Map<string, string>> HTTP请求头
  1355. */
  1356. export async function buildHttpHeadersWithWebDav(
  1357. currentSong: VideoItem | undefined,
  1358. videoUrl: string,
  1359. webDavAuthItem: WebDavAuthItem
  1360. ): Promise<Map<string, string>> {
  1361. const headers = new Map<string, string>();
  1362. let isWebDavSong = false;
  1363. if (currentSong) {
  1364. Logger.info(`heanup 当前歌曲信息 - name: ${currentSong.name}, type: ${currentSong.type}, filePath: ${currentSong.filePath}`);
  1365. // 检查是否为WebDAV歌曲
  1366. if (currentSong.type === CommonConstants.TYPE_WEBDAV) {
  1367. isWebDavSong = true;
  1368. // 优先从实例变量获取认证信息
  1369. if (webDavAuthItem) {
  1370. Logger.info(`heanup 从实例变量获取到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
  1371. } else {
  1372. // 回退到全局上下文
  1373. try {
  1374. const globalContext = GlobalContext.getContext();
  1375. webDavAuthItem = globalContext.getObject('webDavAuthInfo') as WebDavAuthItem;
  1376. if (webDavAuthItem) {
  1377. Logger.info(`heanup 从全局上下文获取到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
  1378. } else {
  1379. Logger.warn(`heanup 全局上下文中没有WebDAV认证信息`);
  1380. }
  1381. } catch (error) {
  1382. Logger.error(`heanup 获取全局上下文失败:`, error.toString());
  1383. }
  1384. }
  1385. // 如果识别为WebDAV但没有认证信息,记录警告
  1386. if (!webDavAuthItem) {
  1387. Logger.warn(`heanup 识别为WebDAV播放但缺少认证信息,可能需要手动配置WebDAV账户`);
  1388. }
  1389. }
  1390. if (isWebDavSong && webDavAuthItem) {
  1391. Logger.info(`heanup 检测到WebDAV播放,添加安全认证头`);
  1392. Logger.info(`heanup 歌曲URL: ${videoUrl}`);
  1393. try {
  1394. if (webDavAuthItem && webDavAuthItem.accountId) {
  1395. Logger.info(`heanup 找到WebDAV认证信息,账户ID: ${webDavAuthItem.accountId}`);
  1396. // 使用RemoteDriveManager获取认证头
  1397. const webdavManager = RemoteDriveManager.getInstance();
  1398. const authHeaders = await webdavManager.getWebDavAuthHeaders(webDavAuthItem.accountId.toString());
  1399. if (authHeaders) {
  1400. // 添加Basic认证头(使用规范字段名)
  1401. headers.set("Authorization", authHeaders.headers.Authorization);
  1402. } else {
  1403. Logger.error(`heanup 无法获取WebDAV认证头`);
  1404. }
  1405. } else {
  1406. Logger.error(`heanup WebDAV认证信息无效或缺少accountId`);
  1407. }
  1408. } catch (error) {
  1409. Logger.error(`heanup 获取WebDAV认证信息失败:`, error.toString());
  1410. }
  1411. // 添加标准的WebDAV请求头
  1412. headers.set("User-Agent", "TTMusic-WebDAV/1.0");
  1413. headers.set("Accept", "*/*");
  1414. // 请求首段数据,触发服务器返回 206(部分内容)以适配流式播放
  1415. headers.set("Range", "bytes=0-");
  1416. Logger.info(`heanup WebDAV安全认证头设置完成`);
  1417. }
  1418. }
  1419. // 输出所有设置的头部信息用于调试
  1420. console.log(`heanup 设置的HTTP头部信息:`);
  1421. const headerIterator = headers.entries();
  1422. let headerEntry = headerIterator.next();
  1423. while (!headerEntry.done) {
  1424. const key = headerEntry.value[0];
  1425. const value = headerEntry.value[1];
  1426. console.log(`heanup ${key}: ${value}`);
  1427. headerEntry = headerIterator.next();
  1428. }
  1429. return headers;
  1430. }
  1431. /**
  1432. * WebDAV认证信息(LocalMusic专用)
  1433. */
  1434. export interface WebDavAuthItem {
  1435. accountId: number;
  1436. host: string;
  1437. port: number;
  1438. account: string;
  1439. password: string;
  1440. enableHttps: boolean;
  1441. }