diff --git a/pmoparadise/src/channels.rs b/pmoparadise/src/channels.rs new file mode 100644 index 00000000..2c1779ef --- /dev/null +++ b/pmoparadise/src/channels.rs @@ -0,0 +1,143 @@ +//! Radio Paradise channel definitions +//! +//! This module defines the available Radio Paradise channels and their metadata. + +use std::str::FromStr; + +/// Logical identifier for a Radio Paradise channel. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ParadiseChannelKind { + Main, + Mellow, + Rock, + Eclectic, +} + +impl ParadiseChannelKind { + pub const fn id(self) -> u8 { + match self { + Self::Main => 0, + Self::Mellow => 1, + Self::Rock => 2, + Self::Eclectic => 3, + } + } + + pub const fn slug(self) -> &'static str { + match self { + Self::Main => "main", + Self::Mellow => "mellow", + Self::Rock => "rock", + Self::Eclectic => "eclectic", + } + } + + pub const fn display_name(self) -> &'static str { + match self { + Self::Main => "Main Mix", + Self::Mellow => "Mellow Mix", + Self::Rock => "Rock Mix", + Self::Eclectic => "Eclectic Mix", + } + } + + pub const fn description(self) -> &'static str { + match self { + Self::Main => "Eclectic mix of rock, world, electronica, and more", + Self::Mellow => "Mellower, less aggressive music", + Self::Rock => "Heavier, more guitar-driven music", + Self::Eclectic => "Curated worldwide selection", + } + } +} + +impl FromStr for ParadiseChannelKind { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_ascii_lowercase().as_str() { + "main" | "0" => Ok(Self::Main), + "mellow" | "1" => Ok(Self::Mellow), + "rock" | "2" => Ok(Self::Rock), + "eclectic" | "3" => Ok(Self::Eclectic), + other => Err(anyhow::anyhow!("Unknown Radio Paradise channel: {}", other)), + } + } +} + +/// Metadata descriptor for a channel. +#[derive(Debug, Clone, Copy)] +pub struct ChannelDescriptor { + pub kind: ParadiseChannelKind, + pub id: u8, + pub slug: &'static str, + pub display_name: &'static str, + pub description: &'static str, +} + +impl ChannelDescriptor { + pub const fn new(kind: ParadiseChannelKind) -> Self { + Self { + id: kind.id(), + slug: kind.slug(), + display_name: kind.display_name(), + description: kind.description(), + kind, + } + } +} + +/// All available Radio Paradise channels +pub const ALL_CHANNELS: [ChannelDescriptor; 4] = [ + ChannelDescriptor::new(ParadiseChannelKind::Main), + ChannelDescriptor::new(ParadiseChannelKind::Mellow), + ChannelDescriptor::new(ParadiseChannelKind::Rock), + ChannelDescriptor::new(ParadiseChannelKind::Eclectic), +]; + +/// Returns the maximum valid channel ID +pub const fn max_channel_id() -> u8 { + (ALL_CHANNELS.len() - 1) as u8 +} + +/// Default maximum number of tracks to keep in history +/// +/// This is used as the default if not configured via pmoconfig. +/// Value: 100 tracks - represents ~5-8 hours of playback history +pub const HISTORY_DEFAULT_MAX_TRACKS: usize = 100; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_channel_ids() { + assert_eq!(ParadiseChannelKind::Main.id(), 0); + assert_eq!(ParadiseChannelKind::Mellow.id(), 1); + assert_eq!(ParadiseChannelKind::Rock.id(), 2); + assert_eq!(ParadiseChannelKind::Eclectic.id(), 3); + } + + #[test] + fn test_max_channel_id() { + assert_eq!(max_channel_id(), 3); + } + + #[test] + fn test_all_channels_length() { + assert_eq!(ALL_CHANNELS.len(), 4); + } + + #[test] + fn test_channel_from_str() { + assert!(matches!( + "main".parse::(), + Ok(ParadiseChannelKind::Main) + )); + assert!(matches!( + "0".parse::(), + Ok(ParadiseChannelKind::Main) + )); + assert!("invalid".parse::().is_err()); + } +} diff --git a/pmoparadise/src/paradise/worker.rs b/pmoparadise/src/paradise/worker.rs index c502436e..c3986389 100644 --- a/pmoparadise/src/paradise/worker.rs +++ b/pmoparadise/src/paradise/worker.rs @@ -547,59 +547,6 @@ impl WorkerState { Ok(()) } - async fn process_song( - &self, - block: &Block, - song_index: &usize, - song: &Song, - position: usize, - ordered_songs: &[(usize, &Song)], - total_frames: usize, - decoded: &DecodedBlock, - ) -> Result> { - let duration_ms = song_duration_ms(block, ordered_songs, position); - let start_frame = ms_to_frames(song.elapsed, decoded.sample_rate); - let end_frame = if position + 1 < ordered_songs.len() { - ms_to_frames(ordered_songs[position + 1].1.elapsed, decoded.sample_rate) - } else { - total_frames - }; - - if end_frame <= start_frame || end_frame > total_frames { - warn!( - channel = self.descriptor.slug, - song_index = song_index, - "Invalid frame range for song, skipping" - ); - return Err(anyhow!("Invalid frame range")); - } - - let channels = decoded.channels; - let start = start_frame * channels; - let end = end_frame * channels; - let slice = decoded - .samples - .get(start..end) - .ok_or_else(|| anyhow!("Sample slice out of bounds"))?; - - let track_samples = slice.to_vec(); - encode_song_to_cache( - Arc::clone(&self.cache_manager), - self.descriptor.id, - self.descriptor.slug, - block.clone(), - *song_index, - song.clone(), - track_samples, - decoded.sample_rate, - decoded.channels, - decoded.bits_per_sample, - self.active_clients, - duration_ms, - ) - .await - } - /// Stocke les métadonnées Radio Paradise pour un fichier audio caché /// /// Cette fonction persiste toutes les métadonnées RP dans la base de données @@ -855,134 +802,6 @@ impl BackoffState { } } -struct DecodedBlock { - samples: Vec, - channels: usize, - sample_rate: u32, - bits_per_sample: u32, -} - -fn song_duration_ms(block: &Block, ordered: &[(usize, &Song)], position: usize) -> u64 { - let song = ordered[position].1; - if song.duration > 0 { - return song.duration; - } - - if let Some((_, next_song)) = ordered.get(position + 1) { - return next_song.elapsed.saturating_sub(song.elapsed); - } - - block.length.saturating_sub(song.elapsed) -} - -fn ms_to_frames(ms: u64, sample_rate: u32) -> usize { - ((ms as u128 * sample_rate as u128) / 1000) as usize -} - -fn decode_block_audio(data: Vec) -> anyhow::Result { - use symphonia::core::audio::SampleBuffer; - use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL}; - use symphonia::core::errors::Error as SymphoniaError; - use symphonia::core::formats::FormatOptions; - use symphonia::core::io::MediaSourceStream; - use symphonia::core::meta::MetadataOptions; - use symphonia::core::probe::Hint; - - let cursor = std::io::Cursor::new(data); - let mss = MediaSourceStream::new(Box::new(cursor), Default::default()); - - let hint = Hint::new(); - let probed = symphonia::default::get_probe() - .format( - &hint, - mss, - &FormatOptions::default(), - &MetadataOptions::default(), - ) - .map_err(|e| anyhow!("Failed to probe format: {e}"))?; - - let mut format = probed.format; - - let track = format - .tracks() - .iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .ok_or_else(|| anyhow!("No audio track found"))?; - - let mut decoder = symphonia::default::get_codecs() - .make(&track.codec_params, &DecoderOptions::default()) - .map_err(|e| anyhow!("Failed to create decoder: {e}"))?; - - let channels = track - .codec_params - .channels - .ok_or_else(|| anyhow!("Missing channel info"))? - .count(); - - let sample_rate = track - .codec_params - .sample_rate - .ok_or_else(|| anyhow!("Missing sample rate"))?; - - let bits_per_sample = track.codec_params.bits_per_sample.unwrap_or(16); - - let mut samples_i32 = Vec::new(); - let track_id = track.id; - - loop { - let packet = match format.next_packet() { - Ok(packet) => packet, - Err(SymphoniaError::ResetRequired) => { - decoder.reset(); - continue; - } - Err(SymphoniaError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => { - break; - } - Err(e) => return Err(anyhow!("Decode error: {e}")), - }; - - if packet.track_id() != track_id { - continue; - } - - match decoder.decode(&packet) { - Ok(decoded) => { - let spec = *decoded.spec(); - let duration = decoded.capacity() as u64; - let mut sample_buf = SampleBuffer::::new(duration, spec); - sample_buf.copy_interleaved_ref(decoded); - samples_i32.extend_from_slice(sample_buf.samples()); - } - Err(SymphoniaError::DecodeError(_)) => continue, - Err(e) => return Err(anyhow!("Decode error: {e}")), - } - } - - if samples_i32.is_empty() { - return Err(anyhow!("No samples decoded")); - } - - let (normalized_samples, target_bits): (Vec, u32) = match bits_per_sample { - 0..=16 => { - let samples = samples_i32.iter().map(|&s| (s >> 16) as i32).collect(); - (samples, 16) - } - 17..=24 => { - let samples = samples_i32.iter().map(|&s| (s >> 8) as i32).collect(); - (samples, 24) - } - _ => (samples_i32, 32), - }; - - Ok(DecodedBlock { - samples: normalized_samples, - channels, - sample_rate, - bits_per_sample: target_bits, - }) -} - fn compute_track_id_for_descriptor(descriptor_id: u8, block: &Block, song_index: usize) -> String { format!( "rp:{}:event_{}_song_{}", diff --git a/pmoparadise/src/pmoserver_ext.rs b/pmoparadise/src/pmoserver_ext.rs index f21d7f16..0ee3e5bd 100644 --- a/pmoparadise/src/pmoserver_ext.rs +++ b/pmoparadise/src/pmoserver_ext.rs @@ -3,31 +3,23 @@ //! Ce module fournit un trait d'extension pour ajouter facilement l'API Radio Paradise //! à un serveur pmoserver. -use crate::paradise::{max_channel_id, ParadiseChannel, PlaylistEntry, ALL_CHANNELS}; -use crate::{Block, NowPlaying, RadioParadiseClient, RadioParadiseSource}; +use crate::paradise::{max_channel_id, ALL_CHANNELS}; +use crate::{Block, NowPlaying, RadioParadiseClient}; use axum::{ - body::Body, extract::{Path, Query, State}, - http::{HeaderMap, HeaderName, HeaderValue, StatusCode}, - response::IntoResponse, + http::StatusCode, routing::get, Json, Router, }; -use chrono::{DateTime, Utc}; -use futures::StreamExt; -use pmosource::api::CacheStatusInfo; -use pmosource::CacheStatus; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; -use tracing::error; -use utoipa::{IntoParams, OpenApi, ToSchema}; +use utoipa::{OpenApi, ToSchema}; /// État partagé pour l'API Radio Paradise #[derive(Clone)] pub struct RadioParadiseState { client: Arc>, - source: Arc, } #[derive(Debug, Default, Deserialize)] @@ -36,46 +28,14 @@ struct ParadiseQuery { channel: Option, } -#[derive(Debug, Default, Deserialize, IntoParams)] -#[serde(default)] -#[into_params(parameter_in = Query)] -struct ListLimitQuery { - /// Nombre maximum d'éléments à retourner (0 = tous) - #[serde(default)] - limit: Option, -} - impl RadioParadiseState { pub async fn new() -> anyhow::Result { let client = RadioParadiseClient::new() .await .map_err(|e| anyhow::anyhow!("Failed to create RadioParadise client: {}", e))?; - #[cfg(feature = "server")] - let source = RadioParadiseSource::from_registry_default(client.clone()) - .map_err(|e| anyhow::anyhow!(e.to_string()))?; - - #[cfg(not(feature = "server"))] - let source = { - let base_dir = std::env::temp_dir().join("pmoparadise_api"); - let cover_dir = base_dir.join("covers"); - let audio_dir = base_dir.join("audio"); - std::fs::create_dir_all(&cover_dir)?; - std::fs::create_dir_all(&audio_dir)?; - - let cover_cache = Arc::new(pmocovers::cache::new_cache( - cover_dir.to_string_lossy().as_ref(), - 256, - )?); - let audio_cache = Arc::new(pmoaudiocache::cache::new_cache( - audio_dir.to_string_lossy().as_ref(), - 256, - )?); - RadioParadiseSource::new_default(client.clone(), cover_cache, audio_cache) - }; Ok(Self { client: Arc::new(RwLock::new(client)), - source: Arc::new(source), }) } @@ -100,19 +60,6 @@ impl RadioParadiseState { Ok(client) } - - fn channel_for_id(&self, channel_id: u8) -> Result, StatusCode> { - if channel_id > max_channel_id() { - return Err(StatusCode::BAD_REQUEST); - } - self.source - .channel(channel_id) - .ok_or(StatusCode::SERVICE_UNAVAILABLE) - } - - pub fn source(&self) -> Arc { - self.source.clone() - } } /// Information sur un canal Radio Paradise @@ -365,425 +312,28 @@ async fn get_channels() -> Json> { Json(channels) } -/// Statut opérationnel d'un canal Radio Paradise -#[derive(Debug, Clone, Serialize, ToSchema)] -pub struct ChannelStatusResponse { - /// ID numérique du canal - pub channel_id: u8, - /// Slug du canal (main, mellow, ...) - pub slug: String, - /// Nom complet du canal - pub name: String, - /// Description - pub description: String, - /// Nombre de clients connectés au flux - pub active_clients: usize, - /// Nombre de morceaux présents dans la file d'attente - pub queue_length: usize, - /// Valeur courante d'update_id - pub update_id: u32, - /// Dernière modification (RFC3339) - pub last_change: Option, - /// Nombre total d'entrées en historique (persisté) - pub history_entries: usize, - /// Limite configurée pour l'historique - pub history_max_tracks: usize, - /// Le canal est-il activé dans la configuration ? - pub configured: bool, - /// Identifiant de collection pour le cache - pub cache_collection_id: String, - /// Nombre total de pistes connues du cache - pub cache_total_tracks: usize, - /// Nombre de pistes déjà en cache - pub cache_cached_tracks: usize, -} - -/// Entrée détaillée de la file d'attente -#[derive(Debug, Clone, Serialize, ToSchema)] -pub struct ChannelPlaylistEntry { - /// Position dans la file - pub index: usize, - /// ID unique de la piste - pub track_id: String, - /// ID du canal - pub channel_id: u8, - /// Titre du morceau - pub title: String, - /// Artiste - pub artist: String, - /// Album - pub album: Option, - /// URL de couverture (si disponible) - pub cover_url: Option, - /// Durée du morceau en ms - pub duration_ms: u64, - /// Offset dans le block (ms) - pub elapsed_ms: u64, - /// Horodatage prévu/démarré (RFC3339) - pub started_at: String, - /// Nombre de clients restants à servir - pub pending_clients: usize, - /// Note éventuelle (0-10) - pub rating: Option, - /// Année éventuelle - pub year: Option, - /// Statut de cache - pub cache_status: CacheStatusInfo, -} - -impl ChannelPlaylistEntry { - fn from_entry(entry: &Arc, index: usize, cache_status: CacheStatusInfo) -> Self { - let song = entry.song.as_ref(); - Self { - index, - track_id: entry.track_id.clone(), - channel_id: entry.channel_id, - title: song.title.clone(), - artist: song.artist.clone(), - album: song.album.clone(), - cover_url: song.cover.clone(), - duration_ms: entry.duration_ms, - elapsed_ms: song.elapsed, - started_at: entry.started_at.to_rfc3339(), - pending_clients: entry.pending_clients(), - rating: song.rating, - year: song.year, - cache_status, - } - } -} - -/// Réponse pour la file d'attente d'un canal -#[derive(Debug, Clone, Serialize, ToSchema)] -pub struct ChannelPlaylistResponse { - /// ID du canal - pub channel_id: u8, - /// Slug du canal - pub slug: String, - /// Update ID du playlist - pub update_id: u32, - /// Taille totale de la file au moment de la capture - pub queue_length: usize, - /// Entrées retournées - pub items: Vec, -} - -/// Entrée d'historique d'écoute -#[derive(Debug, Clone, Serialize, ToSchema)] -pub struct ChannelHistoryEntry { - /// ID unique de la piste - pub track_id: String, - /// ID du canal - pub channel_id: u8, - /// Titre - pub title: String, - /// Artiste - pub artist: String, - /// Album - pub album: Option, - /// URL de couverture - pub cover_url: Option, - /// Début de lecture (RFC3339) - pub started_at: String, - /// Durée en ms - pub duration_ms: u64, -} - -/// Réponse pour l'historique d'un canal -#[derive(Debug, Clone, Serialize, ToSchema)] -pub struct ChannelHistoryResponse { - /// ID du canal - pub channel_id: u8, - /// Slug du canal - pub slug: String, - /// Nombre total d'entrées disponibles - pub total_available: usize, - /// Nombre d'entrées retournées dans cette réponse - pub returned: usize, - /// Entrées - pub entries: Vec, -} - -/// GET /channels/{channel_id}/status - Statut détaillé d'un canal -#[utoipa::path( - get, - path = "/channels/{channel_id}/status", - params( - ("channel_id" = u8, Path, description = "Channel ID (0-3)") - ), - responses( - (status = 200, description = "Statut du canal", body = ChannelStatusResponse), - (status = 400, description = "Canal invalide"), - (status = 503, description = "Canal indisponible"), - (status = 500, description = "Erreur interne lors de la récupération du statut") - ), - tag = "Radio Paradise" -)] -async fn get_channel_status( - State(state): State, - Path(channel_id): Path, -) -> Result, StatusCode> { - let channel = state.channel_for_id(channel_id)?; - let descriptor = channel.descriptor(); - - let playlist = channel.playlist(); - let queue_length = playlist.active_len().await; - let update_id = playlist.update_id(); - let last_change = playlist - .last_change() - .await - .map(|ts| DateTime::::from(ts).to_rfc3339()); - - let history_len = channel.history_backend().len().await.map_err(|e| { - error!( - channel = descriptor.slug, - "Failed to retrieve history size: {e:?}" - ); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - let cache_stats = channel.cache_manager().statistics().await; - - let status = ChannelStatusResponse { - channel_id, - slug: descriptor.slug.to_string(), - name: descriptor.display_name.to_string(), - description: descriptor.description.to_string(), - active_clients: channel.active_client_count(), - queue_length, - update_id, - last_change, - history_entries: history_len, - history_max_tracks: channel.history_max_tracks(), - configured: true, // All channels are always available - cache_collection_id: cache_stats.collection_id, - cache_total_tracks: cache_stats.total_tracks, - cache_cached_tracks: cache_stats.cached_tracks, - }; - - Ok(Json(status)) -} - -/// GET /channels/{channel_id}/playlist - File d'attente du canal -#[utoipa::path( - get, - path = "/channels/{channel_id}/playlist", - params( - ("channel_id" = u8, Path, description = "Channel ID (0-3)"), - ListLimitQuery - ), - responses( - (status = 200, description = "File d'attente courante", body = ChannelPlaylistResponse), - (status = 400, description = "Canal invalide"), - (status = 503, description = "Canal indisponible"), - (status = 500, description = "Erreur lors de la récupération de la file d'attente") - ), - tag = "Radio Paradise" -)] -async fn get_channel_playlist( - State(state): State, - Path(channel_id): Path, - Query(query): Query, -) -> Result, StatusCode> { - let channel = state.channel_for_id(channel_id)?; - let descriptor = channel.descriptor(); - let playlist = channel.playlist(); - let snapshot = playlist.active_snapshot().await; - let total_len = snapshot.len(); - let limit = query.limit.filter(|limit| *limit > 0).unwrap_or(total_len); - - let cache_manager = channel.cache_manager(); - let mut items = Vec::new(); - - for (index, entry) in snapshot.into_iter().enumerate().take(limit) { - let cache_status = match cache_manager.get_cache_status(&entry.track_id).await { - Ok(status) => status, - Err(err) => CacheStatus::Failed { - error: err.to_string(), - }, - }; - - items.push(ChannelPlaylistEntry::from_entry( - &entry, - index, - CacheStatusInfo::from(cache_status), - )); - } - - let response = ChannelPlaylistResponse { - channel_id, - slug: descriptor.slug.to_string(), - update_id: playlist.update_id(), - queue_length: total_len, - items, - }; - - Ok(Json(response)) -} - -/// GET /channels/{channel_id}/history - Historique récent du canal -#[utoipa::path( - get, - path = "/channels/{channel_id}/history", - params( - ("channel_id" = u8, Path, description = "Channel ID (0-3)"), - ListLimitQuery - ), - responses( - (status = 200, description = "Historique récent", body = ChannelHistoryResponse), - (status = 400, description = "Canal invalide"), - (status = 503, description = "Canal indisponible"), - (status = 500, description = "Erreur lors de la récupération de l'historique") - ), - tag = "Radio Paradise" -)] -async fn get_channel_history( - State(state): State, - Path(channel_id): Path, - Query(query): Query, -) -> Result, StatusCode> { - let channel = state.channel_for_id(channel_id)?; - let descriptor = channel.descriptor(); - let backend = channel.history_backend().clone(); - let limit = query.limit.unwrap_or(50); - - let entries_raw = backend.recent(limit).await.map_err(|e| { - error!( - channel = descriptor.slug, - "Failed to retrieve channel history: {e:?}" - ); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - let total_available = backend.len().await.map_err(|e| { - error!( - channel = descriptor.slug, - "Failed to count channel history entries: {e:?}" - ); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - let entries: Vec = entries_raw - .into_iter() - .map(|entry| ChannelHistoryEntry { - track_id: entry.track_id, - channel_id: entry.channel_id, - title: entry.song.title, - artist: entry.song.artist, - album: entry.song.album, - cover_url: entry.song.cover_url, - started_at: entry.started_at.to_rfc3339(), - duration_ms: entry.duration_ms, - }) - .collect(); - - let response = ChannelHistoryResponse { - channel_id, - slug: descriptor.slug.to_string(), - total_available, - returned: entries.len(), - entries, - }; - - Ok(Json(response)) -} - -/// GET /channels/{channel_id}/stream/{connection_id} - Stream audio pour une connexion spécifique -#[utoipa::path( - get, - path = "/channels/{channel_id}/stream/{connection_id}", - params( - ("channel_id" = u8, Path, description = "Channel ID (0-3)"), - ("connection_id" = i32, Path, description = "Connection ID fourni par le media server") - ), - responses( - (status = 200, description = "Flux audio FLAC (gapless)", content_type = "audio/flac"), - (status = 400, description = "Canal invalide"), - (status = 503, description = "Canal indisponible") - ), - tag = "Radio Paradise" -)] -async fn stream_channel_by_connection( - State(state): State, - Path((channel_id, connection_id)): Path<(u8, i32)>, -) -> Result { - let channel = state.channel_for_id(channel_id)?; - - // Convertir connection_id en String pour l'utiliser comme client_id - let client_id = connection_id.to_string(); - - let client_stream = channel.connect_client(client_id).await.map_err(|e| { - error!("Failed to create streaming client: {e:?}"); - StatusCode::SERVICE_UNAVAILABLE - })?; - - let stream = client_stream - .into_byte_stream() - .map(|chunk| chunk.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))); - - let body = Body::from_stream(stream); - - let mut headers = HeaderMap::new(); - headers.insert( - axum::http::header::CONTENT_TYPE, - HeaderValue::from_static("audio/flac"), - ); - headers.insert( - axum::http::header::CACHE_CONTROL, - HeaderValue::from_static("no-cache"), - ); - headers.insert( - HeaderName::from_static("icy-name"), - HeaderValue::from_static("Radio Paradise"), - ); - headers.insert( - HeaderName::from_static("icy-genre"), - HeaderValue::from_static("Eclectic"), - ); - headers.insert( - HeaderName::from_static("icy-description"), - HeaderValue::from_static("PMO Radio Paradise relay"), - ); - headers.insert( - HeaderName::from_static("icy-metaint"), - HeaderValue::from_static("0"), - ); - - Ok((headers, body)) -} - /// Documentation OpenAPI pour l'API Radio Paradise #[derive(OpenApi)] #[openapi( info( title = "Radio Paradise API", version = "1.0.0", - description = "API REST pour accéder aux métadonnées et streams de Radio Paradise" + description = "API REST pour accéder aux métadonnées de Radio Paradise" ), paths( get_now_playing, get_current_block, get_block_by_id, - get_channels, - get_channel_status, - get_channel_playlist, - get_channel_history, - stream_channel_by_connection + get_channels ), components(schemas( NowPlayingResponse, BlockResponse, SongInfo, - ChannelInfo, - ChannelStatusResponse, - ChannelPlaylistEntry, - ChannelPlaylistResponse, - ChannelHistoryEntry, - ChannelHistoryResponse, - CacheStatusInfo + ChannelInfo )), tags( - (name = "Radio Paradise", description = "Endpoints pour Radio Paradise streaming") + (name = "Radio Paradise", description = "Endpoints pour Radio Paradise") ) )] pub struct RadioParadiseApiDoc; @@ -795,13 +345,6 @@ pub fn create_api_router(state: RadioParadiseState) -> Router { .route("/block/current", get(get_current_block)) .route("/block/{event_id}", get(get_block_by_id)) .route("/channels", get(get_channels)) - .route("/channels/{channel_id}/status", get(get_channel_status)) - .route("/channels/{channel_id}/playlist", get(get_channel_playlist)) - .route("/channels/{channel_id}/history", get(get_channel_history)) - .route( - "/channels/{channel_id}/stream/{connection_id}", - get(stream_channel_by_connection), - ) .with_state(state) } @@ -817,7 +360,7 @@ pub trait RadioParadiseExt { /// - API: `/api/radioparadise/*` /// - `/now-playing` /// - `/block/*` - /// - `/channels/{channel_id}/stream/{connection_id}` + /// - `/channels` /// - Swagger: `/swagger-ui/radioparadise` async fn init_radioparadise(&mut self) -> anyhow::Result; }