From 0dab3077cff93e67b1a8e625d6e72fbf256e4d01 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Thu, 11 Jun 2026 21:50:10 +0200 Subject: [PATCH] feat: unify search state and dynamic container routing Refactor search handling across the frontend and backend to use a unified reactive state and dynamic container ID generation. The frontend now leverages a centralized `useMediaServers` composable for search queries and results, while the backend computes container IDs dynamically from source entries instead of using hardcoded values. Bumps version to 0.3.52. --- Cargo.lock | 2 +- PMOMusic/Cargo.toml | 2 +- .../components/pmocontrol/MediaBrowser.vue | 18 ++--- .../src/components/unified/ServerDrawer.vue | 70 +++++-------------- .../webapp/src/composables/useMediaServers.ts | 17 +---- pmocontrol/src/pmoserver_ext.rs | 22 +++++- pmoqobuz/src/source.rs | 15 ++-- version.txt | 2 +- 8 files changed, 57 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6106781..c5bde027 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "PMOMusic" -version = "0.3.51" +version = "0.3.52" dependencies = [ "axum 0.8.7", "console-subscriber", diff --git a/PMOMusic/Cargo.toml b/PMOMusic/Cargo.toml index 2f8ba0ef..efbbf138 100644 --- a/PMOMusic/Cargo.toml +++ b/PMOMusic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "PMOMusic" -version = "0.3.51" +version = "0.3.52" edition = "2024" [dependencies] diff --git a/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue b/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue index 06ea465e..e1861b5c 100644 --- a/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue +++ b/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue @@ -22,17 +22,14 @@ const { loading, loadingMore, error, - searchResults, - searchQuery, searchServer, - clearSearch, } = useMediaServers(); const searchInput = ref(''); async function handleSearch() { if (searchInput.value.trim()) { - const virtualId = await searchServer(props.serverId, searchInput.value.trim(), props.containerId); + const virtualId = await searchServer(props.serverId, searchInput.value.trim()); if (virtualId) { emit("navigate", virtualId); } @@ -41,11 +38,8 @@ async function handleSearch() { function handleClearSearch() { searchInput.value = ''; - clearSearch(); } -const isSearchMode = computed(() => searchQuery.value !== ''); - const { playContent, addToQueue, attachAndPlayPlaylist, attachPlaylist } = useRenderers(); const uiStore = useUIStore(); @@ -55,9 +49,7 @@ const sentinelRef = ref(null); let observer: IntersectionObserver | null = null; const browseData = computed(() => - isSearchMode.value - ? searchResults.value - : getBrowseCached(props.serverId, props.containerId), + getBrowseCached(props.serverId, props.containerId), ); const containers = computed( @@ -68,7 +60,7 @@ const items = computed( () => browseData.value?.entries.filter((e) => !e.is_container) || [], ); -const canLoadMore = computed(() => !isSearchMode.value && hasMore(props.serverId, props.containerId)); +const canLoadMore = computed(() => hasMore(props.serverId, props.containerId)); function setupObserver() { if (observer) observer.disconnect(); @@ -201,7 +193,7 @@ async function handleQueueItem(itemId: string, rendererId: string) { @keyup.enter="handleSearch" /> - -
- - -
- - - - - -
  • ([]) -const searchResults = ref(null) -const searchQuery = ref('') const CACHE_DURATION_MS = 2000 const BROWSE_WINDOW_SIZE = 200 @@ -215,14 +213,14 @@ export function useMediaServers() { } // Recherche dans un serveur — retourne l'ID du container virtuel de résultats - async function searchServer(serverId: string, query: string, context?: string): Promise { + async function searchServer(serverId: string, query: string): Promise { if (!query.trim()) return null try { loading.value = true error.value = null - const data = await api.searchServer(serverId, query, context) + const data = await api.searchServer(serverId, query) // data.container_id est l'ID virtuel réel (ex: "qobuz:search:catalog:all:camille") const key = browseCacheKey(serverId, data.container_id) browseCache.value.set(key, { @@ -240,11 +238,6 @@ export function useMediaServers() { } } - function clearSearch() { - searchResults.value = null - searchQuery.value = '' - } - // Getters function getServerById(id: string) { return serversCache.value.get(id) @@ -299,13 +292,9 @@ export function useMediaServers() { getServerById, getBrowseCached, hasMore, - // Search - searchResults, - searchQuery, - searchServer, - clearSearch, // Actions fetchServers, + searchServer, browseContainer, loadMoreBrowse, setPath, diff --git a/pmocontrol/src/pmoserver_ext.rs b/pmocontrol/src/pmoserver_ext.rs index 1bcfdce4..ecaac228 100644 --- a/pmocontrol/src/pmoserver_ext.rs +++ b/pmocontrol/src/pmoserver_ext.rs @@ -8,6 +8,8 @@ use crate::control_point::ControlPoint; #[cfg(feature = "pmoserver")] use crate::media_server::{MediaBrowser, playback_item_from_entry}; #[cfg(feature = "pmoserver")] +use crate::MediaEntry; +#[cfg(feature = "pmoserver")] use crate::model::{RendererCapabilities, RendererProtocol}; #[cfg(feature = "pmoserver")] use crate::openapi::{ @@ -2392,6 +2394,22 @@ struct SearchQuery { q: String, } +#[cfg(feature = "pmoserver")] +fn search_result_container_id(entries: &[ContainerEntry]) -> String { + entries + .iter() + .find(|entry| entry.is_container) + .and_then(|entry| { + let parts: Vec<&str> = entry.id.splitn(5, ':').collect(); + if parts.len() == 5 && parts[0] == "qobuz" && parts[1] == "search" { + Some(format!("qobuz:search:{}:all:{}", parts[2], parts[4])) + } else { + None + } + }) + .unwrap_or_else(|| "search".to_string()) +} + /// GET /control/servers/{server_id}/search?q= - Recherche dans un serveur #[cfg(feature = "pmoserver")] #[utoipa::path( @@ -2503,8 +2521,10 @@ async fn search_server( }) .collect(); + let container_id = search_result_container_id(&container_entries); + Ok(Json(BrowseResponse { - container_id: "search".to_string(), + container_id, entries: container_entries, total_count, offset: 0, diff --git a/pmoqobuz/src/source.rs b/pmoqobuz/src/source.rs index f234119c..a37b52f4 100644 --- a/pmoqobuz/src/source.rs +++ b/pmoqobuz/src/source.rs @@ -1898,7 +1898,8 @@ impl MusicSource for QobuzSource { } ObjectIdType::SearchResult(scope, media_type, query) => { - tracing::debug!(scope = ?scope, media_type = ?media_type, query, "browse → execute_search"); + tracing::debug!(scope = ?scope, media_type = ?media_type, query, "browse → search"); + let is_all_search = media_type == MediaSearchType::All; let sq = SearchQuery { text: query, media_type, @@ -1906,9 +1907,11 @@ impl MusicSource for QobuzSource { limit: 200, offset: 0, }; - let result = self.execute_search(&sq).await; - tracing::debug!(ok = result.is_ok(), "execute_search returned"); - result + if is_all_search { + self.search_grouped(&sq).await + } else { + self.execute_search(&sq).await + } } ObjectIdType::Track(_) => { @@ -2410,10 +2413,11 @@ impl QobuzSource { }; let (n_albums, n_artists, n_tracks, n_playlists) = counts; + let parent_id = format!("qobuz:search:{}:all:{}", scope_str, query.text); let mk_container = |type_str: &str, title: &str, count: usize| Container { id: format!("qobuz:search:{}:{}:{}", scope_str, type_str, query.text), - parent_id: "qobuz:search".to_string(), + parent_id: parent_id.clone(), restricted: Some("1".to_string()), child_count: Some(count.to_string()), searchable: None, @@ -2466,7 +2470,6 @@ impl QobuzSource { use tracing::debug; debug!(text = %query.text, scope = ?query.scope, media_type = ?query.media_type, "execute_search entry"); let text = &query.text; - let limit = query.limit; let scope_str = match query.scope { SearchScope::Catalog => "catalog", SearchScope::UserLibrary => "favorites", diff --git a/version.txt b/version.txt index 6b9aa4e6..d57e08b5 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.3.51 +0.3.52