onecold 1 год назад
Родитель
Сommit
c52e776733
2 измененных файлов с 138 добавлено и 25 удалено
  1. 95 21
      entry/src/main/ets/common/util/Utility.ets
  2. 43 4
      entry/src/main/ets/view/LocalMusic.ets

+ 95 - 21
entry/src/main/ets/common/util/Utility.ets

@@ -827,8 +827,8 @@ export class Utility {
         let metaItem = await parseAudioMetadata(uri)
         if(metaItem){
           item.lyricContent = metaItem.lyricContent
-          item.bit_rate = metaItem.bit_rate
-          item.year = metaItem.year
+          item.bit_rate = formatBitrateToKbps(metaItem.bit_rate  || "0");
+          item.year = metaItem.year ||'unknown'
           item.probe_score = metaItem.probe_score
           item.nb_streams = metaItem.nb_streams
           item.nb_programs = metaItem.nb_programs
@@ -907,13 +907,13 @@ export class Utility {
             videoItem.pyStr = pinyin4js.getShortPinyin(name)
             videoItem.fileName  = FileUtil.getFileName(inputPath);
             if(format.duration)
-              videoItem.duration = convertSecondsToTime(format.duration.toString())
+              videoItem.duration = formatDuration(format.duration.toString()||'00:00')
             videoItem.size  = fileSize;
-            videoItem.bit_rate  = format.bit_rate;
+            videoItem.bit_rate  =formatBitrateToKbps(format.bit_rate  || "0");
             videoItem.probe_score  = format.probe_score;
             videoItem.nb_streams  = format.nb_streams;
             videoItem.nb_programs  = format.nb_programs;
-            videoItem.year  = tags.TYER || tags.date  || ''; // try different tag names for year
+            videoItem.year  = tags.TYER || tags.date  || 'unknown'; // try different tag names for year
             videoItem.lyricContent  = tags.LYRICS || tags.lyrics  ||  tags.USLT || tags.UNSYNCEDLYRICS || '';
 
             // 检查是否有封面图片流
@@ -924,22 +924,23 @@ export class Utility {
             // 如果有封面图片,则提取
             if (hasCover) {
 
-              // try {
-              //   let md5Name = await MD5.digestSync(inputPath)
-              //   // const imageName = `${md5Name}.jpg`;
-              //   const imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}`;
-              //   console.info('readMetaInfoFFmpeg asset imagePath: ', imagePath);
-              //   // 提取封面图片
-              //   await extractCoverImage(inputPath, imagePath);
-              //
-              //   // 检查图片是否生成成功
-              //   if (fs.accessSync(imagePath))  {
-              //     videoItem.pixelMapPath  = imagePath;
-              //
-              //   }
-              // } catch (error) {
-              //   console.warn(' 提取封面图片失败:', error.message);
-              // }
+              try {
+                let md5Name = await MD5.digestSync(inputPath)
+                // const imageName = `${md5Name}.jpg`;
+                const imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}`;
+                console.info('readMetaInfoFFmpeg asset imagePath: ', imagePath);
+                // 提取封面图片
+                // await extractCoverImage(inputPath, imagePath);
+                await FFmpegCover(inputPath, imagePath);
+
+                // 检查图片是否生成成功
+                if (fs.accessSync(imagePath))  {
+                  videoItem.pixelMapPath  = imagePath;
+
+                }
+              } catch (error) {
+                console.warn(' 提取封面图片失败:', error.message);
+              }
             }
 
             console.info('Successfully  parsed metadata:', videoItem);
@@ -1511,6 +1512,31 @@ function convertSecondsToTime(secondsStr: string): string {
   }
 }
 
+/**
+ * 秒数 → 智能时间格式(自动选择 MM:SS 或 HH:MM:SS)
+ * @param seconds 秒数字符串(如 "283.524351" 或 "3675")
+ * @param forceHHMMSS 强制使用 HH:MM:SS 格式(默认自动判断)
+ * @returns 格式化后的时间字符串
+ */
+function formatDuration(seconds: string, forceHHMMSS: boolean = false): string {
+  // 1. 校验输入
+  const secNum = parseFloat(seconds);
+  if (isNaN(secNum) || secNum < 0) return forceHHMMSS ? "00:00:00" : "00:00";
+
+  // 2. 计算时间分量
+  const totalSec = Math.floor(secNum);
+  const hours = Math.floor(totalSec  / 3600);
+  const mins = Math.floor((totalSec  % 3600) / 60);
+  const secs = totalSec % 60;
+
+  // 3. 格式化输出
+  const pad = (n: number) => n.toString().padStart(2,  '0');
+
+  return forceHHMMSS || hours > 0
+    ? `${pad(hours)}:${pad(mins)}:${pad(secs)}`  // HH:MM:SS
+    : `${pad(mins)}:${pad(secs)}`;              // MM:SS
+}
+
 
 // 定义解析结果的数据结构
 class MusicInfo {
@@ -1574,6 +1600,11 @@ function getFileNameWithoutExtension(filePath: string): string {
   return lastDotIndex > 0 ? fileName.substring(0,  lastDotIndex) : fileName;
 }
 
+/**
+ * 从音乐文件中提取封面
+ * @param inputPath 音乐文件路径
+ * @returns Promise<void>
+ */
 async function extractCoverImage(inputPath: string, outputPath: string): Promise<void> {
   const commands = [
     'ffmpeg',
@@ -1598,6 +1629,30 @@ async function extractCoverImage(inputPath: string, outputPath: string): Promise
   });
 }
 
+
+/**
+ * 从音乐文件中提取封面
+ * @param inputPath 音乐文件路径
+ * @returns Promise<void>
+ */
+async function FFmpegCover(inputPath: string, outputPath: string) {
+  let commands = ["ffmpeg", "-i", inputPath, "-map", "0:v", "-c:v", "copy", outputPath];
+  FFmpeg.execute(commands, {
+    logCallback: (logLevel: number, logMessage: string) => {
+      console.info(`[FFmpeg LOG] [${logLevel}]${logMessage}`)
+    },
+    progressCallback: (message: string) => {
+      console.info(`[FFmpeg progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
+    },
+  })
+    .then(() => {
+      console.info("FFmpeg execution succeeded.");
+    })
+    .catch((error: Error) => {
+      console.error(`FFmpeg execution failed with error: ${error.message}`);
+    });
+}
+
 /**
  * 从音乐文件中提取歌词内容
  * @param inputPath 音乐文件路径
@@ -1702,4 +1757,23 @@ async function parseAudioMetadata(inputPath: string): Promise<VideoItem> {
       reject(new Error(`元数据解析失败: ${error instanceof Error ? error.message  : String(error)}`));
     }
   });
+}
+
+
+/**
+ * 将比特率(bps)转换为 kbps 并格式化
+ * @param bitRate 比特率字符串(如 "5644802")
+ * @param decimalPlaces 保留小数位数(默认0)
+ * @returns 格式化后的 kbps 字符串(如 "5644 kbps")
+ */
+function formatBitrateToKbps(bitRate: string, decimalPlaces: number = 0): string {
+  // 1. 转换为数字
+  const bitsPerSecond = parseInt(bitRate);
+  if (isNaN(bitsPerSecond) || bitsPerSecond < 0) return "0 kbps";
+
+  // 2. 计算 kbps(1 kbps = 1000 bps)
+  const kbps = bitsPerSecond / 1000;
+
+  // 3. 格式化输出
+  return `${kbps.toFixed(decimalPlaces)}  kbps`;
 }

+ 43 - 4
entry/src/main/ets/view/LocalMusic.ets

@@ -2759,7 +2759,7 @@ export struct LocalMusic {
 
   @State currentTitleName: string = '' //点击歌单,艺术家,专辑进去后的标题
   @State currentTitleCover: string | ResourceStr = '' //点击歌单,艺术家,专辑进去后的封面
-
+  @State currentYear:string = ''
   isShowCoverHeader() {
     if (!this.isShowHeader) {
       return false
@@ -2848,6 +2848,14 @@ export struct LocalMusic {
                   .fontWeight(FontWeight.Bold)
                   .fontColor($r('app.color.text_color'))
                   .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 })
+
+                Text(this.currentYear)
+                  .fontSize(16)
+                  .padding({ top: 8 })
+                  .maxLines(1)
+                  .fontWeight(FontWeight.Bold)
+                  .fontColor($r('app.color.text_color'))
+                  .margin({ left: this.currentLyricAlignMode === 0 ? 18 : 0 })
               }
               Row() {
                 Text(`共${this.videoLocalList.length}首歌`)
@@ -3825,7 +3833,7 @@ export struct LocalMusic {
             content: $r('app.string.detail')
           })
             .bindSheet($$this.isShowDetail, this.detailSheet(item), {
-              height:this.isCoverOpacity()?'95%': '66%',
+              height:this.isCoverOpacity()?'95%': '95%',
               dragBar: true,
               showClose: true,
               blurStyle:BlurStyle.Thin,
@@ -4362,6 +4370,7 @@ export struct LocalMusic {
           this.isCanBack = true
           this.titleBarModel.setTitleName(item.name)
           this.currentTitleName = '专辑:' + item.name
+          this.currentYear = '发行时间:' + item.year
           if (item.pixelMapPath) {
             this.currentTitleCover = item.pixelMapPath
           }
@@ -5045,7 +5054,7 @@ export struct LocalMusic {
         .justifyContent(FlexAlign.Start)
 
         Row() {
-          Text('发行年份:')
+          Text('发行时间:')
             .fontSize(14)
             .fontColor(Color.White)
             .margin({ left: 22 })
@@ -5104,6 +5113,36 @@ export struct LocalMusic {
         .margin({ top: 10, bottom: 10 })
         .justifyContent(FlexAlign.Start)
 
+        Row() {
+          Text('播放次数:')
+            .fontSize(14)
+            .fontColor(Color.White)
+            .margin({ left: 22 })
+          Text(currentItem.playCount?.toString())
+            .fontSize(14)
+            .margin({ left: 10 })
+            .fontColor(Color.White)
+            .layoutWeight(1)
+        }
+        .width('100%')
+        .margin({ top: 10, bottom: 10 })
+        .justifyContent(FlexAlign.Start)
+
+        Row() {
+          Text('流数量:')
+            .fontSize(14)
+            .fontColor(Color.White)
+            .margin({ left: 22 })
+          Text(currentItem.nb_streams?.toString())
+            .fontSize(14)
+            .margin({ left: 10 })
+            .fontColor(Color.White)
+            .layoutWeight(1)
+        }
+        .width('100%')
+        .margin({ top: 10, bottom: 10 })
+        .justifyContent(FlexAlign.Start)
+
         Row() {
           Text('文件大小:')
             .fontSize(14)
@@ -8356,7 +8395,7 @@ export struct LocalMusic {
                   .fontSize(16)
                   .fontColor(Color.White)
                   .bindSheet($$this.isShowDetailMore, this.detailSheet(this.currentSong), {
-                    height: this.isCoverOpacity() ? '95%' : '75%',
+                    height: this.isCoverOpacity() ? '95%' : '95%',
                     dragBar: true,
                     preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM,
                     showClose: true,