Correction on cache system
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,3 +24,4 @@ xx
|
|||||||
all.txt
|
all.txt
|
||||||
pmo_src.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 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)
|
||||||
|
|||||||
@@ -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={}",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(¶ms).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(¶ms).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(¶ms).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: {}",
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user