From d49bb604ca7f022f7dc41f5ddd88aff5d6516044 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 14 Jun 2026 13:55:38 +0200 Subject: [PATCH 1/5] feat: add UrlSource for arbitrary URL-based media playback Introduces the pmourlsource crate as a standard MusicSource that resolves HTTP/HTTPS URLs via a priority-ordered UrlHandler registry. Includes specialized handlers for Qobuz and Radio France alongside an SSRF-safe generic scraper supporting playlists, feeds, and HTML audio. Integrates with existing browse flows, REST endpoints, and Android Web Share targets while updating source capability flags to correctly route URL queries. --- Blackboard/ToDiscuss/play_from_url.md | 323 +++++++++++++ Cargo.lock | 126 +++++ Cargo.toml | 1 + PMOMusic/Cargo.toml | 2 +- PMOMusic/src/main.rs | 6 + pmocontrol/src/pmoserver_ext.rs | 9 +- pmomediaserver/Cargo.toml | 3 + pmomediaserver/src/content_handler.rs | 59 ++- pmomediaserver/src/sources.rs | 39 ++ pmoparadise/src/source.rs | 1 + pmoqobuz/src/source.rs | 1 + pmoradiofrance/src/source.rs | 1 + pmosource/src/lib.rs | 4 + pmourlsource/Cargo.toml | 14 + pmourlsource/assets/url-source.webp | Bin 0 -> 3680 bytes pmourlsource/src/handler.rs | 122 +++++ pmourlsource/src/handlers/generic.rs | 573 +++++++++++++++++++++++ pmourlsource/src/handlers/mod.rs | 3 + pmourlsource/src/handlers/qobuz.rs | 138 ++++++ pmourlsource/src/handlers/radiofrance.rs | 384 +++++++++++++++ pmourlsource/src/lib.rs | 9 + pmourlsource/src/source.rs | 253 ++++++++++ 22 files changed, 2053 insertions(+), 18 deletions(-) create mode 100644 Blackboard/ToDiscuss/play_from_url.md create mode 100644 pmourlsource/Cargo.toml create mode 100644 pmourlsource/assets/url-source.webp create mode 100644 pmourlsource/src/handler.rs create mode 100644 pmourlsource/src/handlers/generic.rs create mode 100644 pmourlsource/src/handlers/mod.rs create mode 100644 pmourlsource/src/handlers/qobuz.rs create mode 100644 pmourlsource/src/handlers/radiofrance.rs create mode 100644 pmourlsource/src/lib.rs create mode 100644 pmourlsource/src/source.rs diff --git a/Blackboard/ToDiscuss/play_from_url.md b/Blackboard/ToDiscuss/play_from_url.md new file mode 100644 index 00000000..d6dd2f6a --- /dev/null +++ b/Blackboard/ToDiscuss/play_from_url.md @@ -0,0 +1,323 @@ +# 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 + ├─