فهرست منبع

Merge branch 'master' into feature/桌面卡片-抽离ijkplayer播放控制

# Conflicts:
#	entry/src/main/ets/view/LocalMusic.ets
chendeben 1 سال پیش
والد
کامیت
589f8b376e

+ 2 - 2
AppScope/app.json5

@@ -2,8 +2,8 @@
   "app": {
     "bundleName": "com.xgplayer.ttmusic.hm",
     "vendor": "example",
-    "versionCode": 20250810,
-    "versionName": "1.5.1",
+    "versionCode": 20250819,
+    "versionName": "1.5.2",
     "icon": "$media:app_icon",
     "label": "$string:app_name",
     "multiAppMode": {

+ 63 - 3
entry/src/main/ets/common/util/MediaTable.ets

@@ -36,7 +36,7 @@ interface DBColumnsInterface {
  */
 const DB_COLUMNS: DBColumnsInterface = {
   ID: 'id',
-  NAME: 'name', 
+  NAME: 'name',
   FILE_PATH: 'filePath',
   TYPE: 'mtype',
   VIDEO_SIZE: 'videoSize',
@@ -95,7 +95,7 @@ export default class MediaTable {
     this.accountTable.deleteData(predicates, callback);
   }
 
-
+  //更新音乐封面地址
   public updatePixelMapPath(filePath: string, newPixelMapPath: string, callback: Function) {
     // Step 1: 构建查询条件验证文件存在性
     const queryPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
@@ -133,7 +133,10 @@ export default class MediaTable {
   }
 
   //编辑歌曲的信息更新数据库
-  public updateMediaInfo(filePath: string, title: string, artist: string, album: string, callback: Function) {
+  public updateMediaInfo(filePath: string, title: string, artist: string, album: string,
+    lyricContent:string,year:string,genre:string,track:string,
+    ALBUMARTIST:string,COMPOSER:string,LYRICIST:string,COMMENT:string,disc:string,
+    callback: Function) {
     if (!callback || typeof callback !== 'function') {
       Logger.info(RdbUtils.RDB_TAG, 'updateMediaInfo() has no valid callback!');
       return;
@@ -169,6 +172,34 @@ export default class MediaTable {
       if (album !== '') {
         valuesToUpdate.album = album;
       }
+      if (lyricContent !== '') {
+        valuesToUpdate.lyricContent = lyricContent;
+      }
+      if (year !== '') {
+        valuesToUpdate.year = year;
+      }
+      if (genre !== '') {
+        valuesToUpdate.genre = genre;
+      }
+      if (track !== '') {
+        valuesToUpdate.track = track;
+      }
+
+      if (ALBUMARTIST !== '') {
+        valuesToUpdate.ALBUMARTIST = ALBUMARTIST;
+      }
+      if (COMPOSER !== '') {
+        valuesToUpdate.COMPOSER = COMPOSER;
+      }
+      if (LYRICIST !== '') {
+        valuesToUpdate.LYRICIST = LYRICIST;
+      }
+      if (COMMENT !== '') {
+        valuesToUpdate.COMMENT = COMMENT;
+      }
+      if (disc !== '') {
+        valuesToUpdate.disc = disc;
+      }
 
       resultSet.close();
 
@@ -246,6 +277,12 @@ export default class MediaTable {
       obj.channel_layout  = resultSet.getString(resultSet.getColumnIndex('channel_layout'));
       obj.start_time  = resultSet.getString(resultSet.getColumnIndex('start_time'));
 
+      obj.ALBUMARTIST  = resultSet.getString(resultSet.getColumnIndex('ALBUMARTIST'));
+      obj.COMPOSER  = resultSet.getString(resultSet.getColumnIndex('COMPOSER'));
+      obj.LYRICIST  = resultSet.getString(resultSet.getColumnIndex('LYRICIST'));
+      obj.COMMENT  = resultSet.getString(resultSet.getColumnIndex('COMMENT'));
+      obj.disc  = resultSet.getString(resultSet.getColumnIndex('disc'));
+
       const valueBucket: relationalStore.ValuesBucket = obj
       // Step 4: 执行更新
       const updatePredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
@@ -577,6 +614,12 @@ export default class MediaTable {
     item.channel_layout = safeGet('channel_layout');
     item.start_time = safeGet('start_time');
 
+    item.ALBUMARTIST = safeGet('ALBUMARTIST');
+    item.COMPOSER = safeGet('COMPOSER');
+    item.LYRICIST = safeGet('LYRICIST');
+    item.COMMENT = safeGet('COMMENT');
+    item.disc = safeGet('disc');
+
     return item;
   }
 
@@ -684,5 +727,22 @@ function generateBucket(item: VideoItem): relationalStore.ValuesBucket {
     obj.start_time = item.start_time;
   }
 
+  if(item.ALBUMARTIST){
+    obj.ALBUMARTIST = item.ALBUMARTIST;
+  }
+  if(item.COMPOSER){
+    obj.COMPOSER = item.COMPOSER;
+  }
+  if(item.LYRICIST){
+    obj.LYRICIST = item.LYRICIST;
+  }
+  if(item.COMMENT){
+    obj.COMMENT = item.COMMENT;
+  }
+  if(item.disc){
+    obj.disc = item.disc;
+  }
+
+
   return obj;
 }

+ 460 - 0
entry/src/main/ets/common/util/MusicTagUtils.ets

@@ -0,0 +1,460 @@
+/*
+ * Copyright (c) 2024 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { FFmpeg, FFProgressMessageParser } from "@sj/ffmpeg";
+import fs from '@ohos.file.fs';
+import { FFMpegTags } from "./Utility";
+import { http } from "@kit.NetworkKit";
+import { BusinessError } from "@kit.BasicServicesKit";
+import ResponseCode from '@ohos.net.http';
+import { LogUtil, PreferencesUtil, StrUtil } from "@pura/harmony-utils";
+import { CommonConstants } from "../constants/CommonConstants";
+
+/**
+ * 修复音频文件的元数据标签
+ * @param inputPath 输入文件路径
+ * @param metadata 要更新的元数据对象
+ * @param overwrite 是否覆盖源文件(可选,默认false)
+ * @param outputPath 指定输出路径(可选,优先级高于overwrite)
+ * @returns Promise<boolean> 成功返回true,失败返回false
+ */
+export async function repairAudioMetadata(
+  inputPath: string,
+  lyrics:string,
+  metadata: FFMpegTags,
+  overwrite: boolean = false,
+  outputPath: string = ''
+): Promise<boolean> {
+  // 确保metadata是对象类型
+  if (typeof metadata !== 'object' || metadata === null) {
+    console.error('元数据参数必须是一个对象');
+    return false;
+  }
+
+  // 确定输出路径
+  let finalOutputPath: string = '';
+  let useTempFile: boolean = false;
+
+  if (outputPath && outputPath.length > 0) {
+    finalOutputPath = outputPath;
+  } else if (overwrite) {
+    // 使用临时文件方式处理覆盖,保持原文件扩展名
+    const timestamp = new Date().getTime();
+    const lastDotIndex = inputPath.lastIndexOf('.');
+    if (lastDotIndex >= 0) {
+      finalOutputPath = inputPath.substring(0, lastDotIndex) + '_' + timestamp + inputPath.substring(lastDotIndex);
+    } else {
+      finalOutputPath = inputPath + '_' + timestamp;
+    }
+    useTempFile = true;
+  } else {
+    const lastDotIndex = inputPath.lastIndexOf('.');
+    if (lastDotIndex >= 0) {
+      finalOutputPath = inputPath.substring(0, lastDotIndex) + '_tagged' + inputPath.substring(lastDotIndex);
+    } else {
+      finalOutputPath = inputPath + '_tagged';
+    }
+  }
+
+  // 构建FFmpeg命令
+  const commands: string[] = [
+    "ffmpeg",
+    "-i", inputPath
+  ];
+
+  // 添加元数据参数
+  const dynamicMetadata = metadata as Record<string, string>;
+  Object.keys(dynamicMetadata).forEach((key) => {
+    const value = dynamicMetadata[key];
+    if (value) {
+      commands.push("-metadata");
+      commands.push(`${key}=${value}`);
+    }
+  });
+  //额外再次添加下lyrics-XXX的歌词参数,以便其他音乐播放器可以识别歌词
+  // 额外添加歌词自定义标签(修复后的核心代码)
+  if (StrUtil.isNotEmpty(lyrics)) { // 确保歌词内容存在时才添加
+    // 多种歌词标签格式
+    const lyricTags = [
+      `LYRICS=${lyrics}`,
+      `lyrics-XXX=${lyrics}`,
+      `USLT::XXX=${lyrics}`,
+    // `UNSYNCEDLYRICS=${lyrics}`
+    ];
+
+    lyricTags.forEach(tag  => {
+      commands.push("-metadata");
+      commands.push(tag);
+    });
+  }
+
+
+  commands.push(
+    "-map", "0",
+    "-map_metadata", "0",
+    "-id3v2_version", "3",
+    "-codec", "copy",
+    "-y",
+    finalOutputPath
+  );
+
+  try {
+    await FFmpeg.execute(commands, {
+      logCallback: (logLevel: number, logMessage: string) => {
+        //console.log(`onecold logCallback [${logLevel}]${logMessage}`);
+      },
+      progressCallback: (message: string) => {
+        //console.log(`onecold progressCallback [progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`);
+      },
+    });
+
+    // 如果是使用临时文件,需要替换原文件
+    if (useTempFile) {
+      try {
+        console.info(`onecold unlinkSync`);
+        // 先删除原文件,再重命名临时文件
+        await fs.unlinkSync(inputPath);
+        console.info(`onecold unlinkSync2`);
+        fs.renameSync(finalOutputPath, inputPath);
+        console.info(`onecold renameSync`);
+        finalOutputPath = inputPath; // 更新为最终路径
+      } catch (renameError) {
+        console.error(`onecold 文件替换失败: ${JSON.stringify(renameError)}`);
+        return false;
+      }
+    }
+
+    console.info(`onecold 元数据修复成功,保存路径: ${finalOutputPath}`);
+
+    // 验证文件是否存在
+    try {
+      const file = await fs.open(finalOutputPath, fs.OpenMode.READ_ONLY);
+      await fs.close(file.fd);
+      return true;
+    } catch (e) {
+      console.error(`onecold 文件验证失败: ${JSON.stringify(e)}`);
+      return false;
+    }
+
+  } catch (error) {
+    let errorMsg: string = '';
+    if (error instanceof Error) {
+      errorMsg = error.message;
+    } else {
+      errorMsg = String(error);
+    }
+    console.error(`onecold元数据修复失败 ${errorMsg}`);
+
+    // 清理临时文件(如果存在)
+    if (useTempFile) {
+      try {
+        if (fs.accessSync(finalOutputPath)) {
+          fs.unlinkSync(finalOutputPath);
+        }
+      } catch (cleanupError) {
+        console.warn('清理临时文件失败:', cleanupError);
+      }
+    }
+
+    return false;
+  }
+}
+
+
+
+
+/**
+ * 修改音乐文件的封面
+ * @param inputPath 音乐文件路径
+ * @param coverImagePath 封面图片路径(支持HTTP地址或本地地址)
+ * @param overwrite 是否覆盖源文件(可选,默认false)
+ * @param outputPath 指定输出路径(可选,优先级高于overwrite)
+ * @returns Promise<boolean> 成功返回true,失败返回false
+ */
+/**
+ * 修改音乐文件的封面
+ * @param context 上下文对象
+ * @param inputPath 音乐文件路径
+ * @param coverImagePath 封面图片路径(支持HTTP地址或本地地址)
+ * @param overwrite 是否覆盖源文件(可选,默认false)
+ * @param outputPath 指定输出路径(可选,优先级高于overwrite)
+ * @returns Promise<boolean> 成功返回true,失败返回false
+ */
+export async function changeMusicCover(
+  context: Context,
+  inputPath: string,
+  coverImagePath: string,
+  overwrite: boolean = false,
+  outputPath: string = ''
+): Promise<boolean> {
+  // 确定输出路径
+  let finalOutputPath: string = '';
+  let useTempFile: boolean = false;
+
+  if (outputPath && outputPath.length > 0) {
+    finalOutputPath = outputPath;
+  } else if (overwrite) {
+    // 使用临时文件方式处理覆盖,保持原文件扩展名
+    const timestamp = new Date().getTime();
+    const lastDotIndex = inputPath.lastIndexOf('.');
+    if (lastDotIndex >= 0) {
+      finalOutputPath = inputPath.substring(0, lastDotIndex) + '_' + timestamp + inputPath.substring(lastDotIndex);
+    } else {
+      finalOutputPath = inputPath + '_' + timestamp;
+    }
+    useTempFile = true;
+  } else {
+    const lastDotIndex = inputPath.lastIndexOf('.');
+    if (lastDotIndex >= 0) {
+      finalOutputPath = inputPath.substring(0, lastDotIndex) + '_covered' + inputPath.substring(lastDotIndex);
+    } else {
+      finalOutputPath = inputPath + '_covered';
+    }
+  }
+
+  // 如果是网络图片,先下载到临时文件
+  let tempCoverPath = coverImagePath;
+  console.log(`onecold 开始下载 coverImagePath = [${coverImagePath}]`);
+  if (coverImagePath.startsWith('http')) {
+    try {
+      // 创建临时文件路径
+      const tempDir = context.filesDir + '/'; // 默认缓存目录
+      const tempFileName = 'temp_cover.jpg';
+      tempCoverPath = `${tempDir}${tempFileName}`;
+
+      // 下载图片
+      const result = await loadImageWithUrl(coverImagePath, tempCoverPath);
+      if (!result) {
+        console.error('onecold 下载封面图片失败');
+        // 清理可能已创建的临时文件
+        if (tempCoverPath !== coverImagePath) {
+          try {
+            fs.unlinkSync(tempCoverPath);
+          } catch (unlinkError) {
+            console.warn('onecold 清理临时文件失败:', unlinkError);
+          }
+        }
+        return false;
+      }
+    } catch (error) {
+      console.error('onecold 下载封面图片时出错:', error);
+      // 清理可能已创建的临时文件
+      if (tempCoverPath !== coverImagePath) {
+        try {
+          fs.unlinkSync(tempCoverPath);
+        } catch (unlinkError) {
+          console.warn('onecold 清理临时文件失败:', unlinkError);
+        }
+      }
+      return false;
+    }
+  }
+  console.log(`onecold 开始下载 tempCoverPath = [${tempCoverPath}]`);
+  // 构建FFmpeg命令
+  const commands: string[] = [
+    "ffmpeg",
+    "-i", inputPath,
+    "-i", tempCoverPath,
+    "-map", "0:0",           // 映射音频流
+    "-map", "1:0",           // 映射封面图片流
+    "-c", "copy",            // 复制音频流
+    "-id3v2_version", "3",   // ID3v2版本
+    "-y",                    // 覆盖输出文件
+    finalOutputPath
+  ];
+
+  try {
+    await FFmpeg.execute(commands, {
+      logCallback: (logLevel: number, logMessage: string) => {
+        //console.log(`onecold [FFmpeg LOG] [${logLevel}]${logMessage}`);
+      },
+      progressCallback: (message: string) => {
+        //console.log(`onecold [FFmpeg progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`);
+      },
+    });
+
+    // 如果是使用临时文件,需要替换原文件
+    if (useTempFile) {
+      try {
+        // 先删除原文件,再重命名临时文件
+        await fs.unlinkSync(inputPath);
+        fs.renameSync(finalOutputPath, inputPath);
+        finalOutputPath = inputPath; // 更新为最终路径
+      } catch (renameError) {
+        console.error(`onecold 文件替换失败: ${JSON.stringify(renameError)}`);
+        return false;
+      }
+    }
+
+    console.info(`onecold 封面修改成功,保存路径: ${finalOutputPath}`);
+
+    // 验证文件是否存在
+    try {
+      const file = await fs.open(finalOutputPath, fs.OpenMode.READ_ONLY);
+      await fs.close(file.fd);
+
+      // 如果是下载的临时图片,清理临时文件
+      if (tempCoverPath !== coverImagePath) {
+        try {
+          fs.unlinkSync(tempCoverPath);
+        } catch (unlinkError) {
+          console.warn('onecold 清理临时文件失败:', unlinkError);
+        }
+      }
+
+      return true;
+    } catch (e) {
+      console.error(`onecold 文件验证失败: ${JSON.stringify(e)}`);
+      return false;
+    }
+
+  } catch (error) {
+    let errorMsg: string = '';
+    if (error instanceof Error) {
+      errorMsg = error.message;
+    } else {
+      errorMsg = String(error);
+    }
+    console.error(`onecold封面修改失败: ${errorMsg}`);
+
+    // 清理临时文件
+    if (tempCoverPath !== coverImagePath) {
+      try {
+        fs.unlinkSync(tempCoverPath);
+      } catch (unlinkError) {
+        console.warn('onecold 清理临时文件失败:', unlinkError);
+      }
+    }
+
+    // 如果使用了临时文件但处理失败,也需要清理
+    if (useTempFile && fs.accessSync(finalOutputPath)) {
+      try {
+        fs.unlinkSync(finalOutputPath);
+      } catch (cleanupError) {
+        console.warn('onecold 清理临时输出文件失败:', cleanupError);
+      }
+    }
+
+    return false;
+  }
+}
+
+
+/**
+ * 下载图片到指定路径
+ * @param url 图片URL
+ * @param outputPath 输出路径
+ * @returns Promise<boolean> 成功返回true,失败返回false
+ */
+export async function loadImageWithUrl( url: string, outputPath: string,): Promise<boolean > {
+  return new Promise((resolve, reject) => {
+    http.createHttp().request(url, { method: http.RequestMethod.GET, connectTimeout: 60000, readTimeout: 60000 },
+      async (error: BusinessError, data: http.HttpResponse) => {
+        if (error) {
+          console.error(`http request failed with. Code: ${error.code}, message: ${error.message}`);
+          reject(false);
+        } else {
+          if (ResponseCode.ResponseCode.OK === data.responseCode) {
+            let imageBuffer: ArrayBuffer = data.result as ArrayBuffer;
+            try {
+              // 获取相册路径
+              let file = await fs.open(outputPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
+              // 写入文件
+              await fs.write(file.fd, imageBuffer);
+              // 关闭文件
+              await fs.close(file.fd);
+
+              // 返回文件标识符
+              resolve(true);
+            } catch (error) {
+              console.error("error is " + JSON.stringify(error));
+              reject(false);
+            }
+          } else {
+            console.error("error occurred when image downloaded!");
+            reject(false);
+          }
+        }
+      });
+  });
+}
+
+
+export function getApiLyric(apiUrl:string,title: string, artist: string, isApi2: boolean): Promise<string> {
+  return new Promise(async (resolve) => {
+    try {
+      if (StrUtil.isNotEmpty(title)  && title === '全世界最好的你') {
+        artist = '';
+      }
+
+      // let baseUrl = PreferencesUtil.getStringSync('LRC_API',  '');
+      if (apiUrl == '') {
+        LogUtil.debug("Heanup  未设置API");
+        resolve('');
+        return;
+      }
+
+      if (apiUrl.includes(CommonConstants.LRC_API_2))  {
+        isApi2 = true;
+      }
+
+      let requestUrl = apiUrl +
+        '?title=' + encodeURIComponent(title.trim())  +
+        '&artist=' + encodeURIComponent(artist.trim());
+      console.info(`onecold requestUrl = `+requestUrl);
+      const httpRequest = http.createHttp();
+      const options: http.HttpRequestOptions = {
+        method: http.RequestMethod.GET,
+        readTimeout: 3000,
+        connectTimeout: 3000,
+      };
+
+      const response: http.HttpResponse = await httpRequest.request(requestUrl,  options);
+      console.info(`onecold requestUrl 00= `+response.responseCode);
+      if (response.responseCode  === 200) {
+        let res = response.result  as string;
+        let fileContent = '';
+
+        if (isApi2) {
+          fileContent = res;
+          console.info(`onecold fileContent 1111= `+fileContent);
+        } else {
+          console.info(`onecold fileContent 22= `+fileContent);
+          let parsedData: lyricInfo[] = JSON.parse(res)  as lyricInfo[];
+          fileContent = parsedData[0].lyrics || '';
+        }
+
+        LogUtil.debug("onecold  fileContent 11 =" + fileContent);
+        resolve(fileContent);
+        return;
+      }
+
+      console.log('onecold  getLyric--失败', JSON.stringify(response));
+      resolve('');
+    } catch (error) {
+      console.error('onecold  getLyric catch--失败' + JSON.stringify(error));
+      resolve('');
+    }
+  });
+}
+
+
+interface lyricInfo{
+  code:number
+  album:number
+  artist:string
+  lyrics:string
+  cover_url:string
+  status:string
+}

+ 25 - 12
entry/src/main/ets/common/util/RdbUtils.ets

@@ -75,13 +75,21 @@ export default class RdbUtils {
       '        channel_layout TEXT,\n' +
       '        start_time TEXT,\n' +
 
+      '        ALBUMARTIST TEXT,\n' +
+      '        COMPOSER TEXT,\n' +
+      '        LYRICIST TEXT,\n' +
+      '        COMMENT TEXT,\n' +
+      '        disc TEXT,\n' +
+
       '        mimeType TEXT' +
       ')',
     columns: ['id', 'name', 'filePath','mtype', 'videoSize', 'cTime','size', 'artist', 'album',
       'fileName','parentPath','isFav','pixelMapPath','pixelMapToString',
       'duration',   'sampleRate','playCount',  'lastPlayedStr', 'trackCount',
       'lyricContent','md5Str','extra_json','pyStr','bit_rate','probe_score','year','nb_streams','nb_programs',
-      'genre','track',  'bits_per_raw_sample','channels',  'channel_layout','start_time',  'mimeType']
+      'genre','track',  'bits_per_raw_sample','channels',  'channel_layout','start_time',
+      'ALBUMARTIST',  'COMPOSER','LYRICIST','COMMENT','COMMENT','disc',
+      'mimeType']
   };
 
   constructor(tableName: string, sqlCreateTable: string, columns: Array<string>) {
@@ -103,22 +111,22 @@ export default class RdbUtils {
       callback();
       return
     }
-    
+
     relationalStore.getRdbStore(context, RdbUtils.STORE_CONFIG, (err, rdb) => {
       if (err) {
         Logger.error(RdbUtils.RDB_TAG, `gerRdbStore() failed, err: ${err}`);
         return;
       }
       this.rdbStore = rdb;
-      
+
       // 先创建表(如果不存在)
       this.rdbStore.executeSql(this.sqlCreateTable);
-      
+
       // 检查数据库版本并更新列
       try {
         // 检查表结构
         this.checkAndUpdateTableColumns();
-        Logger.info(RdbUtils.RDB_TAG, `数据库表结构检查完成`);
+        // Logger.info(RdbUtils.RDB_TAG, `数据库表结构检查完成`);
       } catch (e) {
         Logger.error(RdbUtils.RDB_TAG, `检查表结构时出错: ${e.message}`);
       }
@@ -126,7 +134,7 @@ export default class RdbUtils {
       callback();
     });
   }
-  
+
   /**
    * 检查并更新表列结构
    * 使用表信息查询和ALTER TABLE添加缺失的列
@@ -136,7 +144,7 @@ export default class RdbUtils {
       Logger.error(RdbUtils.RDB_TAG, `数据库连接未初始化`);
       return;
     }
-    
+
     try {
       // 查询表信息获取现有列
       const tableInfoQuery = `PRAGMA table_info(${this.tableName})`;
@@ -169,8 +177,13 @@ export default class RdbUtils {
             'channel_layout': 'TEXT',
             'start_time': 'TEXT',
 
+            'ALBUMARTIST': 'TEXT',
+            'COMPOSER': 'TEXT',
+            'LYRICIST': 'TEXT',
+            'COMMENT': 'TEXT',
+            'disc': 'TEXT',
           };
-          
+
           // 逐个添加列,不依赖于检查结果
           if (this.rdbStore) {
             // 遍历映射
@@ -178,7 +191,7 @@ export default class RdbUtils {
             for (let i = 0; i < columnEntries.length; i++) {
               const column = columnEntries[i];
               const type = requiredColumns[column];
-              
+
               const alterSql = `ALTER TABLE ${this.tableName} ADD COLUMN ${column} ${type}`;
               try {
                 // 对每个列使用Promise模式
@@ -188,17 +201,17 @@ export default class RdbUtils {
                   })
                   .catch((alterErr: Error) => {
                     // 列可能已经存在,这是预期的错误
-                    Logger.info(RdbUtils.RDB_TAG, `列 ${column} 可能已存在: ${alterErr.message}`);
+                    // Logger.info(RdbUtils.RDB_TAG, `列 ${column} 可能已存在: ${alterErr.message}`);
                   });
               } catch (e) {
                 Logger.error(RdbUtils.RDB_TAG, `添加列 ${column} 出错: ${e.message}`);
               }
             }
-            
+
             // 设置数据库版本
             if (this.rdbStore) {
               this.rdbStore.version = 1;
-              Logger.info(RdbUtils.RDB_TAG, `数据库升级完成,版本设置为 1`);
+              // Logger.info(RdbUtils.RDB_TAG, `数据库升级完成,版本设置为 1`);
             }
           }
         })

+ 55 - 15
entry/src/main/ets/common/util/Utility.ets

@@ -26,7 +26,7 @@ import { VipPage } from '../../pages/VipPage';
 import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
 import ImageUtils from './ImageUtils';
 
-interface FFMpegTags {
+export interface FFMpegTags {
   album?: string;
   ALBUM?: string;
   artist?: string;
@@ -46,6 +46,19 @@ interface FFMpegTags {
   USLT?: string;         // ID3v2同步歌词
   UNSYNCEDLYRICS?: string; // ID3v2非同步歌词
   // Add any other tag properties you expect
+  ALBUMARTIST?:string//专辑艺术家
+  COMPOSER?:string//作曲家
+  LYRICIST?:string//作词家
+  COMMENT?:string//注释
+  albumartist?:string//专辑艺术家
+  TPE2?:string//专辑艺术家
+  composer?:string//作曲家
+  lyricist?:string//作词家
+  TEXT?:string//作词家
+  comment?:string//注释
+  COMM?:string//注释
+  comm?:string//注释
+  disc?:string//音碟号
 }
 
 interface FFprobeFormat {
@@ -899,10 +912,35 @@ export class Utility {
             videoItem.probe_score  = format.probe_score;
             videoItem.nb_streams  = format.nb_streams;
             videoItem.nb_programs  = format.nb_programs;
-            videoItem.year  = tags.TYER || tags.date ||tags.DATE|| Utility.resourceToString(context, $r('app.string.unknown')); // try different tag names for year
+            videoItem.year  = tags.TYER || tags.date ||tags.DATE|| ''; // try different tag names for year
             videoItem.lyricContent  = tags.LYRICS || tags.lyrics  ||  tags.USLT || tags.UNSYNCEDLYRICS || '';
-            videoItem.genre  = tags.genre||tags.GENRE|| Utility.resourceToString(context, $r('app.string.unknown'));
-            videoItem.track  = tags.track||tags.TRACK|| Utility.resourceToString(context, $r('app.string.unknown'));
+            //console.info('readMetaInfoFFmpeg   videoItem.lyricContent: ',  videoItem.lyricContent);
+            // 如果标准字段没有歌词,则尝试解析 `lyrics-` 字段
+            // tags 必须是 Record<string, string | undefined>(或 any)
+            // 如果还是没有歌词内容,尝试查找自定义的lyrics-开头的属性(电脑版音乐标签内嵌歌词就是lyrics-XXX)
+            const tagsRecord = tags as Record<string, string>;
+            const possibleKeys = Object.keys(tagsRecord);
+            for (let i = 0; i < possibleKeys.length;  i++) {
+              const key = possibleKeys[i];
+              if (key && key.toLowerCase().startsWith('lyrics-'))  {
+                //console.info('readMetaInfoFFmpeg  videoItem.key:  ', key);
+                // 通过转换后的 Record 类型安全访问属性
+                videoItem.lyricContent  = tagsRecord[key];
+                //console.info('readMetaInfoFFmpeg  videoItem.lyricContent2:  ', videoItem.lyricContent);
+                if (videoItem.lyricContent)  {
+                  break;
+                }
+              }
+            }
+
+            videoItem.genre  = tags.genre||tags.GENRE|| '';
+            videoItem.track  = tags.track||tags.TRACK|| '';
+
+            videoItem.ALBUMARTIST  = tags.ALBUMARTIST||tags.albumartist||tags.TPE2|| '';
+            videoItem.COMPOSER  = tags.COMPOSER||tags.composer|| '';
+            videoItem.LYRICIST  = tags.LYRICIST||tags.lyricist||tags.TEXT ||'';
+            videoItem.COMMENT  = tags.COMMENT||tags.comment||tags.COMM|| '';
+            videoItem.disc  = tags.disc|| '';
 
             videoItem.bits_per_raw_sample = bits_per_raw_sample ||'1'
             videoItem.channel_layout = channel_layout || ''
@@ -921,14 +959,14 @@ export class Utility {
             // 如果有封面图片,则提取
             try {
               if (hasCover) {
-                  //提取封面
-                  let isSuccess:boolean= await getFFmpegCover(inputPath, imagePath);
-
-                  if(isSuccess){
-                    imagePath = fileUri.getUriFromPath(imagePath)
-                  }else{
-                    imagePath = ''
-                  }
+                //提取封面
+                let isSuccess:boolean= await getFFmpegCover(inputPath, imagePath);
+
+                if(isSuccess){
+                  imagePath = fileUri.getUriFromPath(imagePath)
+                }else{
+                  imagePath = ''
+                }
                 videoItem.pixelMapPath  = imagePath;
               }else if(Utility.isVideoByExtension(inputPath)){
                 pixelMap = await Utility.getFetchFrameByTime(inputPath)
@@ -1078,7 +1116,7 @@ export class Utility {
     for(let i=0;i<localList.length;i++){
       let pix = localList[i].pixelMapPath
       if(pix){
-       return pix
+        return pix
       }
 
     }
@@ -1465,7 +1503,7 @@ function fetchAlbumCover(avMetadataExtractor: media.AVMetadataExtractor): Promis
       } else {
         resolve(pixelMap);
       }
-       avMetadataExtractor.release();
+      avMetadataExtractor.release();
     });
   });
 }
@@ -1951,4 +1989,6 @@ function getFileFormatByPath(filePath: string): string {
 
   // 返回点号后的部分(转换为小写)
   return filePath.substring(lastDotIndex  + 1).toLowerCase();
-}
+}
+
+

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 539 - 144
entry/src/main/ets/view/LocalMusic.ets


+ 5 - 1
entry/src/main/ets/viewmodel/VideoItem.ets

@@ -62,7 +62,11 @@ export class VideoItem  {
   channel_layout?:string//声道布局:stereo为标准立体声(左+右)
   start_time?:string//起始时间:
 
-
+  ALBUMARTIST?:string//专辑艺术家
+  COMPOSER?:string//作曲家
+  LYRICIST?:string//作词家
+  COMMENT?:string//注释
+  disc?:string//碟号
 
   constructor(name: string, id: string, filePath: string, type:number,videoSize:number,cTime: string,pixelMap?: image.PixelMap,
     size?: string,pixelMapPath?: string,artist?: string,album?: string,fileName?: string, lastPlayed?:Date) {

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است