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;