From 586d27bf43dcc3f02b43e678c236cfc12754b8da Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:08:08 -0600 Subject: [PATCH] feat(qbzd): integrate daemon discovery into CastPicker QBZ Daemon tab appears first in the device picker alongside Chromecast and DLNA. Discovered daemons show name + host:port. Clicking a daemon: 1. Pings to verify it's reachable 2. Connects via playbackTargetStore 3. Starts SSE event subscription 4. Closes picker Connected daemon shows in the connected banner with disconnect. Disconnecting returns to local playback mode. i18n: noQbzdFound + noQbzdFoundHint in all 5 locales. cc --- src/lib/components/CastPicker.svelte | 136 ++++++++++++++++++++++----- src/lib/i18n/locales/de.json | 2 + src/lib/i18n/locales/en.json | 2 + src/lib/i18n/locales/es.json | 2 + src/lib/i18n/locales/fr.json | 2 + src/lib/i18n/locales/pt.json | 2 + 6 files changed, 125 insertions(+), 21 deletions(-) diff --git a/src/lib/components/CastPicker.svelte b/src/lib/components/CastPicker.svelte index 13089f118..2c1a75ed5 100644 --- a/src/lib/components/CastPicker.svelte +++ b/src/lib/components/CastPicker.svelte @@ -11,6 +11,21 @@ type CastProtocol, type CastDevice } from '$lib/stores/castStore'; + import { + connectToRemote, + disconnectFromRemote, + isRemoteMode, + playbackTarget, + } from '$lib/stores/playbackTargetStore'; + import { connectRemoteEvents, disconnectRemoteEvents } from '$lib/services/remoteEvents'; + import { pingRemote } from '$lib/services/remoteApi'; + + interface QbzdDevice { + name: string; + host: string; + port: number; + baseUrl: string; + } interface Props { isOpen: boolean; @@ -19,10 +34,11 @@ let { isOpen, onClose }: Props = $props(); - let activeProtocol = $state('chromecast'); + let activeProtocol = $state('qbzd'); let chromecastDevices = $state([]); let dlnaDevices = $state([]); let airplayDevices = $state([]); + let qbzdDevices = $state([]); let loading = $state(false); let error = $state(null); let discoveryStarted = $state(false); @@ -36,6 +52,7 @@ case 'chromecast': return chromecastDevices; case 'dlna': return dlnaDevices; case 'airplay': return airplayDevices; + case 'qbzd': return []; // qbzd uses its own list } }); @@ -76,8 +93,8 @@ // Note: AirPlay discovery disabled until RAOP streaming is implemented await Promise.allSettled([ invoke('v2_cast_start_discovery'), - invoke('v2_dlna_start_discovery') - // invoke('v2_airplay_start_discovery') // Disabled - see docs/AIRPLAY_IMPLEMENTATION_STATUS.md + invoke('v2_dlna_start_discovery'), + invoke('v2_qbzd_start_discovery'), ]); // Poll for devices pollDevices(); @@ -92,8 +109,8 @@ try { await Promise.allSettled([ invoke('v2_cast_stop_discovery'), - invoke('v2_dlna_stop_discovery') - // invoke('v2_airplay_stop_discovery') // Disabled + invoke('v2_dlna_stop_discovery'), + invoke('v2_qbzd_stop_discovery'), ]); } catch (err) { console.error('Failed to stop discovery:', err); @@ -106,10 +123,10 @@ try { // Poll active protocols in parallel // Note: AirPlay polling disabled until RAOP streaming is implemented - const [chromecast, dlna] = await Promise.allSettled([ + const [chromecast, dlna, qbzd] = await Promise.allSettled([ invoke('v2_cast_get_devices'), - invoke('v2_dlna_get_devices') - // invoke('v2_airplay_get_devices') // Disabled + invoke('v2_dlna_get_devices'), + invoke('v2_qbzd_get_devices'), ]); if (chromecast.status === 'fulfilled') { @@ -118,10 +135,9 @@ if (dlna.status === 'fulfilled') { dlnaDevices = dlna.value; } - // AirPlay disabled - // if (airplay.status === 'fulfilled') { - // airplayDevices = airplay.value; - // } + if (qbzd.status === 'fulfilled') { + qbzdDevices = qbzd.value; + } } catch (err) { console.error('Failed to get devices:', err); } @@ -138,7 +154,7 @@ connecting = true; error = null; try { - await connectToDevice(device, activeProtocol); + await connectToDevice(device, activeProtocol as CastProtocol); onClose(); } catch (err) { error = String(err); @@ -149,22 +165,48 @@ async function handleDisconnect() { try { - await disconnect(); + if ($isRemoteMode) { + disconnectRemoteEvents(); + disconnectFromRemote(); + } else { + await disconnect(); + } } catch (err) { error = String(err); } } - function getProtocolIcon(protocol: CastProtocol) { + async function handleQbzdConnect(device: QbzdDevice) { + connecting = true; + error = null; + try { + const reachable = await pingRemote(device.baseUrl, ''); + if (!reachable) { + error = `Cannot reach ${device.name} at ${device.baseUrl}`; + return; + } + connectToRemote(device.baseUrl, '', device.name); + connectRemoteEvents(); + onClose(); + } catch (err) { + error = String(err); + } finally { + connecting = false; + } + } + + function getProtocolIcon(protocol: CastProtocol | 'qbzd') { switch (protocol) { + case 'qbzd': return Monitor; case 'chromecast': return Cast; case 'dlna': return Tv; case 'airplay': return Speaker; } } - function getProtocolLabel(protocol: CastProtocol): string { + function getProtocolLabel(protocol: CastProtocol | 'qbzd'): string { switch (protocol) { + case 'qbzd': return 'QBZ Daemon'; case 'chromecast': return 'Chromecast'; case 'dlna': return 'DLNA'; case 'airplay': return 'AirPlay'; @@ -183,7 +225,21 @@ - {#if castState.isConnected && castState.device} + {#if $isRemoteMode} +
+
+ +
+ {$t('player.connectedTo')} + {$playbackTarget.name} +
+
+ +
+ {:else if castState.isConnected && castState.device} {@const ProtocolIcon = getProtocolIcon(castState.protocol!)}
@@ -201,6 +257,17 @@ {:else}
+
+ {:else if error} +
+

