amélioration de la webapp

This commit is contained in:
2025-10-19 13:42:29 +02:00
parent 90a7b8b6fa
commit 776c535862
179 changed files with 2426 additions and 1685 deletions

Binary file not shown.

View File

@@ -1,8 +1,8 @@
use pmoapp::{WebAppExt, Webapp}; use pmoapp::{WebAppExt, Webapp};
use pmomediarenderer::MEDIA_RENDERER; use pmomediarenderer::MEDIA_RENDERER;
use pmomediaserver::{MEDIA_SERVER, sources::SourcesExt}; use pmomediaserver::{MEDIA_SERVER, sources::SourcesExt};
use pmosource::MusicSourceExt;
use pmoserver::Server; use pmoserver::Server;
use pmosource::MusicSourceExt;
use pmoupnp::{UpnpServerExt, upnp_api::UpnpApiExt}; use pmoupnp::{UpnpServerExt, upnp_api::UpnpApiExt};
use tracing::info; use tracing::info;

View File

@@ -297,7 +297,7 @@ pub trait WebAppExt {
/// # Type Parameter /// # Type Parameter
/// ///
/// * `W` - Type RustEmbed contenant les fichiers de la webapp /// * `W` - Type RustEmbed contenant les fichiers de la webapp
async fn add_webapp<W>(&mut self, path: &str) async fn add_webapp<W>(&mut self, path: &str)
where where
W: RustEmbed + Clone + Send + Sync + 'static; W: RustEmbed + Clone + Send + Sync + 'static;
@@ -310,7 +310,7 @@ pub trait WebAppExt {
/// # Type Parameter /// # Type Parameter
/// ///
/// * `W` - Type RustEmbed contenant les fichiers de la webapp /// * `W` - Type RustEmbed contenant les fichiers de la webapp
async fn add_webapp_with_redirect<W>(&mut self, path: &str) async fn add_webapp_with_redirect<W>(&mut self, path: &str)
where where
W: RustEmbed + Clone + Send + Sync + 'static; W: RustEmbed + Clone + Send + Sync + 'static;
} }

View File

@@ -8,6 +8,13 @@
<span v-else>🔄</span> <span v-else>🔄</span>
Refresh Refresh
</button> </button>
<button
@click="togglePlayback"
:disabled="!nowPlaying || !nowPlaying.stream_url"
class="btn-play"
>
{{ isPlaying ? '⏹️ Stop' : '▶️ Play' }}
</button>
<select v-model="selectedChannel" @change="changeChannel" class="channel-select"> <select v-model="selectedChannel" @change="changeChannel" class="channel-select">
<option v-for="channel in channels" :key="channel.id" :value="channel.id"> <option v-for="channel in channels" :key="channel.id" :value="channel.id">
{{ channel.name }} {{ channel.name }}
@@ -16,6 +23,21 @@
</div> </div>
</div> </div>
<!-- Audio Player -->
<div v-if="isPlaying || audioError" class="audio-player-container">
<audio
v-if="!audioError"
ref="audioPlayer"
controls
@ended="handleAudioEnded"
@error="handleAudioError"
></audio>
<p v-if="audioError" class="audio-error">{{ audioError }}</p>
<button @click="stopPlayback" class="btn-stop">
{{ audioError ? '✕ Close' : '⏹️ Stop' }}
</button>
</div>
<div v-if="error" class="error-message"> <div v-if="error" class="error-message">
{{ error }} {{ error }}
</div> </div>
@@ -114,7 +136,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, nextTick } from 'vue'
const API_BASE = '/api/radioparadise' const API_BASE = '/api/radioparadise'
@@ -123,6 +145,9 @@ const error = ref(null)
const nowPlaying = ref(null) const nowPlaying = ref(null)
const channels = ref([]) const channels = ref([])
const selectedChannel = ref(0) const selectedChannel = ref(0)
const audioPlayer = ref(null)
const isPlaying = ref(false)
const audioError = ref('')
// Format duration from milliseconds to MM:SS // Format duration from milliseconds to MM:SS
function formatDuration(ms) { function formatDuration(ms) {
@@ -178,6 +203,61 @@ function changeChannel() {
// TODO: Implement channel switching in the API // TODO: Implement channel switching in the API
} }
function playStream() {
if (!nowPlaying.value?.stream_url) {
audioError.value = 'No stream URL available'
return
}
audioError.value = ''
isPlaying.value = true
nextTick(() => {
const player = audioPlayer.value
if (!player) {
return
}
player.src = nowPlaying.value.stream_url
player.play().catch((e) => {
console.error('Failed to start playback:', e)
audioError.value = `Cannot play stream: ${e.message}`
isPlaying.value = false
})
})
}
function stopPlayback() {
if (audioPlayer.value) {
audioPlayer.value.pause()
audioPlayer.value.currentTime = 0
audioPlayer.value.src = ''
}
isPlaying.value = false
audioError.value = ''
}
function togglePlayback() {
if (isPlaying.value) {
stopPlayback()
} else {
playStream()
}
}
function handleAudioEnded() {
isPlaying.value = false
}
function handleAudioError() {
const audio = audioPlayer.value
if (audio?.error) {
audioError.value = `Audio playback error (code ${audio.error.code})`
} else {
audioError.value = 'Unknown audio playback error'
}
isPlaying.value = false
}
// Initialize on mount // Initialize on mount
onMounted(async () => { onMounted(async () => {
await fetchChannels() await fetchChannels()
@@ -239,6 +319,26 @@ onMounted(async () => {
cursor: not-allowed; cursor: not-allowed;
} }
.btn-play {
background: rgba(46, 204, 113, 0.2);
color: #2ecc71;
border: 1px solid rgba(46, 204, 113, 0.4);
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background 0.3s;
}
.btn-play:hover:not(:disabled) {
background: rgba(46, 204, 113, 0.35);
}
.btn-play:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.channel-select { .channel-select {
padding: 8px 12px; padding: 8px 12px;
border-radius: 4px; border-radius: 4px;
@@ -513,4 +613,31 @@ onMounted(async () => {
color: #999; color: #999;
font-size: 0.9em; font-size: 0.9em;
} }
.audio-player-container {
margin: 12px 0 24px;
padding: 16px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid #333;
display: flex;
gap: 12px;
align-items: center;
}
.audio-error {
margin: 0;
color: #ff6b6b;
flex: 1;
}
.btn-stop {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
background: rgba(231, 76, 60, 0.2);
color: #e74c3c;
font-weight: bold;
}
</style> </style>

View File

@@ -55,13 +55,37 @@
</div> </div>
<!-- Services --> <!-- Services -->
<div v-else class="services-list"> <div v-else class="device-content">
<ServicePanel <div class="device-summary">
v-for="service in device.services" <div class="meta-row">
:key="service.name" <span class="meta-label">UDN:</span>
:service="service" <code class="meta-value">{{ device.udn }}</code>
:device-udn="device.udn" </div>
/> <div class="meta-row" v-if="device.description_url">
<span class="meta-label">Description:</span>
<a
:href="device.description_url"
target="_blank"
rel="noopener"
class="meta-link"
>
View XML
</a>
</div>
<div class="meta-row" v-if="device.base_url">
<span class="meta-label">Base URL:</span>
<code class="meta-value">{{ device.base_url }}</code>
</div>
</div>
<div class="services-list">
<ServicePanel
v-for="service in device.services"
:key="service.name"
:service="service"
:device-udn="device.udn"
/>
</div>
</div> </div>
</div> </div>
</transition> </transition>
@@ -353,6 +377,51 @@ onUnmounted(() => {
color: #95a5a6; color: #95a5a6;
} }
.device-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.device-summary {
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1.5rem;
padding: 0.75rem 1rem;
border-radius: 6px;
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(52, 152, 219, 0.25);
}
.meta-row {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.meta-label {
font-weight: 600;
color: #95a5a6;
}
.meta-value {
background: rgba(44, 62, 80, 0.6);
padding: 0.25rem 0.5rem;
border-radius: 4px;
color: #ecf0f1;
}
.meta-link {
color: #1abc9c;
text-decoration: none;
font-weight: 600;
}
.meta-link:hover {
text-decoration: underline;
}
.services-list { .services-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -30,6 +30,13 @@
<span v-if="action.out_arguments.length > 0" class="badge out-badge" title="Output arguments"> <span v-if="action.out_arguments.length > 0" class="badge out-badge" title="Output arguments">
{{ action.out_arguments.length }} {{ action.out_arguments.length }}
</span> </span>
<span
v-if="action.stateless"
class="badge stateless-badge"
title="Does not mutate state variables"
>
🧊 Stateless
</span>
<span class="expand-indicator"> <span class="expand-indicator">
{{ expandedAction === action.name ? '▼' : '▶' }} {{ expandedAction === action.name ? '▼' : '▶' }}
</span> </span>
@@ -38,6 +45,12 @@
<transition name="expand-args"> <transition name="expand-args">
<div v-if="expandedAction === action.name" class="action-details"> <div v-if="expandedAction === action.name" class="action-details">
<div v-if="action.stateless" class="action-flags">
<span class="stateless-pill">
Stateless action no state variables updated
</span>
</div>
<!-- Input arguments --> <!-- Input arguments -->
<div v-if="action.in_arguments.length > 0" class="arguments-section"> <div v-if="action.in_arguments.length > 0" class="arguments-section">
<h5 class="section-title"> <h5 class="section-title">
@@ -299,6 +312,12 @@ onMounted(() => {
border: 1px solid rgba(46, 204, 113, 0.3); border: 1px solid rgba(46, 204, 113, 0.3);
} }
.stateless-badge {
background: rgba(155, 89, 182, 0.2);
color: #9b59b6;
border: 1px solid rgba(155, 89, 182, 0.3);
}
.expand-indicator { .expand-indicator {
color: #3498db; color: #3498db;
font-size: 0.9rem; font-size: 0.9rem;
@@ -316,6 +335,23 @@ onMounted(() => {
border-top: 1px solid rgba(52, 152, 219, 0.2); border-top: 1px solid rgba(52, 152, 219, 0.2);
} }
.action-flags {
margin-top: 0.75rem;
}
.stateless-pill {
display: inline-block;
padding: 0.3rem 0.6rem;
border-radius: 999px;
background: rgba(155, 89, 182, 0.15);
border: 1px solid rgba(155, 89, 182, 0.25);
color: #d2a6e6;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
}
.arguments-section { .arguments-section {
margin-top: 1rem; margin-top: 1rem;
} }

View File

@@ -49,7 +49,10 @@ async fn main() {
tokio::spawn(async move { tokio::spawn(async move {
let mut source = SourceNode::new(); let mut source = SourceNode::new();
source.add_subscriber(buffer_tx); source.add_subscriber(buffer_tx);
source.generate_chunks(30, 4800, 48000, 440.0).await.unwrap(); source
.generate_chunks(30, 4800, 48000, 440.0)
.await
.unwrap();
}); });
println!("Waiting for all rooms to finish...\n"); println!("Waiting for all rooms to finish...\n");

View File

@@ -7,8 +7,7 @@
//! - Système d'événements pour la communication entre nodes //! - Système d'événements pour la communication entre nodes
use pmoaudio::{ use pmoaudio::{
ChromecastConfig, ChromecastSink, DiskSink, DiskSinkConfig, SourceNode, ChromecastConfig, ChromecastSink, DiskSink, DiskSinkConfig, SourceNode, VolumeNode,
VolumeNode,
}; };
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -146,8 +145,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("\n=== Demo completed successfully! ==="); println!("\n=== Demo completed successfully! ===");
println!("\nSummary:"); println!("\nSummary:");
println!("- Generated {} chunks of {} samples each", num_chunks, chunk_size); println!(
println!("- Total duration: {:.2} seconds", (num_chunks as usize * chunk_size) as f32 / sample_rate as f32); "- Generated {} chunks of {} samples each",
num_chunks, chunk_size
);
println!(
"- Total duration: {:.2} seconds",
(num_chunks as usize * chunk_size) as f32 / sample_rate as f32
);
println!("- Output to Chromecast: Living Room (192.168.1.100)"); println!("- Output to Chromecast: Living Room (192.168.1.100)");
println!( println!(
"- Output to file: {}", "- Output to file: {}",

View File

@@ -73,10 +73,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 7. Générer l'audio (10 chunks de 4800 samples à 48kHz = ~1 seconde) // 7. Générer l'audio (10 chunks de 4800 samples à 48kHz = ~1 seconde)
source source
.generate_chunks( .generate_chunks(
10, // nombre de chunks 10, // nombre de chunks
4800, // samples par chunk (100ms @ 48kHz) 4800, // samples par chunk (100ms @ 48kHz)
48000, // sample rate 48000, // sample rate
440.0, // fréquence (La 440 Hz) 440.0, // fréquence (La 440 Hz)
) )
.await?; .await?;

View File

@@ -90,7 +90,13 @@ impl AudioChunk {
} }
/// Crée un nouveau chunk audio avec un gain spécifique /// Crée un nouveau chunk audio avec un gain spécifique
pub fn with_gain(order: u64, left: Vec<f32>, right: Vec<f32>, sample_rate: u32, gain: f32) -> Self { pub fn with_gain(
order: u64,
left: Vec<f32>,
right: Vec<f32>,
sample_rate: u32,
gain: f32,
) -> Self {
Self { Self {
order, order,
left: Arc::new(left), left: Arc::new(left),

View File

@@ -76,13 +76,13 @@
//! - **RwLock** : Pour partage concurrent du compteur [`TimerNode`] //! - **RwLock** : Pour partage concurrent du compteur [`TimerNode`]
mod audio_chunk; mod audio_chunk;
mod nodes;
pub mod events; pub mod events;
mod nodes;
pub use audio_chunk::AudioChunk; pub use audio_chunk::AudioChunk;
pub use events::{ pub use events::{
AudioDataEvent, EventPublisher, EventReceiver, NodeEvent, NodeListener, AudioDataEvent, EventPublisher, EventReceiver, NodeEvent, NodeListener, SourceNameUpdateEvent,
SourceNameUpdateEvent, VolumeChangeEvent, VolumeChangeEvent,
}; };
pub use nodes::{ pub use nodes::{
buffer_node::BufferNode, buffer_node::BufferNode,

View File

@@ -1,4 +1,7 @@
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}}; use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{mpsc, RwLock}; use tokio::sync::{mpsc, RwLock};

View File

@@ -189,10 +189,7 @@ impl ChromecastSink {
// Établir la connexion // Établir la connexion
self.connect().await?; self.connect().await?;
let mut stats = ChromecastStats::new( let mut stats = ChromecastStats::new(self.node_id.clone(), self.config.device_name.clone());
self.node_id.clone(),
self.config.device_name.clone(),
);
// Boucle principale // Boucle principale
while let Some(chunk) = self.rx.recv().await { while let Some(chunk) = self.rx.recv().await {

View File

@@ -1,4 +1,7 @@
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}}; use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -56,10 +59,10 @@ impl DecoderNode {
if src_idx < left_data.len() - 1 { if src_idx < left_data.len() - 1 {
let frac = src_pos - src_idx as f64; let frac = src_pos - src_idx as f64;
let left_sample = let left_sample = left_data[src_idx] * (1.0 - frac as f32)
left_data[src_idx] * (1.0 - frac as f32) + left_data[src_idx + 1] * frac as f32; + left_data[src_idx + 1] * frac as f32;
let right_sample = let right_sample = right_data[src_idx] * (1.0 - frac as f32)
right_data[src_idx] * (1.0 - frac as f32) + right_data[src_idx + 1] * frac as f32; + right_data[src_idx + 1] * frac as f32;
new_left.push(left_sample); new_left.push(left_sample);
new_right.push(right_sample); new_right.push(right_sample);
@@ -69,7 +72,8 @@ impl DecoderNode {
} }
} }
let new_chunk = AudioChunk::new(chunk.order, new_left, new_right, target_sample_rate); let new_chunk =
AudioChunk::new(chunk.order, new_left, new_right, target_sample_rate);
self.subscribers.push(Arc::new(new_chunk)).await?; self.subscribers.push(Arc::new(new_chunk)).await?;
} }
} }

View File

@@ -3,11 +3,7 @@
//! Ce module fournit un sink qui écrit les chunks audio sur disque, //! Ce module fournit un sink qui écrit les chunks audio sur disque,
//! avec support de la dérivation automatique du nom de fichier depuis la source. //! avec support de la dérivation automatique du nom de fichier depuis la source.
use crate::{ use crate::{events::SourceNameUpdateEvent, nodes::AudioError, AudioChunk};
events::SourceNameUpdateEvent,
nodes::AudioError,
AudioChunk,
};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use tokio::fs::File; use tokio::fs::File;
@@ -161,7 +157,13 @@ impl DiskSink {
// Nettoyer le nom de la source pour en faire un nom de fichier valide // Nettoyer le nom de la source pour en faire un nom de fichier valide
let clean_name = name let clean_name = name
.chars() .chars()
.map(|c| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '_' }) .map(|c| {
if c.is_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect::<String>(); .collect::<String>();
format!("{}.{}", clean_name, self.config.format.extension()) format!("{}.{}", clean_name, self.config.format.extension())
@@ -180,9 +182,9 @@ impl DiskSink {
// Créer le répertoire parent si nécessaire // Créer le répertoire parent si nécessaire
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent) tokio::fs::create_dir_all(parent).await.map_err(|e| {
.await AudioError::ProcessingError(format!("Failed to create directory: {}", e))
.map_err(|e| AudioError::ProcessingError(format!("Failed to create directory: {}", e)))?; })?;
} }
// Créer le writer approprié selon le format // Créer le writer approprié selon le format
@@ -328,10 +330,9 @@ impl AudioFileWriter {
bytes.extend_from_slice(&sample_i16.to_le_bytes()); bytes.extend_from_slice(&sample_i16.to_le_bytes());
} }
self.file self.file.write_all(&bytes).await.map_err(|e| {
.write_all(&bytes) AudioError::ProcessingError(format!("Failed to write audio data: {}", e))
.await })?;
.map_err(|e| AudioError::ProcessingError(format!("Failed to write audio data: {}", e)))?;
self.total_samples += chunk.len(); self.total_samples += chunk.len();
Ok(()) Ok(())
@@ -361,10 +362,9 @@ impl AudioFileWriter {
header.extend_from_slice(b"data"); header.extend_from_slice(b"data");
header.extend_from_slice(&0u32.to_le_bytes()); // Taille des données (à mettre à jour) header.extend_from_slice(&0u32.to_le_bytes()); // Taille des données (à mettre à jour)
self.file self.file.write_all(&header).await.map_err(|e| {
.write_all(&header) AudioError::ProcessingError(format!("Failed to write WAV header: {}", e))
.await })?;
.map_err(|e| AudioError::ProcessingError(format!("Failed to write WAV header: {}", e)))?;
Ok(()) Ok(())
} }
@@ -378,24 +378,37 @@ impl AudioFileWriter {
// Positionner au début et réécrire les tailles // Positionner au début et réécrire les tailles
use tokio::io::AsyncSeekExt; use tokio::io::AsyncSeekExt;
self.file.seek(std::io::SeekFrom::Start(4)).await.map_err(|e| { self.file
AudioError::ProcessingError(format!("Failed to seek in file: {}", e)) .seek(std::io::SeekFrom::Start(4))
})?; .await
self.file.write_all(&file_size.to_le_bytes()).await.map_err(|e| { .map_err(|e| {
AudioError::ProcessingError(format!("Failed to update file size: {}", e)) AudioError::ProcessingError(format!("Failed to seek in file: {}", e))
})?; })?;
self.file
.write_all(&file_size.to_le_bytes())
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to update file size: {}", e))
})?;
self.file.seek(std::io::SeekFrom::Start(40)).await.map_err(|e| { self.file
AudioError::ProcessingError(format!("Failed to seek in file: {}", e)) .seek(std::io::SeekFrom::Start(40))
})?; .await
self.file.write_all(&data_size.to_le_bytes()).await.map_err(|e| { .map_err(|e| {
AudioError::ProcessingError(format!("Failed to update data size: {}", e)) AudioError::ProcessingError(format!("Failed to seek in file: {}", e))
})?; })?;
self.file
.write_all(&data_size.to_le_bytes())
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to update data size: {}", e))
})?;
} }
self.file.flush().await.map_err(|e| { self.file
AudioError::ProcessingError(format!("Failed to flush file: {}", e)) .flush()
})?; .await
.map_err(|e| AudioError::ProcessingError(format!("Failed to flush file: {}", e)))?;
Ok(()) Ok(())
} }

View File

@@ -1,4 +1,7 @@
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}}; use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -46,12 +49,8 @@ impl DspNode {
*sample *= self.gain; *sample *= self.gain;
} }
let new_chunk = AudioChunk::new( let new_chunk =
chunk.order, AudioChunk::new(chunk.order, left_data, right_data, chunk.sample_rate);
left_data,
right_data,
chunk.sample_rate,
);
self.subscribers.push(Arc::new(new_chunk)).await?; self.subscribers.push(Arc::new(new_chunk)).await?;
} }
@@ -118,12 +117,7 @@ impl LowPassDspNode {
new_right.push(self.prev_right); new_right.push(self.prev_right);
} }
let new_chunk = AudioChunk::new( let new_chunk = AudioChunk::new(chunk.order, new_left, new_right, chunk.sample_rate);
chunk.order,
new_left,
new_right,
chunk.sample_rate,
);
self.subscribers.push(Arc::new(new_chunk)).await?; self.subscribers.push(Arc::new(new_chunk)).await?;
} }

