Просмотр исходного кода

feat(navidrome): 增强音乐搜索与筛选功能

chendeben 8 месяцев назад
Родитель
Сommit
c0c37f7bdb

+ 66 - 1
entry/src/main/ets/common/network/NavidromeApi.ets

@@ -36,6 +36,8 @@ export interface NavidromeSong {
   title: string;
   artist?: string;
   album?: string;
+  artistId?: string;
+  albumId?: string;
   track?: number;
   year?: number;
   duration?: number;
@@ -44,6 +46,8 @@ export interface NavidromeSong {
   size?: number;
   contentType?: string;
   coverArt?: string;
+  genre?: string;
+  created?: string;
 }
 
 interface SubsonicError {
@@ -76,11 +80,19 @@ interface SubsonicAlbumEntry {
   song?: Array<SubsonicSongEntry>;
 }
 
+interface SubsonicSearchResult2 {
+  song?: Array<SubsonicSongEntry>;
+  album?: Array<SubsonicAlbumEntry>;
+  artist?: Array<SubsonicArtistEntry>;
+}
+
 interface SubsonicSongEntry {
   id?: string;
   title?: string;
   artist?: string;
   album?: string;
+  artistId?: string;
+  albumId?: string;
   track?: number;
   duration?: number;
   bitRate?: number;
@@ -88,6 +100,8 @@ interface SubsonicSongEntry {
   size?: number;
   contentType?: string;
   coverArt?: string;
+  genre?: string;
+  created?: string;
 }
 
 interface SubsonicArtistBody {
@@ -103,6 +117,7 @@ interface SubsonicBody {
   indexes?: SubsonicIndexes;
   artist?: SubsonicArtistBody;
   album?: SubsonicAlbumEntry;
+  searchResult2?: SubsonicSearchResult2;
 }
 
 interface SubsonicRoot {
@@ -194,14 +209,18 @@ export class NavidromeApi {
           id: song.id,
           title: song.title ?? '未知曲目',
           artist: song.artist,
+          artistId: song.artistId,
           album: song.album,
+          albumId: song.albumId ?? album.id,
           track: song.track,
           duration: song.duration,
           bitRate: song.bitRate,
           suffix: song.suffix,
           size: song.size,
           contentType: song.contentType,
-          coverArt: song.coverArt
+          coverArt: song.coverArt,
+          genre: song.genre,
+          created: song.created
         });
       }
     }
@@ -216,6 +235,52 @@ export class NavidromeApi {
     };
   }
 
+  async searchSongs(account: WebDavAccount, keyword: string, limit: number = 200): Promise<NavidromeSong[]> {
+    const query = keyword.trim();
+    if (query.length === 0) {
+      return [];
+    }
+    const params: Array<QueryParam> = [
+      new QueryParam('query', query),
+      new QueryParam('songCount', limit.toString()),
+      new QueryParam('albumCount', '0'),
+      new QueryParam('artistCount', '0')
+    ];
+    void ServerLogUtil.info(TAG, `search2 查询: "${query}" (limit=${limit})`);
+    const response = await this.request(account, 'search2', params);
+    const searchResult = response.searchResult2;
+    if (!searchResult || !searchResult.song || searchResult.song.length === 0) {
+      void ServerLogUtil.warn(TAG, `search2 无结果: "${query}"`);
+      return [];
+    }
+    const songs: NavidromeSong[] = [];
+    for (let i = 0; i < searchResult.song.length; i++) {
+      const entry = searchResult.song[i];
+      if (!entry.id) {
+        continue;
+      }
+      songs.push({
+        id: entry.id,
+        title: entry.title ?? '未知曲目',
+        artist: entry.artist,
+        artistId: entry.artistId,
+        album: entry.album,
+        albumId: entry.albumId,
+        track: entry.track,
+        duration: entry.duration,
+        bitRate: entry.bitRate,
+        suffix: entry.suffix,
+        size: entry.size,
+        contentType: entry.contentType,
+        coverArt: entry.coverArt,
+        genre: entry.genre,
+        created: entry.created
+      });
+    }
+    void ServerLogUtil.info(TAG, `search2 返回 ${songs.length} 首歌曲`);
+    return songs;
+  }
+
   async buildStreamUrl(account: WebDavAccount, songId: string | undefined): Promise<string> {
     if (!songId) {
       throw new Error('Navidrome歌曲ID缺失');

+ 33 - 0
entry/src/main/ets/common/network/NavidromeRestApi.ets

@@ -103,6 +103,14 @@ export class NavidromeRestApi {
     return this.fetchPage<NavidromeRestAlbum>(account, '/api/album', start, 'name', 'ASC');
   }
 
+  async fetchSongsByArtist(account: WebDavAccount, artistId: string): Promise<NavidromeRestSong[]> {
+    return this.fetchSongsWithFilter(account, [new QueryParam('artist_id', artistId)]);
+  }
+
+  async fetchSongsByAlbum(account: WebDavAccount, albumId: string): Promise<NavidromeRestSong[]> {
+    return this.fetchSongsWithFilter(account, [new QueryParam('album_id', albumId)]);
+  }
+
   private async fetchPage<T extends object>(account: WebDavAccount, path: string,
     start: number, sortField: string, order: 'ASC' | 'DESC'): Promise<NavidromePagedResponse<T>> {
     const end = start + this.PAGE_SIZE;
@@ -121,6 +129,31 @@ export class NavidromeRestApi {
     };
   }
 
+  private async fetchSongsWithFilter(account: WebDavAccount, extraParams: QueryParam[]): Promise<NavidromeRestSong[]> {
+    const results: NavidromeRestSong[] = [];
+    let start = 0;
+    while (true) {
+      const end = start + this.PAGE_SIZE;
+      const params = [
+        new QueryParam('_start', `${start}`),
+        new QueryParam('_end', `${end}`),
+        new QueryParam('_sort', 'createdAt'),
+        new QueryParam('_order', 'DESC'),
+        ...extraParams
+      ];
+      const chunk = await this.get<NavidromeRestSong[]>(account, '/api/song', params);
+      if (!chunk || chunk.length === 0) {
+        break;
+      }
+      results.push(...chunk);
+      if (chunk.length < this.PAGE_SIZE) {
+        break;
+      }
+      start = end;
+    }
+    return results;
+  }
+
   private async get<T>(account: WebDavAccount, path: string, params?: Array<QueryParam>, retry: boolean = true): Promise<T> {
     const httpRequest = http.createHttp();
     try {

+ 233 - 108
entry/src/main/ets/view/NavidromePage.ets

@@ -2,7 +2,7 @@ import { VideoItem } from '../viewmodel/VideoItem';
 import { LengthMetrics, SegmentButton, SegmentButtonItemTuple, SegmentButtonOptions,
   SymbolGlyphModifier } from '@kit.ArkUI';
 import {
-  PreferencesUtil, ToastUtil, StrUtil, ArrayUtil
+  PreferencesUtil, ToastUtil, StrUtil
 } from '@pura/harmony-utils';
 import {
   ButtonFancyModifier,
@@ -19,10 +19,11 @@ import { Constants } from '../Constants';
 import { EventConstants } from '../common/constants/EventConstants';
 import { emitter } from '@kit.BasicServicesKit';
 import { setNavidromePlaylist } from '../common/util/NavidromePlaylistStore';
-import { navidromeApi } from '../common/network/NavidromeApi';
+import { navidromeApi, NavidromeSong } from '../common/network/NavidromeApi';
 import { ServerLogUtil } from '../common/util/ServerLogUtil';
 import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
 const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
+const NAVIDROME_SEARCH_LIMIT = 500;
 
 interface PlaylistEventData {
   playlistId: string;
@@ -75,8 +76,11 @@ export struct NavidromePage {
   @State filterType: NavFilterType = NavFilterType.None;
   @State filterLabel: string = '';
   @State filterId: string = '';
+  @State filterSongs: VideoItem[] = [];
+  @State isFilterLoading: boolean = false;
   private coverUrlCache: Map<string, string> = new Map();
   private albumCoverLookup: Map<string, string> = new Map();
+  private searchTicket: number = 0;
 
   private createTabOptions(): SegmentButtonOptions {
     const buttons = [
@@ -125,6 +129,7 @@ export struct NavidromePage {
   @State searchText: string = ''; // 用户输入内容
   @State filteredList: Array<VideoItem> = []; // 过滤后的歌曲结果
   @State sortType: number = 0; // 排序类型 0:名称升序 1:名称降序 2:时间升序 3:时间降序 4:大小升序 5:大小降序
+  @State isSearchLoading: boolean = false;
 
   // SegmentButton选项
   @State tabOptions: SegmentButtonOptions = this.createTabOptions();
@@ -143,6 +148,8 @@ export struct NavidromePage {
       this.isSearchMode = false;
       this.searchText = '';
       this.filteredList = [];
+      this.isSearchLoading = false;
+      this.searchTicket++;
     }
   }
 
@@ -260,9 +267,6 @@ export struct NavidromePage {
         return;
       }
       this.allVideos = [...this.allVideos, ...videos];
-      if (this.isSearchMode && this.searchText.length > 0) {
-        this.onSearchInput(this.searchText);
-      }
       this.songNextStart = response.nextStart;
       void ServerLogUtil.info('NavidromeLoad', `歌曲列表追加: 本次 ${videos.length} 首, 总数 ${this.allVideos.length}`);
     } finally {
@@ -343,6 +347,9 @@ export struct NavidromePage {
     if (this.loading) {
       return;
     }
+    if (this.selectedTab === 0 && this.filterType !== NavFilterType.None) {
+      return;
+    }
     const account = this.selectedAccount;
     if (!account) {
       return;
@@ -454,6 +461,28 @@ export struct NavidromePage {
     return map;
   }
 
+  private convertApiSongsToRestSongs(songs: NavidromeSong[]): NavidromeRestSong[] {
+    return songs.map((song: NavidromeSong): NavidromeRestSong => ({
+      id: song.id,
+      title: song.title,
+      artist: song.artist,
+      artistId: song.artistId,
+      album: song.album,
+      albumId: song.albumId,
+      duration: song.duration,
+      bitRate: song.bitRate,
+      suffix: song.suffix,
+      size: song.size,
+      createdAt: song.created,
+      genre: song.genre,
+      track: song.track,
+      year: song.year,
+      contentType: song.contentType,
+      coverArt: song.coverArt,
+      coverArtId: song.coverArt
+    }));
+  }
+
   private async convertSongsToVideoItems(songs: NavidromeRestSong[], account: WebDavAccount): Promise<VideoItem[]> {
     const tasks: Promise<VideoItem>[] = [];
     let successCount = 0;
@@ -691,7 +720,7 @@ export struct NavidromePage {
           .animation({ duration: 300, curve: Curve.Ease })
           .onClick(() => {
             this.isSearchMode = false;
-            this.onSearchInput('');
+            void this.onSearchInput('');
           })
           .attributeModifier(new ShadowModifier())
           .zIndex(0)
@@ -718,10 +747,10 @@ export struct NavidromePage {
           .placeholderFont({ size: 14, weight: 400 })
           .textFont({ size: 14, weight: 400 })
           .onSubmit((value: string) => {
-            this.onSearchInput(value);
+            void this.onSearchInput(value);
           })
           .onChange((value: string) => {
-            this.onSearchInput(value);
+            void this.onSearchInput(value);
           })
           .visibility(this.isSearchMode?Visibility.Visible:Visibility.None)
           .animation({ duration: 300, curve: Curve.Ease })
@@ -741,8 +770,8 @@ export struct NavidromePage {
           .visibility(this.selectedTab==0?Visibility.Visible:Visibility.None)
           .onClick(() => {
             this.isSearchMode = true;
-            if(ArrayUtil.isEmpty(this.filteredList)){
-              this.filteredList =  [...this.allVideos];
+            if (!this.isSearchLoading && this.searchText.length === 0) {
+              this.filteredList = [];
             }
           })
 
@@ -836,7 +865,7 @@ export struct NavidromePage {
 
   doSortType(index: number) {
     this.sortType = index;
-    const songs = this.isSearchMode ? this.filteredList :this.allVideos;
+    const songs = this.isSearchMode && this.searchText.length > 0 ? this.filteredList : this.allVideos;
     const sortTypeNames = ['名称升序', '名称降序', '艺术家升序', '艺术家降序', '专辑升序', '专辑降序'];
     const sortTypeName = sortTypeNames[index] || '未知排序';
 
@@ -899,39 +928,73 @@ export struct NavidromePage {
     void ServerLogUtil.debug('NavidromeSort', `排序结果示例: ${songs.slice(0, 3).map(s => `${s.name} (${s.artist})`).join(', ')}`);
   }
 
-  // 实时搜索逻辑
-  private onSearchInput(value: string) {
-    this.searchText = value.trim();
-    let mSearchList: Array<VideoItem> = [...this.allVideos];
+  // 实时搜索逻辑(改为远程API搜索)
+  private async onSearchInput(value: string): Promise<void> {
+    const keyword = value.trim();
+    this.searchText = keyword;
+    const ticket = ++this.searchTicket;
 
-    // 记录搜索操作
-    void ServerLogUtil.debug('NavidromeSearch', `搜索输入: "${value}" -> "${this.searchText}"`);
-    void ServerLogUtil.debug('NavidromeSearch', `搜索范围: ${mSearchList.length} 首歌曲`);
+    void ServerLogUtil.debug('NavidromeSearch', `搜索输入: "${value}" -> "${keyword}"`);
 
-    // 新增条件判断:空输入时显示所有数据
-    if (this.searchText === '') {
-      this.filteredList = [...mSearchList]; // 创建新数组以触发状态更新
+    if (keyword.length === 0) {
+      this.filteredList = [];
+      this.isSearchLoading = false;
       void ServerLogUtil.info('NavidromeSearch', '搜索已清空,显示所有歌曲');
-    } else {
-      const startTime = Date.now();
-      this.filteredList = mSearchList.filter((item: VideoItem) => {
-        // 支持模糊匹配和艺术家 专辑匹配
-        const regex = new RegExp(this.searchText.toLowerCase().replace(/\s+/g, '.*'), 'i');
-        return regex.test(item.name.toLowerCase()) ||
-          regex.test(item.fileName?.toLowerCase() ?? "") ||
-          regex.test(item.artist?.toLowerCase() ?? "") ||
-          regex.test(item.album?.toLowerCase() ?? "")
-      });
-      const searchTime = Date.now() - startTime;
+      return;
+    }
 
-      // 记录搜索结果
-      void ServerLogUtil.info('NavidromeSearch', `搜索完成: "${this.searchText}"`);
-      void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${searchTime}ms`);
-      void ServerLogUtil.info('NavidromeSearch', `- 匹配结果: ${this.filteredList.length}/${mSearchList.length}`);
-      void ServerLogUtil.debug('NavidromeSearch', `搜索结果示例: ${this.filteredList.slice(0, 3).map(s => `${s.name} (${s.artist})`).join(', ')}`);
+    if (!this.isSearchMode) {
+      this.isSearchMode = true;
+    }
+
+    const account = this.resolveActiveAccount();
+    if (!account) {
+      this.filteredList = [];
+      this.isSearchLoading = false;
+      ToastUtil.showToast('Navidrome账号不可用');
+      return;
+    }
+
+    this.isSearchLoading = true;
+    this.filteredList = [];
+    const startTime = Date.now();
+    void ServerLogUtil.info('NavidromeSearch', `开始远程搜索: "${keyword}"`);
 
-      if (this.filteredList.length === 0) {
-        void ServerLogUtil.warn('NavidromeSearch', `搜索无结果: "${this.searchText}"`);
+    try {
+      const songs = await navidromeApi.searchSongs(account, keyword, NAVIDROME_SEARCH_LIMIT);
+      if (ticket !== this.searchTicket) {
+        return;
+      }
+      const searchTime = Date.now() - startTime;
+      if (!songs || songs.length === 0) {
+        this.filteredList = [];
+        void ServerLogUtil.warn('NavidromeSearch', `搜索无结果: "${keyword}"`);
+        void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${searchTime}ms`);
+        return;
+      }
+      const restSongs = this.convertApiSongsToRestSongs(songs);
+      const videoItems = await this.convertSongsToVideoItems(restSongs, account);
+      if (ticket !== this.searchTicket) {
+        return;
+      }
+      this.filteredList = videoItems;
+      this.doSortType(this.sortType);
+      void ServerLogUtil.info('NavidromeSearch', `搜索完成: "${keyword}"`);
+      void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${searchTime}ms`);
+      void ServerLogUtil.info('NavidromeSearch', `- 匹配结果: ${videoItems.length}`);
+      void ServerLogUtil.debug('NavidromeSearch', `搜索结果示例: ${videoItems.slice(0, 3).map(s => `${s.name} (${s.artist})`).join(', ')}`);
+    } catch (error) {
+      if (ticket !== this.searchTicket) {
+        return;
+      }
+      this.filteredList = [];
+      const message = (error as Error).message ?? 'Navidrome 搜索失败';
+      ToastUtil.showToast(message);
+      void ServerLogUtil.error('NavidromeSearch', `搜索失败: ${message}`);
+      void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${Date.now() - startTime}ms`);
+    } finally {
+      if (ticket === this.searchTicket) {
+        this.isSearchLoading = false;
       }
     }
   }
@@ -954,6 +1017,12 @@ export struct NavidromePage {
       case 2:
         return '暂无专辑';
       default:
+        if (this.isSearchMode && this.searchText.length > 0) {
+          return this.isSearchLoading ? '正在搜索远程歌曲' : '没有匹配的远程歌曲';
+        }
+        if (this.filterType !== NavFilterType.None && this.isFilterLoading) {
+          return '正在加载筛选结果';
+        }
         return this.filterType === NavFilterType.None ? '暂无音乐' : '该筛选下暂无歌曲';
     }
   }
@@ -965,6 +1034,12 @@ export struct NavidromePage {
       case 2:
         return '当前筛选没有找到专辑';
       default:
+        if (this.isSearchMode && this.searchText.length > 0) {
+          return this.isSearchLoading ? '请稍候,正在通过 API 搜索' : '换个关键词再试试吧';
+        }
+        if (this.filterType !== NavFilterType.None && this.isFilterLoading) {
+          return '请稍候...';
+        }
         return this.filterType === NavFilterType.None ? '当前分类下没有找到音乐文件' : '请尝试调整筛选条件';
     }
   }
@@ -1185,27 +1260,12 @@ export struct NavidromePage {
   }
 
   private getVisibleSongs(): VideoItem[] {
-    if(this.isSearchMode)
-      return this.filteredList;
-
-    if (this.filterType === NavFilterType.Artist && this.filterId.length > 0) {
-      const filtered = this.allVideos.filter(item => item.navArtistId === this.filterId || item.artist === this.filterLabel);
-      Logger.info('heanup', `艺术家筛选: filterId=${this.filterId}, filterLabel=${this.filterLabel}, 结果数=${filtered.length}`);
-      return filtered;
+    if (this.isSearchMode) {
+      return this.searchText.length > 0 ? this.filteredList : this.allVideos;
     }
-    if (this.filterType === NavFilterType.Album && this.filterId.length > 0) {
-      const filtered = this.allVideos.filter(item => {
-        const match = item.navAlbumId === this.filterId || item.album === this.filterLabel;
-        if (!match && this.allVideos.indexOf(item) < 3) {
-          // 只打印前3首歌的调试信息
-          Logger.info('heanup', `歌曲 ${item.name}: navAlbumId=${item.navAlbumId}, album=${item.album}, 期望albumId=${this.filterId}, 期望album=${this.filterLabel}`);
-        }
-        return match;
-      });
-      Logger.info('heanup', `专辑筛选: filterId=${this.filterId}, filterLabel=${this.filterLabel}, 结果数=${filtered.length}`);
-      return filtered;
+    if (this.filterType !== NavFilterType.None) {
+      return this.filterSongs;
     }
-
     return this.allVideos;
   }
 
@@ -1213,17 +1273,17 @@ export struct NavidromePage {
     if (!artist || !artist.id) {
       return;
     }
-    this.applyFilter(NavFilterType.Artist, artist.id, artist.name ?? Constants.UNKNOWN_ARTIST);
+    void this.applyFilter(NavFilterType.Artist, artist.id, artist.name ?? Constants.UNKNOWN_ARTIST);
   }
 
   private onAlbumSelected(album: NavidromeRestAlbum): void {
     if (!album || !album.id) {
       return;
     }
-    this.applyFilter(NavFilterType.Album, album.id, album.name ?? '未知专辑');
+    void this.applyFilter(NavFilterType.Album, album.id, album.name ?? '未知专辑');
   }
 
-  private applyFilter(type: NavFilterType, id: string, label: string): void {
+  private async applyFilter(type: NavFilterType, id: string, label: string): Promise<void> {
 
     // 记录筛选操作
     void ServerLogUtil.info('NavidromeFilter', `应用筛选:`);
@@ -1240,18 +1300,16 @@ export struct NavidromePage {
     this.isSearchMode = false;
     this.searchText = '';
     this.filteredList = [];
+    this.isSearchLoading = false;
+    this.searchTicket++;
+    this.filterSongs = [];
+    this.isFilterLoading = true;
 
     // 调试日志:检查筛选结果
-    const visibleSongs = this.getVisibleSongs();
-    if (visibleSongs.length > 0) {
-      // 记录筛选结果详情
-      void ServerLogUtil.info('NavidromeFilter', `筛选结果统计:`);
-      void ServerLogUtil.info('NavidromeFilter', `- 筛选后歌曲数: ${visibleSongs.length}`);
-      void ServerLogUtil.info('NavidromeFilter', `- 筛选成功率: ${Math.round(visibleSongs.length / this.allVideos.length * 100)}%`);
-      void ServerLogUtil.debug('NavidromeFilter', `筛选结果示例: ${visibleSongs.slice(0, 5).map(s => `${s.name} (${s.artist})`).join(', ')}`);
-    } else {
-      void ServerLogUtil.warn('NavidromeFilter', `筛选结果为空: 未找到匹配的歌曲`);
-    }
+    void this.loadSongsForFilter().catch((error: Error) => {
+      void ServerLogUtil.error('NavidromeFilter', `筛选歌曲加载失败: ${error.message}`);
+      ToastUtil.showToast(error.message || '筛选数据加载失败');
+    });
 
     // 然后执行动画切换标签页
     this.getUIContext().animateTo({ duration: 555 }, () => {
@@ -1260,10 +1318,48 @@ export struct NavidromePage {
     })
   }
 
+  private async loadSongsForFilter(): Promise<void> {
+    if (this.filterType === NavFilterType.None || !this.filterId) {
+      this.filterSongs = [];
+      this.isFilterLoading = false;
+      return;
+    }
+    const account = this.resolveActiveAccount();
+    if (!account) {
+      this.filterSongs = [];
+      this.isFilterLoading = false;
+      throw new Error('Navidrome账号不可用');
+    }
+    const expectedFilterId = this.filterId;
+    const expectedFilterType = this.filterType;
+    try {
+      let songs: NavidromeRestSong[] = [];
+      if (this.filterType === NavFilterType.Artist) {
+        songs = await navidromeRestApi.fetchSongsByArtist(account, this.filterId);
+      } else if (this.filterType === NavFilterType.Album) {
+        songs = await navidromeRestApi.fetchSongsByAlbum(account, this.filterId);
+      }
+      const videoItems = await this.convertSongsToVideoItems(songs, account);
+      if (expectedFilterId !== this.filterId || expectedFilterType !== this.filterType) {
+        return;
+      }
+      this.filterSongs = videoItems;
+      if (videoItems.length === 0) {
+        void ServerLogUtil.warn('NavidromeFilter', '筛选下暂无歌曲');
+      } else {
+        void ServerLogUtil.info('NavidromeFilter', `筛选歌曲加载完成: ${videoItems.length} 首`);
+      }
+    } finally {
+      this.isFilterLoading = false;
+    }
+  }
+
   private clearFilter(): void {
     this.filterType = NavFilterType.None;
     this.filterId = '';
     this.filterLabel = '';
+    this.filterSongs = [];
+    this.isFilterLoading = false;
   }
 
   private playSong(song: VideoItem, index: number, isJump: boolean = false): void {
@@ -1289,20 +1385,21 @@ export struct NavidromePage {
       void ServerLogUtil.info('NavidromePlay', `- 封面: ${song.pixelMapPath ? '有' : '无'}`);
       void ServerLogUtil.info('NavidromePlay', `- 播放路径: ${song.filePath}`);
 
-      const targetIndex = this.allVideos.findIndex(item => item.id === song.id);
-      const startIndex = targetIndex >= 0 ? targetIndex : index;
+      const playlistSource = this.getVisibleSongs().length > 0 ? this.getVisibleSongs() : this.allVideos;
+      const targetIndex = playlistSource.findIndex(item => item.id === song.id);
+      const startIndex = targetIndex >= 0 ? targetIndex : Math.min(index, Math.max(playlistSource.length - 1, 0));
 
-      void ServerLogUtil.info('NavidromePlay', `播放列表设置: 起始索引 ${startIndex}, 总数 ${this.allVideos.length}`);
+      void ServerLogUtil.info('NavidromePlay', `播放列表设置: 起始索引 ${startIndex}, 总数 ${playlistSource.length}`);
 
-      setNavidromePlaylist(this.allVideos, startIndex);
+      setNavidromePlaylist(playlistSource, startIndex);
 
       const playlistData: PlaylistEventData = {
         playlistId: NAVIDROME_PLAYLIST_ID,
         playlistName: `Navidrome - ${account.name ?? '未知账户'}`,
-        songCount: this.allVideos.length,
+        songCount: playlistSource.length,
         startIndex,
         isJump: isJump,//设置true会弹出播放页
-        songFilePaths: this.allVideos.map(item => item.filePath)
+        songFilePaths: playlistSource.map(item => item.filePath)
       };
 
       const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
@@ -1362,41 +1459,69 @@ export struct NavidromePage {
         .padding({ left: 16, right: 16, top: 12, bottom: 2 })
       }
 
-      List({ space: 8 }) {
-        if (this.selectedTab === 0) {
-          ForEach(this.getVisibleSongs(), (item: VideoItem, index: number) => {
-            ListItem() {
-              this.buildSongItem(item, index)
-            }
-          }, (item: VideoItem) => item.id)
-        } else if (this.selectedTab === 1) {
-          ForEach(this.artists, (artist: NavidromeRestArtist) => {
-            ListItem() {
-              this.buildArtistItem(artist)
-            }
-          }, (artist: NavidromeRestArtist) => artist.id)
-        } else {
-          ForEach(this.albums, (album: NavidromeRestAlbum) => {
-            ListItem() {
-              this.buildAlbumItem(album)
-            }
-          }, (album: NavidromeRestAlbum) => album.id)
+      if (this.isSearchMode && this.selectedTab === 0 && this.searchText.length > 0 && this.isSearchLoading) {
+        Column() {
+          LoadingProgress()
+            .width(40)
+            .height(40)
+            .color(this.themeColor)
+          Text('正在通过 API 搜索歌曲...')
+            .margin({ top: 12 })
+            .fontSize(14)
+            .fontColor($r('app.color.index_tab_unselected_font_color'))
         }
-      }
-      .width('100%')
-      .layoutWeight(1)
-      .cachedCount(3)
-      .padding({ top: 5, bottom: 5 })
-      .listDirection(Axis.Vertical)
+        .width('100%')
+        .layoutWeight(1)
+      } else if (this.selectedTab === 0 && this.filterType !== NavFilterType.None && this.isFilterLoading) {
+        Column() {
+          LoadingProgress()
+            .width(40)
+            .height(40)
+            .color(this.themeColor)
+          Text('正在加载筛选歌曲...')
+            .margin({ top: 12 })
+            .fontSize(14)
+            .fontColor($r('app.color.index_tab_unselected_font_color'))
+        }
+        .width('100%')
+        .layoutWeight(1)
+      } else {
+        List({ space: 8 }) {
+          if (this.selectedTab === 0) {
+            ForEach(this.getVisibleSongs(), (item: VideoItem, index: number) => {
+              ListItem() {
+                this.buildSongItem(item, index)
+              }
+            }, (item: VideoItem) => item.id)
+          } else if (this.selectedTab === 1) {
+            ForEach(this.artists, (artist: NavidromeRestArtist) => {
+              ListItem() {
+                this.buildArtistItem(artist)
+              }
+            }, (artist: NavidromeRestArtist) => artist.id)
+          } else {
+            ForEach(this.albums, (album: NavidromeRestAlbum) => {
+              ListItem() {
+                this.buildAlbumItem(album)
+              }
+            }, (album: NavidromeRestAlbum) => album.id)
+          }
+        }
+        .width('100%')
+        .layoutWeight(1)
+        .cachedCount(3)
+        .padding({ top: 5, bottom: 5 })
+        .listDirection(Axis.Vertical)
       .contentEndOffset(this.bottomSafeHeight+70)
-      .scrollBar(BarState.Auto)
-      .edgeEffect(EdgeEffect.Spring)
-      .onReachEnd(() => {
-        void this.handleReachEnd();
-      })
+        .scrollBar(BarState.Auto)
+        .edgeEffect(EdgeEffect.Spring)
+        .onReachEnd(() => {
+          void this.handleReachEnd();
+        })
+      }
 
       // 空状态
-      if (this.getCurrentCount() === 0) {
+      if (this.getCurrentCount() === 0 && !(this.selectedTab === 0 && this.filterType !== NavFilterType.None && this.isFilterLoading) && !(this.isSearchMode && this.searchText.length > 0 && this.isSearchLoading)) {
         Column() {
           Image($r('app.media.music_red'))
             .width(80)