Adding Music Previews to My Now Page

About two months or so ago I added a Now page to my site. It shows my current reading list, my last watched movies, my Untappd beer check-ins, and my most recent Spotify tracks. You can see that part here:

List of recent tracks

When I built it, I used a Pipedream workflow to wrap calls to Spotify’s API. My Pipedream workflow gets my most recent tracks, slims down the data quite a bit, and returns just what I need. I use some client-side code to hit that endpoint and then render it out on the Now page. (I also use a bit of caching with LocalStorage such that the endpoint is only hit every ten minutes.)

Currently, when rendering each track, I link to its URL and Spotify users can listen to the track completely. I thought it would be cool to let people preview the tracks right from the web page. Here’s how I did that.

Updating the "Back End" #

In my case, my back end is just the Pipedream workflow. As I mentioned, it hits the Spotify API and then transforms the data into something smaller before returning it. All I had to do was update that one step:

export default defineComponent({  async run({ steps, $  }) {    return steps.get_recent_tracks.$ return_value.items.map(r => {      return {          artists: r.track.artists,          name: r.track.name,          href: r.track.external_urls.spotify,          preview_url: r.track.preview_url,          album: r.track.album.name,           album_release_date: r.track.album.release_date,           images: r.track.album.images,           played_at:r.played_at      }    })  },})

To be clear, this isn’t strictly necessary, I could simply return everything Spotify sends, but as it is sending a lot I don’t need, this small step really improves the performance of my API. As an example, here’s one result from Spotify (this is in an array of results):

