Bladeren bron

WebDavMain的类型网盘 音质的判断,网盘封面的更改保存

onecold 5 maanden geleden
bovenliggende
commit
74fcf17c95

+ 45 - 4
entry/src/main/ets/common/util/MediaTable.ets

@@ -21,6 +21,7 @@ interface DBColumnsInterface {
   PARENT_PATH: string;
   IS_FAV: string;
   PIXEL_MAP_PATH: string;
+  IS_CUSTOM_COVER: string;
   ARTIST: string;
   ALBUM: string;
   FILE_NAME: string;
@@ -49,6 +50,7 @@ const DB_COLUMNS: DBColumnsInterface = {
   PARENT_PATH: 'parentPath',
   IS_FAV: 'isFav',
   PIXEL_MAP_PATH: 'pixelMapPath',
+  IS_CUSTOM_COVER: 'isCustomCover',
   ARTIST: 'artist',
   ALBUM: 'album',
   FILE_NAME: 'fileName',
@@ -222,6 +224,11 @@ export default  class MediaTable {
       }
       const exists = await this.existsByFilePath(item.filePath);
       if (exists) {
+        const cached = await this.queryVideoByFilePath(item.filePath);
+        if (cached && cached.isCustomCover === 1 && item.isCustomCover !== 1) {
+          item.pixelMapPath = cached.pixelMapPath || item.pixelMapPath;
+          item.isCustomCover = 1;
+        }
         return await new Promise<boolean>((resolve) => {
           const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
           predicates.equalTo(DB_COLUMNS.FILE_PATH, item.filePath);
@@ -268,28 +275,54 @@ export default  class MediaTable {
 
   //更新音乐封面地址
   public updatePixelMapPath(filePath: string, newPixelMapPath: string, callback: Function) {
+    this.updateCoverInfo(filePath, newPixelMapPath, undefined, false, callback);
+  }
+
+  public updateCustomCoverPath(filePath: string, newPixelMapPath: string, callback: Function) {
+    this.updateCoverInfo(filePath, newPixelMapPath, 1, true, callback);
+  }
+
+  private updateCoverInfo(filePath: string, newPixelMapPath: string, isCustomCover: number | undefined,
+    allowReplaceCustomCover: boolean, callback: Function) {
     const normalizedPath = normalizeFilePath(filePath);
-    // Step 1: 构建查询条件验证文件存在性
     const queryPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
     queryPredicates.equalTo('filePath',  normalizedPath);
 
-    // Step 2: 执行存在性验证
     this.accountTable.query(queryPredicates,  (resultSet: relationalStore.ResultSet) => {
       if (resultSet.rowCount  === 0) {
         callback(false, 'Error: Target file not found in database');
         resultSet.close();
         return;
       }
+      let existingPixelMapPath = '';
+      let existingIsCustomCover = 0;
+      if (resultSet.goToFirstRow()) {
+        const pixelMapIndex = resultSet.getColumnIndex(DB_COLUMNS.PIXEL_MAP_PATH);
+        if (pixelMapIndex >= 0) {
+          existingPixelMapPath = resultSet.getString(pixelMapIndex) || '';
+        }
+        const customCoverIndex = resultSet.getColumnIndex(DB_COLUMNS.IS_CUSTOM_COVER);
+        if (customCoverIndex >= 0) {
+          existingIsCustomCover = resultSet.getDouble(customCoverIndex) || 0;
+        }
+      }
       resultSet.close();
 
-      // Step 3: 构建更新条件与数据
+      if (existingIsCustomCover === 1 && !allowReplaceCustomCover &&
+        existingPixelMapPath.length > 0 && existingPixelMapPath !== newPixelMapPath) {
+        callback(true, 'Skip updating custom cover');
+        return;
+      }
+
       const updatePredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
       updatePredicates.equalTo('filePath',  normalizedPath);
       const valueBucket: relationalStore.ValuesBucket = {
         pixelMapPath: newPixelMapPath
       };
+      if (isCustomCover !== undefined) {
+        valueBucket.isCustomCover = isCustomCover;
+      }
 
-      // Step 4: 执行原子化更新操作
       this.accountTable.updateData(updatePredicates,  valueBucket, (success: boolean) => {
         callback(success, success ? null : 'Database update operation failed');
       });
@@ -429,6 +462,10 @@ export default  class MediaTable {
       obj.album  = resultSet.getString(resultSet.getColumnIndex('album'));
       obj.isFav  = resultSet.getDouble(resultSet.getColumnIndex('isFav'));
       obj.pixelMapPath = resultSet.getString(resultSet.getColumnIndex('pixelMapPath'));
+      const customCoverIndex = resultSet.getColumnIndex(DB_COLUMNS.IS_CUSTOM_COVER);
+      if (customCoverIndex >= 0) {
+        obj.isCustomCover = resultSet.getDouble(customCoverIndex);
+      }
 
       obj.duration  = resultSet.getString(resultSet.getColumnIndex('duration'));
       obj.mimeType  = resultSet.getString(resultSet.getColumnIndex('mimeType'));
@@ -917,6 +954,7 @@ export default  class MediaTable {
 
     // 设置额外属性,添加安全检查
     item.isFav = safeGetNumber(DB_COLUMNS.IS_FAV);
+    item.isCustomCover = safeGetNumber(DB_COLUMNS.IS_CUSTOM_COVER);
     item.duration = safeGet(DB_COLUMNS.DURATION);
     item.mimeType = safeGet(DB_COLUMNS.MIME_TYPE);
     item.trackCount = safeGet(DB_COLUMNS.TRACK_COUNT);
@@ -1677,6 +1715,9 @@ function generateBucket(item: VideoItem): relationalStore.ValuesBucket {
   if(item.pixelMapPath){
     obj.pixelMapPath = item.pixelMapPath;
   }
+  if(item.isCustomCover !== undefined && item.isCustomCover !== null){
+    obj.isCustomCover = item.isCustomCover;
+  }
 
 
   if(item.duration){

+ 3 - 1
entry/src/main/ets/common/util/RdbUtils.ets

@@ -51,6 +51,7 @@ export default class RdbUtils {
       '        parentPath TEXT,\n' +
       '        isFav INTEGER,\n' +
       '        pixelMapPath TEXT,\n' +
+      '        isCustomCover INTEGER DEFAULT 0,\n' +
       '        duration TEXT,\n' +
       '        sampleRate TEXT,\n' +
       '        playCount INTEGER DEFAULT 0,\n' +
@@ -85,7 +86,7 @@ export default class RdbUtils {
       '        mimeType TEXT' +
       ')',
     columns: ['id', 'name', 'filePath','mtype', 'videoSize', 'cTime','size', 'artist', 'album',
-      'fileName','parentPath','isFav','pixelMapPath',
+      'fileName','parentPath','isFav','pixelMapPath','isCustomCover',
       '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',
@@ -196,6 +197,7 @@ export default class RdbUtils {
             'lastPlayedStr': 'TEXT',
             'trackCount': 'TEXT',
             'lyricContent': 'TEXT',
+            'isCustomCover': 'INTEGER DEFAULT 0',
             'md5Str': 'TEXT',
             'extra_json': 'TEXT',
             'mimeType': 'TEXT',

+ 49 - 5
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -15,7 +15,7 @@ import Logger from './Logger';
 import { buffer } from '@kit.ArkTS';
 import { CommonConstants } from '../constants/CommonConstants';
 import { GlobalContext, PreferencesUtil } from '@pura/harmony-utils';
-import { MusicInfo, parseMusicFileName, Utility } from './Utility';
+import { MusicInfo, parseMusicFileName, resolveAudioQualityTag, Utility } from './Utility';
 import { RemoteDriveType } from '../enums/RemoteDriveType';
 import { createSmbDirectory, deleteSmbEntry, listSmbDirectory, SmbDirectoryEntry } from '../network/SmbBridge';
 import { NavidromeApi, NavidromeAlbumDetail, NavidromeArtist, NavidromeArtistDetail, NavidromeSong } from '../network/NavidromeApi';
@@ -35,6 +35,25 @@ import { EmbyApi, EmbyAlbum, EmbyArtist, EmbySong } from '../network/EmbyApi';
 
 const TAG = 'heanup RemoteDriveManager';
 
+function mergeCoverWithPriorityLocal(target?: VideoItem | null, source?: VideoItem | null): void {
+  if (!target || !source) {
+    return;
+  }
+  const incomingCover = source.pixelMapPath;
+  const incomingCustomCover = source.isCustomCover === 1;
+  if (incomingCustomCover) {
+    target.isCustomCover = 1;
+  }
+  if (!incomingCover || incomingCover.length === 0) {
+    return;
+  }
+  const targetHasCustomCover = target.isCustomCover === 1 && !!target.pixelMapPath && target.pixelMapPath.length > 0;
+  if (targetHasCustomCover && !incomingCustomCover && target.pixelMapPath !== incomingCover) {
+    return;
+  }
+  target.pixelMapPath = incomingCover;
+}
+
 // 百度网盘上传TaskPool任务接口(使用前缀避免名称冲突)
 interface TaskBaiduUploadParams {
   filePath: string;
@@ -2140,9 +2159,7 @@ export class RemoteDriveManager {
     if (!target.cTime && source.cTime) {
       target.cTime = source.cTime;
     }
-    if (!target.pixelMapPath && source.pixelMapPath) {
-      target.pixelMapPath = source.pixelMapPath;
-    }
+    mergeCoverWithPriorityLocal(target, source);
     if (!target.mimeType && source.mimeType) {
       target.mimeType = source.mimeType;
     }
@@ -2785,7 +2802,7 @@ export class RemoteDriveManager {
     target.album = cached.album || target.album;
     target.duration = cached.duration || target.duration;
     target.size = cached.size || target.size;
-    target.pixelMapPath = cached.pixelMapPath || target.pixelMapPath;
+    mergeCoverWithPriorityLocal(target, cached);
     target.lyricContent = cached.lyricContent || target.lyricContent;
     target.md5Str = cached.md5Str || target.md5Str;
     target.bit_rate = cached.bit_rate || target.bit_rate;
@@ -2800,6 +2817,26 @@ export class RemoteDriveManager {
     target.remote_rel_path = target.remote_rel_path || cached.remote_rel_path || storagePath;
   }
 
+  private resolveRemoteSongQuality(formatHint: string, fallbackFileName: string, bitrate?: number, sampleRate?: number): string {
+    const directQuality = resolveAudioQualityTag(formatHint, bitrate, sampleRate);
+    if (directQuality) {
+      return directQuality;
+    }
+    const extension = this.getFileExtension(fallbackFileName);
+    if (extension.length === 0) {
+      return '';
+    }
+    return resolveAudioQualityTag(extension, bitrate, sampleRate);
+  }
+
+  private applyInitialRemoteSongQuality(videoItem: VideoItem, formatHint: string, fallbackFileName: string, bitrate?: number,
+    sampleRate?: number): void {
+    const quality = this.resolveRemoteSongQuality(formatHint, fallbackFileName, bitrate, sampleRate);
+    if (quality.length > 0) {
+      videoItem.md5Str = quality;
+    }
+  }
+
   // 将FileInfo转换为VideoItem
   private fileInfoToVideoItem(fileInfo: FileInfo, account: WebDavAccount): VideoItem {
     if (account.webType === RemoteDriveType.Smb) {
@@ -2859,6 +2896,7 @@ export class RemoteDriveManager {
     if (videoItem.remote_rel_path) {
       videoItem.parentPath = this.getDirectoryFromRemotePath(this.normalizeRemoteHref(videoItem.remote_rel_path));
     }
+    this.applyInitialRemoteSongQuality(videoItem, '', fileInfo.fileName);
 
     return videoItem;
   }
@@ -2907,6 +2945,7 @@ export class RemoteDriveManager {
     }
     videoItem.fileName = fileInfo.fileName
     videoItem.parentPath = this.getDirectoryFromRemotePath(sanitizedRelative);
+    this.applyInitialRemoteSongQuality(videoItem, '', fileInfo.fileName);
     return videoItem;
   }
 
@@ -2955,6 +2994,7 @@ export class RemoteDriveManager {
     videoItem.fileName = fileInfo.fileName
     videoItem.remote_rel_path = normalizedPath;
     videoItem.parentPath = this.getDirectoryFromRemotePath(normalizedPath);
+    this.applyInitialRemoteSongQuality(videoItem, '', fileInfo.fileName);
     return videoItem;
   }
 
@@ -2996,6 +3036,7 @@ export class RemoteDriveManager {
     } else {
       videoItem.artist = Constants.UNKNOWN_ARTIST;
     }
+    this.applyInitialRemoteSongQuality(videoItem, '', entry.server_filename);
     return videoItem;
   }
 
@@ -3203,6 +3244,7 @@ export class RemoteDriveManager {
     videoItem.bit_rate = song.bitRate ? song.bitRate.toString() : undefined;
     videoItem.mimeType = song.contentType;
     videoItem.pixelMapPath = coverUrl;
+    this.applyInitialRemoteSongQuality(videoItem, song.suffix || song.contentType || '', fileName, song.bitRate);
     if (song.albumId && song.albumId.length > 0) {
       videoItem.parentPath = this.normalizeFullPath(`/album/${song.albumId}`);
       videoItem.navAlbumId = song.albumId;
@@ -3241,6 +3283,7 @@ export class RemoteDriveManager {
     videoItem.bit_rate = song.bitRate ? song.bitRate.toString() : undefined;
     videoItem.sampleRate = song.sampleRate ? song.sampleRate.toString() : undefined;
     videoItem.pixelMapPath = coverUrl;
+    this.applyInitialRemoteSongQuality(videoItem, song.suffix || '', fileName, song.bitRate, song.sampleRate);
     if (song.albumId && song.albumId.length > 0) {
       videoItem.parentPath = this.normalizeFullPath(`/album/${song.albumId}`);
     }
@@ -3293,6 +3336,7 @@ export class RemoteDriveManager {
     videoItem.bit_rate = song.bitRate ? song.bitRate.toString() : undefined;
     videoItem.sampleRate = song.sampleRate ? song.sampleRate.toString() : undefined;
     videoItem.pixelMapPath = coverUrl;
+    this.applyInitialRemoteSongQuality(videoItem, song.suffix || '', fileName, song.bitRate, song.sampleRate);
     if (song.albumId && song.albumId.length > 0) {
       videoItem.parentPath = this.normalizeFullPath(`/album/${song.albumId}`);
     }

+ 41 - 2
entry/src/main/ets/common/util/RemotePlayerUtil.ets

@@ -80,11 +80,46 @@ export interface LocalEditMusicPayload {
 export interface WebDavMetadataUpdatePayload {
   filePath: string;
   pixelMapPath?: string;
+  isCustomCover?: number;
+  md5Str?: string;
   name?: string;
   artist?: string;
 }
 
 
+export function hasCustomCover(item?: VideoItem | null): boolean {
+  return !!item && item.isCustomCover === 1 && !!item.pixelMapPath && item.pixelMapPath.length > 0;
+}
+
+export function mergeCoverWithPriority(target?: VideoItem | null,
+  incomingCover?: string, incomingCustomCover: number = 0): boolean {
+  if (!target) {
+    return false;
+  }
+  const customCoverLocked = incomingCustomCover === 1;
+  let mutated = false;
+
+  if (customCoverLocked && target.isCustomCover !== 1) {
+    target.isCustomCover = 1;
+    mutated = true;
+  }
+
+  if (!incomingCover || incomingCover.length === 0) {
+    return mutated;
+  }
+
+  if (hasCustomCover(target) && !customCoverLocked && target.pixelMapPath !== incomingCover) {
+    return mutated;
+  }
+
+  if (target.pixelMapPath !== incomingCover) {
+    target.pixelMapPath = incomingCover;
+    mutated = true;
+  }
+  return mutated;
+}
+
+
 
 
 /**
@@ -354,7 +389,7 @@ export async function setVideoUrlForSong(
         };
 
         if (shouldExtractCover && metadataItem.pixelMapPath) {
-          targetSong.pixelMapPath = metadataItem.pixelMapPath;
+          mergeCoverWithPriority(targetSong, metadataItem.pixelMapPath);
         }
         if (shouldExtractLyric && metadataItem.lyricContent) {
           targetSong.lyricContent = metadataItem.lyricContent;
@@ -365,7 +400,9 @@ export async function setVideoUrlForSong(
           targetSong.sampleRate = applyStringIfEmpty(targetSong.sampleRate, metadataItem.sampleRate) ?? targetSong.sampleRate;
           targetSong.trackCount = applyStringIfEmpty(targetSong.trackCount, metadataItem.trackCount) ?? targetSong.trackCount;
           targetSong.mimeType = applyStringIfEmpty(targetSong.mimeType, metadataItem.mimeType) ?? targetSong.mimeType;
-          targetSong.md5Str = applyStringIfEmpty(targetSong.md5Str, metadataItem.md5Str) ?? targetSong.md5Str;
+          if (metadataItem.md5Str && metadataItem.md5Str.length > 0 && targetSong.md5Str !== metadataItem.md5Str) {
+            targetSong.md5Str = metadataItem.md5Str;
+          }
           targetSong.size = applyStringIfEmpty(targetSong.size, metadataItem.size) ?? targetSong.size;
           targetSong.videoSize = applyNumberIfEmpty(targetSong.videoSize, metadataItem.videoSize) ?? targetSong.videoSize;
           targetSong.bits_per_raw_sample = applyStringIfEmpty(targetSong.bits_per_raw_sample, metadataItem.bits_per_raw_sample)
@@ -417,6 +454,8 @@ export async function setVideoUrlForSong(
           const payload: WebDavMetadataUpdatePayload = {
             filePath: targetSong.filePath,
             pixelMapPath: targetSong.pixelMapPath,
+            isCustomCover: targetSong.isCustomCover,
+            md5Str: targetSong.md5Str,
             name: targetSong.name,
             artist: targetSong.artist
           };

+ 89 - 0
entry/src/main/ets/common/util/Utility.ets

@@ -2010,6 +2010,95 @@ export enum AudioQuality {
   HR = "HR"       // 高解析度
 }
 
+function normalizeAudioFormatForQuality(formatHint: string): string {
+  if (StrUtil.isEmpty(formatHint)) {
+    return '';
+  }
+  let normalized = formatHint.trim().toLowerCase();
+  if (normalized.startsWith('.')) {
+    normalized = normalized.substring(1);
+  }
+  if (normalized.includes('/')) {
+    normalized = normalized.substring(normalized.lastIndexOf('/') + 1);
+  }
+  normalized = normalized.replace(/^x-/, '');
+  switch (normalized) {
+    case 'mpeg':
+    case 'mpga':
+    case 'mp2':
+      return 'mp3';
+    case 'x-flac':
+      return 'flac';
+    case 'wave':
+    case 'x-wav':
+      return 'wav';
+    case 'aif':
+    case 'x-aiff':
+      return 'aiff';
+    case 'm4a':
+    case 'mp4':
+      return 'aac';
+    case 'oga':
+      return 'ogg';
+    default:
+      return normalized;
+  }
+}
+
+function resolveAudioQualityByNormalizedFormat(normalizedFormat: string): AudioQuality | '' {
+  switch (normalizedFormat) {
+    case 'dsf':
+    case 'dff':
+      return AudioQuality.HIRES;
+    case 'flac':
+    case 'ape':
+    case 'wav':
+    case 'aiff':
+    case 'aif':
+    case 'alac':
+    case 'tta':
+    case 'wv':
+      return AudioQuality.SQ;
+    case 'mp3':
+    case 'aac':
+    case 'ogg':
+    case 'opus':
+    case 'wma':
+    case 'mp2':
+      return AudioQuality.HQ;
+    default:
+      return '';
+  }
+}
+
+export function resolveAudioQualityBySuffix(formatHint: string): AudioQuality | '' {
+  if (StrUtil.isEmpty(formatHint)) {
+    return '';
+  }
+  const normalizedFormat = normalizeAudioFormatForQuality(formatHint);
+  if (StrUtil.isEmpty(normalizedFormat)) {
+    return '';
+  }
+  return resolveAudioQualityByNormalizedFormat(normalizedFormat);
+}
+
+export function resolveAudioQualityTag(formatHint: string, bitrate?: number, sampleRate?: number): AudioQuality | '' {
+  if (StrUtil.isEmpty(formatHint)) {
+    return '';
+  }
+  const normalizedFormat = normalizeAudioFormatForQuality(formatHint);
+  if (StrUtil.isEmpty(normalizedFormat)) {
+    return '';
+  }
+  if (bitrate === undefined || sampleRate === undefined) {
+    return resolveAudioQualityByNormalizedFormat(normalizedFormat);
+  }
+  if (!Number.isFinite(bitrate) || bitrate <= 0 || !Number.isFinite(sampleRate) || sampleRate <= 0) {
+    return resolveAudioQualityByNormalizedFormat(normalizedFormat);
+  }
+  return determineAudioQuality(normalizedFormat, bitrate, sampleRate);
+}
+
 // 明确定义支持的音频格式类型
 interface SupportedFormats {
   LOSSY: Set<string>;

+ 37 - 7
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -26,7 +26,7 @@ import { UploadMusicPage } from './UploadMusicPage';
 import { FFmpeg } from '@sj/ffmpeg';
 import FileManager from '../common/util/FileManager';
 import { fileIo, fileUri, picker } from '@kit.CoreFileKit';
-import { setVideoUrlForSong } from '../common/util/RemotePlayerUtil';
+import { mergeCoverWithPriority, setVideoUrlForSong } from '../common/util/RemotePlayerUtil';
 import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog';
 import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
 import { Playlist } from '../viewmodel/Playlist';
@@ -51,6 +51,8 @@ interface PlaylistEventData {
 interface WebDavMetadataUpdatePayload {
   filePath: string;
   pixelMapPath?: string;
+  isCustomCover?: number;
+  md5Str?: string;
   name?: string;
   artist?: string;
 }
@@ -483,9 +485,13 @@ export struct WebDavMainPage {
   }
 
   private async tryResolveRemoteMusicCover(item: VideoItem, token: number): Promise<void> {
+    if (item.isCustomCover === 1 && StrUtil.isNotEmpty(item.pixelMapPath)) {
+      this.refreshThumbItem(item);
+      return;
+    }
     const coverPath: string = await this.resolveRemoteThumbPath(item);
     if (this.isThumbCacheValid(coverPath)) {
-      item.pixelMapPath = this.normalizeThumbImageSource(coverPath);
+      mergeCoverWithPriority(item, this.normalizeThumbImageSource(coverPath));
       this.refreshThumbItem(item);
       return;
     }
@@ -510,7 +516,7 @@ export struct WebDavMainPage {
       return;
     }
     if (this.isThumbCacheValid(coverPath)) {
-      item.pixelMapPath = this.normalizeThumbImageSource(coverPath);
+      mergeCoverWithPriority(item, this.normalizeThumbImageSource(coverPath));
       this.refreshThumbItem(item);
     }
   }
@@ -532,7 +538,7 @@ export struct WebDavMainPage {
       }
       const thumbPath: string = await this.resolveRemoteThumbPath(item);
       if (this.isThumbCacheValid(thumbPath)) {
-        item.pixelMapPath = this.normalizeThumbImageSource(thumbPath);
+        mergeCoverWithPriority(item, this.normalizeThumbImageSource(thumbPath));
         this.refreshThumbItem(item);
         return;
       }
@@ -557,7 +563,7 @@ export struct WebDavMainPage {
         return;
       }
       if (this.isThumbCacheValid(thumbPath)) {
-        item.pixelMapPath = this.normalizeThumbImageSource(thumbPath);
+        mergeCoverWithPriority(item, this.normalizeThumbImageSource(thumbPath));
         this.refreshThumbItem(item);
       }
     } catch (error) {
@@ -891,6 +897,7 @@ export struct WebDavMainPage {
     dbSong.isFav = song.isFav;
     dbSong.size = song.size;
     dbSong.pixelMapPath = song.pixelMapPath;
+    dbSong.isCustomCover = song.isCustomCover;
     dbSong.artist = song.artist;
     dbSong.album = song.album;
     dbSong.fileName = song.fileName;
@@ -1461,6 +1468,14 @@ export struct WebDavMainPage {
     return parts.join(' · ');
   }
 
+  private buildSongQualityLabel(song: VideoItem): string {
+    if (!song || StrUtil.isEmpty(song.md5Str)) {
+      return '';
+    }
+    const quality = song.md5Str as string;
+    return quality.includes('Lossless') ? '无损' : quality;
+  }
+
   // 对话框控制器
   private accountDialogController: CustomDialogController | null = null;
   // 保存事件处理器引用,用于取消订阅
@@ -1618,8 +1633,11 @@ export struct WebDavMainPage {
     }
     const targetSong = this.songs[targetIndex];
     let mutated = false;
-    if (payload.pixelMapPath && payload.pixelMapPath.length > 0) {
-      targetSong.pixelMapPath = payload.pixelMapPath;
+    if (mergeCoverWithPriority(targetSong, payload.pixelMapPath, payload.isCustomCover ?? 0)) {
+      mutated = true;
+    }
+    if (payload.md5Str && payload.md5Str.length > 0 && targetSong.md5Str !== payload.md5Str) {
+      targetSong.md5Str = payload.md5Str;
       mutated = true;
     }
     if (payload.name && payload.name.length > 0) {
@@ -1668,6 +1686,7 @@ export struct WebDavMainPage {
     clone.album = item.album;
     clone.lyricContent = item.lyricContent;
     clone.pixelMapPath = item.pixelMapPath;
+    clone.isCustomCover = item.isCustomCover;
     clone.cTime = item.cTime;
     clone.fileName = item.fileName;
     clone.md5Str = item.md5Str;
@@ -3031,6 +3050,17 @@ export struct WebDavMainPage {
             .textOverflow({ overflow: TextOverflow.Ellipsis })
 
           Row(){
+            if (StrUtil.isNotEmpty(song.md5Str)) {
+              Text(this.buildSongQualityLabel(song))
+                .fontSize(10)
+                .fontColor($r('app.color.index_tab_font_color'))
+                .fontWeight(500)
+                .padding({ top: 2, right: 5, left: 5, bottom: 2 })
+                .borderRadius(6)
+                .margin({right:3})
+                .backgroundColor('#FFC107')
+                .opacity(0.92)
+            }
             Text((song.artist ?? '') + "  ")
               .fontSize(13)
               .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color'))

+ 60 - 34
entry/src/main/ets/view/LocalMusic.ets

@@ -129,6 +129,7 @@ import {
   isAudioStationType,
   isPlexType,
   isRemoteCloudType,
+  mergeCoverWithPriority,
   WorkerEditMusicResult,
   WebDavMetadataUpdatePayload,
   isDaoLiYuType
@@ -1100,12 +1101,16 @@ export struct LocalMusic {
             }
           }
 
-          if (payload.pixelMapPath && payload.pixelMapPath.length > 0) {
-            this.cover = payload.pixelMapPath;
-            if (this.currentSong) {
-              this.currentSong.pixelMapPath = payload.pixelMapPath;
+          if (this.currentSong) {
+            const coverMutated = mergeCoverWithPriority(this.currentSong, payload.pixelMapPath,
+              payload.isCustomCover ?? 0);
+            if (coverMutated && this.currentSong.pixelMapPath) {
+              this.cover = this.currentSong.pixelMapPath;
+              Logger.info(TAG, `LocalMusic: 封面已更新: ${this.currentSong.pixelMapPath}`);
+            }
+            if (payload.md5Str && payload.md5Str.length > 0) {
+              this.currentSong.md5Str = payload.md5Str;
             }
-            Logger.info(TAG, `LocalMusic: 封面已更新: ${payload.pixelMapPath}`);
           }
 
           // 同步更新 AppStorage
@@ -1662,6 +1667,8 @@ export struct LocalMusic {
         remoteMetadataUpdates.set(target.filePath, {
           filePath: target.filePath,
           pixelMapPath: target.pixelMapPath,
+          isCustomCover: target.isCustomCover,
+          md5Str: target.md5Str,
           name: target.name,
           artist: target.artist
         });
@@ -1676,8 +1683,8 @@ export struct LocalMusic {
       if (payload.artist) {
         this.artist = payload.artist;
       }
-      if (payload.pixelMapPath) {
-        this.cover = payload.pixelMapPath;
+      if (payload.pixelMapPath && this.currentSong?.pixelMapPath) {
+        this.cover = this.currentSong.pixelMapPath;
       }
       if (payload.lyricContent) {
         this.applyInlineLyricContent(payload.lyricContent);
@@ -1806,8 +1813,9 @@ export struct LocalMusic {
     this.assignStringMetadata(target, 'duration', payload.duration);
     this.assignStringMetadata(target, 'sampleRate', payload.sampleRate);
     this.assignStringMetadata(target, 'mimeType', payload.mimeType);
+    this.assignStringMetadata(target, 'md5Str', payload.md5Str);
     this.assignStringMetadata(target, 'size', payload.size);
-    this.assignStringMetadata(target, 'pixelMapPath', payload.pixelMapPath);
+    mergeCoverWithPriority(target, payload.pixelMapPath);
     this.assignStringMetadata(target, 'lyricContent', payload.lyricContent, true);
     this.assignStringMetadata(target, 'genre', payload.genre);
     this.assignStringMetadata(target, 'track', payload.track);
@@ -1837,10 +1845,11 @@ export struct LocalMusic {
     this.assignStringMetadata(target, 'bit_rate', source.bit_rate);
     this.assignStringMetadata(target, 'duration', source.duration);
     this.assignStringMetadata(target, 'sampleRate', source.sampleRate);
+    this.assignStringMetadata(target, 'md5Str', source.md5Str);
     this.assignStringMetadata(target, 'trackCount', source.trackCount);
     this.assignStringMetadata(target, 'mimeType', source.mimeType);
     this.assignStringMetadata(target, 'size', source.size);
-    this.assignStringMetadata(target, 'pixelMapPath', source.pixelMapPath);
+    mergeCoverWithPriority(target, source.pixelMapPath, source.isCustomCover ?? 0);
     this.assignStringMetadata(target, 'lyricContent', source.lyricContent, true);
     this.assignStringMetadata(target, 'genre', source.genre);
     this.assignStringMetadata(target, 'track', source.track);
@@ -3098,16 +3107,20 @@ export struct LocalMusic {
 
       if (item.filePath  == this.currentSong?.filePath)  {
         this.cover  = imagePath
+        if (this.currentSong) {
+          this.currentSong.isCustomCover = 1;
+        }
       }
       if (item) {
         item.pixelMapPath  = imagePath
+        item.isCustomCover = 1;
       }
 
       if (isRemoteCloudType(item.type)) {
         return FileUtil.getFilePath(imagePath);
       }
 
-      this.table.updatePixelMapPath(item.filePath,  imagePath, (success: boolean, error?: string) => {
+      this.table.updateCustomCoverPath(item.filePath,  imagePath, (success: boolean, error?: string) => {
         if (success) {
           this.doUpdateData()
           console.log("onecold  更新音乐封面成功,数据库已同步");
@@ -3183,6 +3196,7 @@ export struct LocalMusic {
       if (isRemoteCloudType(this.currentSong.type)) {
         this.cover = coverUri;
         this.currentSong.pixelMapPath = coverUri;
+        this.currentSong.isCustomCover = 1;
         this.syncRemoteManagerSong(this.currentSong);
         await this.persistRemoteMetadataToDb(this.currentSong);
         this.emitRemoteMetadataUpdated(this.currentSong);
@@ -3211,6 +3225,7 @@ export struct LocalMusic {
 
       this.cover = coverUri;
       this.currentSong.pixelMapPath = coverUri;
+      this.currentSong.isCustomCover = 1;
       await this.doUpateEditedFields(this.currentSong);
       ToastUtil.showToast('更换封面成功');
     } catch (error) {
@@ -8942,6 +8957,8 @@ export struct LocalMusic {
     const payload: WebDavMetadataUpdatePayload = {
       filePath: item.filePath,
       pixelMapPath: item.pixelMapPath,
+      isCustomCover: item.isCustomCover,
+      md5Str: item.md5Str,
       name: item.name,
       artist: item.artist
     };
@@ -8959,9 +8976,6 @@ export struct LocalMusic {
       return;
     }
     this.mergeVideoItemFromSource(target, item);
-    if (StrUtil.isNotEmpty(item.pixelMapPath)) {
-      target.pixelMapPath = item.pixelMapPath;
-    }
   }
 
   private async saveRemoteSongEdit(item: VideoItem): Promise<void> {
@@ -8969,13 +8983,14 @@ export struct LocalMusic {
       this.syncEditedFields(item);
       if (StrUtil.isNotEmpty(this.imagePathStr)) {
         item.pixelMapPath = this.normalizeEditedCoverPath(this.imagePathStr);
+        item.isCustomCover = 1;
       }
       this.name = this.titleStr;
       if (this.currentSong && item.filePath === this.currentSong.filePath) {
         this.syncEditedFields(this.currentSong);
         if (StrUtil.isNotEmpty(item.pixelMapPath)) {
-          this.currentSong.pixelMapPath = item.pixelMapPath;
-          this.cover = item.pixelMapPath;
+          mergeCoverWithPriority(this.currentSong, item.pixelMapPath, item.isCustomCover ?? 0);
+          this.cover = this.currentSong.pixelMapPath || item.pixelMapPath;
         }
         this.artist = this.currentSong.artist ?? '';
         this.currentSong = cloneVideoItem(this.currentSong);
@@ -12172,8 +12187,8 @@ export struct LocalMusic {
           this.PlayOrPauseButton()
         },
         isPx: false,
-        builderHeight: this.isPhoneLan()?40:43,
-        builderWidth: this.isPhoneLan()?40:43,
+        builderHeight: this.isPhoneLan()?38:41,
+        builderWidth: this.isPhoneLan()?38:41,
       })
         .onClick(() =>{
           this.playOrPause()
@@ -12237,8 +12252,8 @@ export struct LocalMusic {
           .style({ strokeWidth: 5, status: ProgressStatus.LOADING })
       }
     }
-    .width(this.isPhoneLan()?40:43)
-    .height(this.isPhoneLan()?40:43)
+    .width(this.isPhoneLan()?38:41)
+    .height(this.isPhoneLan()?38:41)
   }
 
 
@@ -15063,27 +15078,27 @@ export struct LocalMusic {
     }
     const titleS = title||item?.name || '';
     const artistS = artist||item?.artist || ''; // Use empty string if artist is missing
-    return this.searchCover(item,  titleS,  artistS); // Await is not needed here as we return the promise directly
+    return this.searchCover(item,  titleS,  artistS, true); // Await is not needed here as we return the promise directly
   }
 
   /**
    *api搜索封面
    *item,根据title和artist
    */
-  searchCover(item: VideoItem, title: string, artist: string): Promise<string> {
+  searchCover(item: VideoItem, title: string, artist: string, markAsCustomCover: boolean = false): 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;
+            mergeCoverWithPriority(this.currentSong, res, markAsCustomCover ? 1 : 0);
+            this.cover  = this.currentSong.pixelMapPath || res;
           }
         }
 
         if (isRemoteCloudType(item.type)) {
-          item.pixelMapPath = res;
+          mergeCoverWithPriority(item, res, markAsCustomCover ? 1 : 0);
           this.syncRemoteManagerSong(item);
           try {
             await this.persistRemoteMetadataToDb(item);
@@ -15093,15 +15108,26 @@ export struct LocalMusic {
           }
         } else {
           // 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  更新音乐封面成功,数据库已同步");
-            } else {
-              console.error("onecold  更新音乐封面数据库失败原因: " + error);
-            }
-            // Note: We don't resolve/reject here because we already returned res
-          });
+          if (markAsCustomCover) {
+            item.isCustomCover = 1;
+            this.table.updateCustomCoverPath(item.filePath,  res, (success: boolean, error?: string) => {
+              if (success) {
+                this.doUpdateData();
+                console.log("onecold  更新音乐封面成功,数据库已同步");
+              } else {
+                console.error("onecold  更新音乐封面数据库失败原因: " + error);
+              }
+            });
+          } else {
+            this.table.updatePixelMapPath(item.filePath,  res, (success: boolean, error?: string) => {
+              if (success) {
+                this.doUpdateData();
+                console.log("onecold  更新音乐封面成功,数据库已同步");
+              } else {
+                console.error("onecold  更新音乐封面数据库失败原因: " + error);
+              }
+            });
+          }
         }
 
         LogUtil.debug("onecold  this.cover  =" + this.cover);
@@ -19207,7 +19233,7 @@ async function replaceLocalMusicCoverTask(
   });
 
   return await new Promise<boolean>((resolve) => {
-    table.updatePixelMapPath(filePath, fileUri.getUriFromPath(sandboxCoverPath),
+    table.updateCustomCoverPath(filePath, fileUri.getUriFromPath(sandboxCoverPath),
       (success: boolean, _error?: string) => {
         resolve(success);
       });

+ 2 - 0
entry/src/main/ets/viewmodel/VideoItem.ets

@@ -30,6 +30,7 @@ export class VideoItem  {
   isFav:number;
   size?:string;
   pixelMapPath?: string;
+  isCustomCover?: number;
   artist?: string;
   album?: string;
   fileName?: string;//音乐文件的真实文件名称
@@ -94,6 +95,7 @@ export class VideoItem  {
     this.isFav = 0;
     this.size = size;
     this.pixelMapPath = pixelMapPath;
+    this.isCustomCover = 0;
 
     this.artist = artist;
     this.album = album;