hawk / commit
Added batching to retrieve tracks from 20 albums at a time.
public/api.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++ public/index.html | 35 ++++++++++++++++++++++++---------- public/main.js | 57 +++++++++++++++++++++++++++++++------------------------ public/style.css | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 158 insertions(+), 35 deletions(-) diff --git a/public/api.js b/public/api.js index 16431ab..d6d80b7 100644 --- a/public/api.js +++ b/public/api.js @@ -39,6 +39,55 @@ async function getAlbums(statusCallback) { return albums; } +async function batchGetTracks(albumIds, statusCallback) { + const batchSize = 20 + if (albumIds.length == 0) { + return []; + } + if (albumIds.length > batchSize) { + const allTracks = [] + while (albumIds.length > 0) { + const batch = albumIds.splice(0, batchSize); + albumIds = albumIds.splice(batchSize); + const tracks = await batchGetTracks(batch, statusCallback); + allTracks.push(...tracks); + } + return allTracks; + } + + const db = await openDb(); + const cacheKey = `tracks-${albumIds.join('-')}`; + const cachedTracks = await getWithExpiry(db, cacheKey); + if (cachedTracks) { + if (statusCallback) { + statusCallback(createCallbackState(cachedTracks.length, cachedTracks.length)) + } + return cachedTracks; + } + + const tracks = []; + const accessToken = await getAccessToken(); + let url = `https://api.spotify.com/v1/albums?ids=${albumIds.join(',')}&market=from_token`; + while (url) { + const response = await fetch(url, { + headers: { + Authorization: 'Bearer ' + accessToken + } + }); + const data = await response.json(); + data.albums.forEach(album => { + tracks.push(...album.tracks.items); + }); + if (statusCallback) { + statusCallback(createCallbackState(tracks.length, tracks.length)) + } + url = data.next; + } + + await setWithExpiry(db, cacheKey, tracks, apiExpiry); + return tracks; +} + async function getTracks(albumId, statusCallback) { const db = await openDb(); const cacheKey = `tracks-${albumId}`; diff --git a/public/index.html b/public/index.html index 2380a40..84e79eb 100644 --- a/public/index.html +++ b/public/index.html @@ -51,17 +51,9 @@ <div id="controls"> <div> <h3>Currently playing: - <span id="current-song"></span> </h3> + <span id="current-song"></span> </div> - <button id="pause-play" onclick="pauseOrPlay()"> - <span class="icon-and-label"> - <span class="material-symbols-outlined"> - play_pause - </span> - <span>Play/Pause</span> - </span> - </button> <div> <h3>Search for a song by duration:</h3> @@ -113,7 +105,7 @@ <script src="template.js"></script> <script src="main.js"></script> - <template id="song-item"> + <template id="found-song-item"> <div class="song"> <div> <span>${name}</span> - <span>${artist}</span> @@ -139,6 +131,29 @@ </div> </div> </template> + <template id="playing-song"> + <div class="currently-playing"> + <div> + <div> + <a href="${songUrl}" target="_blank"> + <img src="${albumArtUrl}" alt="Album Cover" class="album-image" /> + </a> + </div> + <div> + <button onclick="pauseOrPlay()"> + <span class="material-symbols-outlined"> + play_pause + </span> + </button> + </div> + </div> + <div class="song-info"> + <span class="song-name">${name}</span> + <span class="song-artist">${artist}</span> + <progress class="song-progress" value="${progressPercent}" max="100"></progress> + </div> + </div> + </template> </body> diff --git a/public/main.js b/public/main.js index 3d8c49c..1f1ce6e 100644 --- a/public/main.js +++ b/public/main.js @@ -40,11 +40,29 @@ if (showSignInButton) { async function setCurrentPlayingSong() { const currentSongSpan = document.getElementById('current-song'); const currentTrack = await getPlayingTrack(); - if (currentTrack && currentTrack.is_playing) { - currentSongSpan.innerText = `${currentTrack.item.name} by ${currentTrack.item.artists[0].name}`; + let html; + if (!currentTrack) { + html = 'Nothing playing'; } else { - currentSongSpan.innerText = 'Nothing playing'; + const album = currentTrack.item.album; + const item = currentTrack.item; + const name = item.name; + const progressPercent = currentTrack.progress_ms / item.duration_ms * 100; + const images = album.images; + let smallestImage = images[images.length - 1]; + const albumArtUrl = smallestImage.url; + const songUrl = item.external_urls.spotify; + + const songItem = document.querySelector('#playing-song'); + html = interpolate(songItem.innerHTML, { + name: name, + artist: album.artists[0].name, + albumArtUrl: albumArtUrl, + songUrl: songUrl, + progressPercent: progressPercent, + }); } + currentSongSpan.innerHTML = html; } function formatDuration(ms) { @@ -75,7 +93,7 @@ async function searchForMatch() { } else { resultsDiv.innerHTML = ''; tracks.forEach(track => { - const songItem = document.querySelector('#song-item'); + const songItem = document.querySelector('#found-song-item'); const html = interpolate(songItem.innerHTML, { name: track.name, artist: track.artists[0].name, @@ -104,30 +122,19 @@ async function search(minutes, seconds) { reportStatus(`Retrieving albums: ${processedCount} of ${totalCount}`); }); - const foundTracks = []; - for (let i = 0; i < albums.length; i++) { - const album = albums[i]; - const tracks = await getTracks(album.album.id, (state) => { - const { processedCount, totalCount } = state; - if (totalCount <= 0) { - totalCount = "???" - } - reportStatus(`Retrieving tracks for album ${i + 1} of ${albums.length}: ${processedCount} of ${totalCount}`); - }); - - for (let j = 0; j < tracks.length; j++) { - const track = tracks[j]; - const duration = track.duration_ms; - const durationInSeconds = Math.floor(duration / 1000); - const durationInMinutes = Math.floor(durationInSeconds / 60); - const durationInRemainingSeconds = durationInSeconds % 60; + const checkTrack = function (track) { + const duration = track.duration_ms; + const durationInSeconds = Math.floor(duration / 1000); + const durationInMinutes = Math.floor(durationInSeconds / 60); + const durationInRemainingSeconds = durationInSeconds % 60; - if (durationInMinutes == minutes && durationInRemainingSeconds == seconds) { - foundTracks.push(track); - } - } + const isMatching = (durationInMinutes == minutes && durationInRemainingSeconds == seconds); + return isMatching; } + const tracks = await batchGetTracks(albums.map(album => album.album.id), null) + const foundTracks = tracks.filter(track => checkTrack(track)); + reportStatus(`Found ${foundTracks.length} track(s)`); return foundTracks; } diff --git a/public/style.css b/public/style.css index 65623e5..539db6e 100644 --- a/public/style.css +++ b/public/style.css @@ -103,4 +103,56 @@ button { width: 1em; height: 1em; margin-right: 1em; +} + +.currently-playing { + display: flex; + align-items: start; + /* To align the top edges of the image and the song-info */ + padding: 1em; + width: 100%; + border: 2px solid black; +} + +.album-image { + width: 60px; + /* or any desired size */ + height: 60px; + /* keep it square for typical album covers */ + margin-right: 10px; + /* space between the image and song info */ + border: 1px solid black; +} + +.song-info { + display: flex; + flex-direction: column; + /* stack children vertically */ + justify-content: space-between; + /* distribute space between song name and artist name */ +} + +.song-name { + font-weight: bold; + font-size: 1.2em; + margin-bottom: 5px; + /* space between song name and artist name */ +} + +.song-artist { + font-size: 1em; + margin-bottom: 10px; + /* space between artist name and progress bar */ +} + +.material-symbols-outlined { + margin: .25em; +} + +.song-progress { + height: 2rem; + border-radius: 2rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } \ No newline at end of file