{	"track": {		"album": {			"album_type": "album",			"artists": [				{					"external_urls": {						"spotify": "https://open.spotify.com/artist/5HYNPEO2NNBONQkp3Mvwvc"					},					"href": "https://api.spotify.com/v1/artists/5HYNPEO2NNBONQkp3Mvwvc",					"id": "5HYNPEO2NNBONQkp3Mvwvc",					"name": "Scott Bradlee's Postmodern Jukebox",					"type": "artist",					"uri": "spotify:artist:5HYNPEO2NNBONQkp3Mvwvc"				}			],			"available_markets": [				"AR",				"AU",				"AT",				"BE",				"BO",				"BR",				"BG",				"CA",				"CL",				"CO",				"CR",				"CY",				"CZ",				"DK",				"DO",				"DE",				"EC",				"EE",				"SV",				"FI",				"FR",				"GR",				"GT",				"HN",				"HK",				"HU",				"IS",				"IE",				"IT",				"LV",				"LT",				"LU",				"MY",				"MT",				"MX",				"NL",				"NZ",				"NI",				"NO",				"PA",				"PY",				"PE",				"PH",				"PL",				"PT",				"SG",				"SK",				"ES",				"SE",				"CH",				"TW",				"TR",				"UY",				"US",				"GB",				"AD",				"LI",				"MC",				"ID",				"JP",				"TH",				"VN",				"RO",				"IL",				"ZA",				"SA",				"AE",				"BH",				"QA",				"OM",				"KW",				"EG",				"MA",				"DZ",				"TN",				"LB",				"JO",				"PS",				"IN",				"BY",				"KZ",				"MD",				"UA",				"AL",				"BA",				"HR",				"ME",				"MK",				"RS",				"SI",				"KR",				"BD",				"PK",				"LK",				"GH",				"KE",				"NG",				"TZ",				"UG",				"AG",				"AM",				"BS",				"BB",				"BZ",				"BT",				"BW",				"BF",				"CV",				"CW",				"DM",				"FJ",				"GM",				"GE",				"GD",				"GW",				"GY",				"HT",				"JM",				"KI",				"LS",				"LR",				"MW",				"MV",				"ML",				"MH",				"FM",				"NA",				"NR",				"NE",				"PW",				"PG",				"WS",				"SM",				"ST",				"SN",				"SC",				"SL",				"SB",				"KN",				"LC",				"VC",				"SR",				"TL",				"TO",				"TT",				"TV",				"VU",				"AZ",				"BN",				"BI",				"KH",				"CM",				"TD",				"KM",				"GQ",				"SZ",				"GA",				"GN",				"KG",				"LA",				"MO",				"MR",				"MN",				"NP",				"RW",				"TG",				"UZ",				"ZW",				"BJ",				"MG",				"MU",				"MZ",				"AO",				"CI",				"DJ",				"ZM",				"CD",				"CG",				"IQ",				"LY",				"TJ",				"VE",				"ET",				"XK"			],			"external_urls": {				"spotify": "https://open.spotify.com/album/5CUFurrJe05hnz189d5mDK"			},			"href": "https://api.spotify.com/v1/albums/5CUFurrJe05hnz189d5mDK",			"id": "5CUFurrJe05hnz189d5mDK",			"images": [				{					"height": 640,					"url": "https://i.scdn.co/image/ab67616d0000b2735cb23d27338f4f3d848120ca",					"width": 640				},				{					"height": 300,					"url": "https://i.scdn.co/image/ab67616d00001e025cb23d27338f4f3d848120ca",					"width": 300				},				{					"height": 64,					"url": "https://i.scdn.co/image/ab67616d000048515cb23d27338f4f3d848120ca",					"width": 64				}			],			"name": "33 Resolutions Per Minute",			"release_date": "2017-01-05",			"release_date_precision": "day",			"total_tracks": 18,			"type": "album",			"uri": "spotify:album:5CUFurrJe05hnz189d5mDK"		},		"artists": [			{				"external_urls": {					"spotify": "https://open.spotify.com/artist/5HYNPEO2NNBONQkp3Mvwvc"				},				"href": "https://api.spotify.com/v1/artists/5HYNPEO2NNBONQkp3Mvwvc",				"id": "5HYNPEO2NNBONQkp3Mvwvc",				"name": "Scott Bradlee's Postmodern Jukebox",				"type": "artist",				"uri": "spotify:artist:5HYNPEO2NNBONQkp3Mvwvc"			},			{				"external_urls": {					"spotify": "https://open.spotify.com/artist/5tUXE5XK6VpNJj4LtxeI7W"				},				"href": "https://api.spotify.com/v1/artists/5tUXE5XK6VpNJj4LtxeI7W",				"id": "5tUXE5XK6VpNJj4LtxeI7W",				"name": "Kenton Chen",				"type": "artist",				"uri": "spotify:artist:5tUXE5XK6VpNJj4LtxeI7W"			}		],		"available_markets": [			"AR",			"AU",			"AT",			"BE",			"BO",			"BR",			"BG",			"CA",			"CL",			"CO",			"CR",			"CY",			"CZ",			"DK",			"DO",			"DE",			"EC",			"EE",			"SV",			"FI",			"FR",			"GR",			"GT",			"HN",			"HK",			"HU",			"IS",			"IE",			"IT",			"LV",			"LT",			"LU",			"MY",			"MT",			"MX",			"NL",			"NZ",			"NI",			"NO",			"PA",			"PY",			"PE",			"PH",			"PL",			"PT",			"SG",			"SK",			"ES",			"SE",			"CH",			"TW",			"TR",			"UY",			"US",			"GB",			"AD",			"LI",			"MC",			"ID",			"JP",			"TH",			"VN",			"RO",			"IL",			"ZA",			"SA",			"AE",			"BH",			"QA",			"OM",			"KW",			"EG",			"MA",			"DZ",			"TN",			"LB",			"JO",			"PS",			"IN",			"BY",			"KZ",			"MD",			"UA",			"AL",			"BA",			"HR",			"ME",			"MK",			"RS",			"SI",			"KR",			"BD",			"PK",			"LK",			"GH",			"KE",			"NG",			"TZ",			"UG",			"AG",			"AM",			"BS",			"BB",			"BZ",			"BT",			"BW",			"BF",			"CV",			"CW",			"DM",			"FJ",			"GM",			"GE",			"GD",			"GW",			"GY",			"HT",			"JM",			"KI",			"LS",			"LR",			"MW",			"MV",			"ML",			"MH",			"FM",			"NA",			"NR",			"NE",			"PW",			"PG",			"WS",			"SM",			"ST",			"SN",			"SC",			"SL",			"SB",			"KN",			"LC",			"VC",			"SR",			"TL",			"TO",			"TT",			"TV",			"VU",			"AZ",			"BN",			"BI",			"KH",			"CM",			"TD",			"KM",			"GQ",			"SZ",			"GA",			"GN",			"KG",			"LA",			"MO",			"MR",			"MN",			"NP",			"RW",			"TG",			"UZ",			"ZW",			"BJ",			"MG",			"MU",			"MZ",			"AO",			"CI",			"DJ",			"ZM",			"CD",			"CG",			"IQ",			"LY",			"TJ",			"VE",			"ET",			"XK"		],		"disc_number": 1,		"duration_ms": 255000,		"explicit": false,		"external_ids": {			"isrc": "GBDMT1600258"		},		"external_urls": {			"spotify": "https://open.spotify.com/track/0E32W7S52AaR4ht7i7DwDq"		},		"href": "https://api.spotify.com/v1/tracks/0E32W7S52AaR4ht7i7DwDq",		"id": "0E32W7S52AaR4ht7i7DwDq",		"is_local": false,		"name": "Closer",		"popularity": 48,		"preview_url": "https://p.scdn.co/mp3-preview/62d19079487d6859ec9c587b8e87754424cabeca?cid=2feb4729ba5145d7a7fd92f2af83cf0d",		"track_number": 1,		"type": "track",		"uri": "spotify:track:0E32W7S52AaR4ht7i7DwDq"	},	"played_at": "2023-11-29T14:25:31.873Z",	"context": {		"type": "playlist",		"href": "https://api.spotify.com/v1/playlists/37i9dQZF1DZ06evO3mw43S",		"external_urls": {			"spotify": "https://open.spotify.com/playlist/37i9dQZF1DZ06evO3mw43S"		},		"uri": "spotify:playlist:37i9dQZF1DZ06evO3mw43S"	}}

