feat(serveur): lire en streaming depuis le serveur - #17
Conversation
Troisième étape de la source distante : un morceau du catalogue serveur se lit,
avec la file de son album, dans le même lecteur que les fichiers locaux.
La diffusion passe par un ticket. `POST /tracks/{id}/stream-ticket` rend une URL
qui ne demande aucun en-tête d'autorisation, ce qui permet de la confier telle
quelle à ExoPlayer — requêtes de plage d'un déplacement comprises, ce qu'un
jeton porté en en-tête aurait compliqué au sein d'une couche qui gère ses
propres reconnexions.
Le ticket est demandé au moment où le lecteur ouvre la piste, et non à la
constitution de la file : il ne vit qu'une heure, et une longue file dépasserait
l'échéance avant d'atteindre ses derniers morceaux. Un `ResolvingDataSource`
échange le marqueur `waveflow://track/<id>` contre l'URL réelle sur le fil de
chargement. Les fichiers locaux traversent la même chaîne sans être touchés, ce
qui garde un seul lecteur et une seule file.
Le morceau courant cesse d'être un identifiant à résoudre dans la bibliothèque.
`PlaybackState` porte désormais un `PlayingTrack` décrit par le lecteur
lui-même : une piste du serveur n'existe pas dans le MediaStore, et l'y chercher
ne rendait rien à afficher. `PlayerViewModel` n'a plus besoin du `LibraryStore`,
et sa projection — réévaluée à chaque tic de position — n'a plus de recherche à
faire.
Le `mediaId` est préfixé par sa source. Sans ça, un identifiant distant
purement numérique se convertirait en Long et passerait pour une piste locale.
Validé contre un waveflow-server 2.0.0-beta.0 local : ticket obtenu, URL
absolue résolue depuis le chemin relatif du serveur, lecture sans en-tête en
audio/mpeg, requête de plage en 206, et deux tickets successifs bien distincts.
Claude-Session: https://claude.ai/code/session_01F89rkrDB9TxcwHbfgNoyY1
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughLe lecteur prend désormais en charge les pistes distantes. Le changement ajoute les tickets de streaming, la résolution d’URL, les files distantes et l’état commun ChangesLecture distante
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant AlbumDetail
participant PlayerViewModel
participant PlaybackController
participant ExoPlayer
participant RemoteStreamResolver
participant CatalogRepository
AlbumDetail->>PlayerViewModel: playRemoteFrom(queue, song)
PlayerViewModel->>PlaybackController: playRemote(queue, startIndex)
PlaybackController->>ExoPlayer: charger les MediaItem distants
ExoPlayer->>RemoteStreamResolver: résoudre waveflow://track/id
RemoteStreamResolver->>CatalogRepository: streamUrl(trackId)
CatalogRepository-->>RemoteStreamResolver: URL avec ticket
RemoteStreamResolver-->>ExoPlayer: DataSpec résolu
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/java/app/waveflow/data/remote/ServerHttp.kt`:
- Around line 142-145: Corrigez absoluteUrl pour résoudre le chemin de ticket
relativement au chemin d’application déjà présent dans serverUrl, afin de
conserver les préfixes de proxy. Validez et rejetez les chemins absolus ainsi
que les références réseau commençant par //, conformément au contrat attendu.
Ajoutez un test couvrant une serverUrl avec préfixe de proxy et vérifiez que
l’URL finale conserve ce préfixe.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1bfe7788-a28e-4ae4-9d56-1ceb7166bc37
📒 Files selected for processing (24)
README.mdapp/src/main/java/app/waveflow/MainActivity.ktapp/src/main/java/app/waveflow/data/remote/CatalogApi.ktapp/src/main/java/app/waveflow/data/remote/CatalogRepository.ktapp/src/main/java/app/waveflow/data/remote/Dto.ktapp/src/main/java/app/waveflow/data/remote/HttpCatalogApi.ktapp/src/main/java/app/waveflow/data/remote/ServerHttp.ktapp/src/main/java/app/waveflow/playback/Media3PlaybackController.ktapp/src/main/java/app/waveflow/playback/MediaItemMapper.ktapp/src/main/java/app/waveflow/playback/PlaybackController.ktapp/src/main/java/app/waveflow/playback/PlaybackService.ktapp/src/main/java/app/waveflow/playback/PlayingTrack.ktapp/src/main/java/app/waveflow/playback/RemoteStreamResolver.ktapp/src/main/java/app/waveflow/ui/components/MediaRow.ktapp/src/main/java/app/waveflow/ui/player/MiniPlayer.ktapp/src/main/java/app/waveflow/ui/player/NowPlayingScreen.ktapp/src/main/java/app/waveflow/ui/player/PlayerUiState.ktapp/src/main/java/app/waveflow/ui/player/PlayerViewModel.ktapp/src/main/java/app/waveflow/ui/server/catalog/RemoteDetailScreens.ktapp/src/test/java/app/waveflow/playback/MediaItemMapperTest.ktapp/src/test/java/app/waveflow/playback/RemoteStreamResolverTest.ktapp/src/test/java/app/waveflow/testing/Fakes.ktapp/src/test/java/app/waveflow/testing/ServerFakes.ktapp/src/test/java/app/waveflow/ui/player/PlayerViewModelTest.kt
Le serveur rend le ticket sous la forme `/api/v2/stream/<ticket>`, sans savoir qu'un proxy le préfixe. Résoudre ce chemin absolu contre l'adresse de base en écrasait le chemin : un serveur exposé sur `https://hôte/musique` voyait son préfixe disparaître, et l'URL de lecture n'atteignait plus rien. Le chemin est désormais traité comme n'importe quel chemin d'API, par la même construction que les appels — celle qui conserve déjà le préfixe. Seul un chemin absolu du serveur est accepté au passage. Une URL complète ou une référence réseau `//hôte/…` enverrait la lecture ailleurs que sur le serveur où l'utilisateur s'est authentifié ; le serveur n'en produit pas, autant le refuser franchement. Claude-Session: https://claude.ai/code/session_01F89rkrDB9TxcwHbfgNoyY1
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/test/java/app/waveflow/data/remote/HttpCatalogApiTest.kt`:
- Around line 152-162: Ajoutez dans HttpCatalogApiTest, à côté de `un ticket qui
designe un autre hote est refuse`, un test avec une URL absolue telle que
`https://ailleurs.test/api/v2/stream/x`. Appelez `api.streamTicket` via
`echecDe` et vérifiez que l’erreur obtenue est également une instance de
`ServerException.Unexpected`, en conservant le même scénario d’expiration et de
réponse serveur.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1f280925-a0c5-4cb7-a260-8fb3410e96f7
📒 Files selected for processing (2)
app/src/main/java/app/waveflow/data/remote/ServerHttp.ktapp/src/test/java/app/waveflow/data/remote/HttpCatalogApiTest.kt
La garde refusait déjà tout ce qui ne commence pas par `/`, mais seule la
référence réseau `//hôte/…` était vérifiée. Le cas de l'URL complète passait par
une branche non couverte : retirer la condition `!path.startsWith("/")` ne
faisait tomber aucun test.
Les deux cas sont maintenant distingués, et le second tombe bien sans la garde.
Claude-Session: https://claude.ai/code/session_01F89rkrDB9TxcwHbfgNoyY1
Troisième étape de la source distante. Un morceau du catalogue serveur se lit, avec la file de son album, dans le même lecteur que les fichiers locaux — notification média, écran de verrouillage et lecture en arrière-plan compris, sans rien y ajouter.
Le ticket plutôt que le jeton
POST /tracks/{id}/stream-ticketrend une URL qui ne demande aucun en-tête d'autorisation. C'est ce qui permet de la confier telle quelle à ExoPlayer, requêtes de plage comprises — un jeton porté en en-tête aurait demandé unDataSource.Factorysur mesure, dans une couche qui gère déjà ses propres reconnexions.Le ticket est demandé quand le lecteur ouvre la piste, pas à la constitution de la file. Il ne vit qu'une heure : une file un peu longue dépasserait l'échéance avant d'atteindre ses derniers morceaux. Un
ResolvingDataSourceéchange le marqueurwaveflow://track/<id>contre l'URL réelle, sur le fil de chargement. Un déplacement dans le morceau rouvre la source et en redemande un, ce qui règle aussi l'expiration en cours d'écoute.Les fichiers locaux traversent la même chaîne sans être touchés : un seul lecteur, une seule file.
Le morceau courant n'est plus forcément local
PlaybackStateportait uncurrentSongId: Long?quePlayerViewModelrésolvait dans la bibliothèque. Une piste du serveur n'existe pas dans le MediaStore — l'y chercher ne rendait rien à afficher.Il porte désormais un
PlayingTrackdécrit par le lecteur lui-même. Conséquences :PlayerViewModeln'a plus besoin duLibraryStore; sa projection, réévaluée à chaque tic de position, n'a plus de recherche à faire ;mediaIdest préfixé par sa source. Sans ça, un identifiant distant purement numérique se convertirait enLonget passerait pour une piste locale — le testun identifiant distant purement numerique reste distantéchoue si on retire le préfixe ;Validation
./gradlew clean testDebugUnitTest assembleDebug→ BUILD SUCCESSFUL, 166 tests, 0 échec, aucun avertissement. 13 tests nouveaux.Contre le vrai serveur : ticket obtenu, URL absolue résolue depuis le chemin relatif que rend le serveur, lecture sans en-tête en
audio/mpeg, requête de plage en 206, et deux tickets successifs bien distincts.Deux comportements validés par retrait : sans le préfixe de source, la distinction local/distant tombe ; sans la conversion en
IOException, l'échec du serveur remonterait brut jusqu'au lecteur au lieu de signaler une piste illisible.Limites connues
playShuffledacceptera les deux sources.https://claude.ai/code/session_01F89rkrDB9TxcwHbfgNoyY1
Summary by CodeRabbit
Nouvelles fonctionnalités
Documentation
Tests