137 lines
4.8 KiB
JavaScript
137 lines
4.8 KiB
JavaScript
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; |