Cours Vibe coding avec l'IA · Module 11: Clone de Spotify
Lecteur de musique et contrôle de la lecture
Dans cette leçon5
Bienvenue ! Il est temps de créer Sea Shanty Sounds - une plateforme de streaming musical pirate !
Ce sera comme Spotify - lecteur de musique, playlists, albums, artistes et recommandations personnalisées.
Spotify vs YouTube/TikTok
YouTube :
- Plateforme vidéo (avec la musique en complément)
- Contenu long format
- Métriques de temps de visionnage
TikTok :
- Vidéos courtes avec des sons
- Extraits audio viraux
- Contenu généré par les utilisateurs
Spotify :
- Plateforme de streaming purement audio
- Chansons et albums complets
- Playlists et collections organisées
- Profils d'artistes
- Recommandations personnalisées (Discover Weekly)
- Téléchargements hors ligne
- Synchronisation des paroles
Structure d'un morceau (Track)
1interface Track {
2 id: string;
3 title: string;
4 artist: Artist;
5 album: Album;
6
7 // Audio
8 audioUrl: string;
9 duration: number; // secondes
10 previewUrl?: string; // aperçu de 30 s
11
12 // Métadonnées
13 genre: string[];
14 releaseDate: Date;
15 explicit: boolean;
16
17 // Engagement
18 plays: number;
19 likes: number;
20 inPlaylists: number;
21
22 // Paroles
23 hasLyrics: boolean;
24 lyrics?: Lyric[];
25
26 // État utilisateur
27 isLiked: boolean;
28 isInLibrary: boolean;
29}
30
31interface Artist {
32 id: string;
33 name: string;
34 avatar: string;
35 banner?: string;
36 verified: boolean;
37 followers: number;
38 monthlyListeners: number;
39}
40
41interface Album {
42 id: string;
43 title: string;
44 artist: Artist;
45 coverArt: string;
46 releaseDate: Date;
47 tracks: Track[];
48 totalDuration: number;
49 type: 'album' | 'single' | 'ep';
50}
51
52interface Lyric {
53 timestamp: number; // secondes
54 text: string;
55}Lecteur de musique
1'use client';
2
3import { useState, useRef, useEffect } from 'react';
4
5interface MusicPlayerProps {
6 currentTrack: Track | null;
7 queue: Track[];
8 onNext: () => void;
9 onPrevious: () => void;
10 onShuffle: () => void;
11 onRepeat: () => void;
12}
13
14export default function MusicPlayer({
15 currentTrack,
16 queue,
17 onNext,
18 onPrevious,
19 onShuffle,
20 onRepeat
21}: MusicPlayerProps) {
22 const [isPlaying, setIsPlaying] = useState(false);
23 const [currentTime, setCurrentTime] = useState(0);
24 const [volume, setVolume] = useState(100);
25 const [isMuted, setIsMuted] = useState(false);
26 const [shuffleOn, setShuffleOn] = useState(false);
27 const [repeatMode, setRepeatMode] = useState<'off' | 'all' | 'one'>('off');
28
29 const audioRef = useRef<HTMLAudioElement>(null);
30
31 useEffect(() => {
32 if (audioRef.current && currentTrack) {
33 audioRef.current.src = currentTrack.audioUrl;
34 if (isPlaying) {
35 audioRef.current.play();
36 }
37 }
38 }, [currentTrack]);
39
40 useEffect(() => {
41 const audio = audioRef.current;
42 if (!audio) return;
43
44 const updateTime = () => setCurrentTime(audio.currentTime);
45 const handleEnded = () => {
46 if (repeatMode === 'one') {
47 audio.currentTime = 0;
48 audio.play();
49 } else {
50 onNext();
51 }
52 };
53
54 audio.addEventListener('timeupdate', updateTime);
55 audio.addEventListener('ended', handleEnded);
56
57 return () => {
58 audio.removeEventListener('timeupdate', updateTime);
59 audio.removeEventListener('ended', handleEnded);
60 };
61 }, [repeatMode, onNext]);
62
63 const togglePlay = () => {
64 if (audioRef.current) {
65 if (isPlaying) {
66 audioRef.current.pause();
67 } else {
68 audioRef.current.play();
69 }
70 setIsPlaying(!isPlaying);
71 }
72 };
73
74 const handleSeek = (time: number) => {
75 if (audioRef.current) {
76 audioRef.current.currentTime = time;
77 setCurrentTime(time);
78 }
79 };
80
81 const handleVolumeChange = (newVolume: number) => {
82 if (audioRef.current) {
83 audioRef.current.volume = newVolume / 100;
84 setVolume(newVolume);
85 setIsMuted(newVolume === 0);
86 }
87 };
88
89 const toggleMute = () => {
90 if (audioRef.current) {
91 if (isMuted) {
92 audioRef.current.volume = volume / 100;
93 setIsMuted(false);
94 } else {
95 audioRef.current.volume = 0;
96 setIsMuted(true);
97 }
98 }
99 };
100
101 const toggleShuffle = () => {
102 setShuffleOn(!shuffleOn);
103 onShuffle();
104 };
105
106 const cycleRepeat = () => {
107 const modes: Array<'off' | 'all' | 'one'> = ['off', 'all', 'one'];
108 const currentIndex = modes.indexOf(repeatMode);
109 const nextMode = modes[(currentIndex + 1) % modes.length];
110 setRepeatMode(nextMode);
111 onRepeat();
112 };
113
114 const formatTime = (seconds: number) => {
115 const mins = Math.floor(seconds / 60);
116 const secs = Math.floor(seconds % 60);
117 return `${mins}:${secs.toString().padStart(2, '0')}`;
118 };
119
120 if (!currentTrack) {
121 return (
122 <div className="h-24 bg-gray-900 border-t border-gray-800 flex items-center justify-center text-gray-500">
123 Aucun morceau en lecture
124 </div>
125 );
126 }
127
128 return (
129 <>
130 <audio ref={audioRef} />
131
132 <div className="h-24 bg-gray-900 border-t border-gray-800 px-4 flex items-center gap-4">
133 {/* Infos du morceau */}
134 <div className="flex items-center gap-3 w-1/4">
135 <img
136 src={currentTrack.album.coverArt}
137 alt={currentTrack.title}
138 className="w-14 h-14 rounded"
139 />
140 <div className="flex-1 min-w-0">
141 <div className="font-semibold text-white text-sm truncate">
142 {currentTrack.title}
143 </div>
144 <div className="text-xs text-gray-400 truncate">
145 {currentTrack.artist.name}
146 </div>
147 </div>
148 <button className="text-gray-400 hover:text-white">
149 {currentTrack.isLiked ? 'Aimé' : 'J’aime'}
150 </button>
151 </div>
152
153 {/* Contrôles de lecture */}
154 <div className="flex-1 flex flex-col items-center gap-2">
155 {/* Boutons */}
156 <div className="flex items-center gap-4">
157 <button
158 onClick={toggleShuffle}
159 className={`text-gray-400 hover:text-white ${shuffleOn ? 'text-green-500' : ''}`}
160 >
161 Aléatoire
162 </button>
163
164 <button
165 onClick={onPrevious}
166 className="text-gray-400 hover:text-white"
167 >
168 ⏮
169 </button>
170
171 <button
172 onClick={togglePlay}
173 className="w-8 h-8 bg-white rounded-full flex items-center justify-center hover:scale-105 transition"
174 >
175 <span className="text-black text-xl">
176 {isPlaying ? '⏸' : '▶'}
177 </span>
178 </button>
179
180 <button
181 onClick={onNext}
182 className="text-gray-400 hover:text-white"
183 >
184 ⏭
185 </button>
186
187 <button
188 onClick={cycleRepeat}
189 className={`text-gray-400 hover:text-white ${
190 repeatMode !== 'off' ? 'text-green-500' : ''
191 }`}
192 >
193 {repeatMode === 'one' ? 'Répéter 1' : 'Répéter'}
194 </button>
195 </div>
196
197 {/* Barre de progression */}
198 <div className="w-full flex items-center gap-2">
199 <span className="text-xs text-gray-400 w-10 text-right">
200 {formatTime(currentTime)}
201 </span>
202 <div className="flex-1 group">
203 <input
204 type="range"
205 min={0}
206 max={currentTrack.duration}
207 value={currentTime}
208 onChange={(e) => handleSeek(parseFloat(e.target.value))}
209 className="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer group-hover:h-1.5 transition-all"
210 />
211 </div>
212 <span className="text-xs text-gray-400 w-10">
213 {formatTime(currentTrack.duration)}
214 </span>
215 </div>
216 </div>
217
218 {/* Volume & file d'attente */}
219 <div className="flex items-center gap-3 w-1/4 justify-end">
220 <button className="text-gray-400 hover:text-white">
221 Liste
222 </button>
223
224 <button
225 onClick={toggleMute}
226 className="text-gray-400 hover:text-white"
227 >
228 {isMuted || volume === 0 ? 'Muet' : volume < 50 ? 'Bas' : 'Fort'}
229 </button>
230
231 <input
232 type="range"
233 min={0}
234 max={100}
235 value={isMuted ? 0 : volume}
236 onChange={(e) => handleVolumeChange(parseInt(e.target.value))}
237 className="w-24 h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer"
238 />
239
240 <button className="text-gray-400 hover:text-white">
241 Plein écran
242 </button>
243 </div>
244 </div>
245 </>
246 );
247}Gestion de la file d'attente
1'use client';
2
3interface QueueProps {
4 queue: Track[];
5 currentIndex: number;
6 onTrackClick: (index: number) => void;
7 onRemove: (index: number) => void;
8 onClear: () => void;
9}
10
11export default function Queue({
12 queue,
13 currentIndex,
14 onTrackClick,
15 onRemove,
16 onClear
17}: QueueProps) {
18 const formatDuration = (seconds: number) => {
19 const mins = Math.floor(seconds / 60);
20 const secs = Math.floor(seconds % 60);
21 return `${mins}:${secs.toString().padStart(2, '0')}`;
22 };
23
24 return (
25 <div className="w-80 bg-gray-900 border-l border-gray-800 flex flex-col h-screen">
26 {/* En-tête */}
27 <div className="p-4 border-b border-gray-800">
28 <div className="flex items-center justify-between mb-2">
29 <h3 className="font-bold text-white">File d'attente</h3>
30 <button
31 onClick={onClear}
32 className="text-xs text-gray-400 hover:text-white"
33 >
34 Vider
35 </button>
36 </div>
37 <p className="text-xs text-gray-400">
38 {queue.length} morceaux
39 </p>
40 </div>
41
42 {/* En cours de lecture */}
43 {queue[currentIndex] && (
44 <div className="p-4 bg-gray-800 border-b border-gray-700">
45 <p className="text-xs text-gray-400 mb-2">En cours de lecture</p>
46 <div className="flex items-center gap-3">
47 <img
48 src={queue[currentIndex].album.coverArt}
49 alt={queue[currentIndex].title}
50 className="w-12 h-12 rounded"
51 />
52 <div className="flex-1 min-w-0">
53 <div className="font-semibold text-white text-sm truncate">
54 {queue[currentIndex].title}
55 </div>
56 <div className="text-xs text-gray-400 truncate">
57 {queue[currentIndex].artist.name}
58 </div>
59 </div>
60 </div>
61 </div>
62 )}
63
64 {/* À suivre */}
65 <div className="flex-1 overflow-y-auto">
66 <div className="p-4">
67 <p className="text-xs text-gray-400 mb-3">À suivre</p>
68 <div className="space-y-2">
69 {queue.slice(currentIndex + 1).map((track, idx) => {
70 const actualIndex = currentIndex + 1 + idx;
71 return (
72 <div
73 key={track.id}
74 onClick={() => onTrackClick(actualIndex)}
75 className="flex items-center gap-3 p-2 hover:bg-gray-800 rounded cursor-pointer group"
76 >
77 <img
78 src={track.album.coverArt}
79 alt={track.title}
80 className="w-10 h-10 rounded"
81 />
82 <div className="flex-1 min-w-0">
83 <div className="text-sm text-white truncate">
84 {track.title}
85 </div>
86 <div className="text-xs text-gray-400 truncate">
87 {track.artist.name}
88 </div>
89 </div>
90 <span className="text-xs text-gray-400">
91 {formatDuration(track.duration)}
92 </span>
93 <button
94 onClick={(e) => {
95 e.stopPropagation();
96 onRemove(actualIndex);
97 }}
98 className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-white"
99 >
100 ×
101 </button>
102 </div>
103 );
104 })}
105 </div>
106 </div>
107 </div>
108 </div>
109 );
110}Récapitulatif
Lecteur de musique - lecture/pause, suivant/précédent, avance Contrôles de lecture - aléatoire, répétition (off/all/one) Barre de progression - avancer à n'importe quelle position Contrôle du volume - curseur + bouton muet Infos du morceau - titre, artiste, pochette de l'album Gestion de la file d'attente - voir la file, passer à un morceau, retirer Formatage du temps - affichage MM:SS Avance automatique - morceau suivant quand le courant se termine
Dans le prochain exercice, nous ajouterons les playlists et la bibliothèque !
À très bientôt !
Code de cette leçon : App.tsx
1'use client';
2
3import { useState, useRef, useEffect } from 'react';
4
5interface Track {
6 id: string;
7 title: string;
8 artist: string;
9 album: string;
10 coverArt: string;
11 audioUrl: string;
12 duration: number;
13 isLiked: boolean;
14}
15
16const mockTrack: Track = {
17 id: '1',
18 title: "The Pirate's Anthem",
19 artist: 'Captain Redbeard',
20 album: 'Sea Adventures',
21 coverArt: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=300&h=300&fit=crop',
22 audioUrl: '',
23 duration: 240,
24 isLiked: true
25};
26
27export default function MusicPlayer() {
28 const [isPlaying, setIsPlaying] = useState(false);
29 const [currentTime, setCurrentTime] = useState(0);
30 const [volume, setVolume] = useState(70);
31 const [isMuted, setIsMuted] = useState(false);
32 const [shuffleOn, setShuffleOn] = useState(false);
33 const [repeatMode, setRepeatMode] = useState<'off' | 'all' | 'one'>('off');
34 const audioRef = useRef<HTMLAudioElement>(null);
35
36 useEffect(() => {
37 const interval = setInterval(() => {
38 if (isPlaying && currentTime < mockTrack.duration) {
39 setCurrentTime(prev => prev + 1);
40 }
41 }, 1000);
42 return () => clearInterval(interval);
43 }, [isPlaying, currentTime]);
44
45 const togglePlay = () => setIsPlaying(!isPlaying);
46
47 const handleSeek = (time: number) => setCurrentTime(time);
48
49 const toggleMute = () => setIsMuted(!isMuted);
50
51 const formatTime = (seconds: number) => {
52 const mins = Math.floor(seconds / 60);
53 const secs = Math.floor(seconds % 60);
54 return `${mins}:${secs.toString().padStart(2, '0')}`;
55 };
56
57 return (
58 <div className="min-h-screen bg-black text-white flex items-end">
59 <div className="w-full h-24 bg-gray-900 border-t border-gray-800 px-4 flex items-center gap-4">
60 {/* Infos du morceau */}
61 <div className="flex items-center gap-3 w-1/4">
62 <img
63 src={mockTrack.coverArt}
64 alt={mockTrack.title}
65 className="w-14 h-14 rounded"
66 />
67 <div className="flex-1 min-w-0">
68 <div className="font-semibold text-sm truncate">
69 {mockTrack.title}
70 </div>
71 <div className="text-xs text-gray-400 truncate">
72 {mockTrack.artist}
73 </div>
74 </div>
75 <button className="text-xl">
76 {mockTrack.isLiked ? 'Aimé' : 'J’aime'}
77 </button>
78 </div>
79
80 {/* Contrôles de lecture */}
81 <div className="flex-1 flex flex-col items-center gap-2">
82 <div className="flex items-center gap-4">
83 <button
84 onClick={() => setShuffleOn(!shuffleOn)}
85 className={`text-gray-400 hover:text-white ${shuffleOn ? 'text-green-500' : ''}`}
86 >
87 Aléatoire
88 </button>
89
90 <button className="text-gray-400 hover:text-white text-xl">
91 ⏮
92 </button>
93
94 <button
95 onClick={togglePlay}
96 className="w-8 h-8 bg-white rounded-full flex items-center justify-center hover:scale-105 transition"
97 >
98 <span className="text-black text-xl">
99 {isPlaying ? '⏸' : '▶'}
100 </span>
101 </button>
102
103 <button className="text-gray-400 hover:text-white text-xl">
104 ⏭
105 </button>
106
107 <button
108 onClick={() => {
109 const modes: Array<'off' | 'all' | 'one'> = ['off', 'all', 'one'];
110 const idx = modes.indexOf(repeatMode);
111 setRepeatMode(modes[(idx + 1) % modes.length]);
112 }}
113 className={`text-gray-400 hover:text-white ${repeatMode !== 'off' ? 'text-green-500' : ''}`}
114 >
115 {repeatMode === 'one' ? 'Répéter 1' : 'Répéter'}
116 </button>
117 </div>
118
119 {/* Barre de progression */}
120 <div className="w-full flex items-center gap-2">
121 <span className="text-xs text-gray-400 w-10 text-right">
122 {formatTime(currentTime)}
123 </span>
124 <div className="flex-1">
125 <input
126 type="range"
127 min={0}
128 max={mockTrack.duration}
129 value={currentTime}
130 onChange={(e) => handleSeek(parseFloat(e.target.value))}
131 className="w-full h-1 bg-gray-600 rounded-lg"
132 />
133 </div>
134 <span className="text-xs text-gray-400 w-10">
135 {formatTime(mockTrack.duration)}
136 </span>
137 </div>
138 </div>
139
140 {/* Volume */}
141 <div className="flex items-center gap-3 w-1/4 justify-end">
142 <button className="text-gray-400 hover:text-white">
143 Liste
144 </button>
145
146 <button onClick={toggleMute} className="text-gray-400 hover:text-white">
147 {isMuted || volume === 0 ? 'Muet' : volume < 50 ? 'Bas' : 'Fort'}
148 </button>
149
150 <input
151 type="range"
152 min={0}
153 max={100}
154 value={isMuted ? 0 : volume}
155 onChange={(e) => setVolume(parseInt(e.target.value))}
156 className="w-24 h-1 bg-gray-600 rounded-lg"
157 />
158 </div>
159 </div>
160 </div>
161 );
162}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Principale différence entre Spotify et YouTube ?
2. Que contient un Track (morceau) dans Spotify ?
Voici 2 des 7 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne les sections de la barre du lecteur de musique de gauche à droite :
- Tri horizontal
Ordonne les contrôles de lecture dans l'ordre :
- Éditeur de code
Composant lecteur de musique
- Clique dans l'ordre
Clique sur les éléments pour construire l'appel de lecture :
- Clique dans l'ordre
Clique sur les éléments pour construire la déclaration de l'état de lecture :
- Tri horizontal
Ordonne les éléments de réglage du volume sur une ligne :
- Tri vertical
Ordonne la séquence des modes de répétition dans la fonction cycleRepeat() :
- Tri horizontal
Ordonne les éléments de la barre de progression de gauche à droite :
- Clique dans l'ordre
Clique sur les éléments pour attacher l'écouteur de progression :
- Éditeur de code
Interface de gestion de la file d'attente
- Tri vertical
Ordonne les icônes de volume du muet au plus fort :
- Tri vertical
Ordonne les étapes de la lecture d'un morceau dans le lecteur :