fuzzyworld

hawk / commit

Initial commit of Hawk.

601035fbce70f72f8c7e84b06988e810e5719d6e · Pavel Safronov · 2023-10-12 16:20:50 -0700

Initial commit of Hawk.

Progress so far:
- got Spotify sign-in working
- got sign-out working
- when signed in, we retrieve the user's profile and log it to the console
- when signed in, we show the search controls, but these aren't hooked up yet
- the scope will need to be updated to ensure that we can get the user's albums and such
 README.md         | 15 +++++++++
 public/api.js     | 17 ++++++++++
 public/index.html | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 public/main.js    | 38 ++++++++++++++++++++++
 public/signin.js  | 83 ++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 247 insertions(+)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9b7717e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# Hawk
+
+## Description
+
+You can specify a duration ("5 and a half minutes"), and Hawk will find a song in your library with that length, or as close to it as possible. Name comes from "Hudson Hawk", https://www.imdb.com/title/tt0102070/, where Bruce Willis does this for heists.
+
+## Links
+
+Repo: https://gitlab.com/PavelSafronov/hawk
+Pages location: https://pavelsafronov.gitlab.io/hawk/
+
+## Spotify info
+
+Client ID: 6c4c04a3751b4e409ddad7d71aad9949
+
diff --git a/public/api.js b/public/api.js
new file mode 100644
index 0000000..f25cc8a
--- /dev/null
+++ b/public/api.js
@@ -0,0 +1,17 @@
+
+async function getProfile() {
+    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', {
+        headers: {
+            Authorization: 'Bearer ' + accessToken
+        }
+    });
+
+    const data = await response.json();
+    return data;
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..c622578
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <title>Hawk</title>
+    <favicon href="h.png" />
+    <style>
+        body {
+            font-family: sans-serif;
+            margin: 0;
+            padding: 0;
+        }
+
+        #main,
+        #controls,
+        #sign-in-div {
+            margin-top: 2em;
+            text-align: center;
+        }
+
+        #minutes,
+        #seconds,
+        #sign-in-div {
+            display: flex;
+            flex-direction: row;
+            align-items: center;
+            display: inline-block;
+        }
+
+        #minutes-input,
+        #seconds-input {
+            width: 3em;
+            margin-left: 0.5em;
+            margin-right: 0.5em;
+            padding: 0.5em;
+
+        }
+
+        div {
+            margin-left: 0.5em;
+            margin-right: 0.5em;
+            padding: 0.5em;
+        }
+
+        #header {
+            margin-top: 1em;
+            text-align: center;
+        }
+    </style>
+</head>
+
+<body>
+    <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
+            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.
+        </div>
+    </div>
+    <div id="main">
+        <div id="sign-in-div">
+            <button id="signin">Sign In</button>
+        </div>
+        <div id="controls">
+            <span id="minutes">
+                <input id="minutes-input" type="number" min="0" max="59" value="5" />
+                <label for="minutes-input">minutes</label>
+            </span>
+            <span id="seconds">
+                <input id="seconds-input" type="number" min="0" max="59" value="32" />
+                <label for="seconds-input">seconds</label>
+            </span>
+            <button id="search">Search</button>
+
+            <div id="results"></div>
+
+            <button id="signout">Sign Out</button>
+        </div>
+    </div>
+    <script src="signin.js"></script>
+    <script src="api.js"></script>
+    <script src="main.js"></script>
+
+</body>
+
+</html>
\ No newline at end of file
diff --git a/public/main.js b/public/main.js
new file mode 100644
index 0000000..35b66b8
--- /dev/null
+++ b/public/main.js
@@ -0,0 +1,38 @@
+const urlParams = new URLSearchParams(window.location.search);
+const code = urlParams.get('code');
+const error = urlParams.get('error');
+if (code) {
+    getToken(code).then(() => {
+        window.history.replaceState({}, document.title, '/');
+        window.location = '/public/index.html';
+    })
+} else if (error) {
+    console.error(error);
+    window.history.replaceState({}, document.title, '/');
+    window.location = '/public/index.html';
+}
+
+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');
+    signInButton.addEventListener('click', signIn);
+    controlsDiv.setAttribute('style', 'display: none');
+} else {
+    signInButton.setAttribute('style', 'display: none');
+    controlsDiv.setAttribute('style', 'display: block');
+}
+
+const signOutButton = document.getElementById('signout');
+signOutButton.addEventListener('click', signOut);
+
+if (showProfile) {
+    getProfile().then(profile => {
+        console.log(profile);
+    });
+}
\ No newline at end of file
diff --git a/public/signin.js b/public/signin.js
new file mode 100644
index 0000000..f447a95
--- /dev/null
+++ b/public/signin.js
@@ -0,0 +1,83 @@
+const clientId = '6c4c04a3751b4e409ddad7d71aad9949';
+const redirectUri = 'http://localhost:5500/public/index.html';
+
+async function signOut() {
+    localStorage.removeItem('access_token');
+    window.location = '/public/index.html';
+}
+
+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);
+
+    let args = new URLSearchParams({
+        response_type: 'code',
+        client_id: clientId,
+        scope: scope,
+        redirect_uri: redirectUri,
+        state: state,
+        code_challenge_method: 'S256',
+        code_challenge: codeChallenge
+    });
+
+    window.location = 'https://accounts.spotify.com/authorize?' + args;
+}
+
+async function getToken(code) {
+    let codeVerifier = localStorage.getItem('code_verifier');
+
+    let body = new URLSearchParams({
+        grant_type: 'authorization_code',
+        code: code,
+        redirect_uri: redirectUri,
+        client_id: clientId,
+        code_verifier: codeVerifier
+    });
+
+    try {
+        const response = await fetch('https://accounts.spotify.com/api/token', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/x-www-form-urlencoded'
+            },
+            body: body
+        });
+        if (!response.ok) {
+            console.error('HTTP status ' + response.status);
+            throw new Error('HTTP status ' + response.status);
+        }
+        const data = await response.json();
+        localStorage.setItem('access_token', data.access_token);
+    } catch (error) {
+        console.error('Error:', error);
+    }
+}
+
+function generateRandomString(length) {
+    let text = '';
+    let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+    for (let i = 0; i < length; i++) {
+        text += possible.charAt(Math.floor(Math.random() * possible.length));
+    }
+    return text;
+}
+
+async function generateCodeChallenge(codeVerifier) {
+    function base64encode(string) {
+        return btoa(String.fromCharCode.apply(null, new Uint8Array(string)))
+            .replace(/\+/g, '-')
+            .replace(/\//g, '_')
+            .replace(/=+$/, '');
+    }
+
+    const encoder = new TextEncoder();
+    const data = encoder.encode(codeVerifier);
+    const digest = await window.crypto.subtle.digest('SHA-256', data);
+
+    return base64encode(digest);
+}