{error}

+
+ {:else if activeProtocol === 'qbzd'} + + {#if loading && qbzdDevices.length === 0} +
+ +

{$t('toast.loadingDevices')}

+
+ {:else if qbzdDevices.length === 0} +
+ +

{$t('player.noQbzdFound')}

+

{$t('player.noQbzdFoundHint')}

+
+ {:else} +
+ {#each qbzdDevices as device} + + {/each} +
+ {/if} {:else if loading && devices().length === 0}

{$t('toast.loadingDevices')}

- {:else if error} -
-

{error}

-
{:else if devices().length === 0}
diff --git a/src/lib/i18n/locales/de.json b/src/lib/i18n/locales/de.json index fc6a70247..8bdc0adfe 100644 --- a/src/lib/i18n/locales/de.json +++ b/src/lib/i18n/locales/de.json @@ -563,6 +563,8 @@ "player": { "nowPlaying": "Aktuell abgespielt", "controllingRemote": "Steuert: {name}", + "noQbzdFound": "Keine QBZ-Daemons in Ihrem Netzwerk gefunden", + "noQbzdFoundHint": "Stellen Sie sicher, dass qbzd auf dem Zielgerat lauft", "queue": "Warteschlange", "upNext": "Als Nächstes", "ofTotal": "von", diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json index 9c43f63be..88ab62381 100644 --- a/src/lib/i18n/locales/en.json +++ b/src/lib/i18n/locales/en.json @@ -563,6 +563,8 @@ "player": { "nowPlaying": "Now Playing", "controllingRemote": "Controlling: {name}", + "noQbzdFound": "No QBZ Daemons found on your network", + "noQbzdFoundHint": "Make sure qbzd is running on the target device", "queue": "Queue", "upNext": "Up Next", "ofTotal": "of", diff --git a/src/lib/i18n/locales/es.json b/src/lib/i18n/locales/es.json index 400d857b6..9d7ace666 100644 --- a/src/lib/i18n/locales/es.json +++ b/src/lib/i18n/locales/es.json @@ -563,6 +563,8 @@ "player": { "nowPlaying": "Reproduciendo", "controllingRemote": "Controlando: {name}", + "noQbzdFound": "No se encontraron daemons QBZ en tu red", + "noQbzdFoundHint": "Asegurate de que qbzd este corriendo en el dispositivo", "queue": "Cola", "upNext": "Siguiente", "ofTotal": "de", diff --git a/src/lib/i18n/locales/fr.json b/src/lib/i18n/locales/fr.json index 92e719620..3782afd5e 100644 --- a/src/lib/i18n/locales/fr.json +++ b/src/lib/i18n/locales/fr.json @@ -563,6 +563,8 @@ "player": { "nowPlaying": "Lecture en cours", "controllingRemote": "Controle : {name}", + "noQbzdFound": "Aucun daemon QBZ trouve sur votre reseau", + "noQbzdFoundHint": "Assurez-vous que qbzd est en cours d'execution sur l'appareil cible", "queue": "File d'attente", "upNext": "À suivre", "ofTotal": "de", diff --git a/src/lib/i18n/locales/pt.json b/src/lib/i18n/locales/pt.json index b3e78f9fc..1c25e8d7e 100644 --- a/src/lib/i18n/locales/pt.json +++ b/src/lib/i18n/locales/pt.json @@ -563,6 +563,8 @@ "player": { "nowPlaying": "Tocando Agora", "controllingRemote": "Controlando: {name}", + "noQbzdFound": "Nenhum daemon QBZ encontrado na sua rede", + "noQbzdFoundHint": "Certifique-se de que o qbzd esta em execucao no dispositivo alvo", "queue": "Fila", "upNext": "A Seguir", "ofTotal": "de",