# Play From URL — source `UrlSource` Inspiré par BubbleUPnP : recevoir n'importe quelle URL (lien de partage Qobuz, flux radio, playlist M3U, page web contenant de l'audio…) et la jouer immédiatement sur le renderer actif. --- ## Vision architecturale `UrlSource` est une **source musicale ordinaire** qui implémente `MusicSource`, exactement comme Qobuz, RadioFrance ou RadioParadise. Elle apparaît dans le drawer gauche au même titre que les autres sources du serveur PMO. Sa particularité : sa **barre de recherche est le champ URL**. L'utilisateur colle ou tape une URL, appuie sur Entrée — la source résout l'URL et retourne le contenu jouable comme un `BrowseResult` normal. ``` Drawer gauche └─ PMO Music Server ├─ Qobuz ├─ Radio Paradise ├─ Radio France └─ URL / Partage ← nouvelle source └─ [barre de recherche = champ URL] └─ coller une URL + Entrée └─ résolution → BrowseResult → queue + play ``` Avantages de cette approche : - **Zéro nouvelle UI** : la barre de recherche existante du drawer gère tout - **Zéro nouvel endpoint REST** : browse/search existants suffisent - **Zéro cas particulier** dans le drawer ou le content directory handler - `browse()` du container racine peut afficher un **historique** des URLs jouées --- ## Trait `UrlHandler` (dans `pmosource`) Chaque source (et un handler générique) peut revendiquer les URLs qu'elle sait résoudre. ```rust pub enum ResolvedContent { /// Référence à un container d'une source existante /// → la UrlSource délègue le browse à cette source SourceContainer { source_id: String, // "qobuz", "radiofrance", … container_id: String, // "qobuz:album:l46fxnqnxp5vs" }, /// Liste de tracks (M3U, PLS, XSPF, RSS/podcast…) Playlist { title: Option, items: Vec, }, /// Track unique ou flux continu Track { uri: String, metadata: TrackMetadata, }, Stream { uri: String, metadata: StreamMetadata, }, } #[async_trait] pub trait UrlHandler: Send + Sync { fn name(&self) -> &str; /// Priorité : plus grand = essayé en premier (défaut 50) fn priority(&self) -> u8 { 50 } /// Test rapide sans I/O (regex sur l'URL) fn can_handle(&self, url: &str) -> bool; /// Résolution effective (I/O autorisé) async fn resolve(&self, url: &str) -> Result; } ``` --- ## Handlers spécifiques aux sources ### `QobuzUrlHandler` (dans `pmoqobuz`) — priorité 90 URLs reconnues. Les IDs sont potentiellement alphanumériques pour tous les types (pas seulement les albums) : | Forme d'URL | Exemple | |---|---| | `open.qobuz.com/album/` | `https://open.qobuz.com/album/l46fxnqnxp5vs` | | `play.qobuz.com/album/` | `https://play.qobuz.com/album/l46fxnqnxp5vs` | | `open.qobuz.com/track/` | `https://open.qobuz.com/track/48471123` | | `open.qobuz.com/playlist/` | `https://open.qobuz.com/playlist/63246908` | | `open.qobuz.com/artist/` | `https://open.qobuz.com/artist/125709` | Regex d'extraction : `[a-zA-Z0-9]+` pour tous les types sans exception. Résolution sans appel API — l'ID est directement mappé sur un container_id : ``` open.qobuz.com/album/l46fxnqnxp5vs → ResolvedContent::SourceContainer { source_id: "qobuz", container_id: "qobuz:album:l46fxnqnxp5vs", } ``` ### `RadioFranceUrlHandler` (dans `pmoradiofrance`) — priorité 90 URLs `radiofrance.fr/*`, `francemusique.fr/*`, `fip.fr/*`, etc. → `ResolvedContent::Stream` ### `RadioParadiseUrlHandler` (dans `pmoparadise`) — priorité 90 URLs `radioparadise.com/*` → `ResolvedContent::Stream` --- ## Handler générique (dans `pmourlresolver`, nouveau crate) — priorité 10 Dernier recours. Pipeline interne : ``` URL │ ├─ Garde-fou SSRF : rejeter si IP résolue est privée/locale │ (RFC-1918 : 10/8, 172.16/12, 192.168/16 ; loopback : 127/8, ::1 ; │ link-local : 169.254/16, fe80::/10) │ → aucun cas d'usage légitime pour une URL interne ici │ ├─ HEAD request → Content-Type audio/* ? │ └─ → ResolvedContent::Stream / Track (URI directe) │ ├─ Extension ou Content-Type playlist ? │ ├─ .m3u / .m3u8 / application/vnd.apple.mpegurl → parse M3U │ ├─ .pls / audio/x-scpls → parse PLS │ └─ .xspf / application/xspf+xml → parse XSPF │ ├─ application/rss+xml / application/xml ? │ └─ → parse RSS, extraire les audio → Playlist │ └─ text/html ? └─ GET + parse HTML ├─