Still here? Good. That’s huge, right? Here’s the transformed value:

{	"artists": [		{			"external_urls": {				"spotify": "https://open.spotify.com/artist/5HYNPEO2NNBONQkp3Mvwvc"			},			"href": "https://api.spotify.com/v1/artists/5HYNPEO2NNBONQkp3Mvwvc",			"id": "5HYNPEO2NNBONQkp3Mvwvc",			"name": "Scott Bradlee's Postmodern Jukebox",			"type": "artist",			"uri": "spotify:artist:5HYNPEO2NNBONQkp3Mvwvc"		},		{			"external_urls": {				"spotify": "https://open.spotify.com/artist/5tUXE5XK6VpNJj4LtxeI7W"			},			"href": "https://api.spotify.com/v1/artists/5tUXE5XK6VpNJj4LtxeI7W",			"id": "5tUXE5XK6VpNJj4LtxeI7W",			"name": "Kenton Chen",			"type": "artist",			"uri": "spotify:artist:5tUXE5XK6VpNJj4LtxeI7W"		}	],	"name": "Closer",	"href": "https://open.spotify.com/track/0E32W7S52AaR4ht7i7DwDq",	"preview_url": "https://p.scdn.co/mp3-preview/62d19079487d6859ec9c587b8e87754424cabeca?cid=2feb4729ba5145d7a7fd92f2af83cf0d",	"album": "33 Resolutions Per Minute",	"album_release_date": "2017-01-05",	"images": [		{			"height": 640,			"url": "https://i.scdn.co/image/ab67616d0000b2735cb23d27338f4f3d848120ca",			"width": 640		},		{			"height": 300,			"url": "https://i.scdn.co/image/ab67616d00001e025cb23d27338f4f3d848120ca",			"width": 300		},		{			"height": 64,			"url": "https://i.scdn.co/image/ab67616d000048515cb23d27338f4f3d848120ca",			"width": 64		}	],	"played_at": "2023-11-29T14:25:31.873Z"}

Much slimmer. I could strip even more as I immediately see things I’m not using, but it’s good enough for now.

Coding the Preview #

My initial code simply took the result of the API and rendered out the individual track items. Here’s one example:

A track element showing Hell to the Liars by London Grammar.

Initially, that code looked like so:

let tracks = await getTracks();// while we get 20, limit to 18 as we're doing rows of 3tracks = tracks.slice(0, 18);let s = '';tracks.forEach(t => {	let artists = t.artists.map(a => a.name).join(', ');	let html = `<div class="track"><a href="$ {t.href}" target="_new"><img src="$ {t.images[1].url}"></a><a href="$ {t.href}" target="_new">"$ {t.name}"</a> by $ {artists}</div>	`;	s += html;});document.querySelector('.tracks').innerHTML = s;

I began by removing the link around the image and by adding in the preview URL. I used a data attribute for that:

<img src="$ {t.images[1].url}" data-preview="$ {t.preview_url}">

Next, I needed to add event handlers to each track:

let music = document.querySelectorAll('div.track img');music.forEach(m => {	m.addEventListener('click', e => {		// stuff	});});

So far so good. Now for the tricky part. Playing music in JavaScript is incredibly simple. Given a URL that leads to supported audio, you can do:

let music = new Audio(theURL);music.play();

My first implementation simply grabbed the URL:

let preview = e.currentTarget.dataset.preview;

and did that – which led to me being able to click every rendered track and hear all the music playing at once in a god-awful mashup of epic proportions. To correct this, I had to get a bit fancy:

  • If a person has clicked on track A, then track B, I should stop playing A
  • If a person has clicked on track A, and then A again, they probably want to stop it.

Here’s how I did it:

// add event listener for music previewlet music = document.querySelectorAll('div.track img');let audio = new Audio();music.forEach(m => {	m.addEventListener('click', e => {		let preview = e.currentTarget.dataset.preview;		if(audio.src) { 			audio.pause(); 			audio.currentTime = 0; 			if(audio.src === preview) {				audio.src = '';				return;			}		}		audio.src = preview;		audio.play();	});});

I basically just check the current src. If it matches, I stop (this is done with pause and setting the currentTime). If the "new" URL is the same as the last one, then I just leave. Otherwise, I load up the new song.

This worked perfectly until I realized an issue. If you click to preview track A, let it play and it finishes, if you click the same track, it wouldn’t start up. So I then added one more line of code:

audio.addEventListener('ended', e => { audio.src = '' });

This now lets me listen to the same preview again and again… if I want to. If you want to see the complete code, just view source over on Now or see the repo version here: https://github.com/cfjedimaster/raymondcamden2023/blob/main/src/now.liquid.

Raymond Camden

Posted in: JavaScript

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.