hawk / commit
Initial commit of Hawk.
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); +}