Files
pmomusic/pmoplaylist/ARCHITECTURE.md

520 lines
13 KiB
Markdown
Raw Normal View History

2025-10-16 22:13:00 +02:00
# Architecture de pmoplaylist
## Vue d'ensemble
`pmoplaylist` est une bibliothèque Rust qui fournit une abstraction de playlist FIFO (First-In-First-Out) thread-safe pour des MediaServers UPnP/OpenHome. Elle gère la logique de playlist pure sans aucune dépendance réseau ou protocole UPnP.
## Design Patterns
### 1. Arc + RwLock Pattern (Thread Safety)
```rust
pub struct FifoPlaylist {
inner: Arc<RwLock<FifoPlaylistInner>>,
}
```
**Raison** : Permet le clonage léger de `FifoPlaylist` et le partage entre threads/tasks tout en garantissant un accès concurrent sécurisé.
**Avantages** :
- Clone peu coûteux (clone uniquement le `Arc`, pas les données)
- Accès concurrent : plusieurs lecteurs simultanés, un seul écrivain
- Compatible avec tokio et les runtimes asynchrones
**Exemple d'utilisation** :
```rust
let playlist = FifoPlaylist::new(...);
let p1 = playlist.clone(); // Pour un thread
let p2 = playlist.clone(); // Pour un autre thread
```
### 2. Builder Pattern pour Track
```rust
Track::new("id", "title", "uri")
.with_artist("Artist")
.with_album("Album")
.with_duration(300)
.with_image("url");
```
**Raison** : Facilite la création de tracks avec métadonnées optionnelles de manière fluide et lisible.
### 3. FIFO avec VecDeque
```rust
struct FifoPlaylistInner {
queue: VecDeque<Track>,
capacity: usize,
// ...
}
```
**Raison** : `VecDeque` offre des opérations O(1) pour `push_back` et `pop_front`, parfait pour une FIFO.
**Gestion de la capacité** :
- Lors de `append_track()`, si `len >= capacity`, on appelle `pop_front()` automatiquement
- Garantit que la playlist ne dépasse jamais la capacité configurée
## Structures de données
### Track
```rust
pub struct Track {
pub id: String, // Identifiant unique
pub title: String, // Titre du morceau
pub artist: Option<String>, // Artiste
pub album: Option<String>, // Album
pub duration: Option<u32>, // Durée en secondes
pub uri: String, // URI du fichier/flux
pub image: Option<String>, // URL de la cover
}
```
**Sérialisation** : Implémente `Serialize` et `Deserialize` pour faciliter l'export JSON/autre.
### FifoPlaylistInner
```rust
struct FifoPlaylistInner {
id: String, // ID unique de la playlist
title: String, // Titre de la playlist
default_image: &'static [u8], // Image par défaut embarquée
capacity: usize, // Capacité max de la FIFO
queue: VecDeque<Track>, // Queue des tracks
update_id: u32, // Compteur de modifications
last_change: SystemTime, // Timestamp dernière modif
}
```
**update_id** :
- Incrémenté à chaque modification (append, remove, clear)
- Permet aux clients UPnP de détecter les changements
- Utilise `wrapping_add()` pour éviter les débordements
## Intégration DIDL-Lite
### Génération de Container
```rust
pub async fn as_container(&self) -> Container
```
**Produit** :
```xml
<container id="playlist-id" parentID="0" childCount="5">
<dc:title>My Playlist</dc:title>
<upnp:class>object.container.playlistContainer</upnp:class>
</container>
```
**Utilisation** : Pour exposer la playlist comme container dans le ContentDirectory UPnP.
### Génération d'Items
```rust
pub async fn as_objects(
offset: usize,
count: usize,
default_image_url: Option<&str>
) -> Vec<Item>
```
**Produit** : Un vecteur d'objets `pmodidl::Item` représentant les tracks.
**Mapping Track → DIDL Item** :
- `track.id``item.id`
- `track.title``item.title`
- `track.artist``item.artist` et `item.creator`
- `track.album``item.album`
- `track.uri``resource.url`
- `track.duration` (secondes) → `resource.duration` (format "H:MM:SS")
- `track.image` ou `default_image_url``item.album_art`
**Classe UPnP** : Tous les items ont la classe `object.item.audioItem.musicTrack`.
## Gestion de l'image par défaut
### Intégration avec `include_bytes!`
```rust
pub const DEFAULT_IMAGE: &[u8] = include_bytes!("../assets/default.webp");
```
**Avantages** :
- L'image est compilée directement dans le binaire
- Pas de dépendance au système de fichiers à l'exécution
- Accès instantané et thread-safe
### Format WebP
**Raison du choix** :
- Format moderne et efficace
- Meilleure compression que JPEG/PNG
- Support alpha (transparence)
- Largement supporté par les navigateurs et clients modernes
**Spécifications** :
- Dimension : 300x300 pixels
- Format : WebP
- Qualité : 85
- Taille : ~9-10 KB
### Utilisation
```rust
let image_bytes = playlist.default_image().await;
// Servir via HTTP avec Content-Type: image/webp
```
## Concurrence et Thread Safety
### Scenario 1 : Lecture concurrente
```rust
// Thread 1
let len = playlist.len().await;
// Thread 2 (simultané)
let items = playlist.get_items(0, 10).await;
```
**Comportement** : Les deux opérations peuvent s'exécuter simultanément car `RwLock` permet plusieurs lecteurs.
### Scenario 2 : Écriture exclusive
```rust
// Thread 1
playlist.append_track(track1).await;
// Thread 2 (simultané)
playlist.append_track(track2).await;
```
**Comportement** : Les opérations sont sérialisées. Un seul thread écrit à la fois.
### Scenario 3 : Lecture pendant écriture
```rust
// Thread 1 : Écriture
playlist.append_track(track).await;
// Thread 2 : Lecture (simultané)
let len = playlist.len().await;
```
**Comportement** : La lecture attend que l'écriture se termine.
## Gestion de l'Update ID
### Algorithme
```rust
// À chaque modification
inner.update_id = inner.update_id.wrapping_add(1);
inner.last_change = SystemTime::now();
```
**Opérations qui incrémentent l'update_id** :
- `append_track()` → +1
- `remove_oldest()` → +1 (si un track est supprimé)
- `remove_by_id()` → +1 (si un track est trouvé et supprimé)
- `clear()` → +1 (si la playlist n'était pas vide)
**Opérations qui ne l'incrémentent PAS** :
- `get_items()` (lecture seule)
- `len()`, `is_empty()` (lecture seule)
- `as_container()`, `as_objects()` (lecture seule)
### Utilisation dans UPnP
Les clients UPnP peuvent :
1. Interroger l'`update_id` initial
2. Mémoriser cette valeur
3. Ré-interroger périodiquement
4. Si `update_id` a changé → rafraîchir l'affichage
## Cas d'usage
### 1. Radio en streaming
**Caractéristiques** :
- Capacité limitée (ex: 20 tracks)
- Ajouts fréquents de nouveaux tracks
- Les anciens tracks sont automatiquement supprimés
**Configuration recommandée** :
```rust
let radio = FifoPlaylist::new(
"radio-paradise",
"Radio Paradise",
20, // Historique limité à 20 tracks
DEFAULT_IMAGE,
);
```
### 2. Album statique
**Caractéristiques** :
- Capacité large (ex: 100 tracks)
- Tous les tracks ajoutés une seule fois
- Pas de rotation automatique
**Configuration recommandée** :
```rust
let album = FifoPlaylist::new(
"album-dsotm",
"The Dark Side of the Moon",
100, // Capacité large pour tout l'album
DEFAULT_IMAGE,
);
```
### 3. Playlist locale modifiable
**Caractéristiques** :
- Capacité moyenne (ex: 50 tracks)
- Ajouts et suppressions manuels
- Utilisation de `remove_by_id()` pour contrôle précis
**Configuration recommandée** :
```rust
let playlist = FifoPlaylist::new(
"my-playlist",
"My Favorites",
50,
DEFAULT_IMAGE,
);
```
## Intégration avec un MediaServer
### Architecture typique
```
┌─────────────────┐
│ UPnP Client │
│ (Control Point)│
└────────┬────────┘
│ HTTP/SOAP
┌─────────────────────┐
│ MediaServer UPnP │
│ ┌───────────────┐ │
│ │ ContentDirectory│ │
│ │ Service │ │
│ └───────┬───────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ pmoplaylist │ │ ← Cette crate
│ │ (FIFO) │ │
│ └───────────────┘ │
└─────────────────────┘
```
### Exemple d'endpoints
```rust
// GET /ContentDirectory/Browse?ObjectID=playlist-id
async fn browse_container(playlist: Arc<FifoPlaylist>) -> Response {
let container = playlist.as_container().await;
// Convertir en XML DIDL-Lite et retourner
}
// GET /ContentDirectory/Browse?ObjectID=playlist-id&StartingIndex=0&RequestedCount=10
async fn browse_items(
playlist: Arc<FifoPlaylist>,
offset: usize,
count: usize
) -> Response {
let items = playlist.as_objects(offset, count, Some(DEFAULT_IMAGE_URL)).await;
// Convertir en XML DIDL-Lite et retourner
}
// GET /SystemUpdateID
async fn get_update_id(playlist: Arc<FifoPlaylist>) -> Response {
let update_id = playlist.update_id().await;
// Retourner l'update_id
}
```
## Tests
### Couverture
La crate inclut 11 tests unitaires + 8 doctests couvrant :
1. **Création et état initial**
- `test_create_playlist`
2. **Ajout de tracks**
- `test_append_track`
- `test_fifo_capacity`
3. **Suppression de tracks**
- `test_remove_oldest`
- `test_remove_by_id`
- `test_clear`
4. **Navigation**
- `test_get_items_pagination`
5. **Génération DIDL-Lite**
- `test_as_container`
- `test_as_objects`
6. **Builder pattern**
- `test_track_builder`
7. **Update ID**
- `test_update_id_increments`
### Exécution
```bash
# Tests unitaires
cargo test -p pmoplaylist
# Tests avec doctests
cargo test -p pmoplaylist --doc
# Tous les tests
cargo test -p pmoplaylist --all-targets
```
## Exemples fournis
### 1. basic_usage.rs
Démontre :
- Création d'une playlist
- Ajout et suppression de tracks
- Comportement FIFO
- Génération DIDL-Lite
- Gestion de l'update_id
```bash
cargo run -p pmoplaylist --example basic_usage
```
### 2. radio_streaming.rs
Démontre :
- Utilisation multi-thread
- Simulation d'un flux radio continu
- Surveillance des changements via update_id
- Consultation de l'historique
```bash
cargo run -p pmoplaylist --example radio_streaming
```
### 3. http_server_integration.rs
Démontre :
- Intégration avec un serveur HTTP
- Endpoints REST simulés
- Partage de playlist avec `Arc`
- Serving de l'image par défaut
```bash
cargo run -p pmoplaylist --example http_server_integration
```
## Dépendances
### Runtime
- **pmodidl** (path = "../pmodidl")
- Structures DIDL-Lite (Container, Item, Resource)
- Nécessaire pour la génération d'objets UPnP
- **tokio** (1.42.0, features: sync, time, macros, rt, rt-multi-thread)
- RwLock asynchrone pour thread safety
- Runtime asynchrone pour les méthodes async
- **serde** (1.0.228, features: derive)
- Sérialisation/désérialisation de Track
- Support JSON/autres formats si nécessaire
### Build-time
- **include_bytes!** (macro std)
- Intégration de l'image par défaut dans le binaire
## Performance
### Complexité algorithmique
- `append_track()` : O(1) amorti (VecDeque::push_back + potentiel pop_front)
- `remove_oldest()` : O(1) (VecDeque::pop_front)
- `remove_by_id()` : O(n) (recherche linéaire + VecDeque::remove)
- `get_items()` : O(k) où k = count (iteration + clone)
- `clear()` : O(n) (libération de tous les tracks)
### Allocation mémoire
- Chaque `Track` : ~100-200 bytes (selon la taille des strings)
- VecDeque overhead : ~24 bytes + capacity
- RwLock overhead : ~40 bytes
- Arc overhead : ~16 bytes
**Exemple** : Une playlist de 20 tracks ≈ 2-4 KB
### Lock contention
**Read-heavy workload** : Excellent (RwLock permet plusieurs lecteurs)
**Write-heavy workload** : Acceptable (les écritures sont généralement peu fréquentes pour une playlist)
**Recommandation** : Pour des milliers d'écritures/seconde, envisager un design lock-free ou sharding.
## Extensions futures possibles
### 1. Persistence
```rust
impl FifoPlaylist {
pub async fn save_to_disk(&self, path: &Path) -> io::Result<()>;
pub async fn load_from_disk(path: &Path) -> io::Result<Self>;
}
```
### 2. Événements et callbacks
```rust
pub enum PlaylistEvent {
TrackAdded(Track),
TrackRemoved(String),
Cleared,
}
impl FifoPlaylist {
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<PlaylistEvent>;
}
```
### 3. Indexation et recherche
```rust
impl FifoPlaylist {
pub async fn find_by_artist(&self, artist: &str) -> Vec<Track>;
pub async fn find_by_title(&self, title: &str) -> Vec<Track>;
}
```
### 4. Statistiques
```rust
impl FifoPlaylist {
pub async fn total_duration(&self) -> u32;
pub async fn most_common_artist(&self) -> Option<String>;
}
```
## Licence
Ce projet fait partie du workspace PMOMusic.