diff --git a/app.js b/app.js index 948a093..52b9e3d 100644 --- a/app.js +++ b/app.js @@ -3,11 +3,18 @@ const express = require('express'); const helmet = require("helmet"); const logger = require("./services/logging"); const rateLimit = require("express-rate-limit"); +const sessions = require("./services/session-manager"); +const PlexTracker = require("./services/trackers/PlexTracker"); +const SpotifyTracker = require("./services/trackers/SpotifyTracker"); +const Recorder = require("./services/recorder"); -const poll = require("./services/poll"); -setInterval(poll, 5000); -const PORT = process.env.PORT || config.web.port || 9111; +const spotify = new SpotifyTracker(config.spotify); +(async () => await spotify.loadCredentials())(); +const plex = new PlexTracker(config.plex); +const recorder = new Recorder(sessions, [plex, spotify], config.scrobble, logger); + +setInterval(() => recorder.record(), 5000); const app = express(); app.use(express.json()); @@ -27,6 +34,7 @@ const limiter = rateLimit({ app.use(helmet()); app.use(limiter); +const PORT = process.env.PORT || config.web.port || 9111; app.listen(PORT, () => { logger.info("Listening to port " + PORT + "."); }); \ No newline at end of file diff --git a/models/session.js b/models/session.js index c6212bc..18d45b3 100644 --- a/models/session.js +++ b/models/session.js @@ -19,7 +19,6 @@ class Session { } set playing(value) { - console.log('playing =', value); this.#current = value; } diff --git a/services/poll.js b/services/poll.js deleted file mode 100644 index e7310c8..0000000 --- a/services/poll.js +++ /dev/null @@ -1,126 +0,0 @@ -const plex = require("./plex"); -const logger = require("./logging") -const config = require("../config/configuration"); -const Session = require("../models/session"); -const sessions = require("../services/session-manager"); -const spotify = require("./spotify"); - -let lastTick = Date.now(); - -async function poll() { - const now = Date.now(); - const timeDiff = now - lastTick; - 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); - } catch (ex) { - logger.error(ex, "Could not fetch currently playing data from Plex."); - return; - } - - for (let current of playing) { - let session = sessions.get(current.sessionKey); - if (session == null) { - session = new Session(current.sessionKey); - sessions.add(session); - } - - const previous = session.playing; - session.playing = current; - if (previous == null) { - logger.info(current, "A new session has started."); - continue; - } - - if (session.playing.state == "paused" || previous.state == "paused") { - session.pauseDuration += timeDiff - (session.playing.playtime - previous.playtime); - } - - if (checkIfCanScrobble(session, previous, now, timeDiff)) { - logger.info(previous, "Scrobble"); - session.pauseDuration = 0; - session.lastScrobbleTimestamp = now - (timeDiff - (previous.duration - previous.playtime)); - } else if (session.playing.playtime < previous.playtime && session.playing.mediaKey != previous.mediaKey) { - session.pauseDuration = 0; - if (session.playing.playtime < timeDiff) - session.lastScrobbleTimestamp = now - session.playing.playtime; - else - session.lastScrobbleTimestamp = now; - } - } - - const ids = sessions.getSessionIds(); - for (let sessionId of ids) { - if (playing.some(p => p.sessionKey == sessionId)) - continue; - - session.playing = null; - if (checkIfCanScrobble(session, session.playing, now, timeDiff)) { - logger.info(session.playing, "Scrobble"); - } - sessions.remove(sessionId); - logger.debug("Deleted old session (" + sessionId + ")"); - } - - lastTick = now; -} - -function applyFilter(track, filters) { - if (!filters || filters.length == 0) - return true; - - for (let filter of filters) { - if (filter.library && !filter.library.some(l => l == track.library)) - continue; - if (filter.ip && !filter.ip.some(l => l == track.ip)) - continue; - if (filter.deviceId && !filter.deviceId.some(l => l == track.deviceId)) - continue; - if (filter.platform && !filter.platform.some(l => l == track.platform)) - continue; - if (filter.product && !filter.product.some(l => l == track.product)) - continue; - return true; - } - return false; -} - -function checkIfCanScrobble(session, previous, now, timeDiff) { - if (!previous) - return false; - - let filters = []; - if (previous.source == 'plex') - filters = config.plex.filters; - - if (!applyFilter(previous, filters)) { - logger.debug(previous, 'No filters got triggered. Ignoring.'); - return false; - } - - const scrobbleDuration = config.scrobble.minimum.duration || 240; - const scrobblePercent = config.scrobble.minimum.percent || 50; - - const current = session.playing; - const durationPlayed = now - (session.lastScrobbleTimestamp ?? session.started) - session.pauseDuration; - const newPlayback = current == null || current.playtime < previous.playtime && current.playtime < timeDiff; - const canBeScrobbled = durationPlayed > scrobbleDuration * 1000 || durationPlayed / previous.duration > scrobblePercent / 100.0; - - return newPlayback && canBeScrobbled; -} - -function isInt(value) { - return !isNaN(value) && - parseInt(Number(value)) == value && - !isNaN(parseInt(value, 10)); -} - -module.exports = poll; \ No newline at end of file diff --git a/services/recorder.js b/services/recorder.js new file mode 100644 index 0000000..9cddef9 --- /dev/null +++ b/services/recorder.js @@ -0,0 +1,99 @@ +const AggregateTracker = require("./trackers/AggregateTracker"); +const Session = require("../models/session"); + +class Recorder { + #sessions = null; + #trackers = null; + #config = null; + #logger = null; + #lastTick = null; + + constructor(sessions, trackers, config, logger) { + this.#sessions = sessions; + this.#trackers = new AggregateTracker(trackers); + this.#config = config; + this.#logger = logger; + this.#lastTick = Date.now(); + } + + async record() { + const now = Date.now(); + const timeDiff = now - this.#lastTick; + const media = await this.#trackers.poll(); + const data = media.map(m => this.#fetchSession(m)); + + // Find sessions that ended and that are deemable of a scrobble. + const sessionIds = this.#sessions.getSessionIds(); + const stopped = sessionIds.filter(sessionId => !data.some(d => sessionId == d.session.id)).map(s => this.#sessions.get(s)); + const sessionEnded = stopped.filter(s => this.#canScrobble(s, null, s.playing, now)); + + // Find ongoing sessions that have moved on to the next song. + const finishedPlaying = data.filter(d => this.#listen(d.session, d.media, d.session.playing, now, timeDiff)); + + const scrobbling = finishedPlaying.concat(sessionEnded); + for (let track of scrobbling) + this.#scrobble(track); + + // Remove dead sessions. + for (let sessionId of stopped) + this.#sessions.remove(sessionId); + + this.#lastTick = now; + } + + #fetchSession(media) { + let session = this.#sessions.get(media.session); + if (session == null) { + session = new Session(media.session); + this.#sessions.add(session); + } + + return { session: session, media: media } + } + + #listen(session, current, previous, timestamp, timeDiff) { + session.playing = current; + + if (previous == null) { + this.#logger.info(current, "A new session has started."); + return false; + } + + if (session.playing.state == "paused" || previous.state == "paused") { + session.pauseDuration += timeDiff - (session.playing.progress - previous.progress); + } + + if (this.#canScrobble(session, current, previous, timestamp)) { + session.pauseDuration = 0; + session.lastScrobbleTimestamp = timestamp - (timeDiff - (previous.duration - previous.progress)); + return true; + } else if (current.progress < previous.progress && session.playing.id != previous.id) { + session.pauseDuration = 0; + if (current.progress < timeDiff) + session.lastScrobbleTimestamp = timestamp - session.playing.progress; + else + session.lastScrobbleTimestamp = timestamp; + } + return false; + } + + #canScrobble(session, current, previous, timestamp) { + if (previous == null) + return false; + + const scrobbleDuration = this.#config.minimum.duration || 240; + const scrobblePercent = this.#config.minimum.percent || 50; + + const durationPlayed = timestamp - (session.lastScrobbleTimestamp || session.started) - session.pauseDuration; + const newPlayback = current == null || current.progress < previous.progress; + const canBeScrobbled = durationPlayed > scrobbleDuration * 1000 || durationPlayed / previous.duration > scrobblePercent / 100.0; + + return newPlayback && canBeScrobbled; + } + + #scrobble(media) { + this.#logger.info(media, "Scrobble"); + } +} + +module.exports = Recorder; \ No newline at end of file