const AggregateTracker = require("./trackers/aggregate-tracker"); const Session = require("../models/session"); class Recorder { #sessions = null; #trackers = null; #originalTrackers = []; #scrobblers = []; #config = null; #logger = null; constructor(sessions, trackers, scrobblers, config, logger) { this.#sessions = sessions; this.#trackers = new AggregateTracker('aggregate', trackers); this.#originalTrackers = trackers; this.#scrobblers = scrobblers; this.#config = config; this.#logger = logger; } async record() { const now = Date.now(); const media = await this.#trackers.poll(); const contexts = media.map(m => this.#fetchContext(m)); // Find sessions that ended and that are deemable of a scrobble. const sessionIds = this.#sessions.getSessionIds(); const stopped = sessionIds.filter(sessionId => !contexts.some(context => sessionId == context.session.id)) .map(sessionId => this.#sessions.get(sessionId)) .map(session => this.#fetchContext(session.playing)); const contextEnded = stopped.filter(context => this.#canScrobble(context.session, null, context.session.playing)); for (let context of contextEnded) context.session.playDuration = now - (context.session.lastScrobbleTimestamp || context.session.started) - context.session.pauseDuration; // Find ongoing sessions that have moved on to the next song. const finishedPlaying = contexts.filter(context => this.#listen(context, now)); // Scrobble const scrobbling = finishedPlaying.concat(contextEnded); for (let context of scrobbling) { await this.#scrobble(context); if (context.session.playing == null) continue; context.session.playDuration = context.extraDuration; context.session.pauseDuration = 0; } // Remove dead sessions. for (let context of stopped) { this.#sessions.remove(context.session.id); } } #fetchContext(media) { const tracker = this.#originalTrackers.find(t => t.provider == media.provider && t.name == media.source); let session = this.#sessions.get(media.session); if (session == null) { session = new Session(media.session); this.#sessions.add(session); } return { session, media, tracker, extraDuration: 0, scrobble: null } } #listen(context, timestamp) { const session = context.session; const current = context.media; const previous = context.session.playing; session.playing = current; if (!previous) { this.#logger.info(current, "A new session has started."); session.lastUpdateTimestamp = timestamp; return false; } const updated = current.progress != previous.progress || current.id != previous.id || current.state != previous.state; if (!updated) return false; const timeDiff = timestamp - session.lastUpdateTimestamp; const progressDiff = Math.max(0, Math.min(current.progress - previous.progress, timeDiff)); session.playDuration += progressDiff; session.pauseDuration += timeDiff - progressDiff; const canScrobble = this.#canScrobble(session, current, previous); if (canScrobble || current.id != previous.id) { context.extraDuration = Math.max(Math.min(current.progress, timeDiff - (previous.duration - previous.progress)), 0); context.extraDuration += Math.max(0, session.playDuration - previous.duration); session.lastScrobbleTimestamp = timestamp; if (canScrobble) context.scrobble = previous; } session.lastUpdateTimestamp = timestamp; return canScrobble; } #canScrobble(session, current, previous) { if (previous == null) return false; const scrobbleDuration = this.#config.minimum.duration || 240; const scrobblePercent = this.#config.minimum.percent || 50; const newPlayback = current == null || current.progress < previous.progress; const canBeScrobbled = session.playDuration > scrobbleDuration * 1000 || session.playDuration / previous.duration > scrobblePercent / 100.0; return newPlayback && canBeScrobbled || session.playDuration >= previous.duration; } async #scrobble(context) { this.#logger.info(context.scrobble, "Scrobble"); for (var scrobblerName of context.tracker.scrobblerNames) { const scrobbler = this.#scrobblers.find(s => s.name == scrobblerName); if (scrobbler == null) { this.#logger.error(`Cannot find scrobbler by name of '${scrobblerName}'.`); continue; } try { const duration = context.session.playDuration; const start = Date.now() - context.session.playDuration - context.session.pauseDuration; await scrobbler.queue(context.scrobble, duration, start); } catch (ex) { this.#logger.error(ex, "Could not send to maloja."); } } } } module.exports = Recorder;