|
|
@@ -254,6 +254,22 @@ interface WorkerMetadataPayload {
|
|
|
artist?: string;
|
|
|
album?: string;
|
|
|
name?: string;
|
|
|
+ lyricContent?: string;
|
|
|
+ bits_per_raw_sample?: string;
|
|
|
+ channels?: string;
|
|
|
+ channel_layout?: string;
|
|
|
+ start_time?: string;
|
|
|
+ genre?: string;
|
|
|
+ year?: string;
|
|
|
+ probe_score?: number;
|
|
|
+ track?: string;
|
|
|
+ disc?: string;
|
|
|
+ ALBUMARTIST?: string;
|
|
|
+ COMPOSER?: string;
|
|
|
+ COMMENT?: string;
|
|
|
+ LYRICIST?: string;
|
|
|
+ nb_streams?: number;
|
|
|
+ nb_programs?: number;
|
|
|
error?: string;
|
|
|
}
|
|
|
|
|
|
@@ -358,6 +374,7 @@ function cloneVideoItem(item: VideoItem): VideoItem {
|
|
|
copy.disc = item.disc;
|
|
|
copy.webdav_account_id = item.webdav_account_id;
|
|
|
copy.remote_rel_path = item.remote_rel_path;
|
|
|
+ copy.dbFilePath = item.dbFilePath;
|
|
|
return copy;
|
|
|
}
|
|
|
|
|
|
@@ -1420,6 +1437,8 @@ export struct LocalMusic {
|
|
|
collectFromList(this.videoLocalList);
|
|
|
collectFromList(this.historyList);
|
|
|
collectFromList(this.favList);
|
|
|
+ const remoteManager = RemoteDriveManager.getInstance();
|
|
|
+ collectFromList(remoteManager.webDavSongs);
|
|
|
|
|
|
const updated = new Set<VideoItem>();
|
|
|
for (let i = 0; i < targets.length; i++) {
|
|
|
@@ -1428,42 +1447,11 @@ export struct LocalMusic {
|
|
|
continue;
|
|
|
}
|
|
|
updated.add(target);
|
|
|
- if (payload.name) {
|
|
|
- target.name = payload.name;
|
|
|
- }
|
|
|
- if (payload.artist) {
|
|
|
- target.artist = payload.artist;
|
|
|
- }
|
|
|
- if (payload.album) {
|
|
|
- target.album = payload.album;
|
|
|
- }
|
|
|
- if (payload.bit_rate) {
|
|
|
- target.bit_rate = payload.bit_rate;
|
|
|
- }
|
|
|
- if (payload.duration) {
|
|
|
- target.duration = payload.duration;
|
|
|
- }
|
|
|
- if (payload.sampleRate) {
|
|
|
- target.sampleRate = payload.sampleRate;
|
|
|
- }
|
|
|
- if (payload.mimeType) {
|
|
|
- target.mimeType = payload.mimeType;
|
|
|
- }
|
|
|
- if (payload.md5Str) {
|
|
|
- target.md5Str = payload.md5Str;
|
|
|
- }
|
|
|
- if (payload.videoSize !== undefined) {
|
|
|
- target.videoSize = payload.videoSize;
|
|
|
- }
|
|
|
- if (payload.size) {
|
|
|
- target.size = payload.size;
|
|
|
- }
|
|
|
- if (payload.pixelMapPath) {
|
|
|
- target.pixelMapPath = payload.pixelMapPath;
|
|
|
- }
|
|
|
+ this.mergeVideoItemWithPayload(target, payload);
|
|
|
}
|
|
|
|
|
|
if (this.currentSong && this.currentSong.filePath === filePath) {
|
|
|
+ this.mergeVideoItemWithPayload(this.currentSong, payload);
|
|
|
if (payload.name) {
|
|
|
this.name = payload.name;
|
|
|
}
|
|
|
@@ -1473,9 +1461,193 @@ export struct LocalMusic {
|
|
|
if (payload.pixelMapPath) {
|
|
|
this.cover = payload.pixelMapPath;
|
|
|
}
|
|
|
+ if (payload.lyricContent) {
|
|
|
+ this.applyInlineLyricContent(payload.lyricContent);
|
|
|
+ }
|
|
|
this.currentSong = cloneVideoItem(this.currentSong);
|
|
|
AppStorage.setOrCreate('currentSong', this.currentSong);
|
|
|
}
|
|
|
+
|
|
|
+ const primaryTarget = targets.length > 0 ? targets[0] : undefined;
|
|
|
+ if (primaryTarget) {
|
|
|
+ this.persistRemoteMetadataToDb(primaryTarget).catch((error: Error) => {
|
|
|
+ Logger.error(TAG, `缓存元数据持久化失败: ${error.message}`);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private assignStringMetadata(target: VideoItem, key: keyof VideoItem, value?: string | null, allowEmpty = false): void {
|
|
|
+ if (value === undefined || value === null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const text = value.toString();
|
|
|
+ if (!allowEmpty && text.trim().length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Reflect.set(target, key as string, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private assignNumberMetadata(target: VideoItem, key: keyof VideoItem, value?: number | null): void {
|
|
|
+ if (value === undefined || value === null || Number.isNaN(value)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Reflect.set(target, key as string, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private mergeVideoItemWithPayload(target: VideoItem, payload: WorkerMetadataPayload): void {
|
|
|
+ if (!target || !payload) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.assignStringMetadata(target, 'name', payload.name);
|
|
|
+ this.assignStringMetadata(target, 'artist', payload.artist);
|
|
|
+ this.assignStringMetadata(target, 'album', payload.album);
|
|
|
+ this.assignStringMetadata(target, 'bit_rate', payload.bit_rate);
|
|
|
+ this.assignStringMetadata(target, 'duration', payload.duration);
|
|
|
+ this.assignStringMetadata(target, 'sampleRate', payload.sampleRate);
|
|
|
+ this.assignStringMetadata(target, 'mimeType', payload.mimeType);
|
|
|
+ this.assignStringMetadata(target, 'size', payload.size);
|
|
|
+ this.assignStringMetadata(target, 'pixelMapPath', payload.pixelMapPath);
|
|
|
+ this.assignStringMetadata(target, 'lyricContent', payload.lyricContent, true);
|
|
|
+ this.assignStringMetadata(target, 'genre', payload.genre);
|
|
|
+ this.assignStringMetadata(target, 'track', payload.track);
|
|
|
+ this.assignStringMetadata(target, 'bits_per_raw_sample', payload.bits_per_raw_sample);
|
|
|
+ this.assignStringMetadata(target, 'channels', payload.channels);
|
|
|
+ this.assignStringMetadata(target, 'channel_layout', payload.channel_layout);
|
|
|
+ this.assignStringMetadata(target, 'start_time', payload.start_time);
|
|
|
+ this.assignStringMetadata(target, 'year', payload.year);
|
|
|
+ this.assignStringMetadata(target, 'disc', payload.disc);
|
|
|
+ this.assignStringMetadata(target, 'ALBUMARTIST', payload.ALBUMARTIST);
|
|
|
+ this.assignStringMetadata(target, 'COMPOSER', payload.COMPOSER);
|
|
|
+ this.assignStringMetadata(target, 'LYRICIST', payload.LYRICIST);
|
|
|
+ this.assignStringMetadata(target, 'COMMENT', payload.COMMENT, true);
|
|
|
+ this.assignNumberMetadata(target, 'videoSize', payload.videoSize);
|
|
|
+ this.assignNumberMetadata(target, 'probe_score', payload.probe_score);
|
|
|
+ this.assignNumberMetadata(target, 'nb_streams', payload.nb_streams);
|
|
|
+ this.assignNumberMetadata(target, 'nb_programs', payload.nb_programs);
|
|
|
+ }
|
|
|
+
|
|
|
+ private mergeVideoItemFromSource(target: VideoItem, source: VideoItem): void {
|
|
|
+ if (!target || !source) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.assignStringMetadata(target, 'name', source.name);
|
|
|
+ this.assignStringMetadata(target, 'artist', source.artist);
|
|
|
+ this.assignStringMetadata(target, 'album', source.album);
|
|
|
+ this.assignStringMetadata(target, 'bit_rate', source.bit_rate);
|
|
|
+ this.assignStringMetadata(target, 'duration', source.duration);
|
|
|
+ this.assignStringMetadata(target, 'sampleRate', source.sampleRate);
|
|
|
+ this.assignStringMetadata(target, 'trackCount', source.trackCount);
|
|
|
+ this.assignStringMetadata(target, 'mimeType', source.mimeType);
|
|
|
+ this.assignStringMetadata(target, 'size', source.size);
|
|
|
+ this.assignStringMetadata(target, 'pixelMapPath', source.pixelMapPath);
|
|
|
+ this.assignStringMetadata(target, 'lyricContent', source.lyricContent, true);
|
|
|
+ this.assignStringMetadata(target, 'genre', source.genre);
|
|
|
+ this.assignStringMetadata(target, 'track', source.track);
|
|
|
+ this.assignStringMetadata(target, 'bits_per_raw_sample', source.bits_per_raw_sample);
|
|
|
+ this.assignStringMetadata(target, 'channels', source.channels);
|
|
|
+ this.assignStringMetadata(target, 'channel_layout', source.channel_layout);
|
|
|
+ this.assignStringMetadata(target, 'start_time', source.start_time);
|
|
|
+ this.assignStringMetadata(target, 'year', source.year);
|
|
|
+ this.assignStringMetadata(target, 'disc', source.disc);
|
|
|
+ this.assignStringMetadata(target, 'ALBUMARTIST', source.ALBUMARTIST);
|
|
|
+ this.assignStringMetadata(target, 'COMPOSER', source.COMPOSER);
|
|
|
+ this.assignStringMetadata(target, 'LYRICIST', source.LYRICIST);
|
|
|
+ this.assignStringMetadata(target, 'COMMENT', source.COMMENT, true);
|
|
|
+ this.assignNumberMetadata(target, 'videoSize', source.videoSize);
|
|
|
+ this.assignNumberMetadata(target, 'probe_score', source.probe_score);
|
|
|
+ this.assignNumberMetadata(target, 'nb_streams', source.nb_streams);
|
|
|
+ this.assignNumberMetadata(target, 'nb_programs', source.nb_programs);
|
|
|
+ if (source.pixelMap && !target.pixelMap) {
|
|
|
+ target.pixelMap = source.pixelMap;
|
|
|
+ }
|
|
|
+ if (source.remote_rel_path && StrUtil.isEmpty(target.remote_rel_path)) {
|
|
|
+ target.remote_rel_path = source.remote_rel_path;
|
|
|
+ }
|
|
|
+ if (source.dbFilePath) {
|
|
|
+ target.dbFilePath = source.dbFilePath;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async hydrateRemoteSongFromDb(item: VideoItem | undefined): Promise<void> {
|
|
|
+ if (!item || !isRemoteCloudType(item.type)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const storagePath = item.remote_rel_path || WebDavUrlUtil.toStoragePath(item.filePath);
|
|
|
+ if (!storagePath) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const cached = await this.table.queryVideoByFilePath(storagePath);
|
|
|
+ if (!cached) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.mergeVideoItemFromSource(item, cached);
|
|
|
+ if (StrUtil.isEmpty(item.remote_rel_path)) {
|
|
|
+ item.remote_rel_path = storagePath;
|
|
|
+ }
|
|
|
+ item.dbFilePath = storagePath;
|
|
|
+ if (item === this.currentSong) {
|
|
|
+ if (StrUtil.isNotEmpty(item.name)) {
|
|
|
+ this.name = item.name;
|
|
|
+ }
|
|
|
+ if (StrUtil.isNotEmpty(item.artist)) {
|
|
|
+ this.artist = item.artist!;
|
|
|
+ }
|
|
|
+ if (StrUtil.isNotEmpty(item.pixelMapPath)) {
|
|
|
+ this.cover = item.pixelMapPath!;
|
|
|
+ }
|
|
|
+ if (StrUtil.isNotEmpty(item.lyricContent) && StrUtil.isEmpty(this.lyricContent)) {
|
|
|
+ this.applyInlineLyricContent(item.lyricContent as string);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ Logger.error(TAG, `hydrateRemoteSongFromDb 失败: ${(error as Error).message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private applyInlineLyricContent(content: string): void {
|
|
|
+ if (StrUtil.isEmpty(content)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.lyricContent = content;
|
|
|
+ const lines = content.split('\n').map(line => line.trim());
|
|
|
+ const lyric = this.parser.parse(lines);
|
|
|
+ this.lyricController.setLyric(lyric);
|
|
|
+ this.lyricControllerXF.setLyric(lyric);
|
|
|
+ this.lyricControllerSingle.setLyric(lyric);
|
|
|
+ }
|
|
|
+
|
|
|
+ private extractParentPathFromStoragePath(path: string): string {
|
|
|
+ if (!path) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ const lastSlash = path.lastIndexOf('/');
|
|
|
+ if (lastSlash <= 0) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ return path.substring(0, lastSlash);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async persistRemoteMetadataToDb(target: VideoItem): Promise<void> {
|
|
|
+ if (!this.context || !target || !isWebDavType(target.type)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const storagePath = target.remote_rel_path || WebDavUrlUtil.toStoragePath(target.filePath);
|
|
|
+ if (!storagePath) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ target.remote_rel_path = target.remote_rel_path || storagePath;
|
|
|
+ target.dbFilePath = storagePath;
|
|
|
+ const table = new MediaTable(this.context);
|
|
|
+ await new Promise<void>((resolve) => {
|
|
|
+ table.getRdbStore(this.context as Context, () => resolve());
|
|
|
+ });
|
|
|
+ const cloneItem = cloneVideoItem(target);
|
|
|
+ cloneItem.filePath = storagePath;
|
|
|
+ cloneItem.remote_rel_path = storagePath;
|
|
|
+ cloneItem.dbFilePath = storagePath;
|
|
|
+ cloneItem.parentPath = this.extractParentPathFromStoragePath(storagePath);
|
|
|
+ await table.saveOrUpdateWebDavItem(cloneItem);
|
|
|
}
|
|
|
|
|
|
onPageHide(): void {
|
|
|
@@ -6395,6 +6567,8 @@ export struct LocalMusic {
|
|
|
this.sonDataSource.pushArrayData(this.songList)
|
|
|
}
|
|
|
|
|
|
+ await this.hydrateRemoteSongFromDb(this.currentSong);
|
|
|
+ await this.hydrateRemoteSongFromDb(this.currentSong);
|
|
|
AppStorage.setOrCreate('currentSong', this.currentSong);
|
|
|
|
|
|
// 使用辅助方法设置videoUrl,等待WebDAV URL构建完成
|