View File

@@ -59,10 +59,7 @@ impl SingleSubscriberNode {
} }
pub async fn push(&self, chunk: Arc<AudioChunk>) -> Result<(), AudioError> { pub async fn push(&self, chunk: Arc<AudioChunk>) -> Result<(), AudioError> {
self.tx self.tx.send(chunk).await.map_err(|_| AudioError::SendError)
.send(chunk)
.await
.map_err(|_| AudioError::SendError)
} }
} }

View File

@@ -197,7 +197,9 @@ impl MpdSink {
/// Envoie un chunk au serveur MPD (mock) /// Envoie un chunk au serveur MPD (mock)
async fn send_chunk(&self, _chunk: &AudioChunk) -> Result<(), AudioError> { async fn send_chunk(&self, _chunk: &AudioChunk) -> Result<(), AudioError> {
if !self.connected { if !self.connected {
return Err(AudioError::ProcessingError("Not connected to MPD".to_string())); return Err(AudioError::ProcessingError(
"Not connected to MPD".to_string(),
));
} }
// Dans une vraie implémentation: // Dans une vraie implémentation:

View File

@@ -1,4 +1,4 @@
use crate::{AudioChunk, nodes::AudioError}; use crate::{nodes::AudioError, AudioChunk};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -115,11 +115,13 @@ impl SinkStats {
let sum_squares_left: f64 = chunk.left.iter().map(|&x| (x * x) as f64).sum(); let sum_squares_left: f64 = chunk.left.iter().map(|&x| (x * x) as f64).sum();
let sum_squares_right: f64 = chunk.right.iter().map(|&x| (x * x) as f64).sum(); let sum_squares_right: f64 = chunk.right.iter().map(|&x| (x * x) as f64).sum();
self.rms_left = ((self.rms_left.powi(2) * (self.total_samples - chunk.len() as u64) as f64 self.rms_left = ((self.rms_left.powi(2)
* (self.total_samples - chunk.len() as u64) as f64
+ sum_squares_left) + sum_squares_left)
/ self.total_samples as f64) / self.total_samples as f64)
.sqrt(); .sqrt();
self.rms_right = ((self.rms_right.powi(2) * (self.total_samples - chunk.len() as u64) as f64 self.rms_right = ((self.rms_right.powi(2)
* (self.total_samples - chunk.len() as u64) as f64
+ sum_squares_right) + sum_squares_right)
/ self.total_samples as f64) / self.total_samples as f64)
.sqrt(); .sqrt();

View File

@@ -1,4 +1,7 @@
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}}; use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -63,12 +66,8 @@ impl SourceNode {
sample_rate: u32, sample_rate: u32,
) -> Result<(), AudioError> { ) -> Result<(), AudioError> {
for i in 0..count { for i in 0..count {
let chunk = AudioChunk::new( let chunk =
i, AudioChunk::new(i, vec![0.0; chunk_size], vec![0.0; chunk_size], sample_rate);
vec![0.0; chunk_size],
vec![0.0; chunk_size],
sample_rate,
);
self.subscribers.push(Arc::new(chunk)).await?; self.subscribers.push(Arc::new(chunk)).await?;
} }
Ok(()) Ok(())

View File

@@ -1,4 +1,7 @@
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}}; use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{mpsc, RwLock}; use tokio::sync::{mpsc, RwLock};

View File

@@ -32,7 +32,10 @@ async fn test_complete_pipeline() {
tokio::spawn(async move { tokio::spawn(async move {
let mut source = SourceNode::new(); let mut source = SourceNode::new();
source.add_subscriber(decoder_tx); source.add_subscriber(decoder_tx);
source.generate_chunks(10, 4800, 48000, 440.0).await.unwrap(); source
.generate_chunks(10, 4800, 48000, 440.0)
.await
.unwrap();
}); });
// Attendre la fin // Attendre la fin
@@ -73,7 +76,10 @@ async fn test_multiroom_buffering() {
tokio::spawn(async move { tokio::spawn(async move {
let mut source = SourceNode::new(); let mut source = SourceNode::new();
source.add_subscriber(buffer_tx); source.add_subscriber(buffer_tx);
source.generate_chunks(20, 1000, 48000, 440.0).await.unwrap(); source
.generate_chunks(20, 1000, 48000, 440.0)
.await
.unwrap();
}); });
let stats1 = sink1_handle.await.unwrap(); let stats1 = sink1_handle.await.unwrap();
@@ -108,7 +114,10 @@ async fn test_timer_accuracy() {
source.add_subscriber(timer_tx); source.add_subscriber(timer_tx);
// 48000 samples à 48kHz = 1 seconde // 48000 samples à 48kHz = 1 seconde
source.generate_chunks(1, 48000, 48000, 440.0).await.unwrap(); source
.generate_chunks(1, 48000, 48000, 440.0)
.await
.unwrap();
}); });
sink_handle.await.unwrap(); sink_handle.await.unwrap();

View File

@@ -30,12 +30,20 @@ async fn main() -> anyhow::Result<()> {
let header = &data[0..4]; let header = &data[0..4];
if header == b"fLaC" { if header == b"fLaC" {
println!("✓ File is FLAC!"); println!("✓ File is FLAC!");
} else if header[0..3] == *b"ID3" || (header.len() >= 2 && header[0] == 0xFF && (header[1] & 0xE0) == 0xE0) { } else if header[0..3] == *b"ID3"
|| (header.len() >= 2 && header[0] == 0xFF && (header[1] & 0xE0) == 0xE0)
{
println!("✗ File is still MP3!"); println!("✗ File is still MP3!");
println!(" Header: {:02X} {:02X} {:02X} {:02X}", header[0], header[1], header[2], header[3]); println!(
" Header: {:02X} {:02X} {:02X} {:02X}",
header[0], header[1], header[2], header[3]
);
} else { } else {
println!("? Unknown format"); println!("? Unknown format");
println!(" Header: {:02X} {:02X} {:02X} {:02X}", header[0], header[1], header[2], header[3]); println!(
" Header: {:02X} {:02X} {:02X} {:02X}",
header[0], header[1], header[2], header[3]
);
} }
} }

View File

