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:
18 files changed
+17
-658
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
@@ -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};
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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())
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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};
|
||||||
@@ -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;
|
||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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,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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user