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"
/>
-
-
-
Aucun résultat
-
- -
-
-
![]()
-
-
-
-
-
-
-
{{ item.title }}
-
{{ item.artist }}
-
-
-
-
-
-
-
- ([])
-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