aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/index.ts14
-rw-r--r--app/routes/spotify/router.ts26
-rw-r--r--app/spotify/client.ts158
-rw-r--r--app/websocket/spotify.ts18
-rw-r--r--pages/spotify.html42
-rw-r--r--static/css/globalstyles.css4
-rw-r--r--static/css/spotify.css88
-rw-r--r--static/css/variables.css3
-rw-r--r--static/images/blank-album-cover.pngbin0 -> 2580 bytes
-rw-r--r--static/scripts/spotify.js114
-rw-r--r--views/partials/navbar.ejs2
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
new file mode 100644
index 0000000..aff14e2
--- /dev/null
+++ b/static/images/blank-album-cover.png
Binary files differ
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>