Преглед изворни кода

feat(media): 添加随机歌曲查询功能并优化随机播放逻辑

- 在MediaTable中新增RandomSongQueryOptions接口定义
- 实现queryRandomSong方法支持按类型和排除路径条件随机查询歌曲
- 更新LocalMusic组件导入新的RandomSongQueryOptions类型
- 修复onSelectIndexItem方法调用时缺少void处理的问题
- 重构onSelectIndexItem方法支持异步加载和索引查找优化
- 添加playedLocalFilePaths集合用于本地歌曲随机播放去重
- 完善randomPlay方法支持远程和本地歌曲的独立随机播放逻辑
- 实现随机播放时排除当前歌曲和已播放歌曲的功能
- 添加随机播放失败时的回退机制到列表随机播放
chendeben пре 7 месеци
родитељ
комит
55e74d5eec
2 измењених фајлова са 165 додато и 20 уклоњено
  1. 44 0
      entry/src/main/ets/common/util/MediaTable.ets
  2. 121 20
      entry/src/main/ets/view/LocalMusic.ets

+ 44 - 0
entry/src/main/ets/common/util/MediaTable.ets

@@ -99,6 +99,11 @@ export interface PageQueryResult {
   countMap?: Map<string, number>;
 }
 
+export interface RandomSongQueryOptions {
+  type?: number;
+  excludeFilePaths?: string[];
+}
+
 export default  class MediaTable {
   private accountTable = new RdbUtils(RdbUtils.MEDIA_TABLE.tableName, RdbUtils.MEDIA_TABLE.sqlCreate,
     RdbUtils.MEDIA_TABLE.columns);
@@ -1099,6 +1104,45 @@ export default  class MediaTable {
     });
   }
 
