From 79b27b8e32cb96a232be70ffff9512088c312dc7 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 4 Dec 2024 19:01:40 +0000 Subject: [PATCH] Added Spotify tracking. Fixed filters when none given. --- .gitignore | 3 +- config/configuration.js | 8 ++++ controllers/home.js | 48 ++++++++++++++++++++ routes/home.js | 9 ++++ services/logging.js | 1 - services/poll.js | 13 ++++-- services/spotify.js | 98 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 controllers/home.js create mode 100644 services/spotify.js diff --git a/.gitignore b/.gitignore index c78365a..6d7089b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ logs/ config/* -!config/configuration.js \ No newline at end of file +!config/configuration.js +credentials.* \ No newline at end of file diff --git a/config/configuration.js b/config/configuration.js index 0b3cdfb..a72ef17 100644 --- a/config/configuration.js +++ b/config/configuration.js @@ -22,6 +22,11 @@ const configuration = { */ }, }, + spotify: { + client_id: null, + client_secret: null, + redirect_uri: null + }, web: { host: null, port: null @@ -43,6 +48,9 @@ if (config.has("scrobble.minimum.duration")) if (config.has("scrobble.minimum.percent")) configuration.scrobble.minimum.percent = config.get("scrobble.minimum.percent"); +if (config.has("spotify")) + configuration.spotify = config.get("spotify"); + if (config.has("web.host")) configuration.web.host = config.get("web.host"); if (config.has("web.port")) diff --git a/controllers/home.js b/controllers/home.js new file mode 100644 index 0000000..a34d846 --- /dev/null +++ b/controllers/home.js @@ -0,0 +1,48 @@ +const axios = require("axios"); +const config = require("../config/configuration"); +const fs = require("fs/promises"); +const logger = require("../services/logging"); +const querystring = require('node:querystring'); + +function authorizeSpotify(req, res) { + res.redirect("https://accounts.spotify.com/authorize?" + querystring.stringify({ + response_type: "code", + client_id: config.spotify.client_id, + scope: "user-read-playback-state user-read-currently-playing", + redirect_uri: config.spotify.redirect_uri, + })); +} + +async function callback(req, res) { + const code = req.query.code; + + try { + const response = await axios.post("https://accounts.spotify.com/api/token", + { + code: code, + redirect_uri: config.spotify.redirect_uri, + grant_type: "authorization_code" + }, + { + headers: { + "Authorization": "Basic " + new Buffer.from(config.spotify.client_id + ':' + config.spotify.client_secret).toString('base64'), + "Content-Type": "application/x-www-form-urlencoded" + } + } + ); + + const data = response.data; + data["expires_at"] = Date.now() + data["expires_in"] * 1000; + await fs.writeFile("credentials.spotify.json", JSON.stringify(data)); + + res.redirect("/"); + } catch (ex) { + logger.error(ex, "Failed to get Spotify oauth."); + res.send({ 'error': "Something went wrong with spotify's oauth flow" }); + } +} + +module.exports = { + authorizeSpotify, + callback +} \ No newline at end of file diff --git a/routes/home.js b/routes/home.js index 0eab865..24f103e 100644 --- a/routes/home.js +++ b/routes/home.js @@ -1,7 +1,16 @@ +const home = require("../controllers/home"); const router = require("express").Router(); router.get("/", async (req, res) => { res.send("welcome to an empty page."); }); +router.get("/auth/spotify", (req, res) => { + home.authorizeSpotify(req, res); +}); + +router.get("/callback", (req, res) => { + home.callback(req, res); +}); + module.exports = router; \ No newline at end of file diff --git a/services/logging.js b/services/logging.js index 5aceee1..7ede090 100644 --- a/services/logging.js +++ b/services/logging.js @@ -2,7 +2,6 @@ const pino = require("pino"); const logger = pino(pino.destination({ dest: 'logs/.log', sync: false })); const environment = process.env.NODE_ENV || 'development'; -console.log(process.env.NODE_ENV); if (environment == "production") { logger.level = 30 } else { diff --git a/services/poll.js b/services/poll.js index 6f80ca6..ebfbf21 100644 --- a/services/poll.js +++ b/services/poll.js @@ -1,6 +1,7 @@ const plex = require("./plex"); const logger = require("./logging") const config = require("../config/configuration"); +const spotify = require("./spotify"); const lastPlaying = {}; const lastScrobbleTimes = {}; @@ -9,6 +10,12 @@ async function poll() { const now = Date.now(); const playing = []; + await spotify.loadCredentials(); + await spotify.refreshTokenIfNeeded(); + const spotifyTrack = await spotify.getCurrentlyPlaying(); + if (spotifyTrack != null) + playing.push(spotifyTrack); + try { const data = await plex.getCurrentlyPlaying(); playing.push.apply(playing, data); @@ -46,8 +53,8 @@ async function poll() { } function applyFilter(track, filters) { - if (!filters) - return; + if (!filters || filters.length == 0) + return true; for (let filter of filters) { if (filter.library && !filter.library.some(l => l == track.library)) @@ -67,7 +74,7 @@ function applyFilter(track, filters) { function checkIfCanScrobble(current, previous, now) { if (!previous) - return; + return false; let filters = []; if (previous.source == 'plex') diff --git a/services/spotify.js b/services/spotify.js new file mode 100644 index 0000000..f0472d7 --- /dev/null +++ b/services/spotify.js @@ -0,0 +1,98 @@ +const axios = require("axios"); +const config = require("../config/configuration") +const logger = require("./logging"); +const fs = require("fs/promises"); +const fss = require("fs") + +let token = null; +let cache = {} + +async function refreshTokenIfNeeded() { + if (!token || token["expires_at"] && token["expires_at"] - Date.now() > 900) { + return false; + } + + try { + const response = await axios.post("https://accounts.spotify.com/api/token", + { + client_id: config.spotify.client_id, + refresh_token: token["refresh_token"], + grant_type: "refresh_token" + }, + { + headers: { + "Authorization": "Basic " + new Buffer.from(config.spotify.client_id + ':' + config.spotify.client_secret).toString('base64'), + "Content-Type": "application/x-www-form-urlencoded" + } + } + ); + + const data = response.data; + data["expires_at"] = Date.now() + data["expires_in"] * 1000; + if (!data["refresh_token"]) { + data["refresh_token"] = token["refresh_token"]; + } + await fs.writeFile("credentials.spotify.json", JSON.stringify(data)); + token = data; + logger.info("Updated access token for Spotify."); + return true; + } catch (ex) { + logger.error(ex, "Failed to get Spotify oauth."); + return false; + } +} + +async function loadCredentials() { + if (!fss.existsSync("credentials.spotify.json")) { + logger.info("No Spotify credentials found."); + return; + } + + const content = await fs.readFile("credentials.spotify.json", "utf-8"); + token = JSON.parse(content); +} + +async function getCurrentlyPlaying(cached = false) { + if (cached) { + return cache['spotify'] + } + + try { + const response = await axios.get("https://api.spotify.com/v1/me/player/currently-playing", + { + headers: { + "Authorization": "Bearer " + token["access_token"] + } + } + ); + + if (!response.data) { + cache['spotify'] = null; + return null; + } + + const media = response.data.item; + cache['spotify'] = { + "track": media.name, + "album": media.album.name, + "artist": media.artists.map(a => a.name).join(', '), + "year": media.parentYear, + "duration": media.duration_ms, + "playtime": response.data.progress_ms, + "mediaKey": media.id, + "sessionKey": "spotify", + "state": response.data.is_playing ? "playing" : "paused", + "source": "spotify" + }; + return cache['spotify']; + } catch (ex) { + logger.error(ex, "Failed to get currently playing data from Spotify."); + return null; + } +} + +module.exports = { + refreshTokenIfNeeded, + loadCredentials, + getCurrentlyPlaying +} \ No newline at end of file