diff options
| -rw-r--r-- | app/index.ts | 14 | ||||
| -rw-r--r-- | app/routes/spotify/router.ts | 26 | ||||
| -rw-r--r-- | app/spotify/client.ts | 158 | ||||
| -rw-r--r-- | app/websocket/spotify.ts | 18 | ||||
| -rw-r--r-- | pages/spotify.html | 42 | ||||
| -rw-r--r-- | static/css/globalstyles.css | 4 | ||||
| -rw-r--r-- | static/css/spotify.css | 88 | ||||
| -rw-r--r-- | static/css/variables.css | 3 | ||||
| -rw-r--r-- | static/images/blank-album-cover.png | bin | 0 -> 2580 bytes | |||
| -rw-r--r-- | static/scripts/spotify.js | 114 | ||||
| -rw-r--r-- | views/partials/navbar.ejs | 2 |
11 files changed, 236 insertions, 233 deletions
diff --git a/app/index.ts b/app/index.ts index 3558d1f..664a751 100644 --- a/app/index.ts +++ b/app/index.ts @@ -4,12 +4,6 @@ import * as page from './routes/page/router.js'; import * as blog from './routes/blog/router.js'; import { logger } from './logger.js' import { PageDirectory } from './pages.js'; -// import { SpotifyClient } from './spotify/client.js'; -// import { WebSocketServer } from 'ws'; -// import * as spotifyauth from './routes/spotify/router.js'; -// import * as spotifyWs from './websocket/spotify.js'; - -// TODO: Figure out Spotify's tedious auth flow dotenv.config() @@ -24,7 +18,6 @@ app.use(express.static('static', { app.use(blog.router); app.use(page.router); -// app.use(spotifyauth.router); app.use((req, res) => { res.render('error.ejs', { @@ -35,14 +28,9 @@ app.use((req, res) => { const server = app.listen(process.env.PORT, () => { logger.info(`App listening on port ${process.env.PORT}`); }); -// const websocketServer: WebSocketServer = spotifyWs.createWebsocketServer(server); const exit = () => { logger.info('Stopping server...'); - // websocketServer.clients.forEach(client => { - // client.terminate(); - // }); - // websocketServer.close(); server.close(() => { process.exit(0); }) @@ -50,7 +38,5 @@ const exit = () => { PageDirectory.rebuild('pages'); -// SpotifyClient.initialise(); - process.on('SIGINT', exit); process.on('SIGTERM', exit); diff --git a/app/routes/spotify/router.ts b/app/routes/spotify/router.ts deleted file mode 100644 index faf8f6d..0000000 --- a/app/routes/spotify/router.ts +++ /dev/null @@ -1,26 +0,0 @@ -import express from 'express'; - -export const router = express.Router({ mergeParams: true }); - -router.get('/spotify/auth', (req, res, next) => { - let scope = 'user-read-currently-playing'; - let params = new URLSearchParams(); - params.append('response_type', 'code'); - params.append('client_id', process.env.SPOTIFY_CLIENT_ID); - params.append('scope', scope); - params.append('redirect_uri', process.env.SPOTIFY_REDIRECT_URI); - - res.redirect('https://accounts.spotify.com/authorize?' + params.toString()); -}); - -router.get('/spotify/auth/callback', (req, res, next) => { - if (req.query.error) { - res.send('Error: ' + req.query.error); - return; - } - if (!req.query.code) { - res.send('No code'); - return; - } - res.send('Your authentication code: ' + req.query.code); -}); diff --git a/app/spotify/client.ts b/app/spotify/client.ts deleted file mode 100644 index 2cdf527..0000000 --- a/app/spotify/client.ts +++ /dev/null @@ -1,158 +0,0 @@ -import axios from 'axios'; -import { logger } from '../logger.js'; -import { WebSocket } from 'ws'; - -export namespace SpotifyClient { - let clients = new Set<WebSocket>(); - let interval: NodeJS.Timeout; - - let acceptingClients = false; - let authenticationFailed = false; - - let accessToken: string; - let refreshToken: string; - - export const addClient = (client: WebSocket) => { - if (acceptingClients) { - clients.add(client); - } else { - client.close(); - } - } - - const apiTokenUrl = 'https://accounts.spotify.com/api/token'; - - const spotifyClientHeaders = { - 'Authorization': 'Basic ' + Buffer.from(process.env.SPOTIFY_CLIENT_ID + ':' + process.env.SPOTIFY_CLIENT_SECRET).toString('base64'), - 'Content-Type': 'application/x-www-form-urlencoded', - } - - const handleApiError = (err: any, verb: string) => { - if (err.response?.data?.error) { - logger.error(`Failed to ${verb} access token: ${err.message}: ${err.response.data.error}`); - } else { - logger.error(`Failed to get access token: ${err.message} (${err.response.status} ${err.response.statusText} ${err.response.data.error})`); - } - accessToken = undefined; - refreshToken = undefined; - } - - export const requestAccessToken = async () => { - logger.info('Requesting access token from Spotify'); - await axios.post(apiTokenUrl, { - grant_type: 'authorization_code', - code: process.env.SPOTIFY_AUTH_CODE, - redirect_uri: process.env.SPOTIFY_REDIRECT_URI, - }, - { headers: spotifyClientHeaders, - }).then(res => { - logger.info('Authenticated with Spotify'); - accessToken = res.data.access_token; - }).catch(err => { - handleApiError(err, 'request'); - }); - } - - export const refreshAccessToken = async () => { - logger.info('Refreshing access token from Spotify'); - await axios.post(apiTokenUrl, { - grant_type: 'refresh_token', - refresh_token: refreshToken, - }, - { headers: spotifyClientHeaders, - }).then(res => { - logger.info('Refreshed access token from Spotify'); - accessToken = res.data.access_token; - }).catch(err => { - handleApiError(err, 'refresh'); - }); - } - - - export const initialise = async () => { - if (!accessToken) { - await requestAccessToken(); - if (!accessToken) { - logger.error('Failed to get access token, giving up permanently'); - authenticationFailed = true; - return; - } - } - await updateTimeout(); - acceptingClients = true; - } - - const updateTimeout = async () => { - await update(); - interval = setTimeout(updateTimeout, 5000); - } - - export const update = async () => { - if (authenticationFailed) { - return; - } - clients.forEach(client => { - if (client.readyState !== WebSocket.OPEN) { - clients.delete(client); - } - }); - if (clients.size === 0) { - return; - } - await axios.get('https://api.spotify.com/v1/me/player/currently-playing', { - headers: { - 'Authorization': 'Bearer ' + accessToken, - } - }).then(async (res) => { - if (res.status === 401) { - logger.info('Access token expired, refreshing'); - await refreshAccessToken(); - if (!accessToken) { - authenticationFailed = true; - logger.error('Failed to get access token, giving up permanently'); - stop(); - return; - } - await update(); - return; - } - if (res.status !== 200) { - logger.error(`Failed to get current song: ${res.status} ${res.statusText}`); - return; - } - try { - let song = res.data.item.name; - let duration = res.data.item.duration_ms; - let artist = res.data.item.artists[0].name; - let time = res.data.progress_ms; - let album = res.data.item.album.name; - let albumImage = res.data.item.album.images[0].url; - clients.forEach(client => { - client.send(JSON.stringify({ - song: song, - artist: artist, - time: time, - duration: duration, - album: album, - albumImage: albumImage, - })); - }); - } catch (err) { - logger.error(`Failed to parse and send current song: ${err.message}`); - } - }).catch(err => { - if (err.response?.data?.error?.message) { - logger.error(`Failed to get current song: ${err.message}: ${err.response.data.error.message}`); - } else { - logger.error(`Failed to get current song: ${err.message} (${err.response.status} ${err.response.statusText} ${err.response.data.error})`); - } - }); - } - - export const stop = () => { - clearInterval(interval); - acceptingClients = false; - clients.forEach(client => client.close()); - } - -} diff --git a/app/websocket/spotify.ts b/app/websocket/spotify.ts deleted file mode 100644 index 4b81fe0..0000000 --- a/app/websocket/spotify.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Server } from 'http'; -import { WebSocketServer } from 'ws'; -import { SpotifyClient } from '../spotify/client.js'; - -export const createWebsocketServer = (server: Server): WebSocketServer => { - const wss = new WebSocketServer({ noServer: true }); - server.on('upgrade', (req, socket, head) => { - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit('connection', ws, req) - }) - }) - - wss.on('connection', (ws) => { - SpotifyClient.addClient(ws); - }); - - return wss; -} diff --git a/pages/spotify.html b/pages/spotify.html index b0110ba..e7e51b2 100644 --- a/pages/spotify.html +++ b/pages/spotify.html @@ -1,21 +1,41 @@ --- +stylesheets: + - /css/spotify.css scripts: - /scripts/spotify.js --- <h1>Spotify</h1> -<b> - This is a work in progress once I figure out Spotify's tedious - authentication flow. -</b> -<div id="music-player"> - <span id="song-title"></span> - <span id="song-artist"></span> - <span id="song-album"></span> +<div id="music-player" class="monospace"> + <img id="song-album-art" src=""></img> + <div id="track"> + <span id="song-title"></span> + <div> + <span id="song-artist"></span> - <span id="song-album"></span> + </div> + <div id="song-progress-bar"> + <span id="song-progress"></span> + <div id="song-progress-bar-fill"> + <div id="song-progress-bar-background"></div> + <div id="song-progress-bar-thumb"></div> + </div> + <span id="song-duration"></span> + </div> + </div> <br> - <span id="connection-status"></span> </div> -<br> +<div id="metadata" class="monospace small"> + <div id="connection-status"> + <div id="connection-status-indicator"></div> + <span id="connection-status-text">Disconnected</span> + </div> + <a id="open-in-spotify" target="_blank" href="#"> + Open in Spotify + </a> +</div> +<p class="small"> <em> This page shows what I'm currently listening to on Spotify. - It requires JavaScript to work. + It requires JavaScript to work, and was inspired by + <a href="https://joinmymusic.com/">ssamjh's Music Sync</a>. </em> +</p> diff --git a/static/css/globalstyles.css b/static/css/globalstyles.css index 9fbb974..b6da1f9 100644 --- a/static/css/globalstyles.css +++ b/static/css/globalstyles.css @@ -10,6 +10,10 @@ html, body { font-family: var(--font-sans-serif); } +.small { + font-size: 0.8rem; +} + #main-container { display: flex; flex-direction: row; diff --git a/static/css/spotify.css b/static/css/spotify.css new file mode 100644 index 0000000..a3cde4d --- /dev/null +++ b/static/css/spotify.css @@ -0,0 +1,88 @@ +#music-player { + display: flex; + flex-direction: row; + gap: 1rem; + padding: 1rem; + border: 1px solid var(--color-soft-outline); + background-color: var(--color-soft-fill); +} + +#song-album-art { + width: 5rem; +} + +#track { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: 0.2rem; +} + +#song-title { + color: var(--color-text); + font-size: 1rem; +} + +#song-details, #song-artist, #song-album { + color: var(--color-text-muted); +} + +#song-progress-bar { + display: flex; + flex-direction: row; + align-items: center; + color: var(--color-text-muted); + width: 100%; + gap: 0.5rem; +} + +#song-progress-bar-fill { + position: relative; + height: 4px; + flex-grow: 1; +} + +#song-progress-bar-background { + background-color: var(--color-soft-fill); + height: 4px; + border-radius: 2px; + width: 100%; +} + +#song-progress-bar-thumb { + background-color: var(--color-text-link); + height: 4px; + border-radius: 2px; + position: absolute; + left: 0; + top: 0; +} + +#metadata { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 0.5rem; + margin: 0.5rem 0; +} + +#connection-status { + color: var(--color-text-muted); + display: flex; + flex-direction: row; + align-items: baseline; + gap: 0.3rem; + flex-grow: 1; +} + +#connection-status-indicator { + display: inline-block; + background-color: #ff4136; + border-radius: 50%; + width: 0.5rem; + height: 0.5rem; +} + +#open-in-spotify { + display: none; +} diff --git a/static/css/variables.css b/static/css/variables.css index 123ee30..5d8610f 100644 --- a/static/css/variables.css +++ b/static/css/variables.css @@ -16,6 +16,9 @@ --color-text-muted: #aaa; --color-text-link: #ff851b; --color-text-link-bg: rgba(255, 133, 27, 0.2); + + --color-soft-outline: rgba(255, 255, 255, 0.2); + --color-soft-fill: rgba(255, 255, 255, 0.05); --color-scrollbar: rgba(255, 255, 255, 0.4); } diff --git a/static/images/blank-album-cover.png b/static/images/blank-album-cover.png Binary files differnew file mode 100644 index 0000000..aff14e2 --- /dev/null +++ b/static/images/blank-album-cover.png diff --git a/static/scripts/spotify.js b/static/scripts/spotify.js index e6551a8..7c62150 100644 --- a/static/scripts/spotify.js +++ b/static/scripts/spotify.js @@ -1,12 +1,116 @@ +const connectedColor = '#2ECC40'; +const connectingColor = '#FF851B'; +const disconnectedColor = '#FF4136'; + +const websocketUrl = 'ws://wailt.leonardobishop.com/'; + +let progressMillis; +let durationMillis; + +let predictProgressInterval; + +const msToTime = (duration) => { + const seconds = Math.floor((duration / 1000) % 60); + const minutes = Math.floor((duration / (1000 * 60)) % 60); + + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +} + +const setProgress = (progressMillis, durationMillis) => { + const progressPercent = durationMillis == 0 ? 0 : Math.min((progressMillis / durationMillis) * 100, 100); + + const songProgress = document.getElementById('song-progress'); + const songDuration = document.getElementById('song-duration'); + const songProgressBarThumb = document.getElementById('song-progress-bar-thumb'); + + const progressTime = msToTime(progressMillis); + const durationTime = msToTime(durationMillis); + + songProgress.innerHTML = progressTime; + songDuration.innerHTML = durationTime; + songProgressBarThumb.style.width = `${progressPercent}%`; +} + +const predictProgress = () => { + progressMillis = Math.min(durationMillis, progressMillis + 1000); + setProgress(progressMillis, durationMillis); +} + +const setDefaultData = () => { + const songTitle = document.getElementById('song-title'); + const songArtist = document.getElementById('song-artist'); + const songAlbum = document.getElementById('song-album'); + const songAlbumArt = document.getElementById('song-album-art'); + + songTitle.innerHTML = 'No song playing'; + songArtist.innerHTML = ''; + songAlbum.innerHTML = ''; + songAlbumArt.src = '/images/blank-album-cover.png'; + + setProgress(0, 0); +} + +const setOpenInSpotify = (songUrl) => { + const openInSpotify = document.getElementById('open-in-spotify'); + if (songUrl) { + openInSpotify.href = songUrl; + openInSpotify.style.display = 'block'; + } else { + openInSpotify.style.display = 'none'; + } +} + const connectWebsocket = () => { - document.getElementById('connection-status').innerHTML = "Connecting..."; - let url = new URL(window.location.href); - url.protocol = url.protocol.replace('http', 'ws'); - const socket = new WebSocket(url); + const connectionStatus = document.getElementById('connection-status-text'); + const connectionStatusIndicator = document.getElementById('connection-status-indicator'); + const onDisconnect = () => { + connectionStatus.innerHTML = "Disconnected"; + connectionStatusIndicator.style.backgroundColor = disconnectedColor; + setDefaultData(); + clearInterval(predictProgressInterval); + } + const onConnect = () => { + connectionStatus.innerHTML = "Connected"; + connectionStatusIndicator.style.backgroundColor = connectedColor; + } + + connectionStatus.innerHTML = "Connecting"; + connectionStatusIndicator.style.backgroundColor = connectingColor; + + const songTitle = document.getElementById('song-title'); + const songArtist = document.getElementById('song-artist'); + const songAlbum = document.getElementById('song-album'); + const songAlbumArt = document.getElementById('song-album-art'); + + const updateData = (data) => { + clearInterval(predictProgressInterval); + progressMillis = data.progress; + durationMillis = data.duration; + + songTitle.innerHTML = data.title; + songArtist.innerHTML = data.artist; + songAlbum.innerHTML = data.album; + songAlbumArt.src = data.albumArt || '/images/blank-album-cover.png'; + + setProgress(progressMillis, durationMillis); + setOpenInSpotify(data.url); + + if (data.state === 'playing') { + predictProgressInterval = setInterval(predictProgress, 1000); + } + } + + const socket = new WebSocket(websocketUrl); + socket.onmessage = (event) => { const data = JSON.parse(event.data); - console.log(data); + updateData(data); } + + socket.addEventListener('open', onConnect); + socket.addEventListener('close', onDisconnect); + socket.addEventListener('error', onDisconnect); } +document.addEventListener("DOMContentLoaded", setDefaultData); document.addEventListener("DOMContentLoaded", connectWebsocket); diff --git a/views/partials/navbar.ejs b/views/partials/navbar.ejs index 2797347..5e2d75b 100644 --- a/views/partials/navbar.ejs +++ b/views/partials/navbar.ejs @@ -4,6 +4,6 @@ <a href="/now">now</a> <a href="/blog">blog</a> <a href="/projects">projects</a> - <!-- <a href="/spotify">spotify</a> --> + <a href="/spotify">spotify</a> <a href="/contact">contact</a> </div> |
