Compare commits
No commits in common. "0bfbc369520ba3b97ab008c8cb07289ce76fb323" and "e5e0853d039a5800fc08a283f834eeae1f60acd6" have entirely different histories.
0bfbc36952
...
e5e0853d03
8
app.js
8
app.js
@ -4,17 +4,15 @@ const helmet = require("helmet");
|
|||||||
const logger = require("./services/logging");
|
const logger = require("./services/logging");
|
||||||
const rateLimit = require("express-rate-limit");
|
const rateLimit = require("express-rate-limit");
|
||||||
const sessions = require("./services/session-manager");
|
const sessions = require("./services/session-manager");
|
||||||
const PlexTracker = require("./services/trackers/plex-tracker");
|
const PlexTracker = require("./services/trackers/PlexTracker");
|
||||||
const SpotifyTracker = require("./services/trackers/spotify-tracker");
|
const SpotifyTracker = require("./services/trackers/SpotifyTracker");
|
||||||
const Recorder = require("./services/recorder");
|
const Recorder = require("./services/recorder");
|
||||||
const MalojaScrobbler = require("./services/scrobblers/maloja-scrobbler");
|
|
||||||
|
|
||||||
|
|
||||||
const maloja = new MalojaScrobbler(config.maloja);
|
|
||||||
const spotify = new SpotifyTracker(config.spotify);
|
const spotify = new SpotifyTracker(config.spotify);
|
||||||
(async () => await spotify.loadCredentials())();
|
(async () => await spotify.loadCredentials())();
|
||||||
const plex = new PlexTracker(config.plex);
|
const plex = new PlexTracker(config.plex);
|
||||||
const recorder = new Recorder(sessions, [plex, spotify], [maloja], config.scrobble, logger);
|
const recorder = new Recorder(sessions, [plex, spotify], config.scrobble, logger);
|
||||||
|
|
||||||
setInterval(() => recorder.record(), 5000);
|
setInterval(() => recorder.record(), 5000);
|
||||||
|
|
||||||
|
@ -2,28 +2,10 @@ const schema = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
required: [],
|
required: [],
|
||||||
properties: {
|
properties: {
|
||||||
maloja: {
|
|
||||||
type: 'object',
|
|
||||||
required: ['name', 'url', 'token'],
|
|
||||||
properties: {
|
|
||||||
name: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
token: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plex: {
|
plex: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['name', 'url', 'token', 'scrobblers'],
|
required: ['url', 'token'],
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
url: {
|
url: {
|
||||||
type: 'string'
|
type: 'string'
|
||||||
},
|
},
|
||||||
@ -73,13 +55,6 @@ const schema = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
scrobblers: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
minItems: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -103,11 +78,8 @@ const schema = {
|
|||||||
|
|
||||||
spotify: {
|
spotify: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['name', 'client_id', 'client_secret', 'redirect_uri', 'scrobblers'],
|
required: ['client_id', 'client_secret', 'redirect_uri'],
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
client_id: {
|
client_id: {
|
||||||
type: 'string'
|
type: 'string'
|
||||||
},
|
},
|
||||||
@ -116,13 +88,6 @@ const schema = {
|
|||||||
},
|
},
|
||||||
redirect_uri: {
|
redirect_uri: {
|
||||||
type: 'string'
|
type: 'string'
|
||||||
},
|
|
||||||
scrobblers: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
minItems: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5,11 +5,9 @@ const yaml = require("js-yaml");
|
|||||||
|
|
||||||
const configurationBase = {
|
const configurationBase = {
|
||||||
plex: {
|
plex: {
|
||||||
name: null,
|
|
||||||
url: null,
|
url: null,
|
||||||
token: null,
|
token: null,
|
||||||
filters: [], // { library, ip, deviceId, platform, product }
|
filters: [] // { library, ip, deviceId, platform, product }
|
||||||
scrobblers: []
|
|
||||||
},
|
},
|
||||||
scrobble: {
|
scrobble: {
|
||||||
minimum: {
|
minimum: {
|
||||||
@ -18,11 +16,9 @@ const configurationBase = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
spotify: {
|
spotify: {
|
||||||
name: null,
|
|
||||||
client_id: null,
|
client_id: null,
|
||||||
client_secret: null,
|
client_secret: null,
|
||||||
redirect_uri: null,
|
redirect_uri: null
|
||||||
scrobblers: []
|
|
||||||
},
|
},
|
||||||
web: {
|
web: {
|
||||||
host: null,
|
host: null,
|
||||||
@ -31,6 +27,7 @@ const configurationBase = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const configurationFile = yaml.load(fs.readFileSync('config/config.yml'), yaml.JSON_SCHEMA);
|
const configurationFile = yaml.load(fs.readFileSync('config/config.yml'), yaml.JSON_SCHEMA);
|
||||||
|
const configuration = { ...configurationBase, ...configurationFile }
|
||||||
|
|
||||||
const ajv = new Ajv({ allErrors: true });
|
const ajv = new Ajv({ allErrors: true });
|
||||||
const schema = require("./config.schema");
|
const schema = require("./config.schema");
|
||||||
@ -43,5 +40,4 @@ if (!valid) {
|
|||||||
(async () => { await new Promise(resolve => setTimeout(resolve, 1000)); exit(1); })();
|
(async () => { await new Promise(resolve => setTimeout(resolve, 1000)); exit(1); })();
|
||||||
}
|
}
|
||||||
|
|
||||||
const configuration = { ...configurationBase, ...configurationFile }
|
|
||||||
module.exports = configuration;
|
module.exports = configuration;
|
@ -2,10 +2,8 @@ class Session {
|
|||||||
#id = null;
|
#id = null;
|
||||||
#started = null;
|
#started = null;
|
||||||
#current = null;
|
#current = null;
|
||||||
lastScrobbleTimestamp = 0;
|
#lastScrobble = null;
|
||||||
lastUpdateTimestamp = 0;
|
#pauseDuration = 0;
|
||||||
pauseDuration = 0;
|
|
||||||
playDuration = 0;
|
|
||||||
|
|
||||||
constructor(id) {
|
constructor(id) {
|
||||||
this.#id = id;
|
this.#id = id;
|
||||||
@ -27,6 +25,22 @@ class Session {
|
|||||||
get started() {
|
get started() {
|
||||||
return this.#started;
|
return this.#started;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get lastScrobbleTimestamp() {
|
||||||
|
return this.#lastScrobble;
|
||||||
|
}
|
||||||
|
|
||||||
|
set lastScrobbleTimestamp(value) {
|
||||||
|
this.#lastScrobble = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get pauseDuration() {
|
||||||
|
return this.#pauseDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
set pauseDuration(value) {
|
||||||
|
this.#pauseDuration = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Session;
|
module.exports = Session;
|
@ -9,9 +9,8 @@ class Song {
|
|||||||
session = null;
|
session = null;
|
||||||
state = null;
|
state = null;
|
||||||
source = null;
|
source = null;
|
||||||
provider = null;
|
|
||||||
|
|
||||||
constructor(id, name, album, artists, year, duration, progress, session, state, source, provider) {
|
constructor(id, name, album, artists, year, duration, progress, session, state, source) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.album = album;
|
this.album = album;
|
||||||
@ -22,7 +21,6 @@ class Song {
|
|||||||
this.session = session;
|
this.session = session;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.provider = provider;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,130 +1,98 @@
|
|||||||
const AggregateTracker = require("./trackers/aggregate-tracker");
|
const AggregateTracker = require("./trackers/AggregateTracker");
|
||||||
const Session = require("../models/session");
|
const Session = require("../models/session");
|
||||||
|
|
||||||
class Recorder {
|
class Recorder {
|
||||||
#sessions = null;
|
#sessions = null;
|
||||||
#trackers = null;
|
#trackers = null;
|
||||||
#originalTrackers = [];
|
|
||||||
#scrobblers = [];
|
|
||||||
#config = null;
|
#config = null;
|
||||||
#logger = null;
|
#logger = null;
|
||||||
|
#lastTick = null;
|
||||||
|
|
||||||
constructor(sessions, trackers, scrobblers, config, logger) {
|
constructor(sessions, trackers, config, logger) {
|
||||||
this.#sessions = sessions;
|
this.#sessions = sessions;
|
||||||
this.#trackers = new AggregateTracker('aggregate', trackers);
|
this.#trackers = new AggregateTracker(trackers);
|
||||||
this.#originalTrackers = trackers;
|
|
||||||
this.#scrobblers = scrobblers;
|
|
||||||
this.#config = config;
|
this.#config = config;
|
||||||
this.#logger = logger;
|
this.#logger = logger;
|
||||||
|
this.#lastTick = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
async record() {
|
async record() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
const timeDiff = now - this.#lastTick;
|
||||||
const media = await this.#trackers.poll();
|
const media = await this.#trackers.poll();
|
||||||
const contexts = media.map(m => this.#fetchContext(m));
|
const data = media.map(m => this.#fetchSession(m));
|
||||||
|
|
||||||
// Find sessions that ended and that are deemable of a scrobble.
|
// Find sessions that ended and that are deemable of a scrobble.
|
||||||
const sessionIds = this.#sessions.getSessionIds();
|
const sessionIds = this.#sessions.getSessionIds();
|
||||||
const stopped = sessionIds.filter(sessionId => !contexts.some(context => sessionId == context.session.id))
|
const stopped = sessionIds.filter(sessionId => !data.some(d => sessionId == d.session.id)).map(s => this.#sessions.get(s));
|
||||||
.map(sessionId => this.#sessions.get(sessionId))
|
const sessionEnded = stopped.filter(s => this.#canScrobble(s, null, s.playing, now));
|
||||||
.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.
|
// Find ongoing sessions that have moved on to the next song.
|
||||||
const finishedPlaying = contexts.filter(context => this.#listen(context, now));
|
const finishedPlaying = data.filter(d => this.#listen(d.session, d.media, d.session.playing, now, timeDiff));
|
||||||
|
|
||||||
// Scrobble
|
const scrobbling = finishedPlaying.concat(sessionEnded);
|
||||||
const scrobbling = finishedPlaying.concat(contextEnded);
|
for (let track of scrobbling)
|
||||||
for (let context of scrobbling) {
|
this.#scrobble(track);
|
||||||
await this.#scrobble(context);
|
|
||||||
|
|
||||||
if (context.session.playing == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
context.session.playDuration = context.extraDuration;
|
|
||||||
context.session.pauseDuration = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove dead sessions.
|
// Remove dead sessions.
|
||||||
for (let context of stopped) {
|
for (let sessionId of stopped)
|
||||||
this.#sessions.remove(context.session.id);
|
this.#sessions.remove(sessionId);
|
||||||
}
|
|
||||||
|
this.#lastTick = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fetchContext(media) {
|
#fetchSession(media) {
|
||||||
const tracker = this.#originalTrackers.find(t => t.provider == media.provider && t.name == media.source);
|
|
||||||
let session = this.#sessions.get(media.session);
|
let session = this.#sessions.get(media.session);
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
session = new Session(media.session);
|
session = new Session(media.session);
|
||||||
this.#sessions.add(session);
|
this.#sessions.add(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { session, media, tracker, extraDuration: 0 }
|
return { session: session, media: media }
|
||||||
}
|
}
|
||||||
|
|
||||||
#listen(context, timestamp) {
|
#listen(session, current, previous, timestamp, timeDiff) {
|
||||||
const session = context.session;
|
|
||||||
const current = context.media;
|
|
||||||
const previous = context.session.playing;
|
|
||||||
session.playing = current;
|
session.playing = current;
|
||||||
|
|
||||||
if (!previous) {
|
if (previous == null) {
|
||||||
this.#logger.info(current, "A new session has started.");
|
this.#logger.info(current, "A new session has started.");
|
||||||
session.lastUpdateTimestamp = timestamp;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = current.progress != previous.progress || current.id != previous.id || current.state != previous.state;
|
if (session.playing.state == "paused" || previous.state == "paused") {
|
||||||
if (!updated)
|
session.pauseDuration += timeDiff - (session.playing.progress - previous.progress);
|
||||||
return false;
|
}
|
||||||
|
|
||||||
const timeDiff = timestamp - session.lastUpdateTimestamp;
|
if (this.#canScrobble(session, current, previous, timestamp)) {
|
||||||
const progressDiff = Math.max(0, Math.min(current.progress - previous.progress, timeDiff));
|
session.pauseDuration = 0;
|
||||||
session.playDuration += progressDiff;
|
session.lastScrobbleTimestamp = timestamp - (timeDiff - (previous.duration - previous.progress));
|
||||||
session.pauseDuration += timeDiff - progressDiff;
|
return true;
|
||||||
|
} else if (current.progress < previous.progress && session.playing.id != previous.id) {
|
||||||
const canScrobble = this.#canScrobble(session, current, previous);
|
session.pauseDuration = 0;
|
||||||
if (canScrobble || current.id != previous.id) {
|
if (current.progress < timeDiff)
|
||||||
context.extraDuration = Math.min(current.progress, timeDiff - (previous.duration - previous.progress));
|
session.lastScrobbleTimestamp = timestamp - session.playing.progress;
|
||||||
|
else
|
||||||
session.lastScrobbleTimestamp = timestamp;
|
session.lastScrobbleTimestamp = timestamp;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
session.lastUpdateTimestamp = timestamp;
|
|
||||||
return canScrobble;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#canScrobble(session, current, previous) {
|
#canScrobble(session, current, previous, timestamp) {
|
||||||
if (previous == null)
|
if (previous == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const scrobbleDuration = this.#config.minimum.duration || 240;
|
const scrobbleDuration = this.#config.minimum.duration || 240;
|
||||||
const scrobblePercent = this.#config.minimum.percent || 50;
|
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 newPlayback = current == null || current.progress < previous.progress;
|
||||||
const canBeScrobbled = session.playDuration > scrobbleDuration * 1000 || session.playDuration / previous.duration > scrobblePercent / 100.0;
|
const canBeScrobbled = durationPlayed > scrobbleDuration * 1000 || durationPlayed / previous.duration > scrobblePercent / 100.0;
|
||||||
|
|
||||||
return newPlayback && canBeScrobbled;
|
return newPlayback && canBeScrobbled;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #scrobble(context) {
|
#scrobble(media) {
|
||||||
this.#logger.info(context, "Scrobble");
|
this.#logger.info(media, "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 {
|
|
||||||
await scrobbler.scrobble(context.media, Date.now() - Math.min(context.media.duration, context.session.playDuration));
|
|
||||||
} catch (ex) {
|
|
||||||
this.#logger.error(ex, "Could not send to maloja.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
const axios = require("axios");
|
|
||||||
|
|
||||||
class MalojaScrobbler {
|
|
||||||
#config = null;
|
|
||||||
#counter = 0;
|
|
||||||
|
|
||||||
constructor(config) {
|
|
||||||
this.#config = config;
|
|
||||||
|
|
||||||
if (!config.name)
|
|
||||||
throw new Error("Invalid name for Maloja scrobber.");
|
|
||||||
if (!config.url)
|
|
||||||
throw new Error(`Invalid url for Maloja scrobbler '${this.name}'.`);
|
|
||||||
if (!config.token)
|
|
||||||
throw new Error(`Invalid token for Maloja scrobbler '${this.name}'.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
get counter() {
|
|
||||||
return this.#counter;
|
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.#config.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
async scrobble(song, progress, start) {
|
|
||||||
const url = new URL(this.#config.url);
|
|
||||||
url.pathname += "/apis/mlj_1/newscrobble";
|
|
||||||
url.search = "?key=" + this.#config.token;
|
|
||||||
await axios.post(url.toString(), {
|
|
||||||
title: song.name,
|
|
||||||
album: song.album,
|
|
||||||
artists: song.artists,
|
|
||||||
duration: Math.round(progress / 1000),
|
|
||||||
length: Math.round(song.duration / 1000),
|
|
||||||
//time: start
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#counter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = MalojaScrobbler;
|
|
17
services/trackers/AggregateTracker.js
Normal file
17
services/trackers/AggregateTracker.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
class AggregateTracker {
|
||||||
|
#trackers = []
|
||||||
|
|
||||||
|
constructor(trackers) {
|
||||||
|
this.#trackers = trackers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async poll() {
|
||||||
|
let media = []
|
||||||
|
for (let tracker of this.#trackers)
|
||||||
|
media = media.concat(await tracker.poll());
|
||||||
|
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AggregateTracker;
|
@ -4,20 +4,11 @@ const Song = require("../../models/song");
|
|||||||
class PlexTracker {
|
class PlexTracker {
|
||||||
#config = null;
|
#config = null;
|
||||||
#cache = [];
|
#cache = [];
|
||||||
provider = "plex";
|
|
||||||
|
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.#config = config;
|
this.#config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.#config.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get scrobblerNames() {
|
|
||||||
return this.#config.scrobblers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async poll(useCache = false) {
|
async poll(useCache = false) {
|
||||||
if (!this.#config.token || !this.#config.url)
|
if (!this.#config.token || !this.#config.url)
|
||||||
return [];
|
return [];
|
||||||
@ -37,7 +28,7 @@ class PlexTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filtered = response.data.MediaContainer?.Metadata.filter(m => this.#filter(m));
|
const filtered = response.data.MediaContainer?.Metadata.filter(m => this.#filter(m));
|
||||||
this.#cache = filtered.map(m => this.#transform(m, this.#config.name));
|
this.#cache = filtered.map(m => this.#transform(m));
|
||||||
return this.#cache;
|
return this.#cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,10 +52,9 @@ class PlexTracker {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#transform(data, source) {
|
#transform(data) {
|
||||||
const id = data.guid.substring(data.guid.lastIndexOf('/') + 1);
|
const id = data.guid.substring(data.guid.lastIndexOf('/') + 1);
|
||||||
const artists = data.grandparentTitle.split(',').map(a => a.trim());
|
return new Song(id, data.title, data.parentTitle, data.grandparentTitle, data.parentYear, data.duration, data.viewOffset, data.sessionKey, data.Player.state, "plex");
|
||||||
return new Song(id, data.title, data.parentTitle, artists, data.parentYear, data.duration, data.viewOffset, data.sessionKey, data.Player.state, source, "plex");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,24 +7,16 @@ const Song = require("../../models/song");
|
|||||||
class SpotifyTracker {
|
class SpotifyTracker {
|
||||||
#config = null;
|
#config = null;
|
||||||
#token = null;
|
#token = null;
|
||||||
#cache = [];
|
#cache = null;
|
||||||
#auth = null;
|
#auth = null;
|
||||||
provider = "spotify";
|
|
||||||
|
|
||||||
constructor(config, token = null) {
|
constructor(config, token = null) {
|
||||||
this.#config = config;
|
this.#config = config;
|
||||||
this.#token = token;
|
this.#token = token;
|
||||||
|
this.#cache = null;
|
||||||
this.#auth = new Buffer.from(config.client_id + ':' + config.client_secret).toString('base64');
|
this.#auth = new Buffer.from(config.client_id + ':' + config.client_secret).toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.#config.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get scrobblerNames() {
|
|
||||||
return this.#config.scrobblers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async poll(useCache = false) {
|
async poll(useCache = false) {
|
||||||
if (this.#token == null)
|
if (this.#token == null)
|
||||||
return [];
|
return [];
|
||||||
@ -48,7 +40,7 @@ class SpotifyTracker {
|
|||||||
return this.#cache;
|
return this.#cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#cache = [this.#transform(response.data, this.#config.name)];
|
this.#cache = [this.#transform(response.data)];
|
||||||
return this.#cache;
|
return this.#cache;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
logger.error(ex, "Failed to get currently playing data from Spotify.");
|
logger.error(ex, "Failed to get currently playing data from Spotify.");
|
||||||
@ -66,12 +58,12 @@ class SpotifyTracker {
|
|||||||
this.#token = JSON.parse(content);
|
this.#token = JSON.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
#transform(data, source) {
|
#transform(data) {
|
||||||
const item = data.item;
|
const item = data.item;
|
||||||
const artists = item.artists.map(a => a.name);
|
const artists = item.artists.map(a => a.name);
|
||||||
const year = null;
|
const year = null;
|
||||||
const state = data.is_playing ? "playing" : "paused";
|
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, source, "spotify");
|
return new Song(item.id, item.name, item.album.name, artists, year, item.duration_ms, data.progress_ms, "spotify", state, "spotify");
|
||||||
}
|
}
|
||||||
|
|
||||||
async #refreshTokenIfNeeded() {
|
async #refreshTokenIfNeeded() {
|
@ -1,30 +0,0 @@
|
|||||||
class AggregateTracker {
|
|
||||||
#name = null;
|
|
||||||
#trackers = []
|
|
||||||
provider = null;
|
|
||||||
|
|
||||||
constructor(name, trackers) {
|
|
||||||
this.#name = name;
|
|
||||||
this.#trackers = trackers;
|
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.#name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get scrobblerNames() {
|
|
||||||
return this.#trackers.map(t => t.scrobblerNames)
|
|
||||||
.flat()
|
|
||||||
.filter((v, i, a) => a.indexOf(v) == i);
|
|
||||||
}
|
|
||||||
|
|
||||||
async poll() {
|
|
||||||
let media = []
|
|
||||||
for (let tracker of this.#trackers)
|
|
||||||
media = media.concat(await tracker.poll());
|
|
||||||
|
|
||||||
return media;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = AggregateTracker;
|
|
Loading…
Reference in New Issue
Block a user