Skip to content

Commit

Permalink
Merge branch 'release/v0.6.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
jammus committed Apr 20, 2011
2 parents dcb9ea2 + 9c837fb commit a5d1d6c
Show file tree
Hide file tree
Showing 22 changed files with 745 additions and 334 deletions.
7 changes: 7 additions & 0 deletions History.md
@@ -1,5 +1,12 @@
# Changelog

## 0.6.0

### New features

* Added request() method. Provides low-level support for entire Last.Fm API.
* Event handler options on RecentTrackStream, LastFmUpdate and LastFmInfo have been deprecated and replaced with more generic `handlers` option. These will be removed soon.

## 0.5.1

* More accurate error reporting from RecentTrackParser
Expand Down
87 changes: 73 additions & 14 deletions README.md
Expand Up @@ -17,6 +17,44 @@ Read and write to users recent plays on Last.fm.

## Documentation

### LastFmRequest

lastfm.request(method, options);

Returns a `LastFmRequest` instance.

Send request to Last.fm. Requests automatically include the API key and are signed and/or sent via POST as described in the Last.fm API documentation.

Methods:

Accepts any Last.fm API method name, eg "artist.getInfo".

Options:

All options are passed through to Last.fm with the exception of the following.

- *write*

Force request to act as a write method. Write methods are signed and sent via POST. Useful for new methods not yet recognised by lastfm-node.

- *signed*

Force request to be signed. See Last.fm API docs for signature details. Useful for new methods not yet recognised by lastfm-node.

- *handlers*

Default event handlers to attach to the request object on creation.

Events:

- *success(data)*

Raw data returned by Last.fm.

- *error(error)*

Ruh-roh. Either a error returned by Last.fm or a transmission error.

### RecentTracksStream

lastfm.stream(username);
Expand Down Expand Up @@ -47,9 +85,13 @@ Options:

Start streaming automatically. Defaults to false.

- *handlers*

Default event handlers to attach to the request object on creation.

- *lastPlayed*, *nowPlaying*, *scrobbled*, *stoppedPlaying*, *error*

Event listeners.
**Deprecated:** Event listeners.

Events:

Expand Down Expand Up @@ -97,11 +139,8 @@ Methods:

Authorises user with Last.fm api. See last.fm documentation.
Options:
- *authorised* : `function(session)`
Listener for *authorised* event. See below.

- *error* : `function(error)`
Listener for `error` event. See below.
- *handlers*
Default event handlers to attach to the authorise call.

- *on(event, handler)*

Expand Down Expand Up @@ -146,13 +185,17 @@ Options:

Required for scrobble requests. Timestamp is in unix time (seconds since 01-01-1970 and is in UTC time).

- *handlers*

Default event handlers to attach to the request object on creation.

- *success*

Listener for `success` event.
**Deprecated:** Listener for `success` event.

- *error*

Listener for `error` event.
**Deprecated:** Listener for `error` event.

Events:

Expand Down Expand Up @@ -180,18 +223,22 @@ Public properties:

Options:

- *success*

Listener for `success` event.
- *handlers*

- *error*

Listener for `error` event.
Event handlers to attach to object at creation.

- *various*

Params as specified in Last.fm API, eg user: "username"

- *success*

**Deprecated:** Listener for `success` event.

- *error*

**Deprecated:** Listener for `error` event.

Special cases:

When requesting track info the `track` param can be either the track name or a track object as returned by `RecentTracksStream`.
Expand Down Expand Up @@ -238,6 +285,18 @@ When requesting track info the `track` param can be either the track name or a t
}
});

var request = lastfm.request("artist.getInfo", {
artist: "The Mae Shi",
handlers: {
success: function(data) {
console.log("Success: " + data);
},
error: function(error) {
console.log("Error: " + error.message);
}
}
});

## Influences

