|
@@ -22,8 +22,13 @@ import { Song } from '../../viewmodel/Song';
|
|
|
import { repairAudioMetadata } from './MusicTagUtils';
|
|
import { repairAudioMetadata } from './MusicTagUtils';
|
|
|
import { ServerLogUtil } from './ServerLogUtil';
|
|
import { ServerLogUtil } from './ServerLogUtil';
|
|
|
import { CommonConstants } from '../constants/CommonConstants';
|
|
import { CommonConstants } from '../constants/CommonConstants';
|
|
|
|
|
+import { Utility } from './Utility';
|
|
|
|
|
+import MediaTable from './MediaTable';
|
|
|
|
|
+import emitter from '@ohos.events.emitter';
|
|
|
|
|
+import { EventConstants } from '../constants/EventConstants';
|
|
|
|
|
|
|
|
const TAG = 'heanup RemotePlayerUtil';
|
|
const TAG = 'heanup RemotePlayerUtil';
|
|
|
|
|
+const metadataExtractionInFlight: Map<string, Promise<void>> = new Map();
|
|
|
|
|
|
|
|
|
|
|
|
|
export interface MetadataExtractionOptions {
|
|
export interface MetadataExtractionOptions {
|
|
@@ -253,12 +258,133 @@ export async function setVideoUrlForSong(
|
|
|
expectedSize?: number
|
|
expectedSize?: number
|
|
|
): Promise<void> => {
|
|
): Promise<void> => {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ if (!opts.context) {
|
|
|
|
|
+ Logger.info(TAG, '缺少上下文,跳过缓存元数据解析');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const shouldExtractCover = opts.extractCover !== false;
|
|
|
|
|
+ const shouldExtractLyric = opts.extractLyric !== false;
|
|
|
|
|
+ const shouldExtractAudioInfo = opts.extractAudioInfo !== false;
|
|
|
|
|
+ if (!shouldExtractCover && !shouldExtractLyric && !shouldExtractAudioInfo && !opts.autoParseMusicName) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
const size = await FileManager.getFileSize(cachePath);
|
|
const size = await FileManager.getFileSize(cachePath);
|
|
|
if (expectedSize && size < expectedSize * 0.95) {
|
|
if (expectedSize && size < expectedSize * 0.95) {
|
|
|
Logger.warn(TAG, `缓存文件大小不匹配,跳过元数据提取: ${cachePath}`);
|
|
Logger.warn(TAG, `缓存文件大小不匹配,跳过元数据提取: ${cachePath}`);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- Logger.info(TAG, `从缓存提取元数据: ${cachePath}`);
|
|
|
|
|
|
|
+ const taskKey = `${cachePath}::${targetSong.filePath ?? targetSong.remote_rel_path ?? ''}`;
|
|
|
|
|
+ if (metadataExtractionInFlight.has(taskKey)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const task = (async () => {
|
|
|
|
|
+ Logger.info(TAG, `从缓存提取元数据: ${cachePath}`);
|
|
|
|
|
+ const originName = targetSong.name || targetSong.fileName;
|
|
|
|
|
+ const metadataItem = await Utility.uriGetMusicAssetsFromFile(
|
|
|
|
|
+ opts.context as Context,
|
|
|
|
|
+ cachePath,
|
|
|
|
|
+ targetSong.type ?? CommonConstants.TYPE_LOCAL,
|
|
|
|
|
+ opts.autoParseMusicName ?? false,
|
|
|
|
|
+ originName
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const applyStringIfEmpty = (value?: string, fallback?: string): string | undefined => {
|
|
|
|
|
+ if (value && value.length > 0) {
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fallback && fallback.length > 0) {
|
|
|
|
|
+ return fallback;
|
|
|
|
|
+ }
|
|
|
|
|
+ return value;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const applyNumberIfEmpty = (value?: number, fallback?: number): number | undefined => {
|
|
|
|
|
+ if (value !== undefined && value !== null && value !== 0) {
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fallback !== undefined && fallback !== null && fallback !== 0) {
|
|
|
|
|
+ return fallback;
|
|
|
|
|
+ }
|
|
|
|
|
+ return value;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (shouldExtractCover && metadataItem.pixelMapPath) {
|
|
|
|
|
+ targetSong.pixelMapPath = metadataItem.pixelMapPath;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (shouldExtractLyric && metadataItem.lyricContent) {
|
|
|
|
|
+ targetSong.lyricContent = metadataItem.lyricContent;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (shouldExtractAudioInfo) {
|
|
|
|
|
+ targetSong.duration = applyStringIfEmpty(targetSong.duration, metadataItem.duration) ?? targetSong.duration;
|
|
|
|
|
+ targetSong.bit_rate = applyStringIfEmpty(targetSong.bit_rate, metadataItem.bit_rate) ?? targetSong.bit_rate;
|
|
|
|
|
+ targetSong.sampleRate = applyStringIfEmpty(targetSong.sampleRate, metadataItem.sampleRate) ?? targetSong.sampleRate;
|
|
|
|
|
+ targetSong.trackCount = applyStringIfEmpty(targetSong.trackCount, metadataItem.trackCount) ?? targetSong.trackCount;
|
|
|
|
|
+ targetSong.mimeType = applyStringIfEmpty(targetSong.mimeType, metadataItem.mimeType) ?? targetSong.mimeType;
|
|
|
|
|
+ targetSong.md5Str = applyStringIfEmpty(targetSong.md5Str, metadataItem.md5Str) ?? targetSong.md5Str;
|
|
|
|
|
+ targetSong.size = applyStringIfEmpty(targetSong.size, metadataItem.size) ?? targetSong.size;
|
|
|
|
|
+ targetSong.videoSize = applyNumberIfEmpty(targetSong.videoSize, metadataItem.videoSize) ?? targetSong.videoSize;
|
|
|
|
|
+ targetSong.bits_per_raw_sample = applyStringIfEmpty(targetSong.bits_per_raw_sample, metadataItem.bits_per_raw_sample)
|
|
|
|
|
+ ?? targetSong.bits_per_raw_sample;
|
|
|
|
|
+ targetSong.channels = applyStringIfEmpty(targetSong.channels, metadataItem.channels) ?? targetSong.channels;
|
|
|
|
|
+ targetSong.channel_layout = applyStringIfEmpty(targetSong.channel_layout, metadataItem.channel_layout) ?? targetSong.channel_layout;
|
|
|
|
|
+ targetSong.start_time = applyStringIfEmpty(targetSong.start_time, metadataItem.start_time) ?? targetSong.start_time;
|
|
|
|
|
+ targetSong.genre = applyStringIfEmpty(targetSong.genre, metadataItem.genre) ?? targetSong.genre;
|
|
|
|
|
+ targetSong.year = applyStringIfEmpty(targetSong.year, metadataItem.year) ?? targetSong.year;
|
|
|
|
|
+ targetSong.probe_score = applyNumberIfEmpty(targetSong.probe_score, metadataItem.probe_score) ?? targetSong.probe_score;
|
|
|
|
|
+ targetSong.track = applyStringIfEmpty(targetSong.track, metadataItem.track) ?? targetSong.track;
|
|
|
|
|
+ targetSong.disc = applyStringIfEmpty(targetSong.disc, metadataItem.disc) ?? targetSong.disc;
|
|
|
|
|
+ targetSong.ALBUMARTIST = applyStringIfEmpty(targetSong.ALBUMARTIST, metadataItem.ALBUMARTIST) ?? targetSong.ALBUMARTIST;
|
|
|
|
|
+ targetSong.COMPOSER = applyStringIfEmpty(targetSong.COMPOSER, metadataItem.COMPOSER) ?? targetSong.COMPOSER;
|
|
|
|
|
+ targetSong.COMMENT = applyStringIfEmpty(targetSong.COMMENT, metadataItem.COMMENT) ?? targetSong.COMMENT;
|
|
|
|
|
+ targetSong.LYRICIST = applyStringIfEmpty(targetSong.LYRICIST, metadataItem.LYRICIST) ?? targetSong.LYRICIST;
|
|
|
|
|
+ targetSong.nb_streams = applyNumberIfEmpty(targetSong.nb_streams, metadataItem.nb_streams) ?? targetSong.nb_streams;
|
|
|
|
|
+ targetSong.nb_programs = applyNumberIfEmpty(targetSong.nb_programs, metadataItem.nb_programs) ?? targetSong.nb_programs;
|
|
|
|
|
+
|
|
|
|
|
+ if (!targetSong.name || targetSong.name.length === 0) {
|
|
|
|
|
+ targetSong.name = metadataItem.name || targetSong.name;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!targetSong.artist || targetSong.artist.length === 0) {
|
|
|
|
|
+ targetSong.artist = metadataItem.artist || targetSong.artist;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!targetSong.album || targetSong.album.length === 0) {
|
|
|
|
|
+ targetSong.album = metadataItem.album || targetSong.album;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const storagePath = resolveRemoteStoragePathForMetadata(targetSong);
|
|
|
|
|
+ if (storagePath) {
|
|
|
|
|
+ const dbItem = cloneVideoItem(targetSong);
|
|
|
|
|
+ dbItem.filePath = storagePath;
|
|
|
|
|
+ dbItem.remote_rel_path = storagePath;
|
|
|
|
|
+ if (!dbItem.parentPath && storagePath.includes('/')) {
|
|
|
|
|
+ dbItem.parentPath = storagePath.substring(0, storagePath.lastIndexOf('/'));
|
|
|
|
|
+ }
|
|
|
|
|
+ const table = new MediaTable(opts.context as Context);
|
|
|
|
|
+ await new Promise<void>((resolve, reject) => {
|
|
|
|
|
+ table.getRdbStore(opts.context as Context, (err: Error) => {
|
|
|
|
|
+ err ? reject(err) : resolve();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ await table.saveOrUpdateWebDavItem(dbItem);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (targetSong.filePath && (targetSong.pixelMapPath || targetSong.name || targetSong.artist)) {
|
|
|
|
|
+ const payload: WebDavMetadataUpdatePayload = {
|
|
|
|
|
+ filePath: targetSong.filePath,
|
|
|
|
|
+ pixelMapPath: targetSong.pixelMapPath,
|
|
|
|
|
+ name: targetSong.name,
|
|
|
|
|
+ artist: targetSong.artist
|
|
|
|
|
+ };
|
|
|
|
|
+ const eventUpdate: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
|
|
|
|
|
+ emitter.emit(eventUpdate, { data: [payload] });
|
|
|
|
|
+ }
|
|
|
|
|
+ })();
|
|
|
|
|
+ metadataExtractionInFlight.set(taskKey, task);
|
|
|
|
|
+ try {
|
|
|
|
|
+ await task;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ metadataExtractionInFlight.delete(taskKey);
|
|
|
|
|
+ }
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
const err = error as Error;
|
|
const err = error as Error;
|
|
|
Logger.warn(TAG, `从缓存提取元数据失败: ${err.message}`);
|
|
Logger.warn(TAG, `从缓存提取元数据失败: ${err.message}`);
|
|
@@ -710,6 +836,27 @@ export function sanitizePlaybackUrl(rawUrl: string | null | undefined): string {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function resolveRemoteStoragePathForMetadata(item: VideoItem): string | null {
|
|
|
|
|
+ if (!item) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isWebDavType(item.type)) {
|
|
|
|
|
+ return item.remote_rel_path || WebDavUrlUtil.toStoragePath(item.filePath) || null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isSmbType(item.type)) {
|
|
|
|
|
+ const relative = item.remote_rel_path || extractSmbRelativePath(item);
|
|
|
|
|
+ return relative && relative.length > 0 ? relative : null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isFtpType(item.type)) {
|
|
|
|
|
+ const relative = item.remote_rel_path || extractFtpRelativePath(item);
|
|
|
|
|
+ return relative && relative.length > 0 ? relative : null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isNavidromeType(item.type) || isJellyfinType(item.type) || isEmbyType(item.type) || isBaiduType(item.type)) {
|
|
|
|
|
+ return item.remote_rel_path || item.id || item.filePath || null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return item.filePath || null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
|
|
|
//排序类型
|
|
//排序类型
|
|
|
export function getTypeOrder(type: number) {
|
|
export function getTypeOrder(type: number) {
|