diff --git a/models/song.js b/models/song.js new file mode 100644 index 0000000..6db6f1b --- /dev/null +++ b/models/song.js @@ -0,0 +1,16 @@ +class Song { + constructor(id, name, album, artists, year, duration, progress, session, state, source) { + this.id = id; + this.name = name; + this.album = album; + this.artists = artists; + this.year = year; + this.duration = duration; + this.progress = progress; + this.session = session; + this.state = state; + this.source = source; + } +} + +module.exports = Song; \ No newline at end of file diff --git a/services/trackers/AggregateTracker.js b/services/trackers/AggregateTracker.js new file mode 100644 index 0000000..41afe97 --- /dev/null +++ b/services/trackers/AggregateTracker.js @@ -0,0 +1,13 @@ +class AggregateTracker { + constructor(trackers) { + this._trackers = trackers; + } + + poll() { + const media = [] + for (let tracker of this._trackers) + media.push.apply(tracker.poll()); + + return media; + } +} \ No newline at end of file diff --git a/services/trackers/PlexTracker.js b/services/trackers/PlexTracker.js new file mode 100644 index 0000000..9a41155 --- /dev/null +++ b/services/trackers/PlexTracker.js @@ -0,0 +1,59 @@ +const axios = require("axios"); +const Song = require("../../models/song"); + +class PlexTracker { + constructor(config) { + this._config = config; + this._cache = [] + } + + async poll(useCache = false) { + if (!this._config.token || !this._config.url) + return []; + if (useCache) + return cache; + + const response = await axios.get(this._config.url + "/status/sessions", { + headers: { + "Accept": "application/json", + "X-Plex-Token": this._config.token + } + }); + + if (!response.data.MediaContainer?.Metadata) { + cache = []; + return cache; + } + + const filtered = response.data.MediaContainer?.Metadata.filter(m => this.#filter(m)); + cache = filtered.map(this.#transform); + return cache; + } + + #filter(data) { + if (!this._config.filters || this._config.filters.length == 0) + return true; + + for (let filter of this._config.filters) { + if (filter.library && !filter.library.some(l => l == data.librarySectionTitle)) + continue; + if (filter.ip && !filter.ip.some(l => l == data.address)) + continue; + if (filter.deviceId && !filter.deviceId.some(l => l == data.machineIdentifier)) + continue; + if (filter.platform && !filter.platform.some(l => l == data.Player.platform)) + continue; + if (filter.product && !filter.product.some(l => l == data.Player.product)) + continue; + return true; + } + return false; + } + + #transform(data) { + const id = data.guid.substring(data.guid.lastIndexOf('/') + 1); + return new Song(id, data.title, data.parentTitle, data.grandparentTitle, data.parentYear, data.duration, data.viewOffset, data.sessionKey, data.Player.state, "plex"); + } +} + +module.exports = PlexTracker; \ No newline at end of file diff --git a/services/trackers/SpotifyTracker.js b/services/trackers/SpotifyTracker.js new file mode 100644 index 0000000..5fc38aa --- /dev/null +++ b/services/trackers/SpotifyTracker.js @@ -0,0 +1,83 @@ +const axios = require("axios"); +const logger = require("./logging"); +const fs = require("fs/promises"); +const Song = require("../../models/song"); + +class SpotifyTracker { + constructor(config, token) { + this._token = token; + this._cache = null; + this._config = config; + this._auth = new Buffer.from(config.client_id + ':' + config.client_secret).toString('base64'); + } + + async poll(useCache = false) { + if (token == null) + return null; + if (useCache) + return cache; + + if (token.expires_at < Date.now() + 300) + await this.#refreshTokenIfNeeded(); + + 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 = null; + return null; + } + + cache = [this.#transform(response.data)]; + return cache; + } catch (ex) { + logger.error(ex, "Failed to get currently playing data from Spotify."); + return []; + } + } + + #transform(data) { + const item = data.item; + const artists = item.artists.map(a => a.name); + const year = null; + const state = data.is_playing ? "playing" : "paused"; + return new Song(item.id, item.name, item.album.name, artists, year, item.duration_ms, data.progress_ms, "spotify", state, "spotify"); + } + + async #refreshTokenIfNeeded() { + if (!this._token || this._token.expires_at && this._token.expires_at - Date.now() > 900) + return false; + + const response = await axios.post("https://accounts.spotify.com/api/token", + { + client_id: this._config.client_id, + refresh_token: this._token.refresh_token, + grant_type: "refresh_token" + }, + { + headers: { + "Authorization": "Basic " + this._auth, + "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"] = this._token.refresh_token; + + this._token = data; + await fs.writeFile("credentials.spotify.json", JSON.stringify(data)); + logger.debug("Updated access token for Spotify."); + return true; + } +} + +module.exports = SpotifyTracker; \ No newline at end of file