Utility.ets 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803
  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 } 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 { LocalMusic } from '../../view/LocalMusic';
  26. import { VipPage } from '../../pages/VipPage';
  27. import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
  28. interface FFMpegTags {
  29. album?: string;
  30. ALBUM?: string;
  31. artist?: string;
  32. ARTIST?: string;
  33. TITLE?: string;
  34. title?: string;
  35. track?: string;
  36. TRACK?: string;
  37. TYER?: string;
  38. year?: string;
  39. genre?:string;
  40. GENRE?:string;
  41. date?: string;
  42. DATE?: string;
  43. LYRICS?: string;
  44. lyrics?: string; // 小写变体
  45. USLT?: string; // ID3v2同步歌词
  46. UNSYNCEDLYRICS?: string; // ID3v2非同步歌词
  47. // Add any other tag properties you expect
  48. }
  49. interface FFprobeFormat {
  50. filename: string;
  51. nb_streams: number;
  52. nb_programs: number;
  53. format_name: string;
  54. format_long_name: string;
  55. duration: string;
  56. size: string;
  57. bit_rate: string;
  58. probe_score: number;
  59. tags?: FFMpegTags;
  60. }
  61. interface FFprobeStream {
  62. // Define stream properties as needed
  63. codec_type?: string; // 流类型,如"audio"、"video"
  64. sample_rate?: string; // 采样率
  65. bit_rate?: string; // 比特率
  66. disposition?: StreamDisposition; // 添加disposition属性
  67. }
  68. interface StreamDisposition {
  69. default?: number;
  70. dub?: number;
  71. original?: number;
  72. comment?: number;
  73. lyrics?: number;
  74. karaoke?: number;
  75. forced?: number;
  76. hearing_impaired?: number;
  77. visual_impaired?: number;
  78. clean_effects?: number;
  79. attached_pic?: number; // 添加封面图片标识
  80. timed_thumbnails?: number;
  81. captions?: number;
  82. descriptions?: number;
  83. metadata?: number;
  84. dependent?: number;
  85. still_image?: number;
  86. }
  87. interface FFprobeMetadata {
  88. streams: FFprobeStream[];
  89. format: FFprobeFormat;
  90. }
  91. export class Utility {
  92. private constructor() {}
  93. static isOpenTime():boolean{
  94. const currentDate = new Date();
  95. const targetDate = new Date(CommonConstants.OPEN_DATE);
  96. if (currentDate > targetDate) {
  97. return true
  98. }
  99. return false
  100. }
  101. static readonly FOREVER_DATE: string = 'forever';
  102. /**
  103. * 是否会员(只看线上会员状态)
  104. */
  105. static isNoble(): boolean {
  106. // 只判断线上会员状态
  107. return PreferencesUtil.getBooleanSync('hasActiveSubscription', false);
  108. }
  109. static isForever(): boolean {
  110. // 判断是不是永久会员
  111. return PreferencesUtil.getBooleanSync('isForever', false);
  112. }
  113. /**
  114. * 旧版客户端判断会员状态
  115. * @returns
  116. */
  117. static isNobleForOld():boolean{
  118. if(!Utility.isOpenTime())//提交审核的审核,刚刚开始可以全部是赞助用户。
  119. return true
  120. return PreferencesUtil.getBooleanSync('isNoble',false)
  121. }
  122. /**
  123. * setNoble方法废弃,保留兼容但不做任何操作
  124. */
  125. static setNoble(expireDate:string){
  126. let isNoble = false
  127. if(StrUtil.isNotEmpty(expireDate)){
  128. if(expireDate===VipPage.FOREVER_DATE){
  129. PreferencesUtil.putSync('isForever',true)
  130. isNoble = true
  131. }else{
  132. PreferencesUtil.putSync('isForever',false)
  133. const currentDate = new Date();
  134. const targetDate = DateUtil.getFormatDate(expireDate)
  135. if (currentDate > targetDate) {
  136. isNoble = false
  137. }else{
  138. isNoble = true
  139. }
  140. }
  141. }
  142. PreferencesUtil.putSync('isNoble',isNoble)
  143. }
  144. /**
  145. * 判断用户安装app是否超过2天,超过2天(2800分钟)才展示广告 显示广告 24*60=1440 1440*2=2800
  146. * @param day
  147. * @returns
  148. */
  149. static isPassInstallTime(day:number):boolean{
  150. //获取当前时间
  151. let currentTime = new Date().getTime();
  152. // 尝试从存储中获取安装时间
  153. let installTime = getInstallTime();
  154. if (installTime===0||installTime === null|| installTime===undefined) {
  155. // 如果没有存储安装时间,则存储当前时间为安装时间
  156. setInstallTime(currentTime);
  157. return false
  158. } else {
  159. // 计算时间差(以分钟为单位)
  160. const timeDifference = (currentTime - installTime) / (1000 * 60);
  161. LogUtil.debug("onecold timeDifference=" + timeDifference)
  162. // 如果超过5分钟,显示广告 24*60=1440 1440*2=2800
  163. if (timeDifference > 24*60*day) {
  164. return true
  165. }else{
  166. return false
  167. }
  168. }
  169. }
  170. // 定义一个函数来获取当前的时间段
  171. static getTimePeriod(): string {
  172. const now = new Date();
  173. const hours = now.getHours();
  174. if (hours >= 5 && hours < 12) {
  175. return "早上好";
  176. } else if (hours >= 12 && hours < 18) {
  177. return "下午好";
  178. } else if (hours >= 18 && hours < 22) {
  179. return "晚上好";
  180. } else {
  181. return "夜深了";
  182. }
  183. }
  184. static optimizedFormat(speed: number): string {
  185. if (speed) {
  186. const str = speed.toFixed(2);
  187. let end = str.length;
  188. while (end > 0 && (str[end - 1] === '0' || str[end - 1] === '.')) {
  189. end--;
  190. if (str[end] === '.') {
  191. break;
  192. }
  193. }
  194. return str.slice(0, end || 1) + 'x';
  195. } else {
  196. return '1x'
  197. }
  198. }
  199. static isHaoOpenTime():boolean{
  200. const currentDate = new Date();
  201. const targetDate = new Date('2025-04-20');
  202. if (currentDate < targetDate) {
  203. return true
  204. }
  205. return false
  206. }
  207. static addDays(date: Date, days: number): Date {
  208. const result = new Date(date);
  209. result.setDate(result.getDate() + days);
  210. return result;
  211. }
  212. static convertToKHz(sampleRateHz: string|undefined): string {
  213. if(StrUtil.isEmpty(sampleRateHz)||sampleRateHz ===undefined)
  214. return '0KHz'
  215. const sampleRateKHz = Number(sampleRateHz) / 1000;
  216. return `${sampleRateKHz} KHz`;
  217. }
  218. /**
  219. * 格式化媒体格式类型显示
  220. * @param mimeType 媒体格式类型字符串
  221. * @return 格式化后的显示字符串
  222. */
  223. static formatMimeType(mimeType: string | undefined): string {
  224. if (StrUtil.isEmpty(mimeType) || mimeType === undefined) {
  225. return 'unknown';
  226. }
  227. // 从MIME类型中提取格式部分,例如 "audio/mp3" -> "MP3"
  228. try {
  229. const parts = mimeType.split('/');
  230. if (parts.length > 1) {
  231. return parts[1].toUpperCase();
  232. } else {
  233. return mimeType.toUpperCase();
  234. }
  235. } catch (err) {
  236. console.error(`格式化媒体类型出错: ${err}`);
  237. return 'unknown';
  238. }
  239. }
  240. //根据字节获取大小
  241. static formatFileSize(bytes:number) {
  242. const units = ['Bytes', 'Kbps', 'Mbps'];
  243. let size = bytes;
  244. let unitIndex = 0;
  245. while (size >= 1024 && unitIndex < units.length - 1) {
  246. size /= 1024;
  247. unitIndex++;
  248. }
  249. if(size > 1024&&unitIndex==2){
  250. size = size*0.1
  251. }
  252. if(size > 1024&&unitIndex==2){
  253. size = size*0.1
  254. }
  255. if(size > 480&&unitIndex==2){
  256. size = size*0.5
  257. }
  258. // 保留两位小数, 四舍五入
  259. size = Math.round(size * 10) / 10;
  260. return size + units[unitIndex]
  261. }
  262. //根据字节获取大小无单位
  263. static formatFileSizeWithout(bytes:number) {
  264. const units = ['Bytes', 'Kbps', 'Mbps'];
  265. let size = bytes;
  266. let unitIndex = 0;
  267. while (size >= 1024 && unitIndex < units.length - 1) {
  268. size /= 1024;
  269. unitIndex++;
  270. }
  271. if(size > 1024&&unitIndex==2){
  272. size = size*0.1
  273. }
  274. if(size > 1024&&unitIndex==2){
  275. size = size*0.1
  276. }
  277. if(size > 480&&unitIndex==2){
  278. size = size*0.5
  279. }
  280. // 保留两位小数, 四舍五入
  281. size = Math.round(size * 10) / 10;
  282. return size
  283. }
  284. static copyText(text:string):boolean{
  285. const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN,text)
  286. const systemPasteboard = pasteboard.getSystemPasteboard()
  287. systemPasteboard.setData(pasteboardData)
  288. systemPasteboard.getData().then((data)=>{
  289. return data
  290. })
  291. return false
  292. }
  293. static getDbm(rssi:number):number{
  294. return Math.round((rssi / 2) + 100)
  295. }
  296. //根据数字形式的IP地址获取字符串形式的IP地址
  297. static getIpAddrFromNum(ipNum: number): string {
  298. return (ipNum >>> 24) + '.' + (ipNum >> 16 & 0xFF) + '.' + (ipNum >> 8 & 0xFF) + '.' + (ipNum & 0xFF);
  299. }
  300. static resolveIP(ip:number) {
  301. let address: string = ip.toString()
  302. if (address === '0') {
  303. return '00:00:000:000'
  304. }
  305. address.substring(0, 2)
  306. return `${address.substring(0, 2)}:${address.substring(2, 4)}:${address.substring(4, 7)}:${address.substring(7, 10)}`
  307. }
  308. static isFreeTime():boolean{
  309. if(DateUtil.isWeekend()){
  310. return true
  311. }
  312. let str = DateUtil.getFormatDateStr(new Date(), 'HH')
  313. let a = Number(str);
  314. if(isNaN(a))
  315. return false
  316. // ToastUtil.showToast('a='+a)
  317. if (a >= 7 && a <13) {//白天
  318. return false;
  319. }
  320. if (a >= 13 && a <20) {//白天
  321. return false;
  322. }
  323. if (a >= 0 && a < 7) {//凌晨
  324. return true;
  325. }
  326. if (a >= 20 && a <= 24) {//晚上
  327. return true;
  328. }
  329. return false
  330. }
  331. //判断是否是音乐文件
  332. static isMusicByExtension(filename:string) {
  333. const extensions = CommonConstants.REAL_MUSIC_FORMAT
  334. const lastIndex = filename.lastIndexOf('.');
  335. if (lastIndex!== -1) {
  336. const fileExtension = filename.slice(lastIndex).toLowerCase();
  337. return extensions.includes(fileExtension);
  338. }
  339. return false;
  340. }
  341. //判断是否是媒体文件 音乐和视频都可以
  342. static isMeidaByExtension(filename:string) {
  343. const extensions = CommonConstants.MEDIA_FORMAT
  344. const lastIndex = filename.lastIndexOf('.');
  345. if (lastIndex!== -1) {
  346. const fileExtension = filename.slice(lastIndex).toLowerCase();
  347. return extensions.includes(fileExtension);
  348. }
  349. return false;
  350. }
  351. //判断是否是视频文件
  352. static isVideoByExtension(filename:string) {
  353. const extensions = CommonConstants.VIDEO_FORMAT
  354. const lastIndex = filename.lastIndexOf('.');
  355. if (lastIndex!== -1) {
  356. const fileExtension = filename.slice(lastIndex).toLowerCase();
  357. return extensions.includes(fileExtension);
  358. }
  359. return false;
  360. }
  361. // 获取缩略图
  362. static async getFetchFrameByTime(filePath: string) {
  363. if(Utility.isMusicByExtension(filePath)){
  364. return undefined
  365. }
  366. let pixelMap:image.PixelMap|undefined =undefined;
  367. try{
  368. // 创建AVImageGenerator对象
  369. let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()
  370. let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
  371. let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };
  372. avImageGenerator.fdSrc = avFileDescriptor;
  373. // 初始化入参
  374. let timeUs = 0
  375. let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC
  376. let param: media.PixelMapParams = {
  377. width : 300,
  378. height : 400,
  379. }
  380. // 获取缩略图(promise模式)
  381. pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param)
  382. // 释放资源(promise模式)
  383. avImageGenerator.release()
  384. console.info(`release success.`)
  385. fs.closeSync(file);
  386. }catch (error) {
  387. console.error('uriGetAssets failed with err: ' + JSON.stringify(error));
  388. }
  389. return pixelMap
  390. }
  391. // 获取fd文件路径
  392. static async getFdDir(path:string){
  393. let fdPath = 'fd://';
  394. let file = await fileIo.open(path)
  395. fdPath = fdPath + '' + file.fd;
  396. return fdPath
  397. }
  398. // 根据uri获取文件名称
  399. static getMediaNameByUri(myUri: string) {
  400. let myFileName = (myUri.split('/').pop()) as string;
  401. return decodeURIComponent(myFileName)
  402. }
  403. static msToStandardTime(ms:string):string{
  404. let date = new Date(ms);
  405. let hours = date.getHours();
  406. let minutes = date.getMinutes();
  407. let seconds = date.getSeconds();
  408. return hours + ':' + minutes + ':' + seconds;
  409. }
  410. static getTimestampFromDateStr(dateStr: string, format: string = 'yyyy-MM-dd HH:mm'): number {
  411. // 定义正则表达式以匹配日期字符串中的各部分
  412. const regex = format
  413. .replace('yyyy', '(\\d{4})')
  414. .replace('MM', '(\\d{2})')
  415. .replace('dd', '(\\d{2})')
  416. .replace('HH', '(\\d{2})')
  417. .replace('mm', '(\\d{2})');
  418. const match = new RegExp(regex).exec(dateStr);
  419. if (!match) {
  420. throw new Error('Invalid date string or format');
  421. }
  422. // 解析匹配结果
  423. const year = Number(match[1]);
  424. const month = Number(match[2]);
  425. const day = Number(match[3]);
  426. const hours = Number(match[4]);
  427. const minutes = Number(match[5]);
  428. // 创建 Date 对象
  429. const dateObj = new Date(year, month - 1, day, hours, minutes);
  430. // 返回时间戳(以秒为单位)
  431. return Math.floor(dateObj.getTime() / 1000);
  432. }
  433. static getFormatDateStr(date: number | string | Date, format: string = 'yyyy-MM-dd HH:mm:ss'): string {
  434. // 将输入转换为 Date 对象
  435. let dateObj: Date;
  436. if (typeof date === 'number') {
  437. // 如果是十位时间戳,转换为毫秒数
  438. dateObj = new Date(date.toString().length === 10 ? date * 1000 : date);
  439. } else if (typeof date === 'string') {
  440. // 如果是字符串,尝试解析为 Date 对象
  441. dateObj = new Date(date);
  442. } else {
  443. // 如果是 Date 对象,直接使用
  444. dateObj = date;
  445. }
  446. // 定义替换规则
  447. const replacements = new Map<string, string>([
  448. ['yyyy', String(dateObj.getFullYear())],
  449. ['MM', String(dateObj.getMonth() + 1).padStart(2, '0')],
  450. ['dd', String(dateObj.getDate()).padStart(2, '0')],
  451. ['HH', String(dateObj.getHours()).padStart(2, '0')],
  452. ['mm', String(dateObj.getMinutes()).padStart(2, '0')],
  453. ['ss', String(dateObj.getSeconds()).padStart(2, '0')]
  454. ]);
  455. // 替换格式字符串中的占位符
  456. replacements.forEach((value, key) => {
  457. format = format.replace(new RegExp(key, 'g'), value);
  458. });
  459. return format;
  460. }
  461. //获取从文件管理器获得的视频资源的属性值
  462. static async uriGetAssetsFromFile(context:Context,uri:string,type:number,isLoadPixelMap?:boolean): Promise<VideoItem> {
  463. let item:VideoItem = new VideoItem('',uri,uri,type,0,'')
  464. try {
  465. console.info('asset file.uri: ', uri);
  466. let file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
  467. console.info("file.fd " + file.fd);
  468. let fdfd = 'fd://' + file.fd
  469. //3、通过fs.stat方法获取stat对象
  470. console.info('asset file.name: ', file.name);
  471. console.info('asset file.uri: ', uri);
  472. console.info('asset file.fd: ', file.fd);
  473. console.info('asset file.path: ', file.path);
  474. item = new VideoItem(file.name,uri,uri,type,0,'')
  475. await fs.stat(file.fd).then(async (stat: fs.Stat) => {
  476. console.info("get file info succeed, the size of file is " + stat.size);
  477. let videoSize = stat.size
  478. // let videoTime = stat.ctime
  479. let fileSize = Utility.formatFSize(videoSize)
  480. let cTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
  481. // console.info('asset stat.ino: ', stat.ino);
  482. // console.info('asset stat.mode: ', stat.mode);
  483. // console.info('asset stat.uid: ', stat.uid);
  484. // console.info('asset stat.ino: ', stat.gid);
  485. // console.info('asset stat.size: ', stat.size);
  486. console.info('asset stat.ctime: ', stat.ctime);
  487. // console.info('asset stat.mtime: ', stat.mtime);
  488. // console.info('asset stat.duration: ', duration);
  489. let pixelMap:image.PixelMap|undefined = undefined
  490. if(isLoadPixelMap){
  491. //获取缩略图
  492. if(Utility.isVideoByExtension(uri)){
  493. pixelMap = await Utility.getFetchFrameByTime(uri)
  494. }else{
  495. pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri)
  496. }
  497. }
  498. item = new VideoItem( file.name,uri ,uri,type,videoSize,cTime,pixelMap,fileSize,
  499. await ImageUtil.pixelMapToBase64Str(pixelMap))
  500. })
  501. } catch (error) {
  502. console.error('uriGetAssetsFromFile failed with err: ' + JSON.stringify(error));
  503. }
  504. return item
  505. }
  506. static async getFilePixelMapBig(uri:string){
  507. let pixelMap:image.PixelMap|undefined = undefined
  508. if(Utility.isMusicByExtension(uri)){
  509. pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri)
  510. }else{
  511. pixelMap = await Utility.getFetchFrameByTime(uri)
  512. }
  513. return ImageUtil.pixelMapToBase64StrBig(pixelMap)
  514. }
  515. static async getFilePixelMap(uri:string){
  516. let pixelMap:image.PixelMap|undefined = undefined
  517. if(Utility.isMusicByExtension(uri)){
  518. pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri)
  519. }else{
  520. pixelMap = await Utility.getFetchFrameByTime(uri)
  521. }
  522. return ImageUtil.pixelMapToBase64Str(pixelMap)
  523. }
  524. //根据filePathe获取对应的播放Index
  525. static getIndexFromList(localList:Array<VideoItem>,filePath:string){
  526. let list: Array<string> = [];
  527. for(let i=0;i<localList.length;i++){
  528. if(localList[i].filePath === filePath){
  529. return i
  530. }
  531. }
  532. return 0
  533. }
  534. // 在以下demo中,使用资源管理接口获取打包在HAP内的媒体资源文件,通过设置fdSrc属性,获取音频元数据并打印,
  535. // 获取音频专辑封面并通过Image控件显示在屏幕上。该demo以Promise形式进行异步接口调用
  536. static async getFetchMetadataFromFdSrcByPromise(uri: string): Promise<image.PixelMap | undefined> {
  537. let cover: image.PixelMap | undefined = undefined;
  538. if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
  539. try {
  540. // 创建AVMetadataExtractor对象
  541. const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
  542. // 设置fdSrc
  543. const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
  544. avMetadataExtractor.fdSrc = fd;
  545. // 获取元数据(promise模式)
  546. const metadata = await avMetadataExtractor.fetchMetadata();
  547. metadata.author
  548. console.info(`get meta data, hasAudio: ${metadata.hasAudio}`);
  549. // 获取专辑封面(promise模式)
  550. cover = await avMetadataExtractor.fetchAlbumCover();
  551. // 释放资源(promise模式)
  552. await avMetadataExtractor.release();
  553. console.info('release success.');
  554. } catch (error) {
  555. console.error('Error during metadata extraction:', error);
  556. }
  557. } else {
  558. console.warn('AVMetadataExtractor capability is not supported.');
  559. }
  560. return cover;
  561. }
  562. static formatTimestamp(timestamp: number): string {
  563. // 将时间戳转换为 Date 对象
  564. const date = new Date(timestamp);
  565. // 获取各个部分
  566. const year = date.getFullYear();
  567. const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要加1
  568. const day = String(date.getDate()).padStart(2, '0');
  569. const hours = String(date.getHours()).padStart(2, '0');
  570. const minutes = String(date.getMinutes()).padStart(2, '0');
  571. const seconds = String(date.getSeconds()).padStart(2, '0');
  572. // 拼接成所需的格式
  573. return `${year}-${month}-${day} ${hours}:${minutes}`;
  574. }
  575. //获取音乐资源的属性值,
  576. static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number,isLoadPixelMap?:boolean): Promise<VideoItem> {
  577. return Utility.readMetaInfoFFmpeg(context,uri,type)
  578. //华为官方不支持的格式 不支持内嵌歌词Dsf,aif,aiff,wav 不支持内嵌封面dsf,aif,aiff
  579. if(StrUtil.isNotEmpty(uri)){
  580. if(uri.toLowerCase().endsWith('.dsf')
  581. ||uri.toLowerCase().endsWith('.aif')
  582. // ||uri.toLowerCase().endsWith('.wav')
  583. ||uri.toLowerCase().endsWith('.aiff')){
  584. return Utility.readMetaInfoFFmpeg(context,uri,type)
  585. }
  586. }
  587. let item:VideoItem = new VideoItem('',uri,uri,type,0,'')
  588. try {
  589. console.info('asset file.uri: ', uri);
  590. let file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
  591. console.info("file.fd " + file.fd);
  592. let fdfd = 'fd://' + file.fd
  593. //3、通过fs.stat方法获取stat对象
  594. console.info('asset file.name: ', file.name);
  595. console.info('asset file.uri: ', uri);
  596. console.info('asset file.fd: ', file.fd);
  597. console.info('asset file.path: ', file.path);
  598. item = new VideoItem(file.name,uri,uri,type,0,'')
  599. await fs.stat(file.fd).then(async (stat: fs.Stat) => {
  600. console.info("get file info succeed, the size of file is " + stat.size);
  601. let videoSize = stat.size
  602. // let videoTime = stat.ctime
  603. let fileSize = Utility.formatFSize(videoSize)
  604. //按照添加时间
  605. let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
  606. // console.info('onecold asset addTime: ', addTime);
  607. // let cTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
  608. // console.info('asset stat.ctime: ', stat.ctime);
  609. let musicName:string | undefined = file.name
  610. let artist:string | undefined = ''
  611. let album:string | undefined = ''
  612. let pixelMap:image.PixelMap|undefined|null = undefined
  613. let imagePath = ''
  614. let duration:string | undefined = ''
  615. let mimeType:string | undefined = ''
  616. let trackCount:string | undefined = ''//轨道数量
  617. let sampleRate:string | undefined = ''//音频的采样率单位为Hz
  618. if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
  619. try {
  620. // 创建AVMetadataExtractor对象
  621. const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
  622. // 设置fdSrc
  623. const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
  624. avMetadataExtractor.fdSrc = fd;
  625. // 获取元数据(promise模式)
  626. const metadata = await avMetadataExtractor.fetchMetadata();
  627. if(StrUtil.isNotEmpty(metadata.title)){
  628. musicName = metadata.title
  629. }else{
  630. //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
  631. console.log(`onecold musicName为空:${file.name}`);
  632. const musicData = parseMusicFileName(file.name);
  633. if (musicData.isValid) {
  634. // console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
  635. // console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
  636. musicName = musicData.title
  637. if(artist==''||artist==undefined)
  638. artist = musicData.artist
  639. } else {
  640. musicName = file.name
  641. // console.log("onecold 文件名格式不符合要求");
  642. }
  643. }
  644. if(StrUtil.isNotEmpty(metadata.artist)){
  645. artist = metadata.artist
  646. }
  647. if(artist==undefined)
  648. artist = ''
  649. if(StrUtil.isNotEmpty(metadata.album)){
  650. album = metadata.album
  651. }else{
  652. album = ''
  653. }
  654. if(StrUtil.isNotEmpty(metadata.duration)){
  655. if(metadata.duration)
  656. duration = convertSecondsToTime(metadata.duration.toString())
  657. }
  658. if(StrUtil.isNotEmpty(metadata.mimeType)){
  659. mimeType = metadata.mimeType
  660. }
  661. if(StrUtil.isNotEmpty(metadata.trackCount)){
  662. trackCount = metadata.trackCount
  663. }
  664. if(StrUtil.isNotEmpty(metadata.sampleRate)){
  665. sampleRate = metadata.sampleRate
  666. }
  667. let name = await MD5.digestSync(uri)
  668. if(isLoadPixelMap){
  669. // 获取专辑封面(promise模式)
  670. // pixelMap = await avMetadataExtractor.fetchAlbumCover();
  671. // // 释放资源(promise模式)
  672. // await avMetadataExtractor.release();
  673. pixelMap = await fetchAlbumCover(avMetadataExtractor)
  674. // console.info('onecold release success. name= '+musicName);
  675. if(pixelMap!==undefined&&pixelMap!==null){
  676. // console.info('onecold pixelMap is not empty= '+musicName);
  677. imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,name)
  678. imagePath = fileUri.getUriFromPath(imagePath)
  679. }else{
  680. // console.info('onecold pixelMap is empty= '+musicName);
  681. imagePath = ''
  682. // if(StrUtil.isNotEmpty(artist))
  683. // imagePath = await NetAxiosUtil.getLyricCover(musicName,artist)
  684. }
  685. }else{
  686. imagePath = context.filesDir + FileUtil.separator + name
  687. imagePath = fileUri.getUriFromPath(imagePath)
  688. }
  689. console.info('onecold release success. imagePath= '+imagePath);
  690. } catch (error) {
  691. console.error('Error during metadata extraction:', error);
  692. }
  693. } else {
  694. console.warn('AVMetadataExtractor capability is not supported.');
  695. }
  696. if(musicName==undefined)
  697. musicName = file.name
  698. item = new VideoItem(musicName,uri ,uri,type,videoSize,addTime,undefined,fileSize,
  699. imagePath,artist,album,file.name)
  700. item.duration = duration+'';
  701. item.mimeType = mimeType;
  702. item.trackCount = trackCount;
  703. item.sampleRate = sampleRate;
  704. item.isFav = 0;
  705. item.playCount = 0;
  706. item.pyStr = pinyin4js.getShortPinyin(musicName)
  707. // item.lyricContent = await extractLyricsContent(uri)
  708. let metaItem = await parseAudioMetadata(uri)
  709. if(metaItem){
  710. item.lyricContent = metaItem.lyricContent
  711. item.bit_rate = formatBitrateToKbps(metaItem.bit_rate || "0");
  712. item.year = metaItem.year ||'unknown'
  713. item.probe_score = metaItem.probe_score
  714. item.nb_streams = metaItem.nb_streams
  715. item.nb_programs = metaItem.nb_programs
  716. }
  717. })
  718. } catch (error) {
  719. console.error('uriGetAssetsFromFile failed with err: ' + JSON.stringify(error));
  720. }
  721. return item
  722. }
  723. static async readMetaInfoFFmpeg(context:Context,inputPath: string,type:number): Promise<VideoItem> {
  724. return new Promise((resolve, reject) => {
  725. let commands = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", inputPath];
  726. let outputJson = "";
  727. FFmpeg.execute(commands, {
  728. logCallback: (logLevel: number, logMessage: string) => console.log(`[${logLevel}]${logMessage}`),
  729. outputCallback: (message: string) => {
  730. outputJson += message;
  731. },
  732. }).then(async () => {
  733. try {
  734. let videoItem:VideoItem = new VideoItem('',inputPath,inputPath,type,0,'')
  735. const metadata: FFprobeMetadata = JSON.parse(outputJson);
  736. const format = metadata.format;
  737. let file = fs.openSync(inputPath, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
  738. console.info('readMetaInfoFFmpeg asset file.path: ', file.path);
  739. videoItem = new VideoItem(file.name,inputPath,inputPath,type,0,'')
  740. await fs.stat(file.fd).then(async (stat: fs.Stat) => {
  741. // 获取音频流的采样率
  742. let sampleRate = '';
  743. if (metadata.streams && metadata.streams.length > 0) {
  744. // 查找第一个音频流
  745. const audioStream = metadata.streams.find(stream => StrUtil.isNotEmpty(stream.sample_rate));
  746. if (audioStream&&audioStream.sample_rate) {
  747. sampleRate = audioStream.sample_rate;
  748. }
  749. }
  750. let videoSize = stat.size
  751. let fileSize = Utility.formatFSize(videoSize)
  752. //按照添加时间
  753. let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
  754. // Extract artist and title from tags
  755. const tags = format.tags || {};
  756. let artist = tags.artist ||tags.ARTIST || '';
  757. let title = tags.title ||tags.TITLE || '';
  758. const album = tags.album ||tags.ALBUM|| '';
  759. if(StrUtil.isEmpty(title)){
  760. //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
  761. console.log(`onecold musicName为空:${file.name}`);
  762. const musicData = parseMusicFileName(file.name);
  763. if (musicData.isValid) {
  764. // console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
  765. // console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
  766. title = musicData.title
  767. if(artist==''||artist==undefined)
  768. artist = musicData.artist
  769. } else {
  770. title = file.name
  771. // console.log("onecold 文件名格式不符合要求");
  772. }
  773. }
  774. let name: string = title;
  775. if (!name) {
  776. name = getFileNameWithoutExtension(inputPath);
  777. }
  778. // Create VideoItem
  779. videoItem = new VideoItem(
  780. name,
  781. inputPath, // id can be generated or left empty
  782. inputPath,
  783. type, // assuming it's local
  784. videoSize,
  785. addTime // convert to ISO string
  786. );
  787. // Set additional properties from format metadata
  788. videoItem.artist = artist;
  789. videoItem.album = album;
  790. videoItem.mimeType = format.format_name
  791. videoItem.sampleRate = sampleRate
  792. videoItem.pyStr = pinyin4js.getShortPinyin(name)
  793. videoItem.fileName = FileUtil.getFileName(inputPath);
  794. if(format.duration)
  795. videoItem.duration = formatDuration(format.duration.toString()||'00:00')
  796. videoItem.size = fileSize;
  797. videoItem.bit_rate =formatBitrateToKbps(format.bit_rate || "0");
  798. videoItem.probe_score = format.probe_score;
  799. videoItem.nb_streams = format.nb_streams;
  800. videoItem.nb_programs = format.nb_programs;
  801. videoItem.year = tags.TYER || tags.date ||tags.DATE|| Utility.resourceToString(context, $r('app.string.unknown')); // try different tag names for year
  802. videoItem.lyricContent = tags.LYRICS || tags.lyrics || tags.USLT || tags.UNSYNCEDLYRICS || '';
  803. videoItem.genre = tags.genre||tags.GENRE|| Utility.resourceToString(context, $r('app.string.unknown'));
  804. videoItem.track = tags.track||tags.TRACK|| Utility.resourceToString(context, $r('app.string.unknown'));
  805. // 检查是否有封面图片流
  806. const hasCover = metadata.streams.some(stream =>
  807. stream.disposition?.attached_pic === 1
  808. );
  809. console.info('readMetaInfoFFmpeg asset hasCover: ', hasCover);
  810. // 如果有封面图片,则提取
  811. if (hasCover) {
  812. try {
  813. let md5Name = await MD5.digestSync(inputPath)
  814. let imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}.jpg`;
  815. console.info('readMetaInfoFFmpeg asset imagePath: ', imagePath);
  816. //提取封面
  817. await getFFmpegCover(inputPath, imagePath);
  818. imagePath = fileUri.getUriFromPath(imagePath)
  819. videoItem.pixelMapPath = imagePath;
  820. } catch (error) {
  821. console.warn(' 提取封面图片失败:', error.message);
  822. }
  823. }
  824. console.info('Successfully parsed metadata:', videoItem);
  825. resolve(videoItem);
  826. })
  827. } catch (error) {
  828. console.error('Failed to parse metadata:', error);
  829. reject(new Error('Failed to parse metadata: ' + error.message));
  830. }
  831. }).catch((error: Error) => {
  832. console.error(`Execution failed with error: ${error.message}`);
  833. reject(error);
  834. });
  835. });
  836. }
  837. private completionNum(num: number): string | number {
  838. if (num < 10) {
  839. return '0' + num;
  840. } else {
  841. return num;
  842. }
  843. }
  844. //根据字节获取大小
  845. static formatFSize(bytes:number):string {
  846. const units = ['Bytes', 'K', 'M', 'G'];
  847. let size = bytes;
  848. let unitIndex = 0;
  849. while (size >= 1024 && unitIndex < units.length - 1) {
  850. size /= 1024;
  851. unitIndex++;
  852. }
  853. // 保留两位小数, 四舍五入
  854. size = Math.round(size * 10) / 10;
  855. return size + units[unitIndex]
  856. }
  857. static gotoMarket(context:common.UIAbilityContext,bundleName:string) {
  858. const want: Want = {
  859. uri: `store://appgallery.huawei.com/app/detail?id=${bundleName}`
  860. };
  861. context.startAbility(want).then(()=>{
  862. //拉起成功
  863. }).catch(()=>{
  864. // 拉起失败
  865. });
  866. }
  867. static async doShare(item:VideoItem,context:common.UIAbilityContext){
  868. // 生成视频封面图
  869. const imagePackerApi: image.ImagePacker = image.createImagePacker();
  870. const buffer: ArrayBuffer = await (imagePackerApi.packing(await Utility.getFetchFrameByTime(item.filePath), {
  871. format: 'image/jpeg',
  872. quality: 30
  873. }) as Promise<ArrayBuffer>);
  874. // 构造ShareData,需配置一条有效数据信息
  875. let shareData: systemShare.SharedData = new systemShare.SharedData({
  876. utd: uniformTypeDescriptor.UniformDataType.MEDIA,
  877. uri: fileUri.getUriFromPath(item.filePath),
  878. title: item.name, // 不传title字段时,显示视频文件名
  879. // description: '好听的音乐', // 不传description字段时,显示视频大小
  880. thumbnail: new Uint8Array(buffer), // 优先使用传递的缩略图做预览 不传则默认使用视频第一帧画面做预览图
  881. });
  882. // 进行分享面板显示
  883. let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
  884. // let context = getContext(this) as common.UIAbilityContext;
  885. controller.show(context, {
  886. selectionMode: systemShare.SelectionMode.SINGLE,
  887. previewMode: systemShare.SharePreviewMode.DETAIL,
  888. }).then(() => {
  889. console.info('ShareController show success.');
  890. }).catch((error: BusinessError) => {
  891. console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`);
  892. });
  893. }
  894. //分享索引index的视频
  895. static async doShareMusic(item:VideoItem,context:common.UIAbilityContext){
  896. // 构造ShareData,需配置一条有效数据信息
  897. let shareData: systemShare.SharedData = new systemShare.SharedData({
  898. utd: uniformTypeDescriptor.UniformDataType.AUDIO,
  899. uri: fileUri.getUriFromPath(item.filePath),
  900. title: item.name, // 不传title字段时,显示视频文件名
  901. description: '好听的音乐', // 不传description字段时,显示视频大小
  902. });
  903. // 进行分享面板显示
  904. let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
  905. // let context = getContext(this) as common.UIAbilityContext;
  906. controller.show(context, {
  907. selectionMode: systemShare.SelectionMode.SINGLE,
  908. previewMode: systemShare.SharePreviewMode.DETAIL,
  909. }).then(() => {
  910. console.info('ShareController show success.');
  911. }).catch((error: BusinessError) => {
  912. console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`);
  913. });
  914. }
  915. static getNameList(localList:Array<VideoItem>){
  916. let list: Array<string> = [];
  917. for(let i=0;i<localList.length;i++){
  918. if(StrUtil.isNotEmpty(localList[i].name))
  919. list.push(localList[i].name);
  920. }
  921. return list
  922. }
  923. //获取列表有封面的值
  924. static getFirstCoverFromList(localList:Array<VideoItem>):string{
  925. let list: Array<string> = [];
  926. for(let i=0;i<localList.length;i++){
  927. let pix = localList[i].pixelMapPath
  928. if(pix){
  929. return pix
  930. }
  931. }
  932. return ''
  933. }
  934. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_Dir:私密视频)
  935. static getGlobalNameList(localList:Array<VideoItem>,type:number){
  936. let list: Array<string> = [];
  937. for(let i=0;i<localList.length;i++){
  938. if(localList[i].type === type){
  939. list.push(localList[i].name);
  940. }
  941. }
  942. return list
  943. }
  944. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_LOCK:私密视频)
  945. static getGlobalList(localList:Array<VideoItem>,type:number){
  946. let list: Array<VideoItem> = [];
  947. for(let i=0;i<localList.length;i++){
  948. if(localList[i].type === type){
  949. list.push(localList[i]);
  950. }
  951. }
  952. return list
  953. }
  954. //根据filePath获取当前索引index
  955. static getCurIndexFromGlobalList(localList:Array<VideoItem>,filePath:string){
  956. let index: number = 0;
  957. for(let i=0;i<localList.length;i++){
  958. if(localList[i].filePath === filePath){
  959. index = i
  960. return index
  961. }
  962. }
  963. return index
  964. }
  965. //新的查询是否收藏的方法
  966. static getIsFav(favList: Array<VideoItem>,item:VideoItem|undefined){
  967. if(ArrayUtil.isEmpty(favList)){
  968. return false
  969. }
  970. if(item===undefined){
  971. return false
  972. }
  973. for(let i=0;i<favList.length;i++){
  974. if(favList[i].filePath === item.filePath&&favList[i].isFav===1){
  975. return true
  976. }
  977. }
  978. return false
  979. }
  980. static getIsFac(facList: Array<String>,item:VideoItem|undefined){
  981. if(ArrayUtil.isEmpty(facList)){
  982. return false
  983. }
  984. if(item===undefined){
  985. return false
  986. }
  987. for(let i=0;i<facList.length;i++){
  988. if(facList[i] === item.name||facList[i]===item.fileName){
  989. return true
  990. }
  991. }
  992. return false
  993. }
  994. static resourceToString(context:Context,resource:Resource){
  995. if(!context||!resource)
  996. return ''
  997. return context.resourceManager.getStringSync(resource).toString()
  998. }
  999. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_LOCK:私密视频)
  1000. static getGlobalMusicList(localList:Array<VideoItem>,type:number){
  1001. let list: Array<VideoItem> = [];
  1002. for(let i=0;i<localList.length;i++){
  1003. if(localList[i].type === type){
  1004. list.push(localList[i]);
  1005. }
  1006. }
  1007. return list
  1008. }
  1009. static isHaoFreeTime():boolean{
  1010. if(DateUtil.isWeekend()){
  1011. return true
  1012. }
  1013. let str = DateUtil.getFormatDateStr(new Date(), 'HH')
  1014. let a = Number(str);
  1015. if(isNaN(a))
  1016. return false
  1017. // ToastUtil.showToast('a='+a)
  1018. if (a >= 8 && a <13) {//白天
  1019. return false;
  1020. }
  1021. if (a >= 13 && a <20) {//白天
  1022. return false;
  1023. }
  1024. if (a >= 0 && a < 8) {//凌晨
  1025. return true;
  1026. }
  1027. if (a >= 20 && a <= 24) {//晚上
  1028. return true;
  1029. }
  1030. return false
  1031. }
  1032. static getMusisBg():Resource {
  1033. let index = RandomUtil.randomNumber(0,9)
  1034. LogUtil.info('getMusisBg index = '+index)
  1035. return CommonConstants.musicBgList[index]
  1036. }
  1037. static getMusisBg2(index:number):Resource {
  1038. const adjustedIndex = index % 10;
  1039. LogUtil.info('getMusisBg adjustedIndex = '+adjustedIndex)
  1040. return CommonConstants.musicBgList[adjustedIndex]
  1041. }
  1042. static getParentDirectory(filePath: string): string {
  1043. // 使用正则表达式匹配路径分隔符(支持 Unix 和 Windows)
  1044. const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
  1045. if (lastSlashIndex === -1) {
  1046. return '';
  1047. }
  1048. return filePath.substring(0, lastSlashIndex);
  1049. }
  1050. static getFileDirName(filePath: string,rootPath:string): string{
  1051. if(filePath===rootPath){
  1052. return '首页'
  1053. }
  1054. let result = FileUtil.getFileName(filePath)
  1055. if(StrUtil.isEmpty(result))
  1056. return ''
  1057. if(result.startsWith('.'))
  1058. result = result.replace(/\./g, '')
  1059. return result
  1060. }
  1061. // 开启沉浸式显示模式
  1062. static async enableFullScreen() {
  1063. const ctx = getContext()
  1064. const win = await window.getLastWindow(ctx)
  1065. win.setWindowLayoutFullScreen(true)
  1066. const top = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
  1067. AppStorage.setOrCreate('topHeight', px2vp(top.topRect.height))
  1068. AppStorage.setOrCreate('bottomHeight', px2vp(top.bottomRect.height))
  1069. }
  1070. // 关闭沉浸式显示模式
  1071. static async disableFullScreen() {
  1072. const ctx = getContext()
  1073. const win = await window.getLastWindow(ctx)
  1074. win.setWindowLayoutFullScreen(false)
  1075. AppStorage.setOrCreate('topHeight', 0)
  1076. AppStorage.setOrCreate('bottomHeight', 0)
  1077. }
  1078. // 设置状态栏文字颜色为白色
  1079. static async setStatusBarLight() {
  1080. const ctx = getContext()
  1081. const win = await window.getLastWindow(ctx)
  1082. win.setWindowSystemBarProperties({
  1083. statusBarContentColor: '#ffffff'
  1084. })
  1085. }
  1086. // 设置状态栏文字颜色为黑色
  1087. static async setStatusBarDark() {
  1088. const ctx = getContext()
  1089. const win = await window.getLastWindow(ctx)
  1090. win.setWindowSystemBarProperties({
  1091. statusBarContentColor: '#000000'
  1092. })
  1093. }
  1094. // 优化点:完全基于Promise链的异步处理
  1095. static async getAppName(context: Context): Promise<string> {
  1096. try {
  1097. // 1. 同步化BundleInfo获取
  1098. const bundleInfo = await bundleManager.getBundleInfoForSelf(
  1099. bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
  1100. );
  1101. // 2. 安全获取labelId(新增空值校验)
  1102. const labelId = bundleInfo.appInfo?.labelId;
  1103. if (!labelId || labelId <= 0) {
  1104. throw new Error("Invalid labelId: " + labelId);
  1105. }
  1106. // 3. 异步资源解析(替换危险的getStringSync)
  1107. return await context.resourceManager.getStringValue(labelId);
  1108. } catch (err) {
  1109. console.error(`[${new Date().toISOString()}] AppName Error: CODE=${err.code}, MSG=${err.message}`);
  1110. // 4. 多级降级策略
  1111. return AppUtil.getBundleName() // 最终兜底
  1112. }
  1113. }
  1114. //按名称升序
  1115. static doSortListAscending( list:Array<VideoItem>){
  1116. let options: Intl.CollatorOptions = {
  1117. localeMatcher: "lookup",
  1118. usage: "sort",
  1119. sensitivity: "case" // 区分大小写
  1120. };
  1121. const collator = new Intl.Collator("zh-CN", options);
  1122. list.sort((a, b) => {
  1123. const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type);
  1124. if (typeOrder !== 0) {
  1125. return typeOrder;
  1126. }
  1127. const partsA = extractParts(a.name);
  1128. const partsB = extractParts(b.name);
  1129. const nonNumericComparison = collator.compare(partsA.nonNumeric, partsB.nonNumeric);
  1130. if (nonNumericComparison !== 0) {
  1131. return nonNumericComparison;
  1132. }
  1133. return partsA.numeric - partsB.numeric;
  1134. });
  1135. }
  1136. //按名称降序
  1137. static doSortListDescending(list:Array<VideoItem>){
  1138. const options: Intl.CollatorOptions = {
  1139. localeMatcher: "lookup",
  1140. usage: "sort",
  1141. sensitivity: "case" // 区分大小写
  1142. };
  1143. const collator = new Intl.Collator("zh-CN", options);
  1144. list.sort((a, b) => {
  1145. const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type);
  1146. if (typeOrder !== 0) {
  1147. return typeOrder;
  1148. }
  1149. const partsA = extractParts(a.name);
  1150. const partsB = extractParts(b.name);
  1151. const nonNumericComparison = collator.compare(partsB.nonNumeric, partsA.nonNumeric);
  1152. if (nonNumericComparison !== 0) {
  1153. return nonNumericComparison;
  1154. }
  1155. return partsB.numeric - partsA.numeric;
  1156. });
  1157. }
  1158. /**
  1159. * 读取本地会员到期时间(解密.vv文件)
  1160. * @returns
  1161. */
  1162. static async getLocalNobleExpireDate(): Promise<string> {
  1163. // return "2025-08-08 12:00";
  1164. try {
  1165. const documentViewPicker = new picker.DocumentViewPicker()
  1166. let documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD })
  1167. let download_path = new fileUri.FileUri(documentSaveResult[0]).path + '/'
  1168. let filePath = download_path + LocalMusic.STR_LOCK_VIDEO+ '/' + VipPage.VIP_FILEPATH
  1169. let expireDate = Utility.readDataFromFile(filePath)
  1170. return expireDate;
  1171. } catch (e) {
  1172. return '';
  1173. }
  1174. }
  1175. static readDataFromFile(filePath: string) {
  1176. if(!FileUtil.accessSync(filePath)){
  1177. return ''
  1178. }
  1179. let readTextOptions: ReadTextOptions = {
  1180. offset: 0,
  1181. length: 0,
  1182. encoding: 'utf-8'
  1183. };
  1184. let stat = fileIo.statSync(filePath);
  1185. readTextOptions.length = stat.size;
  1186. let str = fileIo.readTextSync(filePath, readTextOptions);
  1187. const parsedData:VipData = JSON.parse(str) as VipData;
  1188. // LogUtil.info('onecold parsedData.expireDate = ' + parsedData.expireDate)
  1189. let result = Base64Util.decodeSync( parsedData.expireDate)
  1190. // LogUtil.info('onecold 解密的result = ' +StrUtil.unit8ArrayToStr(result))
  1191. return StrUtil.unit8ArrayToStr(result);
  1192. }
  1193. }
  1194. interface PathStat {
  1195. path: string;
  1196. isDirectory: boolean;
  1197. }
  1198. async function isDirectory(filePath: string): Promise<boolean> {
  1199. try {
  1200. const stat = await fs.stat(filePath);
  1201. return stat.isDirectory();
  1202. } catch (error) {
  1203. console.error('Error getting file stat:', error);
  1204. return false;
  1205. }
  1206. }
  1207. //排序模式参数
  1208. interface VideoNameParts {
  1209. nonNumeric: string;
  1210. numeric: number;
  1211. }
  1212. // 提取文件名中的非数字和数字部分
  1213. function extractParts(name: string): VideoNameParts {
  1214. // Adjust the regex to handle names that start with numbers
  1215. const match = name.match(/^(\D*?)(\d+)(\.\w+)?$/);
  1216. if (match) {
  1217. return {
  1218. nonNumeric: match[1] || '', // Ensure nonNumeric is not undefined
  1219. numeric: parseInt(match[2], 10),
  1220. };
  1221. }
  1222. return {
  1223. nonNumeric: name,
  1224. numeric: 0,
  1225. };
  1226. }
  1227. function getInstallTime(): number | null {
  1228. // 从本地存储中获取安装时间
  1229. return PreferencesUtil.getNumberSync('installTime')
  1230. }
  1231. function setInstallTime(time: number) {
  1232. // 将安装时间存储到本地存储中
  1233. PreferencesUtil.putSync('installTime',time)
  1234. }
  1235. function fetchAlbumCover(avMetadataExtractor: media.AVMetadataExtractor): Promise<image.PixelMap | null> {
  1236. return new Promise((resolve, reject) => {
  1237. avMetadataExtractor.fetchAlbumCover((error: BusinessError, pixelMap: image.PixelMap) => {
  1238. if (error) {
  1239. console.error(`Failed to fetch AlbumCover, error = ${JSON.stringify(error)}`);
  1240. resolve(null);
  1241. } else {
  1242. resolve(pixelMap);
  1243. }
  1244. avMetadataExtractor.release();
  1245. });
  1246. });
  1247. }
  1248. //排序类型
  1249. function getTypeOrder(type: number) {
  1250. switch (type) {
  1251. case CommonConstants.TYPE_IS_DIR:
  1252. return 1; // First
  1253. case CommonConstants.TYPE_IS_CSJAD:
  1254. return 2; // Middle
  1255. case CommonConstants.TYPE_LOCAL:
  1256. return 3; // Last
  1257. default:
  1258. return 4; // Unknown types, if any, go last
  1259. }
  1260. }
  1261. function convertSecondsToTime(secondsStr: string): string {
  1262. if (!secondsStr || isNaN(Number(secondsStr))) {
  1263. return "00:00";
  1264. }
  1265. let seconds = parseInt(secondsStr, 10);
  1266. seconds = Math.floor(seconds / 1000);
  1267. const hours = Math.floor(seconds / 3600);
  1268. const minutes = Math.floor((seconds % 3600) / 60);
  1269. const secs = seconds % 60;
  1270. const formatNumber = (num: number) => num.toString().padStart(2, '0');
  1271. if (hours > 0) {
  1272. return `${formatNumber(hours)}:${formatNumber(minutes)}:${formatNumber(secs)}`;
  1273. } else {
  1274. return `${formatNumber(minutes)}:${formatNumber(secs)}`;
  1275. }
  1276. }
  1277. /**
  1278. * 秒数 → 智能时间格式(自动选择 MM:SS 或 HH:MM:SS)
  1279. * @param seconds 秒数字符串(如 "283.524351" 或 "3675")
  1280. * @param forceHHMMSS 强制使用 HH:MM:SS 格式(默认自动判断)
  1281. * @returns 格式化后的时间字符串
  1282. */
  1283. function formatDuration(seconds: string, forceHHMMSS: boolean = false): string {
  1284. // 1. 校验输入
  1285. const secNum = parseFloat(seconds);
  1286. if (isNaN(secNum) || secNum < 0) return forceHHMMSS ? "00:00:00" : "00:00";
  1287. // 2. 计算时间分量
  1288. const totalSec = Math.floor(secNum);
  1289. const hours = Math.floor(totalSec / 3600);
  1290. const mins = Math.floor((totalSec % 3600) / 60);
  1291. const secs = totalSec % 60;
  1292. // 3. 格式化输出
  1293. const pad = (n: number) => n.toString().padStart(2, '0');
  1294. return forceHHMMSS || hours > 0
  1295. ? `${pad(hours)}:${pad(mins)}:${pad(secs)}` // HH:MM:SS
  1296. : `${pad(mins)}:${pad(secs)}`; // MM:SS
  1297. }
  1298. // 定义解析结果的数据结构
  1299. class MusicInfo {
  1300. artist: string = ""; // 艺术家名称
  1301. title: string = ""; // 歌曲名称
  1302. isValid: boolean = false; // 格式是否有效
  1303. }
  1304. /**
  1305. * 解析音乐文件名
  1306. * @param fileName - 待解析的文件名(需包含扩展名)
  1307. * @returns 包含解析结果的MusicInfo对象
  1308. */
  1309. /**
  1310. * 智能解析音乐文件名(支持多种分隔符和前缀序号)
  1311. * @param fileName - 待解析的完整文件名
  1312. * @returns 结构化音乐信息
  1313. */
  1314. function parseMusicFileName(fileName: string): MusicInfo {
  1315. const result = new MusicInfo();
  1316. // 1. 预处理:移除首尾空格(保留中间空格)
  1317. const cleanName = fileName.trim();
  1318. // 2. 提取文件扩展名(以最后一个点分隔)
  1319. const lastDotIndex = cleanName.lastIndexOf('.');
  1320. if (lastDotIndex < 0) return result; // 无扩展名
  1321. const baseName = cleanName.substring(0, lastDotIndex).trim();
  1322. const extension = cleanName.substring(lastDotIndex + 1);
  1323. // 3. 支持多种分隔符(中英文短横线)
  1324. const separators = ['-', '-', '—']; // 半角/全角短横线
  1325. let dashIndex = -1;
  1326. // 查找最后一个有效分隔符位置
  1327. for (const sep of separators) {
  1328. const index = baseName.lastIndexOf(sep);
  1329. if (index > dashIndex) dashIndex = index;
  1330. }
  1331. // 4. 核心解析逻辑
  1332. if (dashIndex > 0 && dashIndex < baseName.length - 1) {
  1333. let artistPart = baseName.substring(0, dashIndex).trim();
  1334. result.title = baseName.substring(dashIndex + 1).trim();
  1335. // 5. 处理前缀序号(如"04 - ")
  1336. const numPrefixRegex = /^\d+\s*[--—]\s*/; // 匹配数字+分隔符组合
  1337. artistPart = artistPart.replace(numPrefixRegex, '').trim();
  1338. // 6. 最终有效性验证
  1339. result.artist = artistPart;
  1340. result.isValid = (result.artist.length > 0 && result.title.length > 0);
  1341. }
  1342. return result;
  1343. }
  1344. function getFileNameWithoutExtension(filePath: string): string {
  1345. const fileName = filePath.split('/').pop() || '';
  1346. const lastDotIndex = fileName.lastIndexOf('.');
  1347. return lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName;
  1348. }
  1349. /**
  1350. * 从视频文件中提取封面
  1351. * @param inputPath 音乐文件路径
  1352. * @returns Promise<void>
  1353. */
  1354. async function extractCoverImage(inputPath: string, outputPath: string): Promise<void> {
  1355. const commands = [
  1356. 'ffmpeg',
  1357. '-i', inputPath,
  1358. '-an', // 禁用音频
  1359. '-vcodec', 'copy', // 直接复制视频流
  1360. '-f', 'image2', // 强制输出为图片
  1361. '-y', // 覆盖输出文件
  1362. outputPath
  1363. ];
  1364. return new Promise((resolve, reject) => {
  1365. FFmpeg.execute(commands, {
  1366. logCallback: (logLevel: number, logMessage: string) => {
  1367. console.log(`[${logLevel}]${logMessage}`);
  1368. },
  1369. outputCallback: (message: string) => {
  1370. console.log(`FFmpeg output: ${message}`);
  1371. },
  1372. }).then(() => resolve())
  1373. .catch((error: BusinessError) => reject(error));
  1374. });
  1375. }
  1376. /**
  1377. * 从音乐文件中提取封面
  1378. * @param inputPath 音乐文件路径
  1379. * @returns Promise<void>
  1380. */
  1381. async function getFFmpegCover(inputPath: string, outputPath: string) {
  1382. let commands = ["ffmpeg", "-y","-i", inputPath, "-map", "0:v", "-c:v", "copy", outputPath];
  1383. FFmpeg.execute(commands, {
  1384. logCallback: (logLevel: number, logMessage: string) => {
  1385. console.info(`[FFmpeg LOG] [${logLevel}]${logMessage}`)
  1386. },
  1387. progressCallback: (message: string) => {
  1388. console.info(`[FFmpeg progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
  1389. },
  1390. })
  1391. .then(() => {
  1392. console.info("FFmpeg execution succeeded.");
  1393. })
  1394. .catch((error: Error) => {
  1395. console.error(`FFmpeg execution failed with error: ${error.message}`);
  1396. });
  1397. }
  1398. /**
  1399. * 从音乐文件中提取歌词内容
  1400. * @param inputPath 音乐文件路径
  1401. * @returns Promise<string> 直接返回歌词内容,若无歌词则返回空字符串
  1402. */
  1403. async function extractLyricsContent(inputPath: string): Promise<string> {
  1404. return new Promise(async (resolve, reject) => {
  1405. try {
  1406. // 1. 使用ffprobe获取元数据
  1407. const commands = [
  1408. 'ffprobe',
  1409. '-v', 'quiet',
  1410. '-print_format', 'json',
  1411. '-show_format',
  1412. inputPath
  1413. ];
  1414. let outputJson = '';
  1415. await FFmpeg.execute(commands, {
  1416. outputCallback: (message: string) => outputJson += message,
  1417. });
  1418. // 2. 解析歌词标签
  1419. const metadata:FFprobeMetadata = JSON.parse(outputJson);
  1420. const tags = metadata.format?.tags || {};
  1421. // 3. 从常见标签中查找歌词(优先级顺序)
  1422. const lyricContent =
  1423. tags.LYRICS || // 标准标签
  1424. tags.lyrics || // 小写变体
  1425. tags.USLT || // ID3v2标签
  1426. tags.UNSYNCEDLYRICS ||
  1427. '';
  1428. resolve(lyricContent.trim());
  1429. } catch (error) {
  1430. reject(`解析失败: ${error instanceof Error ? error.message : String(error)}`);
  1431. }
  1432. });
  1433. }
  1434. /**
  1435. * 解析音乐文件元数据(包含歌词和其他关键字段)
  1436. * @param inputPath 文件路径
  1437. * @returns Promise<VideoItem> 包含完整元数据的对象
  1438. */
  1439. async function parseAudioMetadata(inputPath: string): Promise<VideoItem> {
  1440. return new Promise(async (resolve, reject) => {
  1441. try {
  1442. // 1. 执行FFprobe命令
  1443. const commands: string[] = [
  1444. 'ffprobe',
  1445. '-v', 'quiet',
  1446. '-print_format', 'json',
  1447. '-show_format',
  1448. '-show_streams',
  1449. inputPath
  1450. ];
  1451. let outputJson = '';
  1452. await FFmpeg.execute(commands, {
  1453. outputCallback: (message: string) => outputJson += message,
  1454. });
  1455. // 2. 解析元数据
  1456. const metadata = JSON.parse(outputJson) as FFprobeMetadata;
  1457. const format:FFprobeFormat = metadata.format ;
  1458. const tags = format.tags||{} ;
  1459. // 3. 构建VideoItem基础信息
  1460. const videoItem = new VideoItem(
  1461. tags.title || getFileNameWithoutExtension(inputPath),
  1462. inputPath, // id
  1463. inputPath,
  1464. CommonConstants.TYPE_LOCAL,
  1465. 0,
  1466. new Date().toISOString() // 使用当前时间作为默认创建时间
  1467. );
  1468. // 4. 设置关键字段
  1469. videoItem.bit_rate = format.bit_rate || '';
  1470. videoItem.probe_score = format.probe_score || 0;
  1471. videoItem.year = tags.TYER || tags.date || '';
  1472. videoItem.lyricContent =
  1473. tags.LYRICS ??
  1474. tags.lyrics ??
  1475. tags.USLT ??
  1476. tags.UNSYNCEDLYRICS ??
  1477. '';
  1478. // 5. 设置其他可选字段
  1479. videoItem.artist = tags.artist || '';
  1480. videoItem.album = tags.album || '';
  1481. videoItem.duration = format.duration || '';
  1482. videoItem.nb_streams = format.nb_streams;
  1483. videoItem.nb_programs = format.nb_programs;
  1484. resolve(videoItem);
  1485. } catch (error) {
  1486. reject(new Error(`元数据解析失败: ${error instanceof Error ? error.message : String(error)}`));
  1487. }
  1488. });
  1489. }
  1490. /**
  1491. * 将比特率(bps)转换为 kbps 并格式化
  1492. * @param bitRate 比特率字符串(如 "5644802")
  1493. * @param decimalPlaces 保留小数位数(默认0)
  1494. * @returns 格式化后的 kbps 字符串(如 "5644 kbps")
  1495. */
  1496. function formatBitrateToKbps(bitRate: string, decimalPlaces: number = 0): string {
  1497. // 1. 转换为数字
  1498. const bitsPerSecond = parseInt(bitRate);
  1499. if (isNaN(bitsPerSecond) || bitsPerSecond < 0) return "0 kbps";
  1500. // 2. 计算 kbps(1 kbps = 1000 bps)
  1501. const kbps = bitsPerSecond / 1000;
  1502. // 3. 格式化输出
  1503. return `${kbps.toFixed(decimalPlaces)} kbps`;
  1504. }