chore(cast): remove AirPlay subsystem (dead code)

AirPlay was scaffolded in both crates/qbz-cast/src/airplay and
src-tauri/src/cast/airplay but never worked at runtime: the RAOP
sender required an Apple MFi-licensed stack with no free/open
alternative, and the backend state was already disabled via the
commented `.manage(airplay_state)` line in src-tauri/src/lib.rs.
The CastPicker UI already hid the AirPlay tab pending streaming
support that was never going to ship.

Delete:
- crates/qbz-cast/src/airplay/        (entire directory)
- src-tauri/src/cast/airplay/         (entire directory)
- 10x v2_airplay_* commands from commands_v2/library.rs + their
  tauri::generate_handler entries
- AirPlayError from qbz-cast errors
- 'airplay' from CastProtocol + every case arm in castStore.ts,
  CastPicker.svelte, AudioOutputBadges.svelte
- Corresponding i18n keys if present

Zero behavior change for Chromecast / DLNA / QConnect / MPRIS. If
AirPlay ever becomes feasible we'll revisit from scratch with a
real streaming backend.

cc
This commit is contained in:
vicrodh committed 2026-04-16 19:04:56 -06:00
1 parent 27ab2c8bea
commit edededcf8c
18 files changed
+17 -658

No files matched your search

