diff options
Diffstat (limited to 'app/spotify/client.ts')
| -rw-r--r-- | app/spotify/client.ts | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/app/spotify/client.ts b/app/spotify/client.ts new file mode 100644 index 0000000..2cdf527 --- /dev/null +++ b/app/spotify/client.ts @@ -0,0 +1,158 @@ +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()); + } + +} |
