amélioration de la webapp
This commit is contained in:
Binary file not shown.
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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: {}",
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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, ¶m, content_type, param_generator).await
|
serve_file_with_streaming(&cache, &pk, ¶m, 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>))
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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()?;
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
});
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ use pmoupnp::define_variable;
|
|||||||
define_variable! {
|
define_variable! {
|
||||||
pub static SEEKMODE: String = "SeekMode"
|
pub static SEEKMODE: String = "SeekMode"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,3 @@ define_variable! {
|
|||||||
evented: true,
|
evented: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,4 +17,3 @@ define_variable! {
|
|||||||
evented: true,
|
evented: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,3 @@ define_variable! {
|
|||||||
default: "1",
|
default: "1",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,4 +7,3 @@ define_variable! {
|
|||||||
evented: true,
|
evented: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(¤t_block).await?;
|
client.prefetch_next(¤t_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
|
||||||
|
|||||||
@@ -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!();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user