Răsfoiți Sursa

内嵌音乐标签初步实现

onecold 1 an în urmă
părinte
comite
84c8770abb

+ 15 - 2
entry/src/main/ets/common/util/MediaTable.ets

@@ -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,8 @@ 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,callback: Function) {
     if (!callback || typeof callback !== 'function') {
       Logger.info(RdbUtils.RDB_TAG, 'updateMediaInfo() has no valid callback!');
       return;
@@ -169,6 +170,18 @@ 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;
+      }
 
       resultSet.close();
 

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

@@ -0,0 +1,442 @@
+/*
+ * 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,
+  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}`);
+    }
+  });
+
+  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
+}

+ 1 - 1
entry/src/main/ets/common/util/Utility.ets

@@ -25,7 +25,7 @@ import { VipData } from '../../viewmodel/VipData';
 import { VipPage } from '../../pages/VipPage';
 import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
 
-interface FFMpegTags {
+export interface FFMpegTags {
   album?: string;
   ALBUM?: string;
   artist?: string;

+ 301 - 138
entry/src/main/ets/view/LocalMusic.ets

@@ -21,7 +21,6 @@ import { unifiedDataChannel, uniformDataStruct, uniformTypeDescriptor } from '@k
 import { CommonConstants } from '../common/constants/CommonConstants';
 import Logger from '../common/util/Logger';
 import { photoAccessHelper } from '@kit.MediaLibraryKit';
-import { Utility } from '../common/util/Utility';
 import { BusinessError, emitter } from '@kit.BasicServicesKit';
 import { BubbleBean } from '../viewmodel/BubbleBean';
 import { PopupPosition, XPopup } from '@chinalike/popup';
@@ -36,6 +35,8 @@ import { effectKit } from '@kit.ArkGraphics2D';
 import { ColorConversion } from '../common/util/ColorConversion';
 import { LyricController, LyricParser, LyricView2 } from '@seagazer/cclyric';
 import { AvSessionController } from '../controller/AvSessionController';
+import { repairAudioMetadata,  getApiLyric,changeMusicCover} from '../common/util/MusicTagUtils';
+import { FFMpegTags,Utility } from '../common/util/Utility';
 import {
   // DeviceChangeReason,
   IjkMediaPlayer,
@@ -1583,58 +1584,57 @@ export struct LocalMusic {
   //
   // }
 
-  goSelectImage(item: VideoItem) {
+  async goSelectImage(item: VideoItem): Promise<string | undefined> {
     if (!item) {
-      return
+      return undefined;
     }
-    let selectUris: Array<string> = [];
-    let photoPicker = new photoAccessHelper.PhotoViewPicker();
-    let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
-    photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 过滤选择媒体文件类型为IMAGE
-    photoSelectOptions.maxSelectNumber = 1; // 选择媒体文件的最大数目
 
-    photoPicker.select(photoSelectOptions).then(async (photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
+    try {
+      let selectUris: Array<string> = [];
+      let photoPicker = new photoAccessHelper.PhotoViewPicker();
+      let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
+      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
+      photoSelectOptions.maxSelectNumber  = 1;
 
-      //用一个全局变量存储返回的uri
+      const photoSelectResult = await photoPicker.select(photoSelectOptions);
       selectUris = photoSelectResult.photoUris;
-      console.info('photoViewPicker.select to file succeed and uris are:' + selectUris);
-      //使用fs.openSync接口,通过uri打开这个文件得到fd
-      let file = fs.openSync(selectUris[0], fs.OpenMode.READ_ONLY);
-      console.info('file fd: ' + file.fd);
-      let name = await MD5.digestSync(this.videoUrl)
-      let imagePath = this.context.filesDir + FileUtil.separator + name
+      console.info('photoViewPicker.select  to file succeed and uris are:' + selectUris);
+
+      let file = fs.openSync(selectUris[0],  fs.OpenMode.READ_ONLY);
+      console.info('file  fd: ' + file.fd);
+
+      let name = await MD5.digestSync(this.videoUrl)+'.jpg'
+      let imagePath = this.context.filesDir  + FileUtil.separator  + name
       imagePath = fileUri.getUriFromPath(imagePath)
-      let file2 = fileIo.openSync(imagePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE)
-      fileIo.copyFileSync(file.fd, file2.fd)
+
+      let file2 = fileIo.openSync(imagePath,  fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE)
+      fileIo.copyFileSync(file.fd,  file2.fd)
       fileIo.closeSync(file);
       fileIo.closeSync(file2);
 
-      if (item.filePath == this.currentSong?.filePath) {
-        this.cover = imagePath
+      if (item.filePath  == this.currentSong?.filePath)  {
+        this.cover  = imagePath
       }
       if (item) {
-        item.pixelMapPath = imagePath
+        item.pixelMapPath  = imagePath
       }
 
-      this.table.updatePixelMapPath(item.filePath, imagePath, (success: boolean, error?: string) => {
+      this.table.updatePixelMapPath(item.filePath,  imagePath, (success: boolean, error?: string) => {
         if (success) {
           this.doUpdateData()
-
-          console.log(" onecold 更新音乐封面成功,数据库已同步");
-
+          console.log("onecold  更新音乐封面成功,数据库已同步");
         } else {
-          console.error(" onecold 更新音乐封面数据库失败原因: " + error);
+          console.error("onecold  更新音乐封面数据库失败原因: " + error);
         }
       });
-      LogUtil.debug("onecold this.cover =" + this.cover)
-
-    }).catch((err: BusinessError) => {
-
-      console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
-
-    })
 
+      LogUtil.debug("onecold  this.cover  =" + this.cover)
+      return FileUtil.getFilePath(imagePath); // Return the image path here
 
+    } catch (err) {
+      console.error(`Invoke  photoViewPicker.select  failed, code is ${err.code},  message is ${err.message}`);
+      return undefined;
+    }
   }
 
   // 拉起picker选择文件管理器
@@ -3667,7 +3667,7 @@ export struct LocalMusic {
               bottomRight: 0
             })
             .bindSheet(this.longItemFilePath== item.filePath, this.editSheet(item), {
-              height: this.isCoverOpacity() ? '95%' : '88%',
+              height:  '99%' ,
               dragBar: true,
               onDisappear: () => {
                 this.longItemFilePath = '';
@@ -3675,7 +3675,7 @@ export struct LocalMusic {
               },
               showClose: true,
               preferType: SheetType.CENTER ,
-              title: { title: '编辑信息' }
+              title: { title: '编辑标签' }
             })
             .draggable(false)
             .opacity(this.opacityItem)// 绑定透明度
@@ -3901,7 +3901,7 @@ export struct LocalMusic {
   MenuBuilder(item: VideoItem, index: number, filePath: string) {
     if(
       ((this.modeType===2||this.modeType==3)&&!this.isCanBack)
-      ||(item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO ||
+        ||(item.name === LocalMusic.STR_LOCK_VIDEO || item.name === LocalMusic.STR_FAC_VIDEO ||
         item.name === LocalMusic.STR_HISTORY_MUSIC)
     ){
       //如果是专辑和艺术家的首页,不能长按
@@ -3937,17 +3937,18 @@ export struct LocalMusic {
 
             MenuItem({
               symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.square_and_pencil')),
-              content: '编辑信息'
+              content: '编辑标签'
             })
               .bindSheet($$this.isShowEdit, this.editSheet(item), {
-                height: this.isCoverOpacity() ? '95%' : '88%',
+                height: '99%',
                 dragBar: true,
                 showClose: true,
                 preferType: SheetType.CENTER ,
-                title: { title: '编辑信息' }
+                title: { title: '编辑标签' }
               })
               .visibility(item.type === CommonConstants.TYPE_IS_DIR ? Visibility.None : Visibility.Visible)
               .onClick(async() => {
+                this.setEditStrEmpty()
                 this.tempLyricContent = await this.getLyricContent(item);
                 // console.info('onecold 长按this.tempLyricContent' +this.tempLyricContent)
                 if(this.isGridMusic){
@@ -4782,9 +4783,14 @@ export struct LocalMusic {
   @State artistStr: string = ''
   @State lyricConStr: string = ''
   @State currentEditItem: string = ''
+  @State yearStr: string  = ''
+  @State trackStr: string  = ''
+  @State genreStr: string  = ''
+  @State imagePathStr: string  = ''
 
   doUpdateData() {
     setTimeout(() => {
+
       if (this.modeType === 0) {
         this.deleteCache(this.currentPath)
         this.getSortedFiles(this.currentPath)
@@ -4796,7 +4802,7 @@ export struct LocalMusic {
   }
 
   //编辑信息
-  doEdit(item: VideoItem) {
+  async doEdit(item: VideoItem) {
     if (!item) {
       return
     }
@@ -4805,64 +4811,87 @@ export struct LocalMusic {
       return
     }
 
-    this.table.updateMediaInfo(item.filePath, this.titleStr, this.artistStr, this.ablumStr,
-      (success: boolean, error?: string) => {
-        // this.isShowEdit = false
-        if(this.isGridMusic){
-          this.longItemFilePath =''
-        }else{
-          this.isShowEdit = false
-        }
-        if (success) {
-          console.log(" onecold 编辑信息成功,数据库已同步");
-          ToastUtil.showToast('保存成功')
-          this.isShowMoreView = false
-          this.name = this.titleStr
-          if (item.filePath == this.currentSong?.filePath) {
-            this.currentSong.name = this.titleStr
-            this.artist = this.artistStr
-            this.currentSong.artist = this.artistStr
-            this.currentSong.album = this.ablumStr
-          }
-
-          this.doUpdateData()
-
-        } else {
-          ToastUtil.showToast('保存失败' + error?.toString())
-          console.error(" onecold  编辑信息数据库失败原因: " + error);
-        }
-
-
-      });
     let doChangeLyric = false
-    if (item.filePath == this.currentSong?.filePath) {
-      if (this.lyricConStr !== this.lyricContent) {
+    if(item.filePath==this.currentSong?.filePath){
+      if(this.lyricConStr !== this.lyricContent){
         doChangeLyric = true
       }
-    } else if (this.lyricConStr !== this.tempLyricContent) {
+    }else if(this.lyricConStr !== this.tempLyricContent){
       doChangeLyric = true
     }
 
-    if (doChangeLyric) {
-      let lyricPath = item.filePath.substring(0, this.videoUrl.lastIndexOf('.')) + '.lrc'
-      let jiaMilyricPath = lyricPath.substring(0, lyricPath.lastIndexOf('.')) + '.lrcc'
+    //用户更改了封面,那就内嵌下封面
+    if(StrUtil.isNotEmpty(this.imagePathStr)){
+      const resultCover:boolean = await changeMusicCover(this.context,
+        item.filePath,this.imagePathStr,true);
+      this.imagePathStr = ''
+      if(resultCover){
+        ToastUtil.showToast('内嵌封面成功')
+      }
 
-      let isJiaMi = false;
-      let realLyricPath = lyricPath
-      if (FileUtil.accessSync(jiaMilyricPath)) {
-        isJiaMi = true
-        realLyricPath = jiaMilyricPath
-        console.info("onecold doEdit找到加密本地歌词 ");
-      } else {
-        isJiaMi = false
-        realLyricPath = lyricPath
+    }
+
+    const metadata:FFMpegTags = {
+      title: this.titleStr,
+      artist: this.artistStr,
+      album: this.ablumStr,
+      lyrics:this.lyricConStr,
+      TYER:this.yearStr,
+      genre:this.genreStr,
+      track:this.trackStr,
+    };
+
+    //内嵌下标签的值,设置overwrite为true会直接修改原文件
+    const result:boolean = await repairAudioMetadata(
+      item.filePath,
+      metadata,
+      true
+    );
+
+    if (result) {
+      this.table.updateMediaInfo(item.filePath, this.titleStr, this.artistStr, this.ablumStr,this.lyricConStr,
+        this.yearStr,this.genreStr,this.trackStr,
+        (success: boolean, error?: string) => {
+          // this.isShowEdit = false
+          if(this.isGridMusic){
+            this.longItemFilePath =''
+          }else{
+            this.isShowEdit = false
+          }
+          if (success) {
+            console.log(" onecold 编辑信息成功,数据库已同步");
+            ToastUtil.showToast('内嵌音乐标签成功')
+            this.isShowMoreView = false
+            this.name = this.titleStr
+            if (item.filePath == this.currentSong?.filePath) {
+              this.currentSong.name = this.titleStr
+              this.artist = this.artistStr
+              this.currentSong.artist = this.artistStr
+              this.currentSong.album = this.ablumStr
+              this.currentSong.year = this.yearStr
+              this.currentSong.genre = this.genreStr
+              this.currentSong.track = this.trackStr
+            }
+
+            this.doUpdateData()
+          } else {
+            // ToastUtil.showToast('保存失败' + error?.toString())
+            console.error(" onecold  编辑信息数据库失败原因: " + error);
+          }
 
-        console.info("onecold doEdit本地歌词 ");
-      }
-      this.saveDataToFile(this.lyricConStr, realLyricPath, isJiaMi)
 
+        });
+      console.log(' onecold 元数据标签更新成功,');
+
+
+    } else {
+      console.log('onecold 元数据更新失败');
     }
 
+
+
+
+
   }
 
   updateLyricToDB() {
@@ -4880,6 +4909,8 @@ export struct LocalMusic {
     .height('100%')
   }
 
+
+
   @Builder
   editDetail(item: VideoItem) {
     Column() {
@@ -4887,17 +4918,27 @@ export struct LocalMusic {
         Row() {
           Stack() {
 
-            Image(StrUtil.isEmpty(item.pixelMapPath) ? $r('app.media.music_red') :
-            item.pixelMapPath)
+            Image(StrUtil.isNotEmpty(this.imagePathStr)?this.imagePathStr:
+              StrUtil.isEmpty(item.pixelMapPath) ? $r('app.media.music_red') :
+              item.pixelMapPath)
               .fillColor(this.themeColor)
-              .height(33)
-              .width(33)
+              .height(55)
+              .width(55)
               .borderRadius('100%')
               .clip(true)
               .opacity(this.opacityItem)// 绑定透明度
-              .margin({ left: 20 })
+              .margin({ left: 25 })
           }
-          .width('15%')
+          .onClick(async () => {
+
+            const imagePath:string | undefined = await this.goSelectImage(item);
+            if (imagePath) {
+              //用户更改过图片
+              this.imagePathStr = imagePath
+            }
+
+          })
+          .width('18%')
 
           Row() {
             Column() {
@@ -4911,7 +4952,7 @@ export struct LocalMusic {
                 })
 
                 .fontColor(this.themeColor)
-                .margin({ left: 10 })
+                .margin({ left: 18 })
               Row() {
                 Text(item.artist)
                   .fontSize(11)
@@ -4919,7 +4960,7 @@ export struct LocalMusic {
                   .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
                   .padding({ top: 8 })
                   .fontColor(this.themeColor)
-                  .margin({ left: 10 })
+                  .margin({ left: 18 })
 
               }
 
@@ -4930,7 +4971,7 @@ export struct LocalMusic {
             .justifyContent(FlexAlign.Center)
             .alignItems(HorizontalAlign.Start)
 
-            Button('本地封面')
+            Button('获取封面')
               .fontColor(Color.White)
               .fontSize(12)
               .height(38)
@@ -4939,12 +4980,25 @@ export struct LocalMusic {
               .stateEffect(true)
               .margin({ right: 5, left: 12 })
               .onClick(async () => {
+                if (PreferencesUtil.getStringSync('COVER_API',  '') === '') {
+                  this.isShowMoreView  = false;
+                  this.showTipsDialog();
+                  return; // Return empty string when no API is configured
+                }
+
+                const imagePath:string | undefined = await this.doSearchCover(item);
+                console.info('onecold imagePath = '+imagePath)
+                if (imagePath) {
+                  //用户更改过图片
+                  this.imagePathStr = imagePath
+                }else{
+                  ToastUtil.showToast('获取封面失败,请重试')
+                }
 
-                this.goSelectImage(item)
 
               })
 
-            Button('获取封面')
+            Button('获取歌词')
               .fontColor(Color.White)
               .fontSize(12)
               .height(38)
@@ -4953,7 +5007,29 @@ export struct LocalMusic {
               .stateEffect(true)
               .margin({ right: 12, left: 5 })
               .onClick(async () => {
-                this.doSearchCover(item)
+                //判断有没有设置api
+                let apiUrl = PreferencesUtil.getStringSync('LRC_API', '')
+                if (StrUtil.isEmpty(apiUrl)) {
+                  this.isLyricSetting = false
+                  this.showTipsDialog()
+                  return
+                }
+                let artist = item.artist
+                let title = item.name
+                if(!artist)
+                  artist = ''
+                if(!title)
+                  title = ''
+                let res: string | undefined = await getApiLyric(apiUrl,title, artist, false);
+                if (res && StrUtil.isNotEmpty(res) && res !== 'unknown' && res !== 'Timeout was reached') {
+                  this.lyricConStr = res;
+                }else{
+                  ToastUtil.showToast('获取歌词失败,请重试')
+                }
+
+
+                // let res : string | undefined = await getApiLyric(title, artist, false);
+
 
               })
 
@@ -5029,12 +5105,72 @@ export struct LocalMusic {
         .margin({ top: 10, bottom: 10 })
         .justifyContent(FlexAlign.Start)
 
+        Row() {
+          Text('年份:')
+            .fontSize(14)
+            .fontColor($r('app.color.text_color'))
+            .margin({ left: 22 })
+          TextInput({ text: item.year })
+            .height(40)
+            .maxLines(1)
+            .fontSize(14)
+            .layoutWeight(1)
+            .fontColor($r('app.color.text_color'))
+            .margin({ right: 20, left: 10 })
+            .onChange((val: string) => {
+              this.yearStr = val
+            })
+        }
+        .width('100%')
+        .margin({ top: 10, bottom: 10 })
+        .justifyContent(FlexAlign.Start)
+        Row() {
+          Text('音轨号:')
+            .fontSize(14)
+            .fontColor($r('app.color.text_color'))
+            .margin({ left: 22 })
+          TextInput({ text:item.track=='未知'?'': item.track })
+            .height(40)
+            .maxLines(1)
+            .fontSize(14)
+            .layoutWeight(1)
+            .fontColor($r('app.color.text_color'))
+            .margin({ right: 20, left: 10 })
+            .onChange((val: string) => {
+              this.trackStr = val
+            })
+        }
+        .width('100%')
+        .margin({ top: 10, bottom: 10 })
+        .justifyContent(FlexAlign.Start)
+
+        Row() {
+          Text('风格:')
+            .fontSize(14)
+            .fontColor($r('app.color.text_color'))
+            .margin({ left: 22 })
+          TextInput({ text: item.genre })
+            .height(40)
+            .maxLines(1)
+            .fontSize(14)
+            .layoutWeight(1)
+            .fontColor($r('app.color.text_color'))
+            .margin({ right: 20, left: 10 })
+            .onChange((val: string) => {
+              this.genreStr = val
+            })
+        }
+        .width('100%')
+        .margin({ top: 10, bottom: 10 })
+        .justifyContent(FlexAlign.Start)
+
         Row() {
           Text('歌词:')
             .fontSize(14)
             .fontColor($r('app.color.text_color'))
             .margin({ left: 22 })
-          TextArea({ text: item.filePath === this.currentSong?.filePath ? this.lyricContent : this.tempLyricContent })
+          TextArea({ text: StrUtil.isNotEmpty(this.lyricConStr)?this.lyricConStr:
+            item.filePath === this.currentSong?.filePath ? this.lyricContent : this.tempLyricContent })
             .type(TextAreaType.NORMAL)
             .height(275)
             .fontSize(14)
@@ -5062,6 +5198,7 @@ export struct LocalMusic {
             .margin({ right: 20, bottom: 20 })
             .onClick(async () => {
               this.doEdit(item)
+
             })
 
           Button('取消')
@@ -5073,12 +5210,13 @@ export struct LocalMusic {
             .stateEffect(true)
             .margin({ left: 20, bottom: 20 })
             .onClick(async () => {
-
               if(this.isGridMusic){
                 this.longItemFilePath =''
               }else{
                 this.isShowEdit = false
               }
+              this.setEditStrEmpty()
+
             })
         }
         .margin({
@@ -5096,6 +5234,17 @@ export struct LocalMusic {
     .margin({ bottom: 20 })
   }
 
+  setEditStrEmpty(){
+    this.imagePathStr = ''
+    this.lyricConStr = ''
+    this.artistStr = ''
+    this.titleStr = ''
+    this.ablumStr = ''
+    this.genreStr = ''
+    this.yearStr = ''
+    this.trackStr = ''
+  }
+
   @Builder
   detailSheet(item:VideoItem) {
     Scroll() {
@@ -8674,7 +8823,7 @@ export struct LocalMusic {
           this.callFilePickerSelectFileForLyric()
           break;
         case 1:
-          //判断是不是赞助会员
+          //判断有没有设置api
           if (PreferencesUtil.getStringSync('LRC_API', '') === '') {
             this.isLyricSetting = false
             this.showTipsDialog()
@@ -8801,8 +8950,18 @@ export struct LocalMusic {
           .margin({ left: 6 })
           .onClick(() => {
             DialogHelper.closeDialog('tips'); //关闭弹框
+            this.isShowEdit = false
+            this.longItemFilePath = ''
+            // if(this.isGridMusic&&!this.isShowPlay){
+            //   this.longItemFilePath== ''
+            // }else{
+            //   this.isShowEdit = false
+            // }
+
             this.isShowPlay = false
             this.mType = 3
+
+
             // router.pushUrl({
             //   url: 'pages/SettingPage'
             // }, router.RouterMode.Single);
@@ -8849,7 +9008,7 @@ export struct LocalMusic {
                     backgroundColor: Color.Transparent,
                     title: { title: '歌曲信息' }
                   })
-              } else if (more.id === 15 && this.currentSong) { //编辑信息
+              } else if (more.id === 15 && this.currentSong) { //编辑标签
                 Image(more.image)
                   .width(24)
                   .height(24)
@@ -8858,13 +9017,13 @@ export struct LocalMusic {
                   .fontSize(16)
                   .fontColor(Color.White)
                   .bindSheet($$this.isShowEdit, this.editSheet(this.currentSong), {
-                    height: this.isCoverOpacity() ? '95%' : '93%',
+                    height:  '99%',
                     dragBar: true,
                     showClose: true,
                     preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM,
                     // blurStyle:BlurStyle.Thin,
                     // backgroundColor:Color.Transparent,
-                    title: { title: '编辑信息' }
+                    title: { title: '编辑标签' }
                   })
               } else if (more.id === 5) { //定时关闭
                 Image(more.image)
@@ -9140,7 +9299,7 @@ export struct LocalMusic {
 
     { id: 14, image: $r('app.media.cover_online'), title: '获取封面' },
 
-    { id: 15, image: $r('app.media.rename2'), title: '编辑信息' },
+    { id: 15, image: $r('app.media.rename2'), title: '编辑标签' },
 
     { id: 5, image: $r('app.media.time_close'), title: '定时关闭' },
 
@@ -9230,7 +9389,8 @@ export struct LocalMusic {
         }
         this.isShowMoreView = false
         break;
-      case 15: //编辑信息
+      case 15: //编辑标签
+        this.setEditStrEmpty()
         this.isShowEdit = !this.isShowEdit;
         break;
       case 16: //悬浮歌词
@@ -9393,50 +9553,53 @@ export struct LocalMusic {
    *
    */
 
-  doSearchCover(item: VideoItem) {
-    if (PreferencesUtil.getStringSync('COVER_API', '') === '') {
-      this.isShowMoreView = false
-      this.showTipsDialog()
-      return
-    }
-    if (item !== undefined) {
-      let artist = item?.artist
-      if (StrUtil.isEmpty(artist) || artist === undefined) {
-        artist = ''
-      }
-      this.searchCover(item, item.name, artist)
+  async doSearchCover(item: VideoItem): Promise<string> {
+    if (PreferencesUtil.getStringSync('COVER_API',  '') === '') {
+      this.isShowMoreView  = false;
+      this.showTipsDialog();
+      return ''; // Return empty string when no API is configured
     }
-  }
-
-  searchCover(item: VideoItem, title: string, artist: string) {
 
+    if (!item) {
+      return ''; // Return empty string if item is undefined
+    }
 
-    NetAxiosUtil.getLyricCover(title, artist, PreferencesUtil.getStringSync('COVER_API', '')).then(async (res) => {
+    const artist = item?.artist || ''; // Use empty string if artist is missing
+    return this.searchCover(item,  item.name,  artist); // Await is not needed here as we return the promise directly
+  }
 
-      LogUtil.debug("onecold res =" + res)
-      if (StrUtil.isNotEmpty(res) && res !== 'unknown' && res !== 'Timeout was reached') {
-        if (item.filePath == this.currentSong?.filePath) {
-          this.cover = res
-          if (this.currentSong) {
-            this.currentSong.pixelMapPath = res
+  /**
+   *api搜索封面
+   *item,根据title和artist
+   */
+  searchCover(item: VideoItem, title: string, artist: string): Promise<string> {
+    return NetAxiosUtil.getLyricCover(title,  artist, PreferencesUtil.getStringSync('COVER_API',  '')).then(async (res) => {
+      LogUtil.debug("onecold  res =" + res);
+
+      if (StrUtil.isNotEmpty(res)  && res !== 'unknown' && res !== 'Timeout was reached') {
+        if (item.filePath  == this.currentSong?.filePath)  {
+          this.cover  = res;
+          if (this.currentSong)  {
+            this.currentSong.pixelMapPath  = res;
           }
         }
 
-
-        this.table.updatePixelMapPath(item.filePath, res, (success: boolean, error?: string) => {
+        // Update pixel map path but always return res regardless of success
+        this.table.updatePixelMapPath(item.filePath,  res, (success: boolean, error?: string) => {
           if (success) {
-            this.doUpdateData()
-            console.log(" onecold 更新音乐封面成功,数据库已同步");
-
+            this.doUpdateData();
+            console.log("onecold  更新音乐封面成功,数据库已同步");
           } else {
-            console.error(" onecold 更新音乐封面数据库失败原因: " + error);
+            console.error("onecold  更新音乐封面数据库失败原因: " + error);
           }
+          // Note: We don't resolve/reject here because we already returned res
         });
-        LogUtil.debug("onecold this.cover =" + this.cover)
-      }
 
+        LogUtil.debug("onecold  this.cover  =" + this.cover);
+      }
 
-    })
+      return res; // Always return res regardless of updatePixelMapPath result
+    });
   }
 
   @State jumpTopTime: number = 0