* Use raw web sockets instead of socket.io. closes #114 * promote most plugin code to be actually integrated with groovebasin, except last.fm * ability to upgrade from mpd protocol to groovebasin protocol * serialize/deserialize lastQueueDate and update it for all types of queuing. Fixes DynamicMode losing randomness data on server restart. * repeat state is saved between restarts * fixed a race condition in player.importFile * a smaller subset of db file properties are sent to the server, reducing library payload size * diffs are sent to client for playlist and library. closes #142 * information is no longer requested in the groovebasin API; it is only subscribed to. closes #115 * all commands go through the permissions framework. see #37 * chat is deleted for now. closes #17 * update to latest superagent and leveldb * fix stream activating and deactivating when seeking and not streaming
237 lines
6.6 KiB
JavaScript
237 lines
6.6 KiB
JavaScript
var LastFmNode = require('lastfm').LastFmNode;
|
|
var PlayerServer = require('../player_server');
|
|
|
|
module.exports = LastFm;
|
|
|
|
var DB_KEY = 'Plugin.lastfm';
|
|
|
|
function LastFm(gb) {
|
|
this.gb = gb;
|
|
|
|
this.previousNowPlaying = null;
|
|
this.lastPlayingItem = null;
|
|
this.playingStart = new Date();
|
|
this.playingTime = 0;
|
|
this.previousIsPlaying = false;
|
|
this.scrobblers = {};
|
|
this.scrobbles = [];
|
|
|
|
this.lastFm = new LastFmNode({
|
|
api_key: this.gb.config.lastFmApiKey,
|
|
secret: this.gb.config.lastFmApiSecret,
|
|
});
|
|
|
|
this.gb.player.on('playlistUpdate', checkScrobble.bind(this));
|
|
this.gb.player.on('playlistUpdate', updateNowPlaying.bind(this));
|
|
|
|
this.initActions();
|
|
}
|
|
|
|
LastFm.prototype.initialize = function(cb) {
|
|
var self = this;
|
|
|
|
self.gb.db.get(DB_KEY, function(err, value) {
|
|
if (err) {
|
|
if (err.type !== 'NotFoundError') return cb(err);
|
|
} else {
|
|
var state = JSON.parse(value);
|
|
self.scrobblers = state.scrobblers;
|
|
self.scrobbles = state.scrobbles;
|
|
}
|
|
// in case scrobbling fails and then the user presses stop, this will still
|
|
// flush the queue.
|
|
setInterval(self.flushScrobbleQueue.bind(self), 120000);
|
|
cb();
|
|
});
|
|
};
|
|
|
|
LastFm.prototype.persist = function() {
|
|
var self = this;
|
|
var state = {
|
|
scrobblers: self.scrobblers,
|
|
scrobbles: self.scrobbles,
|
|
};
|
|
self.gb.db.put(DB_KEY, JSON.stringify(state), function(err) {
|
|
if (err) {
|
|
console.error("Unable to persist lastfm state to db:", err.stack);
|
|
}
|
|
});
|
|
}
|
|
|
|
LastFm.prototype.initActions = function() {
|
|
var self = this;
|
|
|
|
PlayerServer.plugins.push({
|
|
handleNewClient: function(client) {
|
|
client.sendMessage('LastFmApiKey', self.gb.config.lastFmApiKey);
|
|
},
|
|
});
|
|
|
|
PlayerServer.actions.LastFmGetSession = {
|
|
permission: 'read',
|
|
args: 'string',
|
|
fn: function(playerServer, client, token){
|
|
self.lastFm.request("auth.getSession", {
|
|
token: token,
|
|
handlers: {
|
|
success: function(data){
|
|
delete self.scrobblers[data.session.name];
|
|
client.sendMessage('LastFmGetSessionSuccess', data);
|
|
},
|
|
error: function(error){
|
|
console.error("error from last.fm auth.getSession:", error.message);
|
|
client.sendMessage('LastFmGetSessionError', error.message);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
PlayerServer.actions.LastFmScrobblersAdd = {
|
|
permission: 'read',
|
|
args: 'object',
|
|
fn: function(playerServer, client, params) {
|
|
var existingUser = self.scrobblers[params.username];
|
|
if (existingUser) {
|
|
console.warn("Trying to overwrite a scrobbler:", params.username);
|
|
return;
|
|
}
|
|
self.scrobblers[params.username] = params.session_key;
|
|
self.persist();
|
|
},
|
|
};
|
|
|
|
PlayerServer.actions.LastFmScrobblersRemove = {
|
|
permission: 'read',
|
|
args: 'object',
|
|
fn: function(playerServer, client, params) {
|
|
var sessionKey = self.scrobblers[params.username];
|
|
if (sessionKey !== params.session_key) {
|
|
console.warn("Invalid session key from user trying to remove scrobbler:", params.username);
|
|
return;
|
|
}
|
|
delete self.scrobblers[params.username];
|
|
self.persist();
|
|
},
|
|
};
|
|
}
|
|
|
|
LastFm.prototype.flushScrobbleQueue = function() {
|
|
var self = this;
|
|
var params;
|
|
var maxSimultaneous = 10;
|
|
var count = 0;
|
|
while ((params = self.scrobbles.shift()) != null && count++ < maxSimultaneous) {
|
|
console.info("scrobbling " + params.track + " for session " + params.sk);
|
|
params.handlers = {
|
|
error: onError,
|
|
};
|
|
self.lastFm.request('track.scrobble', params);
|
|
}
|
|
self.persist();
|
|
|
|
function onError(error){
|
|
console.error("error from last.fm track.scrobble:", error.stack);
|
|
if (!error.code || error.code === 11 || error.code === 16) {
|
|
// try again
|
|
self.scrobbles.push(params);
|
|
self.persist();
|
|
}
|
|
}
|
|
}
|
|
|
|
LastFm.prototype.queueScrobble = function(params){
|
|
console.info("queueScrobble", params);
|
|
this.scrobbles.push(params);
|
|
this.persist();
|
|
};
|
|
|
|
function checkScrobble() {
|
|
var self = this;
|
|
|
|
if (self.gb.player.isPlaying && !self.previousIsPlaying) {
|
|
self.playingStart = new Date(new Date() - self.playingTime);
|
|
self.previousIsPlaying = true;
|
|
}
|
|
self.playingTime = new Date() - self.playingStart;
|
|
|
|
var thisItem = self.gb.player.currentTrack;
|
|
if (thisItem === self.lastPlayingItem) return;
|
|
|
|
if (self.lastPlayingItem) {
|
|
|
|
var dbFile = self.gb.player.libraryIndex.trackTable[self.lastPlayingItem.key];
|
|
|
|
var minAmt = 15 * 1000;
|
|
var maxAmt = 4 * 60 * 1000;
|
|
var halfAmt = dbFile.duration / 2 * 1000;
|
|
|
|
if (self.playingTime >= minAmt && (self.playingTime >= maxAmt || self.playingTime >= halfAmt)) {
|
|
if (dbFile.artistName) {
|
|
for (var username in self.scrobblers) {
|
|
var sessionKey = self.scrobblers[username];
|
|
self.queueScrobble({
|
|
sk: sessionKey,
|
|
chosenByUser: +!self.lastPlayingItem.isRandom,
|
|
timestamp: Math.round(self.playingStart.getTime() / 1000),
|
|
album: dbFile.albumName,
|
|
track: dbFile.name,
|
|
artist: dbFile.artistName,
|
|
albumArtist: dbFile.albumArtistName,
|
|
duration: Math.round(dbFile.duration),
|
|
trackNumber: dbFile.track,
|
|
});
|
|
}
|
|
self.flushScrobbleQueue();
|
|
} else {
|
|
console.warn("Not scrobbling " + dbFile.name + " - missing artist.");
|
|
}
|
|
} else {
|
|
console.info("not scrobbling", dbFile.name, " - only listened for", self.playingTime);
|
|
}
|
|
}
|
|
self.lastPlayingItem = thisItem;
|
|
self.previousIsPlaying = self.gb.player.isPlaying;
|
|
self.playingStart = new Date();
|
|
self.playingTime = 0;
|
|
}
|
|
|
|
function updateNowPlaying() {
|
|
var self = this;
|
|
|
|
if (!self.gb.player.isPlaying) return;
|
|
|
|
var track = self.gb.player.currentTrack;
|
|
if (!track) return;
|
|
|
|
if (self.previousNowPlaying === track) return;
|
|
self.previousNowPlaying = track;
|
|
|
|
var dbFile = self.gb.player.libraryIndex.trackTable[track.key];
|
|
if (!dbFile.artistName) {
|
|
console.warn("Not updating last.fm now playing for " + dbFile.name + ": missing artist");
|
|
return;
|
|
}
|
|
|
|
for (var username in self.scrobblers) {
|
|
var sessionKey = self.scrobblers[username];
|
|
var props = {
|
|
sk: sessionKey,
|
|
track: dbFile.name,
|
|
artist: dbFile.artistName,
|
|
album: dbFile.albumName,
|
|
albumArtist: dbFile.albumArtistName,
|
|
trackNumber: dbFile.track,
|
|
duration: Math.round(dbFile.duration),
|
|
handlers: {
|
|
error: onError
|
|
}
|
|
}
|
|
console.info("updateNowPlaying", props);
|
|
self.lastFm.request("track.updateNowPlaying", props);
|
|
}
|
|
|
|
function onError(error){
|
|
console.error("unable to update last.fm now playing:", error.message);
|
|
}
|
|
}
|