From da12cc82b3be470cdf0b66e6e05e26b8e0cb354f Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 11 Jan 2026 15:26:33 +0100 Subject: [PATCH 1/3] Refactor playback and queue commands to use async background tasks Replace blocking tasks with async background tasks for play, add to queue, and add after current commands. This improves responsiveness by launching commands in the background and returning immediately, with UI updates handled via SSE events. Remove timeout handling and error wrapping as the async task management now handles these cases properly. --- pmocontrol/src/pmoserver_ext.rs | 328 +++++++++++++++----------------- 1 file changed, 149 insertions(+), 179 deletions(-) diff --git a/pmocontrol/src/pmoserver_ext.rs b/pmocontrol/src/pmoserver_ext.rs index c12a2f91..8da92766 100644 --- a/pmocontrol/src/pmoserver_ext.rs +++ b/pmocontrol/src/pmoserver_ext.rs @@ -1497,87 +1497,79 @@ async fn play_content( })?; let control_point = Arc::clone(&state.control_point); + let rid_for_log = rid.clone(); + let object_id_for_log = object_id.clone(); + let object_id_for_debug = object_id_for_log.clone(); - // Spawn blocking task for content loading - let play_task = tokio::task::spawn_blocking(move || { - // Fetch playback items from server - let items = fetch_playback_items(&control_point, &sid, &object_id)?; + // Launch the command in background and return immediately + // The UI will be updated via SSE events when playback starts + tokio::task::spawn(async move { + let result = tokio::task::spawn_blocking(move || { + // Fetch playback items from server + let items = fetch_playback_items(&control_point, &sid, &object_id)?; - if items.is_empty() { - return Err(anyhow::anyhow!("No playable content found")); + if items.is_empty() { + return Err(anyhow::anyhow!("No playable content found")); + } + + if items.len() > 1 { + debug!( + renderer = rid.0.as_str(), + server = sid.0.as_str(), + object = object_id.as_str(), + item_count = items.len(), + "Auto-binding playlist to renderer queue (auto_play = true)" + ); + control_point.attach_queue_to_playlist_with_options( + &rid, + sid.clone(), + object_id.clone(), + true, + )?; + return Ok(()); + } + + // Clear queue + control_point.clear_queue(&rid)?; + + // Enqueue items + control_point.enqueue_items(&rid, items)?; + + // Start playback + // Pour les renderers OpenHome, play_current_from_queue() va gérer automatiquement + // la lecture depuis la playlist native si elle existe + control_point.play_current_from_queue(&rid)?; + + Ok::<(), anyhow::Error>(()) + }) + .await; + + match result { + Ok(Ok(())) => { + debug!( + "Successfully started playing content {} on renderer {}", + object_id_for_log, rid_for_log.0 + ); + } + Ok(Err(e)) => { + warn!( + "Failed to play content on renderer {}: {}", + rid_for_log.0, e + ); + } + Err(e) => { + warn!( + "Task join error during play content for renderer {}: {}", + rid_for_log.0, e + ); + } } - - if items.len() > 1 { - debug!( - renderer = rid.0.as_str(), - server = sid.0.as_str(), - object = object_id.as_str(), - item_count = items.len(), - "Auto-binding playlist to renderer queue (auto_play = true)" - ); - control_point.attach_queue_to_playlist_with_options( - &rid, - sid.clone(), - object_id.clone(), - true, - )?; - return Ok(()); - } - - // Clear queue - control_point.clear_queue(&rid)?; - - // Enqueue items - control_point.enqueue_items(&rid, items)?; - - // Start playback - // Pour les renderers OpenHome, play_current_from_queue() va gérer automatiquement - // la lecture depuis la playlist native si elle existe - control_point.play_current_from_queue(&rid)?; - - Ok::<(), anyhow::Error>(()) }); - time::timeout(QUEUE_COMMAND_TIMEOUT, play_task) - .await - .map_err(|_| { - warn!( - "Play content command for renderer {} exceeded {:?}", - renderer_id, QUEUE_COMMAND_TIMEOUT - ); - ( - StatusCode::GATEWAY_TIMEOUT, - Json(ErrorResponse { - error: format!( - "Play content timed out after {}s", - QUEUE_COMMAND_TIMEOUT.as_secs() - ), - }), - ) - })? - .map_err(|e| { - warn!("Task join error during play content: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: format!("Internal task error: {}", e), - }), - ) - })? - .map_err(|e| { - warn!("Failed to play content on renderer {}: {}", renderer_id, e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: format!("Failed to play content: {}", e), - }), - ) - })?; - debug!( renderer = renderer_id.as_str(), server = req.server_id.as_str(), - object = object_id_for_log.as_str(), + object = object_id_for_debug.as_str(), "Content playing via HTTP API" ); @@ -1626,65 +1618,54 @@ async fn add_to_queue( })?; let control_point = Arc::clone(&state.control_point); + let rid_for_log = rid.clone(); + let object_id_for_log = object_id.clone(); + let object_id_for_debug = object_id_for_log.clone(); - // Spawn blocking task for content loading - let add_task = tokio::task::spawn_blocking(move || { - // Fetch playback items from server - let items = fetch_playback_items(&control_point, &sid, &object_id)?; + // Launch the command in background and return immediately + // The UI will be updated via SSE events when the queue changes + tokio::task::spawn(async move { + let result = tokio::task::spawn_blocking(move || { + // Fetch playback items from server + let items = fetch_playback_items(&control_point, &sid, &object_id)?; - if items.is_empty() { - return Err(anyhow::anyhow!("No playable content found")); + if items.is_empty() { + return Err(anyhow::anyhow!("No playable content found")); + } + + // Enqueue items + control_point.enqueue_items(&rid, items)?; + + Ok::<(), anyhow::Error>(()) + }) + .await; + + match result { + Ok(Ok(())) => { + debug!( + "Successfully added content {} to queue for renderer {}", + object_id_for_log, rid_for_log.0 + ); + } + Ok(Err(e)) => { + warn!( + "Failed to add content to queue for renderer {}: {}", + rid_for_log.0, e + ); + } + Err(e) => { + warn!( + "Task join error during add to queue for renderer {}: {}", + rid_for_log.0, e + ); + } } - - // Enqueue items - control_point.enqueue_items(&rid, items)?; - - Ok::<(), anyhow::Error>(()) }); - time::timeout(QUEUE_COMMAND_TIMEOUT, add_task) - .await - .map_err(|_| { - warn!( - "Add to queue command for renderer {} exceeded {:?}", - renderer_id, QUEUE_COMMAND_TIMEOUT - ); - ( - StatusCode::GATEWAY_TIMEOUT, - Json(ErrorResponse { - error: format!( - "Add to queue timed out after {}s", - QUEUE_COMMAND_TIMEOUT.as_secs() - ), - }), - ) - })? - .map_err(|e| { - warn!("Task join error during add to queue: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: format!("Internal task error: {}", e), - }), - ) - })? - .map_err(|e| { - warn!( - "Failed to add content to queue for renderer {}: {}", - renderer_id, e - ); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: format!("Failed to add to queue: {}", e), - }), - ) - })?; - debug!( renderer = renderer_id.as_str(), server = req.server_id.as_str(), - object = object_id_for_log.as_str(), + object = object_id_for_debug.as_str(), "Content added to queue via HTTP API" ); @@ -1734,69 +1715,58 @@ async fn add_after_current( })?; let control_point = Arc::clone(&state.control_point); + let rid_for_log = rid.clone(); + let object_id_for_log = object_id.clone(); + let object_id_for_debug = object_id_for_log.clone(); - // Spawn blocking task for content loading - let add_task = tokio::task::spawn_blocking(move || { - // Fetch playback items from server - let items = fetch_playback_items(&control_point, &sid, &object_id)?; + // Launch the command in background and return immediately + // The UI will be updated via SSE events when the queue changes + tokio::task::spawn(async move { + let result = tokio::task::spawn_blocking(move || { + // Fetch playback items from server + let items = fetch_playback_items(&control_point, &sid, &object_id)?; - if items.is_empty() { - return Err(anyhow::anyhow!("No playable content found")); + if items.is_empty() { + return Err(anyhow::anyhow!("No playable content found")); + } + + // Insert items after current using the new method + control_point.enqueue_items_with_mode( + &rid, + items, + crate::queue::EnqueueMode::InsertAfterCurrent, + )?; + + Ok::<(), anyhow::Error>(()) + }) + .await; + + match result { + Ok(Ok(())) => { + debug!( + "Successfully added content {} after current for renderer {}", + object_id_for_log, rid_for_log.0 + ); + } + Ok(Err(e)) => { + warn!( + "Failed to add content after current for renderer {}: {}", + rid_for_log.0, e + ); + } + Err(e) => { + warn!( + "Task join error during add after current for renderer {}: {}", + rid_for_log.0, e + ); + } } - - // Insert items after current using the new method - control_point.enqueue_items_with_mode( - &rid, - items, - crate::queue::EnqueueMode::InsertAfterCurrent, - )?; - - Ok::<(), anyhow::Error>(()) }); - time::timeout(QUEUE_COMMAND_TIMEOUT, add_task) - .await - .map_err(|_| { - warn!( - "Add after current command for renderer {} exceeded {:?}", - renderer_id, QUEUE_COMMAND_TIMEOUT - ); - ( - StatusCode::GATEWAY_TIMEOUT, - Json(ErrorResponse { - error: format!( - "Add after current timed out after {}s", - QUEUE_COMMAND_TIMEOUT.as_secs() - ), - }), - ) - })? - .map_err(|e| { - warn!("Task join error during add after current: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: format!("Internal task error: {}", e), - }), - ) - })? - .map_err(|e| { - warn!( - "Failed to add content after current for renderer {}: {}", - renderer_id, e - ); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: format!("Failed to add after current: {}", e), - }), - ) - })?; - debug!( renderer = renderer_id.as_str(), server = req.server_id.as_str(), - object = object_id_for_log.as_str(), + object = object_id_for_debug.as_str(), "Content added after current via HTTP API" ); From 7d2cb75e582b901a8bf6a9078a6dde2498cd8ed0 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 11 Jan 2026 15:44:37 +0100 Subject: [PATCH 2/3] =?UTF-8?q?Optimisation=20des=20d=C3=A9lais=20de=20raf?= =?UTF-8?q?ra=C3=AEchissement=20et=20am=C3=A9lioration=20de=20la=20r=C3=A9?= =?UTF-8?q?activit=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Réduction du délai de rafraîchissement des conteneurs à 2 secondes et ajustement du polling pour une meilleure réactivité de l'interface utilisateur. - Modification du délai de cooldown de 5 secondes à 2 secondes dans MediaBrowser.vue - Réduction du délai de polling de 60 secondes à 10 secondes pour la découverte des appareils dans control_point.rs - Modification du polling de volume et de mute de 3 secondes à 1 seconde (tous les 2 ticks à 500ms) dans control_point.rs - Réduction du délai de polling de position de 1 seconde à 500ms dans control_point.rs --- .../components/pmocontrol/MediaBrowser.vue | 454 +++++++++--------- pmocontrol/src/control_point.rs | 14 +- 2 files changed, 239 insertions(+), 229 deletions(-) diff --git a/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue b/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue index 10af54c8..5a8dfd85 100644 --- a/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue +++ b/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue @@ -1,62 +1,58 @@ diff --git a/pmocontrol/src/control_point.rs b/pmocontrol/src/control_point.rs index 4b2d89e7..560a6628 100644 --- a/pmocontrol/src/control_point.rs +++ b/pmocontrol/src/control_point.rs @@ -112,8 +112,8 @@ impl ControlPoint { ]; loop { - // Attendre 60 secondes avant le prochain cycle - thread::sleep(Duration::from_secs(60)); + // Attendre 10 secondes avant le prochain cycle pour découverte rapide + thread::sleep(Duration::from_secs(10)); debug!("Sending periodic M-SEARCH for device discovery"); @@ -303,9 +303,9 @@ impl ControlPoint { new_snapshot.state = Some(logical_state); } - // Poll volume and mute less frequently (every 3 seconds) - // to reduce SOAP overhead without impacting UI responsiveness - if tick % 3 == 0 { + // Poll volume and mute every second (every 2 ticks at 500ms) + // for responsive volume control feedback + if tick % 2 == 0 { if let Ok(volume) = renderer.volume() { if prev_snapshot.last_volume != Some(volume) { polling_cp.emit_renderer_event(RendererEvent::VolumeChanged { @@ -334,8 +334,8 @@ impl ControlPoint { } tick = tick.wrapping_add(1); - // Keep 1 second polling for smooth position updates - thread::sleep(Duration::from_secs(1)); + // 500ms polling for smoother position updates and progress bar + thread::sleep(Duration::from_millis(500)); } }); From 646633af6b13f68cabeb92bba82e8405cfd354fc Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 11 Jan 2026 15:53:37 +0100 Subject: [PATCH 3/3] =?UTF-8?q?Mise=20=C3=A0=20jour=20de=20la=20version=20?= =?UTF-8?q?et=20am=C3=A9liorations=20du=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mise à jour de la version de PMOMusic de 0.1.0 à 0.3.1 - Correction de l'indentation dans le fichier de workflow - Ajout d'une étape pour extraire la version depuis Cargo.toml - Ajout d'une cible Makefile pour incrémenter automatiquement le numéro de version patch - Ajout de cibles Makefile pour gérer les commits avec jj (jeff) : bump-version, jjnew, jjpush, jjfetch - Suppression du fichier version.txt inutile --- .gitea/workflows/build-push.yaml | 14 ++++++------ Cargo.lock | 2 +- Makefile | 37 +++++++++++++++++++++++++++++++- PMOMusic/Cargo.toml | 2 +- version.txt | 1 - 5 files changed, 46 insertions(+), 10 deletions(-) delete mode 100644 version.txt diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml index 340fb686..cee2ff8b 100644 --- a/.gitea/workflows/build-push.yaml +++ b/.gitea/workflows/build-push.yaml @@ -3,8 +3,7 @@ name: Build and Push Docker Image on: push: branches: - - main # Changez cela si votre branche principale a un autre nom - + - main # Changez cela si votre branche principale a un autre nom jobs: build: @@ -15,7 +14,13 @@ jobs: uses: actions/cache@v3 with: path: ~/.npm - key: dont-cache-${{ github.run_id }} + key: dont-cache-${{ github.run_id }} + + - name: Extract version from Cargo.toml + run: | + grep '^version = ' PMOMusic/Cargo.toml | head -n 1 | sed 's/version = "\(.*\)"/\1/' > version.txt + echo "Version extracted: $(cat version.txt)" + - name: Build and push image uses: https://gargoton.petite-maison-orange.fr/pmo-actions/build-push-image@main with: @@ -24,6 +29,3 @@ jobs: no_cache: true version_file: version.txt check_uuid: 82a30d23-b3bd-4199-9237-776965831d20 - - - \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 363d1caf..54552d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "PMOMusic" -version = "0.1.0" +version = "0.3.1" dependencies = [ "axum 0.8.7", "console-subscriber", diff --git a/Makefile b/Makefile index ef3912cb..d264446c 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ BINARY_NAME = PMOMusic # Couleurs pour l'affichage GREEN = \033[0;32m YELLOW = \033[1;33m +BLUE = \033[1;34m RED = \033[0;31m NC = \033[0m # No Color @@ -193,6 +194,21 @@ update: cd $(WEBAPP_DIR) && $(NPM) update @echo "$(GREEN)✓ Dépendances mises à jour$(NC)" +## bump-version: Incrémente le numéro de version patch (x.y.z -> x.y.z+1) +bump-version: + @echo "$(YELLOW)→ Incrémentation de la version...$(NC)" + @current=$$(grep '^version = ' PMOMusic/Cargo.toml | head -n 1 | sed 's/version = "\(.*\)"/\1/'); \ + echo " Version actuelle: $$current"; \ + major=$$(echo $$current | cut -d. -f1); \ + minor=$$(echo $$current | cut -d. -f2); \ + patch=$$(echo $$current | cut -d. -f3); \ + new_patch=$$((patch + 1)); \ + new_version="$$major.$$minor.$$new_patch"; \ + echo " Nouvelle version: $$new_version"; \ + sed -i.bak "s/^version = \"$$current\"/version = \"$$new_version\"/" PMOMusic/Cargo.toml && \ + rm PMOMusic/Cargo.toml.bak + @echo "$(GREEN)✓ Version mise à jour dans PMOMusic/Cargo.toml$(NC)" + ## bench: Exécute les benchmarks bench: @echo "$(YELLOW)→ Exécution des benchmarks...$(NC)" @@ -203,4 +219,23 @@ coverage: @echo "$(YELLOW)→ Génération du rapport de couverture...$(NC)" $(CARGO) tarpaulin --out Html --output-dir target/coverage @echo "$(GREEN)✓ Rapport disponible dans target/coverage/index.html$(NC)" - + +jjnew: + @echo "$(YELLOW)→ Création d'un nouveau commit...$(NC)" + @echo "$(BLUE)→ Documentation du commit courrant...$(NC)" + @jj auto-describe + @echo "$(BLUE)→ C'est fait.$(NC)" + @jj new + @echo "$(GREEN)✓ nouveau commit créé$(NC)" + +jjpush: bump-version + @echo "$(YELLOW)→ Push du commit sur le dépôt...$(NC)" + @jj auto-describe + @jj git push --change @ + @echo "$(GREEN)✓ Commit pushé sur le dépôt$(NC)" + +jjfetch: + @echo "$(YELLOW)→ Pull des derniers commits...$(NC)" + @jj git fetch + @jj new main@origin + @echo "$(GREEN)✓ Derniers commits pullés$(NC)" diff --git a/PMOMusic/Cargo.toml b/PMOMusic/Cargo.toml index 2955dd5c..3853aa5c 100644 --- a/PMOMusic/Cargo.toml +++ b/PMOMusic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "PMOMusic" -version = "0.1.0" +version = "0.3.1" edition = "2024" [dependencies] diff --git a/version.txt b/version.txt deleted file mode 100644 index 0d91a54c..00000000 --- a/version.txt +++ /dev/null @@ -1 +0,0 @@ -0.3.0