|
|
@@ -86,13 +86,18 @@ interface BaiduUploadProgressMessage {
|
|
|
totalSize?: number;
|
|
|
}
|
|
|
|
|
|
-interface RemoteDirectorySnapshot {
|
|
|
+export interface RemoteDirectorySnapshot {
|
|
|
files: FileInfo[];
|
|
|
folders: FileInfo[];
|
|
|
songs: VideoItem[];
|
|
|
rawEntries?: BaiduListEntry[];
|
|
|
}
|
|
|
|
|
|
+interface DirectoryPreviewSnapshotPayload {
|
|
|
+ files?: FileInfo[];
|
|
|
+ songs?: VideoItem[];
|
|
|
+}
|
|
|
+
|
|
|
interface RemoteDriveGlobalSearchIndex {
|
|
|
fingerprint: string;
|
|
|
songs: VideoItem[];
|
|
|
@@ -318,6 +323,12 @@ export interface TransferTask {
|
|
|
customUploadPath?: string; // 自定义上传路径(可选)
|
|
|
}
|
|
|
|
|
|
+interface UploadTaskResult {
|
|
|
+ success: boolean;
|
|
|
+ skipped?: boolean;
|
|
|
+ message?: string;
|
|
|
+}
|
|
|
+
|
|
|
@Observed
|
|
|
export class RemoteDriveManager {
|
|
|
public rcpSocket: RcpSocket = RcpSocket.getInstance();
|
|
|
@@ -340,6 +351,10 @@ export class RemoteDriveManager {
|
|
|
public webDavAccounts: WebDavAccount[] = [];
|
|
|
public webDavSongs: VideoItem[] = [];
|
|
|
public webDavFiles: FileInfo[] = []; // 当前目录的所有文件(包括文件夹)
|
|
|
+ private readonly DIRECTORY_PREVIEW_CACHE_LIMIT: number = 20;
|
|
|
+ private readonly DIRECTORY_PREVIEW_STORE_PREFIX: string = 'webdav_directory_preview_';
|
|
|
+ private readonly DIRECTORY_PREVIEW_STORE_INDEX_KEY: string = 'webdav_directory_preview_index';
|
|
|
+ private readonly DIRECTORY_PREVIEW_STORE_LIMIT: number = 80;
|
|
|
private static readonly BAIDU_DLINK_CACHE_TTL: number = 10 * 60 * 1000; // 10分钟
|
|
|
private baiduDlinkCache: Map<string, BaiduDlinkCacheEntry> = new Map();
|
|
|
|
|
|
@@ -381,6 +396,10 @@ export class RemoteDriveManager {
|
|
|
private lastAccountId: number | null = null;
|
|
|
private readonly GLOBAL_SEARCH_PROGRESS_NOTIFY_INTERVAL: number = 4;
|
|
|
private readonly GLOBAL_SEARCH_YIELD_INTERVAL: number = 6;
|
|
|
+ private readonly DIRECTORY_METADATA_ENRICH_LIMIT: number = 80;
|
|
|
+ private readonly DIRECTORY_LOAD_YIELD_INTERVAL: number = 40;
|
|
|
+ private directoryLoadRequestVersion: number = 0;
|
|
|
+ private scheduledWebDavAccountIdRepairIds: Set<string> = new Set();
|
|
|
|
|
|
public currentAccount:WebDavAccount = new WebDavAccount();
|
|
|
|
|
|
@@ -1307,43 +1326,91 @@ export class RemoteDriveManager {
|
|
|
}
|
|
|
this.lastAccountId = account.id ?? null;
|
|
|
|
|
|
- // 更新现有WebDAV歌曲的webdav_account_id字段
|
|
|
- if (account && account.id) {
|
|
|
- await this.updateWebDavSongsAccountId(account.id.toString());
|
|
|
- }
|
|
|
+ const normalizedFullPath = this.normalizeFullPath(customPath !== undefined ? customPath : account.filepath);
|
|
|
+ const requestVersion = ++this.directoryLoadRequestVersion;
|
|
|
+ this.currentPath = normalizedFullPath;
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoStart);
|
|
|
+ this.scheduleWebDavSongsAccountIdRepair(account.id);
|
|
|
|
|
|
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);
|
|
|
+ await this.loadSmbFiles(account, normalizedFullPath, requestVersion);
|
|
|
} else if (account.webType === RemoteDriveType.Navidrome) {
|
|
|
- await this.loadNavidromeFiles(account, normalizedFullPath);
|
|
|
+ await this.loadNavidromeFiles(account, normalizedFullPath, requestVersion);
|
|
|
} else if (account.webType === RemoteDriveType.Jellyfin) {
|
|
|
- await this.loadJellyfinFiles(account, normalizedFullPath);
|
|
|
+ await this.loadJellyfinFiles(account, normalizedFullPath, requestVersion);
|
|
|
} else if (account.webType === RemoteDriveType.Emby) {
|
|
|
- await this.loadEmbyFiles(account, normalizedFullPath);
|
|
|
+ await this.loadEmbyFiles(account, normalizedFullPath, requestVersion);
|
|
|
} else if (account.webType === RemoteDriveType.Ftp) {
|
|
|
- await this.loadFtpFiles(account, normalizedFullPath);
|
|
|
+ await this.loadFtpFiles(account, normalizedFullPath, requestVersion);
|
|
|
} else if (account.webType === RemoteDriveType.Baidu) {
|
|
|
- await this.loadBaiduFiles(account, normalizedFullPath);
|
|
|
+ await this.loadBaiduFiles(account, normalizedFullPath, requestVersion);
|
|
|
} else {
|
|
|
- await this.loadWebDavFiles(account, normalizedFullPath);
|
|
|
+ await this.loadWebDavFiles(account, normalizedFullPath, requestVersion);
|
|
|
}
|
|
|
|
|
|
+ if (!this.isDirectoryLoadRequestActive(requestVersion, normalizedFullPath)) {
|
|
|
+ Logger.info(TAG, `忽略过期目录成功事件 path=${normalizedFullPath}, request=${requestVersion}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
this.seedCurrentDirectoryIntoGlobalSearchIndex(account);
|
|
|
- void this.ensureGlobalSearchIndex(account);
|
|
|
this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoSucceed);
|
|
|
} catch (error) {
|
|
|
+ if (!this.isDirectoryLoadRequestActive(requestVersion, normalizedFullPath)) {
|
|
|
+ Logger.info(TAG, `忽略过期目录失败事件 path=${normalizedFullPath}, request=${requestVersion}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
this.ErrorMessage = error as BusinessError;
|
|
|
this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoFailed);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ public async loadDirectorySnapshotSilently(account: WebDavAccount, customPath?: string): Promise<RemoteDirectorySnapshot> {
|
|
|
+ const normalizedFullPath = this.normalizeFullPath(customPath !== undefined ? customPath : account.filepath);
|
|
|
+ if (account.webType === RemoteDriveType.Smb) {
|
|
|
+ return await this.fetchSmbDirectorySnapshot(account, normalizedFullPath, false);
|
|
|
+ }
|
|
|
+ if (account.webType === RemoteDriveType.Ftp) {
|
|
|
+ return await this.fetchFtpDirectorySnapshot(account, normalizedFullPath, false);
|
|
|
+ }
|
|
|
+ if (account.webType === RemoteDriveType.Baidu) {
|
|
|
+ const accessToken = await this.ensureBaiduAccessToken(account);
|
|
|
+ return await this.fetchBaiduDirectorySnapshot(account, normalizedFullPath, false, accessToken);
|
|
|
+ }
|
|
|
+ return await this.fetchWebDavDirectorySnapshot(account, normalizedFullPath, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ private scheduleWebDavSongsAccountIdRepair(accountId?: number | null): void {
|
|
|
+ if (accountId === undefined || accountId === null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const accountIdString = accountId.toString();
|
|
|
+ if (accountIdString.length === 0 || this.scheduledWebDavAccountIdRepairIds.has(accountIdString)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.scheduledWebDavAccountIdRepairIds.add(accountIdString);
|
|
|
+ void this.updateWebDavSongsAccountId(accountIdString);
|
|
|
+ }
|
|
|
+
|
|
|
+ private isDirectoryLoadRequestActive(requestVersion: number, targetPath: string): boolean {
|
|
|
+ return requestVersion === this.directoryLoadRequestVersion && this.currentPath === targetPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ private applyDirectorySnapshot(account: WebDavAccount, fullPath: string, snapshot: RemoteDirectorySnapshot,
|
|
|
+ requestVersion: number, shouldCache: boolean = true): boolean {
|
|
|
+ if (!this.isDirectoryLoadRequestActive(requestVersion, fullPath)) {
|
|
|
+ Logger.info(TAG,
|
|
|
+ `忽略过期目录结果 path=${fullPath}, request=${requestVersion}, latest=${this.directoryLoadRequestVersion}, currentPath=${this.currentPath}`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ this.webDavFiles = snapshot.files;
|
|
|
+ this.webDavSongs = snapshot.songs;
|
|
|
+ if (shouldCache) {
|
|
|
+ this.cacheDirectoryPreview(account, fullPath, snapshot);
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
private normalizeFullPath(path?: string): string {
|
|
|
if (!path || path.trim().length === 0) {
|
|
|
return '/';
|
|
|
@@ -1359,6 +1426,230 @@ export class RemoteDriveManager {
|
|
|
return normalized || '/';
|
|
|
}
|
|
|
|
|
|
+ private getDirectoryPreviewCacheKey(account: WebDavAccount, fullPath: string): string {
|
|
|
+ const accountKey = account?.id !== undefined && account?.id !== null ? `${account.id}` :
|
|
|
+ `${account.webType}_${account.name ?? ''}`;
|
|
|
+ return `${accountKey}:${this.normalizeFullPath(fullPath)}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ private getDirectoryPreviewStorageKey(account: WebDavAccount, fullPath: string): string {
|
|
|
+ return `${this.DIRECTORY_PREVIEW_STORE_PREFIX}${encodeURIComponent(
|
|
|
+ this.getDirectoryPreviewCacheKey(account, fullPath)
|
|
|
+ )}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ private cloneDirectoryPreviewFile(info: FileInfo): FileInfo {
|
|
|
+ const clone = new FileInfo(info.rootpath, info.name, info.totalSize, info.time);
|
|
|
+ clone.readOnly = info.readOnly;
|
|
|
+ clone.fileName = info.fileName;
|
|
|
+ clone.href = info.href;
|
|
|
+ clone.contentLength = info.contentLength;
|
|
|
+ clone.isDirectory = info.isDirectory;
|
|
|
+ return clone;
|
|
|
+ }
|
|
|
+
|
|
|
+ private cloneDirectoryPreviewSong(item: VideoItem): VideoItem {
|
|
|
+ const clone = new VideoItem(
|
|
|
+ item.name,
|
|
|
+ item.id,
|
|
|
+ item.filePath,
|
|
|
+ item.type,
|
|
|
+ item.videoSize,
|
|
|
+ item.cTime,
|
|
|
+ item.size,
|
|
|
+ item.pixelMapPath,
|
|
|
+ item.artist,
|
|
|
+ item.album,
|
|
|
+ item.fileName,
|
|
|
+ item.lastPlayed
|
|
|
+ );
|
|
|
+ clone.parentPath = item.parentPath;
|
|
|
+ clone.isFav = item.isFav;
|
|
|
+ clone.duration = item.duration;
|
|
|
+ clone.mimeType = item.mimeType;
|
|
|
+ clone.sampleRate = item.sampleRate;
|
|
|
+ clone.trackCount = item.trackCount;
|
|
|
+ clone.lastPlayedStr = item.lastPlayedStr;
|
|
|
+ clone.playCount = item.playCount;
|
|
|
+ clone.lyricContent = item.lyricContent;
|
|
|
+ clone.md5Str = item.md5Str;
|
|
|
+ clone.extra_json = item.extra_json;
|
|
|
+ clone.pyStr = item.pyStr;
|
|
|
+ clone.bit_rate = item.bit_rate;
|
|
|
+ clone.probe_score = item.probe_score;
|
|
|
+ clone.year = item.year;
|
|
|
+ clone.nb_streams = item.nb_streams;
|
|
|
+ clone.nb_programs = item.nb_programs;
|
|
|
+ clone.genre = item.genre;
|
|
|
+ clone.track = item.track;
|
|
|
+ clone.bits_per_raw_sample = item.bits_per_raw_sample;
|
|
|
+ clone.channels = item.channels;
|
|
|
+ clone.channel_layout = item.channel_layout;
|
|
|
+ clone.start_time = item.start_time;
|
|
|
+ clone.ALBUMARTIST = item.ALBUMARTIST;
|
|
|
+ clone.COMPOSER = item.COMPOSER;
|
|
|
+ clone.LYRICIST = item.LYRICIST;
|
|
|
+ clone.COMMENT = item.COMMENT;
|
|
|
+ clone.disc = item.disc;
|
|
|
+ clone.isCustomCover = item.isCustomCover;
|
|
|
+ clone.webdav_account_id = item.webdav_account_id;
|
|
|
+ clone.remote_rel_path = item.remote_rel_path;
|
|
|
+ clone.navArtistId = item.navArtistId;
|
|
|
+ clone.navAlbumId = item.navAlbumId;
|
|
|
+ clone.baiduFsId = item.baiduFsId;
|
|
|
+ clone.webdav_id = item.webdav_id;
|
|
|
+ clone.lyricIndex = item.lyricIndex;
|
|
|
+ return clone;
|
|
|
+ }
|
|
|
+
|
|
|
+ private buildDirectoryPreviewSnapshot(files: FileInfo[], songs: VideoItem[]): RemoteDirectorySnapshot {
|
|
|
+ return {
|
|
|
+ files,
|
|
|
+ folders: files.filter((item: FileInfo) => item.isDirectory),
|
|
|
+ songs
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private sortDirectoryPreviewSongs(target: VideoItem[]): VideoItem[] {
|
|
|
+ const sortType = PreferencesUtil.getNumberSync('webDavSortType', 0);
|
|
|
+ const isShowFileName = PreferencesUtil.getBooleanSync('isShowFileName', false);
|
|
|
+ switch (sortType) {
|
|
|
+ case 0:
|
|
|
+ Utility.doSortListAscending(target, isShowFileName);
|
|
|
+ break;
|
|
|
+ case 1:
|
|
|
+ Utility.doSortListDescending(target, isShowFileName);
|
|
|
+ break;
|
|
|
+ case 2:
|
|
|
+ target.sort((a: VideoItem, b: VideoItem) => a.cTime.localeCompare(b.cTime));
|
|
|
+ break;
|
|
|
+ case 3:
|
|
|
+ target.sort((a: VideoItem, b: VideoItem) => b.cTime.localeCompare(a.cTime));
|
|
|
+ break;
|
|
|
+ case 4:
|
|
|
+ target.sort((a: VideoItem, b: VideoItem) => a.videoSize - b.videoSize);
|
|
|
+ break;
|
|
|
+ case 5:
|
|
|
+ target.sort((a: VideoItem, b: VideoItem) => b.videoSize - a.videoSize);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ Utility.doSortListAscending(target, isShowFileName);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return target;
|
|
|
+ }
|
|
|
+
|
|
|
+ private sortDirectoryPreviewFiles(target: FileInfo[]): FileInfo[] {
|
|
|
+ const sortType = PreferencesUtil.getNumberSync('webDavSortType', 0);
|
|
|
+ switch (sortType) {
|
|
|
+ case 1:
|
|
|
+ target.sort((a: FileInfo, b: FileInfo) => b.fileName.localeCompare(a.fileName));
|
|
|
+ break;
|
|
|
+ case 2:
|
|
|
+ target.sort((a: FileInfo, b: FileInfo) => a.time - b.time);
|
|
|
+ break;
|
|
|
+ case 3:
|
|
|
+ target.sort((a: FileInfo, b: FileInfo) => b.time - a.time);
|
|
|
+ break;
|
|
|
+ case 0:
|
|
|
+ default:
|
|
|
+ target.sort((a: FileInfo, b: FileInfo) => a.fileName.localeCompare(b.fileName));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return target;
|
|
|
+ }
|
|
|
+
|
|
|
+ private serializeDirectoryPreviewSnapshot(snapshot: RemoteDirectorySnapshot): string {
|
|
|
+ const files = this.sortDirectoryPreviewFiles(
|
|
|
+ snapshot.files.map((item: FileInfo) => this.cloneDirectoryPreviewFile(item))
|
|
|
+ ).slice(0, this.DIRECTORY_PREVIEW_CACHE_LIMIT);
|
|
|
+ const songs = this.sortDirectoryPreviewSongs(
|
|
|
+ snapshot.songs.map((item: VideoItem) => this.cloneDirectoryPreviewSong(item))
|
|
|
+ ).slice(0, this.DIRECTORY_PREVIEW_CACHE_LIMIT);
|
|
|
+ return JSON.stringify({
|
|
|
+ files,
|
|
|
+ songs,
|
|
|
+ updatedAt: Date.now()
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private deserializeDirectoryPreviewSnapshot(serialized: string): RemoteDirectorySnapshot | undefined {
|
|
|
+ if (!serialized || serialized.length === 0) {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ const parsed = JSON.parse(serialized) as DirectoryPreviewSnapshotPayload;
|
|
|
+ const files = Array.isArray(parsed.files) ? parsed.files
|
|
|
+ .map((item: FileInfo) => this.cloneDirectoryPreviewFile(item)) : [];
|
|
|
+ const songs = Array.isArray(parsed.songs) ? parsed.songs
|
|
|
+ .map((item: VideoItem) => this.cloneDirectoryPreviewSong(item)) : [];
|
|
|
+ if (!Array.isArray(parsed.files) && !Array.isArray(parsed.songs)) {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ return this.buildDirectoryPreviewSnapshot(files, songs);
|
|
|
+ }
|
|
|
+
|
|
|
+ private loadDirectoryPreviewStorageKeys(): string[] {
|
|
|
+ try {
|
|
|
+ const raw = PreferencesUtil.getStringSync(this.DIRECTORY_PREVIEW_STORE_INDEX_KEY, '');
|
|
|
+ if (!raw || raw.length === 0) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ const parsed = JSON.parse(raw) as string[];
|
|
|
+ return Array.isArray(parsed) ? parsed.filter((item: string) => !!item && item.length > 0) : [];
|
|
|
+ } catch (error) {
|
|
|
+ Logger.warn(TAG, `读取目录预览缓存索引失败: ${(error as Error).message}`);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private saveDirectoryPreviewStorageKeys(keys: string[]): void {
|
|
|
+ PreferencesUtil.putSync(this.DIRECTORY_PREVIEW_STORE_INDEX_KEY, JSON.stringify(keys));
|
|
|
+ }
|
|
|
+
|
|
|
+ private touchDirectoryPreviewStorageKey(storageKey: string): void {
|
|
|
+ const currentKeys = this.loadDirectoryPreviewStorageKeys().filter((item: string) => item !== storageKey);
|
|
|
+ currentKeys.unshift(storageKey);
|
|
|
+ if (currentKeys.length > this.DIRECTORY_PREVIEW_STORE_LIMIT) {
|
|
|
+ const staleKeys = currentKeys.slice(this.DIRECTORY_PREVIEW_STORE_LIMIT);
|
|
|
+ for (let i = 0; i < staleKeys.length; i++) {
|
|
|
+ PreferencesUtil.deleteSync(staleKeys[i]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.saveDirectoryPreviewStorageKeys(currentKeys.slice(0, this.DIRECTORY_PREVIEW_STORE_LIMIT));
|
|
|
+ }
|
|
|
+
|
|
|
+ private cacheDirectoryPreview(account: WebDavAccount, fullPath: string, snapshot: RemoteDirectorySnapshot): void {
|
|
|
+ if (!account || !snapshot) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const storageKey = this.getDirectoryPreviewStorageKey(account, fullPath);
|
|
|
+ PreferencesUtil.putSync(storageKey, this.serializeDirectoryPreviewSnapshot(snapshot));
|
|
|
+ this.touchDirectoryPreviewStorageKey(storageKey);
|
|
|
+ } catch (error) {
|
|
|
+ Logger.warn(TAG, `写入目录预览缓存失败 path=${fullPath}, error=${(error as Error).message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public getDirectoryPreview(account: WebDavAccount, fullPath?: string): RemoteDirectorySnapshot | undefined {
|
|
|
+ if (!account) {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const storageKey = this.getDirectoryPreviewStorageKey(account,
|
|
|
+ fullPath !== undefined ? fullPath : this.currentPath || account.filepath || '/');
|
|
|
+ const serialized = PreferencesUtil.getStringSync(storageKey, '');
|
|
|
+ return this.deserializeDirectoryPreviewSnapshot(serialized);
|
|
|
+ } catch (error) {
|
|
|
+ Logger.warn(TAG, `读取目录预览缓存失败: ${(error as Error).message}`);
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public getBreadcrumbsForPreview(account: WebDavAccount, targetPath: string): BreadcrumbItem[] {
|
|
|
+ return this.getBreadcrumbsForPath(account, targetPath);
|
|
|
+ }
|
|
|
+
|
|
|
private getSongKey(song: VideoItem): string {
|
|
|
if (!song) {
|
|
|
return '';
|
|
|
@@ -1406,6 +1697,19 @@ export class RemoteDriveManager {
|
|
|
return normalized.substring(0, lastSlash);
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ private getFileNameFromRemotePath(remotePath: string): string {
|
|
|
+ if (!remotePath || remotePath === '/') {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ const normalized = this.normalizeFullPath(remotePath);
|
|
|
+ const lastSlash = normalized.lastIndexOf('/');
|
|
|
+ if (lastSlash < 0) {
|
|
|
+ return normalized;
|
|
|
+ }
|
|
|
+ return normalized.substring(lastSlash + 1);
|
|
|
+ }
|
|
|
+
|
|
|
private async ensureRemoteDirectories(account: WebDavAccount, directoryPath: string): Promise<void> {
|
|
|
if (!directoryPath || directoryPath === '/') {
|
|
|
return;
|
|
|
@@ -1485,20 +1789,22 @@ export class RemoteDriveManager {
|
|
|
return fullPath.replace(/^\/+/, '');
|
|
|
}
|
|
|
|
|
|
- private async loadWebDavFiles(account: WebDavAccount, fullPath: string): Promise<void> {
|
|
|
+ private async loadWebDavFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
|
|
|
const snapshot = await this.fetchWebDavDirectorySnapshot(account, fullPath, true);
|
|
|
- this.webDavFiles = snapshot.files;
|
|
|
- this.webDavSongs = snapshot.songs;
|
|
|
+ if (!this.applyDirectorySnapshot(account, fullPath, snapshot, requestVersion)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
Logger.info(TAG, `从WebDAV获取到 ${snapshot.files.length} 个文件/文件夹`);
|
|
|
}
|
|
|
|
|
|
- private async loadSmbFiles(account: WebDavAccount, fullPath: string): Promise<void> {
|
|
|
+ private async loadSmbFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
|
|
|
if (!account.smbShare) {
|
|
|
throw new Error('SMB账户缺少共享名称');
|
|
|
}
|
|
|
const snapshot = await this.fetchSmbDirectorySnapshot(account, fullPath, true);
|
|
|
- this.webDavFiles = snapshot.files;
|
|
|
- this.webDavSongs = snapshot.songs;
|
|
|
+ if (!this.applyDirectorySnapshot(account, fullPath, snapshot, requestVersion)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
Logger.info(TAG, `从SMB获取到 ${snapshot.files.length} 个文件/文件夹`);
|
|
|
}
|
|
|
|
|
|
@@ -1528,18 +1834,20 @@ export class RemoteDriveManager {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
- private async loadFtpFiles(account: WebDavAccount, fullPath: string): Promise<void> {
|
|
|
+ private async loadFtpFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
|
|
|
const snapshot = await this.fetchFtpDirectorySnapshot(account, fullPath, true);
|
|
|
- this.webDavFiles = snapshot.files;
|
|
|
- this.webDavSongs = snapshot.songs;
|
|
|
+ if (!this.applyDirectorySnapshot(account, fullPath, snapshot, requestVersion)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
Logger.info(TAG, `从FTP获取到 ${snapshot.files.length} 个文件/文件夹`);
|
|
|
}
|
|
|
|
|
|
- private async loadBaiduFiles(account: WebDavAccount, fullPath: string): Promise<void> {
|
|
|
+ private async loadBaiduFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
|
|
|
const accessToken = await this.ensureBaiduAccessToken(account);
|
|
|
const snapshot = await this.fetchBaiduDirectorySnapshot(account, fullPath, true, accessToken);
|
|
|
- this.webDavFiles = snapshot.files;
|
|
|
- this.webDavSongs = snapshot.songs;
|
|
|
+ if (!this.applyDirectorySnapshot(account, fullPath, snapshot, requestVersion)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
Logger.info(TAG, `从百度网盘获取到 ${snapshot.files.length} 个文件/文件夹`);
|
|
|
const rawEntries = snapshot.rawEntries ?? [];
|
|
|
this.scheduleBaiduPrefetch(account, accessToken, rawEntries);
|
|
|
@@ -2171,7 +2479,7 @@ export class RemoteDriveManager {
|
|
|
void prefetch();
|
|
|
}
|
|
|
|
|
|
- private async loadNavidromeFiles(account: WebDavAccount, fullPath: string): Promise<void> {
|
|
|
+ private async loadNavidromeFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
|
|
|
const normalized = this.normalizeFullPath(fullPath);
|
|
|
this.registerPathLabel('/', '根目录');
|
|
|
const segments = normalized.split('/').filter(part => part.length > 0);
|
|
|
@@ -2180,12 +2488,15 @@ export class RemoteDriveManager {
|
|
|
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 files: FileInfo[] = artists.map(artist => {
|
|
|
const info = this.createNavDirectory(artist.name, `/artist/${artist.id}`);
|
|
|
this.registerPathLabel(info.href, artist.name);
|
|
|
return info;
|
|
|
});
|
|
|
- this.webDavSongs = [];
|
|
|
+ const snapshot = this.buildDirectoryPreviewSnapshot(files, []);
|
|
|
+ if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
void ServerLogUtil.info(TAG, `获取艺术家列表,数量: ${artists.length}`);
|
|
|
void ServerLogUtil.debug(TAG, `artists ids: ${artists.map(a => a.id).join(',')}`)
|
|
|
return;
|
|
|
@@ -2195,12 +2506,15 @@ export class RemoteDriveManager {
|
|
|
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 files: FileInfo[] = detail.albums.map(album => {
|
|
|
const info = this.createNavDirectory(album.name, `/album/${album.id}`);
|
|
|
this.registerPathLabel(info.href, album.name);
|
|
|
return info;
|
|
|
});
|
|
|
- this.webDavSongs = [];
|
|
|
+ const snapshot = this.buildDirectoryPreviewSnapshot(files, []);
|
|
|
+ if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
void ServerLogUtil.info(TAG, `进入艺术家 ${detail.name},专辑数: ${detail.albums.length}`);
|
|
|
void ServerLogUtil.debug(TAG, `artist albums: ${detail.albums.map(a => a.id).join(',')}`)
|
|
|
return;
|
|
|
@@ -2210,11 +2524,18 @@ export class RemoteDriveManager {
|
|
|
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 files: FileInfo[] = 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);
|
|
|
+ const songs: VideoItem[] = detail.songs.map(song => this.buildNavidromeVideoItem(song, account, detail, albumCoverUrl));
|
|
|
+ if (!this.isDirectoryLoadRequestActive(requestVersion, normalized)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await this.enrichSongsWithDatabase(songs);
|
|
|
+ const snapshot = this.buildDirectoryPreviewSnapshot(files, songs);
|
|
|
+ if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
void ServerLogUtil.info(TAG, `进入专辑 ${detail.name},歌曲数: ${detail.songs.length}`);
|
|
|
void ServerLogUtil.debug(TAG, `album songs ids: ${detail.songs.map(s => s.id).join(',')}`)
|
|
|
return;
|
|
|
@@ -2223,18 +2544,24 @@ export class RemoteDriveManager {
|
|
|
throw new Error(`不支持的Navidrome路径: ${fullPath}`);
|
|
|
}
|
|
|
|
|
|
- private async loadJellyfinFiles(account: WebDavAccount, fullPath: string): Promise<void> {
|
|
|
+ private async loadJellyfinFiles(account: WebDavAccount, fullPath: string, requestVersion: number): 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}`);
|
|
|
+ const sourceSongs = await this.jellyfinApi.getAllSongs(account);
|
|
|
+ const songs: VideoItem[] = sourceSongs.map(song => this.buildJellyfinVideoItem(song, account, null, undefined));
|
|
|
+ if (!this.isDirectoryLoadRequestActive(requestVersion, normalized)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await this.enrichSongsWithDatabase(songs);
|
|
|
+ const snapshot = this.buildDirectoryPreviewSnapshot([], songs);
|
|
|
+ if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ void ServerLogUtil.info(TAG, `进入 Jellyfin 根目录,歌曲数: ${sourceSongs.length}`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -2245,12 +2572,15 @@ export class RemoteDriveManager {
|
|
|
if (artistLabel) {
|
|
|
this.registerPathLabel(`/artist/${artistId}`, artistLabel);
|
|
|
}
|
|
|
- this.webDavFiles = albums.map(album => {
|
|
|
+ const files: FileInfo[] = albums.map(album => {
|
|
|
const info = this.createJellyfinDirectory(album.name, `/album/${album.id}`);
|
|
|
this.registerPathLabel(info.href, album.name);
|
|
|
return info;
|
|
|
});
|
|
|
- this.webDavSongs = [];
|
|
|
+ const snapshot = this.buildDirectoryPreviewSnapshot(files, []);
|
|
|
+ if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
void ServerLogUtil.info(TAG, `进入 Jellyfin 艺术家 ${artistLabel},专辑数: ${albums.length}`);
|
|
|
return;
|
|
|
}
|
|
|
@@ -2267,9 +2597,16 @@ export class RemoteDriveManager {
|
|
|
} 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);
|
|
|
+ const files: FileInfo[] = songs.map(song => this.createJellyfinSongFileInfo(song, albumId));
|
|
|
+ const songItems: VideoItem[] = songs.map(song => this.buildJellyfinVideoItem(song, account, album, coverUrl));
|
|
|
+ if (!this.isDirectoryLoadRequestActive(requestVersion, normalized)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await this.enrichSongsWithDatabase(songItems);
|
|
|
+ const snapshot = this.buildDirectoryPreviewSnapshot(files, songItems);
|
|
|
+ if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
void ServerLogUtil.info(TAG, `进入 Jellyfin 专辑 ${albumName},歌曲数: ${songs.length}`);
|
|
|
return;
|
|
|
}
|
|
|
@@ -2277,18 +2614,24 @@ export class RemoteDriveManager {
|
|
|
throw new Error(`不支持的Jellyfin路径: ${fullPath}`);
|
|
|
}
|
|
|
|
|
|
- private async loadEmbyFiles(account: WebDavAccount, fullPath: string): Promise<void> {
|
|
|
+ private async loadEmbyFiles(account: WebDavAccount, fullPath: string, requestVersion: number): 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}`);
|
|
|
+ const sourceSongs = await this.embyApi.getAllSongs(account);
|
|
|
+ const songs: VideoItem[] = sourceSongs.map(song => this.buildEmbyVideoItem(song, account, null, undefined));
|
|
|
+ if (!this.isDirectoryLoadRequestActive(requestVersion, normalized)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await this.enrichSongsWithDatabase(songs);
|
|
|
+ const snapshot = this.buildDirectoryPreviewSnapshot([], songs);
|
|
|
+ if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ void ServerLogUtil.info(TAG, `进入 Emby 根目录,歌曲数: ${sourceSongs.length}`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -2299,12 +2642,15 @@ export class RemoteDriveManager {
|
|
|
if (artistLabel) {
|
|
|
this.registerPathLabel(`/artist/${artistId}`, artistLabel);
|
|
|
}
|
|
|
- this.webDavFiles = albums.map(album => {
|
|
|
+ const files: FileInfo[] = albums.map(album => {
|
|
|
const info = this.createEmbyDirectory(album.name, `/album/${album.id}`);
|
|
|
this.registerPathLabel(info.href, album.name);
|
|
|
return info;
|
|
|
});
|
|
|
- this.webDavSongs = [];
|
|
|
+ const snapshot = this.buildDirectoryPreviewSnapshot(files, []);
|
|
|
+ if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
void ServerLogUtil.info(TAG, `进入 Emby 艺术家 ${artistLabel},专辑数: ${albums.length}`);
|
|
|
return;
|
|
|
}
|
|
|
@@ -2321,9 +2667,16 @@ export class RemoteDriveManager {
|
|
|
} 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);
|
|
|
+ const files: FileInfo[] = songs.map(song => this.createEmbySongFileInfo(song, albumId));
|
|
|
+ const songItems: VideoItem[] = songs.map(song => this.buildEmbyVideoItem(song, account, album, coverUrl));
|
|
|
+ if (!this.isDirectoryLoadRequestActive(requestVersion, normalized)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await this.enrichSongsWithDatabase(songItems);
|
|
|
+ const snapshot = this.buildDirectoryPreviewSnapshot(files, songItems);
|
|
|
+ if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
void ServerLogUtil.info(TAG, `进入 Emby 专辑 ${albumName},歌曲数: ${songs.length}`);
|
|
|
return;
|
|
|
}
|
|
|
@@ -2365,10 +2718,11 @@ export class RemoteDriveManager {
|
|
|
} else if (this.isAudioFile(info.fileName)) {
|
|
|
songs.push(this.fileInfoToVideoItem(info, account));
|
|
|
}
|
|
|
+ if ((i + 1) % this.DIRECTORY_LOAD_YIELD_INTERVAL === 0) {
|
|
|
+ await this.yieldGlobalSearchBuild();
|
|
|
+ }
|
|
|
}
|
|
|
- if (enrichFromDatabase) {
|
|
|
- await this.enrichSongsWithDatabase(songs);
|
|
|
- }
|
|
|
+ await this.enrichDirectorySongsIfNeeded(songs, enrichFromDatabase);
|
|
|
return {
|
|
|
files: normalizedFiles,
|
|
|
folders: normalizedFiles.filter(file => file.isDirectory),
|
|
|
@@ -2400,10 +2754,11 @@ export class RemoteDriveManager {
|
|
|
} else if (this.isAudioFile(info.fileName)) {
|
|
|
songs.push(this.fileInfoToVideoItem(info, account));
|
|
|
}
|
|
|
+ if ((i + 1) % this.DIRECTORY_LOAD_YIELD_INTERVAL === 0) {
|
|
|
+ await this.yieldGlobalSearchBuild();
|
|
|
+ }
|
|
|
}
|
|
|
- if (enrichFromDatabase) {
|
|
|
- await this.enrichSongsWithDatabase(songs);
|
|
|
- }
|
|
|
+ await this.enrichDirectorySongsIfNeeded(songs, enrichFromDatabase);
|
|
|
return {
|
|
|
files,
|
|
|
folders: files.filter(file => file.isDirectory),
|
|
|
@@ -2439,10 +2794,11 @@ export class RemoteDriveManager {
|
|
|
} else if (this.isAudioFile(info.fileName)) {
|
|
|
songs.push(this.buildFtpVideoItem(info, account));
|
|
|
}
|
|
|
+ if ((i + 1) % this.DIRECTORY_LOAD_YIELD_INTERVAL === 0) {
|
|
|
+ await this.yieldGlobalSearchBuild();
|
|
|
+ }
|
|
|
}
|
|
|
- if (enrichFromDatabase) {
|
|
|
- await this.enrichSongsWithDatabase(songs);
|
|
|
- }
|
|
|
+ await this.enrichDirectorySongsIfNeeded(songs, enrichFromDatabase);
|
|
|
return {
|
|
|
files,
|
|
|
folders: files.filter(file => file.isDirectory),
|
|
|
@@ -2467,10 +2823,11 @@ export class RemoteDriveManager {
|
|
|
} else if (this.isAudioFile(info.fileName)) {
|
|
|
songs.push(this.buildBaiduVideoItem(entry, account));
|
|
|
}
|
|
|
+ if ((i + 1) % this.DIRECTORY_LOAD_YIELD_INTERVAL === 0) {
|
|
|
+ await this.yieldGlobalSearchBuild();
|
|
|
+ }
|
|
|
}
|
|
|
- if (enrichFromDatabase) {
|
|
|
- await this.enrichSongsWithDatabase(songs);
|
|
|
- }
|
|
|
+ await this.enrichDirectorySongsIfNeeded(songs, enrichFromDatabase);
|
|
|
return {
|
|
|
files,
|
|
|
folders: files.filter(file => file.isDirectory),
|
|
|
@@ -2479,6 +2836,20 @@ export class RemoteDriveManager {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ private async enrichDirectorySongsIfNeeded(songs: VideoItem[], enrichFromDatabase: boolean): Promise<void> {
|
|
|
+ if (!enrichFromDatabase || !songs || songs.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (songs.length <= this.DIRECTORY_METADATA_ENRICH_LIMIT) {
|
|
|
+ await this.enrichSongsWithDatabase(songs);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const initialSongs = songs.slice(0, this.DIRECTORY_METADATA_ENRICH_LIMIT);
|
|
|
+ Logger.info(TAG,
|
|
|
+ `目录歌曲较多,首屏先补全前 ${initialSongs.length}/${songs.length} 首元数据,避免进入目录卡顿`);
|
|
|
+ await this.enrichSongsWithDatabase(initialSongs);
|
|
|
+ }
|
|
|
+
|
|
|
private supportsGlobalSearchIndex(account: WebDavAccount): boolean {
|
|
|
switch (account.webType) {
|
|
|
case RemoteDriveType.WebDav:
|
|
|
@@ -4176,16 +4547,25 @@ export class RemoteDriveManager {
|
|
|
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);
|
|
|
+ let result: UploadTaskResult;
|
|
|
+ if (task.account.webType === RemoteDriveType.WebDav) {
|
|
|
+ result = await this.uploadSingleFile(task);
|
|
|
+ } else if (task.account.webType === RemoteDriveType.Baidu) {
|
|
|
+ result = await this.uploadBaiduFile(task);
|
|
|
+ } else {
|
|
|
+ throw new Error(`暂不支持该类型上传: ${task.account.webType}`);
|
|
|
}
|
|
|
|
|
|
-
|
|
|
- // 上传成功,移到完成队列
|
|
|
- Logger.info(TAG, `任务成功,移至完成队列: ${task.song.name}`);
|
|
|
this.uploadQueue.shift();
|
|
|
+ if (result.skipped) {
|
|
|
+ Logger.warn(TAG, `任务被跳过: ${task.song.name}, reason=${result.message || 'skip'}`);
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.UploadSkipped);
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
|
|
|
+ await this.processNextUploadTask();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Logger.info(TAG, `任务成功,移至完成队列: ${task.song.name}`);
|
|
|
this.finishUploadQueue.push(task);
|
|
|
this.invalidateGlobalSearchIndex(task.account);
|
|
|
this.notifyObservers(RemoteDriveManagerStates.UploadSuccess);
|
|
|
@@ -4367,7 +4747,7 @@ export class RemoteDriveManager {
|
|
|
* 上传单个文件
|
|
|
* @param task 上传任务
|
|
|
*/
|
|
|
- private async uploadSingleFile(task: TransferTask): Promise<void> {
|
|
|
+ private async uploadSingleFile(task: TransferTask): Promise<UploadTaskResult> {
|
|
|
const song = task.song;
|
|
|
const account = task.account;
|
|
|
const startTime = Date.now();
|
|
|
@@ -4424,7 +4804,11 @@ export class RemoteDriveManager {
|
|
|
|
|
|
if (duplicateAction === 'skip') {
|
|
|
Logger.warn(TAG, '文件已存在,跳过上传:', remotePath);
|
|
|
- return;
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ skipped: true,
|
|
|
+ message: `远程已存在同名文件: ${remotePath}`
|
|
|
+ };
|
|
|
} else if (duplicateAction === 'overwrite') {
|
|
|
Logger.warn(TAG, '文件已存在,将覆盖:', remotePath);
|
|
|
} else if (duplicateAction === 'rename') {
|
|
|
@@ -4476,7 +4860,15 @@ export class RemoteDriveManager {
|
|
|
Logger.info(TAG, `平均速度: ${avgSpeed} MB/s`);
|
|
|
Logger.info(TAG, `目标路径: ${remotePath}`);
|
|
|
Logger.info(TAG, '====================================');
|
|
|
-
|
|
|
+ if (account.webType === RemoteDriveType.WebDav) {
|
|
|
+ const uploadedExists = await this.checkFileExists(account, remotePath);
|
|
|
+ if (!uploadedExists) {
|
|
|
+ throw new Error(`上传请求已完成,但服务端未找到文件: ${remotePath}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ success: true
|
|
|
+ };
|
|
|
} catch (error) {
|
|
|
const err = error as Error;
|
|
|
const endTime = Date.now();
|
|
|
@@ -4507,7 +4899,7 @@ export class RemoteDriveManager {
|
|
|
* 上传百度网盘文件(使用TaskPool避免主线程阻塞)
|
|
|
* @param task 上传任务
|
|
|
*/
|
|
|
- private async uploadBaiduFile(task: TransferTask): Promise<void> {
|
|
|
+ private async uploadBaiduFile(task: TransferTask): Promise<UploadTaskResult> {
|
|
|
const song = task.song;
|
|
|
const account = task.account;
|
|
|
const startTime = Date.now();
|
|
|
@@ -4600,6 +4992,9 @@ export class RemoteDriveManager {
|
|
|
|
|
|
// 更新上传进度到100%
|
|
|
this.notifyObservers(RemoteDriveManagerStates.UploadProgress);
|
|
|
+ return {
|
|
|
+ success: true
|
|
|
+ };
|
|
|
|
|
|
} catch (error) {
|
|
|
const err = error as Error;
|
|
|
@@ -4683,8 +5078,32 @@ export class RemoteDriveManager {
|
|
|
* @param remotePath 远程路径
|
|
|
*/
|
|
|
private async checkFileExists(account: WebDavAccount, remotePath: string): Promise<boolean> {
|
|
|
+ Logger.info(TAG, `检查远程文件是否存在: ${remotePath}`);
|
|
|
+ if (account.webType === RemoteDriveType.WebDav) {
|
|
|
+ try {
|
|
|
+ const parentPath = this.getDirectoryFromRemotePath(remotePath);
|
|
|
+ const targetFileName = this.getFileNameFromRemotePath(remotePath);
|
|
|
+ const files = await this.rcpSocket.getFileList(
|
|
|
+ account.host,
|
|
|
+ account.localHost,
|
|
|
+ account.isUseLocalHost,
|
|
|
+ account.port,
|
|
|
+ parentPath,
|
|
|
+ account.account,
|
|
|
+ account.password,
|
|
|
+ account.enableHttps
|
|
|
+ );
|
|
|
+ const exists = files.some((item: FileInfo) => !item.isDirectory && item.fileName === targetFileName);
|
|
|
+ Logger.info(TAG, `WebDAV目录比对结果: ${targetFileName} => ${exists ? '存在' : '不存在'}`);
|
|
|
+ return exists;
|
|
|
+ } catch (error) {
|
|
|
+ const err = error as Error;
|
|
|
+ Logger.info(TAG, `WebDAV目录比对失败: ${err.message}, 假定文件不存在`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
- Logger.info(TAG, `检查远程文件是否存在: ${remotePath}`);
|
|
|
const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
|
|
|
const fileSize = await this.rcpSocket.RcpSendHead(
|
|
|
host,
|
|
|
@@ -4694,13 +5113,12 @@ export class RemoteDriveManager {
|
|
|
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;
|
|
|
}
|