Utility.ets 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347
  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. export class Utility {
  28. private constructor() {}
  29. static isOpenTime():boolean{
  30. const currentDate = new Date();
  31. const targetDate = new Date(CommonConstants.OPEN_DATE);
  32. if (currentDate > targetDate) {
  33. return true
  34. }
  35. return false
  36. }
  37. static readonly FOREVER_DATE: string = 'forever';
  38. /**
  39. * 是否会员(只看线上会员状态)
  40. */
  41. static isNoble(): boolean {
  42. // 只判断线上会员状态
  43. return PreferencesUtil.getBooleanSync('hasActiveSubscription', false);
  44. }
  45. /**
  46. * 旧版客户端判断会员状态
  47. * @returns
  48. */
  49. static isNobleForOld():boolean{
  50. if(!Utility.isOpenTime())//提交审核的审核,刚刚开始可以全部是赞助用户。
  51. return true
  52. return PreferencesUtil.getBooleanSync('isNoble',false)
  53. }
  54. /**
  55. * setNoble方法废弃,保留兼容但不做任何操作
  56. */
  57. static setNoble(expireDate:string){
  58. let isNoble = false
  59. if(StrUtil.isNotEmpty(expireDate)){
  60. if(expireDate===VipPage.FOREVER_DATE){
  61. isNoble = true
  62. }else{
  63. const currentDate = new Date();
  64. const targetDate = DateUtil.getFormatDate(expireDate)
  65. if (currentDate > targetDate) {
  66. isNoble = false
  67. }else{
  68. isNoble = true
  69. }
  70. }
  71. }
  72. PreferencesUtil.putSync('isNoble',isNoble)
  73. }
  74. /**
  75. * 判断用户安装app是否超过2天,超过2天(2800分钟)才展示广告 显示广告 24*60=1440 1440*2=2800
  76. * @param day
  77. * @returns
  78. */
  79. static isPassInstallTime(day:number):boolean{
  80. //获取当前时间
  81. let currentTime = new Date().getTime();
  82. // 尝试从存储中获取安装时间
  83. let installTime = getInstallTime();
  84. if (installTime===0||installTime === null|| installTime===undefined) {
  85. // 如果没有存储安装时间,则存储当前时间为安装时间
  86. setInstallTime(currentTime);
  87. return false
  88. } else {
  89. // 计算时间差(以分钟为单位)
  90. const timeDifference = (currentTime - installTime) / (1000 * 60);
  91. LogUtil.debug("onecold timeDifference=" + timeDifference)
  92. // 如果超过5分钟,显示广告 24*60=1440 1440*2=2800
  93. if (timeDifference > 24*60*day) {
  94. return true
  95. }else{
  96. return false
  97. }
  98. }
  99. }
  100. // 定义一个函数来获取当前的时间段
  101. static getTimePeriod(): string {
  102. const now = new Date();
  103. const hours = now.getHours();
  104. if (hours >= 5 && hours < 12) {
  105. return "早上好";
  106. } else if (hours >= 12 && hours < 18) {
  107. return "下午好";
  108. } else if (hours >= 18 && hours < 22) {
  109. return "晚上好";
  110. } else {
  111. return "夜深了";
  112. }
  113. }
  114. static optimizedFormat(speed: number): string {
  115. if (speed) {
  116. const str = speed.toFixed(2);
  117. let end = str.length;
  118. while (end > 0 && (str[end - 1] === '0' || str[end - 1] === '.')) {
  119. end--;
  120. if (str[end] === '.') {
  121. break;
  122. }
  123. }
  124. return str.slice(0, end || 1) + 'x';
  125. } else {
  126. return '1x'
  127. }
  128. }
  129. static isHaoOpenTime():boolean{
  130. const currentDate = new Date();
  131. const targetDate = new Date('2025-04-20');
  132. if (currentDate < targetDate) {
  133. return true
  134. }
  135. return false
  136. }
  137. static addDays(date: Date, days: number): Date {
  138. const result = new Date(date);
  139. result.setDate(result.getDate() + days);
  140. return result;
  141. }
  142. static convertToKHz(sampleRateHz: string|undefined): string {
  143. if(StrUtil.isEmpty(sampleRateHz)||sampleRateHz ===undefined)
  144. return '0KHz'
  145. const sampleRateKHz = Number(sampleRateHz) / 1000;
  146. return `${sampleRateKHz} KHz`;
  147. }
  148. /**
  149. * 格式化媒体格式类型显示
  150. * @param mimeType 媒体格式类型字符串
  151. * @return 格式化后的显示字符串
  152. */
  153. static formatMimeType(mimeType: string | undefined): string {
  154. if (StrUtil.isEmpty(mimeType) || mimeType === undefined) {
  155. return 'unknown';
  156. }
  157. // 从MIME类型中提取格式部分,例如 "audio/mp3" -> "MP3"
  158. try {
  159. const parts = mimeType.split('/');
  160. if (parts.length > 1) {
  161. return parts[1].toUpperCase();
  162. } else {
  163. return mimeType.toUpperCase();
  164. }
  165. } catch (err) {
  166. console.error(`格式化媒体类型出错: ${err}`);
  167. return 'unknown';
  168. }
  169. }
  170. //根据字节获取大小
  171. static formatFileSize(bytes:number) {
  172. const units = ['Bytes', 'Kbps', 'Mbps'];
  173. let size = bytes;
  174. let unitIndex = 0;
  175. while (size >= 1024 && unitIndex < units.length - 1) {
  176. size /= 1024;
  177. unitIndex++;
  178. }
  179. if(size > 1024&&unitIndex==2){
  180. size = size*0.1
  181. }
  182. if(size > 1024&&unitIndex==2){
  183. size = size*0.1
  184. }
  185. if(size > 480&&unitIndex==2){
  186. size = size*0.5
  187. }
  188. // 保留两位小数, 四舍五入
  189. size = Math.round(size * 10) / 10;
  190. return size + units[unitIndex]
  191. }
  192. //根据字节获取大小无单位
  193. static formatFileSizeWithout(bytes:number) {
  194. const units = ['Bytes', 'Kbps', 'Mbps'];
  195. let size = bytes;
  196. let unitIndex = 0;
  197. while (size >= 1024 && unitIndex < units.length - 1) {
  198. size /= 1024;
  199. unitIndex++;
  200. }
  201. if(size > 1024&&unitIndex==2){
  202. size = size*0.1
  203. }
  204. if(size > 1024&&unitIndex==2){
  205. size = size*0.1
  206. }
  207. if(size > 480&&unitIndex==2){
  208. size = size*0.5
  209. }
  210. // 保留两位小数, 四舍五入
  211. size = Math.round(size * 10) / 10;
  212. return size
  213. }
  214. static copyText(text:string):boolean{
  215. const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN,text)
  216. const systemPasteboard = pasteboard.getSystemPasteboard()
  217. systemPasteboard.setData(pasteboardData)
  218. systemPasteboard.getData().then((data)=>{
  219. return data
  220. })
  221. return false
  222. }
  223. static getDbm(rssi:number):number{
  224. return Math.round((rssi / 2) + 100)
  225. }
  226. //根据数字形式的IP地址获取字符串形式的IP地址
  227. static getIpAddrFromNum(ipNum: number): string {
  228. return (ipNum >>> 24) + '.' + (ipNum >> 16 & 0xFF) + '.' + (ipNum >> 8 & 0xFF) + '.' + (ipNum & 0xFF);
  229. }
  230. static resolveIP(ip:number) {
  231. let address: string = ip.toString()
  232. if (address === '0') {
  233. return '00:00:000:000'
  234. }
  235. address.substring(0, 2)
  236. return `${address.substring(0, 2)}:${address.substring(2, 4)}:${address.substring(4, 7)}:${address.substring(7, 10)}`
  237. }
  238. static isFreeTime():boolean{
  239. if(DateUtil.isWeekend()){
  240. return true
  241. }
  242. let str = DateUtil.getFormatDateStr(new Date(), 'HH')
  243. let a = Number(str);
  244. if(isNaN(a))
  245. return false
  246. // ToastUtil.showToast('a='+a)
  247. if (a >= 7 && a <13) {//白天
  248. return false;
  249. }
  250. if (a >= 13 && a <20) {//白天
  251. return false;
  252. }
  253. if (a >= 0 && a < 7) {//凌晨
  254. return true;
  255. }
  256. if (a >= 20 && a <= 24) {//晚上
  257. return true;
  258. }
  259. return false
  260. }
  261. //判断是否是音乐文件
  262. static isMusicByExtension(filename:string) {
  263. const extensions = CommonConstants.REAL_MUSIC_FORMAT
  264. const lastIndex = filename.lastIndexOf('.');
  265. if (lastIndex!== -1) {
  266. const fileExtension = filename.slice(lastIndex).toLowerCase();
  267. return extensions.includes(fileExtension);
  268. }
  269. return false;
  270. }
  271. //判断是否是媒体文件 音乐和视频都可以
  272. static isMeidaByExtension(filename:string) {
  273. const extensions = CommonConstants.MEDIA_FORMAT
  274. const lastIndex = filename.lastIndexOf('.');
  275. if (lastIndex!== -1) {
  276. const fileExtension = filename.slice(lastIndex).toLowerCase();
  277. return extensions.includes(fileExtension);
  278. }
  279. return false;
  280. }
  281. //判断是否是视频文件
  282. static isVideoByExtension(filename:string) {
  283. const extensions = CommonConstants.VIDEO_FORMAT
  284. const lastIndex = filename.lastIndexOf('.');
  285. if (lastIndex!== -1) {
  286. const fileExtension = filename.slice(lastIndex).toLowerCase();
  287. return extensions.includes(fileExtension);
  288. }
  289. return false;
  290. }
  291. // 获取缩略图
  292. static async getFetchFrameByTime(filePath: string) {
  293. if(Utility.isMusicByExtension(filePath)){
  294. return undefined
  295. }
  296. let pixelMap:image.PixelMap|undefined =undefined;
  297. try{
  298. // 创建AVImageGenerator对象
  299. let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()
  300. let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
  301. let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };
  302. avImageGenerator.fdSrc = avFileDescriptor;
  303. // 初始化入参
  304. let timeUs = 0
  305. let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC
  306. let param: media.PixelMapParams = {
  307. width : 300,
  308. height : 400,
  309. }
  310. // 获取缩略图(promise模式)
  311. pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param)
  312. // 释放资源(promise模式)
  313. avImageGenerator.release()
  314. console.info(`release success.`)
  315. fs.closeSync(file);
  316. }catch (error) {
  317. console.error('uriGetAssets failed with err: ' + JSON.stringify(error));
  318. }
  319. return pixelMap
  320. }
  321. // 获取fd文件路径
  322. static async getFdDir(path:string){
  323. let fdPath = 'fd://';
  324. let file = await fileIo.open(path)
  325. fdPath = fdPath + '' + file.fd;
  326. return fdPath
  327. }
  328. // 根据uri获取文件名称
  329. static getMediaNameByUri(myUri: string) {
  330. let myFileName = (myUri.split('/').pop()) as string;
  331. return decodeURIComponent(myFileName)
  332. }
  333. static msToStandardTime(ms:string):string{
  334. let date = new Date(ms);
  335. let hours = date.getHours();
  336. let minutes = date.getMinutes();
  337. let seconds = date.getSeconds();
  338. return hours + ':' + minutes + ':' + seconds;
  339. }
  340. static getTimestampFromDateStr(dateStr: string, format: string = 'yyyy-MM-dd HH:mm'): number {
  341. // 定义正则表达式以匹配日期字符串中的各部分
  342. const regex = format
  343. .replace('yyyy', '(\\d{4})')
  344. .replace('MM', '(\\d{2})')
  345. .replace('dd', '(\\d{2})')
  346. .replace('HH', '(\\d{2})')
  347. .replace('mm', '(\\d{2})');
  348. const match = new RegExp(regex).exec(dateStr);
  349. if (!match) {
  350. throw new Error('Invalid date string or format');
  351. }
  352. // 解析匹配结果
  353. const year = Number(match[1]);
  354. const month = Number(match[2]);
  355. const day = Number(match[3]);
  356. const hours = Number(match[4]);
  357. const minutes = Number(match[5]);
  358. // 创建 Date 对象
  359. const dateObj = new Date(year, month - 1, day, hours, minutes);
  360. // 返回时间戳(以秒为单位)
  361. return Math.floor(dateObj.getTime() / 1000);
  362. }
  363. static getFormatDateStr(date: number | string | Date, format: string = 'yyyy-MM-dd HH:mm:ss'): string {
  364. // 将输入转换为 Date 对象
  365. let dateObj: Date;
  366. if (typeof date === 'number') {
  367. // 如果是十位时间戳,转换为毫秒数
  368. dateObj = new Date(date.toString().length === 10 ? date * 1000 : date);
  369. } else if (typeof date === 'string') {
  370. // 如果是字符串,尝试解析为 Date 对象
  371. dateObj = new Date(date);
  372. } else {
  373. // 如果是 Date 对象,直接使用
  374. dateObj = date;
  375. }
  376. // 定义替换规则
  377. const replacements = new Map<string, string>([
  378. ['yyyy', String(dateObj.getFullYear())],
  379. ['MM', String(dateObj.getMonth() + 1).padStart(2, '0')],
  380. ['dd', String(dateObj.getDate()).padStart(2, '0')],
  381. ['HH', String(dateObj.getHours()).padStart(2, '0')],
  382. ['mm', String(dateObj.getMinutes()).padStart(2, '0')],
  383. ['ss', String(dateObj.getSeconds()).padStart(2, '0')]
  384. ]);
  385. // 替换格式字符串中的占位符
  386. replacements.forEach((value, key) => {
  387. format = format.replace(new RegExp(key, 'g'), value);
  388. });
  389. return format;
  390. }
  391. //获取从文件管理器获得的视频资源的属性值
  392. static async uriGetAssetsFromFile(context:Context,uri:string,type:number,isLoadPixelMap?:boolean): Promise<VideoItem> {
  393. let item:VideoItem = new VideoItem('',uri,uri,type,0,'')
  394. try {
  395. console.info('asset file.uri: ', uri);
  396. let file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
  397. console.info("file.fd " + file.fd);
  398. let fdfd = 'fd://' + file.fd
  399. //3、通过fs.stat方法获取stat对象
  400. console.info('asset file.name: ', file.name);
  401. console.info('asset file.uri: ', uri);
  402. console.info('asset file.fd: ', file.fd);
  403. console.info('asset file.path: ', file.path);
  404. item = new VideoItem(file.name,uri,uri,type,0,'')
  405. await fs.stat(file.fd).then(async (stat: fs.Stat) => {
  406. console.info("get file info succeed, the size of file is " + stat.size);
  407. let videoSize = stat.size
  408. // let videoTime = stat.ctime
  409. let fileSize = Utility.formatFSize(videoSize)
  410. let cTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
  411. // console.info('asset stat.ino: ', stat.ino);
  412. // console.info('asset stat.mode: ', stat.mode);
  413. // console.info('asset stat.uid: ', stat.uid);
  414. // console.info('asset stat.ino: ', stat.gid);
  415. // console.info('asset stat.size: ', stat.size);
  416. console.info('asset stat.ctime: ', stat.ctime);
  417. // console.info('asset stat.mtime: ', stat.mtime);
  418. // console.info('asset stat.duration: ', duration);
  419. let pixelMap:image.PixelMap|undefined = undefined
  420. if(isLoadPixelMap){
  421. //获取缩略图
  422. if(Utility.isVideoByExtension(uri)){
  423. pixelMap = await Utility.getFetchFrameByTime(uri)
  424. }else{
  425. pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri)
  426. }
  427. }
  428. item = new VideoItem( file.name,uri ,uri,type,videoSize,cTime,pixelMap,fileSize,
  429. await ImageUtil.pixelMapToBase64Str(pixelMap))
  430. })
  431. } catch (error) {
  432. console.error('uriGetAssetsFromFile failed with err: ' + JSON.stringify(error));
  433. }
  434. return item
  435. }
  436. static async getFilePixelMapBig(uri:string){
  437. let pixelMap:image.PixelMap|undefined = undefined
  438. if(Utility.isMusicByExtension(uri)){
  439. pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri)
  440. }else{
  441. pixelMap = await Utility.getFetchFrameByTime(uri)
  442. }
  443. return ImageUtil.pixelMapToBase64StrBig(pixelMap)
  444. }
  445. static async getFilePixelMap(uri:string){
  446. let pixelMap:image.PixelMap|undefined = undefined
  447. if(Utility.isMusicByExtension(uri)){
  448. pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri)
  449. }else{
  450. pixelMap = await Utility.getFetchFrameByTime(uri)
  451. }
  452. return ImageUtil.pixelMapToBase64Str(pixelMap)
  453. }
  454. //根据filePathe获取对应的播放Index
  455. static getIndexFromList(localList:Array<VideoItem>,filePath:string){
  456. let list: Array<string> = [];
  457. for(let i=0;i<localList.length;i++){
  458. if(localList[i].filePath === filePath){
  459. return i
  460. }
  461. }
  462. return 0
  463. }
  464. // 在以下demo中,使用资源管理接口获取打包在HAP内的媒体资源文件,通过设置fdSrc属性,获取音频元数据并打印,
  465. // 获取音频专辑封面并通过Image控件显示在屏幕上。该demo以Promise形式进行异步接口调用
  466. static async getFetchMetadataFromFdSrcByPromise(uri: string): Promise<image.PixelMap | undefined> {
  467. let cover: image.PixelMap | undefined = undefined;
  468. if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
  469. try {
  470. // 创建AVMetadataExtractor对象
  471. const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
  472. // 设置fdSrc
  473. const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
  474. avMetadataExtractor.fdSrc = fd;
  475. // 获取元数据(promise模式)
  476. const metadata = await avMetadataExtractor.fetchMetadata();
  477. metadata.author
  478. console.info(`get meta data, hasAudio: ${metadata.hasAudio}`);
  479. // 获取专辑封面(promise模式)
  480. cover = await avMetadataExtractor.fetchAlbumCover();
  481. // 释放资源(promise模式)
  482. await avMetadataExtractor.release();
  483. console.info('release success.');
  484. } catch (error) {
  485. console.error('Error during metadata extraction:', error);
  486. }
  487. } else {
  488. console.warn('AVMetadataExtractor capability is not supported.');
  489. }
  490. return cover;
  491. }
  492. static formatTimestamp(timestamp: number): string {
  493. // 将时间戳转换为 Date 对象
  494. const date = new Date(timestamp);
  495. // 获取各个部分
  496. const year = date.getFullYear();
  497. const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要加1
  498. const day = String(date.getDate()).padStart(2, '0');
  499. const hours = String(date.getHours()).padStart(2, '0');
  500. const minutes = String(date.getMinutes()).padStart(2, '0');
  501. const seconds = String(date.getSeconds()).padStart(2, '0');
  502. // 拼接成所需的格式
  503. return `${year}-${month}-${day} ${hours}:${minutes}`;
  504. }
  505. //获取音乐资源的属性值,
  506. static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number,isLoadPixelMap?:boolean): Promise<VideoItem> {
  507. let item:VideoItem = new VideoItem('',uri,uri,type,0,'')
  508. try {
  509. console.info('asset file.uri: ', uri);
  510. let file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
  511. console.info("file.fd " + file.fd);
  512. let fdfd = 'fd://' + file.fd
  513. //3、通过fs.stat方法获取stat对象
  514. console.info('asset file.name: ', file.name);
  515. console.info('asset file.uri: ', uri);
  516. console.info('asset file.fd: ', file.fd);
  517. console.info('asset file.path: ', file.path);
  518. item = new VideoItem(file.name,uri,uri,type,0,'')
  519. await fs.stat(file.fd).then(async (stat: fs.Stat) => {
  520. console.info("get file info succeed, the size of file is " + stat.size);
  521. let videoSize = stat.size
  522. // let videoTime = stat.ctime
  523. let fileSize = Utility.formatFSize(videoSize)
  524. //按照添加时间
  525. let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
  526. // console.info('onecold asset addTime: ', addTime);
  527. // let cTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
  528. // console.info('asset stat.ctime: ', stat.ctime);
  529. let musicName:string | undefined = file.name
  530. let artist:string | undefined = ''
  531. let album:string | undefined = ''
  532. let pixelMap:image.PixelMap|undefined|null = undefined
  533. let imagePath = ''
  534. let duration:string | undefined = ''
  535. let mimeType:string | undefined = ''
  536. let trackCount:string | undefined = ''//轨道数量
  537. let sampleRate:string | undefined = ''//音频的采样率单位为Hz
  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. if(StrUtil.isNotEmpty(metadata.title)){
  548. musicName = metadata.title
  549. }else{
  550. //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
  551. console.log(`onecold musicName为空:${file.name}`);
  552. const musicData = parseMusicFileName(file.name);
  553. if (musicData.isValid) {
  554. // console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
  555. // console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
  556. musicName = musicData.title
  557. if(artist==''||artist==undefined)
  558. artist = musicData.artist
  559. } else {
  560. musicName = file.name
  561. // console.log("onecold 文件名格式不符合要求");
  562. }
  563. }
  564. if(StrUtil.isNotEmpty(metadata.artist)){
  565. artist = metadata.artist
  566. }
  567. if(artist==undefined)
  568. artist = ''
  569. if(StrUtil.isNotEmpty(metadata.album)){
  570. album = metadata.album
  571. }else{
  572. album = ''
  573. }
  574. if(StrUtil.isNotEmpty(metadata.duration)){
  575. if(metadata.duration)
  576. duration = convertSecondsToTime(metadata.duration.toString())
  577. }
  578. if(StrUtil.isNotEmpty(metadata.mimeType)){
  579. mimeType = metadata.mimeType
  580. }
  581. if(StrUtil.isNotEmpty(metadata.trackCount)){
  582. trackCount = metadata.trackCount
  583. }
  584. if(StrUtil.isNotEmpty(metadata.sampleRate)){
  585. sampleRate = metadata.sampleRate
  586. }
  587. let name = await MD5.digestSync(uri)
  588. if(isLoadPixelMap){
  589. // 获取专辑封面(promise模式)
  590. // pixelMap = await avMetadataExtractor.fetchAlbumCover();
  591. // // 释放资源(promise模式)
  592. // await avMetadataExtractor.release();
  593. pixelMap = await fetchAlbumCover(avMetadataExtractor)
  594. // console.info('onecold release success. name= '+musicName);
  595. if(pixelMap!==undefined&&pixelMap!==null){
  596. // console.info('onecold pixelMap is not empty= '+musicName);
  597. imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,name)
  598. imagePath = fileUri.getUriFromPath(imagePath)
  599. }else{
  600. // console.info('onecold pixelMap is empty= '+musicName);
  601. imagePath = ''
  602. // if(StrUtil.isNotEmpty(artist))
  603. // imagePath = await NetAxiosUtil.getLyricCover(musicName,artist)
  604. }
  605. }else{
  606. imagePath = context.filesDir + FileUtil.separator + name
  607. imagePath = fileUri.getUriFromPath(imagePath)
  608. }
  609. console.info('onecold release success. imagePath= '+imagePath);
  610. } catch (error) {
  611. console.error('Error during metadata extraction:', error);
  612. }
  613. } else {
  614. console.warn('AVMetadataExtractor capability is not supported.');
  615. }
  616. if(musicName==undefined)
  617. musicName = file.name
  618. item = new VideoItem(musicName,uri ,uri,type,videoSize,addTime,undefined,fileSize,
  619. imagePath,artist,album,file.name)
  620. item.duration = duration+'';
  621. item.mimeType = mimeType;
  622. item.trackCount = trackCount;
  623. item.sampleRate = sampleRate;
  624. item.isFav = 0;
  625. item.playCount = 0;
  626. item.pyStr = pinyin4js.getShortPinyin(musicName)
  627. console.info('onecold pyStr = '+pinyin4js.getShortPinyin(musicName));
  628. })
  629. } catch (error) {
  630. console.error('uriGetAssetsFromFile failed with err: ' + JSON.stringify(error));
  631. }
  632. return item
  633. }
  634. private completionNum(num: number): string | number {
  635. if (num < 10) {
  636. return '0' + num;
  637. } else {
  638. return num;
  639. }
  640. }
  641. //根据字节获取大小
  642. static formatFSize(bytes:number):string {
  643. const units = ['Bytes', 'K', 'M', 'G'];
  644. let size = bytes;
  645. let unitIndex = 0;
  646. while (size >= 1024 && unitIndex < units.length - 1) {
  647. size /= 1024;
  648. unitIndex++;
  649. }
  650. // 保留两位小数, 四舍五入
  651. size = Math.round(size * 10) / 10;
  652. return size + units[unitIndex]
  653. }
  654. static gotoMarket(context:common.UIAbilityContext,bundleName:string) {
  655. const want: Want = {
  656. uri: `store://appgallery.huawei.com/app/detail?id=${bundleName}`
  657. };
  658. context.startAbility(want).then(()=>{
  659. //拉起成功
  660. }).catch(()=>{
  661. // 拉起失败
  662. });
  663. }
  664. static async doShare(item:VideoItem,context:common.UIAbilityContext){
  665. // 生成视频封面图
  666. const imagePackerApi: image.ImagePacker = image.createImagePacker();
  667. const buffer: ArrayBuffer = await (imagePackerApi.packing(await Utility.getFetchFrameByTime(item.filePath), {
  668. format: 'image/jpeg',
  669. quality: 30
  670. }) as Promise<ArrayBuffer>);
  671. // 构造ShareData,需配置一条有效数据信息
  672. let shareData: systemShare.SharedData = new systemShare.SharedData({
  673. utd: uniformTypeDescriptor.UniformDataType.MEDIA,
  674. uri: fileUri.getUriFromPath(item.filePath),
  675. title: item.name, // 不传title字段时,显示视频文件名
  676. // description: '好听的音乐', // 不传description字段时,显示视频大小
  677. thumbnail: new Uint8Array(buffer), // 优先使用传递的缩略图做预览 不传则默认使用视频第一帧画面做预览图
  678. });
  679. // 进行分享面板显示
  680. let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
  681. // let context = getContext(this) as common.UIAbilityContext;
  682. controller.show(context, {
  683. selectionMode: systemShare.SelectionMode.SINGLE,
  684. previewMode: systemShare.SharePreviewMode.DETAIL,
  685. }).then(() => {
  686. console.info('ShareController show success.');
  687. }).catch((error: BusinessError) => {
  688. console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`);
  689. });
  690. }
  691. //分享索引index的视频
  692. static async doShareMusic(item:VideoItem,context:common.UIAbilityContext){
  693. // 构造ShareData,需配置一条有效数据信息
  694. let shareData: systemShare.SharedData = new systemShare.SharedData({
  695. utd: uniformTypeDescriptor.UniformDataType.AUDIO,
  696. uri: fileUri.getUriFromPath(item.filePath),
  697. title: item.name, // 不传title字段时,显示视频文件名
  698. description: '好听的音乐', // 不传description字段时,显示视频大小
  699. });
  700. // 进行分享面板显示
  701. let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
  702. // let context = getContext(this) as common.UIAbilityContext;
  703. controller.show(context, {
  704. selectionMode: systemShare.SelectionMode.SINGLE,
  705. previewMode: systemShare.SharePreviewMode.DETAIL,
  706. }).then(() => {
  707. console.info('ShareController show success.');
  708. }).catch((error: BusinessError) => {
  709. console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`);
  710. });
  711. }
  712. static getNameList(localList:Array<VideoItem>){
  713. let list: Array<string> = [];
  714. for(let i=0;i<localList.length;i++){
  715. if(StrUtil.isNotEmpty(localList[i].name))
  716. list.push(localList[i].name);
  717. }
  718. return list
  719. }
  720. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_Dir:私密视频)
  721. static getGlobalNameList(localList:Array<VideoItem>,type:number){
  722. let list: Array<string> = [];
  723. for(let i=0;i<localList.length;i++){
  724. if(localList[i].type === type){
  725. list.push(localList[i].name);
  726. }
  727. }
  728. return list
  729. }
  730. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_LOCK:私密视频)
  731. static getGlobalList(localList:Array<VideoItem>,type:number){
  732. let list: Array<VideoItem> = [];
  733. for(let i=0;i<localList.length;i++){
  734. if(localList[i].type === type){
  735. list.push(localList[i]);
  736. }
  737. }
  738. return list
  739. }
  740. //根据filePath获取当前索引index
  741. static getCurIndexFromGlobalList(localList:Array<VideoItem>,filePath:string){
  742. let index: number = 0;
  743. for(let i=0;i<localList.length;i++){
  744. if(localList[i].filePath === filePath){
  745. index = i
  746. return index
  747. }
  748. }
  749. return index
  750. }
  751. //新的查询是否收藏的方法
  752. static getIsFav(favList: Array<VideoItem>,item:VideoItem|undefined){
  753. if(ArrayUtil.isEmpty(favList)){
  754. return false
  755. }
  756. if(item===undefined){
  757. return false
  758. }
  759. for(let i=0;i<favList.length;i++){
  760. if(favList[i].filePath === item.filePath&&favList[i].isFav===1){
  761. return true
  762. }
  763. }
  764. return false
  765. }
  766. static getIsFac(facList: Array<String>,item:VideoItem|undefined){
  767. if(ArrayUtil.isEmpty(facList)){
  768. return false
  769. }
  770. if(item===undefined){
  771. return false
  772. }
  773. for(let i=0;i<facList.length;i++){
  774. if(facList[i] === item.name||facList[i]===item.fileName){
  775. return true
  776. }
  777. }
  778. return false
  779. }
  780. static resourceToString(context:Context,resource:Resource){
  781. if(!context||!resource)
  782. return ''
  783. return context.resourceManager.getStringSync(resource).toString()
  784. }
  785. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_LOCK:私密视频)
  786. static getGlobalMusicList(localList:Array<VideoItem>,type:number){
  787. let list: Array<VideoItem> = [];
  788. for(let i=0;i<localList.length;i++){
  789. if(localList[i].type === type){
  790. list.push(localList[i]);
  791. }
  792. }
  793. return list
  794. }
  795. static isHaoFreeTime():boolean{
  796. if(DateUtil.isWeekend()){
  797. return true
  798. }
  799. let str = DateUtil.getFormatDateStr(new Date(), 'HH')
  800. let a = Number(str);
  801. if(isNaN(a))
  802. return false
  803. // ToastUtil.showToast('a='+a)
  804. if (a >= 8 && a <13) {//白天
  805. return false;
  806. }
  807. if (a >= 13 && a <20) {//白天
  808. return false;
  809. }
  810. if (a >= 0 && a < 8) {//凌晨
  811. return true;
  812. }
  813. if (a >= 20 && a <= 24) {//晚上
  814. return true;
  815. }
  816. return false
  817. }
  818. static getMusisBg():Resource {
  819. let index = RandomUtil.randomNumber(0,9)
  820. LogUtil.info('getMusisBg index = '+index)
  821. return CommonConstants.musicBgList[index]
  822. }
  823. static getMusisBg2(index:number):Resource {
  824. const adjustedIndex = index % 10;
  825. LogUtil.info('getMusisBg adjustedIndex = '+adjustedIndex)
  826. return CommonConstants.musicBgList[adjustedIndex]
  827. }
  828. static getParentDirectory(filePath: string): string {
  829. // 使用正则表达式匹配路径分隔符(支持 Unix 和 Windows)
  830. const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
  831. if (lastSlashIndex === -1) {
  832. return '';
  833. }
  834. return filePath.substring(0, lastSlashIndex);
  835. }
  836. static getFileDirName(filePath: string,rootPath:string): string{
  837. if(filePath===rootPath){
  838. return '首页'
  839. }
  840. let result = FileUtil.getFileName(filePath)
  841. if(StrUtil.isEmpty(result))
  842. return ''
  843. if(result.startsWith('.'))
  844. result = result.replace(/\./g, '')
  845. return result
  846. }
  847. // 开启沉浸式显示模式
  848. static async enableFullScreen() {
  849. const ctx = getContext()
  850. const win = await window.getLastWindow(ctx)
  851. win.setWindowLayoutFullScreen(true)
  852. const top = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
  853. AppStorage.setOrCreate('topHeight', px2vp(top.topRect.height))
  854. AppStorage.setOrCreate('bottomHeight', px2vp(top.bottomRect.height))
  855. }
  856. // 关闭沉浸式显示模式
  857. static async disableFullScreen() {
  858. const ctx = getContext()
  859. const win = await window.getLastWindow(ctx)
  860. win.setWindowLayoutFullScreen(false)
  861. AppStorage.setOrCreate('topHeight', 0)
  862. AppStorage.setOrCreate('bottomHeight', 0)
  863. }
  864. // 设置状态栏文字颜色为白色
  865. static async setStatusBarLight() {
  866. const ctx = getContext()
  867. const win = await window.getLastWindow(ctx)
  868. win.setWindowSystemBarProperties({
  869. statusBarContentColor: '#ffffff'
  870. })
  871. }
  872. // 设置状态栏文字颜色为黑色
  873. static async setStatusBarDark() {
  874. const ctx = getContext()
  875. const win = await window.getLastWindow(ctx)
  876. win.setWindowSystemBarProperties({
  877. statusBarContentColor: '#000000'
  878. })
  879. }
  880. // 优化点:完全基于Promise链的异步处理
  881. static async getAppName(context: Context): Promise<string> {
  882. try {
  883. // 1. 同步化BundleInfo获取
  884. const bundleInfo = await bundleManager.getBundleInfoForSelf(
  885. bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
  886. );
  887. // 2. 安全获取labelId(新增空值校验)
  888. const labelId = bundleInfo.appInfo?.labelId;
  889. if (!labelId || labelId <= 0) {
  890. throw new Error("Invalid labelId: " + labelId);
  891. }
  892. // 3. 异步资源解析(替换危险的getStringSync)
  893. return await context.resourceManager.getStringValue(labelId);
  894. } catch (err) {
  895. console.error(`[${new Date().toISOString()}] AppName Error: CODE=${err.code}, MSG=${err.message}`);
  896. // 4. 多级降级策略
  897. return AppUtil.getBundleName() // 最终兜底
  898. }
  899. }
  900. //按名称升序
  901. static doSortListAscending( list:Array<VideoItem>){
  902. let options: Intl.CollatorOptions = {
  903. localeMatcher: "lookup",
  904. usage: "sort",
  905. sensitivity: "case" // 区分大小写
  906. };
  907. const collator = new Intl.Collator("zh-CN", options);
  908. list.sort((a, b) => {
  909. const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type);
  910. if (typeOrder !== 0) {
  911. return typeOrder;
  912. }
  913. const partsA = extractParts(a.name);
  914. const partsB = extractParts(b.name);
  915. const nonNumericComparison = collator.compare(partsA.nonNumeric, partsB.nonNumeric);
  916. if (nonNumericComparison !== 0) {
  917. return nonNumericComparison;
  918. }
  919. return partsA.numeric - partsB.numeric;
  920. });
  921. }
  922. //按名称降序
  923. static doSortListDescending(list:Array<VideoItem>){
  924. const options: Intl.CollatorOptions = {
  925. localeMatcher: "lookup",
  926. usage: "sort",
  927. sensitivity: "case" // 区分大小写
  928. };
  929. const collator = new Intl.Collator("zh-CN", options);
  930. list.sort((a, b) => {
  931. const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type);
  932. if (typeOrder !== 0) {
  933. return typeOrder;
  934. }
  935. const partsA = extractParts(a.name);
  936. const partsB = extractParts(b.name);
  937. const nonNumericComparison = collator.compare(partsB.nonNumeric, partsA.nonNumeric);
  938. if (nonNumericComparison !== 0) {
  939. return nonNumericComparison;
  940. }
  941. return partsB.numeric - partsA.numeric;
  942. });
  943. }
  944. /**
  945. * 读取本地会员到期时间(解密.vv文件)
  946. * @returns
  947. */
  948. static async getLocalNobleExpireDate(): Promise<string> {
  949. // return "2025-08-08 12:00";
  950. try {
  951. const documentViewPicker = new picker.DocumentViewPicker()
  952. let documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD })
  953. let download_path = new fileUri.FileUri(documentSaveResult[0]).path + '/'
  954. let filePath = download_path + LocalMusic.STR_LOCK_VIDEO+ '/' + VipPage.VIP_FILEPATH
  955. let expireDate = Utility.readDataFromFile(filePath)
  956. return expireDate;
  957. } catch (e) {
  958. return '';
  959. }
  960. }
  961. static readDataFromFile(filePath: string) {
  962. if(!FileUtil.accessSync(filePath)){
  963. return ''
  964. }
  965. let readTextOptions: ReadTextOptions = {
  966. offset: 0,
  967. length: 0,
  968. encoding: 'utf-8'
  969. };
  970. let stat = fileIo.statSync(filePath);
  971. readTextOptions.length = stat.size;
  972. let str = fileIo.readTextSync(filePath, readTextOptions);
  973. const parsedData:VipData = JSON.parse(str) as VipData;
  974. // LogUtil.info('onecold parsedData.expireDate = ' + parsedData.expireDate)
  975. let result = Base64Util.decodeSync( parsedData.expireDate)
  976. // LogUtil.info('onecold 解密的result = ' +StrUtil.unit8ArrayToStr(result))
  977. return StrUtil.unit8ArrayToStr(result);
  978. }
  979. }
  980. interface PathStat {
  981. path: string;
  982. isDirectory: boolean;
  983. }
  984. async function isDirectory(filePath: string): Promise<boolean> {
  985. try {
  986. const stat = await fs.stat(filePath);
  987. return stat.isDirectory();
  988. } catch (error) {
  989. console.error('Error getting file stat:', error);
  990. return false;
  991. }
  992. }
  993. //排序模式参数
  994. interface VideoNameParts {
  995. nonNumeric: string;
  996. numeric: number;
  997. }
  998. function extractParts(name: string): VideoNameParts {
  999. const match = name.match(/^(\D*)(\d*)/);
  1000. return {
  1001. nonNumeric: match?.[1] || '',
  1002. numeric: parseInt(match?.[2] || '0', 10)
  1003. };
  1004. }
  1005. function getInstallTime(): number | null {
  1006. // 从本地存储中获取安装时间
  1007. return PreferencesUtil.getNumberSync('installTime')
  1008. }
  1009. function setInstallTime(time: number) {
  1010. // 将安装时间存储到本地存储中
  1011. PreferencesUtil.putSync('installTime',time)
  1012. }
  1013. function fetchAlbumCover(avMetadataExtractor: media.AVMetadataExtractor): Promise<image.PixelMap | null> {
  1014. return new Promise((resolve, reject) => {
  1015. avMetadataExtractor.fetchAlbumCover((error: BusinessError, pixelMap: image.PixelMap) => {
  1016. if (error) {
  1017. console.error(`Failed to fetch AlbumCover, error = ${JSON.stringify(error)}`);
  1018. resolve(null);
  1019. } else {
  1020. resolve(pixelMap);
  1021. }
  1022. avMetadataExtractor.release();
  1023. });
  1024. });
  1025. }
  1026. //排序类型
  1027. function getTypeOrder(type: number) {
  1028. switch (type) {
  1029. case CommonConstants.TYPE_IS_DIR:
  1030. return 1; // First
  1031. case CommonConstants.TYPE_IS_CSJAD:
  1032. return 2; // Middle
  1033. case CommonConstants.TYPE_LOCAL:
  1034. return 3; // Last
  1035. default:
  1036. return 4; // Unknown types, if any, go last
  1037. }
  1038. }
  1039. function convertSecondsToTime(secondsStr: string): string {
  1040. if (!secondsStr || isNaN(Number(secondsStr))) {
  1041. return "00:00";
  1042. }
  1043. let seconds = parseInt(secondsStr, 10);
  1044. seconds = Math.floor(seconds / 1000);
  1045. const hours = Math.floor(seconds / 3600);
  1046. const minutes = Math.floor((seconds % 3600) / 60);
  1047. const secs = seconds % 60;
  1048. const formatNumber = (num: number) => num.toString().padStart(2, '0');
  1049. if (hours > 0) {
  1050. return `${formatNumber(hours)}:${formatNumber(minutes)}:${formatNumber(secs)}`;
  1051. } else {
  1052. return `${formatNumber(minutes)}:${formatNumber(secs)}`;
  1053. }
  1054. }
  1055. // 定义解析结果的数据结构
  1056. class MusicInfo {
  1057. artist: string = ""; // 艺术家名称
  1058. title: string = ""; // 歌曲名称
  1059. isValid: boolean = false; // 格式是否有效
  1060. }
  1061. /**
  1062. * 解析音乐文件名
  1063. * @param fileName - 待解析的文件名(需包含扩展名)
  1064. * @returns 包含解析结果的MusicInfo对象
  1065. */
  1066. /**
  1067. * 智能解析音乐文件名(支持多种分隔符和前缀序号)
  1068. * @param fileName - 待解析的完整文件名
  1069. * @returns 结构化音乐信息
  1070. */
  1071. function parseMusicFileName(fileName: string): MusicInfo {
  1072. const result = new MusicInfo();
  1073. // 1. 预处理:移除首尾空格(保留中间空格)
  1074. const cleanName = fileName.trim();
  1075. // 2. 提取文件扩展名(以最后一个点分隔)
  1076. const lastDotIndex = cleanName.lastIndexOf('.');
  1077. if (lastDotIndex < 0) return result; // 无扩展名
  1078. const baseName = cleanName.substring(0, lastDotIndex).trim();
  1079. const extension = cleanName.substring(lastDotIndex + 1);
  1080. // 3. 支持多种分隔符(中英文短横线)
  1081. const separators = ['-', '-', '—']; // 半角/全角短横线
  1082. let dashIndex = -1;
  1083. // 查找最后一个有效分隔符位置
  1084. for (const sep of separators) {
  1085. const index = baseName.lastIndexOf(sep);
  1086. if (index > dashIndex) dashIndex = index;
  1087. }
  1088. // 4. 核心解析逻辑
  1089. if (dashIndex > 0 && dashIndex < baseName.length - 1) {
  1090. let artistPart = baseName.substring(0, dashIndex).trim();
  1091. result.title = baseName.substring(dashIndex + 1).trim();
  1092. // 5. 处理前缀序号(如"04 - ")
  1093. const numPrefixRegex = /^\d+\s*[--—]\s*/; // 匹配数字+分隔符组合
  1094. artistPart = artistPart.replace(numPrefixRegex, '').trim();
  1095. // 6. 最终有效性验证
  1096. result.artist = artistPart;
  1097. result.isValid = (result.artist.length > 0 && result.title.length > 0);
  1098. }
  1099. return result;
  1100. }