@@ -70,7 +70,10 @@ fn create_flac_transformer() -> StreamTransformer {
buffer.extend_from_slice(&chunk); buffer.extend_from_slice(&chunk);
} }
tracing::debug!("Downloaded {} bytes total, starting FLAC conversion", buffer.len()); tracing::debug!(
"Downloaded {} bytes total, starting FLAC conversion",
buffer.len()
);
// 2. Si c'est déjà du FLAC, on l'écrit directement // 2. Si c'est déjà du FLAC, on l'écrit directement
if buffer.len() >= 4 && &buffer[0..4] == b"fLaC" { if buffer.len() >= 4 && &buffer[0..4] == b"fLaC" {
@@ -85,21 +88,26 @@ fn create_flac_transformer() -> StreamTransformer {
// 3. Décoder l'audio avec Symphonia // 3. Décoder l'audio avec Symphonia
let (samples, channels, sample_rate, bits_per_sample) = { let (samples, channels, sample_rate, bits_per_sample) = {
use std::io::Cursor;
use symphonia::core::audio::SampleBuffer; use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL}; use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL};
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::FormatOptions; use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream; use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions; use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint; use symphonia::core::probe::Hint;
use symphonia::core::errors::Error as SymphoniaError;
use std::io::Cursor;
let cursor = Cursor::new(buffer); let cursor = Cursor::new(buffer);
let mss = MediaSourceStream::new(Box::new(cursor), Default::default()); let mss = MediaSourceStream::new(Box::new(cursor), Default::default());
let hint = Hint::new(); let hint = Hint::new();
let probed = symphonia::default::get_probe() let probed = symphonia::default::get_probe()
.format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default()) .format(
&hint,
mss,
&FormatOptions::default(),
&MetadataOptions::default(),
)
.map_err(|e| format!("Failed to probe format: {}", e))?; .map_err(|e| format!("Failed to probe format: {}", e))?;
let mut format = probed.format; let mut format = probed.format;
@@ -114,15 +122,18 @@ fn create_flac_transformer() -> StreamTransformer {
.make(&track.codec_params, &DecoderOptions::default()) .make(&track.codec_params, &DecoderOptions::default())
.map_err(|e| format!("Failed to create decoder: {}", e))?; .map_err(|e| format!("Failed to create decoder: {}", e))?;
let channels = track.codec_params.channels let channels = track
.codec_params
.channels
.ok_or_else(|| "No channel info".to_string())? .ok_or_else(|| "No channel info".to_string())?
.count(); .count();
let sample_rate = track.codec_params.sample_rate let sample_rate = track
.codec_params
.sample_rate
.ok_or_else(|| "No sample rate info".to_string())?; .ok_or_else(|| "No sample rate info".to_string())?;
let bits_per_sample = track.codec_params.bits_per_sample let bits_per_sample = track.codec_params.bits_per_sample.unwrap_or(16);
.unwrap_or(16);
let mut samples_i32 = Vec::new(); let mut samples_i32 = Vec::new();
let track_id = track.id; let track_id = track.id;
@@ -135,7 +146,9 @@ fn create_flac_transformer() -> StreamTransformer {
decoder.reset(); decoder.reset();
continue; continue;
} }
Err(SymphoniaError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => { Err(SymphoniaError::IoError(e))
if e.kind() == std::io::ErrorKind::UnexpectedEof =>
{
break; break;
} }
Err(e) => return Err(format!("Decode error: {}", e)), Err(e) => return Err(format!("Decode error: {}", e)),
@@ -183,13 +196,13 @@ fn create_flac_transformer() -> StreamTransformer {
tracing::debug!("Normalizing to 16-bit"); tracing::debug!("Normalizing to 16-bit");
let samples = samples_i32.iter().map(|&s| (s >> 16) as i32).collect(); let samples = samples_i32.iter().map(|&s| (s >> 16) as i32).collect();
(samples, 16) (samples, 16)
}, }
17..=24 => { 17..=24 => {
// Pour 17-24 bits, normaliser vers la plage 24-bit // Pour 17-24 bits, normaliser vers la plage 24-bit
tracing::debug!("Normalizing to 24-bit"); tracing::debug!("Normalizing to 24-bit");
let samples = samples_i32.iter().map(|&s| (s >> 8) as i32).collect(); let samples = samples_i32.iter().map(|&s| (s >> 8) as i32).collect();
(samples, 24) (samples, 24)
}, }
_ => { _ => {
// Pour 25-32 bits, garder la pleine échelle i32 // Pour 25-32 bits, garder la pleine échelle i32
tracing::debug!("Keeping 32-bit"); tracing::debug!("Keeping 32-bit");
@@ -200,15 +213,20 @@ fn create_flac_transformer() -> StreamTransformer {
(normalized_samples, channels, sample_rate, target_bits) (normalized_samples, channels, sample_rate, target_bits)
}; };
tracing::debug!("Encoding to FLAC: {} samples, {} channels, {} Hz, {} bits", tracing::debug!(
samples.len(), channels, sample_rate, bits_per_sample); "Encoding to FLAC: {} samples, {} channels, {} Hz, {} bits",
samples.len(),
channels,
sample_rate,
bits_per_sample
);
// 4. Encoder en FLAC avec flacenc // 4. Encoder en FLAC avec flacenc
// Note: L'encodage FLAC est une opération bloquante/CPU-intensive, // Note: L'encodage FLAC est une opération bloquante/CPU-intensive,
// donc nous l'exécutons dans un thread bloquant pour ne pas bloquer le runtime Tokio // donc nous l'exécutons dans un thread bloquant pour ne pas bloquer le runtime Tokio
let flac_data = tokio::task::spawn_blocking(move || { let flac_data = tokio::task::spawn_blocking(move || {
use flacenc::component::BitRepr;
use flacenc::bitsink::ByteSink; use flacenc::bitsink::ByteSink;
use flacenc::component::BitRepr;
use flacenc::error::Verify; use flacenc::error::Verify;
let config = flacenc::config::Encoder::default() let config = flacenc::config::Encoder::default()
@@ -222,15 +240,13 @@ fn create_flac_transformer() -> StreamTransformer {
sample_rate as usize, sample_rate as usize,
); );
let flac_stream = flacenc::encode_with_fixed_block_size( let flac_stream =
&config, flacenc::encode_with_fixed_block_size(&config, source, config.block_size)
source, .map_err(|e| format!("FLAC encode error: {:?}", e))?;
config.block_size,
)
.map_err(|e| format!("FLAC encode error: {:?}", e))?;
let mut sink = ByteSink::new(); let mut sink = ByteSink::new();
flac_stream.write(&mut sink) flac_stream
.write(&mut sink)
.map_err(|e| format!("FLAC write error: {:?}", e))?; .map_err(|e| format!("FLAC write error: {:?}", e))?;
Ok::<Vec<u8>, String>(sink.into_inner()) Ok::<Vec<u8>, String>(sink.into_inner())
@@ -241,7 +257,9 @@ fn create_flac_transformer() -> StreamTransformer {
tracing::debug!("FLAC encoding complete: {} bytes", flac_data.len()); tracing::debug!("FLAC encoding complete: {} bytes", flac_data.len());
// 5. Écrire le fichier FLAC // 5. Écrire le fichier FLAC
file.write_all(&flac_data).await.map_err(|e| e.to_string())?; file.write_all(&flac_data)
.await
.map_err(|e| e.to_string())?;
file.flush().await.map_err(|e| e.to_string())?; file.flush().await.map_err(|e| e.to_string())?;
// 6. Mettre à jour la progression finale // 6. Mettre à jour la progression finale
@@ -328,13 +346,17 @@ pub async fn add_with_metadata_extraction(
let metadata_json = serde_json::to_string(&metadata)?; let metadata_json = serde_json::to_string(&metadata)?;
// Stocker dans la DB // Stocker dans la DB
cache.db.update_metadata(&pk, &metadata_json) cache
.db
.update_metadata(&pk, &metadata_json)
.map_err(|e| anyhow::anyhow!("Database error: {}", e))?; .map_err(|e| anyhow::anyhow!("Database error: {}", e))?;
// Mettre à jour la collection si les métadonnées en fournissent une // Mettre à jour la collection si les métadonnées en fournissent une
if collection.is_none() { if collection.is_none() {
if let Some(auto_collection) = metadata.collection_key() { if let Some(auto_collection) = metadata.collection_key() {
cache.db.add(&pk, url, Some(&auto_collection)) cache
.db
.add(&pk, url, Some(&auto_collection))
.map_err(|e| anyhow::anyhow!("Database error: {}", e))?; .map_err(|e| anyhow::anyhow!("Database error: {}", e))?;
} }
} }
@@ -366,7 +388,9 @@ pub async fn add_with_metadata_extraction(
/// # } /// # }
/// ``` /// ```
pub fn get_metadata(cache: &Cache, pk: &str) -> Result<crate::metadata::AudioMetadata> { pub fn get_metadata(cache: &Cache, pk: &str) -> Result<crate::metadata::AudioMetadata> {
let metadata_json = cache.db.get_metadata_json(pk) let metadata_json = cache
.db
.get_metadata_json(pk)
.map_err(|e| anyhow::anyhow!("Database error: {}", e))? .map_err(|e| anyhow::anyhow!("Database error: {}", e))?
.ok_or_else(|| anyhow::anyhow!("No metadata found for pk: {}", pk))?; .ok_or_else(|| anyhow::anyhow!("No metadata found for pk: {}", pk))?;
@@ -375,4 +399,3 @@ pub fn get_metadata(cache: &Cache, pk: &str) -> Result<crate::metadata::AudioMet
Ok(metadata) Ok(metadata)
} }

View File

@@ -4,6 +4,7 @@
//! pour standardiser le stockage dans le cache. //! pour standardiser le stockage dans le cache.
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::io::Cursor;
use symphonia::core::audio::SampleBuffer; use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL}; use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL};
use symphonia::core::errors::Error as SymphoniaError; use symphonia::core::errors::Error as SymphoniaError;
@@ -11,7 +12,6 @@ use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream; use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions; use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint; use symphonia::core::probe::Hint;
use std::io::Cursor;
/// Convertit des données audio en FLAC /// Convertit des données audio en FLAC
/// ///
@@ -54,7 +54,12 @@ pub fn convert_to_flac(data: &[u8], extension: Option<&str>) -> Result<Vec<u8>>
// Prober le format // Prober le format
let probed = symphonia::default::get_probe() let probed = symphonia::default::get_probe()
.format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default()) .format(
&hint,
mss,
&FormatOptions::default(),
&MetadataOptions::default(),
)
.map_err(|e| anyhow!("Impossible de détecter le format audio: {}", e))?; .map_err(|e| anyhow!("Impossible de détecter le format audio: {}", e))?;
let mut format = probed.format; let mut format = probed.format;

View File

@@ -131,14 +131,14 @@
//! - [`pmoserver`] : Serveur HTTP //! - [`pmoserver`] : Serveur HTTP
pub mod cache; pub mod cache;
pub mod metadata;
pub mod flac; pub mod flac;
pub mod metadata;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
pub mod openapi; pub mod openapi;
// Re-exports principaux // Re-exports principaux
pub use cache::{Cache, AudioConfig, new_cache, add_with_metadata_extraction, get_metadata}; pub use cache::{add_with_metadata_extraction, get_metadata, new_cache, AudioConfig, Cache};
pub use metadata::AudioMetadata; pub use metadata::AudioMetadata;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
@@ -161,7 +161,11 @@ pub trait AudioCacheExt {
/// # Returns /// # Returns
/// ///
/// * `Arc<Cache>` - Instance partagée du cache /// * `Arc<Cache>` - Instance partagée du cache
async fn init_audio_cache(&mut self, cache_dir: &str, limit: usize) -> anyhow::Result<std::sync::Arc<Cache>>; async fn init_audio_cache(
&mut self,
cache_dir: &str,
limit: usize,
) -> anyhow::Result<std::sync::Arc<Cache>>;
/// Initialise le cache audio avec la configuration par défaut. /// Initialise le cache audio avec la configuration par défaut.
/// ///
@@ -170,7 +174,7 @@ pub trait AudioCacheExt {
} }
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
use pmocache::pmoserver_ext::{create_file_router, create_api_router}; use pmocache::pmoserver_ext::{create_api_router, create_file_router};
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
use std::sync::Arc; use std::sync::Arc;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
@@ -178,14 +182,18 @@ use utoipa::OpenApi;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
impl AudioCacheExt for pmoserver::Server { impl AudioCacheExt for pmoserver::Server {
async fn init_audio_cache(&mut self, cache_dir: &str, limit: usize) -> anyhow::Result<Arc<Cache>> { async fn init_audio_cache(
&mut self,
cache_dir: &str,
limit: usize,
) -> anyhow::Result<Arc<Cache>> {
let cache = Arc::new(crate::cache::new_cache(cache_dir, limit)?); let cache = Arc::new(crate::cache::new_cache(cache_dir, limit)?);
// Router de fichiers pour servir les pistes FLAC // Router de fichiers pour servir les pistes FLAC
// Routes: GET /audio/tracks/{pk} et GET /audio/tracks/{pk}/{param} // Routes: GET /audio/tracks/{pk} et GET /audio/tracks/{pk}/{param}
let file_router = create_file_router( let file_router = create_file_router(
cache.clone(), cache.clone(),
"audio/flac" // Content-Type "audio/flac", // Content-Type
); );
self.add_router("/", file_router).await; self.add_router("/", file_router).await;

View File

@@ -87,12 +87,12 @@ impl AudioMetadata {
/// println!("Titre: {:?}", metadata.title); /// println!("Titre: {:?}", metadata.title);
/// ``` /// ```
pub fn from_file(path: &Path) -> Result<Self> { pub fn from_file(path: &Path) -> Result<Self> {
let tagged_file = Probe::open(path)? let tagged_file = Probe::open(path)?.options(ParseOptions::new()).read()?;
.options(ParseOptions::new())
.read()?;
let properties = tagged_file.properties(); let properties = tagged_file.properties();
let tag = tagged_file.primary_tag().or_else(|| tagged_file.first_tag()); let tag = tagged_file
.primary_tag()
.or_else(|| tagged_file.first_tag());
let mut metadata = Self { let mut metadata = Self {
title: None, title: None,
@@ -138,7 +138,9 @@ impl AudioMetadata {
.read()?; .read()?;
let properties = tagged_file.properties(); let properties = tagged_file.properties();
let tag = tagged_file.primary_tag().or_else(|| tagged_file.first_tag()); let tag = tagged_file
.primary_tag()
.or_else(|| tagged_file.first_tag());
let mut metadata = Self { let mut metadata = Self {
title: None, title: None,

View File

@@ -11,7 +11,9 @@ fn main() {
println!(" dl.wait_until_finished().await?;\n"); println!(" dl.wait_until_finished().await?;\n");
println!("2. Téléchargement avec transformation:"); println!("2. Téléchargement avec transformation:");
println!(" let transformer: StreamTransformer = Box::new(|response, mut file, update_progress| {{"); println!(
" let transformer: StreamTransformer = Box::new(|response, mut file, update_progress| {{"
);
println!(" Box::pin(async move {{"); println!(" Box::pin(async move {{");
println!(" let mut stream = response.bytes_stream();"); println!(" let mut stream = response.bytes_stream();");
println!(" let mut total = 0u64;"); println!(" let mut total = 0u64;");

View File

@@ -1,7 +1,7 @@
// Exemple d'utilisation du module download avec transformations // Exemple d'utilisation du module download avec transformations
use pmocache::download::{download_with_transformer, StreamTransformer};
use futures_util::StreamExt; use futures_util::StreamExt;
use pmocache::download::{download_with_transformer, StreamTransformer};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
/// Exemple de transformer qui compresse les données en gzip /// Exemple de transformer qui compresse les données en gzip
@@ -63,7 +63,13 @@ fn create_uppercase_transformer() -> StreamTransformer {
// Transformer en majuscules (seulement pour texte ASCII) // Transformer en majuscules (seulement pour texte ASCII)
let transformed: Vec<u8> = chunk let transformed: Vec<u8> = chunk
.iter() .iter()
.map(|&b| if b.is_ascii_lowercase() { b.to_ascii_uppercase() } else { b }) .map(|&b| {
if b.is_ascii_lowercase() {
b.to_ascii_uppercase()
} else {
b
}
})
.collect(); .collect();
file.write_all(&transformed) file.write_all(&transformed)
@@ -210,7 +216,10 @@ async fn main() {
Ok(_) => { Ok(_) => {
println!(" ✓ Téléchargement terminé!"); println!(" ✓ Téléchargement terminé!");
println!(" - Taille source: {} bytes", dl.current_size().await); println!(" - Taille source: {} bytes", dl.current_size().await);
println!(" - Taille transformée: {} bytes", dl.transformed_size().await); println!(
" - Taille transformée: {} bytes",
dl.transformed_size().await
);
} }
Err(e) => { Err(e) => {
eprintln!(" ✗ Erreur: {}", e); eprintln!(" ✗ Erreur: {}", e);
@@ -223,16 +232,15 @@ async fn main() {
let _ = std::fs::remove_file(&skip_file); let _ = std::fs::remove_file(&skip_file);
let transformer = create_skip_header_transformer(100); let transformer = create_skip_header_transformer(100);
let dl = download_with_transformer( let dl = download_with_transformer(&skip_file, "https://www.rust-lang.org/", Some(transformer));
&skip_file,
"https://www.rust-lang.org/",
Some(transformer),
);
match dl.wait_until_finished().await { match dl.wait_until_finished().await {
Ok(_) => { Ok(_) => {
println!(" ✓ Téléchargement terminé!"); println!(" ✓ Téléchargement terminé!");
println!(" - Taille transformée: {} bytes", dl.transformed_size().await); println!(
" - Taille transformée: {} bytes",
dl.transformed_size().await
);
} }
Err(e) => { Err(e) => {
eprintln!(" ✗ Erreur: {}", e); eprintln!(" ✗ Erreur: {}", e);
@@ -254,7 +262,10 @@ async fn main() {
match dl.wait_until_finished().await { match dl.wait_until_finished().await {
Ok(_) => { Ok(_) => {
println!(" ✓ Téléchargement terminé!"); println!(" ✓ Téléchargement terminé!");
println!(" - Taille transformée: {} bytes", dl.transformed_size().await); println!(
" - Taille transformée: {} bytes",
dl.transformed_size().await
);
} }
Err(e) => { Err(e) => {
eprintln!(" ✗ Erreur: {}", e); eprintln!(" ✗ Erreur: {}", e);

View File

@@ -92,9 +92,7 @@ pub struct ErrorResponse {
/// Liste tous les items en cache avec leurs statistiques /// Liste tous les items en cache avec leurs statistiques
/// ///
/// Retourne la liste complète des entrées du cache triées par nombre d'accès décroissant. /// Retourne la liste complète des entrées du cache triées par nombre d'accès décroissant.
pub async fn list_items<C: CacheConfig>( pub async fn list_items<C: CacheConfig>(State(cache): State<Arc<Cache<C>>>) -> impl IntoResponse {
State(cache): State<Arc<Cache<C>>>,
) -> impl IntoResponse {
match cache.db.get_all() { match cache.db.get_all() {
Ok(entries) => (StatusCode::OK, Json(entries)).into_response(), Ok(entries) => (StatusCode::OK, Json(entries)).into_response(),
Err(e) => ( Err(e) => (
@@ -192,7 +190,10 @@ pub async fn add_item<C: CacheConfig>(
.into_response(); .into_response();
} }
match cache.add_from_url(&req.url, req.collection.as_deref()).await { match cache
.add_from_url(&req.url, req.collection.as_deref())
.await
{
Ok(pk) => ( Ok(pk) => (
StatusCode::CREATED, StatusCode::CREATED,
Json(AddItemResponse { Json(AddItemResponse {
@@ -268,9 +269,7 @@ pub async fn delete_item<C: CacheConfig>(
/// Purge complètement le cache /// Purge complètement le cache
/// ///
/// Supprime tous les items et vide la base de données. Opération irréversible. /// Supprime tous les items et vide la base de données. Opération irréversible.
pub async fn purge_cache<C: CacheConfig>( pub async fn purge_cache<C: CacheConfig>(State(cache): State<Arc<Cache<C>>>) -> impl IntoResponse {
State(cache): State<Arc<Cache<C>>>,
) -> impl IntoResponse {
match cache.purge().await { match cache.purge().await {
Ok(_) => ( Ok(_) => (
StatusCode::OK, StatusCode::OK,

View File

@@ -3,9 +3,9 @@
//! Ce module fournit une interface générique pour gérer un cache de fichiers //! Ce module fournit une interface générique pour gérer un cache de fichiers
//! avec métadonnées dans une base de données SQLite. //! avec métadonnées dans une base de données SQLite.
use crate::cache_trait::{FileCache, pk_from_url}; use crate::cache_trait::{pk_from_url, FileCache};
use crate::db::DB; use crate::db::DB;
use crate::download::{Download, download_with_transformer, StreamTransformer}; use crate::download::{download_with_transformer, Download, StreamTransformer};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -26,10 +26,10 @@ pub trait CacheConfig: Send + Sync {
"file" "file"
} }
/// Cache name (ex: "covers", "audio", "cache") /// Cache name (ex: "covers", "audio", "cache")
fn cache_name() -> &'static str { fn cache_name() -> &'static str {
"cache" "cache"
} }
/// Default param extension ("orig") /// Default param extension ("orig")
fn default_param() -> &'static str { fn default_param() -> &'static str {
"orig" "orig"
} }
@@ -155,11 +155,7 @@ impl<C: CacheConfig> Cache<C> {
// Lancer le téléchargement avec transformer // Lancer le téléchargement avec transformer
let transformer = self.transformer_factory.as_ref().map(|f| f()); let transformer = self.transformer_factory.as_ref().map(|f| f());
let download = download_with_transformer( let download = download_with_transformer(&file_path, url, transformer);
&file_path,
url,
transformer,
);
// Stocker dans la map des downloads en cours // Stocker dans la map des downloads en cours
{ {
@@ -228,7 +224,6 @@ impl<C: CacheConfig> Cache<C> {
self.add_from_url(url, collection).await self.add_from_url(url, collection).await
} }
/// Récupère le chemin d'un fichier dans le cache /// Récupère le chemin d'un fichier dans le cache
/// ///
/// # Arguments /// # Arguments
@@ -289,8 +284,11 @@ impl<C: CacheConfig> Cache<C> {
let file_path = self.file_path(&entry.pk); let file_path = self.file_path(&entry.pk);
if !file_path.exists() { if !file_path.exists() {
// Re-télécharger le fichier manquant // Re-télécharger le fichier manquant
match self.add_from_url(&entry.source_url, entry.collection.as_deref()).await { match self
Ok(_) => {}, .add_from_url(&entry.source_url, entry.collection.as_deref())
.await
{
Ok(_) => {}
Err(_) => { Err(_) => {
// Si le téléchargement échoue, supprimer l'entrée DB // Si le téléchargement échoue, supprimer l'entrée DB
self.db.delete(&entry.pk)?; self.db.delete(&entry.pk)?;
@@ -413,7 +411,9 @@ impl<C: CacheConfig> Cache<C> {
/// * `min_size` - Taille minimale attendue en bytes /// * `min_size` - Taille minimale attendue en bytes
pub async fn wait_until_min_size(&self, pk: &str, min_size: u64) -> Result<()> { pub async fn wait_until_min_size(&self, pk: &str, min_size: u64) -> Result<()> {
if let Some(download) = self.get_download(pk).await { if let Some(download) = self.get_download(pk).await {
download.wait_until_min_size(min_size).await download
.wait_until_min_size(min_size)
.await
.map_err(|e| anyhow!("Download error: {}", e)) .map_err(|e| anyhow!("Download error: {}", e))
} else { } else {
// Déjà terminé ou n'existe pas // Déjà terminé ou n'existe pas
@@ -432,7 +432,9 @@ impl<C: CacheConfig> Cache<C> {
/// * `pk` - Clé primaire du fichier /// * `pk` - Clé primaire du fichier
pub async fn wait_until_finished(&self, pk: &str) -> Result<()> { pub async fn wait_until_finished(&self, pk: &str) -> Result<()> {
if let Some(download) = self.get_download(pk).await { if let Some(download) = self.get_download(pk).await {
download.wait_until_finished().await download
.wait_until_finished()
.await
.map_err(|e| anyhow!("Download error: {}", e)) .map_err(|e| anyhow!("Download error: {}", e))
} else { } else {
// Déjà terminé ou n'existe pas // Déjà terminé ou n'existe pas
@@ -460,7 +462,8 @@ impl<C: CacheConfig> Cache<C> {
/// ///
/// Format: `{pk}.{qualifier}.{extension}` /// Format: `{pk}.{qualifier}.{extension}`
pub fn file_path_with_qualifier(&self, pk: &str, qualifier: &str) -> PathBuf { pub fn file_path_with_qualifier(&self, pk: &str, qualifier: &str) -> PathBuf {
self.dir.join(format!("{}.{}.{}", pk, qualifier, C::file_extension())) self.dir
.join(format!("{}.{}.{}", pk, qualifier, C::file_extension()))
} }
/// Valide les données avant de les stocker /// Valide les données avant de les stocker
@@ -499,7 +502,9 @@ impl<C: CacheConfig> Cache<C> {
while let Ok(Some(dir_entry)) = dir_entries.next_entry().await { while let Ok(Some(dir_entry)) = dir_entries.next_entry().await {
if let Some(filename) = dir_entry.file_name().to_str() { if let Some(filename) = dir_entry.file_name().to_str() {
// Format: {pk}.{param}.{ext} // Format: {pk}.{param}.{ext}
if filename.starts_with(&entry.pk) && filename.starts_with(&format!("{}.", entry.pk)) { if filename.starts_with(&entry.pk)
&& filename.starts_with(&format!("{}.", entry.pk))
{
let _ = tokio::fs::remove_file(dir_entry.path()).await; let _ = tokio::fs::remove_file(dir_entry.path()).await;
} }
} }
@@ -515,15 +520,18 @@ impl<C: CacheConfig> Cache<C> {
} }
if removed > 0 { if removed > 0 {
tracing::info!("LRU eviction: removed {} old entries (cache size: {} -> {})", tracing::info!(
removed, count, count - removed); "LRU eviction: removed {} old entries (cache size: {} -> {})",
removed,
count,
count - removed
);
} }
Ok(removed) Ok(removed)
} }
} }
/// Implémentation du trait FileCache pour Cache /// Implémentation du trait FileCache pour Cache
impl<C: CacheConfig> FileCache<C> for Cache<C> { impl<C: CacheConfig> FileCache<C> for Cache<C> {
fn get_cache_dir(&self) -> &Path { fn get_cache_dir(&self) -> &Path {

View File

@@ -3,9 +3,9 @@
//! Ce module fournit une interface générique pour gérer les métadonnées //! Ce module fournit une interface générique pour gérer les métadonnées
//! des éléments en cache, avec tracking des accès et des statistiques. //! des éléments en cache, avec tracking des accès et des statistiques.
use rusqlite::{Connection, params};
use serde::Serialize;
use chrono::Utc; use chrono::Utc;
use rusqlite::{params, Connection};
use serde::Serialize;
use std::path::Path; use std::path::Path;
use std::sync::Mutex; use std::sync::Mutex;
@@ -32,7 +32,10 @@ pub struct CacheEntry {
#[cfg_attr(feature = "openapi", schema(example = "2025-01-15T10:30:00Z"))] #[cfg_attr(feature = "openapi", schema(example = "2025-01-15T10:30:00Z"))]
pub last_used: Option<String>, pub last_used: Option<String>,
/// Métadonnées JSON optionnelles (ex: métadonnées audio, EXIF images, etc.) /// Métadonnées JSON optionnelles (ex: métadonnées audio, EXIF images, etc.)
#[cfg_attr(feature = "openapi", schema(example = r#"{"title":"Track","artist":"Artist"}"#))] #[cfg_attr(
feature = "openapi",
schema(example = r#"{"title":"Track","artist":"Artist"}"#)
)]
pub metadata_json: Option<String>, pub metadata_json: Option<String>,
} }
@@ -161,20 +164,16 @@ impl DB {
self.table_name self.table_name
); );
conn.query_row( conn.query_row(&sql, [pk], |row| {
&sql, Ok(CacheEntry {
[pk], pk: row.get(0)?,
|row| { source_url: row.get(1)?,
Ok(CacheEntry { collection: row.get(2)?,
pk: row.get(0)?, hits: row.get(3)?,
source_url: row.get(1)?, last_used: row.get(4)?,
collection: row.get(2)?, metadata_json: row.get(5)?,
hits: row.get(3)?, })
last_used: row.get(4)?, })
metadata_json: row.get(5)?,
})
},
)
} }
/// Met à jour le compteur d'accès et la date du dernier accès /// Met à jour le compteur d'accès et la date du dernier accès
@@ -189,10 +188,7 @@ impl DB {
self.table_name self.table_name
); );
conn.execute( conn.execute(&sql, params![Utc::now().to_rfc3339(), pk])?;
&sql,
params![Utc::now().to_rfc3339(), pk],
)?;
Ok(()) Ok(())
} }
@@ -215,17 +211,18 @@ impl DB {
let mut stmt = conn.prepare(&sql)?; let mut stmt = conn.prepare(&sql)?;
let entries = stmt.query_map([], |row| { let entries = stmt
Ok(CacheEntry { .query_map([], |row| {
pk: row.get(0)?, Ok(CacheEntry {
source_url: row.get(1)?, pk: row.get(0)?,
collection: row.get(2)?, source_url: row.get(1)?,
hits: row.get(3)?, collection: row.get(2)?,
last_used: row.get(4)?, hits: row.get(3)?,
metadata_json: row.get(5)?, last_used: row.get(4)?,
}) metadata_json: row.get(5)?,
})? })
.collect::<rusqlite::Result<Vec<_>>>()?; })?
.collect::<rusqlite::Result<Vec<_>>>()?;
Ok(entries) Ok(entries)
} }
@@ -244,17 +241,18 @@ impl DB {
let mut stmt = conn.prepare(&sql)?; let mut stmt = conn.prepare(&sql)?;
let entries = stmt.query_map([collection], |row| { let entries = stmt
Ok(CacheEntry { .query_map([collection], |row| {
pk: row.get(0)?, Ok(CacheEntry {
source_url: row.get(1)?, pk: row.get(0)?,
collection: row.get(2)?, source_url: row.get(1)?,
hits: row.get(3)?, collection: row.get(2)?,
last_used: row.get(4)?, hits: row.get(3)?,
metadata_json: row.get(5)?, last_used: row.get(4)?,
}) metadata_json: row.get(5)?,
})? })
.collect::<rusqlite::Result<Vec<_>>>()?; })?
.collect::<rusqlite::Result<Vec<_>>>()?;
Ok(entries) Ok(entries)
} }
@@ -319,17 +317,18 @@ impl DB {
let mut stmt = conn.prepare(&sql)?; let mut stmt = conn.prepare(&sql)?;
let entries = stmt.query_map([limit], |row| { let entries = stmt
Ok(CacheEntry { .query_map([limit], |row| {
pk: row.get(0)?, Ok(CacheEntry {
source_url: row.get(1)?, pk: row.get(0)?,
collection: row.get(2)?, source_url: row.get(1)?,
hits: row.get(3)?, collection: row.get(2)?,
last_used: row.get(4)?, hits: row.get(3)?,
metadata_json: row.get(5)?, last_used: row.get(4)?,
}) metadata_json: row.get(5)?,
})? })
.collect::<rusqlite::Result<Vec<_>>>()?; })?
.collect::<rusqlite::Result<Vec<_>>>()?;
Ok(entries) Ok(entries)
} }

View File

@@ -1,3 +1,4 @@
use futures_util::Future;
use std::fs::File; use std::fs::File;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -5,7 +6,6 @@ use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use futures_util::Future;
/// Type pour une fonction de transformation de stream /// Type pour une fonction de transformation de stream
/// ///
@@ -248,20 +248,16 @@ async fn download_impl(
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
// Lancer la requête // Lancer la requête
let response = client let response = client.get(&url).send().await.map_err(|e| {
.get(&url) let error = format!("Failed to fetch URL: {}", e);
.send() tokio::task::block_in_place(|| {
.await tokio::runtime::Handle::current().block_on(async {
.map_err(|e| { let mut s = state.write().await;
let error = format!("Failed to fetch URL: {}", e); s.error = Some(error.clone());
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
let mut s = state.write().await;
s.error = Some(error.clone());
});
}); });
error });
})?; error
})?;
// Vérifier le statut // Vérifier le statut
if !response.status().is_success() { if !response.status().is_success() {
@@ -279,31 +275,30 @@ async fn download_impl(
} }
// Créer le fichier de destination // Créer le fichier de destination
let file = tokio::fs::File::create(&filename) let file = tokio::fs::File::create(&filename).await.map_err(|e| {
.await let error = format!("Failed to create file: {}", e);
.map_err(|e| { tokio::task::block_in_place(|| {
let error = format!("Failed to create file: {}", e); tokio::runtime::Handle::current().block_on(async {
tokio::task::block_in_place(|| { let mut s = state.write().await;
tokio::runtime::Handle::current().block_on(async { s.error = Some(error.clone());
let mut s = state.write().await; s.finished = true;
s.error = Some(error.clone());
s.finished = true;
});
}); });
error });
})?; error
})?;
// Si un transformer est fourni, l'utiliser // Si un transformer est fourni, l'utiliser
if let Some(transformer) = transformer { if let Some(transformer) = transformer {
// Créer un callback pour mettre à jour la progression // Créer un callback pour mettre à jour la progression
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
let progress_callback: Arc<dyn Fn(u64) + Send + Sync> = Arc::new(move |transformed_bytes| { let progress_callback: Arc<dyn Fn(u64) + Send + Sync> =
let state = Arc::clone(&state_clone); Arc::new(move |transformed_bytes| {
tokio::spawn(async move { let state = Arc::clone(&state_clone);
let mut s = state.write().await; tokio::spawn(async move {
s.transformed_size = transformed_bytes; let mut s = state.write().await;
s.transformed_size = transformed_bytes;
});
}); });
});
// Appeler le transformer // Appeler le transformer
match transformer(response, file, progress_callback).await { match transformer(response, file, progress_callback).await {
@@ -331,8 +326,8 @@ async fn default_download(
mut file: tokio::fs::File, mut file: tokio::fs::File,
state: Arc<RwLock<DownloadState>>, state: Arc<RwLock<DownloadState>>,
) -> Result<(), String> { ) -> Result<(), String> {
use tokio::io::AsyncWriteExt;
use futures_util::StreamExt; use futures_util::StreamExt;
use tokio::io::AsyncWriteExt;
let mut stream = response.bytes_stream(); let mut stream = response.bytes_stream();

View File

@@ -122,9 +122,9 @@
//! - [`pmocovers`] : Cache d'images avec conversion WebP //! - [`pmocovers`] : Cache d'images avec conversion WebP
//! - [`pmoaudiocache`] : Cache de pistes audio //! - [`pmoaudiocache`] : Cache de pistes audio
pub mod db;
pub mod cache; pub mod cache;
pub mod cache_trait; pub mod cache_trait;
pub mod db;
pub mod download; pub mod download;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
@@ -136,16 +136,13 @@ pub mod api;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
pub mod openapi; pub mod openapi;
pub use db::{DB, CacheEntry};
pub use cache::{Cache, CacheConfig}; pub use cache::{Cache, CacheConfig};
pub use cache_trait::{FileCache, pk_from_url}; pub use cache_trait::{pk_from_url, FileCache};
pub use download::{Download, download, download_with_transformer, StreamTransformer}; pub use db::{CacheEntry, DB};
pub use download::{download, download_with_transformer, Download, StreamTransformer};
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
pub use pmoserver_ext::{create_file_router, create_api_router, GenericCacheExt}; pub use pmoserver_ext::{create_api_router, create_file_router, GenericCacheExt};
#[cfg(all(feature = "pmoserver", feature = "openapi"))] #[cfg(all(feature = "pmoserver", feature = "openapi"))]
pub use api::{ pub use api::{AddItemRequest, AddItemResponse, DeleteItemResponse, DownloadStatus, ErrorResponse};
DownloadStatus, AddItemRequest, AddItemResponse,
DeleteItemResponse, ErrorResponse,
};

View File

@@ -49,15 +49,15 @@ use axum::{
Router, Router,
}; };
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
use std::future::Future;
#[cfg(feature = "pmoserver")]
use std::pin::Pin;
#[cfg(feature = "pmoserver")]
use std::sync::Arc; use std::sync::Arc;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
use tracing::warn; use tracing::warn;
#[cfg(feature = "pmoserver")]
use std::pin::Pin;
#[cfg(feature = "pmoserver")]
use std::future::Future;
/// Type pour le callback de génération de param /// Type pour le callback de génération de param
/// ///
@@ -75,16 +75,20 @@ use std::future::Future;
/// Les données générées ou None si le param n'est pas supporté /// Les données générées ou None si le param n'est pas supporté
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
pub type ParamGenerator<C> = Arc< pub type ParamGenerator<C> = Arc<
dyn Fn(Arc<Cache<C>>, String, String) dyn Fn(Arc<Cache<C>>, String, String) -> Pin<Box<dyn Future<Output = Option<Vec<u8>>> + Send>>
-> Pin<Box<dyn Future<Output = Option<Vec<u8>>> + Send>> + Send
+ Send + Sync + Sync,
>; >;
/// Handler générique pour GET /{cache_name}/{cache_type}/{pk} /// Handler générique pour GET /{cache_name}/{cache_type}/{pk}
/// Sert un fichier avec le param par défaut /// Sert un fichier avec le param par défaut
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
async fn get_file<C: CacheConfig + 'static>( async fn get_file<C: CacheConfig + 'static>(
State((cache, content_type, param_generator)): State<(Arc<Cache<C>>, &'static str, Option<ParamGenerator<C>>)>, State((cache, content_type, param_generator)): State<(
Arc<Cache<C>>,
&'static str,
Option<ParamGenerator<C>>,
)>,
Path(pk): Path<String>, Path(pk): Path<String>,
) -> Response { ) -> Response {
// Utiliser le param par défaut // Utiliser le param par défaut
@@ -96,7 +100,11 @@ async fn get_file<C: CacheConfig + 'static>(
/// Sert un fichier avec un param spécifique /// Sert un fichier avec un param spécifique
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
async fn get_file_with_param<C: CacheConfig + 'static>( async fn get_file_with_param<C: CacheConfig + 'static>(
State((cache, content_type, param_generator)): State<(Arc<Cache<C>>, &'static str, Option<ParamGenerator<C>>)>, State((cache, content_type, param_generator)): State<(
Arc<Cache<C>>,
&'static str,
Option<ParamGenerator<C>>,
)>,
Path((pk, param)): Path<(String, String)>, Path((pk, param)): Path<(String, String)>,
) -> Response { ) -> Response {
serve_file_with_streaming(&cache, &pk, &param, content_type, param_generator).await serve_file_with_streaming(&cache, &pk, &param, content_type, param_generator).await
@@ -122,11 +130,7 @@ async fn serve_file_with_streaming<C: CacheConfig>(
if let Some(generator) = param_generator { if let Some(generator) = param_generator {
if let Some(data) = generator(cache.clone(), pk.to_string(), param.to_string()).await { if let Some(data) = generator(cache.clone(), pk.to_string(), param.to_string()).await {
// Le générateur a créé les données, les servir directement // Le générateur a créé les données, les servir directement
return ( return (StatusCode::OK, [("content-type", content_type)], data).into_response();
StatusCode::OK,
[("content-type", content_type)],
data,
).into_response();
} }
} }
} }
@@ -207,12 +211,7 @@ async fn serve_complete_file(
} }
match tokio::fs::read(&file_path).await { match tokio::fs::read(&file_path).await {
Ok(data) => ( Ok(data) => (StatusCode::OK, [("content-type", content_type)], data).into_response(),
StatusCode::OK,
[("content-type", content_type)],
data,
)
.into_response(),
Err(e) => { Err(e) => {
warn!("Error reading file {:?}: {}", file_path, e); warn!("Error reading file {:?}: {}", file_path, e);
(StatusCode::INTERNAL_SERVER_ERROR, "Error reading file").into_response() (StatusCode::INTERNAL_SERVER_ERROR, "Error reading file").into_response()
@@ -332,9 +331,7 @@ pub fn create_file_router_with_generator<C: CacheConfig + 'static>(
/// - `DELETE /{pk}` - Supprimer un item /// - `DELETE /{pk}` - Supprimer un item
/// - `POST /consolidate` - Consolider le cache /// - `POST /consolidate` - Consolider le cache
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
pub fn create_api_router<C: CacheConfig + 'static>( pub fn create_api_router<C: CacheConfig + 'static>(cache: Arc<Cache<C>>) -> Router {
cache: Arc<Cache<C>>,
) -> Router {
use crate::api; use crate::api;
Router::new() Router::new()
@@ -346,8 +343,7 @@ pub fn create_api_router<C: CacheConfig + 'static>(
) )
.route( .route(
"/{pk}", "/{pk}",
get(api::get_item_info::<C>) get(api::get_item_info::<C>).delete(api::delete_item::<C>),
.delete(api::delete_item::<C>),
) )
.route("/{pk}/status", get(api::get_download_status::<C>)) .route("/{pk}/status", get(api::get_download_status::<C>))
.route("/consolidate", post(api::consolidate_cache::<C>)) .route("/consolidate", post(api::consolidate_cache::<C>))

View File

@@ -43,12 +43,14 @@ fn create_webp_transformer() -> StreamTransformer {
// Convertir en WebP // Convertir en WebP
let img = image::load_from_memory(&bytes) let img = image::load_from_memory(&bytes)
.map_err(|e| format!("Image decode error: {}", e))?; .map_err(|e| format!("Image decode error: {}", e))?;
let webp_data = crate::webp::encode_webp(&img) let webp_data =
.map_err(|e| format!("WebP encode error: {}", e))?; crate::webp::encode_webp(&img).map_err(|e| format!("WebP encode error: {}", e))?;
// Écrire et mettre à jour la progression // Écrire et mettre à jour la progression
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
file.write_all(&webp_data).await.map_err(|e| e.to_string())?; file.write_all(&webp_data)
.await
.map_err(|e| e.to_string())?;
file.flush().await.map_err(|e| e.to_string())?; file.flush().await.map_err(|e| e.to_string())?;
progress(webp_data.len() as u64); progress(webp_data.len() as u64);

View File

@@ -42,15 +42,15 @@ pub mod webp;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
pub mod openapi; pub mod openapi;
pub use cache::{Cache, CoversConfig, new_cache}; pub use cache::{new_cache, Cache, CoversConfig};
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
pub use openapi::ApiDoc; pub use openapi::ApiDoc;
#[cfg(feature = "pmoserver")]
use utoipa::OpenApi;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
use std::sync::Arc; use std::sync::Arc;
#[cfg(feature = "pmoserver")]
use utoipa::OpenApi;
/// Générateur de variantes d'images /// Générateur de variantes d'images
/// ///
@@ -64,7 +64,13 @@ fn create_variant_generator() -> pmocache::pmoserver_ext::ParamGenerator<CoversC
match webp::generate_variant(&cache, &pk, size).await { match webp::generate_variant(&cache, &pk, size).await {
Ok(data) => return Some(data), Ok(data) => return Some(data),
Err(e) => { Err(e) => {
tracing::warn!("Cannot generate variant {}x{} for {}: {}", size, size, pk, e); tracing::warn!(
"Cannot generate variant {}x{} for {}: {}",
size,
size,
pk,
e
);
return None; return None;
} }
} }
@@ -98,21 +104,26 @@ pub trait CoverCacheExt {
/// - `DELETE /api/covers/{pk}` - Supprimer une image (API REST) /// - `DELETE /api/covers/{pk}` - Supprimer une image (API REST)
/// - `GET /api/covers/{pk}/status` - Statut du téléchargement /// - `GET /api/covers/{pk}/status` - Statut du téléchargement
/// - `GET /swagger-ui/covers` - Documentation interactive /// - `GET /swagger-ui/covers` - Documentation interactive
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize) async fn init_cover_cache(
-> anyhow::Result<Arc<Cache>>; &mut self,
cache_dir: &str,
limit: usize,
) -> anyhow::Result<Arc<Cache>>;
/// Initialise le cache d'images avec la configuration par défaut /// Initialise le cache d'images avec la configuration par défaut
/// ///
/// Utilise automatiquement les paramètres de `pmoconfig::Config` /// Utilise automatiquement les paramètres de `pmoconfig::Config`
async fn init_cover_cache_configured(&mut self) async fn init_cover_cache_configured(&mut self) -> anyhow::Result<Arc<Cache>>;
-> anyhow::Result<Arc<Cache>>;
} }
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
impl CoverCacheExt for pmoserver::Server { impl CoverCacheExt for pmoserver::Server {
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize) async fn init_cover_cache(
-> anyhow::Result<Arc<Cache>> { &mut self,
use pmocache::pmoserver_ext::{create_file_router_with_generator, create_api_router}; cache_dir: &str,
limit: usize,
) -> anyhow::Result<Arc<Cache>> {
use pmocache::pmoserver_ext::{create_api_router, create_file_router_with_generator};
let cache = Arc::new(cache::new_cache(cache_dir, limit)?); let cache = Arc::new(cache::new_cache(cache_dir, limit)?);
@@ -121,7 +132,7 @@ impl CoverCacheExt for pmoserver::Server {
let file_router = create_file_router_with_generator( let file_router = create_file_router_with_generator(
cache.clone(), cache.clone(),
"image/webp", "image/webp",
Some(create_variant_generator()) Some(create_variant_generator()),
); );
self.add_router("/", file_router).await; self.add_router("/", file_router).await;
@@ -134,8 +145,7 @@ impl CoverCacheExt for pmoserver::Server {
Ok(cache) Ok(cache)
} }
async fn init_cover_cache_configured(&mut self) async fn init_cover_cache_configured(&mut self) -> anyhow::Result<Arc<Cache>> {
-> anyhow::Result<Arc<Cache>> {
let config = pmoconfig::get_config(); let config = pmoconfig::get_config();
let cache_dir = config.get_cover_cache_dir()?; let cache_dir = config.get_cover_cache_dir()?;
let limit = config.get_cover_cache_size()?; let limit = config.get_cover_cache_size()?;

View File

@@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use image::{DynamicImage, imageops::FilterType}; use image::{imageops::FilterType, DynamicImage};
use webp::{Encoder, WebPMemory}; use webp::{Encoder, WebPMemory};
pub fn encode_webp(img: &DynamicImage) -> Result<Vec<u8>> { pub fn encode_webp(img: &DynamicImage) -> Result<Vec<u8>> {
@@ -11,34 +11,38 @@ pub fn encode_webp(img: &DynamicImage) -> Result<Vec<u8>> {
pub fn ensure_square(img: &DynamicImage, size: u32) -> DynamicImage { pub fn ensure_square(img: &DynamicImage, size: u32) -> DynamicImage {
let (width, height) = (img.width(), img.height()); let (width, height) = (img.width(), img.height());
// Calculer le ratio de mise à l'échelle // Calculer le ratio de mise à l'échelle
let scale = if width > height { let scale = if width > height {
size as f32 / width as f32 size as f32 / width as f32
} else { } else {
size as f32 / height as f32 size as f32 / height as f32
}; };
let new_width = (width as f32 * scale) as u32; let new_width = (width as f32 * scale) as u32;
let new_height = (height as f32 * scale) as u32; let new_height = (height as f32 * scale) as u32;
// Redimensionner l'image // Redimensionner l'image
let resized = img.resize(new_width, new_height, FilterType::Lanczos3); let resized = img.resize(new_width, new_height, FilterType::Lanczos3);
// Créer une image carrée avec fond transparent // Créer une image carrée avec fond transparent
let mut square = DynamicImage::new_rgba8(size, size); let mut square = DynamicImage::new_rgba8(size, size);
// Calculer la position pour centrer l'image redimensionnée // Calculer la position pour centrer l'image redimensionnée
let x = (size - new_width) / 2; let x = (size - new_width) / 2;
let y = (size - new_height) / 2; let y = (size - new_height) / 2;
// Copier l'image redimensionnée au centre du carré // Copier l'image redimensionnée au centre du carré
image::imageops::overlay(&mut square, &resized, x.into(), y.into()); image::imageops::overlay(&mut square, &resized, x.into(), y.into());
square square
} }
pub async fn generate_variant(cache: &super::cache::Cache, pk: &str, size: usize) -> Result<Vec<u8>> { pub async fn generate_variant(
cache: &super::cache::Cache,
pk: &str,
size: usize,
) -> Result<Vec<u8>> {
// Utiliser file_path_with_qualifier pour obtenir le chemin // Utiliser file_path_with_qualifier pour obtenir le chemin
let variant_path = cache.file_path_with_qualifier(pk, &size.to_string()); let variant_path = cache.file_path_with_qualifier(pk, &size.to_string());
@@ -49,10 +53,7 @@ pub async fn generate_variant(cache: &super::cache::Cache, pk: &str, size: usize
let orig_path = cache.file_path_with_qualifier(pk, "orig"); let orig_path = cache.file_path_with_qualifier(pk, "orig");
// Charger l'image de manière synchrone (image::open n'est pas async) // Charger l'image de manière synchrone (image::open n'est pas async)
let img = tokio::task::spawn_blocking(move || { let img = tokio::task::spawn_blocking(move || image::open(orig_path)).await??;
image::open(orig_path)
})
.await??;
let square = ensure_square(&img, size as u32); let square = ensure_square(&img, size as u32);
let webp_data = encode_webp(&square)?; let webp_data = encode_webp(&square)?;

View File

@@ -2,19 +2,19 @@
//! //!
//! Parser et utilitaires pour le format DIDL-Lite utilisé dans UPnP/DLNA. //! Parser et utilitaires pour le format DIDL-Lite utilisé dans UPnP/DLNA.
use bevy_reflect::Reflect;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Write; use std::fmt::Write;
use bevy_reflect::Reflect;
// ============= Couche d'abstraction générique ============= // ============= Couche d'abstraction générique =============
/// Trait pour tout parser de métadonnées média /// Trait pour tout parser de métadonnées média
pub trait MediaMetadataParser: Sized { pub trait MediaMetadataParser: Sized {
type Error: std::error::Error + Send + Sync + 'static; type Error: std::error::Error + Send + Sync + 'static;
/// Parse une chaîne de métadonnées /// Parse une chaîne de métadonnées
fn parse(input: &str) -> Result<Self, Self::Error>; fn parse(input: &str) -> Result<Self, Self::Error>;
/// Retourne le format du parser /// Retourne le format du parser
fn format_name() -> &'static str; fn format_name() -> &'static str;
} }
@@ -24,10 +24,10 @@ pub trait MediaMetadataParser: Sized {
pub struct ParsedMetadata<T> { pub struct ParsedMetadata<T> {
/// Format du document (ex: "DIDL-Lite", "RSS", etc.) /// Format du document (ex: "DIDL-Lite", "RSS", etc.)
pub format: String, pub format: String,
/// Données parsées /// Données parsées
pub data: T, pub data: T,
/// Timestamp du parsing (exclu de la réflexion car SystemTime n'implémente pas Reflect) /// Timestamp du parsing (exclu de la réflexion car SystemTime n'implémente pas Reflect)
#[reflect(ignore)] #[reflect(ignore)]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -42,7 +42,7 @@ impl<T> ParsedMetadata<T> {
parsed_at: Some(std::time::SystemTime::now()), parsed_at: Some(std::time::SystemTime::now()),
} }
} }
/// Transforme les données avec une fonction /// Transforme les données avec une fonction
pub fn map<U, F>(self, f: F) -> ParsedMetadata<U> pub fn map<U, F>(self, f: F) -> ParsedMetadata<U>
where where
@@ -66,11 +66,11 @@ pub fn parse_metadata<P: MediaMetadataParser>(input: &str) -> Result<ParsedMetad
impl MediaMetadataParser for DIDLLite { impl MediaMetadataParser for DIDLLite {
type Error = quick_xml::de::DeError; type Error = quick_xml::de::DeError;
fn parse(input: &str) -> Result<Self, Self::Error> { fn parse(input: &str) -> Result<Self, Self::Error> {
quick_xml::de::from_str(input) quick_xml::de::from_str(input)
} }
fn format_name() -> &'static str { fn format_name() -> &'static str {
"DIDL-Lite" "DIDL-Lite"
} }
@@ -81,32 +81,31 @@ pub type DidlMetadata = ParsedMetadata<DIDLLite>;
// ============= Structures DIDL-Lite ============= // ============= Structures DIDL-Lite =============
/// Racine d'un document DIDL-Lite /// Racine d'un document DIDL-Lite
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)]
#[serde(rename = "DIDL-Lite")] #[serde(rename = "DIDL-Lite")]
pub struct DIDLLite { pub struct DIDLLite {
#[serde(rename = "@xmlns")] #[serde(rename = "@xmlns")]
pub xmlns: String, pub xmlns: String,
#[serde(rename = "@xmlns:upnp", skip_serializing_if = "Option::is_none")] #[serde(rename = "@xmlns:upnp", skip_serializing_if = "Option::is_none")]
pub xmlns_upnp: Option<String>, pub xmlns_upnp: Option<String>,
#[serde(rename = "@xmlns:dc", skip_serializing_if = "Option::is_none")] #[serde(rename = "@xmlns:dc", skip_serializing_if = "Option::is_none")]
pub xmlns_dc: Option<String>, pub xmlns_dc: Option<String>,
#[serde(rename = "@xmlns:dlna", skip_serializing_if = "Option::is_none")] #[serde(rename = "@xmlns:dlna", skip_serializing_if = "Option::is_none")]
pub xmlns_dlna: Option<String>, pub xmlns_dlna: Option<String>,
#[serde(rename = "@xmlns:sec", skip_serializing_if = "Option::is_none")] #[serde(rename = "@xmlns:sec", skip_serializing_if = "Option::is_none")]
pub xmlns_sec: Option<String>, pub xmlns_sec: Option<String>,
#[serde(rename = "@xmlns:pv", skip_serializing_if = "Option::is_none")] #[serde(rename = "@xmlns:pv", skip_serializing_if = "Option::is_none")]
pub xmlns_pv: Option<String>, pub xmlns_pv: Option<String>,
#[serde(rename = "container", default)] #[serde(rename = "container", default)]
pub containers: Vec<Container>, pub containers: Vec<Container>,
#[serde(rename = "item", default)] #[serde(rename = "item", default)]
pub items: Vec<Item>, pub items: Vec<Item>,
} }
@@ -116,25 +115,25 @@ pub struct DIDLLite {
pub struct Container { pub struct Container {
#[serde(rename = "@id")] #[serde(rename = "@id")]
pub id: String, pub id: String,
#[serde(rename = "@parentID")] #[serde(rename = "@parentID")]
pub parent_id: String, pub parent_id: String,
#[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")] #[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")]
pub restricted: Option<String>, pub restricted: Option<String>,
#[serde(rename = "@childCount", skip_serializing_if = "Option::is_none")] #[serde(rename = "@childCount", skip_serializing_if = "Option::is_none")]
pub child_count: Option<String>, pub child_count: Option<String>,
#[serde(rename = "dc:title", alias = "title")] #[serde(rename = "dc:title", alias = "title")]
pub title: String, pub title: String,
#[serde(rename = "upnp:class", alias = "class")] #[serde(rename = "upnp:class", alias = "class")]
pub class: String, pub class: String,
#[serde(rename = "container", default)] #[serde(rename = "container", default)]
pub containers: Vec<Container>, pub containers: Vec<Container>,
#[serde(rename = "item", default)] #[serde(rename = "item", default)]
pub items: Vec<Item>, pub items: Vec<Item>,
} }
@@ -144,46 +143,74 @@ pub struct Container {
pub struct Item { pub struct Item {
#[serde(rename = "@id")] #[serde(rename = "@id")]
pub id: String, pub id: String,
#[serde(rename = "@parentID")] #[serde(rename = "@parentID")]
pub parent_id: String, pub parent_id: String,
#[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")] #[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")]
pub restricted: Option<String>, pub restricted: Option<String>,
#[serde(rename = "dc:title", alias = "title")] #[serde(rename = "dc:title", alias = "title")]
pub title: String, pub title: String,
#[serde(rename = "dc:creator", alias = "creator", skip_serializing_if = "Option::is_none")] #[serde(
rename = "dc:creator",
alias = "creator",
skip_serializing_if = "Option::is_none"
)]
pub creator: Option<String>, pub creator: Option<String>,
#[serde(rename = "upnp:class", alias = "class")] #[serde(rename = "upnp:class", alias = "class")]
pub class: String, pub class: String,
#[serde(rename = "upnp:artist", alias = "artist", skip_serializing_if = "Option::is_none")] #[serde(
rename = "upnp:artist",
alias = "artist",
skip_serializing_if = "Option::is_none"
)]
pub artist: Option<String>, pub artist: Option<String>,
#[serde(rename = "upnp:album", alias = "album", skip_serializing_if = "Option::is_none")] #[serde(
rename = "upnp:album",
alias = "album",
skip_serializing_if = "Option::is_none"
)]
pub album: Option<String>, pub album: Option<String>,
#[serde(rename = "upnp:genre", alias = "genre", skip_serializing_if = "Option::is_none")] #[serde(
rename = "upnp:genre",
alias = "genre",
skip_serializing_if = "Option::is_none"
)]
pub genre: Option<String>, pub genre: Option<String>,
#[serde(rename = "upnp:albumArtURI", alias = "albumArtURI", skip_serializing_if = "Option::is_none")] #[serde(
rename = "upnp:albumArtURI",
alias = "albumArtURI",
skip_serializing_if = "Option::is_none"
)]
pub album_art: Option<String>, pub album_art: Option<String>,
#[serde(skip)] #[serde(skip)]
pub album_art_pk: Option<String>, pub album_art_pk: Option<String>,
#[serde(rename = "dc:date", alias = "date", skip_serializing_if = "Option::is_none")] #[serde(
rename = "dc:date",
alias = "date",
skip_serializing_if = "Option::is_none"
)]
pub date: Option<String>, pub date: Option<String>,
#[serde(rename = "upnp:originalTrackNumber", alias = "originalTrackNumber", skip_serializing_if = "Option::is_none")] #[serde(
rename = "upnp:originalTrackNumber",
alias = "originalTrackNumber",
skip_serializing_if = "Option::is_none"
)]
pub original_track_number: Option<String>, pub original_track_number: Option<String>,
#[serde(rename = "res", default)] #[serde(rename = "res", default)]
pub resources: Vec<Resource>, pub resources: Vec<Resource>,
#[serde(rename = "desc", default)] #[serde(rename = "desc", default)]
pub descriptions: Vec<Description>, pub descriptions: Vec<Description>,
} }
@@ -193,19 +220,19 @@ pub struct Item {
pub struct Resource { pub struct Resource {
#[serde(rename = "@protocolInfo")] #[serde(rename = "@protocolInfo")]
pub protocol_info: String, pub protocol_info: String,
#[serde(rename = "@bitsPerSample", skip_serializing_if = "Option::is_none")] #[serde(rename = "@bitsPerSample", skip_serializing_if = "Option::is_none")]
pub bits_per_sample: Option<String>, pub bits_per_sample: Option<String>,
#[serde(rename = "@sampleFrequency", skip_serializing_if = "Option::is_none")] #[serde(rename = "@sampleFrequency", skip_serializing_if = "Option::is_none")]
pub sample_frequency: Option<String>, pub sample_frequency: Option<String>,
#[serde(rename = "@nrAudioChannels", skip_serializing_if = "Option::is_none")] #[serde(rename = "@nrAudioChannels", skip_serializing_if = "Option::is_none")]
pub nr_audio_channels: Option<String>, pub nr_audio_channels: Option<String>,
#[serde(rename = "@duration", skip_serializing_if = "Option::is_none")] #[serde(rename = "@duration", skip_serializing_if = "Option::is_none")]
pub duration: Option<String>, pub duration: Option<String>,
#[serde(rename = "$text")] #[serde(rename = "$text")]
pub url: String, pub url: String,
} }
@@ -215,13 +242,13 @@ pub struct Resource {
pub struct Description { pub struct Description {
#[serde(rename = "@id", skip_serializing_if = "Option::is_none")] #[serde(rename = "@id", skip_serializing_if = "Option::is_none")]
pub id: Option<String>, pub id: Option<String>,
#[serde(rename = "@nameSpace", skip_serializing_if = "Option::is_none")] #[serde(rename = "@nameSpace", skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>, pub namespace: Option<String>,
#[serde(rename = "track_gain", skip_serializing_if = "Option::is_none")] #[serde(rename = "track_gain", skip_serializing_if = "Option::is_none")]
pub track_gain: Option<String>, pub track_gain: Option<String>,
#[serde(rename = "track_peak", skip_serializing_if = "Option::is_none")] #[serde(rename = "track_peak", skip_serializing_if = "Option::is_none")]
pub track_peak: Option<String>, pub track_peak: Option<String>,
} }
@@ -233,22 +260,22 @@ impl DIDLLite {
pub fn all_containers(&self) -> impl Iterator<Item = &Container> { pub fn all_containers(&self) -> impl Iterator<Item = &Container> {
AllContainersIter::new(&self.containers) AllContainersIter::new(&self.containers)
} }
/// Itère sur tous les items de manière récursive /// Itère sur tous les items de manière récursive
pub fn all_items(&self) -> impl Iterator<Item = &Item> { pub fn all_items(&self) -> impl Iterator<Item = &Item> {
AllItemsIter::new(&self.containers, &self.items) AllItemsIter::new(&self.containers, &self.items)
} }
/// Trouve un container par ID /// Trouve un container par ID
pub fn get_container_by_id(&self, id: &str) -> Option<&Container> { pub fn get_container_by_id(&self, id: &str) -> Option<&Container> {
self.all_containers().find(|c| c.id == id) self.all_containers().find(|c| c.id == id)
} }
/// Trouve un item par ID /// Trouve un item par ID
pub fn get_item_by_id(&self, id: &str) -> Option<&Item> { pub fn get_item_by_id(&self, id: &str) -> Option<&Item> {
self.all_items().find(|i| i.id == id) self.all_items().find(|i| i.id == id)
} }
/// Filtre les containers /// Filtre les containers
pub fn filter_containers<F>(&self, predicate: F) -> impl Iterator<Item = &Container> pub fn filter_containers<F>(&self, predicate: F) -> impl Iterator<Item = &Container>
where where
@@ -256,7 +283,7 @@ impl DIDLLite {
{ {
self.all_containers().filter(move |c| predicate(c)) self.all_containers().filter(move |c| predicate(c))
} }
/// Filtre les items /// Filtre les items
pub fn filter_items<F>(&self, predicate: F) -> impl Iterator<Item = &Item> pub fn filter_items<F>(&self, predicate: F) -> impl Iterator<Item = &Item>
where where
@@ -264,26 +291,26 @@ impl DIDLLite {
{ {
self.all_items().filter(move |i| predicate(i)) self.all_items().filter(move |i| predicate(i))
} }
/// Génère une représentation Markdown /// Génère une représentation Markdown
pub fn to_markdown(&self) -> String { pub fn to_markdown(&self) -> String {
let mut buf = String::new(); let mut buf = String::new();
buf.push_str("### DIDL-Lite Document\n\n"); buf.push_str("### DIDL-Lite Document\n\n");
if !self.containers.is_empty() { if !self.containers.is_empty() {
buf.push_str("#### Containers\n\n"); buf.push_str("#### Containers\n\n");
for container in &self.containers { for container in &self.containers {
container.write_markdown(&mut buf, 0); container.write_markdown(&mut buf, 0);
} }
} }
if !self.items.is_empty() { if !self.items.is_empty() {
buf.push_str("#### Items\n\n"); buf.push_str("#### Items\n\n");
for item in &self.items { for item in &self.items {
item.write_markdown(&mut buf, 0); item.write_markdown(&mut buf, 0);
} }
} }
buf buf
} }
} }
@@ -293,41 +320,41 @@ impl Container {
pub fn all_containers(&self) -> impl Iterator<Item = &Container> { pub fn all_containers(&self) -> impl Iterator<Item = &Container> {
AllContainersIter::new(&self.containers) AllContainersIter::new(&self.containers)
} }
/// Itère sur tous les items de ce container et ses enfants /// Itère sur tous les items de ce container et ses enfants
pub fn all_items(&self) -> impl Iterator<Item = &Item> { pub fn all_items(&self) -> impl Iterator<Item = &Item> {
AllItemsIter::new(&self.containers, &self.items) AllItemsIter::new(&self.containers, &self.items)
} }
fn write_markdown(&self, buf: &mut String, depth: usize) { fn write_markdown(&self, buf: &mut String, depth: usize) {
let indent = " ".repeat(depth); let indent = " ".repeat(depth);
writeln!(buf, "{}- **Container**: {}", indent, self.title).unwrap(); writeln!(buf, "{}- **Container**: {}", indent, self.title).unwrap();
writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap(); writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap();
writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap(); writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap();
writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap(); writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap();
if let Some(ref restricted) = self.restricted { if let Some(ref restricted) = self.restricted {
writeln!(buf, "{} - Restricted: `{}`", indent, restricted).unwrap(); writeln!(buf, "{} - Restricted: `{}`", indent, restricted).unwrap();
} }
if let Some(ref count) = self.child_count { if let Some(ref count) = self.child_count {
writeln!(buf, "{} - ChildCount: `{}`", indent, count).unwrap(); writeln!(buf, "{} - ChildCount: `{}`", indent, count).unwrap();
} }
if !self.containers.is_empty() { if !self.containers.is_empty() {
writeln!(buf, "{} - Subcontainers:", indent).unwrap(); writeln!(buf, "{} - Subcontainers:", indent).unwrap();
for sub in &self.containers { for sub in &self.containers {
sub.write_markdown(buf, depth + 2); sub.write_markdown(buf, depth + 2);
} }
} }
if !self.items.is_empty() { if !self.items.is_empty() {
writeln!(buf, "{} - Items:", indent).unwrap(); writeln!(buf, "{} - Items:", indent).unwrap();
for item in &self.items { for item in &self.items {
item.write_markdown(buf, depth + 2); item.write_markdown(buf, depth + 2);
} }
} }
buf.push('\n'); buf.push('\n');
} }
} }
@@ -335,21 +362,22 @@ impl Container {
impl Item { impl Item {
/// Itère sur les ressources audio uniquement /// Itère sur les ressources audio uniquement
pub fn audio_resources(&self) -> impl Iterator<Item = &Resource> { pub fn audio_resources(&self) -> impl Iterator<Item = &Resource> {
self.resources.iter() self.resources
.iter()
.filter(|r| r.protocol_info.contains("audio/")) .filter(|r| r.protocol_info.contains("audio/"))
} }
/// Retourne la ressource principale (première disponible) /// Retourne la ressource principale (première disponible)
pub fn primary_resource(&self) -> Option<&Resource> { pub fn primary_resource(&self) -> Option<&Resource> {
self.resources.first() self.resources.first()
} }
/// Itère sur les métadonnées sous forme de paires clé-valeur /// Itère sur les métadonnées sous forme de paires clé-valeur
pub fn metadata(&self) -> impl Iterator<Item = (&str, &str)> { pub fn metadata(&self) -> impl Iterator<Item = (&str, &str)> {
let mut pairs = Vec::new(); let mut pairs = Vec::new();
pairs.push(("title", self.title.as_str())); pairs.push(("title", self.title.as_str()));
if let Some(ref artist) = self.artist { if let Some(ref artist) = self.artist {
pairs.push(("artist", artist.as_str())); pairs.push(("artist", artist.as_str()));
} }
@@ -365,7 +393,7 @@ impl Item {
if let Some(ref track) = self.original_track_number { if let Some(ref track) = self.original_track_number {
pairs.push(("trackNumber", track.as_str())); pairs.push(("trackNumber", track.as_str()));
} }
for desc in &self.descriptions { for desc in &self.descriptions {
if let Some(ref gain) = desc.track_gain { if let Some(ref gain) = desc.track_gain {
pairs.push(("replayGain", gain.as_str())); pairs.push(("replayGain", gain.as_str()));
@@ -374,18 +402,18 @@ impl Item {
pairs.push(("replayPeak", peak.as_str())); pairs.push(("replayPeak", peak.as_str()));
} }
} }
pairs.into_iter() pairs.into_iter()
} }
fn write_markdown(&self, buf: &mut String, depth: usize) { fn write_markdown(&self, buf: &mut String, depth: usize) {
let indent = " ".repeat(depth); let indent = " ".repeat(depth);
writeln!(buf, "{}- **Item**: {}", indent, self.title).unwrap(); writeln!(buf, "{}- **Item**: {}", indent, self.title).unwrap();
writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap(); writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap();
writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap(); writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap();
writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap(); writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap();
if let Some(ref creator) = self.creator { if let Some(ref creator) = self.creator {
writeln!(buf, "{} - Creator: {}", indent, creator).unwrap(); writeln!(buf, "{} - Creator: {}", indent, creator).unwrap();
} }
@@ -407,7 +435,7 @@ impl Item {
if let Some(ref track) = self.original_track_number { if let Some(ref track) = self.original_track_number {
writeln!(buf, "{} - Track: {}", indent, track).unwrap(); writeln!(buf, "{} - Track: {}", indent, track).unwrap();
} }
if !self.resources.is_empty() { if !self.resources.is_empty() {
writeln!(buf, "{} - Resources:", indent).unwrap(); writeln!(buf, "{} - Resources:", indent).unwrap();
for res in &self.resources { for res in &self.resources {
@@ -427,7 +455,7 @@ impl Item {
} }
} }
} }
if !self.descriptions.is_empty() { if !self.descriptions.is_empty() {
writeln!(buf, "{} - Descriptions:", indent).unwrap(); writeln!(buf, "{} - Descriptions:", indent).unwrap();
for desc in &self.descriptions { for desc in &self.descriptions {
@@ -442,7 +470,7 @@ impl Item {
} }
} }
} }
buf.push('\n'); buf.push('\n');
} }
} }
@@ -463,7 +491,7 @@ impl<'a> AllContainersIter<'a> {
impl<'a> Iterator for AllContainersIter<'a> { impl<'a> Iterator for AllContainersIter<'a> {
type Item = &'a Container; type Item = &'a Container;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
self.stack.pop().map(|container| { self.stack.pop().map(|container| {
// Ajouter les enfants à la pile // Ajouter les enfants à la pile
@@ -489,13 +517,13 @@ impl<'a> AllItemsIter<'a> {
impl<'a> Iterator for AllItemsIter<'a> { impl<'a> Iterator for AllItemsIter<'a> {
type Item = &'a Item; type Item = &'a Item;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
loop { loop {
if let Some(item) = self.current_items.next() { if let Some(item) = self.current_items.next() {
return Some(item); return Some(item);
} }
let container = self.containers.pop()?; let container = self.containers.pop()?;
self.containers.extend(container.containers.iter()); self.containers.extend(container.containers.iter());
self.current_items = container.items.iter(); self.current_items = container.items.iter();
@@ -506,7 +534,7 @@ impl<'a> Iterator for AllItemsIter<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_parse_simple_didl() { fn test_parse_simple_didl() {
let xml = r#" let xml = r#"
@@ -520,12 +548,12 @@ mod tests {
</item> </item>
</DIDL-Lite> </DIDL-Lite>
"#; "#;
let didl = DIDLLite::parse(xml).unwrap(); let didl = DIDLLite::parse(xml).unwrap();
assert_eq!(didl.items.len(), 1); assert_eq!(didl.items.len(), 1);
assert_eq!(didl.items[0].title, "Test Song"); assert_eq!(didl.items[0].title, "Test Song");
} }
#[test] #[test]
fn test_parse_without_namespaces() { fn test_parse_without_namespaces() {
// Teste un XML sans namespaces explicites (devices UPnP laxistes) // Teste un XML sans namespaces explicites (devices UPnP laxistes)
@@ -538,12 +566,12 @@ mod tests {
</item> </item>
</DIDL-Lite> </DIDL-Lite>
"#; "#;
let didl = DIDLLite::parse(xml).unwrap(); let didl = DIDLLite::parse(xml).unwrap();
assert_eq!(didl.items.len(), 1); assert_eq!(didl.items.len(), 1);
assert_eq!(didl.items[0].title, "Test Song"); assert_eq!(didl.items[0].title, "Test Song");
} }
#[test] #[test]
fn test_generic_parser() { fn test_generic_parser() {
let xml = r#" let xml = r#"
@@ -552,14 +580,14 @@ mod tests {
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"> xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
</DIDL-Lite> </DIDL-Lite>
"#; "#;
// Utiliser le parser générique // Utiliser le parser générique
let metadata: DidlMetadata = parse_metadata(xml).unwrap(); let metadata: DidlMetadata = parse_metadata(xml).unwrap();
assert_eq!(metadata.format, "DIDL-Lite"); assert_eq!(metadata.format, "DIDL-Lite");
assert!(metadata.parsed_at.is_some()); assert!(metadata.parsed_at.is_some());
} }
#[test] #[test]
fn test_metadata_map() { fn test_metadata_map() {
let xml = r#" let xml = r#"
@@ -568,13 +596,13 @@ mod tests {
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"> xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
</DIDL-Lite> </DIDL-Lite>
"#; "#;
let metadata: DidlMetadata = parse_metadata(xml).unwrap(); let metadata: DidlMetadata = parse_metadata(xml).unwrap();
// Transformer les données // Transformer les données
let item_count = metadata.map(|didl| didl.items.len()); let item_count = metadata.map(|didl| didl.items.len());
assert_eq!(item_count.format, "DIDL-Lite"); assert_eq!(item_count.format, "DIDL-Lite");
assert_eq!(item_count.data, 0); assert_eq!(item_count.data, 0);
} }
} }

View File

@@ -1,4 +1,7 @@
use crate::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, NUMBEROFTRACKS, CURRENTTRACK, AVTRANSPORTURI, AVTRANSPORTURIMETADATA, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA}; use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI,
AVTRANSPORTURIMETADATA, CURRENTTRACK, NUMBEROFTRACKS,
};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -1,4 +1,7 @@
use crate::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, CURRENTTRACK, CURRENTTRACKDURATION, AVTRANSPORTURI, AVTRANSPORTURIMETADATA, RELATIVETIMEPOSITION, ABSOLUTETIMEPOSITION}; use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, ABSOLUTETIMEPOSITION, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
CURRENTTRACK, CURRENTTRACKDURATION, RELATIVETIMEPOSITION,
};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -27,4 +27,3 @@ pub use seek::SEEK;
pub use setavtransportnexturi::SETNEXTAVTRANSPORTURI; pub use setavtransportnexturi::SETNEXTAVTRANSPORTURI;
pub use setavtransporturi::SETAVTRANSPORTURI; pub use setavtransporturi::SETAVTRANSPORTURI;
pub use stop::STOP; pub use stop::STOP;

View File

@@ -1,6 +1,6 @@
use crate::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED}; use crate::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {
pub static PLAY = "Play" { pub static PLAY = "Play" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,

View File

@@ -1,4 +1,6 @@
use crate::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_SEEKMODE, CURRENTTRACKDURATION}; use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_SEEKMODE, CURRENTTRACKDURATION,
};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -1,4 +1,6 @@
use crate::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA}; use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA,
};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -1,4 +1,6 @@
use crate::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTURI, AVTRANSPORTURIMETADATA}; use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -91,22 +91,21 @@
use pmoupnp::define_service; use pmoupnp::define_service;
pub mod variables;
pub mod actions; pub mod actions;
pub mod variables;
use actions::{ use actions::{
GETCURRENTTRANSPORTACTIONS, GETDEVICECAPABILITIES, GETMEDIAINFO, GETCURRENTTRANSPORTACTIONS, GETDEVICECAPABILITIES, GETMEDIAINFO, GETPOSITIONINFO,
GETPOSITIONINFO, GETTRANSPORTINFO, GETTRANSPORTSETTINGS, NEXT, PAUSE, GETTRANSPORTINFO, GETTRANSPORTSETTINGS, NEXT, PAUSE, PLAY, PREVIOUS, SEEK, SETAVTRANSPORTURI,
PLAY, PREVIOUS, SEEK, SETNEXTAVTRANSPORTURI, SETAVTRANSPORTURI, STOP SETNEXTAVTRANSPORTURI, STOP,
}; };
use variables::{ use variables::{
ABSOLUTETIMEPOSITION, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_PLAY_SPEED, A_ARG_TYPE_SEEKMODE, ABSOLUTETIMEPOSITION,
AVTRANSPORTURI, AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
A_ARG_TYPE_PLAY_SPEED, A_ARG_TYPE_SEEKMODE, CURRENTMEDIADURATION, CURRENTMEDIADURATION, CURRENTPLAYMODE, CURRENTTRACK, CURRENTTRACKDURATION,
CURRENTPLAYMODE, CURRENTTRACK, CURRENTTRACKDURATION, CURRENTTRACKMETADATA, CURRENTTRACKMETADATA, CURRENTTRACKURI, NUMBEROFTRACKS, PLAYBACKSTORAGEMEDIUM,
CURRENTTRACKURI, NUMBEROFTRACKS, PLAYBACKSTORAGEMEDIUM, POSSIBLEPLAYBACKSTORAGEMEDIA, RELATIVETIMEPOSITION, SEEKMODE, TRANSPORTPLAYSPEED,
POSSIBLEPLAYBACKSTORAGEMEDIA, RELATIVETIMEPOSITION, SEEKMODE, TRANSPORTSTATE, TRANSPORTSTATUS,
TRANSPORTPLAYSPEED, TRANSPORTSTATE, TRANSPORTSTATUS
}; };
// Service AVTransport:1 conforme à la spécification UPnP AV pour MediaRenderer audio // Service AVTransport:1 conforme à la spécification UPnP AV pour MediaRenderer audio
@@ -154,4 +153,4 @@ define_service! {
STOP, STOP,
] ]
} }
} }

View File

@@ -1,31 +1,37 @@
use std::sync::Arc; use std::sync::Arc;
use pmoupnp::state_variables::{StateVariable, StateVariableError};
use pmoupnp::variable_types::StateVarType;
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pmodidl::{DIDLLite, MediaMetadataParser}; use pmodidl::{DIDLLite, MediaMetadataParser};
use pmoupnp::state_variables::{StateVariable, StateVariableError};
use pmoupnp::variable_types::StateVarType;
fn avtransporturimetadataparser(value: &str) -> Result<Box<dyn Reflect>, StateVariableError> { fn avtransporturimetadataparser(value: &str) -> Result<Box<dyn Reflect>, StateVariableError> {
// Parse DIDL-Lite // Parse DIDL-Lite
let didl = DIDLLite::parse(value) let didl = DIDLLite::parse(value)
.map_err(|e| StateVariableError::ParseError(format!("Failed to parse DIDL-Lite: {}", e)))?; .map_err(|e| StateVariableError::ParseError(format!("Failed to parse DIDL-Lite: {}", e)))?;
// Retourne le résultat sous forme de Box<dyn Reflect> // Retourne le résultat sous forme de Box<dyn Reflect>
Ok(Box::new(didl) as Box<dyn Reflect>) Ok(Box::new(didl) as Box<dyn Reflect>)
} }
pub static AVTRANSPORTURIMETADATA: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> { pub static AVTRANSPORTURIMETADATA: Lazy<Arc<StateVariable>> =
let mut sv = StateVariable::new(StateVarType::String, "AVTransportURIMetaData".to_string()); Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(StateVarType::String, "AVTransportURIMetaData".to_string());
sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser"); sv.set_value_parser(Arc::new(avtransporturimetadataparser))
Arc::new(sv) .expect("Failed to set parser");
}); Arc::new(sv)
});
pub static AVTRANSPORTNEXTURIMETADATA: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> { pub static AVTRANSPORTNEXTURIMETADATA: Lazy<Arc<StateVariable>> =
let mut sv = StateVariable::new(StateVarType::String, "AVTransportNextURIMetaData".to_string()); Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(
StateVarType::String,
"AVTransportNextURIMetaData".to_string(),
);
sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser"); sv.set_value_parser(Arc::new(avtransporturimetadataparser))
Arc::new(sv) .expect("Failed to set parser");
}); Arc::new(sv)
});

View File

@@ -21,10 +21,10 @@ mod transportstatus;
pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID; pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID;
pub use a_arg_type_playspeed::A_ARG_TYPE_PLAY_SPEED; pub use a_arg_type_playspeed::A_ARG_TYPE_PLAY_SPEED;
pub use a_arg_type_seekmode::A_ARG_TYPE_SEEKMODE; pub use a_arg_type_seekmode::A_ARG_TYPE_SEEKMODE;
pub use avtransporturi::AVTRANSPORTURI;
pub use avtransporturi::AVTRANSPORTNEXTURI; pub use avtransporturi::AVTRANSPORTNEXTURI;
pub use avtransporturimetadata::AVTRANSPORTURIMETADATA; pub use avtransporturi::AVTRANSPORTURI;
pub use avtransporturimetadata::AVTRANSPORTNEXTURIMETADATA; pub use avtransporturimetadata::AVTRANSPORTNEXTURIMETADATA;
pub use avtransporturimetadata::AVTRANSPORTURIMETADATA;
pub use currentmediaduration::CURRENTMEDIADURATION; pub use currentmediaduration::CURRENTMEDIADURATION;
pub use currentplaymode::CURRENTPLAYMODE; pub use currentplaymode::CURRENTPLAYMODE;
pub use currenttrackmetadata::CURRENTTRACKMETADATA; pub use currenttrackmetadata::CURRENTTRACKMETADATA;
@@ -42,6 +42,3 @@ pub use trackduration::RELATIVETIMEPOSITION;
pub use transportplayspeed::TRANSPORTPLAYSPEED; pub use transportplayspeed::TRANSPORTPLAYSPEED;
pub use transportstate::TRANSPORTSTATE; pub use transportstate::TRANSPORTSTATE;
pub use transportstatus::TRANSPORTSTATUS; pub use transportstatus::TRANSPORTSTATUS;

View File

@@ -1,10 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use once_cell::sync::Lazy;
use pmoupnp::state_variables::StateVariable; use pmoupnp::state_variables::StateVariable;
use pmoupnp::variable_types::StateVarType; use pmoupnp::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static POSSIBLERECORDSTORAGEMEDIA: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> { pub static POSSIBLERECORDSTORAGEMEDIA: Lazy<Arc<StateVariable>> =
let sv = StateVariable::new(StateVarType::String, "PossibleRecordStorageMedia".to_string()); Lazy::new(|| -> Arc<StateVariable> {
Arc::new(sv) let sv = StateVariable::new(
}); StateVarType::String,
"PossibleRecordStorageMedia".to_string(),
);
Arc::new(sv)
});

View File

@@ -1,8 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use once_cell::sync::Lazy;
use pmoupnp::state_variables::StateVariable; use pmoupnp::state_variables::StateVariable;
use pmoupnp::variable_types::StateVarType; use pmoupnp::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static RECORDSTORAGEMEDIUM: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> { pub static RECORDSTORAGEMEDIUM: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
let sv = StateVariable::new(StateVarType::String, "RecordStorageMedium".to_string()); let sv = StateVariable::new(StateVarType::String, "RecordStorageMedium".to_string());

View File

@@ -3,4 +3,3 @@ use pmoupnp::define_variable;
define_variable! { define_variable! {
pub static SEEKMODE: String = "SeekMode" pub static SEEKMODE: String = "SeekMode"
} }

View File

@@ -11,4 +11,3 @@ define_variable! {
evented: true, evented: true,
} }
} }

View File

@@ -17,4 +17,3 @@ define_variable! {
evented: true, evented: true,
} }
} }

View File

@@ -6,4 +6,3 @@ define_variable! {
default: "1", default: "1",
} }
} }

View File

@@ -7,4 +7,3 @@ define_variable! {
evented: true, evented: true,
} }
} }

View File

@@ -1,6 +1,6 @@
use crate::connectionmanager::variables::{ use crate::connectionmanager::variables::{
A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_RCSID, A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_CONNECTIONSTATUS,
A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_DIRECTION, A_ARG_TYPE_CONNECTIONSTATUS A_ARG_TYPE_DIRECTION, A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_RCSID,
}; };
use pmoupnp::define_action; use pmoupnp::define_action;

View File

@@ -1,4 +1,4 @@
use crate::connectionmanager::variables::{SOURCEPROTOCOLINFO, SINKPROTOCOLINFO}; use crate::connectionmanager::variables::{SINKPROTOCOLINFO, SOURCEPROTOCOLINFO};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -1,7 +1,7 @@
mod getprotocolinfo;
mod getcurrentconnectionids; mod getcurrentconnectionids;
mod getcurrentconnectioninfo; mod getcurrentconnectioninfo;
mod getprotocolinfo;
pub use getprotocolinfo::GETPROTOCOLINFO;
pub use getcurrentconnectionids::GETCURRENTCONNECTIONIDS; pub use getcurrentconnectionids::GETCURRENTCONNECTIONIDS;
pub use getcurrentconnectioninfo::GETCURRENTCONNECTIONINFO; pub use getcurrentconnectioninfo::GETCURRENTCONNECTIONINFO;
pub use getprotocolinfo::GETPROTOCOLINFO;

View File

@@ -55,14 +55,14 @@
use pmoupnp::define_service; use pmoupnp::define_service;
pub mod variables;
pub mod actions; pub mod actions;
pub mod variables;
use actions::{GETCURRENTCONNECTIONIDS, GETCURRENTCONNECTIONINFO, GETPROTOCOLINFO}; use actions::{GETCURRENTCONNECTIONIDS, GETCURRENTCONNECTIONINFO, GETPROTOCOLINFO};
use variables::{ use variables::{
A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_CONNECTIONSTATUS, A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_CONNECTIONSTATUS,
A_ARG_TYPE_DIRECTION, A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_RCSID, A_ARG_TYPE_DIRECTION, A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_RCSID, CURRENTCONNECTIONIDS,
CURRENTCONNECTIONIDS, SINKPROTOCOLINFO, SOURCEPROTOCOLINFO SINKPROTOCOLINFO, SOURCEPROTOCOLINFO,
}; };
// Service ConnectionManager:1 conforme à la spécification UPnP AV pour MediaRenderer audio // Service ConnectionManager:1 conforme à la spécification UPnP AV pour MediaRenderer audio

View File

@@ -1,19 +1,19 @@
mod sourceprotocolinfo; mod a_arg_type_avtransportid;
mod sinkprotocolinfo;
mod currentconnectionids;
mod a_arg_type_connectionid; mod a_arg_type_connectionid;
mod a_arg_type_connectionstatus; mod a_arg_type_connectionstatus;
mod a_arg_type_direction; mod a_arg_type_direction;
mod a_arg_type_protocolinfo; mod a_arg_type_protocolinfo;
mod a_arg_type_rcsid; mod a_arg_type_rcsid;
mod a_arg_type_avtransportid; mod currentconnectionids;
mod sinkprotocolinfo;
mod sourceprotocolinfo;
pub use sourceprotocolinfo::SOURCEPROTOCOLINFO; pub use a_arg_type_avtransportid::A_ARG_TYPE_AVTRANSPORTID;
pub use sinkprotocolinfo::SINKPROTOCOLINFO;
pub use currentconnectionids::CURRENTCONNECTIONIDS;
pub use a_arg_type_connectionid::A_ARG_TYPE_CONNECTIONID; pub use a_arg_type_connectionid::A_ARG_TYPE_CONNECTIONID;
pub use a_arg_type_connectionstatus::A_ARG_TYPE_CONNECTIONSTATUS; pub use a_arg_type_connectionstatus::A_ARG_TYPE_CONNECTIONSTATUS;
pub use a_arg_type_direction::A_ARG_TYPE_DIRECTION; pub use a_arg_type_direction::A_ARG_TYPE_DIRECTION;
pub use a_arg_type_protocolinfo::A_ARG_TYPE_PROTOCOLINFO; pub use a_arg_type_protocolinfo::A_ARG_TYPE_PROTOCOLINFO;
pub use a_arg_type_rcsid::A_ARG_TYPE_RCSID; pub use a_arg_type_rcsid::A_ARG_TYPE_RCSID;
pub use a_arg_type_avtransportid::A_ARG_TYPE_AVTRANSPORTID; pub use currentconnectionids::CURRENTCONNECTIONIDS;
pub use sinkprotocolinfo::SINKPROTOCOLINFO;
pub use sourceprotocolinfo::SOURCEPROTOCOLINFO;

View File

@@ -3,12 +3,11 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::sync::Arc; use std::sync::Arc;
use pmoupnp::devices::Device;
use crate::{ use crate::{
avtransport::AVTTRANSPORT, avtransport::AVTTRANSPORT, connectionmanager::CONNECTIONMANAGER,
renderingcontrol::RENDERINGCONTROL, renderingcontrol::RENDERINGCONTROL,
connectionmanager::CONNECTIONMANAGER,
}; };
use pmoupnp::devices::Device;
/// Device MediaRenderer UPnP. /// Device MediaRenderer UPnP.
/// ///
@@ -54,13 +53,16 @@ pub static MEDIA_RENDERER: Lazy<Arc<Device>> = Lazy::new(|| {
device.set_udn_prefix("pmomusic".to_string()); device.set_udn_prefix("pmomusic".to_string());
// Ajouter les trois services obligatoires // Ajouter les trois services obligatoires
device.add_service(Arc::clone(&AVTTRANSPORT)) device
.add_service(Arc::clone(&AVTTRANSPORT))
.expect("Failed to add AVTransport service"); .expect("Failed to add AVTransport service");
device.add_service(Arc::clone(&RENDERINGCONTROL)) device
.add_service(Arc::clone(&RENDERINGCONTROL))
.expect("Failed to add RenderingControl service"); .expect("Failed to add RenderingControl service");
device.add_service(Arc::clone(&CONNECTIONMANAGER)) device
.add_service(Arc::clone(&CONNECTIONMANAGER))
.expect("Failed to add ConnectionManager service"); .expect("Failed to add ConnectionManager service");
Arc::new(device) Arc::new(device)

View File

@@ -29,7 +29,7 @@
pub mod avtransport; pub mod avtransport;
pub mod connectionmanager; pub mod connectionmanager;
pub mod renderingcontrol;
pub mod device; pub mod device;
pub mod renderingcontrol;
pub use device::MEDIA_RENDERER; pub use device::MEDIA_RENDERER;

View File

@@ -1,4 +1,4 @@
use crate::renderingcontrol::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_CHANNEL, MUTE}; use crate::renderingcontrol::variables::{A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID, MUTE};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -1,4 +1,4 @@
use crate::renderingcontrol::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_CHANNEL, VOLUME}; use crate::renderingcontrol::variables::{A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID, VOLUME};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -1,9 +1,9 @@
mod getvolume;
mod setvolume;
mod getmute; mod getmute;
mod getvolume;
mod setmute; mod setmute;
mod setvolume;
pub use getvolume::GETVOLUME;
pub use setvolume::SETVOLUME;
pub use getmute::GETMUTE; pub use getmute::GETMUTE;
pub use getvolume::GETVOLUME;
pub use setmute::SETMUTE; pub use setmute::SETMUTE;
pub use setvolume::SETVOLUME;

View File

@@ -1,4 +1,4 @@
use crate::renderingcontrol::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_CHANNEL, MUTE}; use crate::renderingcontrol::variables::{A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID, MUTE};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -1,4 +1,4 @@
use crate::renderingcontrol::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_CHANNEL, VOLUME}; use crate::renderingcontrol::variables::{A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID, VOLUME};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -51,8 +51,8 @@
use pmoupnp::define_service; use pmoupnp::define_service;
pub mod variables;
pub mod actions; pub mod actions;
pub mod variables;
use actions::{GETMUTE, GETVOLUME, SETMUTE, SETVOLUME}; use actions::{GETMUTE, GETVOLUME, SETMUTE, SETVOLUME};
use variables::{A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID, MUTE, VOLUME}; use variables::{A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID, MUTE, VOLUME};

View File

@@ -1,9 +1,9 @@
mod a_arg_type_instanceid;
mod a_arg_type_channel; mod a_arg_type_channel;
mod volume; mod a_arg_type_instanceid;
mod mute; mod mute;
mod volume;
pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID;
pub use a_arg_type_channel::A_ARG_TYPE_CHANNEL; pub use a_arg_type_channel::A_ARG_TYPE_CHANNEL;
pub use volume::VOLUME; pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID;
pub use mute::MUTE; pub use mute::MUTE;
pub use volume::VOLUME;

View File

@@ -1,6 +1,6 @@
use crate::connectionmanager::variables::{ use crate::connectionmanager::variables::{
A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_RCSID, A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_CONNECTIONSTATUS,
A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_DIRECTION, A_ARG_TYPE_CONNECTIONSTATUS, A_ARG_TYPE_DIRECTION, A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_RCSID,
}; };
use pmoupnp::define_action; use pmoupnp::define_action;

View File

@@ -1,4 +1,4 @@
use crate::connectionmanager::variables::{SOURCEPROTOCOLINFO, SINKPROTOCOLINFO}; use crate::connectionmanager::variables::{SINKPROTOCOLINFO, SOURCEPROTOCOLINFO};
use pmoupnp::define_action; use pmoupnp::define_action;
define_action! { define_action! {

View File

@@ -1,7 +1,7 @@
mod getprotocolinfo;
mod getcurrentconnectionids; mod getcurrentconnectionids;
mod getcurrentconnectioninfo; mod getcurrentconnectioninfo;
mod getprotocolinfo;
pub use getprotocolinfo::GETPROTOCOLINFO;
pub use getcurrentconnectionids::GETCURRENTCONNECTIONIDS; pub use getcurrentconnectionids::GETCURRENTCONNECTIONIDS;
pub use getcurrentconnectioninfo::GETCURRENTCONNECTIONINFO; pub use getcurrentconnectioninfo::GETCURRENTCONNECTIONINFO;
pub use getprotocolinfo::GETPROTOCOLINFO;

View File

@@ -65,14 +65,14 @@
use pmoupnp::define_service; use pmoupnp::define_service;
pub mod variables;
pub mod actions; pub mod actions;
pub mod variables;
use actions::{GETCURRENTCONNECTIONIDS, GETCURRENTCONNECTIONINFO, GETPROTOCOLINFO}; use actions::{GETCURRENTCONNECTIONIDS, GETCURRENTCONNECTIONINFO, GETPROTOCOLINFO};
use variables::{ use variables::{
A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_CONNECTIONSTATUS, A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_CONNECTIONSTATUS,
A_ARG_TYPE_DIRECTION, A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_RCSID, A_ARG_TYPE_DIRECTION, A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_RCSID, CURRENTCONNECTIONIDS,
CURRENTCONNECTIONIDS, SINKPROTOCOLINFO, SOURCEPROTOCOLINFO SINKPROTOCOLINFO, SOURCEPROTOCOLINFO,
}; };
// Service ConnectionManager:1 conforme à la spécification UPnP AV pour MediaServer // Service ConnectionManager:1 conforme à la spécification UPnP AV pour MediaServer

View File

@@ -1,19 +1,19 @@
mod a_arg_type_avtransportid;
mod a_arg_type_connectionid; mod a_arg_type_connectionid;
mod a_arg_type_connectionstatus; mod a_arg_type_connectionstatus;
mod a_arg_type_direction; mod a_arg_type_direction;
mod a_arg_type_protocolinfo; mod a_arg_type_protocolinfo;
mod a_arg_type_rcsid; mod a_arg_type_rcsid;
mod a_arg_type_avtransportid;
mod currentconnectionids; mod currentconnectionids;
mod sourceprotocolinfo;
mod sinkprotocolinfo; mod sinkprotocolinfo;
mod sourceprotocolinfo;
pub use a_arg_type_avtransportid::A_ARG_TYPE_AVTRANSPORTID;
pub use a_arg_type_connectionid::A_ARG_TYPE_CONNECTIONID; pub use a_arg_type_connectionid::A_ARG_TYPE_CONNECTIONID;
pub use a_arg_type_connectionstatus::A_ARG_TYPE_CONNECTIONSTATUS; pub use a_arg_type_connectionstatus::A_ARG_TYPE_CONNECTIONSTATUS;
pub use a_arg_type_direction::A_ARG_TYPE_DIRECTION; pub use a_arg_type_direction::A_ARG_TYPE_DIRECTION;
pub use a_arg_type_protocolinfo::A_ARG_TYPE_PROTOCOLINFO; pub use a_arg_type_protocolinfo::A_ARG_TYPE_PROTOCOLINFO;
pub use a_arg_type_rcsid::A_ARG_TYPE_RCSID; pub use a_arg_type_rcsid::A_ARG_TYPE_RCSID;
pub use a_arg_type_avtransportid::A_ARG_TYPE_AVTRANSPORTID;
pub use currentconnectionids::CURRENTCONNECTIONIDS; pub use currentconnectionids::CURRENTCONNECTIONIDS;
pub use sourceprotocolinfo::SOURCEPROTOCOLINFO;
pub use sinkprotocolinfo::SINKPROTOCOLINFO; pub use sinkprotocolinfo::SINKPROTOCOLINFO;
pub use sourceprotocolinfo::SOURCEPROTOCOLINFO;

View File

@@ -10,8 +10,8 @@
//! - **Search** : Recherche dans les sources qui le supportent //! - **Search** : Recherche dans les sources qui le supportent
//! - **Update ID** : Suivi des changements pour les notifications UPnP //! - **Update ID** : Suivi des changements pour les notifications UPnP
use pmosource::api::{list_all_sources, get_source as get_source_from_registry};
use pmodidl::{Container, DIDLLite}; 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};
use std::sync::Arc; use std::sync::Arc;
@@ -28,8 +28,7 @@ fn to_didl_lite(containers: &[Container], items: &[pmodidl::Item]) -> Result<Str
items: items.to_vec(), items: items.to_vec(),
}; };
quick_xml::se::to_string(&didl) quick_xml::se::to_string(&didl).map_err(|e| format!("Failed to serialize DIDL-Lite: {}", e))
.map_err(|e| format!("Failed to serialize DIDL-Lite: {}", e))
} }
/// Handler pour le service ContentDirectory /// Handler pour le service ContentDirectory
@@ -208,11 +207,7 @@ impl ContentHandler {
requested_count as usize requested_count as usize
}; };
let paginated: Vec<Container> = containers let paginated: Vec<Container> = containers.into_iter().skip(start).take(count).collect();
.into_iter()
.skip(start)
.take(count)
.collect();
let returned = paginated.len(); let returned = paginated.len();
let didl = to_didl_lite(&paginated, &[])?; let didl = to_didl_lite(&paginated, &[])?;
@@ -278,11 +273,7 @@ impl ContentHandler {
// On commence dans les items // On commence dans les items
containers.clear(); containers.clear();
let item_start = start - total_containers; let item_start = start - total_containers;
items = items items = items.into_iter().skip(item_start).take(count).collect();
.into_iter()
.skip(item_start)
.take(count)
.collect();
} }
let returned = (containers.len() + items.len()) as u32; let returned = (containers.len() + items.len()) as u32;

View File

@@ -1,10 +1,9 @@
use crate::contentdirectory::handlers;
use crate::contentdirectory::variables::{ use crate::contentdirectory::variables::{
A_ARG_TYPE_OBJECTID, A_ARG_TYPE_BROWSEFLAG, A_ARG_TYPE_FILTER, A_ARG_TYPE_BROWSEFLAG, A_ARG_TYPE_COUNT, A_ARG_TYPE_FILTER, A_ARG_TYPE_INDEX,
A_ARG_TYPE_SORTCRITERIA, A_ARG_TYPE_INDEX, A_ARG_TYPE_COUNT, A_ARG_TYPE_OBJECTID, A_ARG_TYPE_RESULT, A_ARG_TYPE_SORTCRITERIA, A_ARG_TYPE_UPDATEID,
A_ARG_TYPE_RESULT, A_ARG_TYPE_UPDATEID,
}; };
use pmoupnp::define_action; use pmoupnp::define_action;
use crate::contentdirectory::handlers;
define_action! { define_action! {
pub static BROWSE = "Browse" stateless { pub static BROWSE = "Browse" stateless {

View File

@@ -1,6 +1,6 @@
use crate::contentdirectory::handlers;
use crate::contentdirectory::variables::SEARCHCAPABILITIES; use crate::contentdirectory::variables::SEARCHCAPABILITIES;
use pmoupnp::define_action; use pmoupnp::define_action;
use crate::contentdirectory::handlers;
define_action! { define_action! {
pub static GETSEARCHCAPABILITIES = "GetSearchCapabilities" stateless { pub static GETSEARCHCAPABILITIES = "GetSearchCapabilities" stateless {

View File

@@ -1,6 +1,6 @@
use crate::contentdirectory::handlers;
use crate::contentdirectory::variables::SORTCAPABILITIES; use crate::contentdirectory::variables::SORTCAPABILITIES;
use pmoupnp::define_action; use pmoupnp::define_action;
use crate::contentdirectory::handlers;
define_action! { define_action! {
pub static GETSORTCAPABILITIES = "GetSortCapabilities" stateless { pub static GETSORTCAPABILITIES = "GetSortCapabilities" stateless {

View File

@@ -1,6 +1,6 @@
use crate::contentdirectory::handlers;
use crate::contentdirectory::variables::SYSTEMUPDATEID; use crate::contentdirectory::variables::SYSTEMUPDATEID;
use pmoupnp::define_action; use pmoupnp::define_action;
use crate::contentdirectory::handlers;
define_action! { define_action! {
pub static GETSYSTEMUPDATEID = "GetSystemUpdateID" stateless { pub static GETSYSTEMUPDATEID = "GetSystemUpdateID" stateless {

View File

@@ -1,11 +1,11 @@
mod browse; mod browse;
mod search;
mod getsearchcapabilities; mod getsearchcapabilities;
mod getsortcapabilities; mod getsortcapabilities;
mod getsystemupdateid; mod getsystemupdateid;
mod search;
pub use browse::BROWSE; pub use browse::BROWSE;
pub use search::SEARCH;
pub use getsearchcapabilities::GETSEARCHCAPABILITIES; pub use getsearchcapabilities::GETSEARCHCAPABILITIES;
pub use getsortcapabilities::GETSORTCAPABILITIES; pub use getsortcapabilities::GETSORTCAPABILITIES;
pub use getsystemupdateid::GETSYSTEMUPDATEID; pub use getsystemupdateid::GETSYSTEMUPDATEID;
pub use search::SEARCH;

View File

@@ -1,10 +1,9 @@
use crate::contentdirectory::handlers;
use crate::contentdirectory::variables::{ use crate::contentdirectory::variables::{
A_ARG_TYPE_OBJECTID, A_ARG_TYPE_SEARCHCRITERIA, A_ARG_TYPE_FILTER, A_ARG_TYPE_COUNT, A_ARG_TYPE_FILTER, A_ARG_TYPE_INDEX, A_ARG_TYPE_OBJECTID, A_ARG_TYPE_RESULT,
A_ARG_TYPE_SORTCRITERIA, A_ARG_TYPE_INDEX, A_ARG_TYPE_COUNT, A_ARG_TYPE_SEARCHCRITERIA, A_ARG_TYPE_SORTCRITERIA, A_ARG_TYPE_UPDATEID,
A_ARG_TYPE_RESULT, A_ARG_TYPE_UPDATEID,
}; };
use pmoupnp::define_action; use pmoupnp::define_action;
use crate::contentdirectory::handlers;
define_action! { define_action! {
pub static SEARCH = "Search" stateless { pub static SEARCH = "Search" stateless {

View File

@@ -23,9 +23,9 @@
//! - [`get_sort_capabilities_handler`] : Capacités de tri supportées //! - [`get_sort_capabilities_handler`] : Capacités de tri supportées
//! - [`get_system_update_id_handler`] : ID de mise à jour du système //! - [`get_system_update_id_handler`] : ID de mise à jour du système
use pmoupnp::{action_handler, get, set};
use pmoupnp::actions::{ActionError, ActionHandler};
use crate::content_handler::ContentHandler; use crate::content_handler::ContentHandler;
use pmoupnp::actions::{ActionError, ActionHandler};
use pmoupnp::{action_handler, get, set};
use tracing::{debug, error}; use tracing::{debug, error};
/// Handler pour l'action Browse. /// Handler pour l'action Browse.
@@ -76,7 +76,10 @@ pub fn browse_handler() -> ActionHandler {
set!(&mut data, "TotalMatches", total); set!(&mut data, "TotalMatches", total);
set!(&mut data, "UpdateID", update_id); set!(&mut data, "UpdateID", update_id);
debug!("✅ Browse completed: returned={}, total={}", returned, total); debug!(
"✅ Browse completed: returned={}, total={}",
returned, total
);
Ok(data) Ok(data)
}) })
} }
@@ -128,7 +131,10 @@ pub fn search_handler() -> ActionHandler {
set!(&mut data, "TotalMatches", total); set!(&mut data, "TotalMatches", total);
set!(&mut data, "UpdateID", update_id); set!(&mut data, "UpdateID", update_id);
debug!("✅ Search completed: returned={}, total={}", returned, total); debug!(
"✅ Search completed: returned={}, total={}",
returned, total
);
Ok(data) Ok(data)
}) })
} }

View File

@@ -74,18 +74,15 @@
use pmoupnp::define_service; use pmoupnp::define_service;
pub mod variables;
pub mod actions; pub mod actions;
pub mod handlers; pub mod handlers;
pub mod variables;
use actions::{ use actions::{BROWSE, GETSEARCHCAPABILITIES, GETSORTCAPABILITIES, GETSYSTEMUPDATEID, SEARCH};
BROWSE, SEARCH, GETSEARCHCAPABILITIES, GETSORTCAPABILITIES, GETSYSTEMUPDATEID
};
use variables::{ use variables::{
A_ARG_TYPE_OBJECTID, A_ARG_TYPE_BROWSEFLAG, A_ARG_TYPE_FILTER, A_ARG_TYPE_BROWSEFLAG, A_ARG_TYPE_COUNT, A_ARG_TYPE_FILTER, A_ARG_TYPE_INDEX,
A_ARG_TYPE_SORTCRITERIA, A_ARG_TYPE_INDEX, A_ARG_TYPE_COUNT, A_ARG_TYPE_OBJECTID, A_ARG_TYPE_RESULT, A_ARG_TYPE_SEARCHCRITERIA, A_ARG_TYPE_SORTCRITERIA,
A_ARG_TYPE_UPDATEID, A_ARG_TYPE_RESULT, A_ARG_TYPE_SEARCHCRITERIA, A_ARG_TYPE_UPDATEID, SEARCHCAPABILITIES, SORTCAPABILITIES, SYSTEMUPDATEID,
SEARCHCAPABILITIES, SORTCAPABILITIES, SYSTEMUPDATEID
}; };
// Service ContentDirectory:1 conforme à la spécification UPnP AV pour MediaServer // Service ContentDirectory:1 conforme à la spécification UPnP AV pour MediaServer

View File

@@ -1,25 +1,25 @@
mod a_arg_type_objectid;
mod a_arg_type_browseflag; mod a_arg_type_browseflag;
mod a_arg_type_filter;
mod a_arg_type_sortcriteria;
mod a_arg_type_index;
mod a_arg_type_count; mod a_arg_type_count;
mod a_arg_type_updateid; mod a_arg_type_filter;
mod a_arg_type_index;
mod a_arg_type_objectid;
mod a_arg_type_result; mod a_arg_type_result;
mod a_arg_type_searchcriteria; mod a_arg_type_searchcriteria;
mod a_arg_type_sortcriteria;
mod a_arg_type_updateid;
mod searchcapabilities; mod searchcapabilities;
mod sortcapabilities; mod sortcapabilities;
mod systemupdateid; mod systemupdateid;
pub use a_arg_type_objectid::A_ARG_TYPE_OBJECTID;
pub use a_arg_type_browseflag::A_ARG_TYPE_BROWSEFLAG; pub use a_arg_type_browseflag::A_ARG_TYPE_BROWSEFLAG;
pub use a_arg_type_filter::A_ARG_TYPE_FILTER;
pub use a_arg_type_sortcriteria::A_ARG_TYPE_SORTCRITERIA;
pub use a_arg_type_index::A_ARG_TYPE_INDEX;
pub use a_arg_type_count::A_ARG_TYPE_COUNT; pub use a_arg_type_count::A_ARG_TYPE_COUNT;
pub use a_arg_type_updateid::A_ARG_TYPE_UPDATEID; pub use a_arg_type_filter::A_ARG_TYPE_FILTER;
pub use a_arg_type_index::A_ARG_TYPE_INDEX;
pub use a_arg_type_objectid::A_ARG_TYPE_OBJECTID;
pub use a_arg_type_result::A_ARG_TYPE_RESULT; pub use a_arg_type_result::A_ARG_TYPE_RESULT;
pub use a_arg_type_searchcriteria::A_ARG_TYPE_SEARCHCRITERIA; pub use a_arg_type_searchcriteria::A_ARG_TYPE_SEARCHCRITERIA;
pub use a_arg_type_sortcriteria::A_ARG_TYPE_SORTCRITERIA;
pub use a_arg_type_updateid::A_ARG_TYPE_UPDATEID;
pub use searchcapabilities::SEARCHCAPABILITIES; pub use searchcapabilities::SEARCHCAPABILITIES;
pub use sortcapabilities::SORTCAPABILITIES; pub use sortcapabilities::SORTCAPABILITIES;
pub use systemupdateid::SYSTEMUPDATEID; pub use systemupdateid::SYSTEMUPDATEID;

View File

@@ -3,11 +3,8 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::sync::Arc; use std::sync::Arc;
use crate::{connectionmanager::CONNECTIONMANAGER, contentdirectory::CONTENTDIRECTORY};
use pmoupnp::devices::Device; use pmoupnp::devices::Device;
use crate::{
contentdirectory::CONTENTDIRECTORY,
connectionmanager::CONNECTIONMANAGER,
};
/// Device MediaServer UPnP. /// Device MediaServer UPnP.
/// ///
@@ -52,10 +49,12 @@ pub static MEDIA_SERVER: Lazy<Arc<Device>> = Lazy::new(|| {
device.set_udn_prefix("pmomusic".to_string()); device.set_udn_prefix("pmomusic".to_string());
// Ajouter les deux services obligatoires // Ajouter les deux services obligatoires
device.add_service(Arc::clone(&CONTENTDIRECTORY)) device
.add_service(Arc::clone(&CONTENTDIRECTORY))
.expect("Failed to add ContentDirectory service"); .expect("Failed to add ContentDirectory service");
device.add_service(Arc::clone(&CONNECTIONMANAGER)) device
.add_service(Arc::clone(&CONNECTIONMANAGER))
.expect("Failed to add ConnectionManager service"); .expect("Failed to add ConnectionManager service");
Arc::new(device) Arc::new(device)

View File

@@ -63,23 +63,23 @@
//! server.register_qobuz_from_config().await?; //! server.register_qobuz_from_config().await?;
//! ``` //! ```
pub mod contentdirectory;
pub mod connectionmanager; pub mod connectionmanager;
pub mod device;
pub mod source_registry;
pub mod server_ext;
pub mod content_handler; pub mod content_handler;
pub mod contentdirectory;
pub mod device;
pub mod server_ext;
pub mod source_registry;
pub mod sources; pub mod sources;
// API REST pour l'enregistrement des sources (requires features qobuz/paradise) // API REST pour l'enregistrement des sources (requires features qobuz/paradise)
#[cfg(any(feature = "qobuz", feature = "paradise"))] #[cfg(any(feature = "qobuz", feature = "paradise"))]
pub mod sources_api; pub mod sources_api;
pub use device::MEDIA_SERVER;
pub use source_registry::SourceRegistry;
pub use server_ext::{MediaServerExt, get_source_registry, MusicSourceExt};
pub use content_handler::ContentHandler; pub use content_handler::ContentHandler;
pub use sources::{SourcesExt, SourceInitError}; pub use device::MEDIA_SERVER;
pub use server_ext::{MediaServerExt, MusicSourceExt, get_source_registry};
pub use source_registry::SourceRegistry;
pub use sources::{SourceInitError, SourcesExt};
// Re-export sources when features are enabled // Re-export sources when features are enabled
#[cfg(feature = "qobuz")] #[cfg(feature = "qobuz")]

View File

@@ -7,8 +7,8 @@
//! méthodes spécifiques au MediaServer UPnP. //! méthodes spécifiques au MediaServer UPnP.
use async_trait::async_trait; use async_trait::async_trait;
use pmosource::MusicSource;
use pmoserver::Server; use pmoserver::Server;
use pmosource::MusicSource;
use std::sync::Arc; use std::sync::Arc;
// Réexporter le trait de base de pmosource // Réexporter le trait de base de pmosource
@@ -23,7 +23,10 @@ pub use pmosource::MusicSourceExt;
/// ///
/// let sources = pmosource::api::list_all_sources().await; /// let sources = pmosource::api::list_all_sources().await;
/// ``` /// ```
#[deprecated(since = "0.2.0", note = "Use pmosource::api::list_all_sources() directly")] #[deprecated(
since = "0.2.0",
note = "Use pmosource::api::list_all_sources() directly"
)]
pub async fn get_source_registry() -> Vec<Arc<dyn MusicSource>> { pub async fn get_source_registry() -> Vec<Arc<dyn MusicSource>> {
pmosource::api::list_all_sources().await pmosource::api::list_all_sources().await
} }

View File

@@ -210,8 +210,8 @@ impl Default for SourceRegistry {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use pmosource::{MusicSource, Result, BrowseResult};
use pmodidl::{Container, Item}; use pmodidl::{Container, Item};
use pmosource::{BrowseResult, MusicSource, Result};
use std::time::SystemTime; use std::time::SystemTime;
#[derive(Debug)] #[derive(Debug)]
@@ -308,9 +308,15 @@ mod tests {
async fn test_list_all() { async fn test_list_all() {
let registry = SourceRegistry::new(); let registry = SourceRegistry::new();
registry.register(Arc::new(TestSource::new("test-1", "Test 1"))).await; registry
registry.register(Arc::new(TestSource::new("test-2", "Test 2"))).await; .register(Arc::new(TestSource::new("test-1", "Test 1")))
registry.register(Arc::new(TestSource::new("test-3", "Test 3"))).await; .await;
registry
.register(Arc::new(TestSource::new("test-2", "Test 2")))
.await;
registry
.register(Arc::new(TestSource::new("test-3", "Test 3")))
.await;
let sources = registry.list_all().await; let sources = registry.list_all().await;
assert_eq!(sources.len(), 3); assert_eq!(sources.len(), 3);
@@ -321,17 +327,23 @@ mod tests {
let registry = SourceRegistry::new(); let registry = SourceRegistry::new();
assert_eq!(registry.count().await, 0); assert_eq!(registry.count().await, 0);
registry.register(Arc::new(TestSource::new("test-1", "Test 1"))).await; registry
.register(Arc::new(TestSource::new("test-1", "Test 1")))
.await;
assert_eq!(registry.count().await, 1); assert_eq!(registry.count().await, 1);
registry.register(Arc::new(TestSource::new("test-2", "Test 2"))).await; registry
.register(Arc::new(TestSource::new("test-2", "Test 2")))
.await;
assert_eq!(registry.count().await, 2); assert_eq!(registry.count().await, 2);
} }
#[tokio::test] #[tokio::test]
async fn test_remove() { async fn test_remove() {
let registry = SourceRegistry::new(); let registry = SourceRegistry::new();
registry.register(Arc::new(TestSource::new("test-1", "Test 1"))).await; registry
.register(Arc::new(TestSource::new("test-1", "Test 1")))
.await;
assert!(registry.contains("test-1").await); assert!(registry.contains("test-1").await);
assert!(registry.remove("test-1").await); assert!(registry.remove("test-1").await);
@@ -345,7 +357,9 @@ mod tests {
assert!(!registry.contains("test-1").await); assert!(!registry.contains("test-1").await);
registry.register(Arc::new(TestSource::new("test-1", "Test 1"))).await; registry
.register(Arc::new(TestSource::new("test-1", "Test 1")))
.await;
assert!(registry.contains("test-1").await); assert!(registry.contains("test-1").await);
assert!(!registry.contains("test-2").await); assert!(!registry.contains("test-2").await);
@@ -355,11 +369,15 @@ mod tests {
async fn test_replace_source() { async fn test_replace_source() {
let registry = SourceRegistry::new(); let registry = SourceRegistry::new();
registry.register(Arc::new(TestSource::new("test-1", "Old Name"))).await; registry
.register(Arc::new(TestSource::new("test-1", "Old Name")))
.await;
let old = registry.get("test-1").await.unwrap(); let old = registry.get("test-1").await.unwrap();
assert_eq!(old.name(), "Old Name"); assert_eq!(old.name(), "Old Name");
registry.register(Arc::new(TestSource::new("test-1", "New Name"))).await; registry
.register(Arc::new(TestSource::new("test-1", "New Name")))
.await;
let new = registry.get("test-1").await.unwrap(); let new = registry.get("test-1").await.unwrap();
assert_eq!(new.name(), "New Name"); assert_eq!(new.name(), "New Name");

View File

@@ -3,8 +3,8 @@
//! Ce module fournit des helpers pour créer et enregistrer facilement des sources //! Ce module fournit des helpers pour créer et enregistrer facilement des sources
//! musicales préconfigurées à partir de la configuration système. //! musicales préconfigurées à partir de la configuration système.
use pmosource::MusicSourceExt;
use pmoserver::Server; use pmoserver::Server;
use pmosource::MusicSourceExt;
use std::sync::Arc; use std::sync::Arc;
/// Erreur lors de l'initialisation d'une source /// Erreur lors de l'initialisation d'une source
@@ -93,7 +93,11 @@ pub trait SourcesExt {
/// server.register_qobuz_with_credentials("user@example.com", "password").await?; /// server.register_qobuz_with_credentials("user@example.com", "password").await?;
/// ``` /// ```
#[cfg(feature = "qobuz")] #[cfg(feature = "qobuz")]
async fn register_qobuz_with_credentials(&mut self, username: &str, password: &str) -> Result<()>; async fn register_qobuz_with_credentials(
&mut self,
username: &str,
password: &str,
) -> Result<()>;
/// Enregistre la source Radio Paradise /// Enregistre la source Radio Paradise
/// ///
@@ -141,7 +145,11 @@ impl SourcesExt for Server {
} }
#[cfg(feature = "qobuz")] #[cfg(feature = "qobuz")]
async fn register_qobuz_with_credentials(&mut self, username: &str, password: &str) -> Result<()> { async fn register_qobuz_with_credentials(
&mut self,
username: &str,
password: &str,
) -> Result<()> {
use pmoqobuz::{QobuzClient, QobuzSource}; use pmoqobuz::{QobuzClient, QobuzSource};
tracing::info!("Initializing Qobuz source with explicit credentials..."); tracing::info!("Initializing Qobuz source with explicit credentials...");
@@ -165,18 +173,19 @@ impl SourcesExt for Server {
#[cfg(feature = "paradise")] #[cfg(feature = "paradise")]
async fn register_paradise(&mut self) -> Result<()> { async fn register_paradise(&mut self) -> Result<()> {
use pmoparadise::{RadioParadiseClient, RadioParadiseSource, RadioParadiseExt}; use pmoparadise::{RadioParadiseClient, RadioParadiseExt, RadioParadiseSource};
tracing::info!("Initializing Radio Paradise source..."); tracing::info!("Initializing Radio Paradise source...");
// Créer le client (Radio Paradise ne nécessite pas d'authentification) // Créer le client (Radio Paradise ne nécessite pas d'authentification)
let client = RadioParadiseClient::new() let client = RadioParadiseClient::new().await.map_err(|e| {
.await SourceInitError::ParadiseError(format!("Failed to create client: {}", e))
.map_err(|e| SourceInitError::ParadiseError(format!("Failed to create client: {}", e)))?; })?;
// Créer la source depuis le registry avec capacité FIFO par défaut // Créer la source depuis le registry avec capacité FIFO par défaut
let source = RadioParadiseSource::from_registry_default(client) let source = RadioParadiseSource::from_registry_default(client).map_err(|e| {
.map_err(|e| SourceInitError::ParadiseError(format!("Failed to create source: {}", e)))?; SourceInitError::ParadiseError(format!("Failed to create source: {}", e))
})?;
// Enregistrer la source // Enregistrer la source
// Note: La FIFO sera peuplée automatiquement lors du premier browse // Note: La FIFO sera peuplée automatiquement lors du premier browse

View File

@@ -13,13 +13,7 @@
//! Ces endpoints sont définis ici plutôt que dans `pmosource` pour éviter les //! Ces endpoints sont définis ici plutôt que dans `pmosource` pour éviter les
//! dépendances circulaires (pmoqobuz et pmoparadise dépendent de pmosource). //! dépendances circulaires (pmoqobuz et pmoparadise dépendent de pmosource).
use axum::{ use axum::{Router, extract::Json, http::StatusCode, response::IntoResponse, routing::post};
extract::Json,
http::StatusCode,
response::IntoResponse,
routing::post,
Router,
};
use pmosource::MusicSource; use pmosource::MusicSource;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;

View File

@@ -43,11 +43,13 @@ async fn main() -> Result<()> {
// Display all tracks // Display all tracks
println!("Available Tracks:"); println!("Available Tracks:");
for (index, song) in block.songs_ordered() { for (index, song) in block.songs_ordered() {
println!(" {}. {} - {} ({:.1}s)", println!(
index, " {}. {} - {} ({:.1}s)",
song.artist, index,
song.title, song.artist,
song.duration as f64 / 1000.0); song.title,
song.duration as f64 / 1000.0
);
} }
println!(); println!();
@@ -67,7 +69,10 @@ async fn main() -> Result<()> {
println!("Track Metadata:"); println!("Track Metadata:");
println!(" Sample Rate: {} Hz", track_stream.metadata.sample_rate); println!(" Sample Rate: {} Hz", track_stream.metadata.sample_rate);
println!(" Channels: {}", track_stream.metadata.channels); println!(" Channels: {}", track_stream.metadata.channels);
println!(" Bits Per Sample: {}", track_stream.metadata.bits_per_sample); println!(
" Bits Per Sample: {}",
track_stream.metadata.bits_per_sample
);
println!(" Total Samples: {}", track_stream.metadata.total_samples); println!(" Total Samples: {}", track_stream.metadata.total_samples);
println!(); println!();
@@ -86,10 +91,15 @@ async fn main() -> Result<()> {
let (start, duration) = client.track_position_seconds(&block, index)?; let (start, duration) = client.track_position_seconds(&block, index)?;
println!("Track {}: {} - {}", index, song.artist, song.title); println!("Track {}: {} - {}", index, song.artist, song.title);
println!(" mpv command:"); println!(" mpv command:");
println!(" mpv --start={:.3} --length={:.3} '{}'", start, duration, block.url); println!(
" mpv --start={:.3} --length={:.3} '{}'",
start, duration, block.url
);
println!(" ffmpeg command (extract to file):"); println!(" ffmpeg command (extract to file):");
println!(" ffmpeg -ss {:.3} -t {:.3} -i '{}' -c copy track_{}.flac", println!(
start, duration, block.url, index); " ffmpeg -ss {:.3} -t {:.3} -i '{}' -c copy track_{}.flac",
start, duration, block.url, index
);
println!(); println!();
} }

View File

@@ -48,9 +48,11 @@ async fn main() -> Result<()> {
if let Some(rating) = song.rating { if let Some(rating) = song.rating {
println!(" Rating: {:.1}/10", rating); println!(" Rating: {:.1}/10", rating);
} }
println!(" Duration: {}:{:02}", println!(
song.duration / 60000, " Duration: {}:{:02}",
(song.duration % 60000) / 1000); song.duration / 60000,
(song.duration % 60000) / 1000
);
// Display cover URL // Display cover URL
if let Some(cover) = &song.cover { if let Some(cover) = &song.cover {

View File

@@ -5,7 +5,7 @@
//! - Accessing the embedded WebP image //! - Accessing the embedded WebP image
//! - Optionally saving it to a file //! - Optionally saving it to a file
use pmoparadise::{RadioParadiseSource, RadioParadiseClient}; use pmoparadise::{RadioParadiseClient, RadioParadiseSource};
use pmosource::MusicSource; use pmosource::MusicSource;
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;

View File

@@ -38,7 +38,10 @@ async fn main() -> Result<()> {
eprintln!("Current Block:"); eprintln!("Current Block:");
eprintln!(" Event: {}", current_block.event); eprintln!(" Event: {}", current_block.event);
eprintln!(" Songs: {}", current_block.song_count()); eprintln!(" Songs: {}", current_block.song_count());
eprintln!(" Duration: {:.1} minutes", current_block.length as f64 / 60000.0); eprintln!(
" Duration: {:.1} minutes",
current_block.length as f64 / 60000.0
);
eprintln!(" URL: {}\n", current_block.url); eprintln!(" URL: {}\n", current_block.url);
// Display tracklist // Display tracklist
@@ -51,7 +54,10 @@ async fn main() -> Result<()> {
// Prefetch next block in advance // Prefetch next block in advance
eprintln!("Prefetching next block..."); eprintln!("Prefetching next block...");
client.prefetch_next(&current_block).await?; client.prefetch_next(&current_block).await?;
eprintln!("Next block prefetched: {}\n", client.next_block_url().unwrap()); eprintln!(
"Next block prefetched: {}\n",
client.next_block_url().unwrap()
);
// Stream the block // Stream the block
eprintln!("Streaming block... (writing to stdout)"); eprintln!("Streaming block... (writing to stdout)");
@@ -71,12 +77,18 @@ async fn main() -> Result<()> {
// Progress indicator (to stderr so it doesn't interfere with piped audio) // Progress indicator (to stderr so it doesn't interfere with piped audio)
if total_bytes % (1024 * 1024) == 0 { if total_bytes % (1024 * 1024) == 0 {
eprintln!(" Downloaded: {:.1} MB", total_bytes as f64 / 1024.0 / 1024.0); eprintln!(
" Downloaded: {:.1} MB",
total_bytes as f64 / 1024.0 / 1024.0
);
} }
} }
eprintln!("\nBlock streaming complete!"); eprintln!("\nBlock streaming complete!");
eprintln!("Total downloaded: {:.2} MB", total_bytes as f64 / 1024.0 / 1024.0); eprintln!(
"Total downloaded: {:.2} MB",
total_bytes as f64 / 1024.0 / 1024.0
);
// In a real application, you would now: // In a real application, you would now:
// 1. Get the next block using prefetched metadata // 1. Get the next block using prefetched metadata

View File

@@ -48,7 +48,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Look for 'Radio Paradise FLAC' in your DLNA/UPnP clients."); println!("Look for 'Radio Paradise FLAC' in your DLNA/UPnP clients.");
println!(); println!();
println!("ContentDirectory service available at:"); println!("ContentDirectory service available at:");
println!(" http://localhost:8080/upnp/device/{}/service/ContentDirectory", server.udn()); println!(
" http://localhost:8080/upnp/device/{}/service/ContentDirectory",
server.udn()
);
println!(); println!();
println!("Press Ctrl+C to stop the server."); println!("Press Ctrl+C to stop the server.");
println!(); println!();

View File

@@ -8,9 +8,9 @@
//! cargo run --example with_cache --features cache //! cargo run --example with_cache --features cache
//! ``` //! ```
use pmoparadise::{RadioParadiseClient, RadioParadiseSource};
use pmocovers::Cache as CoverCache;
use pmoaudiocache::AudioCache; use pmoaudiocache::AudioCache;
use pmocovers::Cache as CoverCache;
use pmoparadise::{RadioParadiseClient, RadioParadiseSource};
use pmosource::MusicSource; use pmosource::MusicSource;
use std::sync::Arc; use std::sync::Arc;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
@@ -64,7 +64,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Add current song to the source // Add current song to the source
println!(" Adding current track to FIFO with caching..."); println!(" Adding current track to FIFO with caching...");
if let Some(song) = &now_playing.current_song { if let Some(song) = &now_playing.current_song {
source.add_song(block.clone(), song, now_playing.current_song_index.unwrap_or(0)).await?; source
.add_song(
block.clone(),
song,
now_playing.current_song_index.unwrap_or(0),
)
.await?;
println!("✅ Track added and caching started!"); println!("✅ Track added and caching started!");
println!(" - Cover image will be cached to: ./cache/covers/"); println!(" - Cover image will be cached to: ./cache/covers/");
println!(" - Audio will be cached to: ./cache/audio/\n"); println!(" - Audio will be cached to: ./cache/audio/\n");
@@ -78,7 +84,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("\n📋 Items in FIFO:"); println!("\n📋 Items in FIFO:");
let items = source.get_items(0, 10).await?; let items = source.get_items(0, 10).await?;
for (i, item) in items.iter().enumerate() { for (i, item) in items.iter().enumerate() {
println!(" {}. {} - {}", println!(
" {}. {} - {}",
i + 1, i + 1,
item.artist.as_deref().unwrap_or("Unknown"), item.artist.as_deref().unwrap_or("Unknown"),
item.title item.title

View File

@@ -133,11 +133,7 @@ impl RadioParadiseClient {
#[cfg(feature = "logging")] #[cfg(feature = "logging")]
tracing::debug!("Fetching block: {}", url); tracing::debug!("Fetching block: {}", url);
let response = self.client let response = self.client.get(url).timeout(self.timeout).send().await?;
.get(url)
.timeout(self.timeout)
.send()
.await?;
if !response.status().is_success() { if !response.status().is_success() {
return Err(Error::other(format!( return Err(Error::other(format!(
@@ -381,6 +377,9 @@ mod tests {
fn test_cover_url() { fn test_cover_url() {
let client = RadioParadiseClient::with_client(Client::new()); let client = RadioParadiseClient::with_client(Client::new());
let url = client.cover_url("test.jpg").unwrap(); let url = client.cover_url("test.jpg").unwrap();
assert_eq!(url.as_str(), "https://img.radioparadise.com/covers/l/test.jpg"); assert_eq!(
url.as_str(),
"https://img.radioparadise.com/covers/l/test.jpg"
);
} }
} }

View File

@@ -269,10 +269,12 @@ pub use stream::BlockStream;
pub use track::{TrackMetadata, TrackStream}; pub use track::{TrackMetadata, TrackStream};
#[cfg(feature = "mediaserver")] #[cfg(feature = "mediaserver")]
pub use mediaserver::{RadioParadiseMediaServer, MediaServerBuilder}; pub use mediaserver::{MediaServerBuilder, RadioParadiseMediaServer};
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
pub use pmoserver_ext::{RadioParadiseExt, RadioParadiseState, RadioParadiseApiDoc, create_api_router}; pub use pmoserver_ext::{
create_api_router, RadioParadiseApiDoc, RadioParadiseExt, RadioParadiseState,
};
// Version information // Version information
pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const VERSION: &str = env!("CARGO_PKG_VERSION");

View File

@@ -1,7 +1,7 @@
//! ConnectionManager service implementation //! ConnectionManager service implementation
use pmoupnp::services::Service;
use pmoupnp::actions::Action; use pmoupnp::actions::Action;
use pmoupnp::services::Service;
use pmoupnp::state_variables::StateVariable; use pmoupnp::state_variables::StateVariable;
use std::sync::Arc; use std::sync::Arc;
@@ -15,23 +15,20 @@ pub fn create_connection_manager_service() -> Service {
service.set_service_id("urn:upnp-org:serviceId:ConnectionManager".to_string()); service.set_service_id("urn:upnp-org:serviceId:ConnectionManager".to_string());
// State variables // State variables
let source_protocol_info = StateVariable::new( let source_protocol_info =
"SourceProtocolInfo".to_string(), StateVariable::new("SourceProtocolInfo".to_string(), "string".to_string())
"string".to_string(), .with_send_events(true)
).with_send_events(true) .with_default_value(get_protocol_info());
.with_default_value(get_protocol_info());
let sink_protocol_info = StateVariable::new( let sink_protocol_info =
"SinkProtocolInfo".to_string(), StateVariable::new("SinkProtocolInfo".to_string(), "string".to_string())
"string".to_string(), .with_send_events(true)
).with_send_events(true) .with_default_value("".to_string());
.with_default_value("".to_string());
let current_connection_ids = StateVariable::new( let current_connection_ids =
"CurrentConnectionIDs".to_string(), StateVariable::new("CurrentConnectionIDs".to_string(), "string".to_string())
"string".to_string(), .with_send_events(true)
).with_send_events(true) .with_default_value("0".to_string());
.with_default_value("0".to_string());
service.add_state_variable(Arc::new(source_protocol_info)); service.add_state_variable(Arc::new(source_protocol_info));
service.add_state_variable(Arc::new(sink_protocol_info)); service.add_state_variable(Arc::new(sink_protocol_info));
@@ -39,14 +36,8 @@ pub fn create_connection_manager_service() -> Service {
// GetProtocolInfo action // GetProtocolInfo action
let mut get_protocol_info = Action::new("GetProtocolInfo".to_string()); let mut get_protocol_info = Action::new("GetProtocolInfo".to_string());
get_protocol_info.add_output_argument( get_protocol_info.add_output_argument("Source".to_string(), "SourceProtocolInfo".to_string());
"Source".to_string(), get_protocol_info.add_output_argument("Sink".to_string(), "SinkProtocolInfo".to_string());
"SourceProtocolInfo".to_string(),
);
get_protocol_info.add_output_argument(
"Sink".to_string(),
"SinkProtocolInfo".to_string(),
);
service.add_action(Arc::new(get_protocol_info)); service.add_action(Arc::new(get_protocol_info));
// GetCurrentConnectionIDs action // GetCurrentConnectionIDs action
@@ -63,10 +54,7 @@ pub fn create_connection_manager_service() -> Service {
"ConnectionID".to_string(), "ConnectionID".to_string(),
"A_ARG_TYPE_ConnectionID".to_string(), "A_ARG_TYPE_ConnectionID".to_string(),
); );
get_connection_info.add_output_argument( get_connection_info.add_output_argument("RcsID".to_string(), "A_ARG_TYPE_RcsID".to_string());
"RcsID".to_string(),
"A_ARG_TYPE_RcsID".to_string(),
);
get_connection_info.add_output_argument( get_connection_info.add_output_argument(
"AVTransportID".to_string(), "AVTransportID".to_string(),
"A_ARG_TYPE_AVTransportID".to_string(), "A_ARG_TYPE_AVTransportID".to_string(),
@@ -83,10 +71,8 @@ pub fn create_connection_manager_service() -> Service {
"PeerConnectionID".to_string(), "PeerConnectionID".to_string(),
"A_ARG_TYPE_ConnectionID".to_string(), "A_ARG_TYPE_ConnectionID".to_string(),
); );
get_connection_info.add_output_argument( get_connection_info
"Direction".to_string(), .add_output_argument("Direction".to_string(), "A_ARG_TYPE_Direction".to_string());
"A_ARG_TYPE_Direction".to_string(),
);
get_connection_info.add_output_argument( get_connection_info.add_output_argument(
"Status".to_string(), "Status".to_string(),
"A_ARG_TYPE_ConnectionStatus".to_string(), "A_ARG_TYPE_ConnectionStatus".to_string(),
@@ -94,34 +80,42 @@ pub fn create_connection_manager_service() -> Service {
service.add_action(Arc::new(get_connection_info)); service.add_action(Arc::new(get_connection_info));
// Additional state variables for arguments // Additional state variables for arguments
service.add_state_variable(Arc::new( service.add_state_variable(Arc::new(StateVariable::new(
StateVariable::new("A_ARG_TYPE_ConnectionID".to_string(), "i4".to_string()) "A_ARG_TYPE_ConnectionID".to_string(),
)); "i4".to_string(),
service.add_state_variable(Arc::new( )));
StateVariable::new("A_ARG_TYPE_RcsID".to_string(), "i4".to_string()) service.add_state_variable(Arc::new(StateVariable::new(
)); "A_ARG_TYPE_RcsID".to_string(),
service.add_state_variable(Arc::new( "i4".to_string(),
StateVariable::new("A_ARG_TYPE_AVTransportID".to_string(), "i4".to_string()) )));
)); service.add_state_variable(Arc::new(StateVariable::new(
service.add_state_variable(Arc::new( "A_ARG_TYPE_AVTransportID".to_string(),
StateVariable::new("A_ARG_TYPE_ProtocolInfo".to_string(), "string".to_string()) "i4".to_string(),
)); )));
service.add_state_variable(Arc::new( service.add_state_variable(Arc::new(StateVariable::new(
StateVariable::new("A_ARG_TYPE_ConnectionManager".to_string(), "string".to_string()) "A_ARG_TYPE_ProtocolInfo".to_string(),
)); "string".to_string(),
)));
service.add_state_variable(Arc::new(StateVariable::new(
"A_ARG_TYPE_ConnectionManager".to_string(),
"string".to_string(),
)));
service.add_state_variable(Arc::new( service.add_state_variable(Arc::new(
StateVariable::new("A_ARG_TYPE_Direction".to_string(), "string".to_string()) StateVariable::new("A_ARG_TYPE_Direction".to_string(), "string".to_string())
.with_allowed_values(vec!["Input".to_string(), "Output".to_string()]) .with_allowed_values(vec!["Input".to_string(), "Output".to_string()]),
)); ));
service.add_state_variable(Arc::new( service.add_state_variable(Arc::new(
StateVariable::new("A_ARG_TYPE_ConnectionStatus".to_string(), "string".to_string()) StateVariable::new(
.with_allowed_values(vec![ "A_ARG_TYPE_ConnectionStatus".to_string(),
"OK".to_string(), "string".to_string(),
"ContentFormatMismatch".to_string(), )
"InsufficientBandwidth".to_string(), .with_allowed_values(vec![
"UnreliableChannel".to_string(), "OK".to_string(),
"Unknown".to_string(), "ContentFormatMismatch".to_string(),
]) "InsufficientBandwidth".to_string(),
"UnreliableChannel".to_string(),
"Unknown".to_string(),
]),
)); ));
service service
@@ -143,7 +137,8 @@ fn get_protocol_info() -> String {
"http-get:*:audio/mpeg:*", "http-get:*:audio/mpeg:*",
"http-get:*:audio/mp3:*", "http-get:*:audio/mp3:*",
"http-get:*:audio/x-mp3:*", "http-get:*:audio/x-mp3:*",
].join(",") ]
.join(",")
} }
#[cfg(test)] #[cfg(test)]
@@ -153,8 +148,14 @@ mod tests {
#[test] #[test]
fn test_create_connection_manager() { fn test_create_connection_manager() {
let service = create_connection_manager_service(); let service = create_connection_manager_service();
assert_eq!(service.service_type(), "urn:schemas-upnp-org:service:ConnectionManager:1"); assert_eq!(
assert_eq!(service.service_id(), "urn:upnp-org:serviceId:ConnectionManager"); service.service_type(),
"urn:schemas-upnp-org:service:ConnectionManager:1"
);
assert_eq!(
service.service_id(),
"urn:upnp-org:serviceId:ConnectionManager"
);
} }
#[test] #[test]

Some files were not shown because too many files have changed in this diff Show More