Correction on cache system
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,4 +23,5 @@ xxx
|
||||
xx
|
||||
all.txt
|
||||
pmo_src.txt
|
||||
upmpdcli/
|
||||
upmpdcli/
|
||||
/*.xml
|
||||
|
||||
5
headers.txt
Normal file
5
headers.txt
Normal 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
7
pmocovers/src/db.rs
Normal 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};
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
use pmodidl::{Container, DIDLLite};
|
||||
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;
|
||||
|
||||
/// Convertit des containers et items en XML DIDL-Lite
|
||||
@@ -113,40 +113,52 @@ impl ContentHandler {
|
||||
}
|
||||
|
||||
// Sinon, chercher dans les sources
|
||||
let mut non_not_found_error: Option<String> = None;
|
||||
for source in list_all_sources().await {
|
||||
if let Ok(result) = source.browse(object_id).await {
|
||||
// L'objet a été trouvé, retourner ses métadonnées
|
||||
match result {
|
||||
BrowseResult::Containers(containers) => {
|
||||
if let Some(container) = containers.first() {
|
||||
let didl = to_didl_lite(&[container.clone()], &[])?;
|
||||
let update_id = source.update_id().await;
|
||||
return Ok((didl, 1, 1, update_id));
|
||||
}
|
||||
}
|
||||
BrowseResult::Items(items) => {
|
||||
if let Some(item) = items.first() {
|
||||
let didl = to_didl_lite(&[], &[item.clone()])?;
|
||||
let update_id = source.update_id().await;
|
||||
return Ok((didl, 1, 1, update_id));
|
||||
}
|
||||
}
|
||||
BrowseResult::Mixed { containers, items } => {
|
||||
if let Some(container) = containers.first() {
|
||||
let didl = to_didl_lite(&[container.clone()], &[])?;
|
||||
let update_id = source.update_id().await;
|
||||
return Ok((didl, 1, 1, update_id));
|
||||
} else if let Some(item) = items.first() {
|
||||
let didl = to_didl_lite(&[], &[item.clone()])?;
|
||||
let update_id = source.update_id().await;
|
||||
return Ok((didl, 1, 1, update_id));
|
||||
match source.browse(object_id).await {
|
||||
Ok(result) => {
|
||||
// L'objet a été trouvé, retourner ses métadonnées
|
||||
match result {
|
||||
BrowseResult::Containers(containers) => {
|
||||
if let Some(container) = containers.first() {
|
||||
let didl = to_didl_lite(&[container.clone()], &[])?;
|
||||
let update_id = source.update_id().await;
|
||||
return Ok((didl, 1, 1, update_id));
|
||||
}
|
||||
}
|
||||
BrowseResult::Items(items) => {
|
||||
if let Some(item) = items.first() {
|
||||
let didl = to_didl_lite(&[], &[item.clone()])?;
|
||||
let update_id = source.update_id().await;
|
||||
return Ok((didl, 1, 1, update_id));
|
||||
}
|
||||
}
|
||||
BrowseResult::Mixed { containers, items } => {
|
||||
if let Some(container) = containers.first() {
|
||||
let didl = to_didl_lite(&[container.clone()], &[])?;
|
||||
let update_id = source.update_id().await;
|
||||
return Ok((didl, 1, 1, update_id));
|
||||
} else if let Some(item) = items.first() {
|
||||
let didl = to_didl_lite(&[], &[item.clone()])?;
|
||||
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
|
||||
let mut non_not_found_error: Option<String> = None;
|
||||
for source in list_all_sources().await {
|
||||
if let Ok(result) = source.browse(object_id).await {
|
||||
return self
|
||||
.browse_result_to_didl(result, source, starting_index, requested_count)
|
||||
.await;
|
||||
match source.browse(object_id).await {
|
||||
Ok(result) => {
|
||||
return self
|
||||
.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)
|
||||
|
||||
@@ -56,10 +56,34 @@ pub fn browse_handler() -> ActionHandler {
|
||||
|
||||
let object_id: String = get!(&data, "ObjectID", 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 _filter: String = get!(&data, "Filter", String);
|
||||
let _sort_criteria: String = get!(&data, "SortCriteria", String);
|
||||
|
||||
let starting_index: u32 = get!(
|
||||
&data,
|
||||
"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!(
|
||||
"📂 Browse requested: object_id={} flag={} start={} count={}",
|
||||
|
||||
@@ -105,6 +105,21 @@ impl RadioParadiseClient {
|
||||
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
|
||||
///
|
||||
/// If `event` is None, returns the current block.
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
//! Ce module fournit un trait d'extension pour ajouter facilement l'API Radio Paradise
|
||||
//! à un serveur pmoserver.
|
||||
|
||||
use crate::{Block, NowPlaying, RadioParadiseClient};
|
||||
use crate::{models::Bitrate, Block, NowPlaying, RadioParadiseClient};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
routing::get,
|
||||
Json, Router,
|
||||
@@ -21,6 +21,15 @@ pub struct RadioParadiseState {
|
||||
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 {
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
let client = RadioParadiseClient::new()
|
||||
@@ -30,6 +39,36 @@ impl RadioParadiseState {
|
||||
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
|
||||
@@ -186,8 +225,9 @@ impl From<NowPlaying> for NowPlayingResponse {
|
||||
)]
|
||||
async fn get_now_playing(
|
||||
State(state): State<RadioParadiseState>,
|
||||
Query(params): Query<ParadiseQuery>,
|
||||
) -> Result<Json<NowPlayingResponse>, StatusCode> {
|
||||
let client = state.client.read().await;
|
||||
let client = state.client_for_params(¶ms).await?;
|
||||
let now_playing = client.now_playing().await.map_err(|e| {
|
||||
tracing::error!("Failed to fetch now playing from Radio Paradise: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
@@ -208,8 +248,9 @@ async fn get_now_playing(
|
||||
)]
|
||||
async fn get_current_block(
|
||||
State(state): State<RadioParadiseState>,
|
||||
Query(params): Query<ParadiseQuery>,
|
||||
) -> Result<Json<BlockResponse>, StatusCode> {
|
||||
let client = state.client.read().await;
|
||||
let client = state.client_for_params(¶ms).await?;
|
||||
let block = client.get_block(None).await.map_err(|e| {
|
||||
tracing::error!("Failed to fetch current block from Radio Paradise: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
@@ -234,8 +275,9 @@ async fn get_current_block(
|
||||
async fn get_block_by_id(
|
||||
State(state): State<RadioParadiseState>,
|
||||
Path(event_id): Path<u64>,
|
||||
Query(params): Query<ParadiseQuery>,
|
||||
) -> Result<Json<BlockResponse>, StatusCode> {
|
||||
let client = state.client.read().await;
|
||||
let client = state.client_for_params(¶ms).await?;
|
||||
let block = client.get_block(Some(event_id)).await.map_err(|e| {
|
||||
tracing::error!(
|
||||
"Failed to fetch block {} from Radio Paradise: {}",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
env::var,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -153,8 +154,13 @@ impl ActionInstance {
|
||||
|
||||
for (arg_name, state_value) in soap_data.iter() {
|
||||
if let Some(arg_inst) = self.argument(arg_name) {
|
||||
if arg_inst.get_model().is_in() {
|
||||
action_data.insert(arg_name.clone(), state_value.to_reflect());
|
||||
if arg_inst.is_in() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,16 +56,16 @@ impl Action {
|
||||
/// Il peut être remplacé via [`set_handler`](Self::set_handler).
|
||||
fn default_handler() -> ActionHandler {
|
||||
action_handler!(|data| {
|
||||
info!("🎬 Action called with default handler");
|
||||
|
||||
let mut s = String::new();
|
||||
// Logger les arguments
|
||||
for (key, value) in data.iter() {
|
||||
trace!(
|
||||
" {} = {}",
|
||||
s.push_str(&format![
|
||||
"- {} = {}\n",
|
||||
key,
|
||||
crate::actions::reflect_to_string(value.as_ref())
|
||||
);
|
||||
]);
|
||||
}
|
||||
info!("🎬 Action called with default handler\n\n{}", s);
|
||||
|
||||
// Retourner les données telles quelles
|
||||
Ok(data)
|
||||
|
||||
@@ -247,6 +247,10 @@ impl ArgumentInstance {
|
||||
pub fn get_variable_instance(&self) -> Option<Arc<StateVarInstance>> {
|
||||
self.variable_instance.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn is_in(&self) -> bool {
|
||||
self.model.is_in()
|
||||
}
|
||||
}
|
||||
|
||||
impl UpnpInstance for ActionInstanceSet {
|
||||
|
||||
@@ -195,6 +195,16 @@ macro_rules! get {
|
||||
($data:expr, $key:expr, $type:ty) => {
|
||||
$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.
|
||||
|
||||
@@ -312,10 +312,9 @@ impl SsdpServer {
|
||||
);
|
||||
match socket.send_to(resp.as_bytes(), src) {
|
||||
Ok(_) => {
|
||||
info!("📡 M-SEARCH response sent to {} with ST={}", src, nt);
|
||||
debug!(
|
||||
"📡 M-SEARCH response payload\n<details>\n\n```\n{}\n```\n</details>\n",
|
||||
resp
|
||||
"📡 M-SEARCH response sent to {} with ST={}\n\n### payload\n\n<details>\n\n```\n{}\n```\n</details>\n",
|
||||
src, nt, resp
|
||||
);
|
||||
}
|
||||
Err(e) => warn!("❌ Failed to send M-SEARCH response to {}: {}", src, e),
|
||||
|
||||
@@ -243,13 +243,16 @@ impl StateVarInstance {
|
||||
///
|
||||
/// Un `Box<dyn Reflect>` contenant la valeur actuelle
|
||||
pub fn to_reflect(&self) -> Box<dyn Reflect> {
|
||||
use crate::variable_types::StateVarType;
|
||||
|
||||
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
|
||||
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 {
|
||||
match parser(s) {
|
||||
Ok(reflected) => return reflected,
|
||||
@@ -266,10 +269,8 @@ impl StateVarInstance {
|
||||
}
|
||||
}
|
||||
|
||||
// Conversion standard pour tous les autres types
|
||||
current_value.to_reflect()
|
||||
value.to_reflect()
|
||||
}
|
||||
|
||||
/// Définit la valeur depuis Box<dyn Reflect>
|
||||
///
|
||||
/// - Si type String ET marshal défini : utilise le marshal
|
||||
|
||||
Reference in New Issue
Block a user