Correction on cache system

This commit is contained in:
2025-10-19 19:29:10 +02:00
parent e270beccd4
commit e0609d86e1
13 changed files with 197 additions and 59 deletions

3
.gitignore vendored
View File

@@ -23,4 +23,5 @@ xxx
xx xx
all.txt all.txt
pmo_src.txt pmo_src.txt
upmpdcli/ upmpdcli/
/*.xml

5
headers.txt Normal file
View File

@@ -0,0 +1,5 @@
HTTP/1.1 500 Internal Server Error
content-type: text/xml; charset="utf-8"
content-length: 597
date: Sun, 19 Oct 2025 19:06:41 GMT

7
pmocovers/src/db.rs Normal file
View File

@@ -0,0 +1,7 @@
//! Module de compatibilité pour l'ancien module db
//!
//! Ce module réexporte les types de `pmocache::db` pour maintenir
//! la compatibilité avec l'API existante.
// Réexporter les types de pmocache
pub use pmocache::db::{CacheEntry, DB};

View File

@@ -12,7 +12,7 @@
use pmodidl::{Container, DIDLLite}; use pmodidl::{Container, DIDLLite};
use pmosource::api::{get_source as get_source_from_registry, list_all_sources}; use pmosource::api::{get_source as get_source_from_registry, list_all_sources};
use pmosource::{BrowseResult, MusicSource}; use pmosource::{BrowseResult, MusicSource, MusicSourceError};
use std::sync::Arc; use std::sync::Arc;
/// Convertit des containers et items en XML DIDL-Lite /// Convertit des containers et items en XML DIDL-Lite
@@ -113,40 +113,52 @@ impl ContentHandler {
} }
// Sinon, chercher dans les sources // Sinon, chercher dans les sources
let mut non_not_found_error: Option<String> = None;
for source in list_all_sources().await { for source in list_all_sources().await {
if let Ok(result) = source.browse(object_id).await { match source.browse(object_id).await {
// L'objet a été trouvé, retourner ses métadonnées Ok(result) => {
match result { // L'objet a été trouvé, retourner ses métadonnées
BrowseResult::Containers(containers) => { match result {
if let Some(container) = containers.first() { BrowseResult::Containers(containers) => {
let didl = to_didl_lite(&[container.clone()], &[])?; if let Some(container) = containers.first() {
let update_id = source.update_id().await; let didl = to_didl_lite(&[container.clone()], &[])?;
return Ok((didl, 1, 1, update_id)); let update_id = source.update_id().await;
} return Ok((didl, 1, 1, update_id));
} }
BrowseResult::Items(items) => { }
if let Some(item) = items.first() { BrowseResult::Items(items) => {
let didl = to_didl_lite(&[], &[item.clone()])?; if let Some(item) = items.first() {
let update_id = source.update_id().await; let didl = to_didl_lite(&[], &[item.clone()])?;
return Ok((didl, 1, 1, update_id)); let update_id = source.update_id().await;
} return Ok((didl, 1, 1, update_id));
} }
BrowseResult::Mixed { containers, items } => { }
if let Some(container) = containers.first() { BrowseResult::Mixed { containers, items } => {
let didl = to_didl_lite(&[container.clone()], &[])?; if let Some(container) = containers.first() {
let update_id = source.update_id().await; let didl = to_didl_lite(&[container.clone()], &[])?;
return Ok((didl, 1, 1, update_id)); let update_id = source.update_id().await;
} else if let Some(item) = items.first() { return Ok((didl, 1, 1, update_id));
let didl = to_didl_lite(&[], &[item.clone()])?; } else if let Some(item) = items.first() {
let update_id = source.update_id().await; let didl = to_didl_lite(&[], &[item.clone()])?;
return Ok((didl, 1, 1, update_id)); let update_id = source.update_id().await;
return Ok((didl, 1, 1, update_id));
}
} }
} }
} }
Err(MusicSourceError::ObjectNotFound(_)) => continue,
Err(e) => {
non_not_found_error = Some(e.to_string());
break;
}
} }
} }
Err(format!("Object not found: {}", object_id)) if let Some(err) = non_not_found_error {
Err(format!("Browse failed: {}", err))
} else {
Err(format!("Object not found: {}", object_id))
}
} }
} }
@@ -170,15 +182,27 @@ impl ContentHandler {
} }
// Sinon, chercher dans les sources // Sinon, chercher dans les sources
let mut non_not_found_error: Option<String> = None;
for source in list_all_sources().await { for source in list_all_sources().await {
if let Ok(result) = source.browse(object_id).await { match source.browse(object_id).await {
return self Ok(result) => {
.browse_result_to_didl(result, source, starting_index, requested_count) return self
.await; .browse_result_to_didl(result, source, starting_index, requested_count)
.await;
}
Err(MusicSourceError::ObjectNotFound(_)) => continue,
Err(e) => {
non_not_found_error = Some(e.to_string());
break;
}
} }
} }
Err(format!("Container not found: {}", object_id)) if let Some(err) = non_not_found_error {
Err(format!("Browse failed: {}", err))
} else {
Err(format!("Container not found: {}", object_id))
}
} }
/// Browse la racine (liste toutes les sources) /// Browse la racine (liste toutes les sources)

View File

@@ -56,10 +56,34 @@ pub fn browse_handler() -> ActionHandler {
let object_id: String = get!(&data, "ObjectID", String); let object_id: String = get!(&data, "ObjectID", String);
let browse_flag: String = get!(&data, "BrowseFlag", String); let browse_flag: String = get!(&data, "BrowseFlag", String);
let starting_index: u32 = get!(&data, "StartingIndex", u32);
let requested_count: u32 = get!(&data, "RequestedCount", u32); let starting_index: u32 = get!(
let _filter: String = get!(&data, "Filter", String); &data,
let _sort_criteria: String = get!(&data, "SortCriteria", String); "StartingIndex",
u32,
"ContentDirectory::Browse misconfigured: 'StartingIndex' missing or not bound"
);
let requested_count: u32 = get!(
&data,
"RequestedCount",
u32,
"ContentDirectory::Browse misconfigured: 'RequestedCount' missing or not bound"
);
let _filter: String = get!(
&data,
"Filter",
String,
"ContentDirectory::Browse misconfigured: 'Filter' missing or not bound"
);
let _sort_criteria: String = get!(
&data,
"SortCriteria",
String,
"ContentDirectory::Browse misconfigured: 'SortCriteria' missing or not bound"
);
info!( info!(
"📂 Browse requested: object_id={} flag={} start={} count={}", "📂 Browse requested: object_id={} flag={} start={} count={}",

View File

@@ -105,6 +105,21 @@ impl RadioParadiseClient {
cloned cloned
} }
/// Clone the client with a different bitrate while preserving other settings.
pub fn clone_with_bitrate(&self, bitrate: Bitrate) -> Self {
let mut cloned = self.clone();
cloned.bitrate = bitrate;
cloned.next_block_url = None;
cloned
}
/// Clone the client with an updated channel and bitrate.
pub fn clone_with_channel_and_bitrate(&self, channel: u8, bitrate: Bitrate) -> Self {
let mut cloned = self.clone_with_channel(channel);
cloned.bitrate = bitrate;
cloned
}
/// Get a block by event ID /// Get a block by event ID
/// ///
/// If `event` is None, returns the current block. /// If `event` is None, returns the current block.

View File

@@ -3,9 +3,9 @@
//! Ce module fournit un trait d'extension pour ajouter facilement l'API Radio Paradise //! Ce module fournit un trait d'extension pour ajouter facilement l'API Radio Paradise
//! à un serveur pmoserver. //! à un serveur pmoserver.
use crate::{Block, NowPlaying, RadioParadiseClient}; use crate::{models::Bitrate, Block, NowPlaying, RadioParadiseClient};
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, Query, State},
http::StatusCode, http::StatusCode,
routing::get, routing::get,
Json, Router, Json, Router,
@@ -21,6 +21,15 @@ pub struct RadioParadiseState {
client: Arc<RwLock<RadioParadiseClient>>, client: Arc<RwLock<RadioParadiseClient>>,
} }
const MAX_CHANNEL_ID: u8 = 3;
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct ParadiseQuery {
channel: Option<u8>,
bitrate: Option<u8>,
}
impl RadioParadiseState { impl RadioParadiseState {
pub async fn new() -> anyhow::Result<Self> { pub async fn new() -> anyhow::Result<Self> {
let client = RadioParadiseClient::new() let client = RadioParadiseClient::new()
@@ -30,6 +39,36 @@ impl RadioParadiseState {
client: Arc::new(RwLock::new(client)), client: Arc::new(RwLock::new(client)),
}) })
} }
async fn client_for_params(
&self,
params: &ParadiseQuery,
) -> Result<RadioParadiseClient, StatusCode> {
let base_client = {
let client_guard = self.client.read().await;
client_guard.clone()
};
let mut client = base_client;
if let Some(channel) = params.channel {
if channel > MAX_CHANNEL_ID {
tracing::warn!("Invalid Radio Paradise channel requested: {}", channel);
return Err(StatusCode::BAD_REQUEST);
}
client = client.clone_with_channel(channel);
}
if let Some(bitrate_id) = params.bitrate {
let bitrate = Bitrate::from_u8(bitrate_id).map_err(|e| {
tracing::warn!("Invalid Radio Paradise bitrate requested: {}", e);
StatusCode::BAD_REQUEST
})?;
client = client.clone_with_bitrate(bitrate);
}
Ok(client)
}
} }
/// Information sur un canal Radio Paradise /// Information sur un canal Radio Paradise
@@ -186,8 +225,9 @@ impl From<NowPlaying> for NowPlayingResponse {
)] )]
async fn get_now_playing( async fn get_now_playing(
State(state): State<RadioParadiseState>, State(state): State<RadioParadiseState>,
Query(params): Query<ParadiseQuery>,
) -> Result<Json<NowPlayingResponse>, StatusCode> { ) -> Result<Json<NowPlayingResponse>, StatusCode> {
let client = state.client.read().await; let client = state.client_for_params(&params).await?;
let now_playing = client.now_playing().await.map_err(|e| { let now_playing = client.now_playing().await.map_err(|e| {
tracing::error!("Failed to fetch now playing from Radio Paradise: {}", e); tracing::error!("Failed to fetch now playing from Radio Paradise: {}", e);
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
@@ -208,8 +248,9 @@ async fn get_now_playing(
)] )]
async fn get_current_block( async fn get_current_block(
State(state): State<RadioParadiseState>, State(state): State<RadioParadiseState>,
Query(params): Query<ParadiseQuery>,
) -> Result<Json<BlockResponse>, StatusCode> { ) -> Result<Json<BlockResponse>, StatusCode> {
let client = state.client.read().await; let client = state.client_for_params(&params).await?;
let block = client.get_block(None).await.map_err(|e| { let block = client.get_block(None).await.map_err(|e| {
tracing::error!("Failed to fetch current block from Radio Paradise: {}", e); tracing::error!("Failed to fetch current block from Radio Paradise: {}", e);
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
@@ -234,8 +275,9 @@ async fn get_current_block(
async fn get_block_by_id( async fn get_block_by_id(
State(state): State<RadioParadiseState>, State(state): State<RadioParadiseState>,
Path(event_id): Path<u64>, Path(event_id): Path<u64>,
Query(params): Query<ParadiseQuery>,
) -> Result<Json<BlockResponse>, StatusCode> { ) -> Result<Json<BlockResponse>, StatusCode> {
let client = state.client.read().await; let client = state.client_for_params(&params).await?;
let block = client.get_block(Some(event_id)).await.map_err(|e| { let block = client.get_block(Some(event_id)).await.map_err(|e| {
tracing::error!( tracing::error!(
"Failed to fetch block {} from Radio Paradise: {}", "Failed to fetch block {} from Radio Paradise: {}",

View File

@@ -1,5 +1,6 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
env::var,
sync::Arc, sync::Arc,
}; };
@@ -153,8 +154,13 @@ impl ActionInstance {
for (arg_name, state_value) in soap_data.iter() { for (arg_name, state_value) in soap_data.iter() {
if let Some(arg_inst) = self.argument(arg_name) { if let Some(arg_inst) = self.argument(arg_name) {
if arg_inst.get_model().is_in() { if arg_inst.is_in() {
action_data.insert(arg_name.clone(), state_value.to_reflect()); if let Some(var_inst) = arg_inst.get_variable_instance() {
action_data
.insert(arg_name.clone(), var_inst.parse_value(state_value.clone()));
} else {
action_data.insert(arg_name.clone(), state_value.to_reflect());
}
updated.insert(arg_name.clone()); updated.insert(arg_name.clone());
} }
} }

View File

@@ -56,16 +56,16 @@ impl Action {
/// Il peut être remplacé via [`set_handler`](Self::set_handler). /// Il peut être remplacé via [`set_handler`](Self::set_handler).
fn default_handler() -> ActionHandler { fn default_handler() -> ActionHandler {
action_handler!(|data| { action_handler!(|data| {
info!("🎬 Action called with default handler"); let mut s = String::new();
// Logger les arguments // Logger les arguments
for (key, value) in data.iter() { for (key, value) in data.iter() {
trace!( s.push_str(&format![
" {} = {}", "- {} = {}\n",
key, key,
crate::actions::reflect_to_string(value.as_ref()) crate::actions::reflect_to_string(value.as_ref())
); ]);
} }
info!("🎬 Action called with default handler\n\n{}", s);
// Retourner les données telles quelles // Retourner les données telles quelles
Ok(data) Ok(data)

View File

@@ -247,6 +247,10 @@ impl ArgumentInstance {
pub fn get_variable_instance(&self) -> Option<Arc<StateVarInstance>> { pub fn get_variable_instance(&self) -> Option<Arc<StateVarInstance>> {
self.variable_instance.read().unwrap().clone() self.variable_instance.read().unwrap().clone()
} }
pub fn is_in(&self) -> bool {
self.model.is_in()
}
} }
impl UpnpInstance for ActionInstanceSet { impl UpnpInstance for ActionInstanceSet {

View File

@@ -195,6 +195,16 @@ macro_rules! get {
($data:expr, $key:expr, $type:ty) => { ($data:expr, $key:expr, $type:ty) => {
$crate::actions::get_value::<$type>($data, $key)? $crate::actions::get_value::<$type>($data, $key)?
}; };
($data:expr, $key:expr, $type:ty, $($msg:tt)+) => {{
match $crate::actions::get_value::<$type>($data, $key) {
Ok(value) => value,
Err(_) => {
let message = format!($($msg)+);
tracing::error!("{}", message);
return Err($crate::actions::ActionError::ArgumentError(message));
}
}
}};
} }
/// Macro pour insérer facilement une valeur dans ActionData. /// Macro pour insérer facilement une valeur dans ActionData.

View File

@@ -312,10 +312,9 @@ impl SsdpServer {
); );
match socket.send_to(resp.as_bytes(), src) { match socket.send_to(resp.as_bytes(), src) {
Ok(_) => { Ok(_) => {
info!("📡 M-SEARCH response sent to {} with ST={}", src, nt);
debug!( debug!(
"📡 M-SEARCH response payload\n<details>\n\n```\n{}\n```\n</details>\n", "📡 M-SEARCH response sent to {} with ST={}\n\n### payload\n\n<details>\n\n```\n{}\n```\n</details>\n",
resp src, nt, resp
); );
} }
Err(e) => warn!("❌ Failed to send M-SEARCH response to {}: {}", src, e), Err(e) => warn!("❌ Failed to send M-SEARCH response to {}: {}", src, e),

View File

@@ -243,13 +243,16 @@ impl StateVarInstance {
/// ///
/// Un `Box<dyn Reflect>` contenant la valeur actuelle /// Un `Box<dyn Reflect>` contenant la valeur actuelle
pub fn to_reflect(&self) -> Box<dyn Reflect> { pub fn to_reflect(&self) -> Box<dyn Reflect> {
use crate::variable_types::StateVarType;
let current_value = self.value.read().unwrap().clone(); let current_value = self.value.read().unwrap().clone();
self.parse_value(current_value)
}
pub fn parse_value(&self, value: StateValue) -> Box<dyn Reflect> {
use crate::variable_types::StateVarType;
// Parser uniquement pour les String // Parser uniquement pour les String
if self.as_state_var_type() == StateVarType::String { if self.as_state_var_type() == StateVarType::String {
if let StateValue::String(ref s) = current_value { if let StateValue::String(ref s) = value {
if let Some(ref parser) = self.model.parse { if let Some(ref parser) = self.model.parse {
match parser(s) { match parser(s) {
Ok(reflected) => return reflected, Ok(reflected) => return reflected,
@@ -266,10 +269,8 @@ impl StateVarInstance {
} }
} }
// Conversion standard pour tous les autres types value.to_reflect()
current_value.to_reflect()
} }
/// Définit la valeur depuis Box<dyn Reflect> /// Définit la valeur depuis Box<dyn Reflect>
/// ///
/// - Si type String ET marshal défini : utilise le marshal /// - Si type String ET marshal défini : utilise le marshal