hawk / commit
Multiple changes:
public/api.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++-- public/favicon.ico | Bin 0 -> 1150 bytes public/favicon.png | Bin 0 -> 872 bytes public/favicon.svg | 19 ++++++++++++++ public/index.html | 26 +++++++++++++++----- public/main.js | 46 +++++++++++++++++++++++++++------- public/signin.js | 19 +++++++++++--- public/template.js | 19 ++++++++++++++ 8 files changed, 179 insertions(+), 21 deletions(-) diff --git a/public/api.js b/public/api.js index f25cc8a..615efe6 100644 --- a/public/api.js +++ b/public/api.js @@ -1,12 +1,28 @@ +async function getAlbums() { + let accessToken = localStorage.getItem('access_token'); + if (!accessToken) { + console.error('No access token found'); + return; + } + + const response = await fetch('https://api.spotify.com/v1/me/albums', { + headers: { + Authorization: 'Bearer ' + accessToken + } + }); -async function getProfile() { + const data = await response.json(); + return data; +} + +async function getTracks(albumId) { let accessToken = localStorage.getItem('access_token'); if (!accessToken) { console.error('No access token found'); return; } - const response = await fetch('https://api.spotify.com/v1/me', { + const response = await fetch(`https://api.spotify.com/v1/albums/${albumId}/tracks`, { headers: { Authorization: 'Bearer ' + accessToken } @@ -14,4 +30,55 @@ async function getProfile() { const data = await response.json(); return data; +} + +async function playTrack(trackId) { + let accessToken = localStorage.getItem('access_token'); + if (!accessToken) { + console.error('No access token found'); + return; + } + + console.info(`Playing track ${trackId}`); + await fetch(`https://api.spotify.com/v1/me/player/play`, { + method: 'PUT', + headers: { + Authorization: 'Bearer ' + accessToken + }, + body: JSON.stringify({ + uris: [`spotify:track:${trackId}`] + }) + }); +} + +async function search(minutes, seconds) { + let accessToken = localStorage.getItem('access_token'); + if (!accessToken) { + console.error('No access token found'); + return; + } + + const albums = await getAlbums(); + // console.log(albums); + + const foundTracks = []; + for (let i = 0; i < albums.items.length; i++) { + const album = albums.items[i]; + const tracks = await getTracks(album.album.id); + // console.log(tracks); + + for (let j = 0; j < tracks.items.length; j++) { + const track = tracks.items[j]; + 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) { + console.log(track); + foundTracks.push(track); + } + } + } + return foundTracks; } \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..1eeafc2 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..35b6215 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..5ae5ac0 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="96.000000pt" height="96.000000pt"><svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="96.000000pt" height="96.000000pt" viewBox="0 0 96.000000 96.000000" preserveAspectRatio="xMidYMid meet"> + +<g transform="translate(0.000000,96.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none"> +<path d="M400 771 c-76 -25 -173 -109 -218 -190 l-23 -41 31 0 31 0 -36 -52 +c-20 -30 -35 -65 -35 -82 0 -25 3 -28 23 -22 12 4 36 9 53 12 l31 6 -10 -55 +c-13 -72 -4 -107 28 -107 13 0 27 5 31 11 3 6 22 17 40 25 34 14 35 14 65 -21 +36 -41 74 -69 107 -79 21 -7 22 -4 22 38 0 101 37 206 86 245 22 17 41 21 101 +21 41 0 84 -5 95 -11 20 -11 20 -9 16 47 -4 50 -9 61 -39 86 -19 16 -38 34 +-44 40 -60 65 -92 89 -142 108 -62 24 -172 35 -213 21z m215 -90 c35 -18 66 +-40 70 -50 10 -31 -22 -90 -58 -104 -53 -23 -96 -75 -117 -145 -11 -35 -20 +-75 -20 -88 0 -13 -4 -24 -9 -24 -10 0 -48 43 -67 76 -10 17 -15 18 -45 6 -69 +-28 -70 -28 -63 9 4 19 15 56 26 83 l19 48 -26 -6 c-14 -3 -37 -9 -52 -13 +l-28 -6 29 39 c16 21 40 51 54 66 l24 27 -37 3 -38 3 27 32 c14 17 50 43 79 +58 68 35 141 30 232 -14z"></path> +<path d="M510 621 c0 -19 39 -51 63 -51 13 0 32 6 42 14 18 13 16 15 -25 26 +-25 6 -53 13 -62 16 -10 3 -18 1 -18 -5z"></path> +</g> +</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } } +</style></svg> \ No newline at end of file diff --git a/public/index.html b/public/index.html index 0f8ce37..e866593 100644 --- a/public/index.html +++ b/public/index.html @@ -4,12 +4,14 @@ <head> <meta charset="utf-8"> <title>Hawk</title> - <favicon href="h.png" /> + <link rel="icon" type="image/png" href="favicon.png"> <style> body { font-family: sans-serif; margin: 0; padding: 0; + max-width: 600px; + margin: 0 auto; } #main, @@ -51,23 +53,34 @@ </head> <body> + <template id="song-item"> + <div class="song"> + <div> + <span>${name}</span> + <span>${duration}</span> + <button id="play-${trackId}" onclick="playTrack('${trackId}')">Play</button> + <button id="open-${trackId}" onclick="window.open('${trackHref}')">Open</button> + </div> + </div> + </template> <div id="header"> <h1>Hawk</h1> <div> - You can specify a duration ("5 and a half minutes"), and Hawk will find a song in your library with that + You can specify a duration ("5 and a half minutes"), and Hawk will find a song in your Spotify library with + that length, or as close to it as possible. </div> <div> - Name comes from <a href="https://www.imdb.com/title/tt0102070/">Hudson Hawk</a>, where Bruce Willis does - this for - heists. + The name 'Hawk' comes from the movie <a href="https://www.imdb.com/title/tt0102070/" target="_blank">Hudson + Hawk</a>, where Bruce Willis picks and sings songs of a particular lengths to time things. Yeah, it's + weird, but it's also a fun movie, so check it out. </div> </div> <div id="main"> <div id="sign-in-div"> - <button id="signin">Sign In</button> + <button id="signin">Sign In to Spotify</button> </div> <div id="controls"> <div id="greeting"></div> @@ -88,6 +101,7 @@ </div> <script src="signin.js"></script> <script src="api.js"></script> + <script src="template.js"></script> <script src="main.js"></script> </body> diff --git a/public/main.js b/public/main.js index 79c14e1..99ee492 100644 --- a/public/main.js +++ b/public/main.js @@ -14,10 +14,8 @@ if (code) { const accessToken = localStorage.getItem('access_token'); const showSignInButton = !accessToken -const showProfile = !!accessToken const signInButton = document.getElementById('signin'); -console.log(signInButton); const controlsDiv = document.getElementById('controls'); if (showSignInButton) { signInButton.setAttribute('style', 'display: block'); @@ -31,10 +29,40 @@ if (showSignInButton) { const signOutButton = document.getElementById('signout'); signOutButton.addEventListener('click', signOut); -if (showProfile) { - getProfile().then(profile => { - console.log(profile); - const greetingDiv = document.getElementById('greeting'); - greetingDiv.innerHTML = `Hello, ${profile.display_name}! Let's find you a song!`; - }); -} \ No newline at end of file +const searchButton = document.getElementById('search'); +searchButton.addEventListener('click', async () => { + searchButton.setAttribute('disabled', 'disabled'); + try { + const minutes = document.getElementById('minutes-input').value; + const seconds = document.getElementById('seconds-input').value; + const tracks = await search(minutes, seconds); + const resultsDiv = document.getElementById('results'); + tracks.forEach(track => { + const songItem = document.querySelector('#song-item'); + const html = interpolate(songItem.innerHTML, { + name: track.name, + duration: formatDuration(track.duration_ms), + trackId: track.id, + trackHref: track.external_urls.spotify + }); + resultsDiv.innerHTML += html; + }); + } finally { + searchButton.removeAttribute('disabled'); + } +}); + +function formatDuration(ms) { + // Convert milliseconds to total seconds + let totalSeconds = Math.floor(ms / 1000); + + // Calculate minutes and seconds + let minutes = Math.floor(totalSeconds / 60); + let seconds = totalSeconds % 60; + + // Ensure seconds are always 2 digits + let formattedSeconds = seconds.toString().padStart(2, '0'); + + // Return formatted string + return `${minutes}:${formattedSeconds}`; +} diff --git a/public/signin.js b/public/signin.js index f447a95..811d842 100644 --- a/public/signin.js +++ b/public/signin.js @@ -1,5 +1,15 @@ const clientId = '6c4c04a3751b4e409ddad7d71aad9949'; -const redirectUri = 'http://localhost:5500/public/index.html'; +const scope = 'user-library-read user-modify-playback-state'; +function getSpotifyRedirectURL() { + // If the current hostname is 'localhost' + if (window.location.hostname === 'localhost') { + return 'http://localhost:5500/public/index.html'; + } else if (window.location.hostname === '127.0.0.1') { + return 'http://127.0.0.1:5500/public/index.html'; + } else { + return 'https://pavelsafronov.gitlab.io/hawk/index.html'; + } +} async function signOut() { localStorage.removeItem('access_token'); @@ -10,15 +20,15 @@ async function signIn() { let codeVerifier = generateRandomString(128); const codeChallenge = await generateCodeChallenge(codeVerifier) let state = generateRandomString(16); - let scope = 'user-read-private user-read-email'; localStorage.setItem('code_verifier', codeVerifier); + const redirectUrl = getSpotifyRedirectURL(); let args = new URLSearchParams({ response_type: 'code', client_id: clientId, scope: scope, - redirect_uri: redirectUri, + redirect_uri: redirectUrl, state: state, code_challenge_method: 'S256', code_challenge: codeChallenge @@ -30,10 +40,11 @@ async function signIn() { async function getToken(code) { let codeVerifier = localStorage.getItem('code_verifier'); + const redirectUrl = getSpotifyRedirectURL(); let body = new URLSearchParams({ grant_type: 'authorization_code', code: code, - redirect_uri: redirectUri, + redirect_uri: redirectUrl, client_id: clientId, code_verifier: codeVerifier }); diff --git a/public/template.js b/public/template.js new file mode 100644 index 0000000..a77573f --- /dev/null +++ b/public/template.js @@ -0,0 +1,19 @@ +/** + * Get a template from a string + * https://stackoverflow.com/a/41015840 + * @param {String} str The string to interpolate + * @param {Object} params The parameters + * @return {String} The interpolated string + */ +function interpolate(str, params) { + let names = Object.keys(params); + let vals = Object.values(params); + return new Function(...names, `return \`${str}\`;`)(...vals); +} + +function htmlToElement(html) { + var template = document.createElement('template'); + html = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = html; + return template.content.firstChild; +} \ No newline at end of file