|
|
@@ -17,6 +17,7 @@ import { GlobalContext } from '@pura/harmony-utils';
|
|
|
import { MusicInfo, parseMusicFileName, Utility } from './Utility';
|
|
|
import { RemoteDriveType } from '../enums/RemoteDriveType';
|
|
|
import { listSmbDirectory, SmbDirectoryEntry } from '../network/SmbBridge';
|
|
|
+import { NavidromeApi, NavidromeAlbumDetail, NavidromeArtist, NavidromeArtistDetail, NavidromeSong } from '../network/NavidromeApi';
|
|
|
|
|
|
const TAG = 'heanup RemoteDriveManager';
|
|
|
|
|
|
@@ -78,6 +79,10 @@ export class RemoteDriveManager {
|
|
|
// 上传队列
|
|
|
public uploadQueue: TransferTask[] = [];
|
|
|
public finishUploadQueue: TransferTask[] = [];
|
|
|
+
|
|
|
+ private navidromeApi: NavidromeApi = new NavidromeApi();
|
|
|
+ private pathDisplayNames: Map<string, string> = new Map();
|
|
|
+ private lastAccountId: number | null = null;
|
|
|
public isProcessingUploadQueue: boolean = false;
|
|
|
public currentUploadTask: TransferTask | null = null;
|
|
|
public isPauseUpload: boolean = true;
|
|
|
@@ -164,12 +169,19 @@ export class RemoteDriveManager {
|
|
|
account.webType = resultSet.getLong(resultSet.getColumnIndex('webType'));
|
|
|
const smbShareIndex = resultSet.getColumnIndex('smbShare');
|
|
|
const smbDomainIndex = resultSet.getColumnIndex('smbDomain');
|
|
|
+ const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
|
|
|
if (smbShareIndex >= 0) {
|
|
|
account.smbShare = resultSet.getString(smbShareIndex) ?? '';
|
|
|
}
|
|
|
if (smbDomainIndex >= 0) {
|
|
|
account.smbDomain = resultSet.getString(smbDomainIndex) ?? '';
|
|
|
}
|
|
|
+ if (navBaseIndex >= 0) {
|
|
|
+ const stored = resultSet.getString(navBaseIndex);
|
|
|
+ account.navidromeBasePath = stored && stored.length > 0 ? stored : '/rest';
|
|
|
+ } else {
|
|
|
+ account.navidromeBasePath = '/rest';
|
|
|
+ }
|
|
|
account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
|
|
|
|
|
|
resultSet.close();
|
|
|
@@ -430,7 +442,8 @@ export class RemoteDriveManager {
|
|
|
coverPath TEXT,
|
|
|
webType INTEGER DEFAULT 0,
|
|
|
smbShare TEXT,
|
|
|
- smbDomain TEXT
|
|
|
+ smbDomain TEXT,
|
|
|
+ navidromeBasePath TEXT
|
|
|
)`;
|
|
|
|
|
|
return this.dataBaseUtil.executeSql(createTableSql)
|
|
|
@@ -489,6 +502,15 @@ export class RemoteDriveManager {
|
|
|
} catch (error) {
|
|
|
Logger.info(TAG, 'smbDomain字段可能已存在或添加失败,继续运行');
|
|
|
}
|
|
|
+
|
|
|
+ try {
|
|
|
+ Logger.info(TAG, '尝试添加navidromeBasePath字段...');
|
|
|
+ const addNavBaseSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN navidromeBasePath TEXT`;
|
|
|
+ await this.dataBaseUtil.executeSql(addNavBaseSql);
|
|
|
+ Logger.info(TAG, 'navidromeBasePath字段添加成功');
|
|
|
+ } catch (error) {
|
|
|
+ Logger.info(TAG, 'navidromeBasePath字段可能已存在或添加失败,继续运行');
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 从数据库查询所有账户
|
|
|
@@ -501,7 +523,7 @@ export class RemoteDriveManager {
|
|
|
// 先查询基础字段(确保这些字段在旧版本中存在)
|
|
|
const baseColumns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
|
|
|
'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
|
|
|
- 'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain'];
|
|
|
+ 'account', 'password', 'enableHttps', 'coverPath', 'webType', 'smbShare', 'smbDomain', 'navidromeBasePath'];
|
|
|
|
|
|
const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, baseColumns, predicates);
|
|
|
|
|
|
@@ -537,6 +559,13 @@ export class RemoteDriveManager {
|
|
|
if (smbDomainIndex >= 0) {
|
|
|
account.smbDomain = resultSet.getString(smbDomainIndex) ?? '';
|
|
|
}
|
|
|
+ const navBaseIndex = resultSet.getColumnIndex('navidromeBasePath');
|
|
|
+ if (navBaseIndex >= 0) {
|
|
|
+ const stored = resultSet.getString(navBaseIndex);
|
|
|
+ account.navidromeBasePath = stored && stored.length > 0 ? stored : '/rest';
|
|
|
+ } else {
|
|
|
+ account.navidromeBasePath = '/rest';
|
|
|
+ }
|
|
|
|
|
|
this.webDavAccounts.push(account);
|
|
|
}
|
|
|
@@ -569,7 +598,8 @@ export class RemoteDriveManager {
|
|
|
coverPath?: string,
|
|
|
webType: number = 0,
|
|
|
smbShare: string = '',
|
|
|
- smbDomain: string = ''
|
|
|
+ smbDomain: string = '',
|
|
|
+ navidromeBasePath: string = '/rest'
|
|
|
): Promise<void> {
|
|
|
try {
|
|
|
const values: relationalStore.ValuesBucket = {
|
|
|
@@ -589,7 +619,8 @@ export class RemoteDriveManager {
|
|
|
'coverPath': coverPath || null,
|
|
|
'webType': webType,
|
|
|
'smbShare': smbShare,
|
|
|
- 'smbDomain': smbDomain
|
|
|
+ 'smbDomain': smbDomain,
|
|
|
+ 'navidromeBasePath': navidromeBasePath
|
|
|
};
|
|
|
|
|
|
await this.dataBaseUtil.insertData(this.webDavTable, values);
|
|
|
@@ -625,7 +656,8 @@ export class RemoteDriveManager {
|
|
|
'coverPath': account.coverPath || null,
|
|
|
'webType': account.webType,
|
|
|
'smbShare': account.smbShare,
|
|
|
- 'smbDomain': account.smbDomain
|
|
|
+ 'smbDomain': account.smbDomain,
|
|
|
+ 'navidromeBasePath': account.navidromeBasePath
|
|
|
};
|
|
|
|
|
|
const predicates = new relationalStore.RdbPredicates(this.webDavTable);
|
|
|
@@ -697,6 +729,12 @@ export class RemoteDriveManager {
|
|
|
|
|
|
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) {
|
|
|
@@ -712,6 +750,8 @@ export class RemoteDriveManager {
|
|
|
|
|
|
if (account.webType === RemoteDriveType.Smb) {
|
|
|
await this.loadSmbFiles(account, normalizedFullPath);
|
|
|
+ } else if (account.webType === RemoteDriveType.Navidrome) {
|
|
|
+ await this.loadNavidromeFiles(account, normalizedFullPath);
|
|
|
} else {
|
|
|
await this.loadWebDavFiles(account, normalizedFullPath);
|
|
|
}
|
|
|
@@ -798,6 +838,51 @@ export class RemoteDriveManager {
|
|
|
Logger.info(TAG, `从SMB获取到 ${entries.length} 个文件/文件夹`);
|
|
|
}
|
|
|
|
|
|
+ private async loadNavidromeFiles(account: WebDavAccount, fullPath: string): Promise<void> {
|
|
|
+ const normalized = this.normalizeFullPath(fullPath);
|
|
|
+ this.registerPathLabel('/', '根目录');
|
|
|
+ const segments = normalized.split('/').filter(part => part.length > 0);
|
|
|
+
|
|
|
+ if (normalized === '/' || segments.length === 0) {
|
|
|
+ const artists: NavidromeArtist[] = await this.navidromeApi.getArtists(account);
|
|
|
+ artists.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
+ this.webDavFiles = artists.map(artist => {
|
|
|
+ const info = this.createNavDirectory(artist.name, `/artist/${artist.id}`);
|
|
|
+ this.registerPathLabel(info.href, artist.name);
|
|
|
+ return info;
|
|
|
+ });
|
|
|
+ this.webDavSongs = [];
|
|
|
+ Logger.info(TAG, `从Navidrome获取到 ${artists.length} 位艺术家`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (segments[0] === 'artist' && segments[1]) {
|
|
|
+ const artistId = segments[1];
|
|
|
+ const detail: NavidromeArtistDetail = await this.navidromeApi.getArtist(account, artistId);
|
|
|
+ this.registerPathLabel(`/artist/${artistId}`, detail.name);
|
|
|
+ this.webDavFiles = detail.albums.map(album => {
|
|
|
+ const info = this.createNavDirectory(album.name, `/album/${album.id}`);
|
|
|
+ this.registerPathLabel(info.href, album.name);
|
|
|
+ return info;
|
|
|
+ });
|
|
|
+ this.webDavSongs = [];
|
|
|
+ Logger.info(TAG, `Navidrome 艺术家 ${detail.name} 包含 ${detail.albums.length} 张专辑`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (segments[0] === 'album' && segments[1]) {
|
|
|
+ const albumId = segments[1];
|
|
|
+ const detail: NavidromeAlbumDetail = await this.navidromeApi.getAlbum(account, albumId);
|
|
|
+ this.registerPathLabel(`/album/${albumId}`, detail.name);
|
|
|
+ this.webDavFiles = detail.songs.map(song => this.createNavSongFileInfo(song, albumId));
|
|
|
+ this.webDavSongs = detail.songs.map(song => this.buildNavidromeVideoItem(song, account, detail));
|
|
|
+ Logger.info(TAG, `Navidrome 专辑 ${detail.name} 包含 ${detail.songs.length} 首歌曲`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error(`不支持的Navidrome路径: ${fullPath}`);
|
|
|
+ }
|
|
|
+
|
|
|
// 将FileInfo转换为VideoItem
|
|
|
private fileInfoToVideoItem(fileInfo: FileInfo, account: WebDavAccount): VideoItem {
|
|
|
if (account.webType === RemoteDriveType.Smb) {
|
|
|
@@ -930,6 +1015,57 @@ export class RemoteDriveManager {
|
|
|
return fullPath;
|
|
|
}
|
|
|
|
|
|
+ private createNavDirectory(name: string, href: string): FileInfo {
|
|
|
+ const info = new FileInfo('', name, 0, Date.now());
|
|
|
+ info.fileName = name;
|
|
|
+ info.href = this.normalizeFullPath(href);
|
|
|
+ info.isDirectory = true;
|
|
|
+ return info;
|
|
|
+ }
|
|
|
+
|
|
|
+ private createNavSongFileInfo(song: NavidromeSong, albumId: string): FileInfo {
|
|
|
+ const displayName = song.title ?? '未知曲目';
|
|
|
+ const info = new FileInfo('', displayName, song.size ?? 0, Date.now());
|
|
|
+ info.fileName = `${displayName}${song.suffix ? '.' + song.suffix : ''}`;
|
|
|
+ info.href = this.normalizeFullPath(`/album/${albumId}/${song.id}`);
|
|
|
+ info.isDirectory = false;
|
|
|
+ info.contentLength = song.size ?? 0;
|
|
|
+ return info;
|
|
|
+ }
|
|
|
+
|
|
|
+ private buildNavidromeVideoItem(song: NavidromeSong, account: WebDavAccount, album?: NavidromeAlbumDetail): VideoItem {
|
|
|
+ const title = song.title ?? '未知曲目';
|
|
|
+ const fileName = `${title}${song.suffix ? '.' + song.suffix : ''}`;
|
|
|
+ const videoItem = new VideoItem(
|
|
|
+ title,
|
|
|
+ song.id,
|
|
|
+ `navidrome://${account.id ?? 0}/${song.id}`,
|
|
|
+ CommonConstants.TYPE_NAVIDROME,
|
|
|
+ song.size ?? 0,
|
|
|
+ Utility.getFormatDateStr(Date.now(), 'yyyy-MM-dd HH:mm'),
|
|
|
+ undefined,
|
|
|
+ Utility.formatFSize(song.size ?? 0),
|
|
|
+ undefined,
|
|
|
+ song.artist ?? album?.artist ?? Constants.UNKNOWN_ARTIST,
|
|
|
+ album?.name ?? song.album,
|
|
|
+ fileName
|
|
|
+ );
|
|
|
+ videoItem.size = Utility.formatFSize(song.size ?? 0);
|
|
|
+ videoItem.webdav_account_id = account.id?.toString();
|
|
|
+ videoItem.remote_rel_path = song.id;
|
|
|
+ videoItem.bit_rate = song.bitRate ? song.bitRate.toString() : undefined;
|
|
|
+ videoItem.mimeType = song.contentType;
|
|
|
+ return videoItem;
|
|
|
+ }
|
|
|
+
|
|
|
+ private registerPathLabel(path: string, label: string): void {
|
|
|
+ if (!path || !label) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const normalized = this.normalizeFullPath(path);
|
|
|
+ this.pathDisplayNames.set(normalized, label);
|
|
|
+ }
|
|
|
+
|
|
|
private normalizeSmbRelativePath(path: string, shareName?: string): string {
|
|
|
if (!path || path.trim().length === 0) {
|
|
|
return '/';
|
|
|
@@ -999,6 +1135,7 @@ export class RemoteDriveManager {
|
|
|
Logger.info(TAG, '目标路径:', folder.href);
|
|
|
|
|
|
// 保存当前路径到历史记录
|
|
|
+ this.registerPathLabel(folder.href, folder.fileName);
|
|
|
this.pathHistory.push(this.currentPath);
|
|
|
Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
|
|
|
|
|
|
@@ -1073,8 +1210,11 @@ export class RemoteDriveManager {
|
|
|
const parts = this.currentPath.split('/').filter(part => part !== '');
|
|
|
const breadcrumbs = ['根目录'];
|
|
|
|
|
|
+ let cumulative = '';
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
|
- breadcrumbs.push(parts[i]);
|
|
|
+ cumulative += `/${parts[i]}`;
|
|
|
+ const label = this.pathDisplayNames.get(cumulative) ?? parts[i];
|
|
|
+ breadcrumbs.push(label);
|
|
|
}
|
|
|
|
|
|
return breadcrumbs;
|