fuzzyworld

hawk / commit

Added batching to retrieve tracks from 20 albums at a time.

6afcc473423d3bc2fb08dcd031cf6da673cacbf8 · Pavel Safronov · 2023-10-14 13:56:42 -0700 · parents 2c8226dcfc75

Added batching to retrieve tracks from 20 albums at a time.
The current song is now being generated from a template.
 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