import { BusinessError, pasteboard } from '@kit.BasicServicesKit'; import { AppUtil, ArrayUtil, Base64Util, DateUtil, FileUtil, ImageUtil, LogUtil, MD5, PreferencesUtil, RandomUtil, StrUtil } from '@pura/harmony-utils'; import { media } from '@kit.MediaKit'; import fs, { ReadTextOptions } from '@ohos.file.fs'; import { image } from '@kit.ImageKit'; import fileIo from '@ohos.file.fs'; import { VideoItem } from '../../viewmodel/VideoItem'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { dataSharePredicates, uniformTypeDescriptor } from '@kit.ArkData'; import { systemShare } from '@kit.ShareKit'; import { fileUri, picker } from '@kit.CoreFileKit'; import { common, UIAbility, Want } from '@kit.AbilityKit'; import { CommonConstants, STR_LOCK_VIDEO, VIP_FILEPATH } from '../constants/CommonConstants'; import { window } from '@kit.ArkUI'; import { bundleManager } from '@kit.AbilityKit' import { pinyin4js } from '@ohos/pinyin4js'; import { VipData } from '../../viewmodel/VipData'; import { VipPage } from '../../pages/VipPage'; import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg'; export interface FFMpegTags { album?: string; ALBUM?: string; artist?: string; ARTIST?: string; TITLE?: string; title?: string; track?: string; TRACK?: string; TYER?: string; year?: string; genre?:string; GENRE?:string; date?: string; DATE?: string; LYRICS?: string; lyrics?: string; // 小写变体 USLT?: string; // ID3v2同步歌词 UNSYNCEDLYRICS?: string; // ID3v2非同步歌词 // Add any other tag properties you expect ALBUMARTIST?:string//专辑艺术家 COMPOSER?:string//作曲家 LYRICIST?:string//作词家 COMMENT?:string//注释 albumartist?:string//专辑艺术家 album_artist?:string//专辑艺术家 TPE2?:string//专辑艺术家 composer?:string//作曲家 lyricist?:string//作词家 TEXT?:string//作词家 comment?:string//注释 COMM?:string//注释 comm?:string//注释 disc?:string//音碟号 } interface FFprobeFormat { filename: string; nb_streams: number; nb_programs: number; format_name: string; format_long_name: string; duration: string; size: string; bit_rate: string; probe_score: number; tags?: FFMpegTags; } interface FFprobeStream { // Define stream properties as needed codec_type?: string; // 流类型,如"audio"、"video" sample_rate?: string; // 采样率 bit_rate?: string; // 比特率 disposition?: StreamDisposition; // 添加disposition属性 bits_per_raw_sample?: string; // 位深 channels?: string; // 声道数 channel_layout?: string; // 声道布局 start_time?: string; // 起始播放 } interface StreamDisposition { default?: number; dub?: number; original?: number; comment?: number; lyrics?: number; karaoke?: number; forced?: number; hearing_impaired?: number; visual_impaired?: number; clean_effects?: number; attached_pic?: number; // 添加封面图片标识 timed_thumbnails?: number; captions?: number; descriptions?: number; metadata?: number; dependent?: number; still_image?: number; } interface FFprobeMetadata { streams: FFprobeStream[]; format: FFprobeFormat; } export class Utility { private constructor() {} static isOpenTime():boolean{ const currentDate = new Date(); const targetDate = new Date(CommonConstants.OPEN_DATE); if (currentDate > targetDate) { return true } return false } static readonly FOREVER_DATE: string = 'forever'; /** * 是否会员(只看线上会员状态) */ static isNoble(): boolean { // 只判断线上会员状态 return PreferencesUtil.getBooleanSync('hasActiveSubscription', false); } static isForever(): boolean { // 判断是不是永久会员 return PreferencesUtil.getBooleanSync('isForever', false); } /** * 旧版客户端判断会员状态 * @returns */ static isNobleForOld():boolean{ if(!Utility.isOpenTime())//提交审核的审核,刚刚开始可以全部是赞助用户。 return true return PreferencesUtil.getBooleanSync('isNoble',false) } /** * setNoble方法废弃,保留兼容但不做任何操作 */ static setNoble(expireDate:string){ let isNoble = false if(StrUtil.isNotEmpty(expireDate)){ if(expireDate===VipPage.FOREVER_DATE){ isNoble = true }else{ const currentDate = new Date(); const targetDate = DateUtil.getFormatDate(expireDate) if (currentDate > targetDate) { isNoble = false }else{ isNoble = true } } } PreferencesUtil.putSync('isNoble',isNoble) } /** * 判断用户安装app是否超过2天,超过2天(2800分钟)才展示广告 显示广告 24*60=1440 1440*2=2800 * @param day * @returns */ static isPassInstallTime(day:number):boolean{ //获取当前时间 let currentTime = new Date().getTime(); // 尝试从存储中获取安装时间 let installTime = getInstallTime(); if (installTime===0||installTime === null|| installTime===undefined) { // 如果没有存储安装时间,则存储当前时间为安装时间 setInstallTime(currentTime); return false } else { // 计算时间差(以分钟为单位) const timeDifference = (currentTime - installTime) / (1000 * 60); LogUtil.debug("onecold timeDifference=" + timeDifference) // 如果超过5分钟,显示广告 24*60=1440 1440*2=2800 if (timeDifference > 24*60*day) { return true }else{ return false } } } // 定义一个函数来获取当前的时间段 static getTimePeriod(): string { const now = new Date(); const hours = now.getHours(); if (hours >= 5 && hours < 12) { return "早上好"; } else if (hours >= 12 && hours < 18) { return "下午好"; } else if (hours >= 18 && hours < 22) { return "晚上好"; } else { return "夜深了"; } } static optimizedFormat(speed: number): string { if (speed) { const str = speed.toFixed(2); let end = str.length; while (end > 0 && (str[end - 1] === '0' || str[end - 1] === '.')) { end--; if (str[end] === '.') { break; } } return str.slice(0, end || 1) + 'x'; } else { return '1x' } } static isHaoOpenTime():boolean{ const currentDate = new Date(); const targetDate = new Date('2025-04-20'); if (currentDate < targetDate) { return true } return false } static addDays(date: Date, days: number): Date { const result = new Date(date); result.setDate(result.getDate() + days); return result; } static convertToKHz(sampleRateHz: string|undefined): string { if(StrUtil.isEmpty(sampleRateHz)||sampleRateHz ===undefined) return '0KHz' const sampleRateKHz = Number(sampleRateHz) / 1000; return `${sampleRateKHz} KHz`; } /** * 格式化媒体格式类型显示 * @param mimeType 媒体格式类型字符串 * @return 格式化后的显示字符串 */ static formatMimeType(mimeType: string | undefined): string { if (StrUtil.isEmpty(mimeType) || mimeType === undefined) { return 'unknown'; } // 从MIME类型中提取格式部分,例如 "audio/mp3" -> "MP3" try { const parts = mimeType.split('/'); if (parts.length > 1) { return parts[1].toUpperCase(); } else { return mimeType.toUpperCase(); } } catch (err) { console.error(`格式化媒体类型出错: ${err}`); return 'unknown'; } } //根据字节获取大小 static formatFileSize(bytes:number) { const units = ['Bytes', 'Kbps', 'Mbps']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } if(size > 1024&&unitIndex==2){ size = size*0.1 } if(size > 1024&&unitIndex==2){ size = size*0.1 } if(size > 480&&unitIndex==2){ size = size*0.5 } // 保留两位小数, 四舍五入 size = Math.round(size * 10) / 10; return size + units[unitIndex] } //根据字节获取大小无单位 static formatFileSizeWithout(bytes:number) { const units = ['Bytes', 'Kbps', 'Mbps']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } if(size > 1024&&unitIndex==2){ size = size*0.1 } if(size > 1024&&unitIndex==2){ size = size*0.1 } if(size > 480&&unitIndex==2){ size = size*0.5 } // 保留两位小数, 四舍五入 size = Math.round(size * 10) / 10; return size } static copyText(text:string):boolean{ const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN,text) const systemPasteboard = pasteboard.getSystemPasteboard() systemPasteboard.setData(pasteboardData) systemPasteboard.getData().then((data)=>{ return data }) return false } static getDbm(rssi:number):number{ return Math.round((rssi / 2) + 100) } //根据数字形式的IP地址获取字符串形式的IP地址 static getIpAddrFromNum(ipNum: number): string { return (ipNum >>> 24) + '.' + (ipNum >> 16 & 0xFF) + '.' + (ipNum >> 8 & 0xFF) + '.' + (ipNum & 0xFF); } static resolveIP(ip:number) { let address: string = ip.toString() if (address === '0') { return '00:00:000:000' } address.substring(0, 2) return `${address.substring(0, 2)}:${address.substring(2, 4)}:${address.substring(4, 7)}:${address.substring(7, 10)}` } static isFreeTime():boolean{ if(DateUtil.isWeekend()){ return true } let str = DateUtil.getFormatDateStr(new Date(), 'HH') let a = Number(str); if(isNaN(a)) return false // ToastUtil.showToast('a='+a) if (a >= 7 && a <13) {//白天 return false; } if (a >= 13 && a <20) {//白天 return false; } if (a >= 0 && a < 7) {//凌晨 return true; } if (a >= 20 && a <= 24) {//晚上 return true; } return false } //判断是否是音乐文件 static isMusicByExtension(filename:string) { const extensions = CommonConstants.REAL_MUSIC_FORMAT const lastIndex = filename.lastIndexOf('.'); if (lastIndex!== -1) { const fileExtension = filename.slice(lastIndex).toLowerCase(); return extensions.includes(fileExtension); } return false; } //判断是否是媒体文件 音乐和视频都可以 static isMeidaByExtension(filename:string) { const extensions = CommonConstants.MEDIA_FORMAT const lastIndex = filename.lastIndexOf('.'); if (lastIndex!== -1) { const fileExtension = filename.slice(lastIndex).toLowerCase(); return extensions.includes(fileExtension); } return false; } //判断是否是视频文件 static isVideoByExtension(filename:string) { const extensions = CommonConstants.VIDEO_FORMAT const lastIndex = filename.lastIndexOf('.'); if (lastIndex!== -1) { const fileExtension = filename.slice(lastIndex).toLowerCase(); return extensions.includes(fileExtension); } return false; } // 获取缩略图 static async getFetchFrameByTime(filePath: string,time?:number,imageWidth?:number,imageHeight?:number) { if(Utility.isMusicByExtension(filePath)){ return undefined } let pixelMap:image.PixelMap|undefined =undefined; try{ // 创建AVImageGenerator对象 let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator() let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY); let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd }; avImageGenerator.fdSrc = avFileDescriptor; // 初始化入参 let timeUs = 0 console.info('onecold time='+time) if(time){ timeUs = (time > 0) ? time*50 : 0 } let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC if(imageWidth){ imageWidth =imageWidth/2.5 } if(imageHeight){ imageHeight = imageHeight/2.5 } let param: media.PixelMapParams = { width : imageWidth||300, height : imageHeight||400, } // 获取缩略图(promise模式) pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param) // 释放资源(promise模式) avImageGenerator.release() console.info(`release success.`) fs.closeSync(file); }catch (error) { console.error('uriGetAssets failed with err: ' + JSON.stringify(error)); } return pixelMap } // 获取fd文件路径 static async getFdDir(path:string){ let fdPath = 'fd://'; let file = await fileIo.open(path) fdPath = fdPath + '' + file.fd; return fdPath } // 根据uri获取文件名称 static getMediaNameByUri(myUri: string) { let myFileName = (myUri.split('/').pop()) as string; return decodeURIComponent(myFileName) } static msToStandardTime(ms:string):string{ let date = new Date(ms); let hours = date.getHours(); let minutes = date.getMinutes(); let seconds = date.getSeconds(); return hours + ':' + minutes + ':' + seconds; } static getTimestampFromDateStr(dateStr: string, format: string = 'yyyy-MM-dd HH:mm'): number { // 定义正则表达式以匹配日期字符串中的各部分 const regex = format .replace('yyyy', '(\\d{4})') .replace('MM', '(\\d{2})') .replace('dd', '(\\d{2})') .replace('HH', '(\\d{2})') .replace('mm', '(\\d{2})'); const match = new RegExp(regex).exec(dateStr); if (!match) { throw new Error('Invalid date string or format'); } // 解析匹配结果 const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); const hours = Number(match[4]); const minutes = Number(match[5]); // 创建 Date 对象 const dateObj = new Date(year, month - 1, day, hours, minutes); // 返回时间戳(以秒为单位) return Math.floor(dateObj.getTime() / 1000); } static getFormatDateStr(date: number | string | Date, format: string = 'yyyy-MM-dd HH:mm:ss'): string { // 将输入转换为 Date 对象 let dateObj: Date; if (typeof date === 'number') { // 如果是十位时间戳,转换为毫秒数 dateObj = new Date(date.toString().length === 10 ? date * 1000 : date); } else if (typeof date === 'string') { // 如果是字符串,尝试解析为 Date 对象 dateObj = new Date(date); } else { // 如果是 Date 对象,直接使用 dateObj = date; } // 定义替换规则 const replacements = new Map([ ['yyyy', String(dateObj.getFullYear())], ['MM', String(dateObj.getMonth() + 1).padStart(2, '0')], ['dd', String(dateObj.getDate()).padStart(2, '0')], ['HH', String(dateObj.getHours()).padStart(2, '0')], ['mm', String(dateObj.getMinutes()).padStart(2, '0')], ['ss', String(dateObj.getSeconds()).padStart(2, '0')] ]); // 替换格式字符串中的占位符 replacements.forEach((value, key) => { format = format.replace(new RegExp(key, 'g'), value); }); return format; } static async getFilePixelMapBig(uri:string){ let pixelMap:image.PixelMap|undefined = undefined if(Utility.isMusicByExtension(uri)){ pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri) }else{ pixelMap = await Utility.getFetchFrameByTime(uri) } return ImageUtil.pixelMapToBase64StrBig(pixelMap) } static async getFilePixelMap(uri:string){ let pixelMap:image.PixelMap|undefined = undefined if(Utility.isMusicByExtension(uri)){ pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri) }else{ pixelMap = await Utility.getFetchFrameByTime(uri) } return ImageUtil.pixelMapToBase64Str(pixelMap) } //根据filePathe获取对应的播放Index static getIndexFromList(localList:Array,filePath:string){ let list: Array = []; for(let i=0;i { let cover: image.PixelMap | undefined = undefined; 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(); metadata.author console.info(`get meta data, hasAudio: ${metadata.hasAudio}`); // 获取专辑封面(promise模式) cover = await avMetadataExtractor.fetchAlbumCover(); // 释放资源(promise模式) await avMetadataExtractor.release(); console.info('release success.'); } catch (error) { console.error('Error during metadata extraction:', error); } } else { console.warn('AVMetadataExtractor capability is not supported.'); } return cover; } static formatTimestamp(timestamp: number): string { // 将时间戳转换为 Date 对象 const date = new Date(timestamp); // 获取各个部分 const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要加1 const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); // 拼接成所需的格式 return `${year}-${month}-${day} ${hours}:${minutes}`; } //获取音乐资源的属性值, static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number,autoParseMusicName?:boolean): Promise { if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){ return new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'') } return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName) //华为官方不支持的格式 不支持内嵌歌词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 = '' } if(StrUtil.isNotEmpty(metadata.duration)){ if(metadata.duration) duration = convertSecondsToTime(metadata.duration.toString()) } if(StrUtil.isNotEmpty(metadata.mimeType)){ mimeType = metadata.mimeType } 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); } catch (error) { console.error('Error during metadata extraction:', error); } } else { console.warn('AVMetadataExtractor capability is not supported.'); } if(musicName==undefined) musicName = file.name item = new VideoItem(musicName,uri ,uri,type,videoSize,addTime,undefined,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 } }) } catch (error) { console.error('uriGetAssetsFromFile failed with err: ' + JSON.stringify(error)); } return item } static async readMetaInfoFFmpeg(context:Context,inputPath: string,type:number,autoParseMusicName?:boolean): Promise { return new Promise((resolve, reject) => { let commands = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", inputPath]; let outputJson = ""; 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 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|| ''; 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); } 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 = file.name } }else{ title = file.name } } 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(或 any) // 如果还是没有歌词内容,尝试查找自定义的lyrics-开头的属性(电脑版音乐标签内嵌歌词就是lyrics-XXX) if(StrUtil.isEmpty( videoItem.lyricContent)){ const tagsRecord = tags as Record; 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; } } } } 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('readMetaInfoFFmpeg asset hasCover: ', hasCover); let md5Name = await MD5.digestSync(inputPath) let imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}.jpg`; console.info('readMetaInfoFFmpeg asset imagePath: ', imagePath); // 如果有封面图片,则提取 try { if (hasCover) { //提取封面 let isSuccess:boolean= await getFFmpegCover(inputPath, imagePath); if(isSuccess){ imagePath = fileUri.getUriFromPath(imagePath) }else{ imagePath = '' } 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(' 提取封面图片失败:', error.message); } if(sampleRate&&format.bit_rate){ videoItem.md5Str = determineAudioQuality( videoItem.mimeType,Number(format.bit_rate),Number(sampleRate)) } console.info('Successfully parsed metadata:', videoItem); resolve(videoItem); }) } catch (error) { console.error('Failed to parse metadata:', error); reject(new Error('Failed to parse metadata: ' + error.message)); } }).catch((error: Error) => { console.error(`Execution failed with error: ${error.message}`); reject(error); }); }); } private completionNum(num: number): string | number { if (num < 10) { return '0' + num; } else { return num; } } //根据字节获取大小 static formatFSize(bytes:number):string { const units = ['Bytes', 'K', 'M', 'G']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } // 保留两位小数, 四舍五入 size = Math.round(size * 10) / 10; return size + units[unitIndex] } static gotoMarket(context:common.UIAbilityContext,bundleName:string) { const want: Want = { uri: `store://appgallery.huawei.com/app/detail?id=${bundleName}` }; context.startAbility(want).then(()=>{ //拉起成功 }).catch(()=>{ // 拉起失败 }); } static async doShare(item:VideoItem,context:common.UIAbilityContext){ // 生成视频封面图 const imagePackerApi: image.ImagePacker = image.createImagePacker(); const buffer: ArrayBuffer = await (imagePackerApi.packing(await Utility.getFetchFrameByTime(item.filePath), { format: 'image/jpeg', quality: 30 }) as Promise); // 构造ShareData,需配置一条有效数据信息 let shareData: systemShare.SharedData = new systemShare.SharedData({ utd: uniformTypeDescriptor.UniformDataType.MEDIA, uri: fileUri.getUriFromPath(item.filePath), title: item.name, // 不传title字段时,显示视频文件名 // description: '好听的音乐', // 不传description字段时,显示视频大小 thumbnail: new Uint8Array(buffer), // 优先使用传递的缩略图做预览 不传则默认使用视频第一帧画面做预览图 }); // 进行分享面板显示 let controller: systemShare.ShareController = new systemShare.ShareController(shareData); // let context = getContext(this) as common.UIAbilityContext; controller.show(context, { selectionMode: systemShare.SelectionMode.SINGLE, previewMode: systemShare.SharePreviewMode.DETAIL, }).then(() => { console.info('ShareController show success.'); }).catch((error: BusinessError) => { console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`); }); } //分享索引index的视频 static async doShareMusic(item:VideoItem,context:common.UIAbilityContext){ // 构造ShareData,需配置一条有效数据信息 let shareData: systemShare.SharedData = new systemShare.SharedData({ utd: uniformTypeDescriptor.UniformDataType.AUDIO, uri: fileUri.getUriFromPath(item.filePath), title: item.name, // 不传title字段时,显示视频文件名 description: '好听的音乐', // 不传description字段时,显示视频大小 }); // 进行分享面板显示 let controller: systemShare.ShareController = new systemShare.ShareController(shareData); // let context = getContext(this) as common.UIAbilityContext; controller.show(context, { selectionMode: systemShare.SelectionMode.SINGLE, previewMode: systemShare.SharePreviewMode.DETAIL, }).then(() => { console.info('ShareController show success.'); }).catch((error: BusinessError) => { console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`); }); } static getNameList(localList:Array){ let list: Array = []; for(let i=0;i):string{ let list: Array = []; for(let i=0;i,type:number){ let list: Array = []; for(let i=0;i,type:number){ let list: Array = []; for(let i=0;i, playType: number): VideoItem | undefined { // 处理空列表情况 if (!localList || localList.length === 0) { return undefined; } // 当 playType 为 3 时随机返回一个元素 if (playType === 3) { const randomIndex = Math.floor(Math.random() * localList.length); return localList[randomIndex]; } // 其他情况返回第一个元素 else { return localList[0]; } } /** * 根据文件路径从项数组中查找对应的VideoItem * @param list VideoItem数组 * @param filePath 文件路径 * @returns 对应的VideoItem,如果未找到则返回undefined */ static getItemByFilePath(list: Array, filePath: string): VideoItem | undefined { if (!list || list.length === 0 || !filePath) { return undefined; } for (let i = 0; i < list.length; i++) { if (list[i].filePath === filePath) { return list[i]; } } return undefined; } //根据filePath获取当前索引index static getCurIndexFromGlobalList(localList:Array,filePath:string){ let index: number = 0; for(let i=0;i,item:VideoItem|undefined){ if(ArrayUtil.isEmpty(favList)){ return false } if(item===undefined){ return false } for(let i=0;i,item:VideoItem|undefined){ if(ArrayUtil.isEmpty(facList)){ return false } if(item===undefined){ return false } for(let i=0;i,type:number){ let list: Array = []; for(let i=0;i { try { // 1. 同步化BundleInfo获取 const bundleInfo = await bundleManager.getBundleInfoForSelf( bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION ); // 2. 安全获取labelId(新增空值校验) const labelId = bundleInfo.appInfo?.labelId; if (!labelId || labelId <= 0) { throw new Error("Invalid labelId: " + labelId); } // 3. 异步资源解析(替换危险的getStringSync) return await context.resourceManager.getStringValue(labelId); } catch (err) { console.error(`[${new Date().toISOString()}] AppName Error: CODE=${err.code}, MSG=${err.message}`); // 4. 多级降级策略 return AppUtil.getBundleName() // 最终兜底 } } //按名称升序 static doSortListAscending( list:Array,isShowFileName?:boolean){ let options: Intl.CollatorOptions = { localeMatcher: "lookup", usage: "sort", sensitivity: "accent", // 对中文更友好 numeric:true }; const collator = new Intl.Collator("zh-CN", options); list.sort((a, b) => { const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type); if (typeOrder !== 0) { return typeOrder; } if(isShowFileName){ return collator.compare(a.fileName||a.name, b.fileName||b.name) }else{ return collator.compare(a.name, b.name) } }); } //按名称降序 static doSortListDescending(list:Array,isShowFileName?:boolean){ const options: Intl.CollatorOptions = { localeMatcher: "lookup", usage: "sort", sensitivity: "accent", // 对中文更友好 numeric:true }; const collator = new Intl.Collator("zh-CN", options); list.sort((a, b) => { const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type); if (typeOrder !== 0) { return typeOrder; } if(isShowFileName){ return collator.compare(b.fileName||b.name, a.fileName||a.name) }else{ return collator.compare(b.name, a.name) } // const partsA = extractParts(a.name); // const partsB = extractParts(b.name); // // const nonNumericComparison = collator.compare(partsB.nonNumeric, partsA.nonNumeric); // if (nonNumericComparison !== 0) { // return nonNumericComparison; // } // return partsB.numeric - partsA.numeric; }); } /** * 读取本地会员到期时间(解密.vv文件) * @returns */ static async getLocalNobleExpireDate(): Promise { // return "2025-08-08 12:00"; try { const documentViewPicker = new picker.DocumentViewPicker() let documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD }) let download_path = new fileUri.FileUri(documentSaveResult[0]).path + '/' let filePath = download_path + STR_LOCK_VIDEO+ '/' + VIP_FILEPATH let expireDate = Utility.readDataFromFile(filePath) return expireDate; } catch (e) { return ''; } } static readDataFromFile(filePath: string) { if(!FileUtil.accessSync(filePath)){ return '' } let readTextOptions: ReadTextOptions = { offset: 0, length: 0, encoding: 'utf-8' }; let stat = fileIo.statSync(filePath); readTextOptions.length = stat.size; let str = fileIo.readTextSync(filePath, readTextOptions); const parsedData:VipData = JSON.parse(str) as VipData; // LogUtil.info('onecold parsedData.expireDate = ' + parsedData.expireDate) let result = Base64Util.decodeSync( parsedData.expireDate) // LogUtil.info('onecold 解密的result = ' +StrUtil.unit8ArrayToStr(result)) return StrUtil.unit8ArrayToStr(result); } } interface PathStat { path: string; isDirectory: boolean; } async function isDirectory(filePath: string): Promise { try { const stat = await fs.stat(filePath); return stat.isDirectory(); } catch (error) { console.error('Error getting file stat:', error); return false; } } //排序模式参数 interface VideoNameParts { nonNumeric: string; numeric: number; suffix:string; } function extractParts(name: string): VideoNameParts { // 更完善的正则表达式,处理各种情况 // 匹配:非数字前缀 + 数字部分 + 可选的剩余部分 const match = name.match(/^(\D*)(\d+)(.*)$/); if (match) { return { nonNumeric: match[1] || '', // 非数字前缀 numeric: parseInt(match[2], 10), // 数字部分 suffix: match[3] || '' // 剩余部分(可能包含扩展名等) }; } // 如果没有找到数字模式,将整个名称作为非数字部分 return { nonNumeric: name, numeric: 0, suffix: '' }; } function getInstallTime(): number | null { // 从本地存储中获取安装时间 return PreferencesUtil.getNumberSync('installTime') } function setInstallTime(time: number) { // 将安装时间存储到本地存储中 PreferencesUtil.putSync('installTime',time) } function fetchAlbumCover(avMetadataExtractor: media.AVMetadataExtractor): Promise { return new Promise((resolve, reject) => { avMetadataExtractor.fetchAlbumCover((error: BusinessError, pixelMap: image.PixelMap) => { if (error) { console.error(`Failed to fetch AlbumCover, error = ${JSON.stringify(error)}`); resolve(null); } else { resolve(pixelMap); } avMetadataExtractor.release(); }); }); } //排序类型 function getTypeOrder(type: number) { switch (type) { case CommonConstants.TYPE_IS_DIR: return 1; // First case CommonConstants.TYPE_IS_CSJAD: return 2; // Middle case CommonConstants.TYPE_LOCAL: return 3; // Last default: return 4; // Unknown types, if any, go last } } function convertSecondsToTime(secondsStr: string): string { if (!secondsStr || isNaN(Number(secondsStr))) { return "00:00"; } let seconds = parseInt(secondsStr, 10); seconds = Math.floor(seconds / 1000); const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; const formatNumber = (num: number) => num.toString().padStart(2, '0'); if (hours > 0) { return `${formatNumber(hours)}:${formatNumber(minutes)}:${formatNumber(secs)}`; } else { return `${formatNumber(minutes)}:${formatNumber(secs)}`; } } /** * 秒数 → 智能时间格式(自动选择 MM:SS 或 HH:MM:SS) * @param seconds 秒数字符串(如 "283.524351" 或 "3675") * @param forceHHMMSS 强制使用 HH:MM:SS 格式(默认自动判断) * @returns 格式化后的时间字符串 */ function formatDuration(seconds: string, forceHHMMSS: boolean = false): string { // 1. 校验输入 const secNum = parseFloat(seconds); if (isNaN(secNum) || secNum < 0) return forceHHMMSS ? "00:00:00" : "00:00"; // 2. 计算时间分量 const totalSec = Math.floor(secNum); const hours = Math.floor(totalSec / 3600); const mins = Math.floor((totalSec % 3600) / 60); const secs = totalSec % 60; // 3. 格式化输出 const pad = (n: number) => n.toString().padStart(2, '0'); return forceHHMMSS || hours > 0 ? `${pad(hours)}:${pad(mins)}:${pad(secs)}` // HH:MM:SS : `${pad(mins)}:${pad(secs)}`; // MM:SS } /** * 定义解析结果的数据结构 */ class MusicInfo { artist: string = ""; // 艺术家名称 title: string = ""; // 歌曲名称 isValid: boolean = false; // 格式是否有效 } /** * 精简版常见中文姓氏(音乐场景优化版) */ const COMMON_CHINESE_SURNAMES = [ // 超常见姓氏(覆盖约80%人口) '李','王','张','刘','陈','杨','黄','赵','吴','周', '徐','孙','马','朱','胡','林','郭','何','高','罗', '郑','梁','谢','宋','唐','许','韩','邓','曹','彭', '曾','萧','田','董','潘','袁','于','蔡','余','杜', // 音乐行业常见姓氏(歌手高频姓) '汪','苏','薛','谭','华','金','魏','陶','姜','窦', '章','毛','易','方','宋','任','沈','贾','江','孔', // 组合/乐队常见字 '羽','泉','信','乐','花','飞','龙','风','云','草' ]; /** * 歌手特征关键词(优化版) */ const ARTIST_KEYWORDS = [ // 中文特征 '乐队','组合','乐团','和','&','合唱',' featuring', // 英文特征 'feat','ft','with','vs','presents','presents', // 符号特征 '×','X','※','◆','♫' ]; /** * 智能解析音乐文件名(支持多种分隔符和格式) * @param fileName - 待解析的完整文件名 * @returns 结构化音乐信息 */ /** * 智能解析音乐文件名 */ function parseMusicFileName(fileName: string): MusicInfo { const result: MusicInfo = new MusicInfo(); if (!fileName) return result; // 预处理 const cleanName: string = fileName.trim(); const lastDotIndex: number = cleanName.lastIndexOf('.'); const baseName: string = lastDotIndex > 0 ? cleanName.substring(0, lastDotIndex).trim() : cleanName; // 支持的分隔符(明确定义类型) const separators: string[] = ['-', '-', '—', '~', '~']; // 寻找分隔位置 let bestSplitIndex: number = -1; for (const sep of separators) { const index: number = baseName.lastIndexOf(sep); if (index > bestSplitIndex) { bestSplitIndex = index; } } // 分割字符串 if (bestSplitIndex > 0 && bestSplitIndex < baseName.length - 1) { let part1: string = baseName.substring(0, bestSplitIndex).trim(); let part2: string = baseName.substring(bestSplitIndex + 1).trim(); // 处理前缀序号 part1 = part1.replace(/^\d+[\s\.\-- —~~]*/, '').trim(); // 判断歌手部分(明确定义返回类型) const identifyArtist = (str: string): boolean => { return COMMON_CHINESE_SURNAMES.some((surname: string) => str.startsWith(surname) || new RegExp(`[ ,,、&&]${surname}`).test(str) ) || ARTIST_KEYWORDS.some((keyword: string) => str.includes(keyword) ); }; // 判断歌手位置 const part1IsArtist: boolean = identifyArtist(part1); const part2IsArtist: boolean = identifyArtist(part2); if (part1IsArtist && !part2IsArtist) { result.artist = part1; result.title = part2; } else if (part2IsArtist && !part1IsArtist) { result.artist = part2; result.title = part1; } else { result.artist = part1.length <= part2.length ? part1 : part2; result.title = part1.length <= part2.length ? part2 : part1; } // 后处理 result.title = result.title .replace(/(?:\(|()[^))]*(?:)|\))/g, '') .replace(/\s*[—-]\s*(?:Live|Version|Remix|伴奏).*/i, '') .trim(); // 有效性验证 result.isValid = result.artist.length > 0 && result.title.length > 0; } else { result.title = baseName; result.isValid = result.title.length > 0; } return result; } function getFileNameWithoutExtension(filePath: string): string { const fileName = filePath.split('/').pop() || ''; const lastDotIndex = fileName.lastIndexOf('.'); return lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName; } /** * 从音乐文件中提取封面 * @param inputPath 音乐文件路径 * @returns Promise */ // async function getFFmpegCover(inputPath: string, outputPath: string) : Promise{ // let commands = ["ffmpeg", "-y","-i", inputPath, "-map", "0:v", "-c:v", "copy", outputPath]; // FFmpeg.execute(commands, { // logCallback: (logLevel: number, logMessage: string) => { // console.info(`[FFmpeg LOG] [${logLevel}]${logMessage}`) // }, // progressCallback: (message: string) => { // console.info(`[FFmpeg progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`) // }, // }) // .then(() => { // return true // console.info("FFmpeg execution succeeded."); // }) // .catch((error: Error) => { // return false // console.error(`FFmpeg execution failed with error: ${error.message}`); // }); // return ; // } /** * 从音乐文件中提取封面 * @param inputPath 音乐文件路径 * @param outputPath 封面输出路径 * @returns Promise 表示操作是否成功 */ async function getFFmpegCover(inputPath: string, outputPath: string): Promise { let commands = ["ffmpeg", "-y","-i", inputPath, "-map", "0:v", "-c:v", "copy", outputPath]; try { await FFmpeg.execute(commands, { logCallback: (logLevel: number, logMessage: string) => { console.info(`[FFmpeg LOG] [${logLevel}] ${logMessage}`); }, progressCallback: (message: string) => { console.info(`[FFmpeg progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`); }, }); console.info("FFmpeg execution succeeded."); return true; } catch (error) { console.error(`FFmpeg execution failed with error: ${error instanceof Error ? error.message : String(error)}`); return false; } } /** * 从视频文件中提取封面 提前视频第5帧的封面 * @param inputPath 文件路径 * @returns Promise */ async function getVideoFFmpegCover(inputPath: string, outputPath: string) { // 更可靠的命令,专门提取视频第一帧作为封面 const commands = [ "ffmpeg", "-y", // 覆盖输出文件(如果存在) "-i", inputPath, // 输入文件 "-ss", "00:00:00", // 定位到开始位置 "-vframes", "1", // 只提取1帧 "-q:v", "2", // 高质量JPEG(1-31,2是最高质量) "-f", "image2", // 强制输出为图像格式 outputPath ]; FFmpeg.execute(commands, { logCallback: (logLevel: number, logMessage: string) => { console.info(`[FFmpegX LOG] [${logLevel}]${logMessage}`) }, progressCallback: (message: string) => { console.info(`[FFmpegX progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`) }, }) .then(() => { }) .catch((error: Error) => { console.error(`FFmpegX execution failed with error: ${error.message}`); }); } /** * 从音乐文件中提取歌词内容 * @param inputPath 音乐文件路径 * @returns Promise 直接返回歌词内容,若无歌词则返回空字符串 */ async function extractLyricsContent(inputPath: string): Promise { return new Promise(async (resolve, reject) => { try { // 1. 使用ffprobe获取元数据 const commands = [ 'ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', inputPath ]; let outputJson = ''; await FFmpeg.execute(commands, { outputCallback: (message: string) => outputJson += message, }); // 2. 解析歌词标签 const metadata:FFprobeMetadata = JSON.parse(outputJson); const tags = metadata.format?.tags || {}; // 3. 从常见标签中查找歌词(优先级顺序) const lyricContent = tags.LYRICS || // 标准标签 tags.lyrics || // 小写变体 tags.USLT || // ID3v2标签 tags.UNSYNCEDLYRICS || ''; resolve(lyricContent.trim()); } catch (error) { reject(`解析失败: ${error instanceof Error ? error.message : String(error)}`); } }); } /** * 解析音乐文件元数据(包含歌词和其他关键字段) * @param inputPath 文件路径 * @returns Promise 包含完整元数据的对象 */ async function parseAudioMetadata(inputPath: string): Promise { return new Promise(async (resolve, reject) => { try { // 1. 执行FFprobe命令 const commands: string[] = [ 'ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', inputPath ]; let outputJson = ''; await FFmpeg.execute(commands, { outputCallback: (message: string) => outputJson += message, }); // 2. 解析元数据 const metadata = JSON.parse(outputJson) as FFprobeMetadata; const format:FFprobeFormat = metadata.format ; const tags = format.tags||{} ; // 3. 构建VideoItem基础信息 const videoItem = new VideoItem( tags.title || getFileNameWithoutExtension(inputPath), inputPath, // id inputPath, CommonConstants.TYPE_LOCAL, 0, new Date().toISOString() // 使用当前时间作为默认创建时间 ); // 4. 设置关键字段 videoItem.bit_rate = format.bit_rate || ''; videoItem.probe_score = format.probe_score || 0; videoItem.year = tags.TYER || tags.date || ''; videoItem.lyricContent = tags.LYRICS ?? tags.lyrics ?? tags.USLT ?? tags.UNSYNCEDLYRICS ?? ''; // 5. 设置其他可选字段 videoItem.artist = tags.artist || ''; videoItem.album = tags.album || ''; videoItem.duration = format.duration || ''; videoItem.nb_streams = format.nb_streams; videoItem.nb_programs = format.nb_programs; resolve(videoItem); } catch (error) { reject(new Error(`元数据解析失败: ${error instanceof Error ? error.message : String(error)}`)); } }); } /** * 将比特率(bps)转换为 kbps 并格式化 * @param bitRate 比特率字符串(如 "5644802") * @param decimalPlaces 保留小数位数(默认0) * @returns 格式化后的 kbps 字符串(如 "5644 kbps") */ function formatBitrateToKbps(bitRate: string, decimalPlaces: number = 0): string { // 1. 转换为数字 const bitsPerSecond = parseInt(bitRate); if (isNaN(bitsPerSecond) || bitsPerSecond < 0) return "0 kbps"; // 2. 计算 kbps(1 kbps = 1000 bps) const kbps = bitsPerSecond / 1000; // 3. 格式化输出 return `${kbps.toFixed(decimalPlaces)} kbps`; } // 定义音质等级 export enum AudioQuality { LQ = "LQ", // 低音质 HQ = "HQ", // 高音质 SQ = "SQ", // 无损 HIRES = "Hi-Res", // 高解析度 HR = "HR" // 高解析度 } // 明确定义支持的音频格式类型 interface SupportedFormats { LOSSY: Set; LOSSLESS: Set; } // 声明并初始化支持的音频格式(符合类型定义) const SUPPORTED_FORMATS: SupportedFormats = { LOSSY: new Set(['mp3', 'aac', 'ogg', 'opus', 'wma']), LOSSLESS: new Set(['flac', 'alac', 'ape', 'wav', 'aiff', 'dsf','aif']) }; /** * 根据音频参数判断音质等级 * @param format 编码格式(字符串,不区分大小写,如"MP3"或"flac") * @param bitrate 比特率(单位:bps,如320kbps需传入320000) * @param sampleRate 采样率(单位:Hz,如44.1kHz需传入44100) * @param bitDepth 位深(单位:bit,默认16bit,Hi-Res需≥24bit) * @returns AudioQuality 音质等级 */ function determineAudioQuality( format: string, bitrate: number, sampleRate: number, ): AudioQuality { // 统一转为小写便于比较 const normalizedFormat = format.toLowerCase(); // --------------- 第一步:判断是否无损/Hi-Res --------------- const isLossless = SUPPORTED_FORMATS.LOSSLESS.has(normalizedFormat); if (isLossless) { // Hi-Res标准:采样率≥96kHz且位深≥24bit if (sampleRate >= 48000 ) { return AudioQuality.HIRES; } // CD级无损标准:采样率≥44.1kHz且位深≥16bit if (sampleRate >= 44100) { return AudioQuality.SQ; } } // --------------- 第二步:有损音质判断 --------------- // 高音质标准:比特率≥256kbps且采样率≥44.1kHz if (bitrate >= 256000 && sampleRate >= 44100) { return AudioQuality.HQ; } // 其余情况为低音质 else { return AudioQuality.LQ; } } function getFileFormatByPath(filePath: string): string { // 检查文件路径是否有效 if (!filePath || filePath.length === 0) { console.error(' 文件路径不能为空'); return ''; } // 获取最后一个点号的位置 const lastDotIndex = filePath.lastIndexOf('.'); // 如果没有点号或者点号在最后一位,返回空字符串 if (lastDotIndex === -1 || lastDotIndex === filePath.length - 1) { return ''; } // 返回点号后的部分(转换为小写) return filePath.substring(lastDotIndex + 1).toLowerCase(); } // 检查乱码的简单方法 function hasGarbledText(tags: FFMpegTags): boolean { if (!tags) return false; const text = [tags.title, tags.artist, tags.album].join(""); return ( /[À-ÿµØÇò]/.test(text) || // 拉丁乱码 /[\uFFFD�]/.test(text) || // Unicode替换字符 /[�□▪▫▢▣▤▥▦▧▨▩▪▫◊○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◢◣◤◥◦◧◨◩◪◫◬◭◮◯]/.test(text) || // 几何图形 /[\uD800-\uDFFF]/.test(text) // 无效的UTF-16代理对 ); } // function hasGarbledText(tags: FFMpegTags): boolean { // if (!tags) return false; // const text = [tags.title, tags.artist, tags.album].join(""); // return /[À-ÿµØÇò]/.test(text); // 匹配常见乱码字符 // } /** * 提取华为接口的媒体文件元数据 * @param filePath 文件路径(如:/data/storage/el1/base/test.mp3 ) * @returns Promise 包含专辑、艺术家、标题的元数据对象 */ export async function extractHwMediaMetadata(filePath: string): Promise { const tags: FFMpegTags = {}; if (!canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) { console.warn("Metadata extraction not supported on this device"); return tags; } try { const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor(); const fd = await fs.open(filePath, fs.OpenMode.READ_ONLY); // 设置文件描述符并提取元数据 avMetadataExtractor.fdSrc = fd; const metadata = await avMetadataExtractor.fetchMetadata(); // 映射标准FFMpeg标签 if (metadata) { tags.album = metadata.album ?? undefined; tags.artist = metadata.artist ?? metadata.author ?? undefined; // 兼容不同元数据字段 tags.title = metadata.title ?? undefined; } // 关闭文件描述符 await fs.close(fd); return tags; } catch (error) { const err = error as BusinessError; console.error(`Metadata extraction failed: ${err.code}, ${err.message}`); return tags; } return tags; }