| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812 |
- // RemoteDriveManager - WebDAV管理器(简化版)
- import { VideoItem } from '../../viewmodel/VideoItem';
- import { common } from '@kit.AbilityKit';
- import { FileInfo } from '../../viewmodel/FileInfo';
- import { connection } from '@kit.NetworkKit';
- 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, PreferencesUtil } from '@pura/harmony-utils';
- import { MusicInfo, parseMusicFileName, Utility } from './Utility';
- import { RemoteDriveType } from '../enums/RemoteDriveType';
- import { createSmbDirectory, deleteSmbEntry, listSmbDirectory, SmbDirectoryEntry } from '../network/SmbBridge';
- import { NavidromeApi, NavidromeAlbumDetail, NavidromeArtist, NavidromeArtistDetail, NavidromeSong } from '../network/NavidromeApi';
- import { WebDavUrlUtil } from './WebDavUrlUtil';
- import MediaTable from './MediaTable';
- import { FtpClient, FileInfo as FtpEntryInfo, StringEncoding, AccessOptions } from '@liuzhosoft/ftp4h';
- import { BaiduConstants } from '../constants/BaiduConstants';
- import { appendAccessTokenToDlink, BaiduListEntry, BaiduFileMeta, buildAudioStreamingUrl, calculateMD5, createFolder, deleteFiles as deleteBaiduFiles, ensureAudioStreamReady, fetchFileMetas as fetchBaiduFileMetas, getUploadDomain, listDirectory as listBaiduDirectory, precreateFile, refreshAccessToken as refreshBaiduAccessToken, uploadFilePart } from '../network/BaiduPanClient';
- import { ServerLogUtil } from './ServerLogUtil';
- import { taskpool } from '@kit.ArkTS';
- import { fileIo } from '@kit.CoreFileKit';
- import { http } from '@kit.NetworkKit';
- import { JSON } from '@kit.ArkTS';
- import { simpleCalculateMD5, simplePrecreateFile, simpleUploadFilePart, simpleCreateFile, TaskPrecreateRequest, TaskPrecreateResponse, TaskUploadPartResponse, TaskCreateFileRequest, TaskCreateFileResponse, convertToBaiduPath, simpleLocateUploadServer } from './TaskPoolHelper';
- import { JellyfinApi, JellyfinAlbum, JellyfinArtist, JellyfinSong } from '../network/JellyfinApi';
- import { EmbyApi, EmbyAlbum, EmbyArtist, EmbySong } from '../network/EmbyApi';
- const TAG = 'heanup RemoteDriveManager';
- // 百度网盘上传TaskPool任务接口(使用前缀避免名称冲突)
- interface TaskBaiduUploadParams {
- filePath: string;
- accessToken: string;
- remotePath: string;
- songName: string;
- songId: string;
- }
- interface TaskBaiduUploadResult {
- success: boolean;
- error?: string;
- filePath?: string;
- remotePath?: string;
- songId?: string;
- uploadId?: string;
- progress?: number;
- currentPart?: number;
- totalParts?: number;
- }
- // TaskPool进度消息接口
- interface BaiduUploadProgressMessage {
- type: 'progress';
- progress: number;
- currentPart?: number;
- totalParts?: number;
- stage: 'md5' | 'upload' | 'create';
- uploadedSize?: number;
- totalSize?: number;
- }
- /**
- * 百度网盘文件上传TaskPool任务
- * 注意:TaskPool任务函数不能访问外部变量,必须使用纯函数
- */
- @Concurrent
- async function executeBaiduUpload(params: TaskBaiduUploadParams): Promise<TaskBaiduUploadResult> {
- const filePath = params.filePath;
- const accessToken = params.accessToken;
- const remotePath = params.remotePath;
- const songName = params.songName;
- const songId = params.songId;
- try {
- // 1. 读取文件数据
- const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
- const stat = await fileIo.stat(filePath);
- const fileSize = stat.size;
- const fileData = new ArrayBuffer(fileSize);
- await fileIo.read(file.fd, fileData);
- fileIo.closeSync(file);
- if (fileSize === 0) {
- throw new Error('文件大小为0');
- }
- // 2. 计算分片信息
- const PART_SIZE = 4 * 1024 * 1024; // 4MB分片大小
- const totalParts = Math.ceil(fileSize / PART_SIZE);
- // 3. 计算分片MD5列表(占总进度的10%)
- const blockMd5List: string[] = [];
- for (let i = 0; i < totalParts; i++) {
- const start = i * PART_SIZE;
- const end = Math.min(start + PART_SIZE, fileSize);
- const partData = fileData.slice(start, end);
- const md5 = await simpleCalculateMD5(partData);
- blockMd5List.push(md5);
- // 发送MD5计算进度
- const md5Progress = ((i + 1) / totalParts) * 10;
- const progressMessage: BaiduUploadProgressMessage = {
- type: 'progress',
- progress: md5Progress,
- currentPart: i + 1,
- totalParts: totalParts,
- stage: 'md5',
- uploadedSize: Math.floor(fileSize * md5Progress / 100),
- totalSize: fileSize
- };
- taskpool.Task.sendData(progressMessage);
- }
- // 4. 预上传(第一阶段)
- const precreateRequest: TaskPrecreateRequest = {
- path: remotePath,
- size: fileSize,
- isdir: 0,
- block_list: JSON.stringify(blockMd5List),
- autoinit: 1,
- rtype: 1,
- local_ctime: Math.floor(Date.now() / 1000).toString(),
- local_mtime: Math.floor(Date.now() / 1000).toString(),
- dupicate_check: 0 // 不检查重复文件
- };
- const precreateResponse = await simplePrecreateFile(accessToken, precreateRequest);
- if (!precreateResponse.uploadid) {
- throw new Error('预上传失败:未获取到uploadid');
- }
- console.info(`heanup 预上传成功,uploadid: ${JSON.stringify(precreateResponse)}`);
- const uploadServer = await simpleLocateUploadServer(accessToken, remotePath, precreateResponse.uploadid!);
- // 5. 分片上传(占总进度的80%)
- const partsToUpload = precreateResponse.block_list || [];
- Logger.info(`heanup 预上传返回block_list: ${JSON.stringify(partsToUpload)}, errno: ${precreateResponse.errno}`);
- // 如果block_list为空,说明所有分片都已存在,直接跳过分片上传
- Logger.info( `heanup 需要上传的分片列表: ${JSON.stringify(partsToUpload)}, 总计${partsToUpload.length}个分片`);
- const baseProgress = 10; // MD5计算已完成
- const uploadProgressRange = 80; // 上传进度范围
- const uploadedParts = new Set<number>();
- for (let i = 0; i < partsToUpload.length; i++) {
- const partSeq = partsToUpload[i];
- const start = partSeq * PART_SIZE;
- const end = Math.min(start + PART_SIZE, fileSize);
- const partData = fileData.slice(start, end);
- Logger.info( `heanup 开始上传分片${partSeq}, 大小: ${partData.byteLength}字节`);
- await simpleUploadFilePart(uploadServer, accessToken, remotePath, precreateResponse.uploadid!, partSeq, partData);
- Logger.info(`heanup 分片${partSeq}上传完成`);
- // 记录已上传的分片并发送进度
- uploadedParts.add(partSeq);
- const uploadProgress = (uploadedParts.size / partsToUpload.length) * uploadProgressRange;
- const totalProgress = baseProgress + uploadProgress;
- const progressMessage: BaiduUploadProgressMessage = {
- type: 'progress',
- progress: totalProgress,
- currentPart: uploadedParts.size,
- totalParts: partsToUpload.length,
- stage: 'upload',
- uploadedSize: Math.floor(fileSize * totalProgress / 100),
- totalSize: fileSize
- };
- taskpool.Task.sendData(progressMessage);
- }
- // 6. 创建文件(第三阶段,占总进度的10%)
- const createFileProgressMessage: BaiduUploadProgressMessage = {
- type: 'progress',
- progress: 90,
- stage: 'create',
- uploadedSize: Math.floor(fileSize * 0.9),
- totalSize: fileSize
- };
- taskpool.Task.sendData(createFileProgressMessage);
- const createFileRequest: TaskCreateFileRequest = {
- path: remotePath,
- size: fileSize,
- uploadid: precreateResponse.uploadid!,
- block_list: JSON.stringify(blockMd5List)
- };
- const createFileResponse = await simpleCreateFile(accessToken, createFileRequest);
- if (!createFileResponse.path) {
- throw new Error('创建文件失败:未获取到文件路径');
- }
- // 发送完成进度
- const finalProgressMessage: BaiduUploadProgressMessage = {
- type: 'progress',
- progress: 100,
- stage: 'create',
- uploadedSize: fileSize,
- totalSize: fileSize
- };
- taskpool.Task.sendData(finalProgressMessage);
- return {
- success: true,
- filePath: filePath,
- remotePath: createFileResponse.path,
- songId: songId,
- uploadId: precreateResponse.uploadid,
- progress: 100
- };
- } catch (error) {
- const err = error as Error;
- return {
- success: false,
- error: err.message,
- filePath: filePath,
- remotePath: remotePath,
- songId: songId,
- progress: 0
- };
- }
- }
- export interface BreadcrumbItem {
- label: string;
- path: string;
- }
- interface BaiduDlinkCacheEntry {
- url: string;
- token: string;
- expireAt: number;
- }
- // WebDAV认证信息接口
- export interface WebDavAuthInfo {
- headers: Record<string, string>;
- url: string;
- accountId?: string; // 添加账号ID字段,用于标识认证信息对应的账号
- }
- // 流媒体认证信息接口
- export interface StreamAuthInfo {
- url: string;
- headers: Record<string, string>;
- }
- // 百度网盘文件信息接口
- export interface BaiduFileInfo {
- size: number;
- dlink: string;
- streamUrl: string;
- }
- export interface TransferTask {
- song: VideoItem;
- account: WebDavAccount;
- retryCount?: number; // 已重试次数
- customUploadPath?: string; // 自定义上传路径(可选)
- }
- @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[] = []; // 当前目录的所有文件(包括文件夹)
- private static readonly BAIDU_DLINK_CACHE_TTL: number = 10 * 60 * 1000; // 10分钟
- private baiduDlinkCache: Map<string, BaiduDlinkCacheEntry> = new Map();
- // 路径导航
- 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[] = [];
- public isProcessingUploadQueue: boolean = false;
- public currentUploadTask: TransferTask | null = null;
- public isPauseUpload: boolean = true;
- public uploadReceivedSize: number = 0;
- public uploadTotalSize: number = 0;
-
- // 性能优化相关
- // 限制队列大小避免内存溢出:每个任务包含VideoItem对象,200个任务约占用几MB内存
- private readonly MAX_UPLOAD_QUEUE_SIZE: number = 200; // 最大上传队列大小
- // 进度更新节流:避免频繁UI刷新导致性能下降,每500ms最多更新一次
- private lastProgressNotifyTime: number = 0; // 上次进度通知时间
- private readonly PROGRESS_THROTTLE_MS: number = 500; // 进度更新节流时间(毫秒)
- private navidromeApi: NavidromeApi = new NavidromeApi();
- private jellyfinApi: JellyfinApi = new JellyfinApi();
- private embyApi: EmbyApi = new EmbyApi();
- private pathDisplayNames: Map<string, string> = new Map();
- private lastAccountId: number | null = null;
- 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');
- const jellyfinBaseIndex = resultSet.getColumnIndex('jellyfinBasePath');
- const embyBaseIndex = resultSet.getColumnIndex('embyBasePath');
- const ftpEncodingIndex = resultSet.getColumnIndex('ftpEncoding');
- const baiduAccessIndex = resultSet.getColumnIndex('baiduAccessToken');
- const baiduRefreshIndex = resultSet.getColumnIndex('baiduRefreshToken');
- const baiduExpireIndex = resultSet.getColumnIndex('baiduTokenExpiresAt');
- 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';
- }
- if (jellyfinBaseIndex >= 0) {
- const stored = resultSet.getString(jellyfinBaseIndex);
- account.jellyfinBasePath = stored ?? '';
- } else {
- account.jellyfinBasePath = '';
- }
- if (embyBaseIndex >= 0) {
- const stored = resultSet.getString(embyBaseIndex);
- account.embyBasePath = stored ?? '';
- } else {
- account.embyBasePath = '';
- }
- if (ftpEncodingIndex >= 0) {
- const encodingValue = resultSet.getString(ftpEncodingIndex);
- account.ftpEncoding = encodingValue && encodingValue.length > 0 ? encodingValue : 'utf-8';
- } else {
- account.ftpEncoding = 'utf-8';
- }
- if (baiduAccessIndex >= 0) {
- account.baiduAccessToken = resultSet.getString(baiduAccessIndex) ?? '';
- }
- if (baiduRefreshIndex >= 0) {
- account.baiduRefreshToken = resultSet.getString(baiduRefreshIndex) ?? '';
- }
- if (baiduExpireIndex >= 0) {
- account.baiduTokenExpiresAt = resultSet.getLong(baiduExpireIndex);
- }
- 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账户`);
- }
- }
- if (currentSong.type === CommonConstants.TYPE_JELLYFIN) {
- if (currentSong.webdav_account_id) {
- try {
- const account = await this.getWebDavAccountById(currentSong.webdav_account_id);
- if (account) {
- const authHeaders = await this.jellyfinApi.getAuthHeaders(account);
- authHeaders.forEach((value, key) => {
- headers.set(key, value);
- });
- Logger.info(TAG, `Jellyfin 认证头已构建`);
- } else {
- Logger.warn(TAG, `Jellyfin 账号不可用: ${currentSong.webdav_account_id}`);
- }
- } catch (error) {
- Logger.error(TAG, `Jellyfin 认证头构建失败: ${(error as Error).message}`);
- }
- } else {
- Logger.warn(TAG, 'Jellyfin歌曲缺少webdav_account_id,无法构建认证头');
- }
- }
- if (currentSong.type === CommonConstants.TYPE_EMBY) {
- if (currentSong.webdav_account_id) {
- try {
- const account = await this.getWebDavAccountById(currentSong.webdav_account_id);
- if (account) {
- const authHeaders = await this.embyApi.getAuthHeaders(account);
- authHeaders.forEach((value, key) => {
- headers.set(key, value);
- });
- Logger.info(TAG, `Emby 认证头已构建`);
- } else {
- Logger.warn(TAG, `Emby 账号不可用: ${currentSong.webdav_account_id}`);
- }
- } catch (error) {
- Logger.error(TAG, `Emby 认证头构建失败: ${(error as Error).message}`);
- }
- } else {
- Logger.warn(TAG, 'Emby歌曲缺少webdav_account_id,无法构建认证头');
- }
- }
- if (currentSong.type === CommonConstants.TYPE_BAIDU) {
- headers.set('User-Agent', BaiduConstants.STREAMING_USER_AGENT);
- headers.set('Accept', '*/*');
- headers.set('Connection', 'Keep-Alive');
- headers.set('Referer', 'https://pan.baidu.com/disk/home');
- headers.set('Accept-Encoding', 'identity');
- headers.set('Pragma', 'no-cache');
- headers.set('Cache-Control', 'no-cache');
- const headerSnapshot: Record<string, string> = {};
- headers.forEach((value, key) => {
- headerSnapshot[key] = value;
- });
- Logger.info(TAG, `构建百度请求头: ${JSON.stringify(headerSnapshot)}`);
- }
- // SMB类型的歌曲通过本地HTTP代理服务器播放,不需要特殊认证头
- if (currentSong.type === CommonConstants.TYPE_SMB) {
- Logger.info(TAG, `SMB歌曲使用HTTP代理流,无需特殊认证头`);
- }
- 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}, ${CommonConstants.TYPE_FTP}, ${CommonConstants.TYPE_BAIDU}, ${CommonConstants.TYPE_JELLYFIN}, ${CommonConstants.TYPE_EMBY})
- 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}, ${CommonConstants.TYPE_FTP}, ${CommonConstants.TYPE_BAIDU}, ${CommonConstants.TYPE_JELLYFIN})
- 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');
- const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
- const jellyfinBaseIndex = resultSet.getColumnIndex('jellyfinBasePath');
- const baiduAccessIndex = resultSet.getColumnIndex('baiduAccessToken');
- const baiduRefreshIndex = resultSet.getColumnIndex('baiduRefreshToken');
- const baiduExpireIndex = resultSet.getColumnIndex('baiduTokenExpiresAt');
- if (smbShareIndex >= 0) {
- account.smbShare = resultSet.getString(smbShareIndex) ?? '';
- }
- if (smbDomainIndex >= 0) {
- account.smbDomain = resultSet.getString(smbDomainIndex) ?? '';
- }
- const ftpEncodingIndex = resultSet.getColumnIndex('ftpEncoding');
- if (navBaseIndex >= 0) {
- const stored = resultSet.getString(navBaseIndex);
- account.navidromeBasePath = stored && stored.length > 0 ? stored : '/rest';
- } else {
- account.navidromeBasePath = '/rest';
- }
- if (ftpEncodingIndex >= 0) {
- const encodingValue = resultSet.getString(ftpEncodingIndex);
- account.ftpEncoding = encodingValue && encodingValue.length > 0 ? encodingValue : 'utf-8';
- } else {
- account.ftpEncoding = 'utf-8';
- }
- if (baiduAccessIndex >= 0) {
- account.baiduAccessToken = resultSet.getString(baiduAccessIndex) ?? '';
- }
- if (baiduRefreshIndex >= 0) {
- account.baiduRefreshToken = resultSet.getString(baiduRefreshIndex) ?? '';
- }
- if (baiduExpireIndex >= 0) {
- account.baiduTokenExpiresAt = resultSet.getLong(baiduExpireIndex);
- }
- 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,
- jellyfinBasePath TEXT,
- embyBasePath TEXT,
- ftpEncoding 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) {
- // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
- }
- 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) {
- // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
- }
- 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) {
- }
- 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) {
- }
- 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) {
- }
- try {
- Logger.info(TAG, '尝试添加jellyfinBasePath字段...');
- const addJellyfinBaseSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN jellyfinBasePath TEXT`;
- await this.dataBaseUtil.executeSql(addJellyfinBaseSql);
- Logger.info(TAG, 'jellyfinBasePath字段添加成功');
- } catch (error) {
- }
- try {
- Logger.info(TAG, '尝试添加embyBasePath字段...');
- const addEmbyBaseSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN embyBasePath TEXT`;
- await this.dataBaseUtil.executeSql(addEmbyBaseSql);
- Logger.info(TAG, 'embyBasePath字段添加成功');
- } catch (error) {
- }
- try {
- Logger.info(TAG, '尝试添加ftpEncoding字段...');
- const addFtpEncodingSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN ftpEncoding TEXT`;
- await this.dataBaseUtil.executeSql(addFtpEncodingSql);
- Logger.info(TAG, 'ftpEncoding字段添加成功');
- } catch (error) {
- }
- try {
- Logger.info(TAG, '尝试添加百度授权字段...');
- await this.dataBaseUtil.executeSql(`ALTER TABLE ${this.webDavTable} ADD COLUMN baiduAccessToken TEXT`);
- await this.dataBaseUtil.executeSql(`ALTER TABLE ${this.webDavTable} ADD COLUMN baiduRefreshToken TEXT`);
- await this.dataBaseUtil.executeSql(`ALTER TABLE ${this.webDavTable} ADD COLUMN baiduTokenExpiresAt INTEGER`);
- Logger.info(TAG, '百度授权字段添加成功');
- } catch (error) {
- }
- try {
- Logger.info(TAG, '尝试添加sortOrder字段...');
- const addSortOrderSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN sortOrder INTEGER DEFAULT 999999`;
- await this.dataBaseUtil.executeSql(addSortOrderSql);
- Logger.info(TAG, 'sortOrder字段添加成功');
- } catch (error) {
- }
- }
- // 从数据库查询所有账户
- public async queryWebDavAccountsFromDB(): Promise<void> {
- try {
- // 确保表结构是最新的
- await this.upgradeWebDavTable();
- const predicates = new relationalStore.RdbPredicates(this.webDavTable);
- // 按sortOrder升序排序,然后按id升序排序
- predicates.orderByAsc('sortOrder').orderByAsc('id');
- // 先查询基础字段(确保这些字段在旧版本中存在)
- const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
- 'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
- 'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain', 'navidromeBasePath', 'jellyfinBasePath', 'embyBasePath', 'ftpEncoding',
- 'baiduAccessToken', 'baiduRefreshToken', 'baiduTokenExpiresAt', 'sortOrder'];
- 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';
- }
- const jellyfinBaseIndex = resultSet.getColumnIndex('jellyfinBasePath');
- if (jellyfinBaseIndex >= 0) {
- const stored = resultSet.getString(jellyfinBaseIndex);
- account.jellyfinBasePath = stored ?? '';
- } else {
- account.jellyfinBasePath = '';
- }
- const embyBaseIndex = resultSet.getColumnIndex('embyBasePath');
- if (embyBaseIndex >= 0) {
- const stored = resultSet.getString(embyBaseIndex);
- account.embyBasePath = stored ?? '';
- } else {
- account.embyBasePath = '';
- }
- const ftpEncodingIndex = resultSet.getColumnIndex('ftpEncoding');
- if (ftpEncodingIndex >= 0) {
- const encodingValue = resultSet.getString(ftpEncodingIndex);
- account.ftpEncoding = encodingValue && encodingValue.length > 0 ? encodingValue : 'utf-8';
- } else {
- account.ftpEncoding = 'utf-8';
- }
- const baiduAccessIndex = resultSet.getColumnIndex('baiduAccessToken');
- if (baiduAccessIndex >= 0) {
- account.baiduAccessToken = resultSet.getString(baiduAccessIndex) ?? '';
- }
- const baiduRefreshIndex = resultSet.getColumnIndex('baiduRefreshToken');
- if (baiduRefreshIndex >= 0) {
- account.baiduRefreshToken = resultSet.getString(baiduRefreshIndex) ?? '';
- }
- const baiduExpireIndex = resultSet.getColumnIndex('baiduTokenExpiresAt');
- if (baiduExpireIndex >= 0) {
- account.baiduTokenExpiresAt = resultSet.getLong(baiduExpireIndex);
- }
- const sortOrderIndex = resultSet.getColumnIndex('sortOrder');
- if (sortOrderIndex >= 0) {
- account.sortOrder = resultSet.getLong(sortOrderIndex);
- // 如果数据库中的值为NULL,使用默认值
- if (resultSet.isColumnNull(sortOrderIndex)) {
- account.sortOrder = 999999;
- }
- } else {
- account.sortOrder = 999999; // 默认值
- }
- //Lo Logger.info(TAG, `加载账户 ${account.name}, sortOrder: ${account.sortOrder}, id: ${account.id}`);
- this.webDavAccounts.push(account);
- }
- resultSet.close();
- // 打印排序信息
- this.webDavAccounts.sort((a, b) => a.sortOrder - b.sortOrder);
- Logger.info(TAG, '从数据库加载了', this.webDavAccounts.length.toString(), '个WebDAV账户');
- if (this.webDavAccounts.length > 0) {
- Logger.info(TAG, `第一个账户: ${this.webDavAccounts[0].name}, sortOrder: ${this.webDavAccounts[0].sortOrder}`);
- }
- 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',
- jellyfinBasePath: string = '',
- embyBasePath: string = '',
- ftpEncoding: string = 'utf-8',
- baiduAccessToken: string = '',
- baiduRefreshToken: string = '',
- baiduTokenExpiresAt: number = 0,
- sortOrder: number = 999999
- ): 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,
- 'jellyfinBasePath': jellyfinBasePath,
- 'embyBasePath': embyBasePath,
- 'ftpEncoding': ftpEncoding,
- 'baiduAccessToken': baiduAccessToken,
- 'baiduRefreshToken': baiduRefreshToken,
- 'baiduTokenExpiresAt': baiduTokenExpiresAt,
- 'sortOrder': sortOrder
- };
- 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,
- 'jellyfinBasePath': account.jellyfinBasePath,
- 'embyBasePath': account.embyBasePath,
- 'ftpEncoding': account.ftpEncoding,
- 'baiduAccessToken': account.baiduAccessToken,
- 'baiduRefreshToken': account.baiduRefreshToken,
- 'baiduTokenExpiresAt': account.baiduTokenExpiresAt,
- 'sortOrder': account.sortOrder
- };
- 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 async pinAccount(account: WebDavAccount): Promise<void> {
- try {
- // 获取当前所有账户中最小的sortOrder值
- const allAccounts = this.getAllWebDavAccounts();
- if (allAccounts.length === 0) {
- return;
- }
- // 找到最小的sortOrder值
- let minSortOrder = allAccounts[0].sortOrder;
- for (let i = 1; i < allAccounts.length; i++) {
- if (allAccounts[i].sortOrder < minSortOrder) {
- minSortOrder = allAccounts[i].sortOrder;
- }
- }
- Logger.info(TAG, `当前最小sortOrder: ${minSortOrder}, 要置顶的账户sortOrder: ${account.sortOrder}, 账户名: ${account.name}`);
- // 如果账户已经是最小的,则不需要置顶
- if (account.sortOrder === minSortOrder && minSortOrder !== 999999) {
- Logger.info(TAG, '账户已经是第一个:', account.name);
- return;
- }
- // 将账户的sortOrder设置为比最小值更小的值
- account.sortOrder = minSortOrder - 1;
- Logger.info(TAG, `设置账户 ${account.name} 的sortOrder为: ${account.sortOrder}`);
- // 更新数据库
- await this.editAccount(account);
- Logger.info(TAG, '置顶WebDAV账户成功:', account.name);
- } catch (err) {
- const error = err as Error;
- Logger.error(TAG, '置顶WebDAV账户失败:', error.message);
- 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){
- console.info('heanup 2'+account.name+'type '+account.webType)
- }
- 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 if (account.webType === RemoteDriveType.Jellyfin) {
- await this.loadJellyfinFiles(account, normalizedFullPath);
- } else if (account.webType === RemoteDriveType.Emby) {
- await this.loadEmbyFiles(account, normalizedFullPath);
- } else if (account.webType === RemoteDriveType.Ftp) {
- await this.loadFtpFiles(account, normalizedFullPath);
- } else if (account.webType === RemoteDriveType.Baidu) {
- await this.loadBaiduFiles(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 || '/';
- }
- private getSongKey(song: VideoItem): string {
- if (!song) {
- return '';
- }
- if (song.filePath && song.filePath.length > 0) {
- return song.filePath;
- }
- if (song.name && song.name.length > 0) {
- return song.name;
- }
- return '';
- }
- private sanitizeRelativeUploadPath(path?: string): string {
- if (!path || path.length === 0) {
- return '';
- }
- let sanitized = path.replace(/\\/g, '/');
- sanitized = sanitized.replace(/^\/+/, '');
- sanitized = sanitized.replace(/\/+/g, '/');
- return sanitized;
- }
- private combineRemoteUploadPath(basePath: string, relativePath: string): string {
- const normalizedBase = this.normalizeFullPath(basePath);
- const cleanedRelative = this.sanitizeRelativeUploadPath(relativePath);
- if (!cleanedRelative || cleanedRelative.length === 0) {
- return normalizedBase;
- }
- if (normalizedBase === '/') {
- return this.normalizeFullPath(`/${cleanedRelative}`);
- }
- return this.normalizeFullPath(`${normalizedBase}/${cleanedRelative}`);
- }
- private getDirectoryFromRemotePath(remotePath: string): string {
- if (!remotePath || remotePath === '/') {
- return '/';
- }
- const normalized = this.normalizeFullPath(remotePath);
- const lastSlash = normalized.lastIndexOf('/');
- if (lastSlash <= 0) {
- return '/';
- }
- return normalized.substring(0, lastSlash);
- }
- private async ensureRemoteDirectories(account: WebDavAccount, directoryPath: string): Promise<void> {
- if (!directoryPath || directoryPath === '/') {
- return;
- }
- const normalizedDir = this.normalizeFullPath(directoryPath);
- if (normalizedDir === '/') {
- return;
- }
- const segments = normalizedDir.split('/').filter(segment => segment.length > 0);
- if (segments.length === 0) {
- return;
- }
- let currentPath = '';
- for (let i = 0; i < segments.length; i++) {
- currentPath = `${currentPath}/${segments[i]}`;
- const normalizedCurrent = this.normalizeFullPath(currentPath);
- await this.ensureSingleRemoteDirectory(account, normalizedCurrent);
- }
- }
- private async ensureSingleRemoteDirectory(account: WebDavAccount, directoryPath: string): Promise<void> {
- try {
- await this.rcpSocket.getFileList(
- account.host,
- account.localHost,
- account.isUseLocalHost,
- account.port,
- directoryPath,
- account.account,
- account.password,
- account.enableHttps
- );
- } catch (_error) {
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- try {
- await this.rcpSocket.createDirectory(
- host,
- account.port,
- account.account,
- account.password,
- directoryPath,
- account.enableHttps
- );
- Logger.info(TAG, `创建远程目录: ${directoryPath}`);
- } catch (createErr) {
- const err = createErr as Error;
- Logger.error(TAG, `创建远程目录失败 (${directoryPath}): ${err.message}`);
- throw err;
- }
- }
- }
- // 将 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 resolveFtpHost(account: WebDavAccount): string {
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- return host?.trim() ?? '';
- }
- private getFtpEncoding(account: WebDavAccount): StringEncoding {
- if (account.ftpEncoding && account.ftpEncoding.length > 0) {
- return account.ftpEncoding as StringEncoding;
- }
- return 'utf-8';
- }
- private buildFtpAccessOptions(account: WebDavAccount): AccessOptions {
- const host = this.resolveFtpHost(account);
- if (!host) {
- throw new Error('FTP账户缺少服务器地址');
- }
- return {
- host,
- port: account.port && account.port > 0 ? account.port : 21,
- user: account.account && account.account.length > 0 ? account.account : undefined,
- password: account.password && account.password.length > 0 ? account.password : undefined,
- encoding: this.getFtpEncoding(account)
- };
- }
- private async loadFtpFiles(account: WebDavAccount, fullPath: string): Promise<void> {
- const client = new FtpClient();
- const options = this.buildFtpAccessOptions(account);
- await client.access(options);
- try {
- const listPath = this.normalizeFullPath(fullPath);
- const entries = await client.list(listPath === '/' ? '/' : listPath);
- this.webDavFiles = [];
- this.webDavSongs = [];
- for (let i = 0; i < entries.length; i++) {
- const entry = entries[i];
- const info = this.ftpEntryToFileInfo(entry, listPath);
- this.webDavFiles.push(info);
- if (info.isDirectory) {
- this.registerPathLabel(info.href, info.fileName);
- } else if (this.isAudioFile(info.fileName)) {
- this.webDavSongs.push(this.buildFtpVideoItem(info, account));
- }
- }
- await this.enrichSongsWithDatabase(this.webDavSongs);
- Logger.info(TAG, `从FTP获取到 ${entries.length} 个文件/文件夹`);
- } finally {
- await client.close();
- }
- }
- private async loadBaiduFiles(account: WebDavAccount, fullPath: string): Promise<void> {
- const accessToken = await this.ensureBaiduAccessToken(account);
- const directory = fullPath && fullPath.length > 0 ? fullPath : '/';
- const entries = await listBaiduDirectory(accessToken, directory);
- this.webDavFiles = [];
- this.webDavSongs = [];
- for (let i = 0; i < entries.length; i++) {
- const entry = entries[i];
- const info = this.baiduEntryToFileInfo(entry);
- this.webDavFiles.push(info);
- if (info.isDirectory) {
- this.registerPathLabel(info.href, info.fileName);
- } else if (this.isAudioFile(info.fileName)) {
- this.webDavSongs.push(this.buildBaiduVideoItem(entry, account));
- }
- }
- await this.enrichSongsWithDatabase(this.webDavSongs);
- Logger.info(TAG, `从百度网盘获取到 ${entries.length} 个文件/文件夹`);
- this.scheduleBaiduPrefetch(account, accessToken, entries);
- Logger.info(TAG, `百度预取任务已调度, account=${account.id}, 文件数=${entries.length}`);
- }
- public async deleteRemoteSongs(account: WebDavAccount, songs: VideoItem[]): Promise<void> {
- if (!account) {
- throw new Error('请先选择账户');
- }
- if (!songs || songs.length === 0) {
- return;
- }
- const deletable = songs.filter(item => item && item.filePath && item.filePath.length > 0);
- if (deletable.length === 0) {
- return;
- }
- if (account.webType === RemoteDriveType.WebDav) {
- await this.deleteWebDavSongs(account, deletable);
- } else if (account.webType === RemoteDriveType.Smb) {
- await this.deleteSmbSongs(account, deletable);
- } else if (account.webType === RemoteDriveType.Ftp) {
- await this.deleteFtpSongs(account, deletable);
- } else if (account.webType === RemoteDriveType.Baidu) {
- await this.deleteBaiduSongs(account, deletable);
- } else {
- throw new Error('当前账户类型暂不支持删除');
- }
- }
- private resolveWebDavDeletePath(song: VideoItem): string {
- if (song.remote_rel_path && song.remote_rel_path.length > 0) {
- return this.normalizeRemoteHref(song.remote_rel_path);
- }
- if (song.filePath) {
- const relative = WebDavUrlUtil.extractRelativePath(song.filePath);
- if (relative) {
- return this.normalizeRemoteHref(relative);
- }
- }
- throw new Error(`无法确定 WebDAV 文件路径: ${song.name}`);
- }
- private async deleteWebDavSongs(account: WebDavAccount, songs: VideoItem[]): Promise<void> {
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- if (!host || host.length === 0) {
- throw new Error('WebDAV账户缺少服务器地址');
- }
- for (let i = 0; i < songs.length; i++) {
- const song = songs[i];
- const relativePath = this.resolveWebDavDeletePath(song);
- try {
- await this.rcpSocket.RcpSendDelete(
- host,
- account.port,
- account.account,
- account.password,
- relativePath,
- account.enableHttps
- );
- } catch (error) {
- const err = error as Error;
- throw new Error(`删除 ${song.name} 失败: ${err.message}`);
- }
- }
- }
- private resolveSmbSongPath(song: VideoItem, account: WebDavAccount): string {
- if (song.remote_rel_path && song.remote_rel_path.length > 0) {
- return this.normalizeSmbRelativePath(song.remote_rel_path, account.smbShare);
- }
- if (song.filePath) {
- const withoutScheme = song.filePath.replace(/^smb:\/\//i, '').replace(/^[^/]+/i, '');
- return this.normalizeSmbRelativePath(withoutScheme, account.smbShare);
- }
- throw new Error(`无法确定SMB路径: ${song.name}`);
- }
- private async deleteSmbSongs(account: WebDavAccount, songs: VideoItem[]): Promise<void> {
- if (!account.smbShare || account.smbShare.length === 0) {
- throw new Error('SMB账户缺少共享名称');
- }
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- if (!host || host.length === 0) {
- throw new Error('SMB账户缺少服务器地址');
- }
- for (let i = 0; i < songs.length; i++) {
- const song = songs[i];
- const relativePath = this.resolveSmbSongPath(song, account);
- if (!relativePath || relativePath.length === 0 || relativePath === '/') {
- throw new Error(`无法确定SMB文件路径: ${song.name}`);
- }
- try {
- await deleteSmbEntry({
- host,
- share: account.smbShare,
- username: account.account,
- password: account.password,
- domain: account.smbDomain,
- path: relativePath,
- isDirectory: false
- });
- } catch (error) {
- const err = error as Error;
- throw new Error(`删除 ${song.name} 失败: ${err.message}`);
- }
- }
- }
- private resolveFtpSongPath(song: VideoItem): string {
- const target = song.remote_rel_path && song.remote_rel_path.length > 0 ? song.remote_rel_path : song.filePath;
- if (!target || target.length === 0) {
- throw new Error(`无法确定FTP路径: ${song.name}`);
- }
- return this.normalizeFullPath(target);
- }
- private async deleteFtpSongs(account: WebDavAccount, songs: VideoItem[]): Promise<void> {
- const options = this.buildFtpAccessOptions(account);
- const client = new FtpClient();
- try {
- await client.access(options);
- for (let i = 0; i < songs.length; i++) {
- const song = songs[i];
- const remotePath = this.resolveFtpSongPath(song);
- const success = await client.remove(remotePath);
- if (!success) {
- throw new Error(`删除 ${song.name} 失败`);
- }
- }
- } finally {
- await client.close();
- }
- }
- private async deleteBaiduSongs(account: WebDavAccount, songs: VideoItem[]): Promise<void> {
- const accessToken = await this.ensureBaiduAccessToken(account);
- const paths: string[] = [];
- for (let i = 0; i < songs.length; i++) {
- const path = songs[i].remote_rel_path;
- if (!path || path.length === 0) {
- throw new Error(`无法确定百度网盘路径: ${songs[i].name}`);
- }
- paths.push(path);
- }
- const batchSize = 20;
- for (let i = 0; i < paths.length; i += batchSize) {
- const batch = paths.slice(i, i + batchSize);
- try {
- await deleteBaiduFiles(accessToken, batch);
- } catch (error) {
- const err = error as Error;
- throw new Error(`删除文件失败: ${err.message}`);
- }
- }
- }
- private scheduleBaiduPrefetch(account: WebDavAccount, accessToken: string, entries: BaiduListEntry[]): void {
- const fileEntries = entries.filter(entry => entry.isdir === 0 && this.isAudioFile(entry.server_filename));
- if (fileEntries.length === 0) {
- return;
- }
- const fsIds = fileEntries.map(entry => entry.fs_id.toString());
- const chunkSize = 50;
- const prefetch = async () => {
- Logger.info(TAG, `开始预取百度dlink,文件数: ${fsIds.length}`);
- Logger.info(TAG, `Baidu dlink prefetch start: account=${account.id} files=${fsIds.length}`);
- for (let i = 0; i < fsIds.length; i += chunkSize) {
- const chunk = fsIds.slice(i, i + chunkSize);
- try {
- const metas = await fetchBaiduFileMetas(accessToken, chunk);
- metas.forEach((meta: BaiduFileMeta) => {
- if (!meta.dlink) {
- return;
- }
- const rawLink = meta.dlink.startsWith('https://') ? meta.dlink : meta.dlink.replace('http://', 'https://');
- const finalUrl = appendAccessTokenToDlink(rawLink, accessToken);
- this.cacheBaiduDlink(account, meta.fs_id.toString(), accessToken, finalUrl);
- });
- Logger.info(TAG, `Baidu dlink prefetch chunk success account=${account.id} size=${chunk.length}`);
- } catch (error) {
- const err = error as Error;
- Logger.warn(TAG, `百度dlink预取失败: ${err.message}`);
- break;
- }
- }
- Logger.info(TAG, `Baidu dlink prefetch finished account=${account.id}`);
- };
- void prefetch();
- }
- 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);
- void ServerLogUtil.info(TAG, `浏览 Navidrome 路径: ${normalized} account=${ServerLogUtil.sanitizeAccount(account)}`);
- 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 = [];
- void ServerLogUtil.info(TAG, `获取艺术家列表,数量: ${artists.length}`);
- void ServerLogUtil.debug(TAG, `artists ids: ${artists.map(a => a.id).join(',')}`)
- 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 = [];
- void ServerLogUtil.info(TAG, `进入艺术家 ${detail.name},专辑数: ${detail.albums.length}`);
- void ServerLogUtil.debug(TAG, `artist albums: ${detail.albums.map(a => a.id).join(',')}`)
- 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));
- const albumCoverUrl = await this.navidromeApi.buildCoverArtUrl(account,
- detail.coverArt ?? (detail.id ? `al-${detail.id}` : undefined));
- this.webDavSongs = detail.songs.map(song => this.buildNavidromeVideoItem(song, account, detail, albumCoverUrl));
- await this.enrichSongsWithDatabase(this.webDavSongs);
- void ServerLogUtil.info(TAG, `进入专辑 ${detail.name},歌曲数: ${detail.songs.length}`);
- void ServerLogUtil.debug(TAG, `album songs ids: ${detail.songs.map(s => s.id).join(',')}`)
- return;
- }
- throw new Error(`不支持的Navidrome路径: ${fullPath}`);
- }
- private async loadJellyfinFiles(account: WebDavAccount, fullPath: string): Promise<void> {
- const normalized = this.normalizeFullPath(fullPath);
- this.registerPathLabel('/', '根目录');
- const segments = normalized.split('/').filter(part => part.length > 0);
- void ServerLogUtil.info(TAG, `浏览 Jellyfin 路径: ${normalized} account=${ServerLogUtil.sanitizeAccount(account)}`);
- if (normalized === '/' || segments.length === 0) {
- const songs = await this.jellyfinApi.getAllSongs(account);
- this.webDavFiles = [];
- this.webDavSongs = songs.map(song => this.buildJellyfinVideoItem(song, account, null, undefined));
- await this.enrichSongsWithDatabase(this.webDavSongs);
- void ServerLogUtil.info(TAG, `进入 Jellyfin 根目录,歌曲数: ${songs.length}`);
- return;
- }
- if (segments[0] === 'artist' && segments[1]) {
- const artistId = segments[1];
- const albums: JellyfinAlbum[] = await this.jellyfinApi.getArtistAlbums(account, artistId);
- const artistLabel = this.pathDisplayNames.get(`/artist/${artistId}`) ?? (albums[0]?.artist ?? '艺术家');
- if (artistLabel) {
- this.registerPathLabel(`/artist/${artistId}`, artistLabel);
- }
- this.webDavFiles = albums.map(album => {
- const info = this.createJellyfinDirectory(album.name, `/album/${album.id}`);
- this.registerPathLabel(info.href, album.name);
- return info;
- });
- this.webDavSongs = [];
- void ServerLogUtil.info(TAG, `进入 Jellyfin 艺术家 ${artistLabel},专辑数: ${albums.length}`);
- return;
- }
- if (segments[0] === 'album' && segments[1]) {
- const albumId = segments[1];
- const album = await this.jellyfinApi.getAlbum(account, albumId);
- const albumName = album?.name ?? this.pathDisplayNames.get(`/album/${albumId}`) ?? '专辑';
- this.registerPathLabel(`/album/${albumId}`, albumName);
- const songs = await this.jellyfinApi.getAlbumSongs(account, albumId);
- let coverUrl: string | undefined = undefined;
- try {
- coverUrl = await this.jellyfinApi.buildPrimaryImageUrl(account, album?.id ?? albumId);
- } catch (error) {
- Logger.warn(TAG, `Jellyfin 专辑封面获取失败: ${(error as Error).message}`);
- }
- this.webDavFiles = songs.map(song => this.createJellyfinSongFileInfo(song, albumId));
- this.webDavSongs = songs.map(song => this.buildJellyfinVideoItem(song, account, album, coverUrl));
- await this.enrichSongsWithDatabase(this.webDavSongs);
- void ServerLogUtil.info(TAG, `进入 Jellyfin 专辑 ${albumName},歌曲数: ${songs.length}`);
- return;
- }
- throw new Error(`不支持的Jellyfin路径: ${fullPath}`);
- }
- private async loadEmbyFiles(account: WebDavAccount, fullPath: string): Promise<void> {
- const normalized = this.normalizeFullPath(fullPath);
- this.registerPathLabel('/', '根目录');
- const segments = normalized.split('/').filter(part => part.length > 0);
- void ServerLogUtil.info(TAG, `浏览 Emby 路径: ${normalized} account=${ServerLogUtil.sanitizeAccount(account)}`);
- if (normalized === '/' || segments.length === 0) {
- const songs = await this.embyApi.getAllSongs(account);
- this.webDavFiles = [];
- this.webDavSongs = songs.map(song => this.buildEmbyVideoItem(song, account, null, undefined));
- await this.enrichSongsWithDatabase(this.webDavSongs);
- void ServerLogUtil.info(TAG, `进入 Emby 根目录,歌曲数: ${songs.length}`);
- return;
- }
- if (segments[0] === 'artist' && segments[1]) {
- const artistId = segments[1];
- const albums: EmbyAlbum[] = await this.embyApi.getArtistAlbums(account, artistId);
- const artistLabel = this.pathDisplayNames.get(`/artist/${artistId}`) ?? (albums[0]?.artist ?? '艺术家');
- if (artistLabel) {
- this.registerPathLabel(`/artist/${artistId}`, artistLabel);
- }
- this.webDavFiles = albums.map(album => {
- const info = this.createEmbyDirectory(album.name, `/album/${album.id}`);
- this.registerPathLabel(info.href, album.name);
- return info;
- });
- this.webDavSongs = [];
- void ServerLogUtil.info(TAG, `进入 Emby 艺术家 ${artistLabel},专辑数: ${albums.length}`);
- return;
- }
- if (segments[0] === 'album' && segments[1]) {
- const albumId = segments[1];
- const album = await this.embyApi.getAlbum(account, albumId);
- const albumName = album?.name ?? this.pathDisplayNames.get(`/album/${albumId}`) ?? '专辑';
- this.registerPathLabel(`/album/${albumId}`, albumName);
- const songs = await this.embyApi.getAlbumSongs(account, albumId);
- let coverUrl: string | undefined = undefined;
- try {
- coverUrl = await this.embyApi.buildPrimaryImageUrl(account, album?.id ?? albumId);
- } catch (error) {
- Logger.warn(TAG, `Emby 专辑封面获取失败: ${(error as Error).message}`);
- }
- this.webDavFiles = songs.map(song => this.createEmbySongFileInfo(song, albumId));
- this.webDavSongs = songs.map(song => this.buildEmbyVideoItem(song, account, album, coverUrl));
- await this.enrichSongsWithDatabase(this.webDavSongs);
- void ServerLogUtil.info(TAG, `进入 Emby 专辑 ${albumName},歌曲数: ${songs.length}`);
- return;
- }
- throw new Error(`不支持的Emby路径: ${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 ||
- item.type === CommonConstants.TYPE_FTP || item.type === CommonConstants.TYPE_BAIDU ||
- item.type === CommonConstants.TYPE_JELLYFIN) {
- 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.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);
- }
- if (account.webType === RemoteDriveType.Ftp) {
- return this.buildFtpVideoItem(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, 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, // 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,
- 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;
- }
- videoItem.fileName = fileInfo.fileName
- return videoItem;
- }
- private ftpEntryToFileInfo(entry: FtpEntryInfo, basePath: string): FileInfo {
- const normalizedBase = this.normalizeFullPath(basePath);
- const targetPath = this.normalizeFullPath(`${normalizedBase === '/' ? '' : normalizedBase}/${entry.name}`);
- const info = new FileInfo(normalizedBase, entry.name, entry.size ?? 0, entry.modifiedAt ? entry.modifiedAt.getTime() : Date.now());
- info.fileName = entry.name;
- info.href = targetPath;
- info.isDirectory = Boolean(entry.isDirectory);
- info.contentLength = entry.size ?? 0;
- info.time = entry.modifiedAt ? entry.modifiedAt.getTime() : Date.now();
- return info;
- }
- private buildFtpVideoItem(fileInfo: FileInfo, account: WebDavAccount): VideoItem {
- const host = this.resolveFtpHost(account);
- if (!host) {
- throw new Error('FTP账号缺少服务器地址');
- }
- const normalizedPath = this.normalizeFullPath(fileInfo.href || `/${fileInfo.fileName}`);
- const encodedPath = encodeURI(normalizedPath);
- const port = account.port && account.port > 0 ? `:${account.port}` : '';
- const ftpUrl = `ftp://${host}${port}${encodedPath}`;
- const videoItem = new VideoItem(
- this.getFileNameWithoutExtension(fileInfo.fileName),
- '',
- ftpUrl,
- CommonConstants.TYPE_FTP,
- fileInfo.contentLength,
- Utility.getFormatDateStr(fileInfo.time, 'yyyy-MM-dd HH:mm')
- );
- videoItem.album = '';
- videoItem.size = Utility.formatFSize(fileInfo.contentLength);
- const musicData: MusicInfo = parseMusicFileName(fileInfo.fileName);
- if (musicData.isValid) {
- videoItem.artist = musicData.artist;
- videoItem.name = musicData.title;
- } else {
- videoItem.artist = Constants.UNKNOWN_ARTIST;
- }
- if (account && account.id) {
- videoItem.webdav_account_id = account.id.toString();
- }
- videoItem.fileName = fileInfo.fileName
- videoItem.remote_rel_path = normalizedPath;
- return videoItem;
- }
- private baiduEntryToFileInfo(entry: BaiduListEntry): FileInfo {
- const timestamp = (entry.server_mtime ?? Date.now()) * 1000;
- const info = new FileInfo('', entry.server_filename, entry.size, timestamp);
- info.fileName = entry.server_filename;
- info.href = entry.path;
- info.isDirectory = entry.isdir === 1;
- info.contentLength = entry.size;
- info.time = timestamp;
- return info;
- }
- private buildBaiduVideoItem(entry: BaiduListEntry, account: WebDavAccount): VideoItem {
- const timestamp = (entry.server_mtime ?? Date.now()) * 1000;
- const videoItem = new VideoItem(
- this.getFileNameWithoutExtension(entry.server_filename),
- entry.fs_id.toString(),
- `baidupan://${account.id ?? 0}${entry.path}`,
- CommonConstants.TYPE_BAIDU,
- entry.size,
- Utility.getFormatDateStr(timestamp, 'yyyy-MM-dd HH:mm'),
- Utility.formatFSize(entry.size)
- );
- videoItem.size = Utility.formatFSize(entry.size);
- videoItem.album = '';
- videoItem.fileName = entry.server_filename
- videoItem.remote_rel_path = entry.path;
- videoItem.baiduFsId = entry.fs_id.toString();
- if (account && account.id) {
- videoItem.webdav_account_id = account.id.toString();
- }
- const musicData: MusicInfo = parseMusicFileName(entry.server_filename);
- if (musicData.isValid) {
- videoItem.artist = musicData.artist;
- videoItem.name = musicData.title;
- } else {
- videoItem.artist = Constants.UNKNOWN_ARTIST;
- }
- return videoItem;
- }
- private async ensureBaiduAccessToken(account: WebDavAccount): Promise<string> {
- if (account.baiduAccessToken) {
- if (!account.baiduTokenExpiresAt || (account.baiduTokenExpiresAt - Date.now()) > 60 * 1000) {
- return account.baiduAccessToken;
- }
- }
- if (!account.baiduRefreshToken) {
- throw new Error('百度网盘授权已过期,请在账户设置中重新登录百度网盘');
- }
- const token = await refreshBaiduAccessToken(account.baiduRefreshToken);
- account.baiduAccessToken = token.access_token;
- account.baiduRefreshToken = token.refresh_token ?? account.baiduRefreshToken;
- account.baiduTokenExpiresAt = token.expires_in ? Date.now() + token.expires_in * 1000 : 0;
- await this.persistBaiduTokens(account);
- return account.baiduAccessToken;
- }
- private async persistBaiduTokens(account: WebDavAccount): Promise<void> {
- if (!account.id) {
- return;
- }
- const predicates = new relationalStore.RdbPredicates(this.webDavTable);
- predicates.equalTo('id', account.id);
- const values: relationalStore.ValuesBucket = {
- baiduAccessToken: account.baiduAccessToken,
- baiduRefreshToken: account.baiduRefreshToken,
- baiduTokenExpiresAt: account.baiduTokenExpiresAt
- };
- await this.dataBaseUtil.updateData(this.webDavTable, values, predicates);
- }
- private getBaiduCacheKey(account: WebDavAccount, fsId: string): string {
- return `${account.id ?? '0'}_${fsId}`;
- }
- private cacheBaiduDlink(account: WebDavAccount, fsId: string, token: string, url: string): void {
- const key = this.getBaiduCacheKey(account, fsId);
- const expireAt = Date.now() + RemoteDriveManager.BAIDU_DLINK_CACHE_TTL;
- this.baiduDlinkCache.set(key, { url, token, expireAt });
- }
- private getCachedBaiduDlink(account: WebDavAccount, fsId: string, token: string): string | null {
- const key = this.getBaiduCacheKey(account, fsId);
- const cached = this.baiduDlinkCache.get(key);
- if (!cached) {
- return null;
- }
- if (cached.token !== token || cached.expireAt <= Date.now()) {
- this.baiduDlinkCache.delete(key);
- return null;
- }
- return cached.url;
- }
- public async getBaiduDownloadUrl(account: WebDavAccount, song: VideoItem): Promise<string> {
- const accessToken = await this.ensureBaiduAccessToken(account);
- const fsId = song.baiduFsId || song.id;
- if (!fsId) {
- throw new Error('缺少百度网盘文件fs_id');
- }
- const cached = this.getCachedBaiduDlink(account, fsId, accessToken);
- if (cached) {
- Logger.info(TAG, `Baidu dlink cache hit, fsId=${fsId}`);
- return cached;
- }
- const metas = await fetchBaiduFileMetas(accessToken, [fsId]);
- if (!metas || metas.length === 0 || !metas[0].dlink) {
- throw new Error('无法获取下载链接');
- }
- const rawLink = metas[0].dlink.startsWith('https://') ? metas[0].dlink : metas[0].dlink.replace('http://', 'https://');
- const urlWithToken = appendAccessTokenToDlink(rawLink, accessToken);
- Logger.info(TAG, `Baidu dlink ready, fsId=${fsId} url=${urlWithToken}`);
- this.cacheBaiduDlink(account, fsId, accessToken, urlWithToken);
- return urlWithToken;
- }
- public async getBaiduFileInfo(account: WebDavAccount, fsId: string, remotePath?: string): Promise<BaiduFileInfo> {
- const accessToken = await this.ensureBaiduAccessToken(account);
- if (!fsId) {
- throw new Error('缺少百度网盘文件fs_id');
- }
- Logger.info(TAG, `查询百度网盘文件信息: fsId=${fsId}`);
- const metas = await fetchBaiduFileMetas(accessToken, [fsId]);
- if (!metas || metas.length === 0) {
- throw new Error('无法获取文件信息');
- }
- const meta = metas[0];
- if (!meta.dlink) {
- throw new Error('无法获取下载链接');
- }
- const rawLink = meta.dlink.startsWith('https://') ? meta.dlink : meta.dlink.replace('http://', 'https://');
- const urlWithToken = appendAccessTokenToDlink(rawLink, accessToken);
- let streamUrl = urlWithToken;
- if (remotePath) {
- try {
- await ensureAudioStreamReady(accessToken, remotePath, BaiduConstants.STREAMING_AUDIO_TYPE, 240, 1000, true);
- streamUrl = buildAudioStreamingUrl(accessToken, remotePath);
- Logger.info(TAG, `百度网盘文件信息获取成功: fsId=${fsId}, size=${meta.size}, streamUrl=${streamUrl}`);
- } catch (streamError) {
- const err = streamError as Error;
- Logger.warn(TAG, `百度音频流暂不可用,回退到下载链接: ${err.message}`);
- streamUrl = urlWithToken;
- }
- } else {
- Logger.warn(TAG, `缺少百度网盘文件路径,使用下载链接播放: fsId=${fsId}`);
- }
- // 缓存 dlink
- this.cacheBaiduDlink(account, fsId, accessToken, urlWithToken);
- const result: BaiduFileInfo = {
- size: meta.size,
- dlink: urlWithToken,
- streamUrl
- };
- return result;
- }
- 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 createJellyfinDirectory(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 createJellyfinSongFileInfo(song: JellyfinSong, 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, coverUrl?: string): 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'),
- 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;
- videoItem.pixelMapPath = coverUrl;
- if (song.track !== undefined && song.track !== null) {
- videoItem.track = song.track.toString();
- }
- if (song.year !== undefined && song.year !== null) {
- videoItem.year = song.year.toString();
- }
- return videoItem;
- }
- private buildJellyfinVideoItem(song: JellyfinSong, account: WebDavAccount, album?: JellyfinAlbum | null, coverUrl?: string): VideoItem {
- const title = song.title ?? '未知曲目';
- const fileName = `${title}${song.suffix ? '.' + song.suffix : ''}`;
- const videoItem = new VideoItem(
- title,
- song.id,
- `jellyfin://${account.id ?? 0}/${song.id}`,
- CommonConstants.TYPE_JELLYFIN,
- song.size ?? 0,
- Utility.getFormatDateStr(Date.now(), 'yyyy-MM-dd HH:mm'),
- 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.sampleRate = song.sampleRate ? song.sampleRate.toString() : undefined;
- videoItem.pixelMapPath = coverUrl;
- if (song.track !== undefined && song.track !== null) {
- videoItem.track = song.track.toString();
- }
- if (song.year !== undefined && song.year !== null) {
- videoItem.year = song.year.toString();
- }
- return videoItem;
- }
- private createEmbyDirectory(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 createEmbySongFileInfo(song: EmbySong, 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 buildEmbyVideoItem(song: EmbySong, account: WebDavAccount, album?: EmbyAlbum | null, coverUrl?: string): VideoItem {
- const title = song.title ?? '未知曲目';
- const fileName = `${title}${song.suffix ? '.' + song.suffix : ''}`;
- const videoItem = new VideoItem(
- title,
- song.id,
- `emby://${account.id ?? 0}/${song.id}`,
- CommonConstants.TYPE_EMBY,
- song.size ?? 0,
- Utility.getFormatDateStr(Date.now(), 'yyyy-MM-dd HH:mm'),
- 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.sampleRate = song.sampleRate ? song.sampleRate.toString() : undefined;
- videoItem.pixelMapPath = coverUrl;
- if (song.track !== undefined && song.track !== null) {
- videoItem.track = song.track.toString();
- }
- if (song.year !== undefined && song.year !== null) {
- videoItem.year = song.year.toString();
- }
- // 存储歌词索引
- if (song.lyricIndex !== undefined && song.lyricIndex !== null) {
- videoItem.lyricIndex = song.lyricIndex;
- }
- 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);
- }
- // ==================== 上传队列管理 ====================
- /**
- * 添加歌曲到上传队列
- * @param songs 要上传的歌曲列表
- * @param account 目标WebDAV账户
- * @param customUploadPath 自定义上传路径(可选,如果不提供则使用账户默认路径)
- */
- public addToUploadQueue(songs: VideoItem[], account: WebDavAccount, customUploadPath?: string): void {
- Logger.info(TAG, '========== 添加上传任务 ==========');
- Logger.info(TAG, `请求添加: ${songs.length} 个文件`);
- Logger.info(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
- Logger.info(TAG, `自定义上传路径: ${customUploadPath || '未指定,使用账户默认路径'}`);
- Logger.info(TAG, `当前队列: ${this.uploadQueue.length}/${this.MAX_UPLOAD_QUEUE_SIZE}`);
-
- // 检查队列大小限制
- const availableSlots = this.MAX_UPLOAD_QUEUE_SIZE - this.uploadQueue.length;
- if (availableSlots <= 0) {
- Logger.warn(TAG, `上传队列已满(最大${this.MAX_UPLOAD_QUEUE_SIZE}个任务),无法添加更多任务`);
- Logger.warn(TAG, '====================================');
- return;
- }
-
- // 限制添加数量
- const songsToAdd = songs.length > availableSlots ? songs.slice(0, availableSlots) : songs;
- const existingKeys = new Set<string>();
- for (let i = 0; i < this.uploadQueue.length; i++) {
- const key = this.getSongKey(this.uploadQueue[i].song);
- if (key) {
- existingKeys.add(key);
- }
- }
- const filteredSongs: VideoItem[] = [];
- for (let i = 0; i < songsToAdd.length; i++) {
- const song = songsToAdd[i];
- const key = this.getSongKey(song);
- if (key && existingKeys.has(key)) {
- Logger.warn(TAG, `检测到重复任务,已跳过: ${song.name}`);
- continue;
- }
- if (key) {
- existingKeys.add(key);
- }
- filteredSongs.push(song);
- }
-
- Logger.info(TAG, `实际添加: ${filteredSongs.length} 个文件`);
-
- for (let i = 0; i < filteredSongs.length; i++) {
- const song = filteredSongs[i];
- const task: TransferTask = {
- song,
- account,
- customUploadPath: customUploadPath // 保存自定义上传路径
- };
- this.uploadQueue.push(task);
- Logger.info(TAG, `[${i + 1}/${songsToAdd.length}] ${song.name}`);
- }
-
- if (songs.length > songsToAdd.length) {
- Logger.warn(TAG, `队列空间不足,仅处理前${songsToAdd.length}/${songs.length}个任务`);
- }
-
- Logger.info(TAG, `新队列大小: ${this.uploadQueue.length}/${this.MAX_UPLOAD_QUEUE_SIZE}`);
- Logger.info(TAG, '====================================');
-
- this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
- this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
- }
- /**
- * 开始处理上传队列
- */
- public async startUploadQueue(): Promise<void> {
- if (this.isProcessingUploadQueue) {
- Logger.warn(TAG, '上传队列正在处理中');
- return;
- }
- if (this.uploadQueue.length === 0) {
- Logger.info(TAG, '上传队列为空');
- return;
- }
- this.isProcessingUploadQueue = true;
- this.isPauseUpload = false;
- Logger.info(TAG, '开始处理上传队列,共', this.uploadQueue.length.toString(), '个任务');
- await this.processNextUploadTask();
- }
- /**
- * 处理下一个上传任务
- */
- private async processNextUploadTask(): Promise<void> {
- if (this.isPauseUpload) {
- Logger.info(TAG, '========== 上传队列已暂停 ==========');
- Logger.info(TAG, `剩余任务数: ${this.uploadQueue.length}`);
- Logger.info(TAG, '====================================');
- this.isProcessingUploadQueue = false;
- return;
- }
- if (this.uploadQueue.length === 0) {
- Logger.info(TAG, '========== 上传队列已完成 ==========');
- Logger.info(TAG, `成功上传: ${this.finishUploadQueue.length} 个文件`);
- Logger.info(TAG, '====================================');
-
- this.isProcessingUploadQueue = false;
- this.currentUploadTask = null;
- this.notifyObservers(RemoteDriveManagerStates.SetCurrentUploadTask);
-
- // 检查是否需要自动清理完成队列
- const autoClear = PreferencesUtil.getBooleanSync('webdavUploadAutoClear', false);
- if (autoClear && this.finishUploadQueue.length > 0) {
- Logger.info(TAG, '自动清理完成队列');
- this.finishUploadQueue = [];
- this.notifyObservers(RemoteDriveManagerStates.ChangeFinishUploadQueue);
- }
-
- return;
- }
- const task = this.uploadQueue[0];
- this.currentUploadTask = task;
-
- Logger.info(TAG, '========== 处理上传队列 ==========');
- Logger.info(TAG, `当前任务: ${task.song.name}`);
- Logger.info(TAG, `队列位置: 1/${this.uploadQueue.length}`);
- Logger.info(TAG, `已完成: ${this.finishUploadQueue.length}`);
- Logger.info(TAG, '====================================');
-
- this.notifyObservers(RemoteDriveManagerStates.SetCurrentUploadTask);
- try {
- if(this.currentAccount.webType==RemoteDriveType.WebDav){
- await this.uploadSingleFile(task);
- }else if(this.currentAccount.webType==RemoteDriveType.Baidu){
- await this.uploadBaiduFile(task);
- }
-
- // 上传成功,移到完成队列
- Logger.info(TAG, `任务成功,移至完成队列: ${task.song.name}`);
- this.uploadQueue.shift();
- this.finishUploadQueue.push(task);
- this.notifyObservers(RemoteDriveManagerStates.UploadSuccess);
- this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
- this.notifyObservers(RemoteDriveManagerStates.ChangeFinishUploadQueue);
- // 继续处理下一个任务
- await this.processNextUploadTask();
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, '---------- 任务失败 ----------');
- Logger.error(TAG, `文件: ${task.song.name}`);
- Logger.error(TAG, `错误: ${err.message}`);
-
- // 获取重试次数设置
- const maxRetryCount = PreferencesUtil.getNumberSync('webdavUploadRetryCount', 3);
- const currentRetryCount = task.retryCount || 0;
-
- if (currentRetryCount < maxRetryCount) {
- // 还可以重试
- task.retryCount = currentRetryCount + 1;
- Logger.info(TAG, `准备重试上传 (${task.retryCount}/${maxRetryCount}): ${task.song.name}`);
- Logger.info(TAG, '将任务移至队列末尾');
-
- // 将任务移到队列末尾重试
- this.uploadQueue.shift();
- this.uploadQueue.push(task);
- this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
- } else {
- // 已达到最大重试次数,放弃上传
- Logger.error(TAG, `上传失败,已达到最大重试次数 (${maxRetryCount}): ${task.song.name}`);
- Logger.error(TAG, '从队列中移除该任务');
- this.uploadQueue.shift();
- this.notifyObservers(RemoteDriveManagerStates.UploadFailed);
- this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
- }
-
- Logger.error(TAG, '------------------------------');
- // 继续处理下一个任务
- await this.processNextUploadTask();
- }
- }
- /**
- * 暂停上传队列
- */
- public pauseUploadQueue(): void {
- Logger.info(TAG, '========== 暂停上传队列 ==========');
-
- if (!this.isProcessingUploadQueue) {
- Logger.warn(TAG, '上传队列未在处理中,无需暂停');
- Logger.info(TAG, '====================================');
- return;
- }
- this.isPauseUpload = true;
- Logger.info(TAG, `当前任务: ${this.currentUploadTask?.song.name || '无'}`);
- Logger.info(TAG, `剩余任务: ${this.uploadQueue.length}`);
- Logger.info(TAG, '上传队列已暂停');
- Logger.info(TAG, '====================================');
-
- this.notifyObservers(RemoteDriveManagerStates.UploadPaused);
- this.notifyObservers(RemoteDriveManagerStates.SetIsPauseUpload);
- }
- /**
- * 恢复上传队列
- */
- public async resumeUploadQueue(): Promise<void> {
- Logger.info(TAG, '========== 恢复上传队列 ==========');
-
- if (!this.isPauseUpload) {
- Logger.warn(TAG, '上传队列未暂停,无需恢复');
- Logger.info(TAG, '====================================');
- return;
- }
- this.isPauseUpload = false;
- Logger.info(TAG, `剩余任务: ${this.uploadQueue.length}`);
- Logger.info(TAG, '上传队列已恢复');
- Logger.info(TAG, '====================================');
-
- this.notifyObservers(RemoteDriveManagerStates.UploadResumed);
- this.notifyObservers(RemoteDriveManagerStates.SetIsPauseUpload);
- if (!this.isProcessingUploadQueue) {
- this.isProcessingUploadQueue = true;
- await this.processNextUploadTask();
- }
- }
- /**
- * 从队列中移除任务
- * @param task 要移除的任务
- */
- public removeFromUploadQueue(task: TransferTask): void {
- const index = this.uploadQueue.indexOf(task);
- if (index >= 0) {
- Logger.info(TAG, '========== 移除上传任务 ==========');
- Logger.info(TAG, `文件名: ${task.song.name}`);
- Logger.info(TAG, `队列位置: ${index + 1}/${this.uploadQueue.length}`);
-
- this.uploadQueue.splice(index, 1);
-
- Logger.info(TAG, `新队列大小: ${this.uploadQueue.length}`);
- Logger.info(TAG, '====================================');
-
- this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
- this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
- } else {
- Logger.warn(TAG, `任务不在队列中: ${task.song.name}`);
- }
- }
-
- /**
- * 清空上传队列
- */
- public clearUploadQueue(): void {
- Logger.info(TAG, '========== 清空上传队列 ==========');
- Logger.info(TAG, `清空前队列大小: ${this.uploadQueue.length}`);
-
- this.uploadQueue = [];
- this.currentUploadTask = null;
-
- Logger.info(TAG, '上传队列已清空');
- Logger.info(TAG, '====================================');
-
- this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
- this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
- this.notifyObservers(RemoteDriveManagerStates.SetCurrentUploadTask);
- }
- /**
- * 检查是否允许在当前网络下上传
- */
- private async checkNetworkAllowed(): Promise<boolean> {
- try {
- Logger.info(TAG, '检查网络连接状态...');
- const allowMobile = PreferencesUtil.getBooleanSync('webdavUploadAllowMobile', false);
- Logger.info(TAG, `用户设置 - 允许移动网络上传: ${allowMobile}`);
-
- // 如果允许移动网络上传,直接返回true
- if (allowMobile) {
- Logger.info(TAG, '用户已允许移动网络上传,跳过网络类型检查');
- return true;
- }
- // 检查当前网络类型
- Logger.info(TAG, '获取当前网络类型...');
- const netHandle = await connection.getDefaultNet();
- const netCapabilities = await connection.getNetCapabilities(netHandle);
-
- // 检查是否为移动网络
- const isCellular = netCapabilities.bearerTypes.includes(connection.NetBearType.BEARER_CELLULAR);
- const isWifi = netCapabilities.bearerTypes.includes(connection.NetBearType.BEARER_WIFI);
- const isEthernet = netCapabilities.bearerTypes.includes(connection.NetBearType.BEARER_ETHERNET);
-
- Logger.info(TAG, `网络类型 - 移动网络: ${isCellular}, WiFi: ${isWifi}, 以太网: ${isEthernet}`);
-
- if (isCellular) {
- Logger.warn(TAG, '当前为移动网络,且未允许移动网络上传');
- Logger.warn(TAG, '请连接WiFi或在设置中允许移动网络上传');
- return false;
- }
-
- Logger.info(TAG, '网络检查通过,允许上传');
- return true;
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, '检查网络类型失败:', err.message);
- Logger.warn(TAG, '网络检查失败,默认允许上传');
- // 检查失败时默认允许上传
- return true;
- }
- }
- /**
- * 上传单个文件
- * @param task 上传任务
- */
- private async uploadSingleFile(task: TransferTask): Promise<void> {
- const song = task.song;
- const account = task.account;
- const startTime = Date.now();
- // 详细日志:上传开始
- Logger.info(TAG, '========== 上传任务开始 ==========');
- Logger.info(TAG, `文件名: ${song.name}`);
- Logger.info(TAG, `文件路径: ${song.filePath}`);
- Logger.info(TAG, `文件大小: ${song.videoSize || 0} 字节`);
- Logger.info(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
- Logger.info(TAG, `目标主机: ${account.host}:${account.port}`);
- Logger.info(TAG, `使用HTTPS: ${account.enableHttps}`);
- Logger.info(TAG, `重试次数: ${task.retryCount || 0}`);
-
- try {
- // 检查网络是否允许上传
- Logger.info(TAG, '检查网络连接状态...');
- const networkAllowed = await this.checkNetworkAllowed();
- if (!networkAllowed) {
- const errorMsg = '当前网络不允许上传,请连接WiFi或在设置中允许移动网络上传';
- Logger.error(TAG, `网络检查失败: ${errorMsg}`);
- throw new Error(errorMsg);
- }
- Logger.info(TAG, '网络检查通过');
-
- this.notifyObservers(RemoteDriveManagerStates.UploadStart);
- // 构建远程路径 - 优先使用任务中的自定义路径,否则使用账户默认路径
- const uploadPath = task.customUploadPath || account.uploadFilePath || '/';
- const normalizedPath = this.normalizeFullPath(uploadPath);
- const relativeSegment = this.sanitizeRelativeUploadPath(song.remote_rel_path);
- const fallbackFileName = song.fileName || song.name || `upload-${Date.now()}`;
- const relativePath = relativeSegment.length > 0 ? relativeSegment : fallbackFileName;
- const remotePath = this.combineRemoteUploadPath(normalizedPath, relativePath);
- Logger.info(TAG, '---------- 路径信息 ----------');
- Logger.info(TAG, `任务自定义路径: ${task.customUploadPath || '未指定'}`);
- Logger.info(TAG, `账户默认路径: ${account.uploadFilePath || '未配置'}`);
- Logger.info(TAG, `实际使用路径: ${uploadPath}`);
- Logger.info(TAG, `规范化后的路径: ${normalizedPath}`);
- Logger.info(TAG, `文件名 (fileName): ${song.fileName}`);
- Logger.info(TAG, `文件名 (name): ${song.name}`);
- Logger.info(TAG, `文件相对路径: ${relativePath}`);
- Logger.info(TAG, `最终远程路径: ${remotePath}`);
- Logger.info(TAG, `完整URL将是: ${account.enableHttps ? 'https' : 'http'}://${account.host}:${account.port}${remotePath}`);
- Logger.info(TAG, '------------------------------');
- // 检查文件是否存在
- Logger.info(TAG, '检查远程文件是否存在...');
- const exists = await this.checkFileExists(account, remotePath);
- if (exists) {
- // 根据用户配置的重复文件处理方式来处理
- const duplicateAction = PreferencesUtil.getStringSync('webdavUploadDuplicateAction', 'skip');
- Logger.info(TAG, `文件已存在,处理方式: ${duplicateAction}`);
-
- if (duplicateAction === 'skip') {
- Logger.warn(TAG, '文件已存在,跳过上传:', remotePath);
- return;
- } else if (duplicateAction === 'overwrite') {
- Logger.warn(TAG, '文件已存在,将覆盖:', remotePath);
- } else if (duplicateAction === 'rename') {
- Logger.warn(TAG, '文件已存在,将重命名上传');
- // 重命名逻辑可以在这里实现
- }
- } else {
- Logger.info(TAG, '远程文件不存在,可以上传');
- }
- const remoteDirectory = this.getDirectoryFromRemotePath(remotePath);
- await this.ensureRemoteDirectories(account, remoteDirectory);
- // 执行上传
- Logger.info(TAG, '开始上传文件数据...');
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- await this.rcpSocket.uploadFile(
- song.filePath,
- remotePath,
- host,
- account.port,
- account.account,
- account.password,
- account.enableHttps,
- (uploaded: number, total: number) => {
- this.uploadReceivedSize = uploaded;
- this.uploadTotalSize = total;
- // 直接通知UI更新,因为RcpSocketUtil层已经做了节流
- // 详细日志:上传进度
- const progress = total > 0 ? Math.min(100, ((uploaded / total) * 100)) : 0;
- const currentTime = Date.now();
- const elapsedSeconds = (currentTime - startTime) / 1000;
- const speedMBps = uploaded > 0 && elapsedSeconds > 0 ? (uploaded / (1024 * 1024) / elapsedSeconds).toFixed(2) : '0.00';
- Logger.info(TAG, `上传进度: ${progress.toFixed(2)}%, 已上传: ${uploaded}/${total} 字节, 速度: ${speedMBps} MB/s`);
- this.notifyObservers(RemoteDriveManagerStates.UploadProgress);
- }
- );
- // 上传成功
- const endTime = Date.now();
- const duration = ((endTime - startTime) / 1000).toFixed(2);
- const avgSpeed = song.videoSize > 0 ? ((song.videoSize / (1024 * 1024)) / parseFloat(duration)).toFixed(2) : '0.00';
-
- Logger.info(TAG, '========== 上传任务成功 ==========');
- Logger.info(TAG, `文件名: ${song.name}`);
- Logger.info(TAG, `耗时: ${duration} 秒`);
- Logger.info(TAG, `平均速度: ${avgSpeed} MB/s`);
- Logger.info(TAG, `目标路径: ${remotePath}`);
- Logger.info(TAG, '====================================');
-
- } catch (error) {
- const err = error as Error;
- const endTime = Date.now();
- const duration = ((endTime - startTime) / 1000).toFixed(2);
- // 详细错误日志
- Logger.error(TAG, '========== 上传任务失败 ==========');
- Logger.error(TAG, `文件名: ${song.name}`);
- Logger.error(TAG, `文件路径: ${song.filePath}`);
- Logger.error(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
- Logger.error(TAG, `目标主机: ${account.host}:${account.port}`);
- Logger.error(TAG, `耗时: ${duration} 秒`);
- Logger.error(TAG, `错误类型: ${this.getErrorType(err)}`);
- Logger.error(TAG, `错误信息: ${err.message}`);
- Logger.error(TAG, `错误堆栈: ${err.stack || '无堆栈信息'}`);
- Logger.error(TAG, `重试次数: ${task.retryCount || 0}`);
- Logger.error(TAG, '====================================');
- // 根据错误类型进行分类处理
- this.handleUploadError(err, task);
- // 重新抛出错误以便上层处理
- throw err;
- }
- }
- /**
- * 上传百度网盘文件(使用TaskPool避免主线程阻塞)
- * @param task 上传任务
- */
- private async uploadBaiduFile(task: TransferTask): Promise<void> {
- const song = task.song;
- const account = task.account;
- const startTime = Date.now();
- // 详细日志:百度网盘上传开始
- Logger.info(TAG, '========== 百度网盘上传任务开始 ==========');
- Logger.info(TAG, `文件名: ${song.name}`);
- Logger.info(TAG, `文件路径: ${song.filePath}`);
- Logger.info(TAG, `文件大小: ${song.videoSize || 0} 字节`);
- Logger.info(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
- Logger.info(TAG, `重试次数: ${task.retryCount || 0}`);
- try {
- // 检查网络是否允许上传
- Logger.info(TAG, '检查网络连接状态...');
- const networkAllowed = await this.checkNetworkAllowed();
- if (!networkAllowed) {
- const errorMsg = '当前网络不允许上传,请连接WiFi或在设置中允许移动网络上传';
- Logger.error(TAG, `网络检查失败: ${errorMsg}`);
- throw new Error(errorMsg);
- }
- Logger.info(TAG, '网络检查通过');
- this.notifyObservers(RemoteDriveManagerStates.UploadStart);
- // 获取访问令牌
- const accessToken = await this.ensureBaiduAccessToken(account);
- if (!accessToken) {
- throw new Error('百度网盘授权已过期,请重新登录');
- }
- // 构建上传路径
- const uploadPath = task.customUploadPath || account.uploadFilePath || '/apps/ttmusic';
- const normalizedPath = this.normalizeFullPath(uploadPath);
- const fileName = song.fileName || song.name || `upload-${Date.now()}`;
- const remotePath = `${normalizedPath === '/' ? '' : normalizedPath}/${fileName}`;
- Logger.info(TAG, '---------- 百度网盘路径信息 ----------');
- Logger.info(TAG, `上传基础路径: ${uploadPath}`);
- Logger.info(TAG, `规范化后的路径: ${normalizedPath}`);
- Logger.info(TAG, `文件名: ${fileName}`);
- Logger.info(TAG, `最终远程路径: ${remotePath}`);
- Logger.info(TAG, '------------------------------');
- // 转换路径为百度网盘要求的格式
- const baiduRemotePath = convertToBaiduPath(remotePath);
- Logger.info(TAG, `原始路径: ${remotePath}, 转换后路径: ${baiduRemotePath}`);
- // 使用TaskPool执行上传任务(支持进度消息)
- Logger.info(TAG, '创建TaskPool上传任务...');
- const taskParams: TaskBaiduUploadParams = {
- filePath: song.filePath,
- accessToken: accessToken,
- remotePath: baiduRemotePath,
- songName: song.name,
- songId: song.id || `file-${Date.now()}`
- };
- const uploadTask: taskpool.Task = new taskpool.Task(executeBaiduUpload, taskParams);
- // 设置进度消息接收器
- uploadTask.onReceiveData((progressData: BaiduUploadProgressMessage) => {
- // 更新上传进度
- this.uploadReceivedSize = progressData.uploadedSize || 0;
- this.uploadTotalSize = progressData.totalSize || 0;
- Logger.info(TAG, `百度网盘上传进度: ${progressData.progress.toFixed(1)}% (${progressData.currentPart || 0}/${progressData.totalParts || 0} ${progressData.stage})`);
- this.notifyObservers(RemoteDriveManagerStates.UploadProgress);
- });
- // 提交任务到TaskPool
- Logger.info(TAG, '提交上传任务到TaskPool...');
- const result = await taskpool.execute(uploadTask) as TaskBaiduUploadResult;
- // 检查上传结果
- if (!result.success) {
- const errorMessage = result.error || '上传失败';
- throw new Error(errorMessage);
- }
- // 上传成功
- const endTime = Date.now();
- const duration = ((endTime - startTime) / 1000).toFixed(2);
- Logger.info(TAG, '========== 百度网盘上传任务成功 ==========');
- Logger.info(TAG, `文件名: ${song.name}`);
- Logger.info(TAG, `文件路径: ${song.filePath}`);
- Logger.info(TAG, `目标路径: ${remotePath}`);
- Logger.info(TAG, `耗时: ${duration} 秒`);
- Logger.info(TAG, '====================================');
- // 更新上传进度到100%
- this.notifyObservers(RemoteDriveManagerStates.UploadProgress);
- } catch (error) {
- const err = error as Error;
- const endTime = Date.now();
- const duration = ((endTime - startTime) / 1000).toFixed(2);
- // 详细错误日志
- Logger.error(TAG, '========== 百度网盘上传任务失败 ==========');
- Logger.error(TAG, `文件名: ${song.name}`);
- Logger.error(TAG, `文件路径: ${song.filePath}`);
- Logger.error(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
- Logger.error(TAG, `耗时: ${duration} 秒`);
- Logger.error(TAG, `错误信息: ${err.message}`);
- Logger.error(TAG, `错误堆栈: ${err.stack || '无堆栈信息'}`);
- Logger.error(TAG, `重试次数: ${task.retryCount || 0}`);
- Logger.error(TAG, '====================================');
- // 根据错误类型进行分类处理
- this.handleUploadError(err, task);
- // 重新抛出错误以便上层处理
- throw err;
- }
- }
-
-
-
- /**
- * 获取错误类型
- * @param error 错误对象
- * @returns 错误类型字符串
- */
- private getErrorType(error: Error): string {
- const message = error.message.toLowerCase();
-
- if (message.includes('network') || message.includes('连接') || message.includes('timeout') || message.includes('超时')) {
- return '网络错误';
- } else if (message.includes('auth') || message.includes('认证') || message.includes('401') || message.includes('403')) {
- return '认证错误';
- } else if (message.includes('file') || message.includes('文件') || message.includes('not found') || message.includes('不存在')) {
- return '文件错误';
- } else if (message.includes('server') || message.includes('服务器') || message.includes('500') || message.includes('503')) {
- return '服务器错误';
- } else if (message.includes('space') || message.includes('空间') || message.includes('disk') || message.includes('磁盘')) {
- return '存储空间错误';
- } else {
- return '未知错误';
- }
- }
-
- /**
- * 处理上传错误
- * @param error 错误对象
- * @param task 上传任务
- */
- private handleUploadError(error: Error, task: TransferTask): void {
- const errorType = this.getErrorType(error);
-
- Logger.info(TAG, `处理上传错误,类型: ${errorType}`);
-
- // 根据错误类型决定是否自动暂停
- if (errorType === '网络错误') {
- Logger.warn(TAG, '检测到网络错误,自动暂停上传队列');
- this.pauseUploadQueue();
- } else if (errorType === '认证错误') {
- Logger.error(TAG, '检测到认证错误,请检查账户密码是否正确');
- // 认证错误通常不需要暂停整个队列,但应该提示用户
- } else if (errorType === '服务器错误') {
- Logger.error(TAG, '检测到服务器错误,服务器可能暂时不可用');
- // 服务器错误可能是临时的,可以继续尝试其他文件
- } else if (errorType === '存储空间错误') {
- Logger.error(TAG, '检测到存储空间不足,暂停上传队列');
- this.pauseUploadQueue();
- }
- }
- /**
- * 检查文件是否存在
- * @param account WebDAV账户
- * @param remotePath 远程路径
- */
- private async checkFileExists(account: WebDavAccount, remotePath: string): Promise<boolean> {
- try {
- Logger.info(TAG, `检查远程文件是否存在: ${remotePath}`);
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- const fileSize = await this.rcpSocket.RcpSendHead(
- host,
- account.port,
- account.account,
- account.password,
- remotePath,
- account.enableHttps
- );
-
- const exists = fileSize > 0;
- Logger.info(TAG, `文件${exists ? '存在' : '不存在'}, 大小: ${fileSize} 字节`);
- return exists;
- } catch (error) {
- const err = error as Error;
- // HEAD请求失败,说明文件不存在或网络错误
- Logger.info(TAG, `HEAD请求失败: ${err.message}, 假定文件不存在`);
- return false;
- }
- }
- /**
- * 处理重复文件
- * @param account WebDAV账户
- * @param remotePath 远程路径
- * @param action 处理动作:'skip' | 'overwrite' | 'rename'
- */
- private async handleDuplicateFile(
- account: WebDavAccount,
- remotePath: string,
- action: string
- ): Promise<string> {
- if (action === 'skip') {
- throw new Error('文件已存在,跳过上传');
- }
- if (action === 'overwrite') {
- // 删除现有文件
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- await this.rcpSocket.RcpSendDelete(
- host,
- account.port,
- account.account,
- account.password,
- remotePath,
- account.enableHttps
- );
- return remotePath;
- }
- if (action === 'rename') {
- // 生成新文件名
- const lastDotIndex = remotePath.lastIndexOf('.');
- const lastSlashIndex = remotePath.lastIndexOf('/');
- let basePath = '';
- let fileName = '';
- let extension = '';
- if (lastSlashIndex >= 0) {
- basePath = remotePath.substring(0, lastSlashIndex + 1);
- fileName = remotePath.substring(lastSlashIndex + 1);
- } else {
- fileName = remotePath;
- }
- if (lastDotIndex > lastSlashIndex) {
- extension = fileName.substring(lastDotIndex);
- fileName = fileName.substring(0, lastDotIndex - lastSlashIndex - 1);
- }
- // 尝试添加序号
- let counter = 1;
- let newPath = '';
- let exists = true;
- while (exists && counter < 100) {
- newPath = `${basePath}${fileName}(${counter})${extension}`;
- exists = await this.checkFileExists(account, newPath);
- counter++;
- }
- if (exists) {
- throw new Error('无法生成唯一文件名');
- }
- return newPath;
- }
- return remotePath;
- }
- // ==================== 安全认证方法 ====================
- // 获取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;
- }
- /**
- * 在百度网盘中创建文件夹
- * @param account 百度网盘账户
- * @param folderName 要创建的文件夹名称
- * @param parentPath 父级路径,默认为当前路径
- * @returns Promise<string> 创建成功后的完整路径
- */
- public async createBaiduFolder(account: WebDavAccount, folderName: string, parentPath?: string): Promise<string> {
- if (!account || account.webType !== RemoteDriveType.Baidu) {
- throw new Error('只支持在百度网盘账户中创建文件夹');
- }
- if (!folderName || folderName.trim().length === 0) {
- throw new Error('文件夹名称不能为空');
- }
- // 检查文件夹名称是否包含非法字符
- const invalidChars = /[\\/:*?"<>|]/;
- if (invalidChars.test(folderName.trim())) {
- throw new Error('文件夹名称不能包含 \\ / : * ? " < > | 字符');
- }
- const accessToken = account.baiduAccessToken;
- if (!accessToken) {
- throw new Error('百度网盘授权已过期,请重新登录');
- }
- // 确定父级路径
- const basePath = parentPath || this.currentPath;
- // 构建完整的文件夹路径
- const fullPath = basePath === '' || basePath === '/'
- ? `/${folderName.trim()}`
- : `${basePath.replace(/\/$/, '')}/${folderName.trim()}`;
- Logger.info(TAG, `heanup 准备在百度网盘创建文件夹: ${fullPath}`);
- try {
- const result = await createFolder(accessToken, fullPath, 1); // rtype=1表示自动重命名
- Logger.info(TAG, `heanup 百度网盘文件夹创建成功: ${result.path}`);
- return result.path || fullPath;
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `heanup 百度网盘文件夹创建失败: ${err.message}`);
- throw err;
- }
- }
- /**
- * 在WebDAV服务器上创建文件夹
- * @param account WebDAV账户
- * @param folderName 要创建的文件夹名称
- * @param parentPath 父级路径,默认为当前路径
- * @returns Promise<string> 创建成功后的完整路径
- */
- public async createWebDavFolder(account: WebDavAccount, folderName: string, parentPath?: string): Promise<string> {
- if (!account || account.webType !== RemoteDriveType.WebDav) {
- throw new Error('只支持在WebDAV账户中创建文件夹');
- }
- if (!folderName || folderName.trim().length === 0) {
- throw new Error('文件夹名称不能为空');
- }
- // 检查文件夹名称是否包含非法字符
- const invalidChars = /[\\/:*?"<>|]/;
- if (invalidChars.test(folderName.trim())) {
- throw new Error('文件夹名称不能包含 \\ / : * ? " < > | 字符');
- }
- // 确定服务器地址
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- if (!host || host.length === 0) {
- throw new Error('WebDAV账户缺少服务器地址');
- }
- if (!account.account || account.account.length === 0) {
- throw new Error('WebDAV账户缺少用户名');
- }
- if (!account.password || account.password.length === 0) {
- throw new Error('WebDAV账户缺少密码');
- }
- // 确定父级路径
- const basePath = parentPath || this.currentPath;
- // 构建完整的文件夹路径
- const fullPath = basePath === '' || basePath === '/'
- ? `/${folderName.trim()}`
- : `${basePath.replace(/\/$/, '')}/${folderName.trim()}`;
- Logger.info(TAG, `heanup 准备在WebDAV服务器创建文件夹: ${fullPath}`);
- try {
- // 调用RcpSocket的createDirectory方法(使用MKCOL协议)
- await this.rcpSocket.createDirectory(
- host,
- account.port,
- account.account,
- account.password,
- fullPath,
- account.enableHttps
- );
- Logger.info(TAG, `heanup WebDAV文件夹创建成功: ${fullPath}`);
- return fullPath;
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `heanup WebDAV文件夹创建失败: ${err.message}`);
- throw err;
- }
- }
- /**
- * 在SMB服务器上创建文件夹
- * @param account SMB账户
- * @param folderName 要创建的文件夹名称
- * @param parentPath 父级路径,默认为当前路径
- * @returns Promise<string> 创建成功后的完整路径
- */
- public async createSMBFolder(account: WebDavAccount, folderName: string, parentPath?: string): Promise<string> {
- if (!account || account.webType !== RemoteDriveType.Smb) {
- throw new Error('只支持在SMB账户中创建文件夹');
- }
- if (!folderName || folderName.trim().length === 0) {
- throw new Error('文件夹名称不能为空');
- }
- // 检查文件夹名称是否包含非法字符
- const invalidChars = /[\\/:*?"<>|]/;
- if (invalidChars.test(folderName.trim())) {
- throw new Error('文件夹名称不能包含 \\ / : * ? " < > | 字符');
- }
- // 确定服务器地址
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- if (!host || host.length === 0) {
- throw new Error('SMB账户缺少服务器地址');
- }
- if (!account.smbShare || account.smbShare.length === 0) {
- throw new Error('SMB账户缺少共享名称');
- }
- // 确定父级路径
- const basePath = parentPath || this.currentPath;
- // 构建完整的文件夹路径
- const fullPath = basePath === '' || basePath === '/'
- ? `${folderName.trim()}`
- : `${basePath.replace(/\/$/, '').replace(/^\/+/, '')}/${folderName.trim()}`;
- Logger.info(TAG, `heanup 准备在SMB服务器创建文件夹: ${fullPath}`);
- try {
- // 调用createSmbDirectory函数
- await createSmbDirectory({
- host,
- share: account.smbShare,
- username: account.account || '',
- password: account.password || '',
- domain: account.smbDomain,
- path: fullPath
- });
- Logger.info(TAG, `heanup SMB文件夹创建成功: ${fullPath}`);
- return `/${fullPath}`;
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `heanup SMB文件夹创建失败: ${err.message}`);
- throw err;
- }
- }
- }
- /**
- * 构建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;
- }
|