2025-12-03 19:36:56 +00:00
|
|
|
use std::{
|
|
|
|
|
collections::HashMap,
|
|
|
|
|
path::{Path, PathBuf},
|
|
|
|
|
sync::Arc,
|
|
|
|
|
time::{Duration, SystemTime},
|
|
|
|
|
};
|
|
|
|
|
use tokio::sync::RwLock;
|
|
|
|
|
|
|
|
|
|
// Imports simplifiés pour éviter les erreurs de compilation
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
// Note: Use tracing::info! macro directly instead of importing
|
|
|
|
|
use crate::config::Config;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AudioMetadata {
|
|
|
|
|
pub duration_seconds: f64,
|
|
|
|
|
pub sample_rate: u32,
|
|
|
|
|
pub channels: u32,
|
|
|
|
|
pub bitrate_kbps: Option<u32>,
|
|
|
|
|
pub codec: String,
|
|
|
|
|
pub title: Option<String>,
|
|
|
|
|
pub artist: Option<String>,
|
|
|
|
|
pub album: Option<String>,
|
|
|
|
|
pub year: Option<u32>,
|
|
|
|
|
pub genre: Option<String>,
|
|
|
|
|
pub artwork_available: bool,
|
|
|
|
|
pub file_size: u64,
|
|
|
|
|
pub last_modified: SystemTime,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct WaveformData {
|
|
|
|
|
pub peaks: Vec<f32>, // Amplitude peaks pour la visualisation
|
|
|
|
|
pub rms: Vec<f32>, // RMS values pour un rendu plus smooth
|
|
|
|
|
pub sample_rate: u32, // Taux d'échantillonnage des données de waveform
|
|
|
|
|
pub duration_ms: u32, // Durée en millisecondes
|
|
|
|
|
pub generated_at: SystemTime,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AudioQuality {
|
|
|
|
|
pub bitrate_kbps: u32,
|
|
|
|
|
pub sample_rate: u32,
|
|
|
|
|
pub channels: u32,
|
|
|
|
|
pub codec: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AudioQuality {
|
|
|
|
|
pub fn high_quality() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
bitrate_kbps: 320,
|
|
|
|
|
sample_rate: 44100,
|
|
|
|
|
channels: 2,
|
|
|
|
|
codec: "mp3".to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn medium_quality() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
bitrate_kbps: 192,
|
|
|
|
|
sample_rate: 44100,
|
|
|
|
|
channels: 2,
|
|
|
|
|
codec: "mp3".to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn low_quality() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
bitrate_kbps: 128,
|
|
|
|
|
sample_rate: 22050,
|
|
|
|
|
channels: 1,
|
|
|
|
|
codec: "mp3".to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct AudioProcessor {
|
|
|
|
|
_config: Arc<Config>,
|
|
|
|
|
metadata_cache: Arc<RwLock<HashMap<PathBuf, AudioMetadata>>>,
|
|
|
|
|
waveform_cache: Arc<RwLock<HashMap<PathBuf, WaveformData>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AudioProcessor {
|
|
|
|
|
pub fn new(config: Arc<Config>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
_config: config,
|
|
|
|
|
metadata_cache: Arc::new(RwLock::new(HashMap::new())),
|
|
|
|
|
waveform_cache: Arc::new(RwLock::new(HashMap::new())),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Extrait les métadonnées d'un fichier audio
|
|
|
|
|
pub async fn extract_metadata(
|
|
|
|
|
&self,
|
|
|
|
|
file_path: &Path,
|
|
|
|
|
) -> Result<AudioMetadata, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
// Vérifier le cache d'abord
|
|
|
|
|
if let Some(metadata) = self.metadata_cache.read().await.get(file_path) {
|
|
|
|
|
return Ok(metadata.clone());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extraire les métadonnées réelles
|
|
|
|
|
let metadata = self.extract_metadata_from_file(file_path).await?;
|
|
|
|
|
|
|
|
|
|
// Mettre en cache
|
|
|
|
|
self.metadata_cache
|
|
|
|
|
.write()
|
|
|
|
|
.await
|
|
|
|
|
.insert(file_path.to_path_buf(), metadata.clone());
|
|
|
|
|
|
|
|
|
|
Ok(metadata)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn extract_metadata_from_file(
|
|
|
|
|
&self,
|
|
|
|
|
file_path: &Path,
|
|
|
|
|
) -> Result<AudioMetadata, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
// Implémentation basique - dans un vrai projet, on utiliserait symphonia
|
|
|
|
|
let file_metadata = std::fs::metadata(file_path)?;
|
|
|
|
|
|
|
|
|
|
Ok(AudioMetadata {
|
|
|
|
|
duration_seconds: 180.0, // Valeur par défaut
|
|
|
|
|
sample_rate: 44100,
|
|
|
|
|
channels: 2,
|
|
|
|
|
bitrate_kbps: Some(320),
|
|
|
|
|
codec: "MP3".to_string(),
|
|
|
|
|
title: file_path
|
|
|
|
|
.file_stem()
|
|
|
|
|
.and_then(|s| s.to_str())
|
|
|
|
|
.map(|s| s.to_string()),
|
|
|
|
|
artist: None,
|
|
|
|
|
album: None,
|
|
|
|
|
year: None,
|
|
|
|
|
genre: None,
|
|
|
|
|
artwork_available: false,
|
|
|
|
|
file_size: file_metadata.len(),
|
|
|
|
|
last_modified: file_metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Génère les données de waveform pour la visualisation
|
|
|
|
|
pub async fn generate_waveform(
|
|
|
|
|
&self,
|
|
|
|
|
file_path: &Path,
|
|
|
|
|
resolution: usize,
|
|
|
|
|
) -> Result<WaveformData, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
// Vérifier le cache d'abord
|
|
|
|
|
if let Some(waveform) = self.waveform_cache.read().await.get(file_path) {
|
|
|
|
|
return Ok(waveform.clone());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Générer la waveform
|
|
|
|
|
let waveform = self.generate_waveform_data(file_path, resolution).await?;
|
|
|
|
|
|
|
|
|
|
// Mettre en cache
|
|
|
|
|
self.waveform_cache
|
|
|
|
|
.write()
|
|
|
|
|
.await
|
|
|
|
|
.insert(file_path.to_path_buf(), waveform.clone());
|
|
|
|
|
|
|
|
|
|
Ok(waveform)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn generate_waveform_data(
|
|
|
|
|
&self,
|
|
|
|
|
_file_path: &Path,
|
|
|
|
|
resolution: usize,
|
|
|
|
|
) -> Result<WaveformData, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
// Implémentation basique - génère des données de test
|
|
|
|
|
let peaks: Vec<f32> = (0..resolution)
|
|
|
|
|
.map(|i| ((i as f32 * 0.1).sin() * 0.8).abs())
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let rms: Vec<f32> = peaks.iter().map(|&p| p * 0.7).collect();
|
|
|
|
|
|
|
|
|
|
Ok(WaveformData {
|
|
|
|
|
peaks,
|
|
|
|
|
rms,
|
|
|
|
|
sample_rate: 44100,
|
|
|
|
|
duration_ms: 180000, // 3 minutes
|
|
|
|
|
generated_at: SystemTime::now(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Analyse spectrale pour obtenir les fréquences dominantes
|
|
|
|
|
pub async fn analyze_spectrum(
|
|
|
|
|
&self,
|
|
|
|
|
_file_path: &Path,
|
|
|
|
|
fft_size: usize,
|
|
|
|
|
) -> Result<Vec<f32>, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
// Implémentation basique - génère des données de test
|
|
|
|
|
let spectrum: Vec<f32> = (0..fft_size / 2)
|
|
|
|
|
.map(|i| {
|
|
|
|
|
let freq = i as f32 / (fft_size as f32 / 2.0);
|
|
|
|
|
(freq * 10.0).sin().abs() * (1.0 - freq).max(0.0)
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Ok(spectrum)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Transcode un fichier audio vers une qualité différente
|
|
|
|
|
pub async fn transcode_quality(
|
|
|
|
|
&self,
|
|
|
|
|
input_path: &Path,
|
|
|
|
|
output_path: &Path,
|
|
|
|
|
quality: AudioQuality,
|
|
|
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Transcodage de {:?} vers {:?} avec qualité {:?}",
|
|
|
|
|
input_path,
|
|
|
|
|
output_path,
|
|
|
|
|
quality
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Pour l'instant, implémentation simplifiée
|
|
|
|
|
// En production, on utiliserait FFmpeg ou un encodeur dédié
|
|
|
|
|
let metadata = self.extract_metadata(input_path).await?;
|
|
|
|
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
"Transcodage requis: {} Hz -> {} Hz, {} -> {} canaux",
|
|
|
|
|
metadata.sample_rate,
|
|
|
|
|
quality.sample_rate,
|
|
|
|
|
metadata.channels,
|
|
|
|
|
quality.channels
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Cette fonction serait complétée avec un vrai transcodeur
|
|
|
|
|
tracing::warn!("Transcodage non encore implémenté - nécessite un encodeur MP3/AAC");
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Nettoie les caches expirés
|
|
|
|
|
pub async fn cleanup_caches(&self, max_age_hours: u64) {
|
|
|
|
|
let cutoff = SystemTime::now() - std::time::Duration::from_secs(max_age_hours * 3600);
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let mut metadata_cache = self.metadata_cache.write().await;
|
|
|
|
|
let before_count = metadata_cache.len();
|
|
|
|
|
metadata_cache.retain(|_, _| true); // Pour l'instant, pas de timestamp sur les métadonnées
|
|
|
|
|
tracing::debug!("Nettoyage cache métadonnées: {} entrées", before_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let mut waveform_cache = self.waveform_cache.write().await;
|
|
|
|
|
let before_count = waveform_cache.len();
|
|
|
|
|
waveform_cache.retain(|_, waveform| waveform.generated_at > cutoff);
|
|
|
|
|
let after_count = waveform_cache.len();
|
|
|
|
|
if before_count > after_count {
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Nettoyage cache waveform: {} -> {} entrées",
|
|
|
|
|
before_count,
|
|
|
|
|
after_count
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Obtient les statistiques des caches
|
|
|
|
|
pub async fn get_cache_stats(&self) -> serde_json::Value {
|
|
|
|
|
let metadata_cache = self.metadata_cache.read().await;
|
|
|
|
|
let waveform_cache = self.waveform_cache.read().await;
|
|
|
|
|
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"metadata_cache_entries": metadata_cache.len(),
|
|
|
|
|
"waveform_cache_entries": waveform_cache.len(),
|
|
|
|
|
"total_cache_entries": metadata_cache.len() + waveform_cache.len()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn clear_cache(&self) {
|
|
|
|
|
self.metadata_cache.write().await.clear();
|
|
|
|
|
self.waveform_cache.write().await.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for AudioProcessor {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
// Utiliser une configuration par défaut simple pour les tests
|
|
|
|
|
let config = Config::from_env().unwrap_or_else(|_| {
|
|
|
|
|
// Valeurs par défaut minimales si from_env échoue
|
|
|
|
|
Config {
|
|
|
|
|
secret_key: "default_secret_key_for_testing_only".to_string(),
|
|
|
|
|
port: 8082,
|
|
|
|
|
backend_url: "http://backend-api:8080".to_string(),
|
|
|
|
|
audio_dir: "./audio".to_string(),
|
|
|
|
|
allowed_origins: vec!["*".to_string()],
|
|
|
|
|
max_file_size: 100 * 1024 * 1024,
|
|
|
|
|
max_range_size: 10 * 1024 * 1024,
|
|
|
|
|
signature_tolerance: 60,
|
|
|
|
|
database: crate::config::DatabaseConfig {
|
|
|
|
|
url: std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
|
|
|
|
"postgres://veza:veza_password@postgres:5432/veza_db?sslmode=disable"
|
|
|
|
|
.to_string()
|
|
|
|
|
}),
|
|
|
|
|
max_connections: 10,
|
|
|
|
|
min_connections: 1,
|
|
|
|
|
connection_timeout: Duration::from_secs(30),
|
|
|
|
|
idle_timeout: Duration::from_secs(600),
|
|
|
|
|
max_lifetime: Duration::from_secs(1800),
|
|
|
|
|
enable_logging: false,
|
|
|
|
|
migrate_on_start: true,
|
|
|
|
|
},
|
|
|
|
|
cache: crate::config::CacheConfig {
|
|
|
|
|
max_size_mb: 256,
|
|
|
|
|
ttl_seconds: 3600,
|
|
|
|
|
cleanup_interval: Duration::from_secs(300),
|
|
|
|
|
compression_enabled: false,
|
|
|
|
|
redis_url: None,
|
|
|
|
|
redis_pool_size: None,
|
|
|
|
|
},
|
|
|
|
|
security: crate::config::SecurityConfig {
|
|
|
|
|
jwt_secret: None,
|
2026-03-05 18:22:31 +00:00
|
|
|
jwt_public_key_path: None,
|
2025-12-03 19:36:56 +00:00
|
|
|
jwt_expiration: Duration::from_secs(3600),
|
|
|
|
|
bcrypt_cost: 10,
|
|
|
|
|
rate_limit_requests_per_minute: 60,
|
|
|
|
|
rate_limit_burst: 10,
|
|
|
|
|
cors_max_age: Duration::from_secs(3600),
|
|
|
|
|
csrf_protection: false,
|
|
|
|
|
secure_headers: true,
|
|
|
|
|
tls_cert_path: None,
|
|
|
|
|
tls_key_path: None,
|
|
|
|
|
},
|
|
|
|
|
performance: crate::config::PerformanceConfig {
|
|
|
|
|
worker_threads: None,
|
|
|
|
|
max_blocking_threads: None,
|
|
|
|
|
thread_stack_size: None,
|
|
|
|
|
tcp_nodelay: true,
|
|
|
|
|
tcp_keepalive: None,
|
|
|
|
|
buffer_size: 65536,
|
|
|
|
|
max_concurrent_streams: 100,
|
|
|
|
|
stream_timeout: Duration::from_secs(30),
|
|
|
|
|
compression_level: 6,
|
|
|
|
|
},
|
|
|
|
|
monitoring: crate::config::MonitoringConfig {
|
|
|
|
|
metrics_enabled: false,
|
|
|
|
|
metrics_port: 9090,
|
|
|
|
|
health_check_interval: Duration::from_secs(30),
|
|
|
|
|
log_level: "info".to_string(),
|
|
|
|
|
log_format: crate::config::LogFormat::Pretty,
|
|
|
|
|
jaeger_endpoint: None,
|
|
|
|
|
prometheus_namespace: "stream_server".to_string(),
|
|
|
|
|
alert_webhooks: vec![],
|
|
|
|
|
},
|
|
|
|
|
notifications: crate::config::NotificationConfig {
|
|
|
|
|
enabled: false,
|
|
|
|
|
max_queue_size: 1000,
|
|
|
|
|
delivery_workers: 2,
|
|
|
|
|
retry_attempts: 3,
|
|
|
|
|
retry_delay: Duration::from_secs(60),
|
|
|
|
|
batch_size: 10,
|
|
|
|
|
email_provider: None,
|
|
|
|
|
sms_provider: None,
|
|
|
|
|
push_provider: None,
|
|
|
|
|
},
|
|
|
|
|
compression: crate::config::CompressionConfig {
|
|
|
|
|
enabled: false,
|
|
|
|
|
output_dir: "./compressed".to_string(),
|
|
|
|
|
temp_dir: "./temp".to_string(),
|
|
|
|
|
max_concurrent_jobs: 2,
|
|
|
|
|
cleanup_after_days: 30,
|
|
|
|
|
ffmpeg_path: None,
|
|
|
|
|
quality_profiles: vec![],
|
|
|
|
|
},
|
|
|
|
|
rabbit_mq: crate::config::RabbitMQConfig {
|
|
|
|
|
// Added rabbit_mq config
|
|
|
|
|
url: "amqp://guest:guest@localhost:5672/".to_string(),
|
|
|
|
|
max_retries: 3,
|
|
|
|
|
retry_interval_secs: 2,
|
|
|
|
|
enable: true,
|
|
|
|
|
},
|
|
|
|
|
environment: crate::config::Environment::Development,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
Self::new(Arc::new(config))
|
|
|
|
|
}
|
|
|
|
|
}
|