Utility.ets 73 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283
  1. import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
  2. import {
  3. AppUtil,
  4. ArrayUtil,
  5. Base64Util,
  6. DateUtil, FileUtil, ImageUtil, LogUtil,
  7. MD5,
  8. PreferencesUtil,
  9. RandomUtil, StrUtil } from '@pura/harmony-utils';
  10. import { media } from '@kit.MediaKit';
  11. import fs, { ReadTextOptions } from '@ohos.file.fs';
  12. import { image } from '@kit.ImageKit';
  13. import fileIo from '@ohos.file.fs';
  14. import { VideoItem } from '../../viewmodel/VideoItem';
  15. import { photoAccessHelper } from '@kit.MediaLibraryKit';
  16. import { dataSharePredicates, uniformTypeDescriptor } from '@kit.ArkData';
  17. import { systemShare } from '@kit.ShareKit';
  18. import { fileUri, picker } from '@kit.CoreFileKit';
  19. import { common, UIAbility, Want } from '@kit.AbilityKit';
  20. import { CommonConstants, STR_LOCK_VIDEO, VIP_FILEPATH } from '../constants/CommonConstants';
  21. import { window } from '@kit.ArkUI';
  22. import { bundleManager } from '@kit.AbilityKit'
  23. import { pinyin4js } from '@ohos/pinyin4js';
  24. import { VipData } from '../../viewmodel/VipData';
  25. import { VipPage } from '../../pages/VipPage';
  26. import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
  27. import ReqPermissionUtil from './ReqPermissionUtil';
  28. import { commentManager } from '@kit.AppGalleryKit';
  29. import { hilog } from '@kit.PerformanceAnalysisKit';
  30. import Logger from './Logger';
  31. export interface FFMpegTags {
  32. album?: string;
  33. ALBUM?: string;
  34. artist?: string;
  35. ARTIST?: string;
  36. TITLE?: string;
  37. title?: string;
  38. track?: string;
  39. TRACK?: string;
  40. TYER?: string;
  41. year?: string;
  42. genre?:string;
  43. GENRE?:string;
  44. date?: string;
  45. DATE?: string;
  46. LYRICS?: string;
  47. lyrics?: string; // 小写变体
  48. USLT?: string; // ID3v2同步歌词
  49. UNSYNCEDLYRICS?: string; // ID3v2非同步歌词
  50. // Add any other tag properties you expect
  51. ALBUMARTIST?:string//专辑艺术家
  52. COMPOSER?:string//作曲家
  53. LYRICIST?:string//作词家
  54. COMMENT?:string//注释
  55. albumartist?:string//专辑艺术家
  56. album_artist?:string//专辑艺术家
  57. TPE2?:string//专辑艺术家
  58. composer?:string//作曲家
  59. lyricist?:string//作词家
  60. TEXT?:string//作词家
  61. comment?:string//注释
  62. COMM?:string//注释
  63. comm?:string//注释
  64. disc?:string//音碟号
  65. }
  66. interface FFprobeFormat {
  67. filename: string;
  68. nb_streams: number;
  69. nb_programs: number;
  70. format_name: string;
  71. format_long_name: string;
  72. duration: string;
  73. size: string;
  74. bit_rate: string;
  75. probe_score: number;
  76. tags?: FFMpegTags;
  77. }
  78. interface FFprobeStream {
  79. // Define stream properties as needed
  80. codec_name?: string;
  81. sample_fmt?: string;
  82. codec_type?: string; // 流类型,如"audio"、"video"
  83. sample_rate?: string; // 采样率
  84. bit_rate?: string; // 比特率
  85. disposition?: StreamDisposition; // 添加disposition属性
  86. bits_per_raw_sample?: string; // 位深
  87. channels?: string; // 声道数
  88. channel_layout?: string; // 声道布局
  89. start_time?: string; // 起始播放
  90. }
  91. interface StreamDisposition {
  92. default?: number;
  93. dub?: number;
  94. original?: number;
  95. comment?: number;
  96. lyrics?: number;
  97. karaoke?: number;
  98. forced?: number;
  99. hearing_impaired?: number;
  100. visual_impaired?: number;
  101. clean_effects?: number;
  102. attached_pic?: number; // 添加封面图片标识
  103. timed_thumbnails?: number;
  104. captions?: number;
  105. descriptions?: number;
  106. metadata?: number;
  107. dependent?: number;
  108. still_image?: number;
  109. }
  110. export interface FFprobeMetadata {
  111. streams: FFprobeStream[];
  112. format: FFprobeFormat;
  113. }
  114. export class Utility {
  115. private constructor() {}
  116. static isOpenTime():boolean{
  117. const currentDate = new Date();
  118. const targetDate = new Date(CommonConstants.OPEN_DATE);
  119. if (currentDate > targetDate) {
  120. return true
  121. }
  122. return false
  123. }
  124. static readonly FOREVER_DATE: string = 'forever';
  125. /**
  126. * 是否会员(只看线上会员状态)
  127. */
  128. static isNoble(): boolean {
  129. // 只判断线上会员状态
  130. return PreferencesUtil.getBooleanSync('hasActiveSubscription', false);
  131. }
  132. static isForever(): boolean {
  133. // 判断是不是永久会员
  134. return PreferencesUtil.getBooleanSync('isForever', false);
  135. }
  136. /**
  137. * 旧版客户端判断会员状态
  138. * @returns
  139. */
  140. static isNobleForOld():boolean{
  141. if(!Utility.isOpenTime())//提交审核的审核,刚刚开始可以全部是赞助用户。
  142. return true
  143. return PreferencesUtil.getBooleanSync('isNoble',false)
  144. }
  145. /**
  146. * setNoble方法废弃,保留兼容但不做任何操作
  147. */
  148. static setNoble(expireDate:string){
  149. let isNoble = false
  150. if(StrUtil.isNotEmpty(expireDate)){
  151. if(expireDate===VipPage.FOREVER_DATE){
  152. isNoble = true
  153. }else{
  154. const currentDate = new Date();
  155. const targetDate = DateUtil.getFormatDate(expireDate)
  156. if (currentDate > targetDate) {
  157. isNoble = false
  158. }else{
  159. isNoble = true
  160. }
  161. }
  162. }
  163. PreferencesUtil.putSync('isNoble',isNoble)
  164. }
  165. /**
  166. * 判断用户安装app是否超过2天,超过2天(2800分钟)才展示广告 显示广告 24*60=1440 1440*2=2800
  167. * @param day
  168. * @returns
  169. */
  170. static isPassInstallTime(day:number):boolean{
  171. //获取当前时间
  172. let currentTime = new Date().getTime();
  173. // 尝试从存储中获取安装时间
  174. let installTime = getInstallTime();
  175. if (installTime===0||installTime === null|| installTime===undefined) {
  176. // 如果没有存储安装时间,则存储当前时间为安装时间
  177. setInstallTime(currentTime);
  178. return false
  179. } else {
  180. // 计算时间差(以分钟为单位)
  181. const timeDifference = (currentTime - installTime) / (1000 * 60);
  182. LogUtil.debug("onecold timeDifference=" + timeDifference)
  183. // 如果超过5分钟,显示广告 24*60=1440 1440*2=2800
  184. if (timeDifference > 24*60*day) {
  185. return true
  186. }else{
  187. return false
  188. }
  189. }
  190. }
  191. // 定义一个函数来获取当前的时间段
  192. static getTimePeriod(): string {
  193. const now = new Date();
  194. const hours = now.getHours();
  195. if (hours >= 5 && hours < 12) {
  196. return "早上好";
  197. } else if (hours >= 12 && hours < 18) {
  198. return "下午好";
  199. } else if (hours >= 18 && hours < 22) {
  200. return "晚上好";
  201. } else {
  202. return "夜深了";
  203. }
  204. }
  205. static optimizedFormat(speed: number): string {
  206. if (speed) {
  207. const str = speed.toFixed(2);
  208. let end = str.length;
  209. while (end > 0 && (str[end - 1] === '0' || str[end - 1] === '.')) {
  210. end--;
  211. if (str[end] === '.') {
  212. break;
  213. }
  214. }
  215. return str.slice(0, end || 1) + 'x';
  216. } else {
  217. return '1x'
  218. }
  219. }
  220. static isHaoOpenTime():boolean{
  221. const currentDate = new Date();
  222. const targetDate = new Date('2025-04-20');
  223. if (currentDate < targetDate) {
  224. return true
  225. }
  226. return false
  227. }
  228. static addDays(date: Date, days: number): Date {
  229. const result = new Date(date);
  230. result.setDate(result.getDate() + days);
  231. return result;
  232. }
  233. static convertToKHz(sampleRateHz: string|undefined): string {
  234. if(StrUtil.isEmpty(sampleRateHz)||sampleRateHz ===undefined)
  235. return '0KHz'
  236. const sampleRateKHz = Number(sampleRateHz) / 1000;
  237. return `${sampleRateKHz} KHz`;
  238. }
  239. static showHaoPingDialog(uicontext: UIContext,context:common.UIAbilityContext,appName:string,bundleName:string) {
  240. try {
  241. uicontext.getPromptAction().showDialog({
  242. title: `喜欢${appName}吗?`,
  243. message:'如果这个应用对您有帮助,请给我们一个五星好评,谢谢!让我们一起为鸿蒙生态助力,您的支持是我们努力提升的动力!让我们一起共赴鸿蒙满天星光。',
  244. buttons: [
  245. {
  246. text: '下次再说',
  247. color: '#000000'
  248. },
  249. {
  250. text: '给个好评',
  251. color: '#000000'
  252. },
  253. ]
  254. }, (err, data) => {
  255. if (err) {
  256. console.error('showDialog err: ' + err);
  257. return;
  258. }
  259. // 根据点击的按钮索引处理不同逻辑
  260. if (data.index === 1) {//查看路径
  261. Utility.gotoMarket(context, bundleName)
  262. }
  263. console.info('showDialog success callback, click button: ' + data.index);
  264. });
  265. } catch (error) {
  266. let message = (error as BusinessError).message;
  267. let code = (error as BusinessError).code;
  268. console.error(`showDialog args error code is ${code}, message is ${message}`);
  269. };
  270. }
  271. static isMerge(localList:Array<VideoItem>,packName:string):boolean{
  272. if(ArrayUtil.isEmpty(localList))
  273. return false
  274. for(let i=0;i<localList.length;i++){
  275. if(!localList[i].filePath.includes(packName)){
  276. return false
  277. }
  278. }
  279. return true
  280. }
  281. /**
  282. * 格式化媒体格式类型显示
  283. * @param mimeType 媒体格式类型字符串
  284. * @return 格式化后的显示字符串
  285. */
  286. static formatMimeType(mimeType: string | undefined): string {
  287. if (StrUtil.isEmpty(mimeType) || mimeType === undefined) {
  288. return 'unknown';
  289. }
  290. // 从MIME类型中提取格式部分,例如 "audio/mp3" -> "MP3"
  291. try {
  292. const parts = mimeType.split('/');
  293. if (parts.length > 1) {
  294. return parts[1].toUpperCase();
  295. } else {
  296. return mimeType.toUpperCase();
  297. }
  298. } catch (err) {
  299. console.error(`格式化媒体类型出错: ${err}`);
  300. return 'unknown';
  301. }
  302. }
  303. //根据字节获取大小
  304. static formatFileSize(bytes:number) {
  305. const units = ['Bytes', 'Kbps', 'Mbps'];
  306. let size = bytes;
  307. let unitIndex = 0;
  308. while (size >= 1024 && unitIndex < units.length - 1) {
  309. size /= 1024;
  310. unitIndex++;
  311. }
  312. if(size > 1024&&unitIndex==2){
  313. size = size*0.1
  314. }
  315. if(size > 1024&&unitIndex==2){
  316. size = size*0.1
  317. }
  318. if(size > 480&&unitIndex==2){
  319. size = size*0.5
  320. }
  321. // 保留两位小数, 四舍五入
  322. size = Math.round(size * 10) / 10;
  323. return size + units[unitIndex]
  324. }
  325. //根据字节获取大小无单位
  326. static formatFileSizeWithout(bytes:number) {
  327. const units = ['Bytes', 'Kbps', 'Mbps'];
  328. let size = bytes;
  329. let unitIndex = 0;
  330. while (size >= 1024 && unitIndex < units.length - 1) {
  331. size /= 1024;
  332. unitIndex++;
  333. }
  334. if(size > 1024&&unitIndex==2){
  335. size = size*0.1
  336. }
  337. if(size > 1024&&unitIndex==2){
  338. size = size*0.1
  339. }
  340. if(size > 480&&unitIndex==2){
  341. size = size*0.5
  342. }
  343. // 保留两位小数, 四舍五入
  344. size = Math.round(size * 10) / 10;
  345. return size
  346. }
  347. static copyText(text:string):boolean{
  348. const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN,text)
  349. const systemPasteboard = pasteboard.getSystemPasteboard()
  350. systemPasteboard.setData(pasteboardData)
  351. systemPasteboard.getData().then((data)=>{
  352. return data
  353. })
  354. return false
  355. }
  356. static getDbm(rssi:number):number{
  357. return Math.round((rssi / 2) + 100)
  358. }
  359. //根据数字形式的IP地址获取字符串形式的IP地址
  360. static getIpAddrFromNum(ipNum: number): string {
  361. return (ipNum >>> 24) + '.' + (ipNum >> 16 & 0xFF) + '.' + (ipNum >> 8 & 0xFF) + '.' + (ipNum & 0xFF);
  362. }
  363. static resolveIP(ip:number) {
  364. let address: string = ip.toString()
  365. if (address === '0') {
  366. return '00:00:000:000'
  367. }
  368. address.substring(0, 2)
  369. return `${address.substring(0, 2)}:${address.substring(2, 4)}:${address.substring(4, 7)}:${address.substring(7, 10)}`
  370. }
  371. static isFreeTime():boolean{
  372. if(DateUtil.isWeekend()){
  373. return true
  374. }
  375. let str = DateUtil.getFormatDateStr(new Date(), 'HH')
  376. let a = Number(str);
  377. if(isNaN(a))
  378. return false
  379. // ToastUtil.showToast('a='+a)
  380. if (a >= 7 && a <13) {//白天
  381. return false;
  382. }
  383. if (a >= 13 && a <20) {//白天
  384. return false;
  385. }
  386. if (a >= 0 && a < 7) {//凌晨
  387. return true;
  388. }
  389. if (a >= 20 && a <= 24) {//晚上
  390. return true;
  391. }
  392. return false
  393. }
  394. //判断是否是音乐文件
  395. static isMusicByExtension(filename:string) {
  396. const extensions = CommonConstants.REAL_MUSIC_FORMAT
  397. const lastIndex = filename.lastIndexOf('.');
  398. if (lastIndex!== -1) {
  399. const fileExtension = filename.slice(lastIndex).toLowerCase();
  400. return extensions.includes(fileExtension);
  401. }
  402. return false;
  403. }
  404. //判断是否是媒体文件 音乐和视频都可以
  405. static isMeidaByExtension(filename:string) {
  406. const extensions = CommonConstants.MEDIA_FORMAT
  407. const lastIndex = filename.lastIndexOf('.');
  408. if (lastIndex!== -1) {
  409. const fileExtension = filename.slice(lastIndex).toLowerCase();
  410. return extensions.includes(fileExtension);
  411. }
  412. return false;
  413. }
  414. //判断是否是视频文件
  415. static isVideoByExtension(filename:string) {
  416. const extensions = CommonConstants.VIDEO_FORMAT
  417. const lastIndex = filename.lastIndexOf('.');
  418. if (lastIndex!== -1) {
  419. const fileExtension = filename.slice(lastIndex).toLowerCase();
  420. return extensions.includes(fileExtension);
  421. }
  422. return false;
  423. }
  424. // 获取缩略图
  425. static async getFetchFrameByTime(filePath: string,time?:number,imageWidth?:number,imageHeight?:number) {
  426. if(Utility.isMusicByExtension(filePath)){
  427. return undefined
  428. }
  429. let pixelMap:image.PixelMap|undefined =undefined;
  430. try{
  431. // 创建AVImageGenerator对象
  432. let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()
  433. let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
  434. let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };
  435. avImageGenerator.fdSrc = avFileDescriptor;
  436. // 初始化入参
  437. let timeUs = 0
  438. console.info('onecold time='+time)
  439. if(time){
  440. timeUs = (time > 0) ? time*50 : 0
  441. }
  442. let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC
  443. if(imageWidth){
  444. imageWidth =imageWidth/2.5
  445. }
  446. if(imageHeight){
  447. imageHeight = imageHeight/2.5
  448. }
  449. let param: media.PixelMapParams = {
  450. width : imageWidth||300,
  451. height : imageHeight||400,
  452. }
  453. // 获取缩略图(promise模式)
  454. pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param)
  455. // 释放资源(promise模式)
  456. avImageGenerator.release()
  457. console.info(`release success.`)
  458. fs.closeSync(file);
  459. }catch (error) {
  460. console.error('uriGetAssets failed with err: ' + JSON.stringify(error));
  461. }
  462. return pixelMap
  463. }
  464. // 获取fd文件路径
  465. static async getFdDir(path:string){
  466. let fdPath = 'fd://';
  467. let file = await fileIo.open(path)
  468. fdPath = fdPath + '' + file.fd;
  469. return fdPath
  470. }
  471. // 根据uri获取文件名称
  472. static getMediaNameByUri(myUri: string) {
  473. let myFileName = (myUri.split('/').pop()) as string;
  474. return decodeURIComponent(myFileName)
  475. }
  476. static msToStandardTime(ms:string):string{
  477. let date = new Date(ms);
  478. let hours = date.getHours();
  479. let minutes = date.getMinutes();
  480. let seconds = date.getSeconds();
  481. return hours + ':' + minutes + ':' + seconds;
  482. }
  483. static getTimestampFromDateStr(dateStr: string, format: string = 'yyyy-MM-dd HH:mm'): number {
  484. // 定义正则表达式以匹配日期字符串中的各部分
  485. const regex = format
  486. .replace('yyyy', '(\\d{4})')
  487. .replace('MM', '(\\d{2})')
  488. .replace('dd', '(\\d{2})')
  489. .replace('HH', '(\\d{2})')
  490. .replace('mm', '(\\d{2})');
  491. const match = new RegExp(regex).exec(dateStr);
  492. if (!match) {
  493. throw new Error('Invalid date string or format');
  494. }
  495. // 解析匹配结果
  496. const year = Number(match[1]);
  497. const month = Number(match[2]);
  498. const day = Number(match[3]);
  499. const hours = Number(match[4]);
  500. const minutes = Number(match[5]);
  501. // 创建 Date 对象
  502. const dateObj = new Date(year, month - 1, day, hours, minutes);
  503. // 返回时间戳(以秒为单位)
  504. return Math.floor(dateObj.getTime() / 1000);
  505. }
  506. static getFormatDateStr(date: number | string | Date, format: string = 'yyyy-MM-dd HH:mm:ss'): string {
  507. // 将输入转换为 Date 对象
  508. let dateObj: Date;
  509. if (typeof date === 'number') {
  510. // 如果是十位时间戳,转换为毫秒数
  511. dateObj = new Date(date.toString().length === 10 ? date * 1000 : date);
  512. } else if (typeof date === 'string') {
  513. // 如果是字符串,尝试解析为 Date 对象
  514. dateObj = new Date(date);
  515. } else {
  516. // 如果是 Date 对象,直接使用
  517. dateObj = date;
  518. }
  519. // 定义替换规则
  520. const replacements = new Map<string, string>([
  521. ['yyyy', String(dateObj.getFullYear())],
  522. ['MM', String(dateObj.getMonth() + 1).padStart(2, '0')],
  523. ['dd', String(dateObj.getDate()).padStart(2, '0')],
  524. ['HH', String(dateObj.getHours()).padStart(2, '0')],
  525. ['mm', String(dateObj.getMinutes()).padStart(2, '0')],
  526. ['ss', String(dateObj.getSeconds()).padStart(2, '0')]
  527. ]);
  528. // 替换格式字符串中的占位符
  529. replacements.forEach((value, key) => {
  530. format = format.replace(new RegExp(key, 'g'), value);
  531. });
  532. return format;
  533. }
  534. static async getFilePixelMapBig(uri: string): Promise<string | undefined> {
  535. let pixelMap: image.PixelMap | undefined = undefined;
  536. if (Utility.isMusicByExtension(uri)) {
  537. pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri);
  538. } else {
  539. pixelMap = await Utility.getFetchFrameByTime(uri);
  540. }
  541. return pixelMap ? ImageUtil.pixelMapToBase64Str(pixelMap) : undefined;
  542. }
  543. static async getFilePixelMap(uri: string): Promise<string | undefined> {
  544. let pixelMap: image.PixelMap | undefined = undefined;
  545. if (Utility.isMusicByExtension(uri)) {
  546. pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri);
  547. } else {
  548. pixelMap = await Utility.getFetchFrameByTime(uri);
  549. }
  550. return pixelMap ? ImageUtil.pixelMapToBase64Str(pixelMap) : undefined;
  551. }
  552. //根据filePathe获取对应的播放Index
  553. static getIndexFromList(localList:Array<VideoItem>,filePath:string){
  554. let list: Array<string> = [];
  555. for(let i=0;i<localList.length;i++){
  556. if(localList[i].filePath === filePath){
  557. return i
  558. }
  559. }
  560. return 0
  561. }
  562. // 在以下demo中,使用资源管理接口获取打包在HAP内的媒体资源文件,通过设置fdSrc属性,获取音频元数据并打印,
  563. // 获取音频专辑封面并通过Image控件显示在屏幕上。该demo以Promise形式进行异步接口调用
  564. static async getFetchMetadataFromFdSrcByPromise(uri: string): Promise<image.PixelMap | undefined> {
  565. let cover: image.PixelMap | undefined = undefined;
  566. if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
  567. try {
  568. // 创建AVMetadataExtractor对象
  569. const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
  570. // 设置fdSrc
  571. const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
  572. avMetadataExtractor.fdSrc = fd;
  573. // 获取元数据(promise模式)
  574. const metadata = await avMetadataExtractor.fetchMetadata();
  575. metadata.author
  576. console.info(`get meta data, hasAudio: ${metadata.hasAudio}`);
  577. // 获取专辑封面(promise模式)
  578. cover = await avMetadataExtractor.fetchAlbumCover();
  579. // 释放资源(promise模式)
  580. await avMetadataExtractor.release();
  581. console.info('release success.');
  582. } catch (error) {
  583. console.error('Error during metadata extraction:', error);
  584. }
  585. } else {
  586. console.warn('AVMetadataExtractor capability is not supported.');
  587. }
  588. return cover;
  589. }
  590. static formatTimestamp(timestamp: number): string {
  591. // 将时间戳转换为 Date 对象
  592. const date = new Date(timestamp);
  593. // 获取各个部分
  594. const year = date.getFullYear();
  595. const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要加1
  596. const day = String(date.getDate()).padStart(2, '0');
  597. const hours = String(date.getHours()).padStart(2, '0');
  598. const minutes = String(date.getMinutes()).padStart(2, '0');
  599. const seconds = String(date.getSeconds()).padStart(2, '0');
  600. // 拼接成所需的格式
  601. return `${year}-${month}-${day} ${hours}:${minutes}`;
  602. }
  603. //获取音乐资源的属性值,
  604. static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number
  605. ,autoParseMusicName?:boolean,originFileName?:string): Promise<VideoItem> {
  606. await ReqPermissionUtil.persistPermission(uri);
  607. //这里加个判断,如果文件的大小是0K,这个文件是空的,直接返回
  608. try {
  609. const file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
  610. const fileStat = await fs.stat(file.fd);
  611. if (fileStat.size < 1024) {// 检查是否为有效文件(>1KB)
  612. console.warn(`File ${uri} is empty (0KB). Returning default VideoItem.`);
  613. // Return a default or empty VideoItem object
  614. return new VideoItem(FileUtil.getFileName(uri), uri, uri, type, 0, '');
  615. }
  616. } catch (error) {
  617. console.error(`Failed to get stats for file ${uri}:`, error);
  618. // If we can't read the file, still return a default item
  619. return new VideoItem(FileUtil.getFileName(uri), uri, uri, type, 0, '');
  620. }
  621. if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){
  622. const cueItem = new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
  623. cueItem.fileName = FileUtil.getFileName(uri)
  624. return cueItem
  625. }
  626. return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName,originFileName)
  627. //华为官方不支持的格式 不支持内嵌歌词Dsf,aif,aiff,wav 不支持内嵌封面dsf,aif,aiff
  628. if(StrUtil.isNotEmpty(uri)){
  629. if(uri.toLowerCase().endsWith('.dsf')
  630. ||uri.toLowerCase().endsWith('.aif')
  631. // ||uri.toLowerCase().endsWith('.wav')
  632. ||uri.toLowerCase().endsWith('.aiff')){
  633. return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName)
  634. }
  635. }
  636. let item:VideoItem = new VideoItem('',uri,uri,type,0,'')
  637. try {
  638. console.info('asset file.uri: ', uri);
  639. let file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
  640. console.info("file.fd " + file.fd);
  641. let fdfd = 'fd://' + file.fd
  642. //3、通过fs.stat方法获取stat对象
  643. console.info('asset file.name: ', file.name);
  644. console.info('asset file.uri: ', uri);
  645. console.info('asset file.fd: ', file.fd);
  646. console.info('asset file.path: ', file.path);
  647. item = new VideoItem(file.name,uri,uri,type,0,'')
  648. await fs.stat(file.fd).then(async (stat: fs.Stat) => {
  649. console.info("get file info succeed, the size of file is " + stat.size);
  650. let videoSize = stat.size
  651. // let videoTime = stat.ctime
  652. let fileSize = Utility.formatFSize(videoSize)
  653. //按照添加时间
  654. let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
  655. // console.info('onecold asset addTime: ', addTime);
  656. // let cTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
  657. // console.info('asset stat.ctime: ', stat.ctime);
  658. let musicName:string | undefined = file.name
  659. let artist:string | undefined = ''
  660. let album:string | undefined = ''
  661. let pixelMap:image.PixelMap|undefined|null = undefined
  662. let imagePath = ''
  663. let duration:string | undefined = ''
  664. let mimeType:string | undefined = ''
  665. let trackCount:string | undefined = ''//轨道数量
  666. let sampleRate:string | undefined = ''//音频的采样率单位为Hz
  667. if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
  668. try {
  669. // 创建AVMetadataExtractor对象
  670. const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
  671. // 设置fdSrc
  672. const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
  673. avMetadataExtractor.fdSrc = fd;
  674. // 获取元数据(promise模式)
  675. const metadata = await avMetadataExtractor.fetchMetadata();
  676. if(StrUtil.isNotEmpty(metadata.title)){
  677. musicName = metadata.title
  678. }else{
  679. //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
  680. console.log(`onecold musicName为空:${file.name}`);
  681. const musicData = parseMusicFileName(file.name);
  682. if (musicData.isValid) {
  683. // console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
  684. // console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
  685. musicName = musicData.title
  686. if(artist==''||artist==undefined)
  687. artist = musicData.artist
  688. } else {
  689. musicName = file.name
  690. // console.log("onecold 文件名格式不符合要求");
  691. }
  692. }
  693. if(StrUtil.isNotEmpty(metadata.artist)){
  694. artist = metadata.artist
  695. }
  696. if(artist==undefined)
  697. artist = ''
  698. if(StrUtil.isNotEmpty(metadata.album)){
  699. album = metadata.album
  700. }else{
  701. album = ''
  702. }
  703. if(StrUtil.isNotEmpty(metadata.duration)){
  704. if(metadata.duration)
  705. duration = convertSecondsToTime(metadata.duration.toString())
  706. }
  707. if(StrUtil.isNotEmpty(metadata.mimeType)){
  708. mimeType = metadata.mimeType
  709. }
  710. if(StrUtil.isNotEmpty(metadata.trackCount)){
  711. trackCount = metadata.trackCount
  712. }
  713. if(StrUtil.isNotEmpty(metadata.sampleRate)){
  714. sampleRate = metadata.sampleRate
  715. }
  716. let name = await MD5.digestSync(uri)
  717. // if(isLoadPixelMap){
  718. // 获取专辑封面(promise模式)
  719. // pixelMap = await avMetadataExtractor.fetchAlbumCover();
  720. // // 释放资源(promise模式)
  721. // await avMetadataExtractor.release();
  722. pixelMap = await fetchAlbumCover(avMetadataExtractor)
  723. // console.info('onecold release success. name= '+musicName);
  724. if(pixelMap!==undefined&&pixelMap!==null){
  725. // console.info('onecold pixelMap is not empty= '+musicName);
  726. imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,name)
  727. imagePath = fileUri.getUriFromPath(imagePath)
  728. }else{
  729. // console.info('onecold pixelMap is empty= '+musicName);
  730. imagePath = ''
  731. // if(StrUtil.isNotEmpty(artist))
  732. // imagePath = await NetAxiosUtil.getLyricCover(musicName,artist)
  733. }
  734. // }else{
  735. // imagePath = context.filesDir + FileUtil.separator + name
  736. // imagePath = fileUri.getUriFromPath(imagePath)
  737. // }
  738. console.info('onecold release success. imagePath= '+imagePath);
  739. } catch (error) {
  740. console.error('Error during metadata extraction:', error);
  741. }
  742. } else {
  743. console.warn('AVMetadataExtractor capability is not supported.');
  744. }
  745. if(musicName==undefined)
  746. musicName = file.name
  747. item = new VideoItem(musicName,uri ,uri,type,videoSize,addTime,fileSize,
  748. imagePath,artist,album,file.name)
  749. item.duration = duration+'';
  750. item.mimeType = mimeType;
  751. item.trackCount = trackCount;
  752. item.sampleRate = sampleRate;
  753. item.isFav = 0;
  754. item.playCount = 0;
  755. item.pyStr = pinyin4js.getShortPinyin(musicName)
  756. // item.lyricContent = await extractLyricsContent(uri)
  757. let metaItem = await parseAudioMetadata(uri)
  758. if(metaItem){
  759. item.lyricContent = metaItem.lyricContent
  760. item.bit_rate = formatBitrateToKbps(metaItem.bit_rate || "0");
  761. item.year = metaItem.year ||'unknown'
  762. item.probe_score = metaItem.probe_score
  763. item.nb_streams = metaItem.nb_streams
  764. item.nb_programs = metaItem.nb_programs
  765. }
  766. })
  767. } catch (error) {
  768. console.error('uriGetAssetsFromFile failed with err: ' + JSON.stringify(error));
  769. }
  770. return item
  771. }
  772. static async readMetaInfoFFmpeg(context:Context,inputPath: string,type:number,
  773. autoParseMusicName?:boolean,originFileName?:string): Promise<VideoItem> {
  774. return new Promise((resolve, reject) => {
  775. inputPath = FileUtil.getFilePath(inputPath)
  776. let commands = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", inputPath];
  777. let outputJson = "";
  778. FFmpeg.execute(commands, {
  779. logCallback: (logLevel: number, logMessage: string) => console.log(`[${logLevel}]${logMessage}`),
  780. outputCallback: (message: string) => {
  781. outputJson += message;
  782. },
  783. }).then(async () => {
  784. try {
  785. const metadata: FFprobeMetadata = JSON.parse(outputJson);
  786. const format = metadata.format;
  787. console.log(`onecold outputJson:${outputJson}`);
  788. let file = fs.openSync(inputPath, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
  789. console.info('readMetaInfoFFmpeg asset file.path: ', file.path);
  790. let videoItem = new VideoItem(file.name,inputPath,inputPath,type,0,'')
  791. await fs.stat(file.fd).then(async (stat: fs.Stat) => {
  792. // 获取音频流的采样率
  793. let sampleRate:string|undefined = '';
  794. let bits_per_raw_sample:string|undefined = '';
  795. let channels:string|undefined = '';
  796. let channel_layout:string|undefined = '';
  797. let start_time:string|undefined = '';
  798. if (metadata.streams && metadata.streams.length > 0) {
  799. // 查找第一个音频流
  800. const audioStream = metadata.streams.find(stream => StrUtil.isNotEmpty(stream.sample_rate));
  801. if (audioStream) {
  802. sampleRate = audioStream.sample_rate;
  803. bits_per_raw_sample = audioStream.bits_per_raw_sample
  804. channel_layout = audioStream.channel_layout
  805. channels = audioStream.channels
  806. start_time = audioStream.start_time
  807. }
  808. }
  809. let videoSize = stat.size
  810. const modifyTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
  811. let fileSize = Utility.formatFSize(videoSize)
  812. //按照添加时间
  813. let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
  814. // Extract artist and title from tags
  815. const tags = format.tags || {};
  816. let artist = tags.artist ||tags.ARTIST || '';
  817. let title = tags.title ||tags.TITLE || '';
  818. let album = tags.album ||tags.ALBUM|| '';
  819. Logger.info('readMetaInfoFFmpeg', `原始标签 title="${title}", artist="${artist}", album="${album}"`);
  820. console.log(`onecold tags.tags:${JSON.stringify(tags)}`);
  821. // 检查是否有乱码
  822. if (hasGarbledText(tags)) {
  823. // 第二次尝试用华为的接口提取元数据
  824. let hwTags = await extractHwMediaMetadata(inputPath)
  825. artist = hwTags?.artist|| '';
  826. title = hwTags?.title|| '';
  827. album = hwTags?.album|| '';
  828. console.info('onecold 乱码修正 artist='+artist);
  829. console.info('onecold 乱码修正 title='+title);
  830. console.info('onecold 乱码修正 album='+album);
  831. }
  832. Logger.info('readMetaInfoFFmpeg', `最终标签 title="${title}", artist="${artist}", album="${album}", autoParse=${autoParseMusicName}`);
  833. if(StrUtil.isEmpty(title)){
  834. //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
  835. console.log(`onecold musicName为空:${file.name}`);
  836. // let autoParseMusicName = PreferencesUtil.getBooleanSync('autoParseMusicName',false)
  837. if(autoParseMusicName){
  838. const musicData = parseMusicFileName(file.name);
  839. if (musicData.isValid) {
  840. console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
  841. console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
  842. title = musicData.title
  843. if(artist==''||artist==undefined)
  844. artist = musicData.artist
  845. } else {
  846. title = originFileName||file.name
  847. }
  848. }else{
  849. title = originFileName||file.name
  850. }
  851. }
  852. let name: string = title;
  853. if (!name) {
  854. name = getFileNameWithoutExtension(inputPath);
  855. }
  856. let pixelMap:image.PixelMap|undefined|null = undefined
  857. // Create VideoItem
  858. videoItem = new VideoItem(
  859. name,
  860. inputPath, // id can be generated or left empty
  861. inputPath,
  862. type, // assuming it's local
  863. videoSize,
  864. addTime // convert to ISO string
  865. );
  866. // Set additional properties from format metadata
  867. videoItem.artist = artist;
  868. videoItem.album = album;
  869. videoItem.sampleRate = sampleRate
  870. videoItem.pyStr = pinyin4js.getShortPinyin(name)
  871. videoItem.fileName = FileUtil.getFileName(inputPath);
  872. if(format.duration)
  873. videoItem.duration = formatDuration(format.duration.toString()||'00:00')
  874. videoItem.size = fileSize;
  875. videoItem.bit_rate =formatBitrateToKbps(format.bit_rate || "0");
  876. videoItem.probe_score = format.probe_score;
  877. videoItem.nb_streams = format.nb_streams;
  878. videoItem.nb_programs = format.nb_programs;
  879. videoItem.year = tags.TYER || tags.date ||tags.DATE|| ''; // try different tag names for year
  880. videoItem.lyricContent = tags.LYRICS || tags.lyrics || tags.USLT || tags.UNSYNCEDLYRICS || '';
  881. //console.info('readMetaInfoFFmpeg videoItem.lyricContent: ', videoItem.lyricContent);
  882. // 如果标准字段没有歌词,则尝试解析 `lyrics-` 字段
  883. // tags 必须是 Record<string, string | undefined>(或 any)
  884. // 如果还是没有歌词内容,尝试查找自定义的lyrics-开头的属性(电脑版音乐标签内嵌歌词就是lyrics-XXX)
  885. if(StrUtil.isEmpty( videoItem.lyricContent)){
  886. const tagsRecord = tags as Record<string, string>;
  887. const possibleKeys = Object.keys(tagsRecord);
  888. for (let i = 0; i < possibleKeys.length; i++) {
  889. const key = possibleKeys[i];
  890. if (key && key.toLowerCase().startsWith('lyrics-')) {
  891. //console.info('readMetaInfoFFmpeg videoItem.key: ', key);
  892. // 通过转换后的 Record 类型安全访问属性
  893. videoItem.lyricContent = tagsRecord[key];
  894. //console.info('readMetaInfoFFmpeg videoItem.lyricContent2: ', videoItem.lyricContent);
  895. if (videoItem.lyricContent) {
  896. break;
  897. }
  898. }
  899. }
  900. }
  901. videoItem.genre = tags.genre||tags.GENRE|| '';
  902. videoItem.track = tags.track||tags.TRACK|| '';
  903. videoItem.ALBUMARTIST = tags.ALBUMARTIST||tags.album_artist||tags.albumartist||tags.TPE2|| '';
  904. videoItem.COMPOSER = tags.COMPOSER||tags.composer|| '';
  905. videoItem.LYRICIST = tags.LYRICIST||tags.lyricist||tags.TEXT ||'';
  906. videoItem.COMMENT = tags.COMMENT||tags.comment||tags.COMM|| '';
  907. videoItem.disc = tags.disc|| '';
  908. videoItem.bits_per_raw_sample = bits_per_raw_sample ||'1'
  909. videoItem.channel_layout = channel_layout || ''
  910. videoItem.channels = channels ||'0'
  911. videoItem.start_time = start_time || '00:00'
  912. videoItem.mimeType = format.format_name
  913. // 检查是否有封面图片流
  914. const hasCover = metadata.streams.some(stream =>
  915. stream.disposition?.attached_pic === 1
  916. );
  917. console.info('onecold readMetaInfoFFmpeg asset hasCover: ', hasCover);
  918. let md5Name = await MD5.digestSync(inputPath)
  919. let imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}.jpg`;
  920. console.info('onecold readMetaInfoFFmpeg asset imagePath: ', imagePath);
  921. // 如果有封面图片,则提取
  922. try {
  923. if (hasCover) {
  924. //提取封面
  925. let isSuccess:boolean= await getFFmpegCover(inputPath, imagePath);
  926. if(isSuccess){
  927. imagePath = fileUri.getUriFromPath(imagePath)
  928. }else{
  929. // 使用华为接口提取封面(备用方案)
  930. console.warn('onecold 提取封面图片失败,使用华为接口提取封面');
  931. imagePath = await extractCoverWithHwInterface(inputPath, context, name);
  932. }
  933. videoItem.pixelMapPath = imagePath;
  934. }else if(Utility.isVideoByExtension(inputPath)){
  935. pixelMap = await Utility.getFetchFrameByTime(inputPath)
  936. if(pixelMap!==undefined&&pixelMap!==null){
  937. imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,md5Name)
  938. imagePath = fileUri.getUriFromPath(imagePath)
  939. }else{
  940. imagePath = ''
  941. }
  942. imagePath = fileUri.getUriFromPath(imagePath)
  943. videoItem.pixelMapPath = imagePath;
  944. }
  945. } catch (error) {
  946. console.warn('onecold 提取封面图片失败:', error.message);
  947. }
  948. if(sampleRate&&format.bit_rate){
  949. videoItem.md5Str = determineAudioQuality( videoItem.mimeType,Number(format.bit_rate),Number(sampleRate))
  950. }
  951. console.info('onecold Successfully parsed metadata:', videoItem);
  952. resolve(videoItem);
  953. })
  954. } catch (error) {
  955. console.error('onecold Failed to parse metadata:', error);
  956. reject(new Error('onecold Failed to parse metadata: ' + error.message));
  957. }
  958. }).catch((error: Error) => {
  959. console.error(`onecold Execution failed with error: ${error.message}`);
  960. reject(error);
  961. });
  962. });
  963. }
  964. private completionNum(num: number): string | number {
  965. if (num < 10) {
  966. return '0' + num;
  967. } else {
  968. return num;
  969. }
  970. }
  971. //根据字节获取大小
  972. static formatFSize(bytes:number):string {
  973. const units = ['Bytes', 'K', 'M', 'G'];
  974. let size = bytes;
  975. let unitIndex = 0;
  976. while (size >= 1024 && unitIndex < units.length - 1) {
  977. size /= 1024;
  978. unitIndex++;
  979. }
  980. // 保留两位小数, 四舍五入
  981. size = Math.round(size * 10) / 10;
  982. return size + units[unitIndex]
  983. }
  984. static gotoMarket(context:common.UIAbilityContext,bundleName:string) {
  985. const isAvailable = canIUse('SystemCapability.AppGalleryService.Distribution.Comment');
  986. if(isAvailable) {
  987. try {//需要api 20 才可以调用用户评论对话框showCommentDialog
  988. commentManager.showCommentDialog(context).then(()=>{
  989. hilog.info(0, 'TAG', "succeeded in showing commentDialog.");
  990. return
  991. }).catch((error: BusinessError<Object>) => {
  992. Utility.goMarket(context,bundleName)
  993. hilog.error(0, 'TAG', `showCommentDialog failed, Code: ${error.code}, message: ${error.message}`);
  994. });
  995. } catch (error) {
  996. Utility.goMarket(context,bundleName)
  997. hilog.error(0, 'TAG', `showCommentDialog failed, Code: ${error.code}, message: ${error.message}`);
  998. }
  999. }else{
  1000. Utility.goMarket(context,bundleName)
  1001. }
  1002. }
  1003. static async goMarket(context:common.UIAbilityContext,bundleName:string){
  1004. const want: Want = {
  1005. uri: `store://appgallery.huawei.com/app/detail?id=${bundleName}`
  1006. };
  1007. context.startAbility(want).then(()=>{
  1008. //拉起成功
  1009. }).catch(()=>{
  1010. // 拉起失败
  1011. });
  1012. }
  1013. static async doShare(item:VideoItem,context:common.UIAbilityContext){
  1014. // 生成视频封面图
  1015. const imagePackerApi: image.ImagePacker = image.createImagePacker();
  1016. const buffer: ArrayBuffer = await (imagePackerApi.packing(await Utility.getFetchFrameByTime(item.filePath), {
  1017. format: 'image/jpeg',
  1018. quality: 30
  1019. }) as Promise<ArrayBuffer>);
  1020. // 构造ShareData,需配置一条有效数据信息
  1021. let shareData: systemShare.SharedData = new systemShare.SharedData({
  1022. utd: uniformTypeDescriptor.UniformDataType.MEDIA,
  1023. uri: fileUri.getUriFromPath(item.filePath),
  1024. title: item.name, // 不传title字段时,显示视频文件名
  1025. // description: '好听的音乐', // 不传description字段时,显示视频大小
  1026. thumbnail: new Uint8Array(buffer), // 优先使用传递的缩略图做预览 不传则默认使用视频第一帧画面做预览图
  1027. });
  1028. // 进行分享面板显示
  1029. let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
  1030. // let context = getContext(this) as common.UIAbilityContext;
  1031. controller.show(context, {
  1032. selectionMode: systemShare.SelectionMode.SINGLE,
  1033. previewMode: systemShare.SharePreviewMode.DETAIL,
  1034. }).then(() => {
  1035. console.info('ShareController show success.');
  1036. }).catch((error: BusinessError) => {
  1037. console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`);
  1038. });
  1039. }
  1040. //分享索引index的视频
  1041. static async doShareMusic(item:VideoItem,context:common.UIAbilityContext){
  1042. // 构造ShareData,需配置一条有效数据信息
  1043. let shareData: systemShare.SharedData = new systemShare.SharedData({
  1044. utd: uniformTypeDescriptor.UniformDataType.AUDIO,
  1045. uri: fileUri.getUriFromPath(item.filePath),
  1046. title: item.name, // 不传title字段时,显示视频文件名
  1047. description: '好听的音乐', // 不传description字段时,显示视频大小
  1048. });
  1049. // 进行分享面板显示
  1050. let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
  1051. // let context = getContext(this) as common.UIAbilityContext;
  1052. controller.show(context, {
  1053. selectionMode: systemShare.SelectionMode.SINGLE,
  1054. previewMode: systemShare.SharePreviewMode.DETAIL,
  1055. }).then(() => {
  1056. console.info('ShareController show success.');
  1057. }).catch((error: BusinessError) => {
  1058. console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`);
  1059. });
  1060. }
  1061. static getNameList(localList:Array<VideoItem>){
  1062. let list: Array<string> = [];
  1063. for(let i=0;i<localList.length;i++){
  1064. if(StrUtil.isNotEmpty(localList[i].name))
  1065. list.push(localList[i].name);
  1066. }
  1067. return list
  1068. }
  1069. //获取列表有封面的值
  1070. static getFirstCoverFromList(localList:Array<VideoItem>):string{
  1071. let list: Array<string> = [];
  1072. for(let i=0;i<localList.length;i++){
  1073. let pix = localList[i].pixelMapPath
  1074. if(pix){
  1075. return pix
  1076. }
  1077. }
  1078. return ''
  1079. }
  1080. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_Dir:私密视频)
  1081. static getGlobalNameList(localList:Array<VideoItem>,type:number){
  1082. let list: Array<string> = [];
  1083. for(let i=0;i<localList.length;i++){
  1084. if(localList[i].type === type){
  1085. list.push(localList[i].name);
  1086. }
  1087. }
  1088. return list
  1089. }
  1090. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_LOCK:私密视频)
  1091. static getGlobalList(localList:Array<VideoItem>,type:number){
  1092. let list: Array<VideoItem> = [];
  1093. for(let i=0;i<localList.length;i++){
  1094. //播放列表过滤.cue文件
  1095. if(localList[i].type === type&&!localList[i].filePath.toLowerCase().endsWith('.cue') ){
  1096. list.push(localList[i]);
  1097. }
  1098. }
  1099. return list
  1100. }
  1101. //以下是实现 getPlayItem 方法的代码,根据 playType 的值返回随机项或第一项
  1102. static getPlayItem(localList: Array<VideoItem>, playType: number): VideoItem | undefined {
  1103. // 处理空列表情况
  1104. if (!localList || localList.length === 0) {
  1105. return undefined;
  1106. }
  1107. // 当 playType 为 3 时随机返回一个元素
  1108. if (playType === 3) {
  1109. const randomIndex = Math.floor(Math.random() * localList.length);
  1110. return localList[randomIndex];
  1111. }
  1112. // 其他情况返回第一个元素
  1113. else {
  1114. return localList[0];
  1115. }
  1116. }
  1117. /**
  1118. * 根据文件路径从项数组中查找对应的VideoItem
  1119. * @param list VideoItem数组
  1120. * @param filePath 文件路径
  1121. * @returns 对应的VideoItem,如果未找到则返回undefined
  1122. */
  1123. static getItemByFilePath(list: Array<VideoItem>, filePath: string): VideoItem | undefined {
  1124. if (!list || list.length === 0 || !filePath) {
  1125. return undefined;
  1126. }
  1127. for (let i = 0; i < list.length; i++) {
  1128. if (list[i].filePath === filePath) {
  1129. return list[i];
  1130. }
  1131. }
  1132. return undefined;
  1133. }
  1134. //根据filePath获取当前索引index
  1135. static getCurIndexFromGlobalList(localList:Array<VideoItem>,filePath:string){
  1136. let index: number = 0;
  1137. for(let i=0;i<localList.length;i++){
  1138. if(localList[i].filePath === filePath){
  1139. index = i
  1140. return index
  1141. }
  1142. }
  1143. return index
  1144. }
  1145. //新的查询是否收藏的方法
  1146. static getIsFav(favList: Array<VideoItem>,item:VideoItem|undefined){
  1147. if(ArrayUtil.isEmpty(favList)){
  1148. return false
  1149. }
  1150. if(item===undefined){
  1151. return false
  1152. }
  1153. for(let i=0;i<favList.length;i++){
  1154. if(favList[i].filePath === item.filePath&&favList[i].isFav===1){
  1155. return true
  1156. }
  1157. }
  1158. return false
  1159. }
  1160. static getIsFac(facList: Array<String>,item:VideoItem|undefined){
  1161. if(ArrayUtil.isEmpty(facList)){
  1162. return false
  1163. }
  1164. if(item===undefined){
  1165. return false
  1166. }
  1167. for(let i=0;i<facList.length;i++){
  1168. if(facList[i] === item.name||facList[i]===item.fileName){
  1169. return true
  1170. }
  1171. }
  1172. return false
  1173. }
  1174. static resourceToString(context:Context,resource:Resource){
  1175. if(!context||!resource)
  1176. return ''
  1177. return context.resourceManager.getStringSync(resource).toString()
  1178. }
  1179. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_LOCK:私密视频)
  1180. static getGlobalMusicList(localList:Array<VideoItem>,type:number){
  1181. let list: Array<VideoItem> = [];
  1182. for(let i=0;i<localList.length;i++){
  1183. if(localList[i].type === type){
  1184. list.push(localList[i]);
  1185. }
  1186. }
  1187. return list
  1188. }
  1189. static isHaoFreeTime():boolean{
  1190. if(DateUtil.isWeekend()){
  1191. return true
  1192. }
  1193. let str = DateUtil.getFormatDateStr(new Date(), 'HH')
  1194. let a = Number(str);
  1195. if(isNaN(a))
  1196. return false
  1197. // ToastUtil.showToast('a='+a)
  1198. if (a >= 8 && a <13) {//白天
  1199. return false;
  1200. }
  1201. if (a >= 13 && a <20) {//白天
  1202. return false;
  1203. }
  1204. if (a >= 0 && a < 8) {//凌晨
  1205. return true;
  1206. }
  1207. if (a >= 20 && a <= 24) {//晚上
  1208. return true;
  1209. }
  1210. return false
  1211. }
  1212. static getParentDirectory(filePath: string): string {
  1213. // 使用正则表达式匹配路径分隔符(支持 Unix 和 Windows)
  1214. const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
  1215. if (lastSlashIndex === -1) {
  1216. return '';
  1217. }
  1218. return filePath.substring(0, lastSlashIndex);
  1219. }
  1220. // 开启沉浸式显示模式
  1221. static async enableFullScreen() {
  1222. const ctx = getContext()
  1223. const win = await window.getLastWindow(ctx)
  1224. win.setWindowLayoutFullScreen(true)
  1225. const top = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
  1226. AppStorage.setOrCreate('topHeight', px2vp(top.topRect.height))
  1227. AppStorage.setOrCreate('bottomHeight', px2vp(top.bottomRect.height))
  1228. }
  1229. // 关闭沉浸式显示模式
  1230. static async disableFullScreen() {
  1231. const ctx = getContext()
  1232. const win = await window.getLastWindow(ctx)
  1233. win.setWindowLayoutFullScreen(false)
  1234. AppStorage.setOrCreate('topHeight', 0)
  1235. AppStorage.setOrCreate('bottomHeight', 0)
  1236. }
  1237. // 设置状态栏文字颜色为白色
  1238. static async setStatusBarLight() {
  1239. const ctx = getContext()
  1240. const win = await window.getLastWindow(ctx)
  1241. win.setWindowSystemBarProperties({
  1242. statusBarContentColor: '#ffffff'
  1243. })
  1244. }
  1245. // 设置状态栏文字颜色为黑色
  1246. static async setStatusBarDark() {
  1247. const ctx = getContext()
  1248. const win = await window.getLastWindow(ctx)
  1249. win.setWindowSystemBarProperties({
  1250. statusBarContentColor: '#000000'
  1251. })
  1252. }
  1253. // 优化点:完全基于Promise链的异步处理
  1254. static async getAppName(context: Context): Promise<string> {
  1255. try {
  1256. // 1. 同步化BundleInfo获取
  1257. const bundleInfo = await bundleManager.getBundleInfoForSelf(
  1258. bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
  1259. );
  1260. // 2. 安全获取labelId(新增空值校验)
  1261. const labelId = bundleInfo.appInfo?.labelId;
  1262. if (!labelId || labelId <= 0) {
  1263. throw new Error("Invalid labelId: " + labelId);
  1264. }
  1265. // 3. 异步资源解析(替换危险的getStringSync)
  1266. return await context.resourceManager.getStringValue(labelId);
  1267. } catch (err) {
  1268. console.error(`[${new Date().toISOString()}] AppName Error: CODE=${err.code}, MSG=${err.message}`);
  1269. // 4. 多级降级策略
  1270. return AppUtil.getBundleName() // 最终兜底
  1271. }
  1272. }
  1273. //按名称升序
  1274. static doSortListAscending( list:Array<VideoItem>,isShowFileName?:boolean){
  1275. let options: Intl.CollatorOptions = {
  1276. localeMatcher: "lookup",
  1277. usage: "sort",
  1278. sensitivity: "accent", // 对中文更友好
  1279. numeric:true
  1280. };
  1281. const collator = new Intl.Collator("zh-CN", options);
  1282. list.sort((a, b) => {
  1283. const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type);
  1284. if (typeOrder !== 0) {
  1285. return typeOrder;
  1286. }
  1287. if(isShowFileName){
  1288. return collator.compare(a.fileName||a.name, b.fileName||b.name)
  1289. }else{
  1290. return collator.compare(a.name, b.name)
  1291. }
  1292. });
  1293. }
  1294. //按名称降序
  1295. static doSortListDescending(list:Array<VideoItem>,isShowFileName?:boolean){
  1296. const options: Intl.CollatorOptions = {
  1297. localeMatcher: "lookup",
  1298. usage: "sort",
  1299. sensitivity: "accent", // 对中文更友好
  1300. numeric:true
  1301. };
  1302. const collator = new Intl.Collator("zh-CN", options);
  1303. list.sort((a, b) => {
  1304. const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type);
  1305. if (typeOrder !== 0) {
  1306. return typeOrder;
  1307. }
  1308. if(isShowFileName){
  1309. return collator.compare(b.fileName||b.name, a.fileName||a.name)
  1310. }else{
  1311. return collator.compare(b.name, a.name)
  1312. }
  1313. // const partsA = extractParts(a.name);
  1314. // const partsB = extractParts(b.name);
  1315. //
  1316. // const nonNumericComparison = collator.compare(partsB.nonNumeric, partsA.nonNumeric);
  1317. // if (nonNumericComparison !== 0) {
  1318. // return nonNumericComparison;
  1319. // }
  1320. // return partsB.numeric - partsA.numeric;
  1321. });
  1322. }
  1323. /**
  1324. * 读取本地会员到期时间(解密.vv文件)
  1325. * @returns
  1326. */
  1327. static async getLocalNobleExpireDate(): Promise<string> {
  1328. // return "2025-08-08 12:00";
  1329. try {
  1330. const documentViewPicker = new picker.DocumentViewPicker()
  1331. let documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD })
  1332. let download_path = new fileUri.FileUri(documentSaveResult[0]).path + '/'
  1333. let filePath = download_path + STR_LOCK_VIDEO+ '/' + VIP_FILEPATH
  1334. let expireDate = Utility.readDataFromFile(filePath)
  1335. return expireDate;
  1336. } catch (e) {
  1337. return '';
  1338. }
  1339. }
  1340. static readDataFromFile(filePath: string) {
  1341. if(!FileUtil.accessSync(filePath)){
  1342. return ''
  1343. }
  1344. let readTextOptions: ReadTextOptions = {
  1345. offset: 0,
  1346. length: 0,
  1347. encoding: 'utf-8'
  1348. };
  1349. let stat = fileIo.statSync(filePath);
  1350. readTextOptions.length = stat.size;
  1351. let str = fileIo.readTextSync(filePath, readTextOptions);
  1352. const parsedData:VipData = JSON.parse(str) as VipData;
  1353. // LogUtil.info('onecold parsedData.expireDate = ' + parsedData.expireDate)
  1354. let result = Base64Util.decodeSync( parsedData.expireDate)
  1355. // LogUtil.info('onecold 解密的result = ' +StrUtil.unit8ArrayToStr(result))
  1356. return StrUtil.unit8ArrayToStr(result);
  1357. }
  1358. }
  1359. interface PathStat {
  1360. path: string;
  1361. isDirectory: boolean;
  1362. }
  1363. async function isDirectory(filePath: string): Promise<boolean> {
  1364. try {
  1365. const stat = await fs.stat(filePath);
  1366. return stat.isDirectory();
  1367. } catch (error) {
  1368. console.error('Error getting file stat:', error);
  1369. return false;
  1370. }
  1371. }
  1372. //排序模式参数
  1373. interface VideoNameParts {
  1374. nonNumeric: string;
  1375. numeric: number;
  1376. suffix:string;
  1377. }
  1378. function extractParts(name: string): VideoNameParts {
  1379. // 更完善的正则表达式,处理各种情况
  1380. // 匹配:非数字前缀 + 数字部分 + 可选的剩余部分
  1381. const match = name.match(/^(\D*)(\d+)(.*)$/);
  1382. if (match) {
  1383. return {
  1384. nonNumeric: match[1] || '', // 非数字前缀
  1385. numeric: parseInt(match[2], 10), // 数字部分
  1386. suffix: match[3] || '' // 剩余部分(可能包含扩展名等)
  1387. };
  1388. }
  1389. // 如果没有找到数字模式,将整个名称作为非数字部分
  1390. return {
  1391. nonNumeric: name,
  1392. numeric: 0,
  1393. suffix: ''
  1394. };
  1395. }
  1396. function getInstallTime(): number | null {
  1397. // 从本地存储中获取安装时间
  1398. return PreferencesUtil.getNumberSync('installTime')
  1399. }
  1400. function setInstallTime(time: number) {
  1401. // 将安装时间存储到本地存储中
  1402. PreferencesUtil.putSync('installTime',time)
  1403. }
  1404. function fetchAlbumCover(avMetadataExtractor: media.AVMetadataExtractor): Promise<image.PixelMap | null> {
  1405. return new Promise((resolve, reject) => {
  1406. avMetadataExtractor.fetchAlbumCover((error: BusinessError, pixelMap: image.PixelMap) => {
  1407. if (error) {
  1408. console.error(`Failed to fetch AlbumCover, error = ${JSON.stringify(error)}`);
  1409. resolve(null);
  1410. } else {
  1411. resolve(pixelMap);
  1412. }
  1413. avMetadataExtractor.release();
  1414. });
  1415. });
  1416. }
  1417. //排序类型
  1418. function getTypeOrder(type: number) {
  1419. switch (type) {
  1420. case CommonConstants.TYPE_IS_DIR:
  1421. return 1; // First
  1422. case CommonConstants.TYPE_IS_CSJAD:
  1423. return 2; // Middle
  1424. case CommonConstants.TYPE_LOCAL:
  1425. return 3; // Last
  1426. default:
  1427. return 4; // Unknown types, if any, go last
  1428. }
  1429. }
  1430. function convertSecondsToTime(secondsStr: string): string {
  1431. if (!secondsStr || isNaN(Number(secondsStr))) {
  1432. return "00:00";
  1433. }
  1434. let seconds = parseInt(secondsStr, 10);
  1435. seconds = Math.floor(seconds / 1000);
  1436. const hours = Math.floor(seconds / 3600);
  1437. const minutes = Math.floor((seconds % 3600) / 60);
  1438. const secs = seconds % 60;
  1439. const formatNumber = (num: number) => num.toString().padStart(2, '0');
  1440. if (hours > 0) {
  1441. return `${formatNumber(hours)}:${formatNumber(minutes)}:${formatNumber(secs)}`;
  1442. } else {
  1443. return `${formatNumber(minutes)}:${formatNumber(secs)}`;
  1444. }
  1445. }
  1446. /**
  1447. * 秒数 → 智能时间格式(自动选择 MM:SS 或 HH:MM:SS)
  1448. * @param seconds 秒数字符串(如 "283.524351" 或 "3675")
  1449. * @param forceHHMMSS 强制使用 HH:MM:SS 格式(默认自动判断)
  1450. * @returns 格式化后的时间字符串
  1451. */
  1452. function formatDuration(seconds: string, forceHHMMSS: boolean = false): string {
  1453. // 1. 校验输入
  1454. const secNum = parseFloat(seconds);
  1455. if (isNaN(secNum) || secNum < 0) return forceHHMMSS ? "00:00:00" : "00:00";
  1456. // 2. 计算时间分量
  1457. const totalSec = Math.floor(secNum);
  1458. const hours = Math.floor(totalSec / 3600);
  1459. const mins = Math.floor((totalSec % 3600) / 60);
  1460. const secs = totalSec % 60;
  1461. // 3. 格式化输出
  1462. const pad = (n: number) => n.toString().padStart(2, '0');
  1463. return forceHHMMSS || hours > 0
  1464. ? `${pad(hours)}:${pad(mins)}:${pad(secs)}` // HH:MM:SS
  1465. : `${pad(mins)}:${pad(secs)}`; // MM:SS
  1466. }
  1467. /**
  1468. * 定义解析结果的数据结构
  1469. */
  1470. export class MusicInfo {
  1471. artist: string = ""; // 艺术家名称
  1472. title: string = ""; // 歌曲名称
  1473. isValid: boolean = false; // 格式是否有效
  1474. }
  1475. /**
  1476. * 精简版常见中文姓氏(音乐场景优化版)
  1477. */
  1478. const COMMON_CHINESE_SURNAMES = [
  1479. // 超常见姓氏(覆盖约80%人口)
  1480. '李','王','张','刘','陈','杨','黄','赵','吴','周',
  1481. '徐','孙','马','朱','胡','林','郭','何','高','罗',
  1482. '郑','梁','谢','宋','唐','许','韩','邓','曹','彭',
  1483. '曾','萧','田','董','潘','袁','于','蔡','余','杜',
  1484. // 音乐行业常见姓氏(歌手高频姓)
  1485. '汪','苏','薛','谭','华','金','魏','陶','姜','窦',
  1486. '章','毛','易','方','宋','任','沈','贾','江','孔',
  1487. // 组合/乐队常见字
  1488. '羽','泉','信','乐','花','飞','龙','风','云','草'
  1489. ];
  1490. /**
  1491. * 歌手特征关键词(优化版)
  1492. */
  1493. const ARTIST_KEYWORDS = [
  1494. // 中文特征
  1495. '乐队','组合','乐团','和','&','合唱',' featuring',
  1496. // 英文特征
  1497. 'feat','ft','with','vs','presents','presents',
  1498. // 符号特征
  1499. '×','X','※','◆','♫'
  1500. ];
  1501. /**
  1502. * 智能解析音乐文件名(支持多种分隔符和格式)
  1503. * @param fileName - 待解析的完整文件名
  1504. * @returns 结构化音乐信息
  1505. */
  1506. /**
  1507. * 智能解析音乐文件名
  1508. */
  1509. /**
  1510. * 智能解析音乐文件名(改进版)
  1511. */
  1512. export function parseMusicFileName(fileName: string): MusicInfo {
  1513. const result: MusicInfo = new MusicInfo();
  1514. if (!fileName) return result;
  1515. // 预处理
  1516. const cleanName: string = fileName.trim();
  1517. const lastDotIndex: number = cleanName.lastIndexOf('.');
  1518. const baseName: string = lastDotIndex > 0 ?
  1519. cleanName.substring(0, lastDotIndex).trim() :
  1520. cleanName;
  1521. // 支持的分隔符(明确定义类型)
  1522. const separators: string[] = ['-', '-', '—', '~', '~'];
  1523. // 寻找分隔位置
  1524. let bestSplitIndex: number = -1;
  1525. for (const sep of separators) {
  1526. const index: number = baseName.lastIndexOf(sep);
  1527. if (index > bestSplitIndex) {
  1528. bestSplitIndex = index;
  1529. }
  1530. }
  1531. // 分割字符串
  1532. if (bestSplitIndex > 0 && bestSplitIndex < baseName.length - 1) {
  1533. let part1: string = baseName.substring(0, bestSplitIndex).trim();
  1534. let part2: string = baseName.substring(bestSplitIndex + 1).trim();
  1535. // 处理前缀序号
  1536. part1 = part1.replace(/^\d+[\s\.\-- —~~]*/, '').trim();
  1537. // 判断歌手部分(改进版)
  1538. const identifyArtist = (str: string): boolean => {
  1539. // 检查是否包含常见歌手关键词
  1540. if (ARTIST_KEYWORDS.some((keyword: string) => str.includes(keyword))) {
  1541. return true;
  1542. }
  1543. // 检查是否以常见姓氏开头
  1544. return COMMON_CHINESE_SURNAMES.some((surname: string) =>
  1545. str.startsWith(surname) ||
  1546. new RegExp(`[ ,,、&&]${surname}`).test(str)
  1547. );
  1548. };
  1549. // 判断歌手位置(改进逻辑)
  1550. const part1IsArtist: boolean = identifyArtist(part1);
  1551. const part2IsArtist: boolean = identifyArtist(part2);
  1552. // 特殊处理:如果两个部分都识别为艺术家或都不是艺术家
  1553. if ((part1IsArtist && part2IsArtist) || (!part1IsArtist && !part2IsArtist)) {
  1554. // 基于常见模式判断:通常"歌曲名 - 歌手"格式更常见
  1555. // 或者根据长度判断,歌手名通常较短
  1556. if (part2.length <= part1.length && identifyArtist(part2)) {
  1557. result.artist = part2;
  1558. result.title = part1;
  1559. } else {
  1560. result.artist = part1;
  1561. result.title = part2;
  1562. }
  1563. } else if (part1IsArtist && !part2IsArtist) {
  1564. result.artist = part1;
  1565. result.title = part2;
  1566. } else if (part2IsArtist && !part1IsArtist) {
  1567. result.artist = part2;
  1568. result.title = part1;
  1569. } else {
  1570. // 默认处理
  1571. result.artist = part1;
  1572. result.title = part2;
  1573. }
  1574. // 后处理
  1575. result.title = result.title
  1576. .replace(/(?:\(|()[^))]*(?:)|\))/g, '')
  1577. .replace(/\s*[—-]\s*(?:Live|Version|Remix|伴奏).*/i, '')
  1578. .trim();
  1579. // 有效性验证
  1580. result.isValid = result.artist.length > 0 &&
  1581. result.title.length > 0;
  1582. } else {
  1583. result.title = baseName;
  1584. result.isValid = false;
  1585. }
  1586. return result;
  1587. }
  1588. function getFileNameWithoutExtension(filePath: string): string {
  1589. const fileName = filePath.split('/').pop() || '';
  1590. const lastDotIndex = fileName.lastIndexOf('.');
  1591. return lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName;
  1592. }
  1593. /**
  1594. * 从音乐文件中提取封面
  1595. * @param inputPath 音乐文件路径
  1596. * @returns Promise<void>
  1597. */
  1598. // async function getFFmpegCover(inputPath: string, outputPath: string) : Promise<boolean>{
  1599. // let commands = ["ffmpeg", "-y","-i", inputPath, "-map", "0:v", "-c:v", "copy", outputPath];
  1600. // FFmpeg.execute(commands, {
  1601. // logCallback: (logLevel: number, logMessage: string) => {
  1602. // console.info(`[FFmpeg LOG] [${logLevel}]${logMessage}`)
  1603. // },
  1604. // progressCallback: (message: string) => {
  1605. // console.info(`[FFmpeg progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
  1606. // },
  1607. // })
  1608. // .then(() => {
  1609. // return true
  1610. // console.info("FFmpeg execution succeeded.");
  1611. // })
  1612. // .catch((error: Error) => {
  1613. // return false
  1614. // console.error(`FFmpeg execution failed with error: ${error.message}`);
  1615. // });
  1616. // return ;
  1617. // }
  1618. /**
  1619. * 使用华为接口提取音乐文件封面(备用方案)
  1620. * @param inputPath 音乐文件路径
  1621. * @param context 应用上下文
  1622. * @param name 文件名称(用于保存)
  1623. * @returns Promise<string> 返回封面图片的URI路径,失败返回空字符串
  1624. */
  1625. async function extractCoverWithHwInterface(inputPath: string, context: Context, name: string): Promise<string> {
  1626. try {
  1627. // 创建AVMetadataExtractor对象
  1628. const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
  1629. // 设置fdSrc
  1630. const fd = await fs.openSync(inputPath, fs.OpenMode.READ_ONLY);
  1631. avMetadataExtractor.fdSrc = fd;
  1632. // 获取专辑封面
  1633. const pixelMap = await fetchAlbumCover(avMetadataExtractor);
  1634. // 释放资源(promise模式)
  1635. await avMetadataExtractor.release();
  1636. console.info('onecold release success. name= ' + name);
  1637. if (pixelMap !== undefined && pixelMap !== null) {
  1638. const imagePath = await ImageUtil.savePixelMap(pixelMap, context.filesDir, name);
  1639. return fileUri.getUriFromPath(imagePath);
  1640. } else {
  1641. return '';
  1642. }
  1643. } catch (error) {
  1644. console.error('华为接口提取封面失败:', error instanceof Error ? error.message : String(error));
  1645. return '';
  1646. }
  1647. }
  1648. /**
  1649. * 从音乐文件中提取封面
  1650. * @param inputPath 音乐文件路径
  1651. * @param outputPath 封面输出路径
  1652. * @returns Promise<boolean> 表示操作是否成功
  1653. */
  1654. async function getFFmpegCover(inputPath: string, outputPath: string): Promise<boolean> {
  1655. let commands = ["ffmpeg", "-y","-i", inputPath, "-map", "0:v", "-c:v", "copy", outputPath];
  1656. try {
  1657. await FFmpeg.execute(commands, {
  1658. logCallback: (logLevel: number, logMessage: string) => {
  1659. console.info(`[FFmpeg LOG] [${logLevel}] ${logMessage}`);
  1660. },
  1661. progressCallback: (message: string) => {
  1662. console.info(`[FFmpeg progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
  1663. },
  1664. });
  1665. console.info("FFmpeg execution succeeded.");
  1666. return true;
  1667. } catch (error) {
  1668. console.error(`FFmpeg execution failed with error: ${error instanceof Error ? error.message : String(error)}`);
  1669. try {
  1670. let primaryCommands = [
  1671. "ffmpeg", "-y",
  1672. "-i", inputPath,
  1673. "-map", "0:2", // 直接提取第三个流(Stream #0:2),这是附加图片流
  1674. "-c:v", "copy", // 直接复制流数据
  1675. outputPath
  1676. ];
  1677. await FFmpeg.execute(primaryCommands, {
  1678. logCallback: (logLevel: number, logMessage: string) => {
  1679. console.info(`[FFmpeg LOG] [${logLevel}] ${logMessage}`);
  1680. },
  1681. progressCallback: (message: string) => {
  1682. console.info(`[FFmpeg progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
  1683. },
  1684. });
  1685. console.info("FFmpeg 2 execution succeeded.");
  1686. return true;
  1687. }catch (error) {
  1688. console.error(`FFmpeg execution 2 failed with error: ${error instanceof Error ? error.message : String(error)}`);
  1689. return false;
  1690. }
  1691. }
  1692. }
  1693. /**
  1694. * 从视频文件中提取封面 提前视频第5帧的封面
  1695. * @param inputPath 文件路径
  1696. * @returns Promise<void>
  1697. */
  1698. async function getVideoFFmpegCover(inputPath: string, outputPath: string) {
  1699. // 更可靠的命令,专门提取视频第一帧作为封面
  1700. const commands = [
  1701. "ffmpeg",
  1702. "-y", // 覆盖输出文件(如果存在)
  1703. "-i", inputPath, // 输入文件
  1704. "-ss", "00:00:00", // 定位到开始位置
  1705. "-vframes", "1", // 只提取1帧
  1706. "-q:v", "2", // 高质量JPEG(1-31,2是最高质量)
  1707. "-f", "image2", // 强制输出为图像格式
  1708. outputPath
  1709. ];
  1710. FFmpeg.execute(commands, {
  1711. logCallback: (logLevel: number, logMessage: string) => {
  1712. console.info(`[FFmpegX LOG] [${logLevel}]${logMessage}`)
  1713. },
  1714. progressCallback: (message: string) => {
  1715. console.info(`[FFmpegX progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
  1716. },
  1717. })
  1718. .then(() => {
  1719. })
  1720. .catch((error: Error) => {
  1721. console.error(`FFmpegX execution failed with error: ${error.message}`);
  1722. });
  1723. }
  1724. /**
  1725. * 从音乐文件中提取歌词内容
  1726. * @param inputPath 音乐文件路径
  1727. * @returns Promise<string> 直接返回歌词内容,若无歌词则返回空字符串
  1728. */
  1729. async function extractLyricsContent(inputPath: string): Promise<string> {
  1730. return new Promise(async (resolve, reject) => {
  1731. try {
  1732. // 1. 使用ffprobe获取元数据
  1733. const commands = [
  1734. 'ffprobe',
  1735. '-v', 'quiet',
  1736. '-print_format', 'json',
  1737. '-show_format',
  1738. inputPath
  1739. ];
  1740. let outputJson = '';
  1741. await FFmpeg.execute(commands, {
  1742. outputCallback: (message: string) => outputJson += message,
  1743. });
  1744. // 2. 解析歌词标签
  1745. const metadata:FFprobeMetadata = JSON.parse(outputJson);
  1746. const tags = metadata.format?.tags || {};
  1747. // 3. 从常见标签中查找歌词(优先级顺序)
  1748. const lyricContent =
  1749. tags.LYRICS || // 标准标签
  1750. tags.lyrics || // 小写变体
  1751. tags.USLT || // ID3v2标签
  1752. tags.UNSYNCEDLYRICS ||
  1753. '';
  1754. resolve(lyricContent.trim());
  1755. } catch (error) {
  1756. reject(`解析失败: ${error instanceof Error ? error.message : String(error)}`);
  1757. }
  1758. });
  1759. }
  1760. /**
  1761. * 解析音乐文件元数据(包含歌词和其他关键字段)
  1762. * @param inputPath 文件路径
  1763. * @returns Promise<VideoItem> 包含完整元数据的对象
  1764. */
  1765. async function parseAudioMetadata(inputPath: string): Promise<VideoItem> {
  1766. return new Promise(async (resolve, reject) => {
  1767. inputPath = FileUtil.getFilePath(inputPath)
  1768. try {
  1769. // 1. 执行FFprobe命令
  1770. const commands: string[] = [
  1771. 'ffprobe',
  1772. '-v', 'quiet',
  1773. '-print_format', 'json',
  1774. '-show_format',
  1775. '-show_streams',
  1776. inputPath
  1777. ];
  1778. let outputJson = '';
  1779. await FFmpeg.execute(commands, {
  1780. outputCallback: (message: string) => outputJson += message,
  1781. });
  1782. // 2. 解析元数据
  1783. const metadata = JSON.parse(outputJson) as FFprobeMetadata;
  1784. const format:FFprobeFormat = metadata.format ;
  1785. const tags = format.tags||{} ;
  1786. // 3. 构建VideoItem基础信息
  1787. const videoItem = new VideoItem(
  1788. tags.title || getFileNameWithoutExtension(inputPath),
  1789. inputPath, // id
  1790. inputPath,
  1791. CommonConstants.TYPE_LOCAL,
  1792. 0,
  1793. new Date().toISOString() // 使用当前时间作为默认创建时间
  1794. );
  1795. // 4. 设置关键字段
  1796. videoItem.bit_rate = format.bit_rate || '';
  1797. videoItem.probe_score = format.probe_score || 0;
  1798. videoItem.year = tags.TYER || tags.date || '';
  1799. videoItem.lyricContent =
  1800. tags.LYRICS ??
  1801. tags.lyrics ??
  1802. tags.USLT ??
  1803. tags.UNSYNCEDLYRICS ??
  1804. '';
  1805. // 5. 设置其他可选字段
  1806. videoItem.artist = tags.artist || '';
  1807. videoItem.album = tags.album || '';
  1808. videoItem.duration = format.duration || '';
  1809. videoItem.nb_streams = format.nb_streams;
  1810. videoItem.nb_programs = format.nb_programs;
  1811. resolve(videoItem);
  1812. } catch (error) {
  1813. reject(new Error(`元数据解析失败: ${error instanceof Error ? error.message : String(error)}`));
  1814. }
  1815. });
  1816. }
  1817. /**
  1818. * 将比特率(bps)转换为 kbps 并格式化
  1819. * @param bitRate 比特率字符串(如 "5644802")
  1820. * @param decimalPlaces 保留小数位数(默认0)
  1821. * @returns 格式化后的 kbps 字符串(如 "5644 kbps")
  1822. */
  1823. function formatBitrateToKbps(bitRate: string, decimalPlaces: number = 0): string {
  1824. // 1. 转换为数字
  1825. const bitsPerSecond = parseInt(bitRate);
  1826. if (isNaN(bitsPerSecond) || bitsPerSecond < 0) return "0 kbps";
  1827. // 2. 计算 kbps(1 kbps = 1000 bps)
  1828. const kbps = bitsPerSecond / 1000;
  1829. // 3. 格式化输出
  1830. return `${kbps.toFixed(decimalPlaces)} kbps`;
  1831. }
  1832. // 定义音质等级
  1833. export enum AudioQuality {
  1834. LQ = "LQ", // 低音质
  1835. HQ = "HQ", // 高音质
  1836. SQ = "SQ", // 无损
  1837. HIRES = "Hi-Res", // 高解析度
  1838. HR = "HR" // 高解析度
  1839. }
  1840. // 明确定义支持的音频格式类型
  1841. interface SupportedFormats {
  1842. LOSSY: Set<string>;
  1843. LOSSLESS: Set<string>;
  1844. }
  1845. // 声明并初始化支持的音频格式(符合类型定义)
  1846. const SUPPORTED_FORMATS: SupportedFormats = {
  1847. LOSSY: new Set(['mp3', 'aac', 'ogg', 'opus', 'wma']),
  1848. LOSSLESS: new Set(['flac', 'alac', 'ape', 'wav', 'aiff', 'dsf','aif'])
  1849. };
  1850. /**
  1851. * 根据音频参数判断音质等级
  1852. * @param format 编码格式(字符串,不区分大小写,如"MP3"或"flac")
  1853. * @param bitrate 比特率(单位:bps,如320kbps需传入320000)
  1854. * @param sampleRate 采样率(单位:Hz,如44.1kHz需传入44100)
  1855. * @param bitDepth 位深(单位:bit,默认16bit,Hi-Res需≥24bit)
  1856. * @returns AudioQuality 音质等级
  1857. */
  1858. function determineAudioQuality(
  1859. format: string,
  1860. bitrate: number,
  1861. sampleRate: number,
  1862. ): AudioQuality {
  1863. // 统一转为小写便于比较
  1864. const normalizedFormat = format.toLowerCase();
  1865. // --------------- 第一步:判断是否无损/Hi-Res ---------------
  1866. const isLossless = SUPPORTED_FORMATS.LOSSLESS.has(normalizedFormat);
  1867. if (isLossless) {
  1868. // Hi-Res标准:采样率≥96kHz且位深≥24bit
  1869. if (sampleRate >= 48000 ) {
  1870. return AudioQuality.HIRES;
  1871. }
  1872. // CD级无损标准:采样率≥44.1kHz且位深≥16bit
  1873. if (sampleRate >= 44100) {
  1874. return AudioQuality.SQ;
  1875. }
  1876. }
  1877. // --------------- 第二步:有损音质判断 ---------------
  1878. // 高音质标准:比特率≥256kbps且采样率≥44.1kHz
  1879. if (bitrate >= 256000 && sampleRate >= 44100) {
  1880. return AudioQuality.HQ;
  1881. }
  1882. // 其余情况为低音质
  1883. else {
  1884. return AudioQuality.LQ;
  1885. }
  1886. }
  1887. function getFileFormatByPath(filePath: string): string {
  1888. // 检查文件路径是否有效
  1889. if (!filePath || filePath.length === 0) {
  1890. console.error(' 文件路径不能为空');
  1891. return '';
  1892. }
  1893. // 获取最后一个点号的位置
  1894. const lastDotIndex = filePath.lastIndexOf('.');
  1895. // 如果没有点号或者点号在最后一位,返回空字符串
  1896. if (lastDotIndex === -1 || lastDotIndex === filePath.length - 1) {
  1897. return '';
  1898. }
  1899. // 返回点号后的部分(转换为小写)
  1900. return filePath.substring(lastDotIndex + 1).toLowerCase();
  1901. }
  1902. // 检查乱码的简单方法
  1903. function hasGarbledText(tags: FFMpegTags): boolean {
  1904. if (!tags) return false;
  1905. const text = [tags.title, tags.artist, tags.album].join("");
  1906. return (
  1907. /[À-ÿµØÇò]/.test(text) || // 拉丁乱码
  1908. /[\uFFFD�]/.test(text) || // Unicode替换字符
  1909. /[�□▪▫▢▣▤▥▦▧▨▩▪▫◊○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◢◣◤◥◦◧◨◩◪◫◬◭◮◯]/.test(text) || // 几何图形
  1910. /[\uD800-\uDFFF]/.test(text) // 无效的UTF-16代理对
  1911. );
  1912. }
  1913. /**
  1914. * 提取华为接口的媒体文件元数据
  1915. * @param filePath 文件路径(如:/data/storage/el1/base/test.mp3 )
  1916. * @returns Promise<FFMpegTags> 包含专辑、艺术家、标题的元数据对象
  1917. */
  1918. export async function extractHwMediaMetadata(filePath: string): Promise<FFMpegTags> {
  1919. const tags: FFMpegTags = {};
  1920. if (!canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
  1921. console.warn("Metadata extraction not supported on this device");
  1922. return tags;
  1923. }
  1924. try {
  1925. const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
  1926. const fd = await fs.open(filePath, fs.OpenMode.READ_ONLY);
  1927. // 设置文件描述符并提取元数据
  1928. avMetadataExtractor.fdSrc = fd;
  1929. const metadata = await avMetadataExtractor.fetchMetadata();
  1930. // 映射标准FFMpeg标签
  1931. if (metadata) {
  1932. tags.album = metadata.album ?? undefined;
  1933. tags.artist = metadata.artist ?? metadata.author ?? undefined; // 兼容不同元数据字段
  1934. tags.title = metadata.title ?? undefined;
  1935. }
  1936. // 关闭文件描述符
  1937. await fs.close(fd);
  1938. return tags;
  1939. } catch (error) {
  1940. const err = error as BusinessError;
  1941. console.error(`Metadata extraction failed: ${err.code}, ${err.message}`);
  1942. return tags;
  1943. }
  1944. return tags;
  1945. }