Compare commits
15 Commits
35e2e037f0
...
push-zpokn
| Author | SHA1 | Date | |
|---|---|---|---|
| 3401ee839c | |||
| e8e33414f0 | |||
| b0e24c3b3c | |||
| b5b6becb25 | |||
| 353e54af76 | |||
| f2be1b9bc1 | |||
| 6e65558ba9 | |||
| 4c7c99f0a4 | |||
| 5c6e2d43ac | |||
| 6fd11ab450 | |||
| 57c2552247 | |||
| 9935a5e5af | |||
| 3979d346a5 | |||
| 6240179397 | |||
| 934bbaf4b8 |
306
Blackboard/Todo/mdns_sd_migration.md
Normal file
306
Blackboard/Todo/mdns_sd_migration.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Migration `mdns` → `mdns-sd`
|
||||
|
||||
## Contexte
|
||||
|
||||
La crate `mdns 3.0.0` (dernière version, 2021, abandonnée) s'appuie sur `dns-parser 0.8.0`
|
||||
qui rejette les labels DNS non-ASCII avec `LabelIsNotAscii`. Les appareils Apple utilisent
|
||||
U+2019 (RIGHT SINGLE QUOTATION MARK) dans leurs noms (ex : "Sophie's MacBook Air"), ce qui
|
||||
spamme les logs en WARN à chaque paquet mDNS reçu.
|
||||
|
||||
`mdns-sd 0.19` (avril 2026, activement maintenu) gère l'UTF-8 correctement, fournit une API
|
||||
de plus haut niveau (service pré-assemblé), et ne nécessite pas `async-std`.
|
||||
|
||||
## Périmètre
|
||||
|
||||
Deux fichiers à modifier, un fichier à nettoyer :
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---|---|
|
||||
| `pmocontrol/Cargo.toml` | Dépendances |
|
||||
| `pmocontrol/src/control_point.rs` | Thread de découverte mDNS (lignes 145–195) |
|
||||
| `pmocontrol/src/discovery/chromecast_discovery.rs` | Parsing des réponses mDNS |
|
||||
| `pmoserver/src/logs/mod.rs` | Filtre de bruit `mdns=error` devenu inutile |
|
||||
|
||||
---
|
||||
|
||||
## Étape 1 — `pmocontrol/Cargo.toml`
|
||||
|
||||
### Supprimer
|
||||
```toml
|
||||
mdns = "3.0"
|
||||
```
|
||||
|
||||
Vérifier si `async-std` et `futures-util` sont utilisés **ailleurs** que dans le thread mDNS.
|
||||
D'après l'analyse :
|
||||
- `async_std` : uniquement `control_point.rs:161` → **supprimer**
|
||||
- `futures-util` : uniquement `control_point.rs:150,167` → **supprimer**
|
||||
|
||||
### Ajouter
|
||||
```toml
|
||||
mdns-sd = "0.19"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 2 — `pmocontrol/src/control_point.rs`
|
||||
|
||||
### Code actuel (lignes 145–195) à remplacer intégralement
|
||||
|
||||
```rust
|
||||
// Thread de découverte mDNS pour Chromecast
|
||||
let registry_for_mdns = Arc::clone(®istry);
|
||||
let udn_cache_for_mdns = Arc::clone(&udn_cache);
|
||||
thread::spawn(move || {
|
||||
use crate::discovery::ChromecastDiscoveryManager;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
let mut discovery_manager =
|
||||
ChromecastDiscoveryManager::new(registry_for_mdns, udn_cache_for_mdns);
|
||||
|
||||
debug!("Starting mDNS discovery thread for Chromecast devices");
|
||||
|
||||
const SERVICE_NAME: &str = "_googlecast._tcp.local";
|
||||
|
||||
async_std::task::block_on(async {
|
||||
match mdns::discover::all(SERVICE_NAME, Duration::from_secs(15)) {
|
||||
Ok(discovery) => {
|
||||
let stream = discovery.listen();
|
||||
futures_util::pin_mut!(stream);
|
||||
debug!("mDNS discovery stream started for Chromecast devices");
|
||||
while let Some(result) = stream.next().await {
|
||||
match result {
|
||||
Ok(response) => {
|
||||
debug!("Received mDNS response with {} records", response.records().count());
|
||||
discovery_manager.handle_mdns_response(response);
|
||||
}
|
||||
Err(e) => { warn!("mDNS discovery error: {}", e); }
|
||||
}
|
||||
}
|
||||
warn!("mDNS discovery stream ended unexpectedly");
|
||||
}
|
||||
Err(e) => { error!("Failed to start mDNS discovery: {}", e); }
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Nouveau code
|
||||
|
||||
```rust
|
||||
// Thread de découverte mDNS pour Chromecast
|
||||
let registry_for_mdns = Arc::clone(®istry);
|
||||
let udn_cache_for_mdns = Arc::clone(&udn_cache);
|
||||
thread::spawn(move || {
|
||||
use crate::discovery::ChromecastDiscoveryManager;
|
||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||
|
||||
let mut discovery_manager =
|
||||
ChromecastDiscoveryManager::new(registry_for_mdns, udn_cache_for_mdns);
|
||||
|
||||
debug!("Starting mDNS discovery thread for Chromecast devices");
|
||||
|
||||
// Note: mdns-sd requires the trailing dot in the service type
|
||||
const SERVICE_TYPE: &str = "_googlecast._tcp.local.";
|
||||
|
||||
let daemon = match ServiceDaemon::new() {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!("Failed to create mDNS daemon: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let receiver = match daemon.browse(SERVICE_TYPE) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Failed to start mDNS browse for {}: {}", SERVICE_TYPE, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!("mDNS discovery started for Chromecast devices");
|
||||
|
||||
while let Ok(event) = receiver.recv() {
|
||||
match event {
|
||||
ServiceEvent::ServiceResolved(info) => {
|
||||
debug!(
|
||||
fullname = info.get_fullname(),
|
||||
host = info.get_hostname(),
|
||||
port = info.get_port(),
|
||||
"mDNS Chromecast service resolved"
|
||||
);
|
||||
discovery_manager.handle_service_resolved(&info);
|
||||
}
|
||||
ServiceEvent::ServiceRemoved(_service_type, fullname) => {
|
||||
debug!(fullname = %fullname, "mDNS Chromecast service removed");
|
||||
// Pas de retrait actif du registre : le timeout habituel s'en charge
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
warn!("mDNS discovery receiver closed");
|
||||
});
|
||||
```
|
||||
|
||||
### Points d'attention
|
||||
- Le point final `.` dans `"_googlecast._tcp.local."` est **obligatoire** pour `mdns-sd`.
|
||||
- `receiver.recv()` est bloquant synchrone — pas besoin d'async runtime.
|
||||
- `ServiceDaemon` gère son propre thread interne ; inutile de relancer manuellement.
|
||||
|
||||
---
|
||||
|
||||
## Étape 3 — `pmocontrol/src/discovery/chromecast_discovery.rs`
|
||||
|
||||
### Supprimer l'import `mdns`
|
||||
|
||||
```rust
|
||||
// Supprimer ces uses implicites via le type dans la signature
|
||||
use std::collections::HashMap; // <- plus nécessaire si on passe par TxtProperties
|
||||
```
|
||||
|
||||
### Ajouter l'import `mdns-sd`
|
||||
|
||||
```rust
|
||||
use mdns_sd::ServiceInfo;
|
||||
```
|
||||
|
||||
### Remplacer `handle_mdns_response` par `handle_service_resolved`
|
||||
|
||||
#### Code actuel (lignes 39–179) — ~80 lignes de parsing manuel
|
||||
|
||||
Toute la logique d'extraction PTR / A / AAAA / SRV / TXT disparaît.
|
||||
|
||||
#### Nouveau code
|
||||
|
||||
```rust
|
||||
/// Traite un service Chromecast résolu par mDNS-SD.
|
||||
///
|
||||
/// `ServiceInfo` arrive pré-assemblé : plus besoin de jointure manuelle
|
||||
/// des enregistrements PTR / A / SRV / TXT.
|
||||
pub fn handle_service_resolved(&mut self, info: &ServiceInfo) {
|
||||
let fullname = info.get_fullname().to_string();
|
||||
|
||||
debug!("Processing resolved Chromecast service: {}", fullname);
|
||||
|
||||
// Adresse IP : préférer IPv4
|
||||
let host = match info
|
||||
.get_addresses()
|
||||
.iter()
|
||||
.find(|a| a.is_ipv4())
|
||||
.or_else(|| info.get_addresses().iter().next())
|
||||
{
|
||||
Some(addr) => addr.to_string(),
|
||||
None => {
|
||||
warn!("No IP address for Chromecast service: {}", fullname);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let port = info.get_port();
|
||||
|
||||
// TXT records — API directe par clé
|
||||
let uuid = info
|
||||
.get_property_val_str("id")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let uuid = if uuid.is_empty() {
|
||||
format!("chromecast-{}-{}", host, port)
|
||||
} else {
|
||||
uuid
|
||||
};
|
||||
|
||||
let model = info.get_property_val_str("md").map(|s| s.to_string());
|
||||
|
||||
let friendly_name = info
|
||||
.get_property_val_str("fn")
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
// Fallback : extraire depuis le fullname, supprimer le suffixe de service
|
||||
fullname
|
||||
.split("._googlecast._tcp.local")
|
||||
.next()
|
||||
.unwrap_or("Unknown Chromecast")
|
||||
.split('-')
|
||||
.take_while(|part| part.len() != 32)
|
||||
.collect::<Vec<_>>()
|
||||
.join("-")
|
||||
.trim()
|
||||
.to_string()
|
||||
});
|
||||
|
||||
debug!(
|
||||
"Discovered Chromecast: {} at {}:{} (UUID: {}, Model: {:?})",
|
||||
friendly_name, host, port, uuid, model
|
||||
);
|
||||
|
||||
let udn = format!("uuid:{}", uuid);
|
||||
let default_max_age = 1800u64;
|
||||
|
||||
if !UDNRegistry::should_fetch(self.udn_cache.clone(), &udn, default_max_age) {
|
||||
debug!("Chromecast {} recently seen, skipping", udn);
|
||||
return;
|
||||
}
|
||||
|
||||
let renderer_info = build_renderer_info(
|
||||
&uuid,
|
||||
&friendly_name,
|
||||
&host,
|
||||
port,
|
||||
model.as_deref(),
|
||||
Some("Google Inc."),
|
||||
);
|
||||
|
||||
self.device_registry
|
||||
.write()
|
||||
.expect("DeviceRegistry mutex lock failed")
|
||||
.push_renderer(&renderer_info, default_max_age as u32);
|
||||
}
|
||||
```
|
||||
|
||||
### Supprimer
|
||||
- L'import `use std::collections::HashMap` (plus utilisé)
|
||||
- Tout le bloc `handle_mdns_response` (lignes 39–179)
|
||||
|
||||
### Conserver sans modification
|
||||
- `build_renderer_info` (lignes 182–234)
|
||||
- `extract_host_from_location` / `extract_port_from_location` (lignes 239–258)
|
||||
- Les tests (lignes 260–287)
|
||||
|
||||
---
|
||||
|
||||
## Étape 4 — `pmoserver/src/logs/mod.rs`
|
||||
|
||||
Le filtre de bruit `mdns=error` injecté dans `build_filter_with_noise_suppressions` n'est
|
||||
plus nécessaire. Deux options :
|
||||
|
||||
**Option A (recommandée)** — Supprimer l'entrée du tableau :
|
||||
```rust
|
||||
const NOISE_FILTERS: &[(&str, &str)] = &[
|
||||
// ("mdns", "mdns=error"), // supprimé : migration vers mdns-sd
|
||||
];
|
||||
```
|
||||
Ou supprimer `build_filter_with_noise_suppressions` entièrement si aucun autre bruit n'est
|
||||
à filtrer, et revenir à `EnvFilter::try_new(base)` direct.
|
||||
|
||||
**Option B** — Laisser en place. La directive `mdns=error` ne cause aucun dommage si la
|
||||
crate `mdns` n'est plus dans le build (elle sera simplement ignorée).
|
||||
|
||||
---
|
||||
|
||||
## Résumé des diffs
|
||||
|
||||
| Fichier | Lignes supprimées | Lignes ajoutées |
|
||||
|---|---|---|
|
||||
| `Cargo.toml` | `mdns`, `async-std`, `futures-util` | `mdns-sd` |
|
||||
| `control_point.rs` | ~50 (async block) | ~35 (sync recv loop) |
|
||||
| `chromecast_discovery.rs` | ~80 (parsing manuel) | ~50 (lecture ServiceInfo) |
|
||||
| `logs/mod.rs` | ~5 (filtre bruit) | 0 |
|
||||
|
||||
## Vérification
|
||||
|
||||
Après implémentation :
|
||||
1. `cargo check -p pmocontrol` sans erreurs ni `use of undeclared crate mdns`
|
||||
2. `cargo check -p pmoserver` sans erreurs
|
||||
3. Tester la découverte d'un Chromecast en réseau local
|
||||
4. Vérifier l'absence de `LabelIsNotAscii` dans les logs avec des appareils Apple présents
|
||||
373
Cargo.lock
generated
373
Cargo.lock
generated
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "PMOMusic"
|
||||
version = "0.3.41"
|
||||
version = "0.3.47"
|
||||
dependencies = [
|
||||
"axum 0.8.7",
|
||||
"console-subscriber",
|
||||
@@ -49,7 +49,7 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
@@ -100,7 +100,7 @@ checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -176,27 +176,6 @@ dependencies = [
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-attributes"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener 2.5.3",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
@@ -234,21 +213,6 @@ dependencies = [
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-global-executor"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
@@ -256,7 +220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
@@ -273,7 +237,7 @@ version = "3.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
|
||||
dependencies = [
|
||||
"event-listener 5.4.1",
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
@@ -295,14 +259,14 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if 1.0.4",
|
||||
"event-listener 5.4.1",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix 1.1.2",
|
||||
]
|
||||
@@ -327,7 +291,7 @@ dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix 1.1.2",
|
||||
@@ -336,66 +300,17 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-std"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
|
||||
dependencies = [
|
||||
"async-attributes",
|
||||
"async-channel 1.9.0",
|
||||
"async-global-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"crossbeam-utils",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"gloo-timers",
|
||||
"kv-log-macro",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22068c0c19514942eefcfd4daf8976ef1aad84e61539f95cd200c35202f80af5"
|
||||
dependencies = [
|
||||
"async-stream-impl 0.2.1",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||
dependencies = [
|
||||
"async-stream-impl 0.3.6",
|
||||
"async-stream-impl",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.3.6"
|
||||
@@ -682,7 +597,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
"spin",
|
||||
"spin 0.10.0",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
@@ -815,7 +730,7 @@ version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
@@ -930,12 +845,6 @@ dependencies = [
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -1030,7 +939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
@@ -1202,7 +1111,7 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1458,16 +1367,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd"
|
||||
|
||||
[[package]]
|
||||
name = "dns-parser"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4d33be9473d06f75f58220f71f7a9317aca647dc061dbd3c361b0bef505fbea"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"quick-error 1.2.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
@@ -1522,7 +1421,7 @@ version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1562,20 +1461,6 @@ dependencies = [
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "err-derive"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22deed3a8124cff5fa835713fa105621e43bbdc46690c3a6b68328a012d350d4"
|
||||
dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 1.0.109",
|
||||
"synstructure 0.12.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -1586,12 +1471,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
@@ -1609,7 +1488,7 @@ version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener 5.4.1",
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -1734,6 +1613,17 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"spin 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -1966,7 +1856,7 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
@@ -1977,7 +1867,7 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
@@ -2018,18 +1908,6 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.12"
|
||||
@@ -2055,7 +1933,7 @@ version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
@@ -2452,6 +2330,16 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "if-addrs"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.8"
|
||||
@@ -2483,7 +2371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error 2.0.1",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2599,7 +2487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
@@ -2634,15 +2522,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kv-log-macro"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -2698,7 +2577,7 @@ version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
@@ -2815,9 +2694,6 @@ name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
dependencies = [
|
||||
"value-bag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loop9"
|
||||
@@ -2904,7 +2780,7 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
@@ -2914,24 +2790,23 @@ version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdns"
|
||||
version = "3.0.0"
|
||||
name = "mdns-sd"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c769962ac75a6ea437f0922b27834bcccd4c013d591383a16ae5731e3ef0f3f3"
|
||||
checksum = "451927183d65d600e52b4e877a1251e051576f84fa01e5b4a50b450dfaaa537c"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-stream 0.2.1",
|
||||
"dns-parser",
|
||||
"err-derive",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"fastrand",
|
||||
"flume",
|
||||
"if-addrs",
|
||||
"log",
|
||||
"net2",
|
||||
"mio 1.1.0",
|
||||
"socket-pktinfo",
|
||||
"socket2 0.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3011,6 +2886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -3050,7 +2926,7 @@ dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"equivalent",
|
||||
"event-listener 5.4.1",
|
||||
"event-listener",
|
||||
"futures-util",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
@@ -3116,17 +2992,6 @@ dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "net2"
|
||||
version = "0.2.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"libc",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netlink-packet-core"
|
||||
version = "0.7.0"
|
||||
@@ -3207,7 +3072,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
@@ -3573,7 +3438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
@@ -3664,7 +3529,7 @@ version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
@@ -3927,15 +3792,13 @@ name = "pmocontrol"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
"async-stream 0.3.6",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum 0.8.7",
|
||||
"chrono",
|
||||
"crossbeam-channel",
|
||||
"crossterm",
|
||||
"futures-util",
|
||||
"mdns",
|
||||
"mdns-sd",
|
||||
"percent-encoding",
|
||||
"pmocovers",
|
||||
"pmodidl",
|
||||
@@ -4095,7 +3958,7 @@ name = "pmoparadise"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream 0.3.6",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum 0.8.7",
|
||||
"bytes",
|
||||
@@ -4140,7 +4003,7 @@ name = "pmoplaylist"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream 0.3.6",
|
||||
"async-stream",
|
||||
"axum 0.8.7",
|
||||
"chrono",
|
||||
"once_cell",
|
||||
@@ -4234,7 +4097,7 @@ name = "pmoserver"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream 0.3.6",
|
||||
"async-stream",
|
||||
"axum 0.8.7",
|
||||
"axum-server",
|
||||
"futures",
|
||||
@@ -4376,7 +4239,7 @@ version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
@@ -4390,7 +4253,7 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
@@ -4460,30 +4323,6 @@ dependencies = [
|
||||
"toml_edit 0.23.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
@@ -4629,12 +4468,6 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
@@ -4757,7 +4590,7 @@ dependencies = [
|
||||
"av1-grain",
|
||||
"bitstream-io",
|
||||
"built",
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"interpolate_name",
|
||||
"itertools 0.12.1",
|
||||
"libc",
|
||||
@@ -4789,7 +4622,7 @@ dependencies = [
|
||||
"avif-serialize",
|
||||
"imgref",
|
||||
"loop9",
|
||||
"quick-error 2.0.1",
|
||||
"quick-error",
|
||||
"rav1e",
|
||||
"rayon",
|
||||
"rgb",
|
||||
@@ -4923,7 +4756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.16",
|
||||
"libc",
|
||||
"untrusted",
|
||||
@@ -5303,7 +5136,7 @@ version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
@@ -5314,7 +5147,7 @@ version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
@@ -5420,7 +5253,7 @@ version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-channel",
|
||||
"async-executor",
|
||||
"async-fs",
|
||||
"async-io",
|
||||
@@ -5440,6 +5273,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket-pktinfo"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927136cc2ae6a1b0e66ac6b1210902b75c3f726db004a73bc18686dcd0dcd22f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"socket2 0.6.1",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
@@ -5471,6 +5315,15 @@ dependencies = [
|
||||
"libsoxr-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.10.0"
|
||||
@@ -5781,18 +5634,6 @@ dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.12.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
@@ -5810,7 +5651,7 @@ version = "0.30.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"ntapi",
|
||||
@@ -5935,7 +5776,7 @@ version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5947,7 +5788,7 @@ dependencies = [
|
||||
"fax",
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error 2.0.1",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
]
|
||||
@@ -6075,7 +5916,7 @@ version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
|
||||
dependencies = [
|
||||
"async-stream 0.3.6",
|
||||
"async-stream",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"tokio",
|
||||
@@ -6177,7 +6018,7 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
|
||||
dependencies = [
|
||||
"async-stream 0.3.6",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum 0.7.9",
|
||||
"base64 0.22.1",
|
||||
@@ -6426,12 +6267,6 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
@@ -6617,12 +6452,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
|
||||
|
||||
[[package]]
|
||||
name = "variadics_please"
|
||||
version = "1.1.0"
|
||||
@@ -6692,7 +6521,7 @@ version = "0.2.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
@@ -6705,7 +6534,7 @@ version = "0.4.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -7368,7 +7197,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"synstructure 0.13.2",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7409,7 +7238,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"synstructure 0.13.2",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "PMOMusic"
|
||||
version = "0.3.41"
|
||||
version = "0.3.48"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -13,6 +13,7 @@ const props = defineProps<{
|
||||
const { state } = useRenderer(toRef(props, "rendererId"));
|
||||
const uiStore = useUIStore();
|
||||
const metadata = computed(() => state.value?.current_track);
|
||||
const rendererName = computed(() => state.value?.friendly_name ?? props.rendererId);
|
||||
const isSeeking = ref(false);
|
||||
const isDragging = ref(false);
|
||||
const dragProgress = ref(0);
|
||||
@@ -149,7 +150,7 @@ function handleProgressBarMouseDown(event: MouseEvent) {
|
||||
await api.seekTo(props.rendererId, newPositionSeconds);
|
||||
} catch (error) {
|
||||
uiStore.notifyError(
|
||||
`Impossible de seek: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
`« ${rendererName.value} » — impossible de changer la position: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
// En cas d'erreur, réinitialiser
|
||||
seekTargetMs.value = null;
|
||||
@@ -188,7 +189,7 @@ async function handleProgressBarClick(event: MouseEvent) {
|
||||
await api.seekTo(props.rendererId, newPositionSeconds);
|
||||
} catch (error) {
|
||||
uiStore.notifyError(
|
||||
`Impossible de seek: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
`« ${rendererName.value} » — impossible de changer la position: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
// En cas d'erreur, réinitialiser
|
||||
seekTargetMs.value = null;
|
||||
@@ -251,7 +252,7 @@ function handleOverlayProgressBarTouchStart(event: TouchEvent) {
|
||||
await api.seekTo(props.rendererId, newPositionSeconds);
|
||||
} catch (error) {
|
||||
uiStore.notifyError(
|
||||
`Impossible de seek: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
`« ${rendererName.value} » — impossible de changer la position: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
seekTargetMs.value = null;
|
||||
dragProgress.value = 0;
|
||||
|
||||
@@ -20,12 +20,14 @@ const isStopped = computed(
|
||||
state.value?.transport_state === "NO_MEDIA",
|
||||
);
|
||||
|
||||
const rendererName = computed(() => state.value?.friendly_name ?? props.rendererId);
|
||||
|
||||
async function handlePlay() {
|
||||
try {
|
||||
await resumeOrPlayFromQueue(props.rendererId);
|
||||
} catch (error) {
|
||||
uiStore.notifyError(
|
||||
`Impossible de démarrer la lecture: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
`« ${rendererName.value} » — impossible de démarrer la lecture: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +37,7 @@ async function handlePause() {
|
||||
await pause(props.rendererId);
|
||||
} catch (error) {
|
||||
uiStore.notifyError(
|
||||
`Impossible de mettre en pause: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
`« ${rendererName.value} » — impossible de mettre en pause: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +47,7 @@ async function handleStop() {
|
||||
await stop(props.rendererId);
|
||||
} catch (error) {
|
||||
uiStore.notifyError(
|
||||
`Impossible d'arrêter la lecture: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
`« ${rendererName.value} » — impossible d'arrêter la lecture: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +57,7 @@ async function handleNext() {
|
||||
await next(props.rendererId);
|
||||
} catch (error) {
|
||||
uiStore.notifyError(
|
||||
`Impossible de passer au morceau suivant: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
`« ${rendererName.value} » — impossible de passer au morceau suivant: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, toRef } from "vue";
|
||||
import { ref, watch, computed, toRef } from "vue";
|
||||
import { useRenderer, useRenderers } from "@/composables/useRenderers";
|
||||
import { useUIStore } from "@/stores/ui";
|
||||
import { Volume2, VolumeX } from "lucide-vue-next";
|
||||
@@ -12,6 +12,7 @@ const { state } = useRenderer(toRef(props, "rendererId"));
|
||||
const { setVolume, toggleMute } = useRenderers();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const rendererName = computed(() => state.value?.friendly_name ?? props.rendererId);
|
||||
const localVolume = ref(state.value?.volume ?? 50);
|
||||
|
||||
// Synchroniser localVolume avec le state
|
||||
@@ -41,7 +42,7 @@ function handleVolumeChange(event: Event) {
|
||||
await setVolume(props.rendererId, localVolume.value);
|
||||
} catch (error) {
|
||||
uiStore.notifyError(
|
||||
`Impossible de régler le volume: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
`« ${rendererName.value} » — impossible de régler le volume: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
}
|
||||
debounceTimer = null;
|
||||
@@ -53,7 +54,7 @@ async function handleToggleMute() {
|
||||
await toggleMute(props.rendererId);
|
||||
} catch (error) {
|
||||
uiStore.notifyError(
|
||||
`Impossible de basculer le mode muet: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
`« ${rendererName.value} » — impossible de couper/rétablir le son: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const { renderer, state, queue, refresh } = useRenderer(
|
||||
toRef(props, "rendererId"),
|
||||
);
|
||||
const uiStore = useUIStore();
|
||||
const rendererName = computed(() => state.value?.friendly_name ?? props.rendererId);
|
||||
|
||||
// Détection mobile portrait pour afficher le drawer au lieu de la colonne
|
||||
const isMobilePortrait = useMediaQuery(
|
||||
@@ -59,7 +60,7 @@ async function handleQueueItemClick(item: QueueItem) {
|
||||
error,
|
||||
);
|
||||
uiStore.notifyError(
|
||||
`Erreur: ${error instanceof Error ? error.message : "Impossible de sauter à cet item"}`,
|
||||
`« ${rendererName.value} » — impossible de lire ce morceau: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,6 @@ const loadingIds = reactive(new Set<string>());
|
||||
const queueRefreshingIds = reactive(new Set<string>());
|
||||
const selectedRendererId = ref<string | null>(null);
|
||||
|
||||
// Debounce pour les refetches queue_updated
|
||||
const queueUpdateDebounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
const QUEUE_UPDATE_DEBOUNCE_MS = 300;
|
||||
|
||||
// Cache des renderers (summary)
|
||||
const renderersCache = ref<Map<string, RendererSummary>>(new Map());
|
||||
const RENDERERS_CACHE_MS = 2000;
|
||||
@@ -205,15 +201,10 @@ function ensureSSEInitialized() {
|
||||
snapshot.state.queue_len = event.queue_length;
|
||||
queueRefreshingIds.delete(rendererId);
|
||||
|
||||
// Annuler le timer précédent pour ce renderer
|
||||
const existingTimer = queueUpdateDebounceTimers.get(rendererId);
|
||||
if (existingTimer) clearTimeout(existingTimer);
|
||||
|
||||
// Programmer un seul fetch après stabilisation
|
||||
queueUpdateDebounceTimers.set(rendererId, setTimeout(() => {
|
||||
queueUpdateDebounceTimers.delete(rendererId);
|
||||
void fetchRendererSnapshot(rendererId, { force: true });
|
||||
}, QUEUE_UPDATE_DEBOUNCE_MS));
|
||||
// Fetch queue items immediately when we get queue_updated
|
||||
// The debounce was causing race conditions where queue_len was updated
|
||||
// but items weren't fetched yet when user clicked play
|
||||
void fetchRendererSnapshot(rendererId, { force: true });
|
||||
break;
|
||||
|
||||
case "binding_changed":
|
||||
@@ -342,7 +333,7 @@ async function fetchRenderers(force = false, retries = 2) {
|
||||
error.value = lastError?.message ?? "Erreur fetch renderers";
|
||||
|
||||
// Notifier l'utilisateur en cas d'erreur finale
|
||||
uiStore.notifyError("Impossible de rafraîchir la liste des renderers");
|
||||
uiStore.notifyError("Impossible de rafraîchir la liste des lecteurs audio");
|
||||
}
|
||||
|
||||
async function fetchRendererSnapshot(
|
||||
@@ -383,7 +374,8 @@ async function fetchRendererSnapshot(
|
||||
snapshots.delete(rendererId);
|
||||
|
||||
// Notifier l'utilisateur
|
||||
uiStore.notifyError(`Impossible de récupérer l'état du renderer`);
|
||||
const name = renderersCache.value.get(rendererId)?.friendly_name ?? rendererId;
|
||||
uiStore.notifyError(`Impossible de récupérer l'état de « ${name} »`);
|
||||
} finally {
|
||||
// Toujours nettoyer le flag de chargement
|
||||
loadingIds.delete(rendererId);
|
||||
@@ -447,6 +439,7 @@ async function resumeOrPlayFromQueue(id: string) {
|
||||
return play(id);
|
||||
}
|
||||
|
||||
// Check if queue has content
|
||||
if (
|
||||
["STOPPED", "NO_MEDIA"].includes(state.transport_state) &&
|
||||
snapshot.queue.items.length > 0
|
||||
|
||||
@@ -155,6 +155,7 @@ impl DB {
|
||||
let conn = Connection::open(path)?;
|
||||
conn.execute("PRAGMA foreign_keys = ON", [])?;
|
||||
|
||||
// 1. CRÉATION DES TABLES EN PREMIER
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS asset (
|
||||
pk TEXT PRIMARY KEY,
|
||||
@@ -180,21 +181,30 @@ impl DB {
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Créer un index sur la collection pour les requêtes rapides
|
||||
// 2. Optimisations SQLite pour production
|
||||
conn.execute_batch(
|
||||
"
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
PRAGMA cache_size = -32768;
|
||||
PRAGMA temp_store = MEMORY;
|
||||
PRAGMA mmap_size = 268435456;
|
||||
",
|
||||
)?;
|
||||
|
||||
// 3. CRÉATION DES INDEX APRÈS LES TABLES
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_asset_collection
|
||||
ON ASSET (collection)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Créer un index composite pour optimiser la politique LRU (get_oldest)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_asset_lru
|
||||
ON asset (last_used ASC, hits ASC)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Crée un index composite pour rendre unique les ids si défini dans une collection
|
||||
conn.execute(
|
||||
"CREATE UNIQUE INDEX
|
||||
IF NOT EXISTS asset_collection_id_unique
|
||||
@@ -203,12 +213,32 @@ impl DB {
|
||||
[],
|
||||
)?;
|
||||
|
||||
// LAZY PK SUPPORT: Index sur lazy_pk pour lookups rapides (lazy_pk → real pk)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_metadata_key_value ON metadata (key, value)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_asset_last_used ON asset (last_used DESC)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_asset_hits ON asset (hits DESC)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_asset_lazy_pk ON asset (lazy_pk)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_asset_lazy_pk_unique
|
||||
ON asset (lazy_pk) WHERE lazy_pk IS NOT NULL",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Index unique sur lazy_pk (non-NULL) pour éviter les doublons
|
||||
// Un lazy_pk ne peut pointer que vers un seul entry
|
||||
conn.execute(
|
||||
@@ -220,7 +250,12 @@ impl DB {
|
||||
// Inscrire la version du schéma
|
||||
conn.execute_batch(&format!("PRAGMA user_version = {}", SCHEMA_VERSION))?;
|
||||
|
||||
Ok((Self { conn: Mutex::new(conn) }, was_reset))
|
||||
Ok((
|
||||
Self {
|
||||
conn: Mutex::new(conn),
|
||||
},
|
||||
was_reset,
|
||||
))
|
||||
}
|
||||
|
||||
/// Ajoute ou met à jour une entrée dans la base de données
|
||||
@@ -1150,7 +1185,9 @@ impl DB {
|
||||
|
||||
tracing::debug!(
|
||||
"update_lazy_to_downloaded: {} → {} ({} metadata rows still under lazy_pk)",
|
||||
lazy_pk, real_pk, meta_under_lazy
|
||||
lazy_pk,
|
||||
real_pk,
|
||||
meta_under_lazy
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -20,9 +20,7 @@ ratatui = { version = "0.26", default-features = false, features = ["crossterm"]
|
||||
crossterm = "0.27"
|
||||
rust_cast = "0.19"
|
||||
rustls = { version = "0.23", features = ["aws-lc-rs"] }
|
||||
mdns = "3.0"
|
||||
async-std = "1.12"
|
||||
futures-util = "0.3"
|
||||
mdns-sd = "0.19"
|
||||
smol = "2.0"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -13,7 +13,7 @@ use tracing::{debug, error, info, warn};
|
||||
use crate::discovery::manager::UDNRegistry;
|
||||
use crate::errors::ControlPointError;
|
||||
use crate::events::{MediaServerEventBus, RendererEventBus};
|
||||
use crate::media_server::{MediaBrowser, MusicServer, playback_item_from_entry};
|
||||
use crate::media_server::{playback_item_from_entry, MediaBrowser, MusicServer};
|
||||
use crate::media_server_events::spawn_media_server_event_runtime;
|
||||
use crate::model::{MediaServerEvent, RendererEvent};
|
||||
use crate::model::{PlaybackState, TrackMetadata};
|
||||
@@ -147,51 +147,52 @@ impl ControlPoint {
|
||||
let udn_cache_for_mdns = Arc::clone(&udn_cache);
|
||||
thread::spawn(move || {
|
||||
use crate::discovery::ChromecastDiscoveryManager;
|
||||
use futures_util::StreamExt;
|
||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||
|
||||
// Créer le gestionnaire de découverte UPNP
|
||||
let mut discovery_manager =
|
||||
ChromecastDiscoveryManager::new(registry_for_mdns, udn_cache_for_mdns);
|
||||
|
||||
debug!("Starting mDNS discovery thread for Chromecast devices");
|
||||
|
||||
const SERVICE_NAME: &str = "_googlecast._tcp.local";
|
||||
const SERVICE_TYPE: &str = "_googlecast._tcp.local.";
|
||||
|
||||
// Run async discovery in a blocking task
|
||||
async_std::task::block_on(async {
|
||||
// Create mDNS discovery stream with 15 second query interval
|
||||
// (shorter interval for faster initial discovery)
|
||||
match mdns::discover::all(SERVICE_NAME, Duration::from_secs(15)) {
|
||||
Ok(discovery) => {
|
||||
let stream = discovery.listen();
|
||||
futures_util::pin_mut!(stream);
|
||||
|
||||
debug!("mDNS discovery stream started for Chromecast devices");
|
||||
|
||||
// Listen to mDNS responses
|
||||
while let Some(result) = stream.next().await {
|
||||
match result {
|
||||
Ok(response) => {
|
||||
debug!(
|
||||
"Received mDNS response with {} records",
|
||||
response.records().count()
|
||||
);
|
||||
|
||||
discovery_manager.handle_mdns_response(response);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("mDNS discovery error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
warn!("mDNS discovery stream ended unexpectedly");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to start mDNS discovery: {}", e);
|
||||
}
|
||||
let daemon = match ServiceDaemon::new() {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!("Failed to create mDNS daemon: {}", e);
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let receiver = match daemon.browse(SERVICE_TYPE) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Failed to start mDNS browse for {}: {}", SERVICE_TYPE, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!("mDNS discovery started for Chromecast devices");
|
||||
|
||||
while let Ok(event) = receiver.recv() {
|
||||
match event {
|
||||
ServiceEvent::ServiceResolved(info) => {
|
||||
debug!(
|
||||
fullname = info.get_fullname(),
|
||||
host = info.get_hostname(),
|
||||
port = info.get_port(),
|
||||
"mDNS Chromecast service resolved"
|
||||
);
|
||||
discovery_manager.handle_service_resolved(&info);
|
||||
}
|
||||
ServiceEvent::ServiceRemoved(_service_type, fullname) => {
|
||||
debug!(fullname = %fullname, "mDNS Chromecast service removed");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
warn!("mDNS discovery receiver closed");
|
||||
});
|
||||
|
||||
// Note: Renderer polling is now handled by each MusicRenderer's own watcher thread.
|
||||
@@ -838,11 +839,17 @@ impl ControlPoint {
|
||||
"Playing current playback item from queue"
|
||||
);
|
||||
|
||||
// Temporarily disable auto-advance to prevent race condition
|
||||
// when renderer sends Stopped event during SetAVTransportURI
|
||||
renderer.set_playback_source(PlaybackSource::None);
|
||||
// Note: We set playback_source to FromQueue BEFORE calling play_from_queue().
|
||||
// This prevents a race condition where:
|
||||
// 1. play_from_queue() is called
|
||||
// 2. watcher detects STOPPED from track transition (before playback_source is set)
|
||||
// 3. with source=None, is_playing_from_queue() returns false, breaking auto-advance
|
||||
// By setting it BEFORE, even if STOPPED is detected, source will be FromQueue.
|
||||
// Note: We no longer temporarily set playback_source to None (that caused Bug #2).
|
||||
|
||||
// Start playback using play_from_queue which preserves the queue
|
||||
// Set source to FromQueue BEFORE calling to prevent race condition
|
||||
renderer.set_playback_source(PlaybackSource::FromQueue);
|
||||
if let Err(err) = renderer.play_from_queue() {
|
||||
error!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
@@ -864,7 +871,7 @@ impl ControlPoint {
|
||||
let metadata = playback_item_track_metadata(&item);
|
||||
renderer.set_last_metadata(Some(metadata));
|
||||
|
||||
renderer.set_playback_source(PlaybackSource::FromQueue);
|
||||
// Note: playback_source already set to FromQueue above
|
||||
|
||||
// Prefetch next track if supported (gapless playback)
|
||||
self.prefetch_next_track(&renderer, renderer_id);
|
||||
@@ -894,8 +901,9 @@ impl ControlPoint {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Temporarily disable auto-advance to prevent race condition
|
||||
renderer.set_playback_source(PlaybackSource::None);
|
||||
// Note: Same fix as play_current_from_queue - set source BEFORE calling.
|
||||
// Set source to FromQueue BEFORE calling to prevent race condition
|
||||
renderer.set_playback_source(PlaybackSource::FromQueue);
|
||||
|
||||
// Use the backend's play_next which handles queue advancement correctly for each backend type
|
||||
if let Err(err) = renderer.play_next_from_queue() {
|
||||
@@ -1034,8 +1042,9 @@ impl ControlPoint {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Temporarily disable auto-advance to prevent race condition
|
||||
renderer.set_playback_source(PlaybackSource::None);
|
||||
// Note: Same fix as play_current_from_queue - set source BEFORE calling.
|
||||
// Set source to FromQueue BEFORE calling to prevent race condition
|
||||
renderer.set_playback_source(PlaybackSource::FromQueue);
|
||||
|
||||
// Use the backend's play_from_index which handles everything correctly
|
||||
if let Err(err) = renderer.play_from_index(index) {
|
||||
@@ -1169,24 +1178,22 @@ impl ControlPoint {
|
||||
binding.auto_play_on_refresh = auto_play;
|
||||
renderer.set_playlist_binding(Some(binding));
|
||||
|
||||
let callback: Option<Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>> =
|
||||
if auto_play {
|
||||
let reg = Arc::clone(&self.registry);
|
||||
Some(Box::new(move |rid: &DeviceId| {
|
||||
let renderer = reg.read().unwrap().get_renderer(rid).ok_or_else(||
|
||||
ControlPointError::ControlPoint(format!("Renderer {} not found", rid.0))
|
||||
)?;
|
||||
renderer.play_current_from_queue()
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let _ = schedule_queue_refresh_for(
|
||||
&self.registry,
|
||||
renderer_id,
|
||||
&self.event_bus,
|
||||
callback,
|
||||
);
|
||||
let callback: Option<
|
||||
Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>,
|
||||
> = if auto_play {
|
||||
let reg = Arc::clone(&self.registry);
|
||||
Some(Box::new(move |rid: &DeviceId| {
|
||||
let renderer = reg.read().unwrap().get_renderer(rid).ok_or_else(|| {
|
||||
ControlPointError::ControlPoint(format!("Renderer {} not found", rid.0))
|
||||
})?;
|
||||
// Use retry version to handle transient renderer errors (like JBL Authentics 300)
|
||||
renderer.play_current_from_queue_with_retry()
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let _ =
|
||||
schedule_queue_refresh_for(&self.registry, renderer_id, &self.event_bus, callback);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1240,22 +1247,24 @@ impl ControlPoint {
|
||||
// Note: BindingChanged event is emitted automatically by MusicRenderer::set_playlist_binding()
|
||||
|
||||
// For initial attach with auto_play, force playback start (don't check if idle)
|
||||
let callback: Option<Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>> =
|
||||
if auto_play {
|
||||
let reg = Arc::clone(&self.registry);
|
||||
Some(Box::new(move |rid: &DeviceId| {
|
||||
debug!(
|
||||
renderer = rid.0.as_str(),
|
||||
"Attach callback: forcing playback start (not checking if idle)"
|
||||
);
|
||||
let renderer = reg.read().unwrap().get_renderer(rid).ok_or_else(||
|
||||
ControlPointError::ControlPoint(format!("Renderer {} not found", rid.0))
|
||||
)?;
|
||||
renderer.play_current_from_queue()
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let callback: Option<
|
||||
Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>,
|
||||
> = if auto_play {
|
||||
let reg = Arc::clone(&self.registry);
|
||||
Some(Box::new(move |rid: &DeviceId| {
|
||||
debug!(
|
||||
renderer = rid.0.as_str(),
|
||||
"Attach callback: forcing playback start (not checking if idle)"
|
||||
);
|
||||
let renderer = reg.read().unwrap().get_renderer(rid).ok_or_else(|| {
|
||||
ControlPointError::ControlPoint(format!("Renderer {} not found", rid.0))
|
||||
})?;
|
||||
// Use retry version to handle transient renderer errors (like JBL Authentics 300)
|
||||
renderer.play_current_from_queue_with_retry()
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let _ = schedule_queue_refresh_for(&self.registry, renderer_id, &self.event_bus, callback);
|
||||
Ok(())
|
||||
@@ -1806,7 +1815,9 @@ fn schedule_queue_refresh_for(
|
||||
registry: &Arc<RwLock<DeviceRegistry>>,
|
||||
renderer_id: &DeviceId,
|
||||
event_bus: &RendererEventBus,
|
||||
after_refresh: Option<Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>>,
|
||||
after_refresh: Option<
|
||||
Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>,
|
||||
>,
|
||||
) -> SyncScheduleOutcome {
|
||||
// Step 1: Get renderer from registry
|
||||
let renderer = {
|
||||
@@ -1895,7 +1906,9 @@ fn schedule_queue_refresh_for(
|
||||
};
|
||||
match music_server {
|
||||
Some(s) => fetch_queue_items_for(&s, &container_id_clone),
|
||||
None => Err(ControlPointError::MediaServerError("Server not found".to_string())),
|
||||
None => Err(ControlPointError::MediaServerError(
|
||||
"Server not found".to_string(),
|
||||
)),
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
//! This module handles the discovery of Chromecast devices and registers them
|
||||
//! directly into the `DeviceRegistry`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use crate::DeviceId;
|
||||
use crate::DeviceRegistry;
|
||||
use mdns_sd::ResolvedService;
|
||||
use mdns_sd::ServiceInfo;
|
||||
|
||||
use crate::discovery::manager::UDNRegistry;
|
||||
use crate::model::{RendererCapabilities, RendererInfo, RendererProtocol};
|
||||
use crate::DeviceId;
|
||||
use crate::DeviceRegistry;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// Gestionnaire des événements mDNS pour Chromecast.
|
||||
@@ -32,146 +33,80 @@ impl ChromecastDiscoveryManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Traite une réponse mDNS pour un appareil Chromecast.
|
||||
/// Traite un service Chromecast résolu par mDNS-SD.
|
||||
///
|
||||
/// Cette fonction parse les réponses de service discovery mDNS pour les appareils
|
||||
/// Chromecast et les enregistre directement dans le registre.
|
||||
pub fn handle_mdns_response(&mut self, response: mdns::Response) {
|
||||
// Extract basic information from the mDNS response
|
||||
let service_name = match response.records().find_map(|r| {
|
||||
if let mdns::RecordKind::PTR(ref name) = r.kind {
|
||||
Some(name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
warn!("No PTR record found in mDNS response");
|
||||
return;
|
||||
}
|
||||
};
|
||||
/// `ServiceInfo` arrive pré-assemblé : plus besoin de jointure manuelle
|
||||
/// des enregistrements PTR / A / SRV / TXT.
|
||||
pub fn handle_service_resolved(&mut self, info: &ResolvedService) {
|
||||
let fullname = info.get_fullname().to_string();
|
||||
|
||||
debug!("Processing mDNS response for service: {}", service_name);
|
||||
debug!("Processing resolved Chromecast service: {}", fullname);
|
||||
|
||||
// Extract IP addresses
|
||||
let addresses: Vec<IpAddr> = response
|
||||
.records()
|
||||
.filter_map(|r| match r.kind {
|
||||
mdns::RecordKind::A(addr) => Some(IpAddr::V4(addr)),
|
||||
mdns::RecordKind::AAAA(addr) => Some(IpAddr::V6(addr)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if addresses.is_empty() {
|
||||
warn!(
|
||||
"No IP address found for Chromecast device: {}",
|
||||
service_name
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer IPv4 addresses
|
||||
let host = match addresses
|
||||
let host = match info
|
||||
.get_addresses()
|
||||
.iter()
|
||||
.find(|addr| matches!(addr, IpAddr::V4(_)))
|
||||
.or_else(|| addresses.first())
|
||||
.find(|a| a.is_ipv4())
|
||||
.or_else(|| info.get_addresses().iter().next())
|
||||
{
|
||||
Some(addr) => addr.to_string(),
|
||||
None => {
|
||||
warn!("Could not extract host from addresses");
|
||||
warn!("No IP address for Chromecast service: {}", fullname);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Extract port from SRV record
|
||||
let port = response
|
||||
.records()
|
||||
.find_map(|r| {
|
||||
if let mdns::RecordKind::SRV { port, .. } = r.kind {
|
||||
Some(port)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(8009); // Default Chromecast port
|
||||
let port = info.get_port();
|
||||
|
||||
// Extract TXT records for additional metadata
|
||||
let txt_records: HashMap<String, String> = response
|
||||
.records()
|
||||
.filter_map(|r| {
|
||||
if let mdns::RecordKind::TXT(ref data) = r.kind {
|
||||
Some(data.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flat_map(|data| {
|
||||
// data is Vec<String>, each string is "key=value"
|
||||
data.into_iter().filter_map(|s| {
|
||||
let parts: Vec<&str> = s.splitn(2, '=').collect();
|
||||
if parts.len() == 2 {
|
||||
Some((parts[0].to_string(), parts[1].to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let uuid = info
|
||||
.get_property_val_str("id")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let uuid = if uuid.is_empty() {
|
||||
format!("chromecast-{}-{}", host, port)
|
||||
} else {
|
||||
uuid
|
||||
};
|
||||
|
||||
// Extract metadata from TXT records
|
||||
let model = txt_records.get("md").cloned();
|
||||
let uuid = txt_records
|
||||
.get("id")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("chromecast-{}-{}", host, port));
|
||||
let manufacturer = Some("Google Inc.".to_string());
|
||||
let model = info.get_property_val_str("md").map(|s| s.to_string());
|
||||
|
||||
// Extract friendly name from TXT record "fn" if available
|
||||
// Otherwise, extract from service instance name (PTR record)
|
||||
let friendly_name = txt_records.get("fn").cloned().unwrap_or_else(|| {
|
||||
// Fallback: extract from service name, removing the UUID suffix if present
|
||||
service_name
|
||||
.split("._googlecast._tcp.local")
|
||||
.next()
|
||||
.unwrap_or("Unknown Chromecast")
|
||||
.split('-')
|
||||
.take_while(|part| part.len() != 32) // Skip 32-char hex UUID
|
||||
.collect::<Vec<_>>()
|
||||
.join("-")
|
||||
.trim()
|
||||
.to_string()
|
||||
});
|
||||
let friendly_name = info
|
||||
.get_property_val_str("fn")
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
fullname
|
||||
.split("._googlecast._tcp.local")
|
||||
.next()
|
||||
.unwrap_or("Unknown Chromecast")
|
||||
.split('-')
|
||||
.take_while(|part| part.len() != 32)
|
||||
.collect::<Vec<_>>()
|
||||
.join("-")
|
||||
.trim()
|
||||
.to_string()
|
||||
});
|
||||
|
||||
debug!(
|
||||
"Discovered Chromecast: {} at {}:{} (UUID: {}, Model: {:?})",
|
||||
friendly_name, host, port, uuid, model
|
||||
);
|
||||
|
||||
// Build UDN and check cache
|
||||
let udn = format!("uuid:{}", uuid);
|
||||
let default_max_age = 1800u64;
|
||||
|
||||
// Pour Chromecast, on utilise un max_age par défaut car mDNS n'a pas ce concept
|
||||
let default_max_age = 1800u64; // 30 minutes
|
||||
|
||||
// Check cache to avoid redundant updates
|
||||
if !UDNRegistry::should_fetch(self.udn_cache.clone(), &udn, default_max_age) {
|
||||
debug!("Chromecast {} recently seen, skipping", udn);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create RendererInfo for the registry
|
||||
let renderer_info = build_renderer_info(
|
||||
&uuid,
|
||||
&friendly_name,
|
||||
&host,
|
||||
port,
|
||||
model.as_deref(),
|
||||
manufacturer.as_deref(),
|
||||
Some("Google Inc."),
|
||||
);
|
||||
|
||||
// Register the renderer
|
||||
self.device_registry
|
||||
.write()
|
||||
.expect("DeviceRegistry mutex lock failed")
|
||||
|
||||
@@ -693,18 +693,37 @@ impl MusicRenderer {
|
||||
self.set_playback_source(PlaybackSource::None);
|
||||
self.clear_has_played_flag();
|
||||
} else if self.is_playing_from_queue() {
|
||||
// Only auto-advance if we have actually seen a PLAYING state
|
||||
// since the track was started. This prevents auto-advance on
|
||||
// transient STOPPED states during track initialization.
|
||||
if self.check_and_clear_has_played_flag() {
|
||||
debug!(
|
||||
renderer = self.info.friendly_name(),
|
||||
"Renderer stopped after queue-driven playback; advancing"
|
||||
);
|
||||
let has_played = self.check_and_clear_has_played_flag();
|
||||
|
||||
// ✅ CORRECTION BUG PRODUCTION: Garde fou contre latence DB
|
||||
// Si la base de données est saturée (mutex >400ms) on rate parfois l'événement PLAYING.
|
||||
// On autorise l'auto-avance si:
|
||||
// 1. On a bien vu PLAYING OU
|
||||
// 2. Le titre a été lancé depuis plus de 20 secondes
|
||||
let track_start = self.state.lock().unwrap().track_start_time;
|
||||
let elapsed = track_start
|
||||
.and_then(|t| t.elapsed().ok())
|
||||
.unwrap_or_default();
|
||||
let force_advance = elapsed.as_secs() > 20;
|
||||
|
||||
if has_played || force_advance {
|
||||
if force_advance {
|
||||
debug!(
|
||||
renderer = self.info.friendly_name(),
|
||||
elapsed_sec = elapsed.as_secs(),
|
||||
"⚠️ On force l'auto-avance: on a raté l'événement PLAYING (latence DB)"
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
renderer = self.info.friendly_name(),
|
||||
"Renderer stopped after queue-driven playback; advancing"
|
||||
);
|
||||
}
|
||||
// Use catch_unwind to prevent panics in play_next_from_queue
|
||||
// from poisoning the backend mutex
|
||||
// Also add retry logic for transient errors from renderer
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
self.play_next_from_queue()
|
||||
self.play_next_from_queue_with_retry()
|
||||
}));
|
||||
match result {
|
||||
Ok(Ok(())) => {}
|
||||
@@ -712,7 +731,7 @@ impl MusicRenderer {
|
||||
error!(
|
||||
renderer = self.info.friendly_name(),
|
||||
error = %err,
|
||||
"Auto-advance failed; clearing queue playback state"
|
||||
"Auto-advance failed after retries; clearing queue playback state"
|
||||
);
|
||||
self.set_playback_source(PlaybackSource::None);
|
||||
}
|
||||
@@ -993,13 +1012,69 @@ impl MusicRenderer {
|
||||
self.lock_backend_for("upcoming_len").upcoming_len()
|
||||
}
|
||||
|
||||
/// Play the current item from the queue with retry logic for transient renderer errors.
|
||||
///
|
||||
/// Some renderers (like JBL Authentics 300) may fail the first Play command
|
||||
/// due to timing issues. This method retries with a small delay.
|
||||
pub fn play_current_from_queue_with_retry(&self) -> Result<(), ControlPointError> {
|
||||
const MAX_RETRIES: usize = 3;
|
||||
const RETRY_DELAY_MS: u64 = 200;
|
||||
|
||||
let mut last_error = None;
|
||||
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
// Reset the has_played flag before starting playback to prevent
|
||||
// auto-advance on transient STOPPED states during track initialization.
|
||||
// The flag will be set back to true when PLAYING state is detected.
|
||||
self.clear_has_played_flag();
|
||||
|
||||
// Set playback_source to FromQueue BEFORE calling backend
|
||||
// to prevent race condition where watcher sees STOPPED before
|
||||
// source is set, breaking auto-advance
|
||||
self.set_playback_source(PlaybackSource::FromQueue);
|
||||
|
||||
match self
|
||||
.lock_backend_for("play_current_from_queue_with_retry")
|
||||
.play_from_queue()
|
||||
{
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
last_error = Some(e);
|
||||
if attempt < MAX_RETRIES - 1 {
|
||||
tracing::warn!(
|
||||
renderer = self.info.friendly_name(),
|
||||
attempt = attempt + 1,
|
||||
error = %last_error.as_ref().unwrap(),
|
||||
"Initial play attempt failed, retrying..."
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All retries failed
|
||||
Err(last_error.unwrap_or_else(|| {
|
||||
ControlPointError::ControlPoint("Unknown error in retry logic".into())
|
||||
}))
|
||||
}
|
||||
|
||||
/// Play the current item from the queue.
|
||||
#[deprecated(
|
||||
since = "0.1.0",
|
||||
note = "Use play_current_from_queue_with_retry instead"
|
||||
)]
|
||||
pub fn play_current_from_queue(&self) -> Result<(), ControlPointError> {
|
||||
// Reset the has_played flag before starting playback to prevent
|
||||
// Reset the has_played_flag before starting playback to prevent
|
||||
// auto-advance on transient STOPPED states during track initialization.
|
||||
// The flag will be set back to true when PLAYING state is detected.
|
||||
self.clear_has_played_flag();
|
||||
|
||||
// Set playback_source to FromQueue BEFORE calling backend
|
||||
// to prevent race condition where watcher sees STOPPED before
|
||||
// source is set, breaking auto-advance
|
||||
self.set_playback_source(PlaybackSource::FromQueue);
|
||||
|
||||
self.lock_backend_for("play_current_from_queue")
|
||||
.play_from_queue()
|
||||
}
|
||||
@@ -1011,11 +1086,50 @@ impl MusicRenderer {
|
||||
// The flag will be set back to true when PLAYING state is detected.
|
||||
self.clear_has_played_flag();
|
||||
|
||||
// Set playback_source to FromQueue BEFORE calling backend
|
||||
// to prevent race condition where watcher sees STOPPED before
|
||||
// source is set, breaking auto-advance
|
||||
self.set_playback_source(PlaybackSource::FromQueue);
|
||||
|
||||
self.lock_backend_for("play_next_from_queue").play_next()?;
|
||||
self.emit_queue_updated();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Play next from queue with retry logic for transient renderer errors.
|
||||
///
|
||||
/// Some renderers (like JBL Authentics 300) may fail the first Play command
|
||||
/// due to timing issues. This method retries with a small delay.
|
||||
pub fn play_next_from_queue_with_retry(&self) -> Result<(), ControlPointError> {
|
||||
const MAX_RETRIES: usize = 3;
|
||||
const RETRY_DELAY_MS: u64 = 200;
|
||||
|
||||
let mut last_error = None;
|
||||
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
match self.play_next_from_queue() {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
last_error = Some(e);
|
||||
if attempt < MAX_RETRIES - 1 {
|
||||
tracing::warn!(
|
||||
renderer = self.info.friendly_name(),
|
||||
attempt = attempt + 1,
|
||||
error = %last_error.as_ref().unwrap(),
|
||||
"Auto-advance attempt failed, retrying..."
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All retries failed
|
||||
Err(last_error.unwrap_or_else(|| {
|
||||
ControlPointError::ControlPoint("Unknown error in retry logic".into())
|
||||
}))
|
||||
}
|
||||
|
||||
/// Advance the queue index by one without starting playback.
|
||||
///
|
||||
/// Used by the WebRenderer gapless path: the browser autonomously transitions
|
||||
@@ -1033,14 +1147,21 @@ impl MusicRenderer {
|
||||
|
||||
/// Play from a specific index in the queue.
|
||||
pub fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
|
||||
// Reset the has_played flag before starting playback to prevent
|
||||
// auto-advance on transient STOPPED states during track initialization.
|
||||
// The flag will be set back to true when PLAYING state is detected.
|
||||
// ✅ CORRECTIF BUG SHUFFLE: Quand on change d'index manuellement
|
||||
// (par exemple shuffle, clique sur un titre), on réinitialise OBLIGATOIREMENT
|
||||
// le flag has_played. Sinon quand le titre se termine l'auto-avance
|
||||
// pense qu'il n'a jamais démarré et s'arrête.
|
||||
tracing::debug!(
|
||||
index = index,
|
||||
renderer = self.info.friendly_name(),
|
||||
"🎯 play_from_index appelé, réinitialisation has_played_flag"
|
||||
);
|
||||
self.clear_has_played_flag();
|
||||
|
||||
self.lock_backend_for("play_from_index")
|
||||
.play_from_index(index)?;
|
||||
self.emit_queue_updated();
|
||||
|
||||
self.set_playback_source(PlaybackSource::FromQueue);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1063,6 +1184,11 @@ impl MusicRenderer {
|
||||
let queue_not_empty = backend.len().unwrap_or(0) > 0;
|
||||
|
||||
if queue_not_empty {
|
||||
// Set playback_source to FromQueue BEFORE calling backend
|
||||
// to prevent race condition where watcher sees STOPPED before
|
||||
// source is set, breaking auto-advance
|
||||
self.set_playback_source(PlaybackSource::FromQueue);
|
||||
|
||||
// Si on a des items dans la queue, jouer le track courant (ou le premier si aucun n'est sélectionné)
|
||||
// Cela fonctionne pour tous les backends (UPnP interne, OpenHome, etc.)
|
||||
backend.play_from_queue()
|
||||
@@ -1528,6 +1654,11 @@ impl MusicRenderer {
|
||||
// The flag will be set back to true when PLAYING state is detected.
|
||||
self.clear_has_played_flag();
|
||||
|
||||
// Set playback_source to FromQueue BEFORE calling backend
|
||||
// to prevent race condition where watcher sees STOPPED before
|
||||
// source is set, breaking auto-advance
|
||||
self.set_playback_source(PlaybackSource::FromQueue);
|
||||
|
||||
self.lock_backend_for("play_from_queue").play_from_queue()
|
||||
}
|
||||
|
||||
@@ -1583,11 +1714,18 @@ impl MusicRenderer {
|
||||
)
|
||||
}
|
||||
|
||||
/// Marks playback as external if currently idle (source is None).
|
||||
/// Marks playback as external if currently idle.
|
||||
///
|
||||
/// Only sets to External if we were already playing from an external source.
|
||||
/// Does NOT change None -> External because that would break queue playback
|
||||
/// (the control_point will set it to FromQueue after play_from_queue succeeds).
|
||||
pub fn mark_external_if_idle(&self) {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if matches!(state.playback_source, PlaybackSource::None) {
|
||||
state.playback_source = PlaybackSource::External;
|
||||
if matches!(state.playback_source, PlaybackSource::External) {
|
||||
// Keep External if we were already playing externally
|
||||
} else {
|
||||
// Don't change None -> External - that breaks queue auto-advance!
|
||||
// The control_point will set playback_source to FromQueue after play_from_queue succeeds.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1740,8 +1878,9 @@ impl MusicRenderer {
|
||||
// 5. Replace the queue with shuffled items, starting at index 0
|
||||
self.replace_queue(shuffled_items, Some(0))?;
|
||||
|
||||
// 6. Start playback from the first track
|
||||
self.play_from_index(0)?;
|
||||
// 6. Start playback from the first track with retry for JBL-like renderers
|
||||
// that fail the first Play command due to timing issues
|
||||
self.play_current_from_queue_with_retry()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -688,10 +688,36 @@ impl QueueBackend for OpenHomeRenderer {
|
||||
items: Vec<PlaybackItem>,
|
||||
current_index: Option<usize>,
|
||||
) -> Result<(), ControlPointError> {
|
||||
// ✅ CORRECTION BUG PRODUCTION: On ne charge PAS toutes les métadonnées
|
||||
// dans le thread principal. OpenHome sur 1000 titres inondait la base SQLite
|
||||
// et bloquait TOUS les autres threads (mutex >500ms).
|
||||
//
|
||||
// On fait juste l'insertion minimaliste maintenant. Le préchargement
|
||||
// des métadonnées est délégué à un thread background.
|
||||
self.queue
|
||||
.lock()
|
||||
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
||||
.replace_queue(items, current_index)
|
||||
.map_err(|_| ControlPointError::QueueError("Mutex poisoned".into()))?
|
||||
.replace_queue(items, current_index)?;
|
||||
|
||||
// Background worker: charge les métadonnées petit à petit sans bloquer personne
|
||||
let queue = self.queue.clone();
|
||||
std::thread::spawn(move || {
|
||||
debug!("🔄 OpenHome: préchargement métadonnées queue en background");
|
||||
if let Ok(mut queue) = queue.lock() {
|
||||
// On ne fait que les 10 prochains titres maintenant, le reste on s'en fout
|
||||
if let Ok(Some(idx)) = queue.current_index() {
|
||||
let end = std::cmp::min(idx + 10, queue.len().unwrap_or(0));
|
||||
for i in idx..end {
|
||||
let _ = queue.get_item(i);
|
||||
// Petit délai pour ne pas noyer la base de données
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("✅ OpenHome: préchargement métadonnées terminé");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_queue(
|
||||
|
||||
@@ -218,9 +218,22 @@ impl QueueTransportControl for UpnpRenderer {
|
||||
// Détecte si l'URL est un flux continu en interrogeant le serveur HTTP
|
||||
let is_stream = crate::music_renderer::is_continuous_stream_url(&item.uri);
|
||||
*self.continuous_stream.lock().unwrap() = is_stream;
|
||||
|
||||
// Log current queue state for debugging
|
||||
let queue_state = {
|
||||
let queue = self.queue.lock().unwrap();
|
||||
let idx = queue.current_index().unwrap_or(None);
|
||||
let len = queue.len().unwrap_or(0);
|
||||
let uri = item.uri.clone();
|
||||
let title = item.metadata.as_ref().and_then(|m| m.title.clone());
|
||||
(idx, len, uri, title)
|
||||
};
|
||||
tracing::debug!(
|
||||
"UpnpRenderer play_from_queue: URI={}, continuous_stream={}",
|
||||
item.uri,
|
||||
"UpnpRenderer play_from_queue: index={:?}/{}, uri={}, title={:?}, continuous_stream={}",
|
||||
queue_state.0,
|
||||
queue_state.1,
|
||||
queue_state.2,
|
||||
queue_state.3,
|
||||
is_stream
|
||||
);
|
||||
|
||||
@@ -243,11 +256,28 @@ impl QueueTransportControl for UpnpRenderer {
|
||||
}
|
||||
|
||||
fn play_next(&self) -> Result<(), ControlPointError> {
|
||||
let current_idx = {
|
||||
let queue = self.queue.lock().unwrap();
|
||||
let idx = queue.current_index().unwrap_or(None);
|
||||
let len = queue.len().unwrap_or(0);
|
||||
tracing::debug!(
|
||||
current_index = ?idx,
|
||||
queue_len = len,
|
||||
"play_next: attempting to advance"
|
||||
);
|
||||
idx
|
||||
};
|
||||
{
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
if !queue.advance()? {
|
||||
return Err(ControlPointError::QueueError("No next track".into()));
|
||||
}
|
||||
let new_idx = queue.current_index().unwrap_or(None);
|
||||
tracing::debug!(
|
||||
previous_index = ?current_idx,
|
||||
new_index = ?new_idx,
|
||||
"play_next: advanced"
|
||||
);
|
||||
}
|
||||
|
||||
self.play_from_queue()
|
||||
@@ -269,6 +299,9 @@ impl QueueTransportControl for UpnpRenderer {
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
queue.set_index(Some(index))?;
|
||||
}
|
||||
// CORRECTIF: Quand on change l'index manuellement (shuffle, sélection d'un titre)
|
||||
// on logue pour être sûr que c'est bien appelé
|
||||
tracing::debug!(index = index, "✅ SHUFFLE / SEEK: play_from_index appelé");
|
||||
|
||||
self.play_from_queue()
|
||||
}
|
||||
|
||||
@@ -178,7 +178,9 @@ impl MusicQueue {
|
||||
};
|
||||
// Queue lock is released here.
|
||||
// Now safe to call on_ready (which may re-lock the queue).
|
||||
if on_ready_triggered.load(SeqCst) {
|
||||
// If on_ready was triggered by the proxy, consume and call it.
|
||||
// If not (cancelled before first insert), keep it to pass to retry.
|
||||
let carry_on_ready = if on_ready_triggered.load(SeqCst) {
|
||||
tracing::debug!(
|
||||
thread = %std::thread::current().name().unwrap_or("?"),
|
||||
"queue-sync: on_ready triggered, calling callback"
|
||||
@@ -186,12 +188,16 @@ impl MusicQueue {
|
||||
if let Some(f) = real_on_ready {
|
||||
f();
|
||||
}
|
||||
} else if real_on_ready.is_some() {
|
||||
tracing::debug!(
|
||||
thread = %std::thread::current().name().unwrap_or("?"),
|
||||
"queue-sync: on_ready not triggered (cancelled or skipped)"
|
||||
);
|
||||
}
|
||||
None
|
||||
} else {
|
||||
if real_on_ready.is_some() {
|
||||
tracing::debug!(
|
||||
thread = %std::thread::current().name().unwrap_or("?"),
|
||||
"queue-sync: on_ready not triggered, carrying to next attempt"
|
||||
);
|
||||
}
|
||||
real_on_ready
|
||||
};
|
||||
|
||||
match result {
|
||||
Err(ControlPointError::SyncCancelled) => {
|
||||
@@ -222,7 +228,7 @@ impl MusicQueue {
|
||||
match pending_items_fn() {
|
||||
Ok(new_items) => {
|
||||
current_items = new_items;
|
||||
current_on_ready = Some(None);
|
||||
current_on_ready = Some(carry_on_ready);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("queue-sync pending re-fetch error: {}", e);
|
||||
|
||||
@@ -402,7 +402,7 @@ unsafe fn setup_metadata(
|
||||
use libflac_sys::*;
|
||||
|
||||
// Create a Vorbis Comment block
|
||||
let meta = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT);
|
||||
let meta = unsafe { FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT) };
|
||||
if meta.is_null() {
|
||||
return Err(FlacError::LibFlacInit(
|
||||
"Failed to create metadata block".into(),
|
||||
@@ -420,12 +420,14 @@ unsafe fn setup_metadata(
|
||||
FlacError::LibFlacInit("Failed to create CString for field value".into())
|
||||
})?;
|
||||
|
||||
let mut entry: FLAC__StreamMetadata_VorbisComment_Entry = std::mem::zeroed();
|
||||
let success = FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(
|
||||
&mut entry as *mut _,
|
||||
c_field_name.as_ptr(),
|
||||
c_value.as_ptr(),
|
||||
);
|
||||
let mut entry: FLAC__StreamMetadata_VorbisComment_Entry = unsafe { std::mem::zeroed() };
|
||||
let success = unsafe {
|
||||
FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(
|
||||
&mut entry as *mut _,
|
||||
c_field_name.as_ptr(),
|
||||
c_value.as_ptr(),
|
||||
)
|
||||
};
|
||||
|
||||
if success == 0 {
|
||||
return Err(FlacError::LibFlacInit(format!(
|
||||
@@ -435,7 +437,7 @@ unsafe fn setup_metadata(
|
||||
}
|
||||
|
||||
let append_success =
|
||||
FLAC__metadata_object_vorbiscomment_append_comment(meta, entry, 0 /* copy */);
|
||||
unsafe { FLAC__metadata_object_vorbiscomment_append_comment(meta, entry, 0 /* copy */) };
|
||||
|
||||
if append_success == 0 {
|
||||
return Err(FlacError::LibFlacInit(format!(
|
||||
@@ -476,7 +478,7 @@ unsafe fn setup_metadata(
|
||||
|
||||
// Set the metadata on the encoder
|
||||
let mut metadata_array = [meta];
|
||||
let set_success = FLAC__stream_encoder_set_metadata(encoder, metadata_array.as_mut_ptr(), 1);
|
||||
let set_success = unsafe { FLAC__stream_encoder_set_metadata(encoder, metadata_array.as_mut_ptr(), 1) };
|
||||
|
||||
if set_success == 0 {
|
||||
return Err(FlacError::LibFlacInit(
|
||||
@@ -666,8 +668,8 @@ unsafe extern "C" fn write_callback(
|
||||
_current_frame: u32,
|
||||
client_data: *mut c_void,
|
||||
) -> libflac_sys::FLAC__StreamEncoderWriteStatus {
|
||||
let state = &mut *(client_data as *mut EncoderClientState);
|
||||
let slice = std::slice::from_raw_parts(buffer, bytes);
|
||||
let state = unsafe { &mut *(client_data as *mut EncoderClientState) };
|
||||
let slice = unsafe { std::slice::from_raw_parts(buffer, bytes) };
|
||||
match state.tx.blocking_send(Ok(slice.to_vec())) {
|
||||
Ok(_) => libflac_sys::FLAC__STREAM_ENCODER_WRITE_STATUS_OK,
|
||||
Err(_) => {
|
||||
|
||||
@@ -83,7 +83,7 @@ pub mod paradise_streaming;
|
||||
pub use content_handler::ContentHandler;
|
||||
pub use device::MEDIA_SERVER;
|
||||
pub use device_ext::MediaServerDeviceExt;
|
||||
pub use server_ext::{MediaServerExt, MusicSourceExt, get_source_registry};
|
||||
pub use server_ext::{MediaServerExt, MusicSourceExt};
|
||||
pub use source_registry::SourceRegistry;
|
||||
pub use sources::{SourceInitError, SourcesExt};
|
||||
|
||||
|
||||
@@ -14,23 +14,6 @@ use std::sync::Arc;
|
||||
// Réexporter le trait de base de pmosource
|
||||
pub use pmosource::MusicSourceExt;
|
||||
|
||||
/// Récupère le registre global de sources (délègue à pmosource)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// use pmomediaserver::server_ext::get_source_registry;
|
||||
///
|
||||
/// let sources = pmosource::api::list_all_sources().await;
|
||||
/// ```
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Use pmosource::api::list_all_sources() directly"
|
||||
)]
|
||||
pub async fn get_source_registry() -> Vec<Arc<dyn MusicSource>> {
|
||||
pmosource::api::list_all_sources().await
|
||||
}
|
||||
|
||||
/// Trait d'extension pour le serveur MediaServer UPnP
|
||||
///
|
||||
/// Ce trait ajoute des méthodes spécifiques au MediaServer UPnP.
|
||||
|
||||
@@ -273,13 +273,13 @@ pub fn init_logging() -> LogState {
|
||||
Ok(value) => {
|
||||
let trimmed = value.trim();
|
||||
if let Some(level) = string_to_level(trimmed) {
|
||||
let filter =
|
||||
EnvFilter::try_new(trimmed).unwrap_or_else(|_| EnvFilter::new("trace"));
|
||||
let filter = build_filter_with_noise_suppressions(trimmed);
|
||||
(filter, level, format!("RUST_LOG ({})", trimmed))
|
||||
} else {
|
||||
let filter = build_filter_with_noise_suppressions(trimmed);
|
||||
match EnvFilter::try_new(trimmed) {
|
||||
Ok(filter) => {
|
||||
let level_hint = filter
|
||||
Ok(raw) => {
|
||||
let level_hint = raw
|
||||
.max_level_hint()
|
||||
.and_then(levelfilter_to_level)
|
||||
.unwrap_or(Level::TRACE);
|
||||
@@ -295,7 +295,7 @@ pub fn init_logging() -> LogState {
|
||||
.ok()
|
||||
.and_then(|cfg| string_to_level(cfg.trim()))
|
||||
.unwrap_or(Level::TRACE);
|
||||
let filter = EnvFilter::new(level_to_string(cfg_level));
|
||||
let filter = build_filter_with_noise_suppressions(&level_to_string(cfg_level));
|
||||
(filter, cfg_level, "config".to_string())
|
||||
}
|
||||
}
|
||||
@@ -307,7 +307,7 @@ pub fn init_logging() -> LogState {
|
||||
.ok()
|
||||
.and_then(|cfg| string_to_level(cfg.trim()))
|
||||
.unwrap_or(Level::TRACE);
|
||||
let filter = EnvFilter::new(level_to_string(cfg_level));
|
||||
let filter = build_filter_with_noise_suppressions(&level_to_string(cfg_level));
|
||||
(filter, cfg_level, "config".to_string())
|
||||
}
|
||||
};
|
||||
@@ -446,6 +446,27 @@ pub async fn log_setup_post(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// Construit un `EnvFilter` en ajoutant les suppressions de bruit connues
|
||||
/// si la directive correspondante n'est pas déjà présente dans `base`.
|
||||
fn build_filter_with_noise_suppressions(base: &str) -> EnvFilter {
|
||||
const NOISE_FILTERS: &[(&str, &str)] = &[];
|
||||
|
||||
let mut directives = base.to_string();
|
||||
for (prefix, directive) in NOISE_FILTERS {
|
||||
// N'ajouter que si l'utilisateur n'a pas déjà configuré cette cible
|
||||
let already_set = base.split(',').any(|part| {
|
||||
let part = part.trim();
|
||||
part == *prefix || part.starts_with(&format!("{}=", prefix))
|
||||
});
|
||||
if !already_set {
|
||||
directives.push(',');
|
||||
directives.push_str(directive);
|
||||
}
|
||||
}
|
||||
|
||||
EnvFilter::try_new(&directives).unwrap_or_else(|_| EnvFilter::new(base))
|
||||
}
|
||||
|
||||
fn string_to_level(s: &str) -> Option<Level> {
|
||||
match s.to_uppercase().as_str() {
|
||||
"ERROR" => Some(Level::ERROR),
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.3.41
|
||||
0.3.48
|
||||
|
||||
Reference in New Issue
Block a user