I recently added a music player to my website. So aughts, right? It features the latest playlist I've created on YouTube Music replete with cover art, track controls, and custom styling to match my site 💅
In this post, I'll explain how I built this player, top to bottom. I will:
- Walk through the player's features
- Discuss the Node script that generates the basic markup
- Reveal the trick that makes this all work 🪄
Are you ready for this?
Music Player: Features Breakdown
You gotta run before you can walk, which means, in this case, we gotta take care of the basic functions first—play, pause, previous, next, and volume—before we get to anything more interesting. I'll talk more about how these actually work, but even implementing the basics always turns out to be more challenging than you first think.
Turns out walking is hard.
Track selection
Aside from navigating the track list using the default previous and next buttons, it's also possible to jump to a track by clicking on the artist and song title in the tracklist.
Progress indicator
The progress indicator shows how much of the song has been played and how much is left in real-time.
Background cover art
When a song is playing, the album's cover art displays. A blurred version is set as the music player's background.
Generate the Playlist Markup
I wrote a Node script that uses the YouTube API to fetch my latest playlist. The code below fetches my latest playlist and returns the playlist's id
, title
, and description
.
export async function getLatestPlaylist() {
try {
const res = await fetch(
`https://youtube.googleapis.com/youtube/v3/playlists?part=id,snippet&channelId=UCa_0Tn_aYic-2d4XRr75Mtg&key=${API_KEY}&maxResults=1`
);
if (!res.ok) {
throw Error(res.statusText);
}
const { items } = await res.json();
return {
id: items[0].id,
title: items[0].snippet.title,
description: items[0].snippet.description,
};
} catch (error) {
console.log(error);
}
}
The most important part of the script extracts and formats the data and renders it into HTML.
async function buildPlaylist() {
const { id, title } = await getLatestPlaylist();
const playlistUrl = `https://music.youtube.com/playlist?list=${id}`;
const playlistItems = await getPlaylistItems(id);
let videoIds = [];
const songData = playlistItems.map((song, idx) => {
videoIds.push(song.snippet.resourceId.videoId);
const songMarkup = createSongMarkup(song, idx);
return songMarkup;
});
const videoData = await getVideo(videoIds);
const totalDuration = {
minutes: 0,
seconds: 0,
};
const playListPromises = Promise.all(
songData.map(async (el, idx) => {
const durationTimestamp = videoData[idx].contentDetails.duration;
const duration = isoDuration(durationTimestamp);
const minutes = duration.parse().minutes;
const seconds = duration.parse().seconds;
totalDuration.minutes += minutes;
totalDuration.seconds += seconds;
el.duration = duration.humanize("en");
el.vidId = videoData[idx].id;
el.thumbnail = videoData[idx].snippet.thumbnails.maxres.url;
const [r, g, b] = await getColor(el.thumbnail);
el.color = `rgb(${r},${g},${b})`;
el.timestamp = `${minutes}:${seconds.toString().padStart(2, "0")}`;
return el;
})
);
const playListItems = await playListPromises;
const humanReadableDuration = isoDuration(totalDuration)
.normalize()
.humanize("en", { largest: 2 });
const markup = createPlaylistElementMarkup(
title,
playlistUrl,
playListItems,
humanReadableDuration,
videoData,
id
);
try {
await fs.writeFile("index.html", markup);
console.log("Playlist built!");
} catch (error) {
console.log(error);
}
}
There's a lot going on in the script, but there are two things to note:
- I wanted to show the total duration of the playlist. Seems easy, right? But this isn't provided via the API, so that's why you see the
total duration
variable. When looping through each song, I have to extract the duration as minutes and seconds (converted from the ISO format that Google provides). Then, after looping through all the data, I use the library,isoDuration
, to process the time into something human readable: 1 hour, 32 minutes, not 83 minutes, 549 seconds, which it would show by default. - From the thumbnail, I extract the main color from the album artwork using the
colorthief
library. I pass this color to the UI as the accent color, which works to varying effects. In the screenshot below, the yellow underline shows the accent color in action. As is, the color blends in a bit too much depending on the album artwork, so I'll likely modify the color's lightness and saturation a bit in a future version.
HTML Template
An essential part of the script is rendering the data from the YouTube API into HTML, so I can use it on my site. I won't share the entire output here, but, as an example, the track listing gets rendered like this:
<li data-track="01" data-marker="▸">
<button class="sm-playlist-item" data-src="wvuEQurocVQ" data-idx="0" data-image-src="https://i.ytimg.com/vi/wvuEQurocVQ/maxresdefault.jpg" data-artist="Yin Yin" data-title="The Sacred Valley of Cusco" data-timestamp="4:42" data-color="rgb(88,27,27)">
<span class="sm-playlist-item-title">The Sacred Valley of Cusco</span>
<span class="sm-playlist-item-artist">Yin Yin</span>
</button>
</li>
I use custom data
attributes to store information about the track, like the accent color, album thumbnail, and timestamp, which my client JS uses when playing the songs.
The generated HTML markup is ultimately dumped into a Handlebars partial in my Ghost theme, which I use for my website. If you don't know, Ghost is a super flexible publishing platform that allows for this kind of customization without fighting against frameworks but also takes care of a lot of the hard stuff like having an amazing editor and newsletter system. (Full disclosure: I work there!)
Once in my Ghost theme, I deploy the changes and the playlist is ready to go on my homepage. The last piece of the puzzle is the clientside JS that gives the player its interactivity.
And revealing the trick behind the scenes that makes this all work...
Music Player Javascript
Here's the big reveal: The YouTube player is embedded on the page but hidden with CSS! I then implement my custom UI that interacts with the YouTube iframe API behind the scenes and controls the music player.
A question you may ask is, "Why bother building this whole thing when you could just use the normal embedded player?" And, well, bud, you got me there because there's not really any added benefit or increased functionality (reduced, most likely).
The biggest advantage here is the ability to create a totally bespoke UI that integrates seamlessly with my existing site and its aesthetic. Sure, it's YouTube, but it doesn't look like YouTube, you know. And I often just open my website, scroll down, and hit play.
The YouTube iframe API exposes methods for controlling every aspect of the music player via custom elements. For example, when the volume value changes, I call the API's setVolume
method with the new value. If you're interested more in using the API, check out the iframe API docs or the relevant code I use in my Ghost theme to power this modern-day music box.
Conclusion
This post had jams and JavaScript, so I consider that a success. Here's a list of the repos relevant to this project, if you're interested in some deeper cuts:
Or, even better, head on over to my homepage and have a listen 🎧