Heavily drawn from technoweenie's twitter-node
Expand Down
79 changes: 14 additions & 65 deletions lib/lastfm/index.js
@@ -1,75 +1,24 @@
var path = require("path");
var http = require('http');
var querystring = require('querystring');
var crypto = require("crypto");

require.paths.unshift(path.dirname(__dirname));

var RecentTracksStream = require('lastfm/recenttracks-stream');
var LastFmSession = require('lastfm/lastfm-session');
var LastFmUpdate = require('lastfm/lastfm-update');
var LastFmInfo = require('lastfm/lastfm-info');
var LastFmRequest = require('lastfm/lastfm-request');
var RecentTracksStream = require("lastfm/recenttracks-stream"),
LastFmSession = require("lastfm/lastfm-session"),
LastFmUpdate = require("lastfm/lastfm-update"),
LastFmInfo = require("lastfm/lastfm-info"),
LastFmRequest = require("lastfm/lastfm-request");

function LastFmNode(options) {
if (!options) options = {};
this.url = '/2.0';
this.host = 'ws.audioscrobbler.com';
this.secret = options.secret || '';
this.params = {
format : "json",
api_key : options.api_key
};
var that = this;
var LastFmNode = exports.LastFmNode = function(options) {
options = options || {};
this.url = "/2.0";
this.host = "ws.audioscrobbler.com";
this.format = "json";
this.secret = options.secret;
this.api_key = options.api_key;
};
exports.LastFmNode = LastFmNode;

LastFmNode.prototype.mergeParams = function(a, b) {
function copyKeys(source, destination) {
if (!source) {
return;
}
Object.keys(source).forEach(function(key) {
destination[key] = source[key];
});
}

var c = {};
copyKeys(a, c);
copyKeys(b, c);
return c;
};

LastFmNode.prototype.signature = function(params) {
var sig = "";
Object.keys(params).sort().forEach(function(key) {
if (key != 'format') sig += key + params[key];
});
sig += this.secret;
return crypto.createHash("md5").update(sig, "utf8").digest("hex");
};

LastFmNode.prototype.requestUrl = function(additionalParams, signed) {
var params = this.mergeParams(this.params, additionalParams);
if (signed) {
params.api_sig = this.signature(params);
}
return this.url + '?' + querystring.stringify(params);
};

LastFmNode.prototype.read = function(params, signed, callback) {
var connection = http.createClient(80, this.host);
return new LastFmRequest(connection, this.requestUrl(params, signed));
};

LastFmNode.prototype.write = function(params, signed, callback) {
var writeParams = this.mergeParams(this.params, params);
if (signed) {
writeParams.api_sig = this.signature(writeParams);
}
var body = querystring.stringify(writeParams);
var connection = http.createClient(80, this.host);
return new LastFmRequest(connection, this.url, body);
LastFmNode.prototype.request = function(method, params) {
return new LastFmRequest(this, method, params);
};

LastFmNode.prototype.stream = function(user, options) {
Expand Down
9 changes: 6 additions & 3 deletions lib/lastfm/lastfm-info.js
@@ -1,4 +1,5 @@
var EventEmitter = require('events').EventEmitter;
var EventEmitter = require("events").EventEmitter;
var utils = require("lastfm/utils");

var LastFmInfo = function(lastfm, type, options) {
options = options || {};
Expand All @@ -13,6 +14,8 @@ var LastFmInfo = function(lastfm, type, options) {
this.on("success", options.success);
}

utils.registerHandlers(this, options.handlers);

if (!type) {
this.emit("error", new Error("Item type not specified"));
return;
Expand All @@ -37,9 +40,9 @@ var LastFmInfo = function(lastfm, type, options) {
params[name] = options[name];
}
});
params.method = type + ".getinfo";
var method = type + ".getinfo";

var request = lastfm.read(params, false);
var request = lastfm.request(method, params);
request.on("success", function(data) {
try {
var response = JSON.parse(data);
Expand Down
102 changes: 87 additions & 15 deletions lib/lastfm/lastfm-request.js
@@ -1,28 +1,90 @@
var EventEmitter = require('events').EventEmitter;
if (global.GENTLY_HIJACK) require = GENTLY_HIJACK.hijack(require);

var LastFmRequest = module.exports = function(connection, url, body) {
var http = require("http");
var querystring = require('querystring');
var EventEmitter = require("events").EventEmitter;
var _ = require("underscore");
var crypto = require("crypto");
var utils = require("lastfm/utils");

var LastFmRequest = module.exports = function(lastfm, method, params) {
var that = this;
var method = body ? "POST" : "GET";
params = params || {};

EventEmitter.call(this);
utils.registerHandlers(this, params.handlers);

var requestParams = filterParams(params);
requestParams.method = method;
requestParams.api_key = requestParams.api_key || lastfm.api_key;
requestParams.format = requestParams.format || lastfm.format;

var isWriteRequest = params.write || isWriteMethod(method);
var requiresSignature = isWriteRequest || params.signed || isSignedMethod(method);
if (requiresSignature) {
requestParams = signParams(requestParams);
}
var data = querystring.stringify(requestParams);

var httpVerb = isWriteRequest ? "POST" : "GET";
sendRequest(httpVerb, lastfm.host, lastfm.url, data);

function sendRequest(httpVerb, host, url, data) {
var port = 80;
var connection = http.createClient(port, host);
connection.on("error", function(error) {
that.emit("error", error);
});
if (httpVerb == "GET") {
url += "?" + data;
}
var headers = generateRequestHeaders(httpVerb, host, data);
var request = connection.request(httpVerb, url, headers);
if (httpVerb == "POST") {
request.write(data);
}
request.on("response", handleResponse);
request.end();
}

function filterParams(params) {
var paramsBlackList = ["handlers", "signed", "write"];
var filteredParams = {};
_(Object.keys(params)).each(function(key) {
if (!_(paramsBlackList).include(key)) {
filteredParams[key] = params[key];
}
});
return filteredParams;
}

connection.on("error", function(error) {
that.emit("error", error);
});
function isSignedMethod(method) {
var signedMethods = ["auth.getmobilesession", "auth.getsession", "auth.gettoken",
"radio.getplaylist"];
return method && _(signedMethods).include(method.toLowerCase());
}

var request = connection.request(method, url, getHeaders());
if (body) {
request.write(body);
function isWriteMethod(method) {
var writeMethods = ["album.addtags", "album.removetag", "album.share",
"artist.addtags", "artist.removetag", "artist.share", "artist.shout",
"event.attend", "event.share", "event.shout",
"library.addalbum", "library.addartist", "library.addtrack",
"playlist.addtrack", "playlist.create",
"radio.tune",
"track.addtags", "track.ban", "track.love", "track.removetag",
"track.scrobble", "track.share", "track.unban", "track.unlove",
"track.updatenowplaying",
"user.shout"];
return method && _(writeMethods).include(method.toLowerCase());
}
request.on("response", handleResponse);
request.end();

function getHeaders() {
function generateRequestHeaders(httpVerb, host, data) {
var headers = {
host: connection.host
host: host
};

if (body) {
headers["Content-Length"] = body.length;
if (httpVerb == "POST") {
headers["Content-Length"] = data.length;
headers["Content-Type"] = "application/x-www-form-urlencoded";
}

Expand All @@ -38,6 +100,16 @@ var LastFmRequest = module.exports = function(connection, url, body) {
that.emit("success", data);
});
}

function signParams(params) {
var sig = "";
Object.keys(params).sort().forEach(function(key) {
if (key != "format") sig += key + params[key];
});
sig += lastfm.secret;
params.api_sig = crypto.createHash("md5").update(sig, "utf8").digest("hex");
return params;
};
};

LastFmRequest.prototype = Object.create(EventEmitter.prototype);

0 comments on commit a5d1d6c

Please sign in to comment.