Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Music Player i Playback Control

Witaj @name! Czas stworzyć Sea Shanty Sounds - platformę streamingu muzyki pirackich! 🎵

To będzie jak Spotify - music player, playlists, albums, artists i personalized recommendations.

Spotify vs YouTube/TikTok

YouTube:

  • Video platform (z muzyką jako dodatek)
  • Long-form content
  • Watch time metrics

TikTok:

  • Short videos z sounds
  • Viral audio clips
  • User-generated content

Spotify:

  • Pure audio streaming platform
  • Full songs i albums
  • Playlists i curated collections
  • Artist profiles
  • Personalized recommendations (Discover Weekly)
  • Offline downloads
  • Lyrics sync

Track Structure

1interface Track {
2  id: string;
3  title: string;
4  artist: Artist;
5  album: Album;
6
7  // Audio
8  audioUrl: string;
9  duration: number; // seconds
10  previewUrl?: string; // 30s preview
11
12  // Metadata
13  genre: string[];
14  releaseDate: Date;
15  explicit: boolean;
16
17  // Engagement
18  plays: number;
19  likes: number;
20  inPlaylists: number;
21
22  // Lyrics
23  hasLyrics: boolean;
24  lyrics?: Lyric[];
25
26  // User state
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; // seconds
54  text: string;
55}

Music Player

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        No track playing
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        {/* Track Info */}
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 ? '❤️' : '🤍'}
150          </button>
151        </div>
152
153        {/* Playback Controls */}
154        <div className="flex-1 flex flex-col items-center gap-2">
155          {/* Buttons */}
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              🔀
162            </button>
163
164            <button
165              onClick={onPrevious}
166              className="text-gray-400 hover:text-white"
167            >
168169            </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            >
184185            </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' ? '🔂' : '🔁'}
194            </button>
195          </div>
196
197          {/* Progress Bar */}
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 & Queue */}
219        <div className="flex items-center gap-3 w-1/4 justify-end">
220          <button className="text-gray-400 hover:text-white">
221            📋
222          </button>
223
224          <button
225            onClick={toggleMute}
226            className="text-gray-400 hover:text-white"
227          >
228            {isMuted || volume === 0 ? '🔇' : volume < 50 ? '🔉' : '🔊'}
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">
241242          </button>
243        </div>
244      </div>
245    </>
246  );
247}

Queue Management

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      {/* Header */}
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">Queue</h3>
30          <button
31            onClick={onClear}
32            className="text-xs text-gray-400 hover:text-white"
33          >
34            Clear
35          </button>
36        </div>
37        <p className="text-xs text-gray-400">
38          {queue.length} tracks
39        </p>
40      </div>
41
42      {/* Now Playing */}
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">Now Playing</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      {/* Next Up */}
65      <div className="flex-1 overflow-y-auto">
66        <div className="p-4">
67          <p className="text-xs text-gray-400 mb-3">Next Up</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                  >
100101                  </button>
102                </div>
103              );
104            })}
105          </div>
106        </div>
107      </div>
108    </div>
109  );
110}

Podsumowanie 🎓

Music player - play/pause, next/previous, seek ✅ Playback controls - shuffle, repeat (off/all/one) ✅ Progress bar - seek to any position ✅ Volume control - slider + mute button ✅ Track info - title, artist, album art ✅ Queue management - view queue, skip to track, remove ✅ Time formatting - MM:SS display ✅ Auto-advance - next track when current ends

W następnym ćwiczeniu dodamy playlists i library!

Do zobaczenia! 🚀

Przejdź do CodeWorlds