aboutsummaryrefslogtreecommitdiffstats
path: root/app/spotify
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.com>2023-08-06 12:47:15 +0100
committerLeonardo Bishop <me@leonardobishop.com>2023-08-06 12:57:19 +0100
commit5091b1bf0501d08bb5af90eb75a0833d7d9aba3e (patch)
tree844dd4740092b38a0168e56944d33a9e3301d32d /app/spotify
Initial commit
Diffstat (limited to 'app/spotify')
-rw-r--r--app/spotify/client.ts141
1 files changed, 141 insertions, 0 deletions
diff --git a/app/spotify/client.ts b/app/spotify/client.ts
new file mode 100644
index 0000000..cfb0d67
--- /dev/null
+++ b/app/spotify/client.ts
@@ -0,0 +1,141 @@
+import axios, { AxiosResponse } from 'axios';
+import { logger } from '../logger.js';
+import { WebSocket } from 'ws';
+import { RedisClientConnection } from '../config/redis.js';
+
+export namespace SpotifyClient {
+ let clients = new Set<WebSocket>();
+ let interval: NodeJS.Timeout;
+
+ let authenticationFailed = false;
+
+ let lastUpdate: any;
+ let lastUpdateTimestamp: number;
+
+ let redis: RedisClientConnection;
+
+ export const addClient = (client: WebSocket) => {
+ clients.add(client);
+ if (lastUpdate && lastUpdateTimestamp > Date.now() - 10000) {
+ client.send(JSON.stringify(lastUpdate));
+ }
+ }
+
+ const apiTokenUrl = 'https://accounts.spotify.com/api/token';
+
+ const spotifyClientHeaders = {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+
+ export const setTokens = async (accessToken: string, refreshToken: string) => {
+ logger.info('Re-identifying with Spotify');
+ await redis.set('spotify_access_token', accessToken);
+ await redis.set('spotify_refresh_token', refreshToken);
+ authenticationFailed = false;
+ }
+
+ const refreshAccessToken = async () => {
+ logger.info('Refreshing access token from Spotify');
+ try {
+ const refreshToken = await redis.get('spotify_refresh_token');
+ const res = await axios.post(apiTokenUrl, {
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken,
+ client_id: process.env.SPOTIFY_CLIENT_ID,
+ client_secret: process.env.SPOTIFY_CLIENT_SECRET
+ }, { headers: spotifyClientHeaders });
+ await redis.set('spotify_access_token', res.data.access_token);
+ if (res.data.refresh_token) {
+ await redis.set('spotify_refresh_token', res.data.refresh_token);
+ }
+ logger.info('Access token refreshed');
+ } catch (err) {
+ if (err.response?.data?.error) {
+ logger.error(`Failed to refresh access token: ${err.message}: ${err.response.data.error}`);
+ } else {
+ logger.error(`Failed to refresh access token: ${err.message} (${err.response.status} ${err.response.statusText} ${err.response.data.error})`);
+ }
+ authenticationFailed = true;
+ }
+ }
+
+ export const initialise = async (client: RedisClientConnection) => {
+ redis = client;
+ await refreshAccessToken();
+ await updateTimeout();
+ }
+
+ const updateTimeout = async () => {
+ const delay = await update();
+ interval = setTimeout(updateTimeout, delay*1000);
+ }
+
+ const update = async (): Promise<number> => {
+ if (authenticationFailed) {
+ return 5;
+ }
+
+ clients.forEach(client => {
+ if (client.readyState !== WebSocket.OPEN) {
+ clients.delete(client);
+ }
+ });
+
+ if (clients.size === 0) {
+ return 1;
+ }
+
+ try {
+ const token = await redis.get('spotify_access_token');
+
+ let res: AxiosResponse;
+ try {
+ res = await axios.get('https://api.spotify.com/v1/me/player/currently-playing', {
+ headers: {
+ 'Authorization': 'Bearer ' + token,
+ }
+ });
+ } catch (err) {
+ if (err.response?.status === 401) {
+ await refreshAccessToken();
+ return 1;
+ } else {
+ throw err;
+ }
+ }
+ try {
+ let update = {
+ title: res.data.item?.name,
+ duration: res.data.item?.duration_ms,
+ artist: res.data.item?.artists[0]?.name,
+ progress: res.data.progress_ms,
+ album: res.data.item?.album.name,
+ albumArt: res.data.item?.album.images[0]?.url,
+ url: res.data.item?.external_urls.spotify,
+ state: res.data.is_playing ? 'playing' : 'paused',
+ }
+ lastUpdate = update;
+ lastUpdateTimestamp = Date.now();
+ clients.forEach(client => {
+ client.send(JSON.stringify(update));
+ });
+ } 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})`);
+ }
+ }
+ return 5;
+ }
+
+ export const stop = () => {
+ clearInterval(interval);
+ clients.forEach(client => client.close());
+ }
+
+}
+