+3 -3
View File
@@ -164,7 +164,7 @@ Multi-device playback control using Qobuz's real-time streaming protocol.
### Casting ### Casting
- **Chromecast**, **DLNA/UPnP**, and **AirPlay** discovery and streaming - **Chromecast** and **DLNA/UPnP** discovery and streaming
- Seamless playback handoff to network devices - Seamless playback handoff to network devices
### Integrations ### Integrations
@@ -212,7 +212,7 @@ Multi-device playback control using Qobuz's real-time streaming protocol.
| **Database** | rusqlite (bundled SQLite, WAL mode) | | **Database** | rusqlite (bundled SQLite, WAL mode) |
| **PDF** | MuPDF 0.6 (native rendering) | | **PDF** | MuPDF 0.6 (native rendering) |
| **Desktop** | souvlaki (MPRIS), ashpd (XDG notifications), keyring | | **Desktop** | souvlaki (MPRIS), ashpd (XDG notifications), keyring |
| **Casting** | rust_cast (Chromecast), rupnp (DLNA/UPnP), mdns-sd (AirPlay) | | **Casting** | rust_cast (Chromecast), rupnp (DLNA/UPnP), mdns-sd |
| **i18n** | svelte-i18n (5 locales) | | **i18n** | svelte-i18n (5 locales) |
### Multi-Crate Architecture ### Multi-Crate Architecture
@@ -227,7 +227,7 @@ crates/
qbz-library/ Local library scanning and metadata qbz-library/ Local library scanning and metadata
qbz-integrations/ Last.fm, ListenBrainz, MusicBrainz, Discogs qbz-integrations/ Last.fm, ListenBrainz, MusicBrainz, Discogs
qbz-cache/ L1 memory + L2 disk audio caching qbz-cache/ L1 memory + L2 disk audio caching
qbz-cast/ Chromecast, DLNA/UPnP, AirPlay qbz-cast/ Chromecast, DLNA/UPnP
qconnect-protocol/ Qobuz Connect protobuf wire format qconnect-protocol/ Qobuz Connect protobuf wire format
qconnect-core/ Queue and renderer domain models qconnect-core/ Queue and renderer domain models
qconnect-app/ Application logic and concurrency qconnect-app/ Application logic and concurrency
+1 -1
View File
@@ -2,7 +2,7 @@
name = "qbz-cast" name = "qbz-cast"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
description = "Casting support for QBZ - Chromecast, AirPlay, DLNA" description = "Casting support for QBZ - Chromecast, DLNA"
[dependencies] [dependencies]
# Chromecast # Chromecast
-88
View File
@@ -1,88 +0,0 @@
//! AirPlay device connection and streaming (placeholder)
use serde::{Deserialize, Serialize};
use crate::airplay::DiscoveredAirPlayDevice;
use crate::AirPlayError;
/// Metadata for AirPlay playback
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AirPlayMetadata {
pub title: String,
pub artist: String,
pub album: String,
pub artwork_url: Option<String>,
pub duration_secs: Option<u64>,
}
/// AirPlay device status (minimal for now)
#[derive(Debug, Clone, Serialize)]
pub struct AirPlayStatus {
pub device_id: String,
pub device_name: String,
pub is_connected: bool,
}
/// AirPlay connection stub (RAOP sender integration pending)
pub struct AirPlayConnection {
device: DiscoveredAirPlayDevice,
connected: bool,
}
impl AirPlayConnection {
/// Connect to an AirPlay device
pub fn connect(device: DiscoveredAirPlayDevice) -> Result<Self, AirPlayError> {
Ok(Self {
device,
connected: true,
})
}
/// Disconnect from the device
pub fn disconnect(&mut self) -> Result<(), AirPlayError> {
self.connected = false;
Ok(())
}
/// Current connection status
pub fn get_status(&self) -> AirPlayStatus {
AirPlayStatus {
device_id: self.device.id.clone(),
device_name: self.device.name.clone(),
is_connected: self.connected,
}
}
/// Load media (requires RAOP sender integration)
pub fn load_media(&mut self, _metadata: AirPlayMetadata) -> Result<(), AirPlayError> {
Err(AirPlayError::NotImplemented(
"RAOP sender integration not implemented".to_string(),
))
}
/// Play/pause/stop controls
pub fn play(&mut self) -> Result<(), AirPlayError> {
Err(AirPlayError::NotImplemented(
"RAOP sender integration not implemented".to_string(),
))
}
pub fn pause(&mut self) -> Result<(), AirPlayError> {
Err(AirPlayError::NotImplemented(
"RAOP sender integration not implemented".to_string(),
))
}
pub fn stop(&mut self) -> Result<(), AirPlayError> {
Err(AirPlayError::NotImplemented(
"RAOP sender integration not implemented".to_string(),
))
}
/// Volume control (0.0 - 1.0)
pub fn set_volume(&mut self, _volume: f32) -> Result<(), AirPlayError> {
Err(AirPlayError::NotImplemented(
"RAOP sender integration not implemented".to_string(),
))
}
}
-216
View File
@@ -1,216 +0,0 @@
//! AirPlay device discovery via mDNS
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use mdns_sd::{ServiceDaemon, ServiceEvent};
use serde::Serialize;
use crate::AirPlayError;
const SERVICE_RAOP: &str = "_raop._tcp.local.";
const SERVICE_AIRPLAY: &str = "_airplay._tcp.local.";
/// Discovered AirPlay device
#[derive(Debug, Clone, Serialize)]
pub struct DiscoveredAirPlayDevice {
pub id: String,
pub name: String,
pub model: String,
pub ip: String,
pub port: u16,
pub service: String,
pub requires_password: bool,
}
#[derive(Default)]
struct DiscoveryState {
devices: HashMap<String, DiscoveredAirPlayDevice>,
fullname_to_id: HashMap<String, String>,
}
/// Discovery manager for AirPlay devices
pub struct AirPlayDiscovery {
state: Arc<Mutex<DiscoveryState>>,
running: Arc<AtomicBool>,
handles: Vec<JoinHandle<()>>,
daemon: Option<ServiceDaemon>,
}
impl AirPlayDiscovery {
pub fn new() -> Self {
Self {
state: Arc::new(Mutex::new(DiscoveryState::default())),
running: Arc::new(AtomicBool::new(false)),
handles: Vec::new(),
daemon: None,
}
}
/// Start mDNS discovery in background
pub fn start_discovery(&mut self) -> Result<(), AirPlayError> {
if self.running.load(Ordering::SeqCst) {
return Ok(());
}
let mdns = ServiceDaemon::new()
.map_err(|e| AirPlayError::Discovery(format!("Failed to create mDNS daemon: {}", e)))?;
let raop_rx = mdns.browse(SERVICE_RAOP).map_err(|e| {
AirPlayError::Discovery(format!("Failed to browse RAOP services: {}", e))
})?;
let airplay_rx = mdns.browse(SERVICE_AIRPLAY).map_err(|e| {
AirPlayError::Discovery(format!("Failed to browse AirPlay services: {}", e))
})?;
self.running.store(true, Ordering::SeqCst);
let state = self.state.clone();
let running = self.running.clone();
self.handles.push(spawn_receiver(
"raop",
raop_rx,
state.clone(),
running.clone(),
));
self.handles
.push(spawn_receiver("airplay", airplay_rx, state, running));
self.daemon = Some(mdns);
Ok(())
}
/// Stop discovery and release resources
pub fn stop_discovery(&mut self) -> Result<(), AirPlayError> {
self.running.store(false, Ordering::SeqCst);
if let Some(daemon) = self.daemon.take() {
daemon
.shutdown()
.map_err(|e| AirPlayError::Discovery(format!("Failed to shutdown mDNS: {}", e)))?;
}
for handle in self.handles.drain(..) {
let _ = handle.join();
}
Ok(())
}
/// Return list of discovered devices
pub fn get_discovered_devices(&self) -> Vec<DiscoveredAirPlayDevice> {
self.state
.lock()
.map(|state| state.devices.values().cloned().collect())
.unwrap_or_default()
}
/// Get a specific device by ID
pub fn get_device(&self, device_id: &str) -> Option<DiscoveredAirPlayDevice> {
self.state
.lock()
.ok()
.and_then(|state| state.devices.get(device_id).cloned())
}
}
impl Default for AirPlayDiscovery {
fn default() -> Self {
Self::new()
}
}
fn spawn_receiver(
service_label: &'static str,
receiver: mdns_sd::Receiver<ServiceEvent>,
state: Arc<Mutex<DiscoveryState>>,
running: Arc<AtomicBool>,
) -> JoinHandle<()> {
thread::spawn(move || {
for event in receiver.iter() {
if !running.load(Ordering::SeqCst) {
break;
}
match event {
ServiceEvent::ServiceResolved(info) => {
let fullname = info.get_fullname().to_string();
let id = device_id_from_info(&fullname, &info);
let name = device_name_from_info(&fullname, &info, &id);
let model = info
.get_property_val_str("am")
.or_else(|| info.get_property_val_str("model"))
.unwrap_or("Unknown")
.to_string();
let requires_password = parse_password_required(&info);
let ip =
pick_ip(info.get_addresses()).unwrap_or_else(|| "127.0.0.1".to_string());
let device = DiscoveredAirPlayDevice {
id: id.clone(),
name,
model,
ip,
port: info.get_port(),
service: service_label.to_string(),
requires_password,
};
let key = format!("{}|{}", service_label, fullname);
if let Ok(mut state) = state.lock() {
state.fullname_to_id.insert(key, id.clone());
state.devices.insert(id, device);
}
}
ServiceEvent::ServiceRemoved(_, fullname) => {
let key = format!("{}|{}", service_label, fullname);
if let Ok(mut state) = state.lock() {
if let Some(id) = state.fullname_to_id.remove(&key) {
state.devices.remove(&id);
}
}
}
_ => {}
}
}
})
}
fn device_id_from_info(fullname: &str, info: &mdns_sd::ServiceInfo) -> String {
if let Some(id) = info.get_property_val_str("deviceid") {
return id.to_string();
}
if let Some(id) = info.get_property_val_str("id") {
return id.to_string();
}
if let Some((prefix, _)) = fullname.split_once('@') {
return prefix.to_string();
}
fullname.to_string()
}
fn device_name_from_info(fullname: &str, info: &mdns_sd::ServiceInfo, fallback: &str) -> String {
if let Some(name) = info.get_property_val_str("fn") {
return name.to_string();
}
if let Some((_, name)) = fullname.split_once('@') {
return name.to_string();
}
fallback.to_string()
}
fn parse_password_required(info: &mdns_sd::ServiceInfo) -> bool {
match info.get_property_val_str("pw") {
Some(value) => matches!(value, "1" | "true" | "yes" | "on"),
None => false,
}
}
fn pick_ip(addresses: &std::collections::HashSet<IpAddr>) -> Option<String> {
let ipv4 = addresses.iter().find(|addr| addr.is_ipv4());
let ip = ipv4.or_else(|| addresses.iter().next())?;
Some(ip.to_string())
}
-8
View File
@@ -1,8 +0,0 @@
//! AirPlay casting module (discovery + scaffolding)
pub mod device;
pub mod discovery;
pub use crate::AirPlayError;
pub use device::{AirPlayConnection, AirPlayMetadata, AirPlayStatus};
pub use discovery::{AirPlayDiscovery, DiscoveredAirPlayDevice};
-22
View File
@@ -30,28 +30,6 @@ pub enum CastError {
InvalidRequest(String), InvalidRequest(String),
} }
/// AirPlay casting errors
#[derive(Error, Debug)]
pub enum AirPlayError {
#[error("Discovery error: {0}")]
Discovery(String),
#[error("Connection error: {0}")]
Connection(String),
#[error("Playback error: {0}")]
Playback(String),
#[error("Not connected")]
NotConnected,
#[error("Device not found: {0}")]
DeviceNotFound(String),
#[error("Not implemented: {0}")]
NotImplemented(String),
}
/// DLNA/UPnP casting errors /// DLNA/UPnP casting errors
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum DlnaError { pub enum DlnaError {
+2 -10
View File
@@ -1,6 +1,6 @@
//! qbz-cast - Casting support for QBZ //! qbz-cast - Casting support for QBZ
//! //!
//! Provides casting capabilities for Chromecast, AirPlay (scaffolded), and DLNA devices. //! Provides casting capabilities for Chromecast and DLNA devices.
//! This crate is Tauri-agnostic and can be used by any Rust application. //! This crate is Tauri-agnostic and can be used by any Rust application.
//! //!
//! # Architecture //! # Architecture
@@ -8,21 +8,18 @@
//! - **Chromecast**: Full implementation using rust-cast. Uses a dedicated thread //! - **Chromecast**: Full implementation using rust-cast. Uses a dedicated thread
//! for connection management due to rust-cast's use of Rc (not thread-safe). //! for connection management due to rust-cast's use of Rc (not thread-safe).
//! //!
//! - **AirPlay**: Discovery implemented, playback scaffolded (RAOP sender integration pending).
//!
//! - **DLNA/UPnP**: Full implementation using rupnp for AVTransport/RenderingControl. //! - **DLNA/UPnP**: Full implementation using rupnp for AVTransport/RenderingControl.
//! //!
//! - **MediaServer**: Local HTTP server for streaming audio to cast devices. //! - **MediaServer**: Local HTTP server for streaming audio to cast devices.
//! Supports byte-range requests for seeking. //! Supports byte-range requests for seeking.
pub mod airplay;
pub mod chromecast; pub mod chromecast;
pub mod dlna; pub mod dlna;
pub mod errors; pub mod errors;
pub mod media_server; pub mod media_server;
// Re-export error types at root // Re-export error types at root
pub use errors::{AirPlayError, CastError, DlnaError}; pub use errors::{CastError, DlnaError};
// Re-export media server // Re-export media server
pub use media_server::MediaServer; pub use media_server::MediaServer;
@@ -33,11 +30,6 @@ pub use chromecast::{
ChromecastHandle, DeviceDiscovery, DiscoveredDevice, MediaMetadata, ChromecastHandle, DeviceDiscovery, DiscoveredDevice, MediaMetadata,
}; };
// Re-export AirPlay types
pub use airplay::{
AirPlayConnection, AirPlayDiscovery, AirPlayMetadata, AirPlayStatus, DiscoveredAirPlayDevice,
};
// Re-export DLNA types // Re-export DLNA types
pub use dlna::{ pub use dlna::{
DiscoveredDlnaDevice, DlnaConnection, DlnaDiscovery, DlnaMetadata, DlnaPositionInfo, DlnaStatus, DiscoveredDlnaDevice, DlnaConnection, DlnaDiscovery, DlnaMetadata, DlnaPositionInfo, DlnaStatus,
+1 -1
View File
@@ -147,7 +147,7 @@ keyring = { version = "3", features = ["async-secret-service", "crypto-rust"] }
tauri-plugin-clipboard-manager = "2" tauri-plugin-clipboard-manager = "2"
# Casting: qbz-cast crate provides all casting functionality # Casting: qbz-cast crate provides all casting functionality
# (Chromecast via rust_cast, AirPlay discovery, DLNA via rupnp) # (Chromecast via rust_cast, DLNA via rupnp)
# Async utilities (used across codebase) # Async utilities (used across codebase)
futures-util = "0.3" futures-util = "0.3"
-137
View File
@@ -1,137 +0,0 @@
//! Tauri commands for AirPlay casting (discovery + connection scaffolding)
use std::sync::Arc;
use tauri::State;
use tokio::sync::Mutex;
use crate::cast::airplay::{
AirPlayConnection, AirPlayDiscovery, AirPlayError, AirPlayMetadata, AirPlayStatus,
DiscoveredAirPlayDevice,
};
/// AirPlay state shared across commands
pub struct AirPlayState {
pub discovery: Arc<Mutex<AirPlayDiscovery>>,
pub connection: Arc<Mutex<Option<AirPlayConnection>>>,
}
impl AirPlayState {
pub fn new() -> Result<Self, AirPlayError> {
Ok(Self {
discovery: Arc::new(Mutex::new(AirPlayDiscovery::new())),
connection: Arc::new(Mutex::new(None)),
})
}
}
// === Discovery ===
#[tauri::command]
pub async fn airplay_start_discovery(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut discovery = state.discovery.lock().await;
discovery.start_discovery().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn airplay_stop_discovery(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut discovery = state.discovery.lock().await;
discovery.stop_discovery().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn airplay_get_devices(
state: State<'_, AirPlayState>,
) -> Result<Vec<DiscoveredAirPlayDevice>, String> {
let discovery = state.discovery.lock().await;
Ok(discovery.get_discovered_devices())
}
// === Connection ===
#[tauri::command]
pub async fn airplay_connect(
device_id: String,
state: State<'_, AirPlayState>,
) -> Result<(), String> {
let device = {
let discovery = state.discovery.lock().await;
discovery
.get_device(&device_id)
.ok_or_else(|| AirPlayError::DeviceNotFound(device_id.clone()))
.map_err(|e| e.to_string())?
};
let connection = AirPlayConnection::connect(device).map_err(|e| e.to_string())?;
let mut state_connection = state.connection.lock().await;
*state_connection = Some(connection);
Ok(())
}
#[tauri::command]
pub async fn airplay_disconnect(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut connection = state.connection.lock().await;
if let Some(conn) = connection.as_mut() {
conn.disconnect().map_err(|e| e.to_string())?;
}
*connection = None;
Ok(())
}
#[tauri::command]
pub async fn airplay_get_status(state: State<'_, AirPlayState>) -> Result<AirPlayStatus, String> {
let connection = state.connection.lock().await;
let conn = connection
.as_ref()
.ok_or_else(|| "Not connected".to_string())?;
Ok(conn.get_status())
}
// === Playback (stubs) ===
#[tauri::command]
pub async fn airplay_load_media(
metadata: AirPlayMetadata,
state: State<'_, AirPlayState>,
) -> Result<(), String> {
let mut connection = state.connection.lock().await;
let conn = connection
.as_mut()
.ok_or_else(|| "Not connected".to_string())?;
conn.load_media(metadata).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn airplay_play(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut connection = state.connection.lock().await;
let conn = connection
.as_mut()
.ok_or_else(|| "Not connected".to_string())?;
conn.play().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn airplay_pause(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut connection = state.connection.lock().await;
let conn = connection
.as_mut()
.ok_or_else(|| "Not connected".to_string())?;
conn.pause().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn airplay_stop(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut connection = state.connection.lock().await;
let conn = connection
.as_mut()
.ok_or_else(|| "Not connected".to_string())?;
conn.stop().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn airplay_set_volume(volume: f32, state: State<'_, AirPlayState>) -> Result<(), String> {
let mut connection = state.connection.lock().await;
let conn = connection
.as_mut()
.ok_or_else(|| "Not connected".to_string())?;
conn.set_volume(volume).map_err(|e| e.to_string())
}
-14
View File
@@ -1,14 +0,0 @@
//! AirPlay casting module (discovery + scaffolding)
//!
//! Core functionality is in qbz-cast crate, this module provides Tauri commands.
pub mod commands;
// Re-export from qbz-cast
pub use qbz_cast::{
AirPlayConnection, AirPlayDiscovery, AirPlayError, AirPlayMetadata, AirPlayStatus,
DiscoveredAirPlayDevice,
};
// Re-export Tauri command state
pub use commands::AirPlayState;
+1 -1
View File
@@ -2,4 +2,4 @@
//! //!
//! Re-exported from qbz-cast crate. //! Re-exported from qbz-cast crate.
pub use qbz_cast::{AirPlayError, CastError, DlnaError}; pub use qbz_cast::{CastError, DlnaError};
+5 -8
View File
@@ -1,9 +1,8 @@
//! Casting module (Chromecast-first, designed for future AirPlay/DLNA expansion). //! Casting module (Chromecast-first, designed for future DLNA expansion).
//! //!
//! This module provides Tauri commands for casting functionality. //! This module provides Tauri commands for casting functionality.
//! The core casting logic is in the qbz-cast crate. //! The core casting logic is in the qbz-cast crate.
pub mod airplay;
pub mod chromecast_thread; pub mod chromecast_thread;
pub mod commands; pub mod commands;
pub mod device; pub mod device;
@@ -14,14 +13,12 @@ pub mod media_server;
// Re-export from qbz-cast for internal use // Re-export from qbz-cast for internal use
pub use qbz_cast::{ pub use qbz_cast::{
AirPlayConnection, AirPlayDiscovery, AirPlayError, AirPlayMetadata, AirPlayStatus, CastCommand, CastCommand, CastDevice, CastDeviceConnection, CastError, CastPositionInfo, CastStatus,
CastDevice, CastDeviceConnection, CastError, CastPositionInfo, CastStatus, ChromecastHandle, ChromecastHandle, DeviceDiscovery, DiscoveredDevice, DiscoveredDlnaDevice, DlnaConnection,
DeviceDiscovery, DiscoveredAirPlayDevice, DiscoveredDevice, DiscoveredDlnaDevice, DlnaDiscovery, DlnaError, DlnaMetadata, DlnaPositionInfo, DlnaStatus, MediaMetadata,
DlnaConnection, DlnaDiscovery, DlnaError, DlnaMetadata, DlnaPositionInfo, DlnaStatus, MediaServer,
MediaMetadata, MediaServer,
}; };
// Re-export Tauri command states // Re-export Tauri command states
pub use airplay::commands::AirPlayState;
pub use commands::CastState; pub use commands::CastState;
pub use dlna::commands::DlnaState; pub use dlna::commands::DlnaState;
+1 -91
View File
@@ -2,9 +2,7 @@ use tauri::State;
use crate::api_cache::ApiCacheState; use crate::api_cache::ApiCacheState;
use crate::artist_blacklist::BlacklistState; use crate::artist_blacklist::BlacklistState;
use crate::cast::{ use crate::cast::{CastState, DlnaMetadata, DlnaState, MediaMetadata};
AirPlayMetadata, AirPlayState, CastState, DlnaMetadata, DlnaState, MediaMetadata,
};
use crate::config::download_settings::DownloadSettingsState; use crate::config::download_settings::DownloadSettingsState;
use crate::core_bridge::CoreBridgeState; use crate::core_bridge::CoreBridgeState;
use crate::integrations_v2::MusicBrainzV2State; use crate::integrations_v2::MusicBrainzV2State;
@@ -322,94 +320,6 @@ pub async fn v2_dlna_set_volume(volume: f32, state: State<'_, DlnaState>) -> Res
conn.set_volume(volume).await.map_err(|e| e.to_string()) conn.set_volume(volume).await.map_err(|e| e.to_string())
} }
#[tauri::command]
pub async fn v2_airplay_start_discovery(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut discovery = state.discovery.lock().await;
discovery.start_discovery().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn v2_airplay_stop_discovery(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut discovery = state.discovery.lock().await;
discovery.stop_discovery().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn v2_airplay_get_devices(
state: State<'_, AirPlayState>,
) -> Result<Vec<crate::cast::DiscoveredAirPlayDevice>, String> {
let discovery = state.discovery.lock().await;
Ok(discovery.get_discovered_devices())
}
#[tauri::command]
pub async fn v2_airplay_connect(
device_id: String,
state: State<'_, AirPlayState>,
) -> Result<(), String> {
let device = {
let discovery = state.discovery.lock().await;
discovery
.get_device(&device_id)
.ok_or_else(|| format!("Device not found: {}", device_id))?
};
let connection = crate::cast::AirPlayConnection::connect(device).map_err(|e| e.to_string())?;
let mut state_connection = state.connection.lock().await;
*state_connection = Some(connection);
Ok(())
}
#[tauri::command]
pub async fn v2_airplay_disconnect(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut connection = state.connection.lock().await;
if let Some(conn) = connection.as_mut() {
conn.disconnect().map_err(|e| e.to_string())?;
}
*connection = None;
Ok(())
}
#[tauri::command]
pub async fn v2_airplay_load_media(
metadata: AirPlayMetadata,
state: State<'_, AirPlayState>,
) -> Result<(), String> {
let mut connection = state.connection.lock().await;
let conn = connection.as_mut().ok_or("Not connected")?;
conn.load_media(metadata).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn v2_airplay_play(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut connection = state.connection.lock().await;
let conn = connection.as_mut().ok_or("Not connected")?;
conn.play().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn v2_airplay_pause(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut connection = state.connection.lock().await;
let conn = connection.as_mut().ok_or("Not connected")?;
conn.pause().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn v2_airplay_stop(state: State<'_, AirPlayState>) -> Result<(), String> {
let mut connection = state.connection.lock().await;
let conn = connection.as_mut().ok_or("Not connected")?;
conn.stop().map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn v2_airplay_set_volume(
volume: f32,
state: State<'_, AirPlayState>,
) -> Result<(), String> {
let mut connection = state.connection.lock().await;
let conn = connection.as_mut().ok_or("Not connected")?;
conn.set_volume(volume).map_err(|e| e.to_string())
}
#[tauri::command] #[tauri::command]
pub async fn v2_clear_offline_cache( pub async fn v2_clear_offline_cache(
cache_state: State<'_, OfflineCacheState>, cache_state: State<'_, OfflineCacheState>,
-11
View File
@@ -1216,7 +1216,6 @@ pub fn run() {
.manage(library_state) .manage(library_state)
.manage(cast_state) .manage(cast_state)
.manage(dlna_state) .manage(dlna_state)
// .manage(airplay_state) // AirPlay DISABLED
.manage(offline_cache_state) .manage(offline_cache_state)
.manage(lyrics_state) .manage(lyrics_state)
.manage(reco_state) .manage(reco_state)
@@ -1415,16 +1414,6 @@ pub fn run() {
commands_v2::v2_dlna_seek, commands_v2::v2_dlna_seek,
commands_v2::v2_dlna_get_position, commands_v2::v2_dlna_get_position,
commands_v2::v2_dlna_set_volume, commands_v2::v2_dlna_set_volume,
commands_v2::v2_airplay_start_discovery,
commands_v2::v2_airplay_stop_discovery,
commands_v2::v2_airplay_get_devices,
commands_v2::v2_airplay_connect,
commands_v2::v2_airplay_disconnect,
commands_v2::v2_airplay_load_media,
commands_v2::v2_airplay_play,
commands_v2::v2_airplay_pause,
commands_v2::v2_airplay_stop,
commands_v2::v2_airplay_set_volume,
commands_v2::v2_clear_offline_cache, commands_v2::v2_clear_offline_cache,
commands_v2::v2_library_remove_folder, commands_v2::v2_library_remove_folder,
commands_v2::v2_library_check_folder_accessible, commands_v2::v2_library_check_folder_accessible,
+1 -2
View File
@@ -1,7 +1,6 @@
//! mDNS discovery for qbzd daemon instances on the LAN. //! mDNS discovery for qbzd daemon instances on the LAN.
//! //!
//! Uses the same mdns-sd crate as AirPlay discovery in qbz-cast. //! Uses the mdns-sd crate. Discovers `_qbz._tcp.local` services.
//! Discovers `_qbz._tcp.local` services.
use serde::Serialize; use serde::Serialize;
use std::sync::Arc; use std::sync::Arc;
@@ -249,8 +249,6 @@
DLNA DLNA
{:else if castProtocol === 'chromecast'} {:else if castProtocol === 'chromecast'}
CAST CAST
{:else if castProtocol === 'airplay'}
AIR
{:else} {:else}
CAST CAST
{/if} {/if}
+1 -9
View File
@@ -2,7 +2,7 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { X, Cast, LoaderCircle, Monitor, Wifi, Tv, Speaker, Power } from 'lucide-svelte'; import { X, Cast, LoaderCircle, Monitor, Wifi, Tv, Power } from 'lucide-svelte';
import { import {
subscribe as subscribeCast, subscribe as subscribeCast,
getCastState, getCastState,
@@ -72,7 +72,6 @@
let savedDaemons = $state<SavedDaemon[]>(loadSavedDaemons()); let savedDaemons = $state<SavedDaemon[]>(loadSavedDaemons());
let chromecastDevices = $state<CastDevice[]>([]); let chromecastDevices = $state<CastDevice[]>([]);
let dlnaDevices = $state<CastDevice[]>([]); let dlnaDevices = $state<CastDevice[]>([]);
let airplayDevices = $state<CastDevice[]>([]);
let qbzdDevices = $state<QbzdDevice[]>([]); let qbzdDevices = $state<QbzdDevice[]>([]);
let loading = $state(false); let loading = $state(false);
let error = $state<string | null>(null); let error = $state<string | null>(null);
@@ -93,7 +92,6 @@
switch (activeProtocol) { switch (activeProtocol) {
case 'chromecast': return chromecastDevices; case 'chromecast': return chromecastDevices;
case 'dlna': return dlnaDevices; case 'dlna': return dlnaDevices;
case 'airplay': return airplayDevices;
case 'qbzd': return []; // qbzd uses its own list case 'qbzd': return []; // qbzd uses its own list
} }
}); });
@@ -135,7 +133,6 @@
try { try {
// Start discovery protocols in parallel // Start discovery protocols in parallel
// Note: AirPlay discovery disabled until RAOP streaming is implemented
await Promise.allSettled([ await Promise.allSettled([
invoke('v2_cast_start_discovery'), invoke('v2_cast_start_discovery'),
invoke('v2_dlna_start_discovery'), invoke('v2_dlna_start_discovery'),
@@ -171,7 +168,6 @@
try { try {
// Poll active protocols in parallel // Poll active protocols in parallel
// Note: AirPlay polling disabled until RAOP streaming is implemented
const results = await Promise.allSettled([ const results = await Promise.allSettled([
invoke<CastDevice[]>('v2_cast_get_devices'), invoke<CastDevice[]>('v2_cast_get_devices'),
invoke<CastDevice[]>('v2_dlna_get_devices'), invoke<CastDevice[]>('v2_dlna_get_devices'),
@@ -314,7 +310,6 @@
case 'qbzd': return Monitor; case 'qbzd': return Monitor;
case 'chromecast': return Cast; case 'chromecast': return Cast;
case 'dlna': return Tv; case 'dlna': return Tv;
case 'airplay': return Speaker;
} }
} }
@@ -323,7 +318,6 @@
case 'qbzd': return 'QBZ Daemon'; case 'qbzd': return 'QBZ Daemon';
case 'chromecast': return 'Chromecast'; case 'chromecast': return 'Chromecast';
case 'dlna': return 'DLNA'; case 'dlna': return 'DLNA';
case 'airplay': return 'AirPlay';
} }
} }
</script> </script>
@@ -406,8 +400,6 @@
<span class="count">{dlnaDevices.length}</span> <span class="count">{dlnaDevices.length}</span>
{/if} {/if}
</button> </button>
<!-- AirPlay hidden until RAOP streaming is implemented -->
<!-- See docs/AIRPLAY_IMPLEMENTATION_STATUS.md for details -->
</div> </div>
<div class="content"> <div class="content">
+1 -34
View File
@@ -15,7 +15,7 @@ import {
type PlayingTrack type PlayingTrack
} from './playerStore'; } from './playerStore';
export type CastProtocol = 'chromecast' | 'dlna' | 'airplay'; export type CastProtocol = 'chromecast' | 'dlna';
export interface CastDevice { export interface CastDevice {
id: string; id: string;
@@ -154,9 +154,6 @@ export async function connectToDevice(device: CastDevice, protocol: CastProtocol
case 'dlna': case 'dlna':
await invoke('v2_dlna_connect', { deviceId: device.id }); await invoke('v2_dlna_connect', { deviceId: device.id });
break; break;
case 'airplay':
await invoke('v2_airplay_connect', { deviceId: device.id });
break;
} }
state = { state = {
@@ -240,9 +237,6 @@ export async function disconnect(): Promise<void> {
case 'dlna': case 'dlna':
await invoke('v2_dlna_disconnect'); await invoke('v2_dlna_disconnect');
break; break;
case 'airplay':
await invoke('v2_airplay_disconnect');
break;
} }
} catch (err) { } catch (err) {
console.error('[CastStore] Failed to disconnect:', err); console.error('[CastStore] Failed to disconnect:', err);
@@ -348,18 +342,6 @@ export async function castTrack(
} }
}); });
break; break;
case 'airplay':
await invoke('v2_airplay_load_media', {
metadata: {
title: metadata.title,
artist: metadata.artist,
album: metadata.album,
artwork_url: metadata.artworkUrl,
duration_secs: metadata.durationSecs
}
});
await invoke('v2_airplay_play');
break;
} }
state = { state = {
@@ -396,9 +378,6 @@ export async function castPlay(): Promise<void> {
case 'dlna': case 'dlna':
await invoke('v2_dlna_play'); await invoke('v2_dlna_play');
break; break;
case 'airplay':
await invoke('v2_airplay_play');
break;
} }
state = { ...state, isPlaying: true }; state = { ...state, isPlaying: true };
notifyListeners(); notifyListeners();
@@ -422,9 +401,6 @@ export async function castPause(): Promise<void> {
case 'dlna': case 'dlna':
await invoke('v2_dlna_pause'); await invoke('v2_dlna_pause');
break; break;
case 'airplay':
await invoke('v2_airplay_pause');
break;
} }
state = { ...state, isPlaying: false }; state = { ...state, isPlaying: false };
notifyListeners(); notifyListeners();
@@ -448,9 +424,6 @@ export async function castStop(): Promise<void> {
case 'dlna': case 'dlna':
await invoke('v2_dlna_stop'); await invoke('v2_dlna_stop');
break; break;
case 'airplay':
await invoke('v2_airplay_stop');
break;
} }
state = { ...state, isPlaying: false, currentTrackId: null }; state = { ...state, isPlaying: false, currentTrackId: null };
notifyListeners(); notifyListeners();
@@ -474,9 +447,6 @@ export async function castSeek(positionSecs: number): Promise<void> {
case 'dlna': case 'dlna':
await invoke('v2_dlna_seek', { positionSecs }); await invoke('v2_dlna_seek', { positionSecs });
break; break;
case 'airplay':
// AirPlay seek - not implemented
break;
} }
} catch (err) { } catch (err) {
console.error('[CastStore] Failed to seek:', err); console.error('[CastStore] Failed to seek:', err);
@@ -500,9 +470,6 @@ export async function castSetVolume(volume: number): Promise<void> {
case 'dlna': case 'dlna':
await invoke('v2_dlna_set_volume', { volume: normalizedVolume }); await invoke('v2_dlna_set_volume', { volume: normalizedVolume });
break; break;
case 'airplay':
await invoke('v2_airplay_set_volume', { volume: normalizedVolume });
break;
} }
} catch (err) { } catch (err) {
console.error('[CastStore] Failed to set volume:', err); console.error('[CastStore] Failed to set volume:', err);