|
@@ -120,6 +120,15 @@ export interface FFprobeMetadata {
|
|
|
format: FFprobeFormat;
|
|
format: FFprobeFormat;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 音频流信息接口
|
|
|
|
|
+interface AudioStreamInfo {
|
|
|
|
|
+ sampleRate: string;
|
|
|
|
|
+ bits_per_raw_sample: string;
|
|
|
|
|
+ channels: string;
|
|
|
|
|
+ channel_layout: string;
|
|
|
|
|
+ start_time: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export class Utility {
|
|
export class Utility {
|
|
|
|
|
|
|
|
private constructor() {}
|
|
private constructor() {}
|
|
@@ -705,20 +714,6 @@ export class Utility {
|
|
|
static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number
|
|
static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number
|
|
|
,autoParseMusicName?:boolean,originFileName?:string): Promise<VideoItem> {
|
|
,autoParseMusicName?:boolean,originFileName?:string): Promise<VideoItem> {
|
|
|
await ReqPermissionUtil.persistPermission(uri);
|
|
await ReqPermissionUtil.persistPermission(uri);
|
|
|
- //这里加个判断,如果文件的大小是0K,这个文件是空的,直接返回
|
|
|
|
|
- try {
|
|
|
|
|
- const file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
|
|
|
|
|
- const fileStat = await fs.stat(file.fd);
|
|
|
|
|
- if (fileStat.size < 1024) {// 检查是否为有效文件(>1KB)
|
|
|
|
|
- console.warn(`File ${uri} is empty (0KB). Returning default VideoItem.`);
|
|
|
|
|
- // Return a default or empty VideoItem object
|
|
|
|
|
- return new VideoItem(FileUtil.getFileName(uri), uri, uri, type, 0, '');
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error(`Failed to get stats for file ${uri}:`, error);
|
|
|
|
|
- // If we can't read the file, still return a default item
|
|
|
|
|
- return new VideoItem(FileUtil.getFileName(uri), uri, uri, type, 0, '');
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){
|
|
if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){
|
|
|
const cueItem = new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
|
|
const cueItem = new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
|
|
@@ -726,393 +721,233 @@ export class Utility {
|
|
|
return cueItem
|
|
return cueItem
|
|
|
}
|
|
}
|
|
|
return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName,originFileName)
|
|
return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName,originFileName)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- //华为官方不支持的格式 不支持内嵌歌词Dsf,aif,aiff,wav 不支持内嵌封面dsf,aif,aiff
|
|
|
|
|
- if(StrUtil.isNotEmpty(uri)){
|
|
|
|
|
- if(uri.toLowerCase().endsWith('.dsf')
|
|
|
|
|
- ||uri.toLowerCase().endsWith('.aif')
|
|
|
|
|
- // ||uri.toLowerCase().endsWith('.wav')
|
|
|
|
|
- ||uri.toLowerCase().endsWith('.aiff')){
|
|
|
|
|
- return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
- let item:VideoItem = new VideoItem('',uri,uri,type,0,'')
|
|
|
|
|
- try {
|
|
|
|
|
- console.info('asset file.uri: ', uri);
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
- let file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
|
|
|
|
|
- console.info("file.fd " + file.fd);
|
|
|
|
|
- let fdfd = 'fd://' + file.fd
|
|
|
|
|
- //3、通过fs.stat方法获取stat对象
|
|
|
|
|
- console.info('asset file.name: ', file.name);
|
|
|
|
|
- console.info('asset file.uri: ', uri);
|
|
|
|
|
- console.info('asset file.fd: ', file.fd);
|
|
|
|
|
- console.info('asset file.path: ', file.path);
|
|
|
|
|
- item = new VideoItem(file.name,uri,uri,type,0,'')
|
|
|
|
|
- await fs.stat(file.fd).then(async (stat: fs.Stat) => {
|
|
|
|
|
- console.info("get file info succeed, the size of file is " + stat.size);
|
|
|
|
|
- let videoSize = stat.size
|
|
|
|
|
- // let videoTime = stat.ctime
|
|
|
|
|
-
|
|
|
|
|
- let fileSize = Utility.formatFSize(videoSize)
|
|
|
|
|
- //按照添加时间
|
|
|
|
|
- let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
|
|
|
|
|
- // console.info('onecold asset addTime: ', addTime);
|
|
|
|
|
- // let cTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
|
|
|
|
|
-
|
|
|
|
|
- // console.info('asset stat.ctime: ', stat.ctime);
|
|
|
|
|
-
|
|
|
|
|
- let musicName:string | undefined = file.name
|
|
|
|
|
- let artist:string | undefined = ''
|
|
|
|
|
- let album:string | undefined = ''
|
|
|
|
|
- let pixelMap:image.PixelMap|undefined|null = undefined
|
|
|
|
|
- let imagePath = ''
|
|
|
|
|
-
|
|
|
|
|
- let duration:string | undefined = ''
|
|
|
|
|
- let mimeType:string | undefined = ''
|
|
|
|
|
- let trackCount:string | undefined = ''//轨道数量
|
|
|
|
|
- let sampleRate:string | undefined = ''//音频的采样率单位为Hz
|
|
|
|
|
-
|
|
|
|
|
- if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
|
|
|
|
|
- try {
|
|
|
|
|
- // 创建AVMetadataExtractor对象
|
|
|
|
|
- const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
|
|
|
|
|
- // 设置fdSrc
|
|
|
|
|
- const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
|
|
|
|
- avMetadataExtractor.fdSrc = fd;
|
|
|
|
|
-
|
|
|
|
|
- // 获取元数据(promise模式)
|
|
|
|
|
- const metadata = await avMetadataExtractor.fetchMetadata();
|
|
|
|
|
- if(StrUtil.isNotEmpty(metadata.title)){
|
|
|
|
|
- musicName = metadata.title
|
|
|
|
|
- }else{
|
|
|
|
|
- //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
|
|
|
|
|
- console.log(`onecold musicName为空:${file.name}`);
|
|
|
|
|
- const musicData = parseMusicFileName(file.name);
|
|
|
|
|
- if (musicData.isValid) {
|
|
|
|
|
- // console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
|
|
|
|
|
- // console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
|
|
|
|
|
- musicName = musicData.title
|
|
|
|
|
- if(artist==''||artist==undefined)
|
|
|
|
|
- artist = musicData.artist
|
|
|
|
|
- } else {
|
|
|
|
|
- musicName = file.name
|
|
|
|
|
- // console.log("onecold 文件名格式不符合要求");
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
- if(StrUtil.isNotEmpty(metadata.artist)){
|
|
|
|
|
- artist = metadata.artist
|
|
|
|
|
- }
|
|
|
|
|
- if(artist==undefined)
|
|
|
|
|
- artist = ''
|
|
|
|
|
-
|
|
|
|
|
- if(StrUtil.isNotEmpty(metadata.album)){
|
|
|
|
|
- album = metadata.album
|
|
|
|
|
- }else{
|
|
|
|
|
- album = ''
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ static async readMetaInfoFFmpeg(context: Context, inputPath: string, type: number,
|
|
|
|
|
+ autoParseMusicName?: boolean, originFileName?: string): Promise<VideoItem> {
|
|
|
|
|
+ // 辅助函数:创建默认的VideoItem
|
|
|
|
|
+ const createDefaultItem = (filePath: string): VideoItem => {
|
|
|
|
|
+ return new VideoItem(FileUtil.getFileName(filePath), filePath, filePath, type, 0, '');
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- if(StrUtil.isNotEmpty(metadata.duration)){
|
|
|
|
|
- if(metadata.duration)
|
|
|
|
|
- duration = convertSecondsToTime(metadata.duration.toString())
|
|
|
|
|
- }
|
|
|
|
|
- if(StrUtil.isNotEmpty(metadata.mimeType)){
|
|
|
|
|
- mimeType = metadata.mimeType
|
|
|
|
|
|
|
+ // 辅助函数:提取歌词(处理标准字段和lyrics-开头的自定义字段)
|
|
|
|
|
+ const extractLyrics = (tags: FFMpegTags): string => {
|
|
|
|
|
+ // 1. 尝试标准字段
|
|
|
|
|
+ let lyrics = tags.LYRICS || tags.lyrics || tags.USLT || tags.UNSYNCEDLYRICS || '';
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 如果标准字段为空,尝试lyrics-开头的自定义字段
|
|
|
|
|
+ if (StrUtil.isEmpty(lyrics)) {
|
|
|
|
|
+ const tagsRecord = tags as Record<string, string>;
|
|
|
|
|
+ const possibleKeys = Object.keys(tagsRecord);
|
|
|
|
|
+ for (let i = 0; i < possibleKeys.length; i++) {
|
|
|
|
|
+ const key = possibleKeys[i];
|
|
|
|
|
+ if (key && key.toLowerCase().startsWith('lyrics-')) {
|
|
|
|
|
+ lyrics = tagsRecord[key];
|
|
|
|
|
+ if (lyrics) {
|
|
|
|
|
+ break;
|
|
|
}
|
|
}
|
|
|
- if(StrUtil.isNotEmpty(metadata.trackCount)){
|
|
|
|
|
- trackCount = metadata.trackCount
|
|
|
|
|
- }
|
|
|
|
|
- if(StrUtil.isNotEmpty(metadata.sampleRate)){
|
|
|
|
|
- sampleRate = metadata.sampleRate
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let name = await MD5.digestSync(uri)
|
|
|
|
|
-
|
|
|
|
|
- // if(isLoadPixelMap){
|
|
|
|
|
- // 获取专辑封面(promise模式)
|
|
|
|
|
- // pixelMap = await avMetadataExtractor.fetchAlbumCover();
|
|
|
|
|
- // // 释放资源(promise模式)
|
|
|
|
|
- // await avMetadataExtractor.release();
|
|
|
|
|
-
|
|
|
|
|
- pixelMap = await fetchAlbumCover(avMetadataExtractor)
|
|
|
|
|
-
|
|
|
|
|
- // console.info('onecold release success. name= '+musicName);
|
|
|
|
|
- if(pixelMap!==undefined&&pixelMap!==null){
|
|
|
|
|
- // console.info('onecold pixelMap is not empty= '+musicName);
|
|
|
|
|
- imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,name)
|
|
|
|
|
- imagePath = fileUri.getUriFromPath(imagePath)
|
|
|
|
|
- }else{
|
|
|
|
|
- // console.info('onecold pixelMap is empty= '+musicName);
|
|
|
|
|
- imagePath = ''
|
|
|
|
|
- // if(StrUtil.isNotEmpty(artist))
|
|
|
|
|
- // imagePath = await NetAxiosUtil.getLyricCover(musicName,artist)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // }else{
|
|
|
|
|
- // imagePath = context.filesDir + FileUtil.separator + name
|
|
|
|
|
- // imagePath = fileUri.getUriFromPath(imagePath)
|
|
|
|
|
- // }
|
|
|
|
|
- console.info('onecold release success. imagePath= '+imagePath);
|
|
|
|
|
|
|
+ return lyrics;
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
|
|
+ // 辅助函数:处理封面提取
|
|
|
|
|
+ const extractCover = async (hasCover: boolean, filePath: string, name: string): Promise<string> => {
|
|
|
|
|
+ const md5Name = await MD5.digestSync(filePath);
|
|
|
|
|
+ let imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}.jpg`;
|
|
|
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('Error during metadata extraction:', error);
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (hasCover) {
|
|
|
|
|
+ const isSuccess = await getFFmpegCover(filePath, imagePath);
|
|
|
|
|
+ if (isSuccess) {
|
|
|
|
|
+ return fileUri.getUriFromPath(imagePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Logger.warn('readMetaInfoFFmpeg', 'FFmpeg提取封面失败,使用华为接口');
|
|
|
|
|
+ return await extractCoverWithHwInterface(filePath, context, name);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (Utility.isVideoByExtension(filePath)) {
|
|
|
|
|
+ const pixelMap = await Utility.getFetchFrameByTime(filePath);
|
|
|
|
|
+ if (pixelMap) {
|
|
|
|
|
+ imagePath = await ImageUtil.savePixelMap(pixelMap, context.filesDir, md5Name);
|
|
|
|
|
+ return fileUri.getUriFromPath(imagePath);
|
|
|
}
|
|
}
|
|
|
- } else {
|
|
|
|
|
- console.warn('AVMetadataExtractor capability is not supported.');
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Logger.error('readMetaInfoFFmpeg', `提取封面失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ return '';
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- if(musicName==undefined)
|
|
|
|
|
- musicName = file.name
|
|
|
|
|
-
|
|
|
|
|
- item = new VideoItem(musicName,uri ,uri,type,videoSize,addTime,fileSize,
|
|
|
|
|
- imagePath,artist,album,file.name)
|
|
|
|
|
- item.duration = duration+'';
|
|
|
|
|
- item.mimeType = mimeType;
|
|
|
|
|
- item.trackCount = trackCount;
|
|
|
|
|
- item.sampleRate = sampleRate;
|
|
|
|
|
- item.isFav = 0;
|
|
|
|
|
- item.playCount = 0;
|
|
|
|
|
- item.pyStr = pinyin4js.getShortPinyin(musicName)
|
|
|
|
|
-
|
|
|
|
|
- // item.lyricContent = await extractLyricsContent(uri)
|
|
|
|
|
- let metaItem = await parseAudioMetadata(uri)
|
|
|
|
|
- if(metaItem){
|
|
|
|
|
- item.lyricContent = metaItem.lyricContent
|
|
|
|
|
- item.bit_rate = formatBitrateToKbps(metaItem.bit_rate || "0");
|
|
|
|
|
- item.year = metaItem.year ||'unknown'
|
|
|
|
|
- item.probe_score = metaItem.probe_score
|
|
|
|
|
- item.nb_streams = metaItem.nb_streams
|
|
|
|
|
- item.nb_programs = metaItem.nb_programs
|
|
|
|
|
|
|
+ // 辅助函数:解析音频流信息
|
|
|
|
|
+ const parseAudioStream = (metadata: FFprobeMetadata): AudioStreamInfo => {
|
|
|
|
|
+ let sampleRate = '';
|
|
|
|
|
+ let bits_per_raw_sample = '';
|
|
|
|
|
+ let channels = '';
|
|
|
|
|
+ let channel_layout = '';
|
|
|
|
|
+ let start_time = '';
|
|
|
|
|
+
|
|
|
|
|
+ if (metadata.streams && metadata.streams.length > 0) {
|
|
|
|
|
+ const audioStream = metadata.streams.find(stream => StrUtil.isNotEmpty(stream.sample_rate));
|
|
|
|
|
+ if (audioStream) {
|
|
|
|
|
+ sampleRate = audioStream.sample_rate || '';
|
|
|
|
|
+ bits_per_raw_sample = audioStream.bits_per_raw_sample || '';
|
|
|
|
|
+ channel_layout = audioStream.channel_layout || '';
|
|
|
|
|
+ channels = audioStream.channels || '';
|
|
|
|
|
+ start_time = audioStream.start_time || '';
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- })
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('uriGetAssetsFromFile failed with err: ' + JSON.stringify(error));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return item
|
|
|
|
|
-
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return { sampleRate, bits_per_raw_sample, channels, channel_layout, start_time };
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ inputPath = FileUtil.getFilePath(inputPath);
|
|
|
|
|
+ Logger.info('readMetaInfoFFmpeg', `正在获取元数据: ${inputPath}`)
|
|
|
|
|
|
|
|
- static async readMetaInfoFFmpeg(context:Context,inputPath: string,type:number,
|
|
|
|
|
- autoParseMusicName?:boolean,originFileName?:string): Promise<VideoItem> {
|
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
- inputPath = FileUtil.getFilePath(inputPath)
|
|
|
|
|
- let commands = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", inputPath];
|
|
|
|
|
|
|
+ const commands = ["ffprobe", "-v", "quiet", "-print_format",
|
|
|
|
|
+ "json", "-show_format", "-show_streams", inputPath];
|
|
|
let outputJson = "";
|
|
let outputJson = "";
|
|
|
|
|
|
|
|
|
|
+ // 使用Promise方式执行FFprobe命令
|
|
|
|
|
+ await new Promise<void>((resolve, reject) => {
|
|
|
|
|
+ FFmpeg.execute(commands, {
|
|
|
|
|
+ outputCallback: (message: string) => {
|
|
|
|
|
+ outputJson += message;
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ .then(() => {
|
|
|
|
|
+ Logger.info('readMetaInfoFFmpeg', `获取元数据成功, JSON长度: ${outputJson.length}`);
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch((error: Error) => {
|
|
|
|
|
+ Logger.error('readMetaInfoFFmpeg', `FFprobe执行失败: ${error.message}`);
|
|
|
|
|
+ reject(error);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ // 解析元数据
|
|
|
|
|
+ const metadata: FFprobeMetadata = JSON.parse(outputJson);
|
|
|
|
|
+ const format = metadata.format;
|
|
|
|
|
+ const tags = format.tags || {};
|
|
|
|
|
|
|
|
- FFmpeg.execute(commands, {
|
|
|
|
|
- logCallback: (logLevel: number, logMessage: string) => console.log(`[${logLevel}]${logMessage}`),
|
|
|
|
|
- outputCallback: (message: string) => {
|
|
|
|
|
- outputJson += message;
|
|
|
|
|
- },
|
|
|
|
|
- }).then(async () => {
|
|
|
|
|
- try {
|
|
|
|
|
- const metadata: FFprobeMetadata = JSON.parse(outputJson);
|
|
|
|
|
- const format = metadata.format;
|
|
|
|
|
- console.log(`onecold outputJson:${outputJson}`);
|
|
|
|
|
- let file = fs.openSync(inputPath, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
|
|
|
|
|
- console.info('readMetaInfoFFmpeg asset file.path: ', file.path);
|
|
|
|
|
- let videoItem = new VideoItem(file.name,inputPath,inputPath,type,0,'')
|
|
|
|
|
- await fs.stat(file.fd).then(async (stat: fs.Stat) => {
|
|
|
|
|
-
|
|
|
|
|
- // 获取音频流的采样率
|
|
|
|
|
- let sampleRate:string|undefined = '';
|
|
|
|
|
- let bits_per_raw_sample:string|undefined = '';
|
|
|
|
|
- let channels:string|undefined = '';
|
|
|
|
|
- let channel_layout:string|undefined = '';
|
|
|
|
|
- let start_time:string|undefined = '';
|
|
|
|
|
- if (metadata.streams && metadata.streams.length > 0) {
|
|
|
|
|
- // 查找第一个音频流
|
|
|
|
|
- const audioStream = metadata.streams.find(stream => StrUtil.isNotEmpty(stream.sample_rate));
|
|
|
|
|
- if (audioStream) {
|
|
|
|
|
- sampleRate = audioStream.sample_rate;
|
|
|
|
|
- bits_per_raw_sample = audioStream.bits_per_raw_sample
|
|
|
|
|
- channel_layout = audioStream.channel_layout
|
|
|
|
|
- channels = audioStream.channels
|
|
|
|
|
- start_time = audioStream.start_time
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let videoSize = stat.size
|
|
|
|
|
- const modifyTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
|
|
|
|
|
- let fileSize = Utility.formatFSize(videoSize)
|
|
|
|
|
- //按照添加时间
|
|
|
|
|
- let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
|
|
|
|
|
- // Extract artist and title from tags
|
|
|
|
|
- const tags = format.tags || {};
|
|
|
|
|
- let artist = tags.artist ||tags.ARTIST || '';
|
|
|
|
|
- let title = tags.title ||tags.TITLE || '';
|
|
|
|
|
- let album = tags.album ||tags.ALBUM|| '';
|
|
|
|
|
- Logger.info('readMetaInfoFFmpeg', `原始标签 title="${title}", artist="${artist}", album="${album}"`);
|
|
|
|
|
- console.log(`onecold tags.tags:${JSON.stringify(tags)}`);
|
|
|
|
|
- // 检查是否有乱码
|
|
|
|
|
- if (hasGarbledText(tags)) {
|
|
|
|
|
- // 第二次尝试用华为的接口提取元数据
|
|
|
|
|
- let hwTags = await extractHwMediaMetadata(inputPath)
|
|
|
|
|
- artist = hwTags?.artist|| '';
|
|
|
|
|
- title = hwTags?.title|| '';
|
|
|
|
|
- album = hwTags?.album|| '';
|
|
|
|
|
- console.info('onecold 乱码修正 artist='+artist);
|
|
|
|
|
- console.info('onecold 乱码修正 title='+title);
|
|
|
|
|
- console.info('onecold 乱码修正 album='+album);
|
|
|
|
|
- }
|
|
|
|
|
- Logger.info('readMetaInfoFFmpeg', `最终标签 title="${title}", artist="${artist}", album="${album}", autoParse=${autoParseMusicName}`);
|
|
|
|
|
- if(StrUtil.isEmpty(title)){
|
|
|
|
|
- //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
|
|
|
|
|
- console.log(`onecold musicName为空:${file.name}`);
|
|
|
|
|
- // let autoParseMusicName = PreferencesUtil.getBooleanSync('autoParseMusicName',false)
|
|
|
|
|
- if(autoParseMusicName){
|
|
|
|
|
- const musicData = parseMusicFileName(file.name);
|
|
|
|
|
- if (musicData.isValid) {
|
|
|
|
|
- console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
|
|
|
|
|
- console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
|
|
|
|
|
- title = musicData.title
|
|
|
|
|
- if(artist==''||artist==undefined)
|
|
|
|
|
- artist = musicData.artist
|
|
|
|
|
- } else {
|
|
|
|
|
-
|
|
|
|
|
- title = originFileName||file.name
|
|
|
|
|
- }
|
|
|
|
|
- }else{
|
|
|
|
|
- title = originFileName||file.name
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 获取文件信息
|
|
|
|
|
+ let file: fs.File;
|
|
|
|
|
+ try {
|
|
|
|
|
+ file = fs.openSync(inputPath, fs.OpenMode.READ_ONLY);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Logger.error('readMetaInfoFFmpeg', `打开文件失败: ${inputPath}`);
|
|
|
|
|
+ return createDefaultItem(inputPath);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ const stat = await fs.stat(file.fd);
|
|
|
|
|
+ const videoSize = stat.size;
|
|
|
|
|
+ const addTime = Utility.getFormatDateStr(new Date(), 'yyyy-MM-dd HH:mm');
|
|
|
|
|
+ const fileSize = Utility.formatFSize(videoSize);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析音频流信息
|
|
|
|
|
+ const audioStreamInfo = parseAudioStream(metadata);
|
|
|
|
|
+
|
|
|
|
|
+ // 提取基础标签信息
|
|
|
|
|
+ let artist = tags.artist || tags.ARTIST || '';
|
|
|
|
|
+ let title = tags.title || tags.TITLE || '';
|
|
|
|
|
+ let album = tags.album || tags.ALBUM || '';
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info('readMetaInfoFFmpeg', `原始标签 title="${title}", artist="${artist}", album="${album}"`);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查并修正乱码
|
|
|
|
|
+ if (hasGarbledText(tags)) {
|
|
|
|
|
+ const hwTags = await extractHwMediaMetadata(inputPath);
|
|
|
|
|
+ artist = hwTags?.artist || artist;
|
|
|
|
|
+ title = hwTags?.title || title;
|
|
|
|
|
+ album = hwTags?.album || album;
|
|
|
|
|
+ Logger.info('readMetaInfoFFmpeg', `乱码修正后的标签 title="${title}", artist="${artist}"`);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- let name: string = title;
|
|
|
|
|
- if (!name) {
|
|
|
|
|
- name = getFileNameWithoutExtension(inputPath);
|
|
|
|
|
- }
|
|
|
|
|
- let pixelMap:image.PixelMap|undefined|null = undefined
|
|
|
|
|
- // Create VideoItem
|
|
|
|
|
- videoItem = new VideoItem(
|
|
|
|
|
- name,
|
|
|
|
|
- inputPath, // id can be generated or left empty
|
|
|
|
|
- inputPath,
|
|
|
|
|
- type, // assuming it's local
|
|
|
|
|
- videoSize,
|
|
|
|
|
- addTime // convert to ISO string
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- // Set additional properties from format metadata
|
|
|
|
|
- videoItem.artist = artist;
|
|
|
|
|
- videoItem.album = album;
|
|
|
|
|
- videoItem.sampleRate = sampleRate
|
|
|
|
|
- videoItem.pyStr = pinyin4js.getShortPinyin(name)
|
|
|
|
|
- videoItem.fileName = FileUtil.getFileName(inputPath);
|
|
|
|
|
- if(format.duration)
|
|
|
|
|
- videoItem.duration = formatDuration(format.duration.toString()||'00:00')
|
|
|
|
|
- videoItem.size = fileSize;
|
|
|
|
|
- videoItem.bit_rate =formatBitrateToKbps(format.bit_rate || "0");
|
|
|
|
|
- videoItem.probe_score = format.probe_score;
|
|
|
|
|
- videoItem.nb_streams = format.nb_streams;
|
|
|
|
|
- videoItem.nb_programs = format.nb_programs;
|
|
|
|
|
- videoItem.year = tags.TYER || tags.date ||tags.DATE|| ''; // try different tag names for year
|
|
|
|
|
- videoItem.lyricContent = tags.LYRICS || tags.lyrics || tags.USLT || tags.UNSYNCEDLYRICS || '';
|
|
|
|
|
- //console.info('readMetaInfoFFmpeg videoItem.lyricContent: ', videoItem.lyricContent);
|
|
|
|
|
- // 如果标准字段没有歌词,则尝试解析 `lyrics-` 字段
|
|
|
|
|
- // tags 必须是 Record<string, string | undefined>(或 any)
|
|
|
|
|
- // 如果还是没有歌词内容,尝试查找自定义的lyrics-开头的属性(电脑版音乐标签内嵌歌词就是lyrics-XXX)
|
|
|
|
|
- if(StrUtil.isEmpty( videoItem.lyricContent)){
|
|
|
|
|
- const tagsRecord = tags as Record<string, string>;
|
|
|
|
|
- const possibleKeys = Object.keys(tagsRecord);
|
|
|
|
|
- for (let i = 0; i < possibleKeys.length; i++) {
|
|
|
|
|
- const key = possibleKeys[i];
|
|
|
|
|
- if (key && key.toLowerCase().startsWith('lyrics-')) {
|
|
|
|
|
- //console.info('readMetaInfoFFmpeg videoItem.key: ', key);
|
|
|
|
|
- // 通过转换后的 Record 类型安全访问属性
|
|
|
|
|
- videoItem.lyricContent = tagsRecord[key];
|
|
|
|
|
- //console.info('readMetaInfoFFmpeg videoItem.lyricContent2: ', videoItem.lyricContent);
|
|
|
|
|
- if (videoItem.lyricContent) {
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 处理空标题的情况
|
|
|
|
|
+ if (StrUtil.isEmpty(title)) {
|
|
|
|
|
+ if (autoParseMusicName) {
|
|
|
|
|
+ const musicData = parseMusicFileName(file.name);
|
|
|
|
|
+ if (musicData.isValid) {
|
|
|
|
|
+ title = musicData.title;
|
|
|
|
|
+ if (StrUtil.isEmpty(artist)) {
|
|
|
|
|
+ artist = musicData.artist;
|
|
|
}
|
|
}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ title = originFileName || file.name;
|
|
|
}
|
|
}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ title = originFileName || file.name;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ // 确保有有效的名称
|
|
|
|
|
+ let name = title || getFileNameWithoutExtension(inputPath);
|
|
|
|
|
+
|
|
|
|
|
+ // 构建VideoItem
|
|
|
|
|
+ const videoItem = new VideoItem(name, inputPath, inputPath, type, videoSize, addTime);
|
|
|
|
|
+ videoItem.artist = artist;
|
|
|
|
|
+ videoItem.album = album;
|
|
|
|
|
+ videoItem.sampleRate = audioStreamInfo.sampleRate;
|
|
|
|
|
+ videoItem.pyStr = pinyin4js.getShortPinyin(name);
|
|
|
|
|
+ videoItem.fileName = FileUtil.getFileName(inputPath);
|
|
|
|
|
+ videoItem.duration = format.duration ? formatDuration(format.duration.toString()) : '00:00';
|
|
|
|
|
+ videoItem.size = fileSize;
|
|
|
|
|
+ videoItem.bit_rate = formatBitrateToKbps(format.bit_rate || "0");
|
|
|
|
|
+ videoItem.probe_score = format.probe_score;
|
|
|
|
|
+ videoItem.nb_streams = format.nb_streams;
|
|
|
|
|
+ videoItem.nb_programs = format.nb_programs;
|
|
|
|
|
+ videoItem.year = tags.TYER || tags.date || tags.DATE || '';
|
|
|
|
|
+ videoItem.lyricContent = extractLyrics(tags);
|
|
|
|
|
+ videoItem.genre = tags.genre || tags.GENRE || '';
|
|
|
|
|
+ videoItem.track = tags.track || tags.TRACK || '';
|
|
|
|
|
+ videoItem.ALBUMARTIST = tags.ALBUMARTIST || tags.album_artist || tags.albumartist || tags.TPE2 || '';
|
|
|
|
|
+ videoItem.COMPOSER = tags.COMPOSER || tags.composer || '';
|
|
|
|
|
+ videoItem.LYRICIST = tags.LYRICIST || tags.lyricist || tags.TEXT || '';
|
|
|
|
|
+ videoItem.COMMENT = tags.COMMENT || tags.comment || tags.COMM || '';
|
|
|
|
|
+ videoItem.disc = tags.disc || '';
|
|
|
|
|
+ videoItem.bits_per_raw_sample = audioStreamInfo.bits_per_raw_sample || '1';
|
|
|
|
|
+ videoItem.channel_layout = audioStreamInfo.channel_layout || '';
|
|
|
|
|
+ videoItem.channels = audioStreamInfo.channels || '0';
|
|
|
|
|
+ videoItem.start_time = audioStreamInfo.start_time || '00:00';
|
|
|
|
|
+ videoItem.mimeType = format.format_name;
|
|
|
|
|
+
|
|
|
|
|
+ // 检查并提取封面
|
|
|
|
|
+ const hasCover = metadata.streams.some(stream => stream.disposition?.attached_pic === 1);
|
|
|
|
|
+ videoItem.pixelMapPath = await extractCover(hasCover, inputPath, name);
|
|
|
|
|
+
|
|
|
|
|
+ // 确定音质等级
|
|
|
|
|
+ if (audioStreamInfo.sampleRate && format.bit_rate) {
|
|
|
|
|
+ videoItem.md5Str = determineAudioQuality(
|
|
|
|
|
+ videoItem.mimeType,
|
|
|
|
|
+ Number(format.bit_rate),
|
|
|
|
|
+ Number(audioStreamInfo.sampleRate)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- videoItem.genre = tags.genre||tags.GENRE|| '';
|
|
|
|
|
- videoItem.track = tags.track||tags.TRACK|| '';
|
|
|
|
|
-
|
|
|
|
|
- videoItem.ALBUMARTIST = tags.ALBUMARTIST||tags.album_artist||tags.albumartist||tags.TPE2|| '';
|
|
|
|
|
- videoItem.COMPOSER = tags.COMPOSER||tags.composer|| '';
|
|
|
|
|
- videoItem.LYRICIST = tags.LYRICIST||tags.lyricist||tags.TEXT ||'';
|
|
|
|
|
- videoItem.COMMENT = tags.COMMENT||tags.comment||tags.COMM|| '';
|
|
|
|
|
- videoItem.disc = tags.disc|| '';
|
|
|
|
|
-
|
|
|
|
|
- videoItem.bits_per_raw_sample = bits_per_raw_sample ||'1'
|
|
|
|
|
- videoItem.channel_layout = channel_layout || ''
|
|
|
|
|
- videoItem.channels = channels ||'0'
|
|
|
|
|
- videoItem.start_time = start_time || '00:00'
|
|
|
|
|
-
|
|
|
|
|
- videoItem.mimeType = format.format_name
|
|
|
|
|
- // 检查是否有封面图片流
|
|
|
|
|
- const hasCover = metadata.streams.some(stream =>
|
|
|
|
|
- stream.disposition?.attached_pic === 1
|
|
|
|
|
- );
|
|
|
|
|
- console.info('onecold readMetaInfoFFmpeg asset hasCover: ', hasCover);
|
|
|
|
|
- let md5Name = await MD5.digestSync(inputPath)
|
|
|
|
|
- let imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}.jpg`;
|
|
|
|
|
- console.info('onecold readMetaInfoFFmpeg asset imagePath: ', imagePath);
|
|
|
|
|
- // 如果有封面图片,则提取
|
|
|
|
|
- try {
|
|
|
|
|
- if (hasCover) {
|
|
|
|
|
- //提取封面
|
|
|
|
|
- let isSuccess:boolean= await getFFmpegCover(inputPath, imagePath);
|
|
|
|
|
-
|
|
|
|
|
- if(isSuccess){
|
|
|
|
|
- imagePath = fileUri.getUriFromPath(imagePath)
|
|
|
|
|
- }else{
|
|
|
|
|
- // 使用华为接口提取封面(备用方案)
|
|
|
|
|
- console.warn('onecold 提取封面图片失败,使用华为接口提取封面');
|
|
|
|
|
- imagePath = await extractCoverWithHwInterface(inputPath, context, name);
|
|
|
|
|
- }
|
|
|
|
|
- videoItem.pixelMapPath = imagePath;
|
|
|
|
|
- }else if(Utility.isVideoByExtension(inputPath)){
|
|
|
|
|
- pixelMap = await Utility.getFetchFrameByTime(inputPath)
|
|
|
|
|
- if(pixelMap!==undefined&&pixelMap!==null){
|
|
|
|
|
- imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,md5Name)
|
|
|
|
|
- imagePath = fileUri.getUriFromPath(imagePath)
|
|
|
|
|
- }else{
|
|
|
|
|
- imagePath = ''
|
|
|
|
|
- }
|
|
|
|
|
- imagePath = fileUri.getUriFromPath(imagePath)
|
|
|
|
|
- videoItem.pixelMapPath = imagePath;
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.warn('onecold 提取封面图片失败:', error.message);
|
|
|
|
|
- }
|
|
|
|
|
- if(sampleRate&&format.bit_rate){
|
|
|
|
|
- videoItem.md5Str = determineAudioQuality( videoItem.mimeType,Number(format.bit_rate),Number(sampleRate))
|
|
|
|
|
- }
|
|
|
|
|
- console.info('onecold Successfully parsed metadata:', videoItem);
|
|
|
|
|
- resolve(videoItem);
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
|
|
+ Logger.info('readMetaInfoFFmpeg', `成功解析元数据: ${name}`);
|
|
|
|
|
+ return videoItem;
|
|
|
|
|
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ // 确保文件句柄被关闭
|
|
|
|
|
+ try {
|
|
|
|
|
+ fs.closeSync(file);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('onecold Failed to parse metadata:', error);
|
|
|
|
|
- reject(new Error('onecold Failed to parse metadata: ' + error.message));
|
|
|
|
|
|
|
+ Logger.error('readMetaInfoFFmpeg', `关闭文件失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
}
|
|
}
|
|
|
- }).catch((error: Error) => {
|
|
|
|
|
- console.error(`onecold Execution failed with error: ${error.message}`);
|
|
|
|
|
- reject(error);
|
|
|
|
|
- });
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Logger.error('onecold readMetaInfoFFmpeg', `解析失败: ${JSON.stringify(error)})`);
|
|
|
|
|
+ return createDefaultItem(inputPath);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
private completionNum(num: number): string | number {
|
|
private completionNum(num: number): string | number {
|
|
|
if (num < 10) {
|
|
if (num < 10) {
|
|
|
return '0' + num;
|
|
return '0' + num;
|
|
@@ -1949,9 +1784,7 @@ async function getFFmpegCover(inputPath: string, outputPath: string): Promise<bo
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
await FFmpeg.execute(commands, {
|
|
await FFmpeg.execute(commands, {
|
|
|
- logCallback: (logLevel: number, logMessage: string) => {
|
|
|
|
|
- console.info(`[FFmpeg LOG] [${logLevel}] ${logMessage}`);
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ logCallback: (message) => console.info(`[log] ${message}`),
|
|
|
progressCallback: (message: string) => {
|
|
progressCallback: (message: string) => {
|
|
|
console.info(`[FFmpeg progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
|
|
console.info(`[FFmpeg progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
|
|
|
},
|
|
},
|
|
@@ -1969,9 +1802,7 @@ async function getFFmpegCover(inputPath: string, outputPath: string): Promise<bo
|
|
|
outputPath
|
|
outputPath
|
|
|
];
|
|
];
|
|
|
await FFmpeg.execute(primaryCommands, {
|
|
await FFmpeg.execute(primaryCommands, {
|
|
|
- logCallback: (logLevel: number, logMessage: string) => {
|
|
|
|
|
- console.info(`[FFmpeg LOG] [${logLevel}] ${logMessage}`);
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ logCallback: (message) => console.info(`[log] ${message}`),
|
|
|
progressCallback: (message: string) => {
|
|
progressCallback: (message: string) => {
|
|
|
console.info(`[FFmpeg progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
|
|
console.info(`[FFmpeg progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
|
|
|
},
|
|
},
|
|
@@ -2003,9 +1834,7 @@ async function getVideoFFmpegCover(inputPath: string, outputPath: string) {
|
|
|
outputPath
|
|
outputPath
|
|
|
];
|
|
];
|
|
|
FFmpeg.execute(commands, {
|
|
FFmpeg.execute(commands, {
|
|
|
- logCallback: (logLevel: number, logMessage: string) => {
|
|
|
|
|
- console.info(`[FFmpegX LOG] [${logLevel}]${logMessage}`)
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ logCallback: (message) => console.info(`[log]${message}`),
|
|
|
progressCallback: (message: string) => {
|
|
progressCallback: (message: string) => {
|
|
|
console.info(`[FFmpegX progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
|
|
console.info(`[FFmpegX progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
|
|
|
},
|
|
},
|