+  /**
+   * 从数据库随机获取一首歌曲(可选条件:类型、排除路径)
+   */
+  public async queryRandomSong(options?: RandomSongQueryOptions): Promise<VideoItem | null> {
+    try {
+      const conditions: string[] = [];
+      const params: Array<string | number> = [];
+      if (options?.type !== undefined) {
+        conditions.push(`${DB_COLUMNS.TYPE} = ?`);
+        params.push(options.type);
+      }
+      if (options?.excludeFilePaths && options.excludeFilePaths.length > 0) {
+        const normalizedPaths = options.excludeFilePaths.map(path => normalizeFilePath(path));
+        const placeholders = normalizedPaths.map(() => '?').join(', ');
+        conditions.push(`${DB_COLUMNS.FILE_PATH} NOT IN (${placeholders})`);
+        params.push(...normalizedPaths);
+      }
+      let sql = `SELECT * FROM ${RdbUtils.MEDIA_TABLE.tableName}`;
+      if (conditions.length > 0) {
+        sql += ` WHERE ${conditions.join(' AND ')}`;
+      }
+      sql += ' ORDER BY RANDOM() LIMIT 1';
+
+      const resultSet = await this.accountTable.querySql(sql, params);
+      try {
+        if (resultSet.rowCount === 0) {
+          return null;
+        }
+        resultSet.goToFirstRow();
+        return this.buildVideoItem(resultSet);
+      } finally {
+        resultSet.close();
+      }
+    } catch (error) {
+      Logger.error(RdbUtils.RDB_TAG, `queryRandomSong failed: ${(error as Error).message}`);
+      return null;
+    }
+  }
+
   /**
    * 应用搜索条件到查询谓词
    * 支持对 name、artist、album、fileName 多字段模糊搜索

+ 121 - 20
entry/src/main/ets/view/LocalMusic.ets

@@ -76,7 +76,7 @@ import { secondToTime,getTransverterText } from '../common/util/CommUtils';
 import { CommonConstants2 } from '../common/util/CommonConstants2';
 import { hilog } from '@kit.PerformanceAnalysisKit';
 import { LazyDataSource } from '../common/util/LazyDataSource';
-import  MediaTable, { PageQueryOptions, PageQueryResult }  from '../common/util/MediaTable';
+import  MediaTable, { PageQueryOptions, PageQueryResult, RandomSongQueryOptions }  from '../common/util/MediaTable';
 import FileDeletionWatcher from '../common/util/FileDeletionWatcher';
 import LyricUtil from '../common/util/LyricUtil';
 import NetAxiosUtil from '../common/util/NetAxiosUtil';
@@ -3236,7 +3236,7 @@ export struct LocalMusic {
               y: this.alphabetHeight / 2
             })
             .onSelect((index: number) => {
-              this.onSelectIndexItem(this.alphaBet[index])
+              void this.onSelectIndexItem(this.alphaBet[index])
             })
             .onTouch((event: TouchEvent) =>{
               this.isShowAlphaBet = true
@@ -3277,29 +3277,48 @@ export struct LocalMusic {
     }
   }
 
-  onSelectIndexItem(letter: string) {
-    let songs = this.videoLocalList
-    let targetIndex = -1;
-    for (let i = 0; i < songs.length; i++) {
-      let song = songs[i]
-      let text = this.isShowFileName?song.fileName:
-        (song.type!=CommonConstants.TYPE_IS_DIR&&song.pyStr)?song.pyStr:song.name
-      if(this.sortType === 0||this.sortType==1){
-        text = song.artist
-      }else if(this.sortType === 2||this.sortType==3){
-        text = song.album
-      }
-      if(text){
-        let firstPinyin = getFirstLetter(text)
-        if (firstPinyin&&firstPinyin === letter) {
-          targetIndex = i;
-          break;
+  async onSelectIndexItem(letter: string) {
+    const findTargetIndex = (): number => {
+      const songs = this.videoLocalList;
+      for (let i = 0; i < songs.length; i++) {
+        const song = songs[i];
+        let text = this.isShowFileName ? song.fileName :
+          (song.type != CommonConstants.TYPE_IS_DIR && song.pyStr) ? song.pyStr : song.name;
+        if (this.sortType === 0 || this.sortType == 1) {
+          text = song.artist;
+        } else if (this.sortType === 2 || this.sortType == 3) {
+          text = song.album;
+        }
+        if (text) {
+          const firstPinyin = getFirstLetter(text);
+          if (firstPinyin && firstPinyin === letter) {
+            return i;
+          }
         }
       }
+      return -1;
+    };
+
+    let targetIndex = findTargetIndex();
+    let attempts = 0;
+    const canLoadMore = () =>
+      !this.isCanBack &&
+      (this.modeType === 1 || this.modeType === 2 || this.modeType === 3) &&
+      this.hasMoreData &&
+      !this.isLoadingPage;
 
+    while (targetIndex === -1 && canLoadMore() && attempts < 10) {
+      await this.loadNextPage();
+      targetIndex = findTargetIndex();
+      attempts++;
     }
+
     if (targetIndex !== -1) {
-      this.listScroller.scrollToIndex(targetIndex);
+      if (this.isGridMusic || this.twoFingerType == 4) {
+        this.scroller.scrollToIndex(targetIndex, false);
+      } else {
+        this.listScroller.scrollToIndex(targetIndex);
+      }
     }
   }
   // 找到对应字母表的索引
@@ -14602,13 +14621,95 @@ export struct LocalMusic {
 
   // 存储已播放的歌曲索引
   private playedIndices: Set<number> = new Set();
+  // 存储已播放的本地歌曲路径(随机播放去重用)
+  private playedLocalFilePaths: Set<string> = new Set();
 
   //随机播放
   private async randomPlay() {
     // if (!this.debounce()) {
     //   return;
     // }
+    if (this.currentSong) {
+      const isCurrentRemote = isRemoteCloudType(this.currentSong.type);
+      const currentFilePath = this.currentSong.filePath;
+      if (isCurrentRemote) {
+        const remoteOptions: RandomSongQueryOptions = {
+          type: this.currentSong.type,
+          excludeFilePaths: currentFilePath ? [currentFilePath] : []
+        };
+        const randomRemoteSong = await this.table.queryRandomSong(remoteOptions);
+        if (randomRemoteSong) {
+          let nextIndex = this.songList.findIndex(song => song.filePath === randomRemoteSong.filePath);
+          if (nextIndex < 0) {
+            this.songList = [...this.songList, randomRemoteSong];
+            this.sonDataSource.pushArrayData(this.songList);
+            nextIndex = this.songList.length - 1;
+          }
+          this.curIndex = nextIndex;
+          this.playedIndices.add(this.curIndex);
+          this.CONTROL_PlayStatus = PlayStatus.INIT;
+          this.stop();
+          this.currentSong = randomRemoteSong;
+          // 设置videoUrl,WebDAV歌曲会通过webdav_account_id构建完整URL
+          this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+            context: this.context,
+            autoParseMusicName: this.autoParseMusicName
+          });
+          this.name = this.currentSong.name;
+          this.artist = this.currentSong.artist;
+          this.changeImageAnimation();
+          return;
+        }
+        Logger.warn(TAG, '随机播放未获取到网盘歌曲,回退到列表随机');
+      } else {
+        if (currentFilePath) {
+          this.playedLocalFilePaths.add(currentFilePath);
+        }
+        const localOptions: RandomSongQueryOptions = {
+          type: CommonConstants.TYPE_LOCAL,
+          excludeFilePaths: Array.from(this.playedLocalFilePaths)
+        };
+        let randomLocalSong = await this.table.queryRandomSong(localOptions);
+        if (!randomLocalSong) {
+          this.playedLocalFilePaths.clear();
+          if (currentFilePath) {
+            this.playedLocalFilePaths.add(currentFilePath);
+          }
+          const fallbackLocalOptions: RandomSongQueryOptions = {
+            type: CommonConstants.TYPE_LOCAL,
+            excludeFilePaths: currentFilePath ? [currentFilePath] : []
+          };
+          randomLocalSong = await this.table.queryRandomSong(fallbackLocalOptions);
+        }
+        if (randomLocalSong !== null) {
+          const selectedLocalSong = randomLocalSong;
+          let nextIndex = this.songList.findIndex(song => song.filePath === selectedLocalSong.filePath);
+          if (nextIndex < 0) {
+            this.songList = [...this.songList, selectedLocalSong];
+            this.sonDataSource.pushArrayData(this.songList);
+            nextIndex = this.songList.length - 1;
+          }
+          this.curIndex = nextIndex;
+          this.playedIndices.add(this.curIndex);
+          this.CONTROL_PlayStatus = PlayStatus.INIT;
+          this.stop();
+          this.currentSong = selectedLocalSong;
+          this.videoUrl = await setVideoUrlForSong(this.currentSong, {
+            context: this.context,
+            autoParseMusicName: this.autoParseMusicName
+          });
+          this.name = this.currentSong.name;
+          this.artist = this.currentSong.artist;
+          this.changeImageAnimation();
+          return;
+        }
+        Logger.warn(TAG, '随机播放未获取到本地歌曲,回退到列表随机');
+      }
+    }
     if (ArrayUtil.isNotEmpty(this.songList)) {
+      if (this.curIndex >= 0 && this.curIndex < this.songList.length) {
+        this.playedIndices.add(this.curIndex);
+      }
       if (this.songList.length > 3) {
         // 优先使用预缓存的随机索引(仅网盘播放时)
         const nextSong = this.songList[this.preloadNextRandomIndex >= 0 ? this.preloadNextRandomIndex : 0];