From 6b5282903e2ed6d454171e374f49a333e7464720 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 27 Dec 2025 10:20:16 +0100 Subject: [PATCH] =?UTF-8?q?Grosse=20correction=20sur=20l'interface=20graph?= =?UTF-8?q?ique=20et=20d=C3=A9bugage=20de=20Open=20Home.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pmoapp/webapp/src/App.vue | 197 ------ .../components/pmocontrol/CurrentTrack.vue | 370 +++++++++- .../pmocontrol/TransportControls.vue | 15 + .../components/pmocontrol/VolumeControl.vue | 34 + .../src/components/unified/BottomTabBar.vue | 45 +- .../components/unified/RendererTabContent.vue | 115 ++-- .../src/components/unified/ServerDrawer.vue | 632 ++++++++++++++++-- pmoapp/webapp/src/composables/useTabs.ts | 16 +- .../webapp/src/views/UnifiedControlView.vue | 59 +- .../src/control_point/openhome_queue.rs | 83 ++- pmocontrol/src/openhome_client.rs | 101 +-- pmocontrol/src/openhome_renderer.rs | 10 +- 12 files changed, 1213 insertions(+), 464 deletions(-) diff --git a/pmoapp/webapp/src/App.vue b/pmoapp/webapp/src/App.vue index a201f637..5da222ac 100644 --- a/pmoapp/webapp/src/App.vue +++ b/pmoapp/webapp/src/App.vue @@ -1,28 +1,5 @@ diff --git a/pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue b/pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue index 77e9ef71..f1b71e49 100644 --- a/pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue +++ b/pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue @@ -1,7 +1,7 @@ @@ -46,8 +160,8 @@ function handleServerClick(server: MediaServerSummary) { @@ -121,22 +335,27 @@ function handleServerClick(server: MediaServerSummary) { .drawer-backdrop { position: fixed; top: 0; - left: 0; + left: 50vw; /* Commence après le drawer (desktop: 50vw) */ right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.35); /* Moins sombre */ backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); z-index: 200; } +@media (max-width: 768px) and (orientation: portrait) { + .drawer-backdrop { + left: 80vw; /* Mobile portrait: 80vw */ + } +} + .server-drawer { position: fixed; top: 0; left: 0; bottom: 0; - width: 320px; - max-width: 80vw; + width: 50vw; /* Desktop/landscape: 50% de l'écran */ background: rgba(255, 255, 255, 0.12); backdrop-filter: blur(30px) saturate(180%); -webkit-backdrop-filter: blur(30px) saturate(180%); @@ -160,6 +379,7 @@ function handleServerClick(server: MediaServerSummary) { display: flex; align-items: center; justify-content: space-between; + gap: var(--spacing-sm); padding: var(--spacing-lg); border-bottom: 1px solid rgba(255, 255, 255, 0.1); flex-shrink: 0; @@ -170,12 +390,42 @@ function handleServerClick(server: MediaServerSummary) { align-items: center; gap: var(--spacing-sm); color: var(--color-text); + flex: 1; + min-width: 0; } .drawer-title { font-size: var(--text-xl); font-weight: 700; margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.drawer-title.small { + font-size: var(--text-base); +} + +.back-btn { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + flex-shrink: 0; + padding: 0; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; + color: var(--color-text); +} + +.back-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); } .drawer-close-btn { @@ -184,6 +434,7 @@ function handleServerClick(server: MediaServerSummary) { justify-content: center; width: 40px; height: 40px; + flex-shrink: 0; padding: 0; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); @@ -202,6 +453,47 @@ function handleServerClick(server: MediaServerSummary) { transform: scale(0.95); } +/* Breadcrumb */ +.breadcrumb { + display: flex; + align-items: center; + gap: 4px; + padding: var(--spacing-sm) var(--spacing-md); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + overflow-x: auto; + flex-shrink: 0; +} + +.breadcrumb-item { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + font-size: var(--text-sm); + color: var(--color-text-secondary); + background: transparent; + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.breadcrumb-item:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--color-text); +} + +.breadcrumb-item.active { + color: var(--color-text); + font-weight: 600; + cursor: default; +} + +.breadcrumb-item.active:hover { + background: transparent; +} + /* Content */ .drawer-content { flex: 1; @@ -307,6 +599,168 @@ function handleServerClick(server: MediaServerSummary) { color: var(--status-offline); } +/* Content list */ +.content-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.content-item { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-md); + border-radius: 8px; + background: rgba(255, 255, 255, 0.05); + transition: all 0.2s ease; + position: relative; +} + +.content-item.navigable { + cursor: pointer; +} + +.content-item.navigable:hover { + background: rgba(255, 255, 255, 0.12); + transform: translateX(2px); +} + +.content-item.navigable:active { + transform: translateX(1px); +} + +.content-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + flex-shrink: 0; + border-radius: 6px; + background: rgba(255, 255, 255, 0.08); + color: var(--color-text-secondary); +} + +.content-info { + flex: 1; + min-width: 0; +} + +.content-title { + font-size: var(--text-sm); + font-weight: 500; + color: var(--color-text); + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.content-subtitle { + font-size: var(--text-xs); + color: var(--color-text-secondary); + margin: 2px 0 0 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.content-chevron { + flex-shrink: 0; + color: var(--color-text-tertiary); +} + +/* Actions pour items jouables */ +.content-actions { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + opacity: 1; /* Toujours visible pour le tactile */ + transition: all 0.2s ease; + z-index: 1; /* Au-dessus pour capturer les clicks */ +} + +.action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + color: var(--color-text); + pointer-events: auto; /* S'assurer que les boutons capturent les clicks */ +} + +.action-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); +} + +.action-btn:active { + transform: scale(0.95); +} + +.play-btn { + color: var(--color-primary); + border-color: var(--color-primary); +} + +.play-btn:hover { + background: var(--color-primary); + color: white; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4); +} + +.queue-btn { + color: var(--color-text-secondary); +} + +.queue-btn:hover { + background: rgba(255, 255, 255, 0.25); + color: var(--color-text); +} + +/* Loading state */ +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--spacing-md); + padding: var(--spacing-2xl); + color: var(--color-text-secondary); +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid rgba(255, 255, 255, 0.2); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loading-state p { + margin: 0; + font-size: var(--text-sm); +} + .empty-servers { display: flex; flex-direction: column; @@ -323,10 +777,51 @@ function handleServerClick(server: MediaServerSummary) { font-size: var(--text-base); } +/* Footer */ +.drawer-footer { + flex-shrink: 0; + padding: var(--spacing-md); + border-top: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.1); +} + +.settings-btn { + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + width: 100%; + padding: var(--spacing-md); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + color: var(--color-text-secondary); + font-size: var(--text-sm); + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.settings-btn:hover { + background: rgba(255, 255, 255, 0.2); + color: var(--color-text); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.settings-btn:active { + transform: translateY(0); +} + /* Animations */ -.backdrop-enter-active, +.backdrop-enter-active { + transition: opacity 0.2s ease; + transition-delay: 0.15s; /* Attend que le drawer soit à moitié visible */ +} + .backdrop-leave-active { - transition: opacity 0.3s ease; + transition: opacity 0.2s ease; + /* Pas de delay au leave - disparaît en même temps que le drawer */ } .backdrop-enter-from, @@ -345,28 +840,33 @@ function handleServerClick(server: MediaServerSummary) { } /* Scrollbar styling */ -.drawer-content::-webkit-scrollbar { +.drawer-content::-webkit-scrollbar, +.breadcrumb::-webkit-scrollbar { width: 6px; + height: 6px; } -.drawer-content::-webkit-scrollbar-track { +.drawer-content::-webkit-scrollbar-track, +.breadcrumb::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); border-radius: 3px; } -.drawer-content::-webkit-scrollbar-thumb { +.drawer-content::-webkit-scrollbar-thumb, +.breadcrumb::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 3px; } -.drawer-content::-webkit-scrollbar-thumb:hover { +.drawer-content::-webkit-scrollbar-thumb:hover, +.breadcrumb::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); } -/* Mobile responsive */ -@media (max-width: 768px) { +/* Mobile responsive - portrait */ +@media (max-width: 768px) and (orientation: portrait) { .server-drawer { - width: 280px; + width: 80vw; /* Mobile portrait: 80% de l'écran */ } .drawer-header { diff --git a/pmoapp/webapp/src/composables/useTabs.ts b/pmoapp/webapp/src/composables/useTabs.ts index ee6edd56..dcaf1527 100644 --- a/pmoapp/webapp/src/composables/useTabs.ts +++ b/pmoapp/webapp/src/composables/useTabs.ts @@ -4,6 +4,7 @@ * Onglets server ouverts manuellement via le drawer (fermables). */ import { reactive, computed, watch, onMounted, type Component } from 'vue' +import { useMediaQuery } from '@vueuse/core' import { Radio, Server } from 'lucide-vue-next' import type { RendererSummary, MediaServerSummary } from '../services/pmocontrol/types' @@ -28,7 +29,6 @@ interface TabsState { const MAX_TABS = 12 // Augmenté car onglets auto-générés const STORAGE_KEY = 'pmo-tabs-state' -const COMPACT_MODE_THRESHOLD = 5 // Passer en mode icônes seulement au-delà de 5 onglets // État global partagé entre toutes les instances du composable const state = reactive({ @@ -41,10 +41,12 @@ const state = reactive({ let isRestoringFromStorage = false /** - * Tronque un titre pour mobile si nécessaire + * Retourne le titre complet sans troncature + * Note: On laisse le CSS gérer l'overflow avec ellipsis pour un affichage stable */ -function truncateTitle(title: string, maxLength = 15): string { - return title.length > maxLength ? title.slice(0, maxLength) + '...' : title +function truncateTitle(title: string): string { + // Retourner le titre complet, le CSS gère l'ellipsis de façon stable + return title } /** @@ -345,12 +347,16 @@ export function useTabs() { } }) + // Détection de la largeur d'écran pour le mode compact + // Mode compact sur les écrans < 900px (tablettes et mobiles) + const isNarrowScreen = useMediaQuery('(max-width: 900px)') + // Computed properties const activeTab = computed(() => findTab(state.activeTabId) || state.tabs[0] || null) const hasMultipleTabs = computed(() => state.tabs.length > 1) const canAddTab = computed(() => state.tabs.length < MAX_TABS) const isEmpty = computed(() => state.tabs.length === 0) - const compactMode = computed(() => state.tabs.length > COMPACT_MODE_THRESHOLD) + const compactMode = computed(() => isNarrowScreen.value) return { // State diff --git a/pmoapp/webapp/src/views/UnifiedControlView.vue b/pmoapp/webapp/src/views/UnifiedControlView.vue index 614a2343..fad696ba 100644 --- a/pmoapp/webapp/src/views/UnifiedControlView.vue +++ b/pmoapp/webapp/src/views/UnifiedControlView.vue @@ -5,7 +5,8 @@ import { useTabs } from '@/composables/useTabs' import { useRenderers } from '@/composables/useRenderers' import { useMediaServers } from '@/composables/useMediaServers' import { useSwipe } from '@vueuse/core' -import type { MediaServerSummary } from '@/services/pmocontrol/types' +import { api } from '@/services/pmocontrol/api' +import type { ContainerEntry } from '@/services/pmocontrol/types' // Import des composants import BottomTabBar from '@/components/unified/BottomTabBar.vue' @@ -16,7 +17,7 @@ import ServerTabContent from '@/components/unified/ServerTabContent.vue' const route = useRoute() const router = useRouter() -const { tabs, activeTabId, switchTab, activeTab, syncWithRenderers, isEmpty, openServer } = useTabs() +const { tabs, activeTabId, switchTab, activeTab, syncWithRenderers, isEmpty } = useTabs() const { allRenderers, fetchRenderers } = useRenderers() const { allServers, fetchServers } = useMediaServers() @@ -41,9 +42,50 @@ useSwipe(viewRef, { }, }) -// Gestion de l'ouverture d'un server depuis le drawer -function handleServerSelected(server: MediaServerSummary) { - openServer(server) +// Gestion de la lecture d'un item depuis le drawer +async function handlePlayItem(item: ContainerEntry, serverId: string) { + const currentTab = activeTab.value + + if (!currentTab || currentTab.type !== 'renderer' || !currentTab.metadata?.rendererId) { + console.error('[UnifiedControlView] Pas de renderer actif') + return + } + + const rendererId = currentTab.metadata.rendererId + + try { + console.log('[UnifiedControlView] Play item:', item.title, 'on renderer:', rendererId) + + await api.attachPlaylist(rendererId, serverId, item.id, true) // autoPlay = true + + // Fermer le drawer après succès + drawerOpen.value = false + } catch (error) { + console.error('[UnifiedControlView] Erreur lors de la lecture:', error) + } +} + +// Gestion de l'ajout d'un item à la queue depuis le drawer +async function handleQueueItem(item: ContainerEntry, serverId: string) { + const currentTab = activeTab.value + + if (!currentTab || currentTab.type !== 'renderer' || !currentTab.metadata?.rendererId) { + console.error('[UnifiedControlView] Pas de renderer actif') + return + } + + const rendererId = currentTab.metadata.rendererId + + try { + console.log('[UnifiedControlView] Queue item:', item.title, 'on renderer:', rendererId) + + await api.attachPlaylist(rendererId, serverId, item.id, false) // autoPlay = false + + // Fermer le drawer après succès + drawerOpen.value = false + } catch (error) { + console.error('[UnifiedControlView] Erreur lors de l\'ajout à la queue:', error) + } } // Nombre de servers online pour afficher dans le badge @@ -163,7 +205,11 @@ const currentTabProps = computed(() => { - + @@ -182,6 +228,7 @@ const currentTabProps = computed(() => { overflow-y: auto; overflow-x: hidden; padding: 0; + padding-bottom: 80px; /* Espace pour la barre fixe en bas (64px + marge) */ position: relative; } diff --git a/pmocontrol/src/control_point/openhome_queue.rs b/pmocontrol/src/control_point/openhome_queue.rs index 3e861280..df8c24f2 100644 --- a/pmocontrol/src/control_point/openhome_queue.rs +++ b/pmocontrol/src/control_point/openhome_queue.rs @@ -62,51 +62,52 @@ impl OpenHomeQueue { // 1. Info.Id() - Direct ID query (fastest, but fails if track no longer in playlist) // 2. Info.Track() - Returns URI, which we can search for (works even if track removed) // 3. None - No current track can be determined - let current_id = self.info_client.as_ref().and_then(|client| { - // Try Info.Id() first - if let Ok(id) = client.id() { - debug!( - renderer = self.renderer_id.0.as_str(), - track_id = id, - "Detected current track via Info.Id()" - ); - return Some(id); - } + let current_id = self.playlist.id()?; + // { + // // Try Info.Id() first + // if let Ok(id) = client.id() { + // debug!( + // renderer = self.renderer_id.0.as_str(), + // track_id = id, + // "Detected current track via Info.Id()" + // ); + // return Some(id); + // } - // If Id() fails, try Track() to get the URI and search for it - if let Ok(track_info) = client.track() { - debug!( - renderer = self.renderer_id.0.as_str(), - track_uri = track_info.uri.as_str(), - "Info.Id() failed, searching for current track by URI from Info.Track()" - ); - return entries - .iter() - .find(|entry| entry.uri == track_info.uri) - .map(|entry| { - debug!( - renderer = self.renderer_id.0.as_str(), - found_id = entry.id, - found_uri = entry.uri.as_str(), - "Found current track ID by matching URI" - ); - entry.id - }); - } + // // If Id() fails, try Track() to get the URI and search for it + // if let Ok(track_info) = client.track() { + // debug!( + // renderer = self.renderer_id.0.as_str(), + // track_uri = track_info.uri.as_str(), + // "Info.Id() failed, searching for current track by URI from Info.Track()" + // ); + // return entries + // .iter() + // .find(|entry| entry.uri == track_info.uri) + // .map(|entry| { + // debug!( + // renderer = self.renderer_id.0.as_str(), + // found_id = entry.id, + // found_uri = entry.uri.as_str(), + // "Found current track ID by matching URI" + // ); + // entry.id + // }); + // } - debug!( - renderer = self.renderer_id.0.as_str(), - "Both Info.Id() and Info.Track() failed, cannot determine current track" - ); - None - }); + // debug!( + // renderer = self.renderer_id.0.as_str(), + // "Both Info.Id() and Info.Track() failed, cannot determine current track" + // ); + // None + // }); - let current_index = current_id - .and_then(|id| track_ids.iter().position(|entry_id| *entry_id == id)); + // let current_index = current_id + // .and_then(|id| track_ids.iter().position(|entry_id| *entry_id == id)); self.items = items; self.track_ids = track_ids; - self.current_index = current_index; + self.current_index = Some(current_id as usize); Ok(()) } @@ -844,9 +845,7 @@ impl QueueBackend for OpenHomeQueue { // Note: Some OpenHome renderers (like upmpdcli) don't reliably support Info.Id(), // so we fall back to using our internal current_index pointer. let currently_playing_id_from_renderer = self - .info_client - .as_ref() - .and_then(|client| client.id().ok()); + .playlist.id().ok(); // Find the currently playing item in our local state. // Priority: 1) Renderer-reported ID, 2) Our internal current_index diff --git a/pmocontrol/src/openhome_client.rs b/pmocontrol/src/openhome_client.rs index d820ecf5..b866c6fb 100644 --- a/pmocontrol/src/openhome_client.rs +++ b/pmocontrol/src/openhome_client.rs @@ -85,7 +85,7 @@ impl OhPlaylistClient { .ok_or_else(|| anyhow!("Missing ReadListResponse element in SOAP body"))?; let track_list_b64 = - extract_child_text_any(response, &["aTrackList", "TrackList", "aValue", "Value"])?; + extract_child_text_any(response, &["TrackList", "Value"])?; let track_list_sample: String = track_list_b64.chars().take(256).collect(); debug!( control_url = self.control_url.as_str(), @@ -111,10 +111,10 @@ impl OhPlaylistClient { let response = find_child_with_suffix(&envelope.body.content, "InsertResponse") .ok_or_else(|| anyhow!("Missing InsertResponse element in SOAP body"))?; let new_id_text = - extract_child_text_any(response, &["aNewId", "NewId", "aValue", "Value"])?; + extract_child_text_any(response, &["NewId", "Value"])?; let new_id = new_id_text .parse::() - .map_err(|_| anyhow!("Invalid aNewId value: {}", new_id_text))?; + .map_err(|_| anyhow!("Invalid NewId value: {}", new_id_text))?; Ok(new_id) } @@ -129,6 +129,31 @@ impl OhPlaylistClient { handle_action_response("SeekId", &call_result) } + pub fn transport_state(&self) -> Result { + let call_result = + invoke_upnp_action(&self.control_url, &self.service_type, "TransportState", &[])?; + + let envelope = ensure_success("TransportState", &call_result)?; + let response = find_child_with_suffix(&envelope.body.content, "TransportStateResponse") + .ok_or_else(|| anyhow!("Missing TransportStateResponse element in SOAP body"))?; + let state = extract_child_text_any(response, &["State", "Value"])?; + Ok(state) + } + + pub fn id(&self) -> Result { + let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Id", &[])?; + + let envelope = ensure_success("Id", &call_result)?; + let response = find_child_with_suffix(&envelope.body.content, "IdResponse") + .ok_or_else(|| anyhow!("Missing IdResponse element in SOAP body"))?; + let id_text = extract_child_text(response, "Id")?; + let id = id_text + .parse::() + .map_err(|_| anyhow!("Invalid Info.Id value: {}", id_text))?; + Ok(id) + } + + pub fn play(&self) -> Result<()> { let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Play", &[])?; handle_action_response("Play", &call_result) @@ -227,10 +252,10 @@ impl OhPlaylistClient { let envelope = ensure_success("Id", &call_result)?; let response = find_child_with_suffix(&envelope.body.content, "IdResponse") .ok_or_else(|| anyhow!("Missing IdResponse element in SOAP body"))?; - let value: String = extract_child_text_any(response, &["aValue", "Value"])?; + let value: String = extract_child_text_any(response, &["Value"])?; Ok(value) } - + pub fn tracks_max(&self) -> Result { let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "TracksMax", &[])?; @@ -238,7 +263,7 @@ impl OhPlaylistClient { let envelope = ensure_success("TracksMax", &call_result)?; let response = find_child_with_suffix(&envelope.body.content, "TracksMaxResponse") .ok_or_else(|| anyhow!("Missing TracksMaxResponse element in SOAP body"))?; - let value_text: String = extract_child_text_any(response, &["aValue", "Value"])?; + let value_text: String = extract_child_text_any(response, &["Value"])?; let value = value_text .parse::() .map_err(|_| anyhow!("Invalid TracksMax value: {}", value_text))?; @@ -256,7 +281,7 @@ impl OhPlaylistClient { // Try to extract the array element. If missing, assume empty playlist. let array_text = match extract_child_text_any( response, - &["aArray", "Array", "aIdArray", "IdArray", "aValue", "Value"], + &["Array", "IdArray", "Value"], ) { Ok(text) => text, Err(_) => { @@ -355,9 +380,9 @@ impl OhInfoClient { let response = find_child_with_suffix(&envelope.body.content, "TrackResponse") .ok_or_else(|| anyhow!("Missing TrackResponse element in SOAP body"))?; - let uri = extract_child_text_any(response, &["aUri", "Uri", "aValue", "Value"]) + let uri = extract_child_text_any(response, &["Uri", "Uri", "Value"]) .unwrap_or_default(); - let metadata_xml = extract_child_text_optional(response, "aMetadata") + let metadata_xml = extract_child_text_optional(response, "Metadata") .unwrap_or(None) .filter(|s| !s.is_empty()); @@ -382,39 +407,15 @@ impl OhInfoClient { let response = find_child_with_suffix(&envelope.body.content, "NextResponse") .ok_or_else(|| anyhow!("Missing NextResponse element in SOAP body"))?; - let uri = extract_child_text_any(response, &["aUri", "Uri", "aValue", "Value"]) + let uri = extract_child_text_any(response, &["Uri", "Value"]) .unwrap_or_default(); - let metadata_xml = extract_child_text_optional(response, "aMetadata") + let metadata_xml = extract_child_text_optional(response, "Metadata") .unwrap_or(None) .filter(|s| !s.is_empty()); Ok(OhInfoTrack { uri, metadata_xml }) } - pub fn id(&self) -> Result { - let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Id", &[])?; - - let envelope = ensure_success("Id", &call_result)?; - let response = find_child_with_suffix(&envelope.body.content, "IdResponse") - .ok_or_else(|| anyhow!("Missing IdResponse element in SOAP body"))?; - let id_text = extract_child_text(response, "aId")?; - let id = id_text - .parse::() - .map_err(|_| anyhow!("Invalid Info.Id value: {}", id_text))?; - Ok(id) - } - - pub fn transport_state(&self) -> Result { - let call_result = - invoke_upnp_action(&self.control_url, &self.service_type, "TransportState", &[])?; - - let envelope = ensure_success("TransportState", &call_result)?; - let response = find_child_with_suffix(&envelope.body.content, "TransportStateResponse") - .ok_or_else(|| anyhow!("Missing TransportStateResponse element in SOAP body"))?; - let state = extract_child_text_any(response, &["aState", "State", "aValue", "Value"])?; - Ok(state) - } - pub fn read_current_metadata(&self) -> Result> { let track = self.track()?; Ok(track.metadata()) @@ -443,17 +444,17 @@ impl OhTimeClient { .ok_or_else(|| anyhow!("Missing TimeResponse element in SOAP body"))?; let track_count = - extract_child_text_any(response, &["aTrackCount", "TrackCount", "aValue", "Value"])? + extract_child_text_any(response, &["TrackCount", "Value"])? .parse::() - .map_err(|_| anyhow!("Invalid aTrackCount value in Time response"))?; + .map_err(|_| anyhow!("Invalid TrackCount value in Time response"))?; let duration_secs = - extract_child_text_any(response, &["aDuration", "Duration", "aValue", "Value"])? + extract_child_text_any(response, &["Duration","Value"])? .parse::() - .map_err(|_| anyhow!("Invalid aDuration value in Time response"))?; + .map_err(|_| anyhow!("Invalid Duration value in Time response"))?; let elapsed_secs = - extract_child_text_any(response, &["aSeconds", "Seconds", "aValue", "Value"])? + extract_child_text_any(response, &["Seconds", "Value"])? .parse::() - .map_err(|_| anyhow!("Invalid aSeconds value in Time response"))?; + .map_err(|_| anyhow!("Invalid Seconds value in Time response"))?; Ok(OhTimePosition { track_count, @@ -482,7 +483,7 @@ impl OhVolumeClient { let envelope = ensure_success("Volume", &call_result)?; let response = find_child_with_suffix(&envelope.body.content, "VolumeResponse") .ok_or_else(|| anyhow!("Missing VolumeResponse element in SOAP body"))?; - let value = extract_child_text_any(response, &["aVolume", "Volume", "aValue", "Value"])?; + let value = extract_child_text_any(response, &["Volume", "Value"])?; let parsed = value .parse::() .map_err(|_| anyhow!("Invalid volume value: {}", value))?; @@ -502,7 +503,7 @@ impl OhVolumeClient { let envelope = ensure_success("Mute", &call_result)?; let response = find_child_with_suffix(&envelope.body.content, "MuteResponse") .ok_or_else(|| anyhow!("Missing MuteResponse element in SOAP body"))?; - let value = extract_child_text_any(response, &["aMute", "Mute", "aValue", "Value"])?; + let value = extract_child_text_any(response, &["Mute", "Value"])?; parse_bool(&value) } @@ -547,9 +548,9 @@ impl OhRadioClient { let response = find_child_with_suffix(&envelope.body.content, "ChannelResponse") .ok_or_else(|| anyhow!("Missing ChannelResponse element in SOAP body"))?; - let uri = extract_child_text_any(response, &["aUri", "Uri", "aValue", "Value"]) + let uri = extract_child_text_any(response, &["Uri", "Value"]) .unwrap_or_default(); - let metadata_xml = extract_child_text_optional(response, "aMetadata") + let metadata_xml = extract_child_text_optional(response, "Metadata") .unwrap_or(None) .filter(|s| !s.is_empty()); @@ -577,7 +578,7 @@ impl OhProductClient { let envelope = ensure_success("SourceXml", &call_result)?; let response = find_child_with_suffix(&envelope.body.content, "SourceXmlResponse") .ok_or_else(|| anyhow!("Missing SourceXmlResponse element in SOAP body"))?; - let xml = extract_child_text_any(response, &["aSourceXml", "aXml", "aValue", "Value"])?; + let xml = extract_child_text_any(response, &["SourceXml", "Xml", "Value"])?; parse_product_source_list(&xml) } @@ -587,7 +588,7 @@ impl OhProductClient { let envelope = ensure_success("SourceIndex", &call_result)?; let response = find_child_with_suffix(&envelope.body.content, "SourceIndexResponse") .ok_or_else(|| anyhow!("Missing SourceIndexResponse element in SOAP body"))?; - let value = extract_child_text_any(response, &["aIndex", "Index", "aValue", "Value"])?; + let value = extract_child_text_any(response, &["Index", "Value"])?; value .parse::() .map_err(|_| anyhow!("Invalid Product.SourceIndex value: {}", value)) @@ -998,7 +999,7 @@ mod tests { let mut cursor = Cursor::new(xml.as_bytes()); let response = Element::parse(&mut cursor).expect("valid xml"); let value = - extract_child_text_any(&response, &["aNewId", "NewId", "aValue", "Value"]).unwrap(); + extract_child_text_any(&response, &["NewId", "Value"]).unwrap(); assert_eq!(value, "1337"); } @@ -1008,7 +1009,7 @@ mod tests { let mut cursor = Cursor::new(xml.as_bytes()); let response = Element::parse(&mut cursor).expect("valid xml"); let value = - extract_child_text_any(&response, &["aTrackList", "TrackList", "aValue", "Value"]) + extract_child_text_any(&response, &["TrackList", "Value"]) .expect("tracklist"); assert_eq!(value, "PGVudHJ5PjwvZW50cnk+"); } @@ -1020,7 +1021,7 @@ mod tests { let response = Element::parse(&mut cursor).expect("valid xml"); let value = extract_child_text_any( &response, - &["aArray", "Array", "aIdArray", "IdArray", "aValue", "Value"], + &["Array", "IdArray", "Value"], ) .expect("array content"); assert_eq!(value, "AAAAAQAAAAI="); diff --git a/pmocontrol/src/openhome_renderer.rs b/pmocontrol/src/openhome_renderer.rs index 3f325018..a78a7f3e 100644 --- a/pmocontrol/src/openhome_renderer.rs +++ b/pmocontrol/src/openhome_renderer.rs @@ -120,7 +120,7 @@ impl OpenHomeRenderer { // Essayer d'obtenir current_id depuis Info.Id() let mut current_id = self - .info_client + .playlist .as_ref() .and_then(|client| { match client.id() { @@ -316,7 +316,7 @@ impl VolumeControl for OpenHomeRenderer { impl PlaybackStatus for OpenHomeRenderer { fn playback_state(&self) -> Result { - let client = self.info_client_for("playback_state")?; + let client = self.playlist_client_for("playback_state")?; let state = client.transport_state()?; Ok(map_openhome_state(&state)) } @@ -330,8 +330,8 @@ impl PlaybackPosition for OpenHomeRenderer { let mut track_uri = None; let mut track_metadata_xml = None; - if let Some(info_client) = &self.info_client { - match info_client.id() { + if let Some(playlist_client) = &self.playlist { + match playlist_client.id() { Ok(id) => track_id = Some(id), Err(err) => debug!( renderer = self.info.id.0.as_str(), @@ -339,7 +339,9 @@ impl PlaybackPosition for OpenHomeRenderer { "Failed to read OpenHome track id" ), } + } + if let Some(info_client) = &self.info_client { match info_client.track() { Ok(track) => { track_uri = Some(track.uri);