├── README.md ├── lib ├── GraceError.js └── Gracenote.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | node-gracenote 2 | ============== 3 | 4 | A node.js wrapper for the Gracenote API - https://developer.gracenote.com/web-api 5 | 6 | ## Installation 7 | 8 | ``` 9 | npm install node-gracenote 10 | ``` 11 | 12 | 13 | ## Gracenote Options 14 | 15 | Request defaults can be sent as an extra parameter to the constructor. 16 | More information can be found at [the Request Github page](https://github.com/mikeal/request#requestdefaultsoptions). 17 | 18 | ``` 19 | var api = new Gracenote(clientId,clientTag,userId,requestDefaults); 20 | ``` 21 | 22 | ## Register 23 | 24 | Function - `api.register(req callback)` 25 | 26 | ``` 27 | var Gracenote = require("node-gracenote"); 28 | var clientId = "XXX"; 29 | var clientTag = "YYY"; 30 | var userId = null; 31 | var api = new Gracenote(clientId,clientTag,userId); 32 | api.register(function(err, uid) { 33 | // store this somewhere for the next session 34 | })`; 35 | ``` 36 | 37 | ## Search For Track 38 | 39 | Function - `api.searchTrack(req artistName, req albumTitle, req trackTitle, req callback, opt matchMode)` 40 | 41 | ``` 42 | var Gracenote = require("node-gracenote"); 43 | var clientId = "XXX"; 44 | var clientTag = "YYY"; 45 | var userId = "ZZZ"; 46 | var api = new Gracenote(clientId,clientTag,userId); 47 | api.searchTrack("Kings of Leon", "Only by the Night", "Sex on fire", function(err, result) { 48 | // Search Result as array 49 | }); 50 | ``` 51 | 52 | ## Search for Artist 53 | 54 | Function - `api.searchArtist(req artistName, req callback, opt matchMode)` 55 | 56 | ``` 57 | var Gracenote = require("node-gracenote"); 58 | var clientId = "XXX"; 59 | var clientTag = "YYY"; 60 | var userId = "ZZZ"; 61 | var api = new Gracenote(clientId,clientTag,userId); 62 | api.searchArtist("Kings of Leon", function(err, result) { 63 | // Search Result as array 64 | }); 65 | ``` 66 | 67 | ## Search for Album 68 | 69 | Function - `api.searchAlbum(req artistName, req albumTitle, req callback, opt matchMode)` 70 | 71 | ``` 72 | var Gracenote = require("node-gracenote"); 73 | var clientId = "XXX"; 74 | var clientTag = "YYY"; 75 | var userId = "ZZZ"; 76 | var api = new Gracenote(clientId,clientTag,userId); 77 | api.searchAlbum("Kings of Leon", "Only by the Night", function(err, result) { 78 | // Search Result as array 79 | }); 80 | ``` 81 | 82 | ## Config options 83 | 84 | `matchMode`- can be either `Gracenote.BEST_MATCH_ONLY`or `Gracenote.ALL_RESULTS`(default) 85 | 86 | `setExtended(str)` - let you change the extended data from gracenote - default : COVER,REVIEW,ARTIST_BIOGRAPHY,ARTIST_IMAGE,ARTIST_OET,MOOD,TEMPO 87 | `setCoverSize(str)` - let you change the cover size - default: MEDIUM 88 | `setLanguage(iso)` - pass a language parameter - default: null 89 | -------------------------------------------------------------------------------- /lib/GraceError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //var util = require('util'); 4 | 5 | module.exports = GraceError; 6 | 7 | function GraceError(code, msg) { 8 | if (typeof msg !== 'undefined' && msg != null && msg !== '') 9 | msg = ' | ' + msg; 10 | else 11 | msg = ''; 12 | var err = new Error(GraceError.MESSAGES[code]+msg); 13 | //GraceError.super_.apply(this, arguments); 14 | err.statusCode = code; 15 | 16 | return err; 17 | }; 18 | 19 | GraceError.UNABLE_TO_PARSE_RESPONSE = 1; // The response couldn't be parsed. Maybe an error, or maybe the API changed. 20 | GraceError.API_RESPONSE_ERROR = 1000; // There was a GN error code returned in the response. 21 | GraceError.API_NO_MATCH = 1001; // The API returned a NO_MATCH (i.e. there were no results). 22 | GraceError.API_NON_OK_RESPONSE = 1002; // There was some unanticipated non-"OK" response from the API. 23 | GraceError.HTTP_REQUEST_ERROR = 2000; // An uncaught exception was raised while doing a cURL request. 24 | GraceError.HTTP_REQUEST_TIMEOUT = 2001; // The external request timed out. 25 | GraceError.HTTP_RESPONSE_ERROR_CODE = 2002; // There was a HTTP400 error code returned. 26 | GraceError.HTTP_RESPONSE_ERROR = 2003; // A cURL error that wasn't a timeout or HTTP400 response. 27 | GraceError.INVALID_INPUT_SPECIFIED = 3000; // Some input the user gave wasn't valid. 28 | GraceError.RANGE_NOT_VALID = 4000; 29 | GraceError.MESSAGES = { 30 | // Generic Errors 31 | 1 : "Unable to parse response from Gracenote WebAPI.", 32 | // Specific API Errors 33 | 1000 : "The API returned an error code.", 34 | 1001 : "The API returned no results.", 35 | 1002 : "The API returned an unacceptable response.", 36 | // HTTP Errors 37 | 2000 : "There was an error while performing an external request.", 38 | 2001 : "Request to a Gracenote WebAPI timed out.", 39 | 2002 : "WebAPI response had a HTTP error code.", 40 | 2003 : "cURL returned an error when trying to make the request.", 41 | // Input Errors 42 | 3000 : "Invalid input.", 43 | 4000 : "Range was not valid (needs start & end)" 44 | }; 45 | 46 | //util.inherits(GraceError, Error); -------------------------------------------------------------------------------- /lib/Gracenote.js: -------------------------------------------------------------------------------- 1 | var GraceError = require('./GraceError'), 2 | request = require('request'), 3 | qs = require('querystring'), 4 | parseString = require('xml2js').parseString, 5 | cache = require('memory-cache'), 6 | extend = require('util')._extend, 7 | API_URL = 'https://c[[CLID]].web.cddbp.net/webapi/xml/1.0/'; 8 | 9 | module.exports = Gracenote; 10 | 11 | function Gracenote(clientId, clientTag, userId, requestDefaults) { 12 | if (requestDefaults) 13 | request = request.defaults(requestDefaults); 14 | var me = this; 15 | if (clientId === null || typeof clientId === 'undefined' || clientId === '') 16 | throw new GraceError(GraceError.INVALID_INPUT_SPECIFIED, 'clientId'); 17 | if (clientTag === null || typeof clientTag === 'undefined' || clientTag === '') 18 | throw new Error(GraceError.INVALID_INPUT_SPECIFIED, 'clientTag'); 19 | if (typeof userId === 'undefined' || userId === '') { 20 | if (cache.get('Gracenote.userId')) { 21 | userId = cache.get('Gracenote.userId'); 22 | } 23 | else 24 | userId = null; 25 | } 26 | 27 | this.clientId = clientId; 28 | this.clientTag = clientTag; 29 | this.userId = userId; 30 | this.apiURL = API_URL.replace('[[CLID]]', this.clientId); 31 | 32 | this._coverSize = 'MEDIUM'; 33 | this._extendedData = 'COVER,REVIEW,ARTIST_BIOGRAPHY,ARTIST_IMAGE,ARTIST_OET,MOOD,TEMPO'; //COVER,REVIEW,ARTIST_BIOGRAPHY,ARTIST_IMAGE,ARTIST_OET,MOOD,TEMPO 34 | this._lang = null; 35 | 36 | this._execute = function(data, cb) { 37 | this._performRequest(data, function(error, response, body) { 38 | if (error) return cb(error); 39 | var xml = me._parseResponse(body, function(err, xml) { 40 | if (err) return cb(err); 41 | cb(null, xml); 42 | }); 43 | }); 44 | }; 45 | 46 | this._constructQueryRequest = function(body, command) { 47 | if (command === null || typeof command === 'undefined' || command === '') 48 | command = 'ALBUM_SEARCH'; 49 | 50 | return ''+ 51 | ''+ 52 | '' + this.clientId + '-' + this.clientTag + ''+ 53 | '' + this.userId + ''+ 54 | ''+ 55 | (this._lang ? ''+this._lang+'' : '') + 56 | ''+ 57 | body + 58 | ''+ 59 | ''; 60 | }; 61 | 62 | this._constructQueryBody = function(artist, album, track, gn_id, command, options) { 63 | if (command === null || typeof command === 'undefined' || command === '') 64 | command = 'ALBUM_SEARCH'; 65 | 66 | if (typeof options === 'undefined' || options === null) 67 | options = {matchMode: Gracenote.ALL_RESULTS}; 68 | else if (!options.hasOwnProperty('matchMode')) 69 | options = extend(options, {matchMode: Gracenote.ALL_RESULTS}); 70 | 71 | var body = ""; 72 | 73 | if (command === 'ALBUM_FETCH') { 74 | body += '' + gn_id + ''; 75 | } 76 | else { 77 | if (options.matchMode === Gracenote.BEST_MATCH_ONLY) { body += 'SINGLE_BEST_COVER'; } 78 | if (options.hasOwnProperty('range')) { 79 | var range = options.range; 80 | if (!range.hasOwnProperty('start') || !range.hasOwnProperty('end')) 81 | throw new Error(GraceError.RANGE_NOT_VALID, 'clientTag'); 82 | body += ''+options.range.start+''+options.range.end+''; 83 | } 84 | 85 | if (artist != "") { body += '' + artist + ''; } 86 | if (track != "") { body += '' + track + ''; } 87 | if (album != "") { body += '' + album + ''; } 88 | } 89 | 90 | body += ''; 94 | 95 | body += ''; 99 | 100 | // (LARGE,XLARGE,SMALL,MEDIUM,THUMBNAIL) 101 | body += ''; 105 | 106 | return body; 107 | } 108 | 109 | this._checkResponse = function(response, cb) { 110 | response = this._formatXML(response); 111 | parseString(response, function (err, xml) { 112 | 113 | if (err !== null) 114 | throw new GraceError(GraceError.UNABLE_TO_PARSE_RESPONSE); 115 | 116 | var status = xml.RESPONSES.RESPONSE[0].$.STATUS; 117 | switch (status) { 118 | case "ERROR": throw new GraceError(GraceError.API_RESPONSE_ERROR, xml.RESPONSES.MESSAGE[0]); break; 119 | case "NO_MATCH": throw new GraceError(GraceError.API_NO_MATCH); break; 120 | default: 121 | if (status !== "OK") { throw new GraceError(GraceError.API_NON_OK_RESPONSE, status); } 122 | } 123 | 124 | cb(xml.RESPONSES); 125 | }); 126 | } 127 | 128 | this._parseResponse = function(response, cb) { 129 | response = this._formatXML(response); 130 | parseString(response, function (err, xml) { 131 | if (err !== null) 132 | throw new GraceError(GraceError.UNABLE_TO_PARSE_RESPONSE); 133 | 134 | try { 135 | me._checkResponse(response, function() { 136 | var output = [], 137 | entries = xml.RESPONSES.RESPONSE[0].ALBUM; 138 | for (var i = 0; i < entries.length; i++) { 139 | var entry = entries[i]; 140 | 141 | var obj = { 142 | "album_gnid": entry.GN_ID[0], 143 | 'album_artist_name': entry.ARTIST[0], 144 | 'album_title': entry.TITLE[0], 145 | 'album_year': '', 146 | 'genre': me._getOETElem(entry.GENRE), 147 | 'album_art_url': '', 148 | 'artist_image_url': '', 149 | 'artist_bio_url': '', 150 | 'review_url': '' 151 | }; 152 | 153 | if (entry.DATE) { 154 | obj.album_year = entry.DATE[0]; 155 | } 156 | 157 | if (entry.URL) { 158 | obj.album_art_url = me._getAttribElem(entry.URL, "TYPE", "COVERART"), 159 | obj.artist_image_url = me._getAttribElem(entry.URL, "TYPE", "ARTIST_IMAGE"), 160 | obj.artist_bio_url = me._getAttribElem(entry.URL, "TYPE", "ARTIST_BIOGRAPHY"), 161 | obj.review_url = me._getAttribElem(entry.URL, "TYPE", "REVIEW") 162 | } 163 | 164 | if (entry.ARTIST_ORIGIN) { 165 | obj.artist_era = me._getOETElem(entry.ARTIST_ERA); 166 | obj.artist_type = me._getOETElem(entry.ARTIST_TYPE); 167 | obj.artist_origin = me._getOETElem(entry.ARTIST_ORIGIN); 168 | } 169 | else { 170 | // NOT A GOOD APROACH TO ASK AN ASYNC CALL 171 | //me.fetchOETData(entry.GN_ID[0], function(data) { 172 | // console.log("CALLBACK",data); 173 | //}) 174 | } 175 | 176 | var tracks = []; 177 | if (entry.TRACK) { 178 | for (var x = 0; x < entry.TRACK.length; x++) { 179 | var t = entry.TRACK[x]; 180 | var track = { 181 | 'track_number': t.TRACK_NUM[0], 182 | 'track_gnid': t.GN_ID[0], 183 | 'track_title': t.TITLE[0], 184 | 'mood': '', 185 | 'tempo': '' 186 | }; 187 | 188 | if (t.MOOD) 189 | track.mood = me._getOETElem(t.MOOD); 190 | if (t.TEMPO) 191 | track.tempo = me._getOETElem(t.TEMPO) 192 | 193 | 194 | if (!t.ARTIST) { 195 | track.track_artist_name = obj.album_artist_name; 196 | } 197 | else { 198 | track.track_artist_name = t.ARTIST[0]; 199 | } 200 | 201 | if (t.GENRE) 202 | obj.genre = me._getOETElem(t.GENRE); 203 | if (t.ARTIST_ERA) 204 | obj.artist_era = me._getOETElem(t.ARTIST_ERA); 205 | if (t.ARTIST_TYPE) 206 | obj.artist_type = me._getOETElem(t.ARTIST_TYPE); 207 | if (t.ARTIST_ORIGIN) 208 | obj.artist_origin = me._getOETElem(t.ARTIST_ORIGIN); 209 | 210 | tracks.push(track); 211 | } 212 | } 213 | obj.tracks = tracks; 214 | output.push(obj); 215 | } 216 | 217 | cb(null, output); 218 | }); 219 | } 220 | catch (err) { 221 | if (err.statusCode == GraceError.API_NO_MATCH) 222 | return cb(null, []); 223 | cb(err.message); 224 | } 225 | }); 226 | } 227 | 228 | this._getAttribElem = function(root, attribute, value) { 229 | for (var i = 0; i < root.length; i++) { 230 | var r = root[i]; 231 | 232 | if (r.$[attribute] == value) { return r._; } 233 | } 234 | return ""; 235 | } 236 | 237 | this._getOETElem = function(root) { 238 | if (root) { 239 | var out = []; 240 | for (var i = 0; i < root.length; i++) { 241 | var r = root[i]; 242 | out.push({ 243 | 'id': r.$.ID, 244 | 'text': r._, 245 | }); 246 | } 247 | return out; 248 | } 249 | return ''; 250 | } 251 | 252 | this._performRequest = function(body, cb) { 253 | request({ 254 | url: this.apiURL, 255 | body: body, 256 | method: 'POST', 257 | headers: { 258 | 'User-Agent': 'nodejs-gracenote' 259 | } 260 | }, cb) 261 | }, 262 | 263 | this._formatXML = function(response) { 264 | response = response.replace(/\r\n+|\r\n|\n+|\n|\s+|\s$/, ''); 265 | return response; 266 | } 267 | }; 268 | 269 | 270 | Gracenote.prototype.register = function(cb) { 271 | var me = this; 272 | 273 | if (this.userId !== null) { 274 | console.warn('Warning: You already have a userId, no need to register another. Using current ID.'); 275 | return this.userId; 276 | } 277 | 278 | var data = ''+ 279 | ''+ 280 | '' + this.clientId + '-' + this.clientTag + ''+ 281 | ''+ 282 | ''; 283 | 284 | this._performRequest(data, function(error, response, body) { 285 | if (error) return cb(error); 286 | var xml = me._checkResponse(body, function(xml) { 287 | me.userId = xml.RESPONSE[0].USER[0]; 288 | cache.put('Gracenote.userId',me.userId); 289 | cb(null, me.userId); 290 | }); 291 | }); 292 | }; 293 | 294 | Gracenote.prototype.searchTrack = function(artistName, albumTitle, trackTitle, cb, options) { 295 | var body = this._constructQueryBody(artistName, albumTitle, trackTitle, "", "ALBUM_SEARCH", options), 296 | data = this._constructQueryRequest(body); 297 | this._execute(data, cb); 298 | }; 299 | 300 | Gracenote.prototype.searchArtist = function(artistName, cb, options) { 301 | this.searchTrack(artistName, "", "", cb, options); 302 | }; 303 | 304 | Gracenote.prototype.searchAlbum = function(artistName, albumTitle, cb, options) { 305 | this.searchTrack(artistName, albumTitle, "", cb, options); 306 | }; 307 | 308 | Gracenote.prototype.fetchAlbum = function(gn_id, cb) { 309 | var body = this._constructQueryBody("", "", "", gn_id, "ALBUM_FETCH"); 310 | var data = this._constructQueryRequest(body, "ALBUM_FETCH"); 311 | this._execute(data, cb); 312 | }; 313 | 314 | Gracenote.prototype.setExtended = function(str) { 315 | this._extendedData = str; 316 | }; 317 | 318 | Gracenote.prototype.setLanguage = function(iso) { 319 | this._lang = iso; 320 | }; 321 | 322 | Gracenote.prototype.setCoverSize = function(size) { 323 | this._coverSize = size; 324 | }; 325 | 326 | Gracenote.prototype.fetchOETData = function(gn_id, cb) { 327 | var me = this; 328 | var body = '' + gn_id +''+ 329 | ''+ 333 | ''; 337 | 338 | var data = this._constructQueryRequest(body, 'ALBUM_FETCH'); 339 | this._performRequest(data, function(error, response, body) { 340 | if (error) return cb(error); 341 | me._checkResponse(body, function(xml) { 342 | var output = { 343 | 'artist_origin': (xml.RESPONSE[0].ALBUM[0].ARTIST_ORIGIN[0]) ? me._getOETElem(xml.RESPONSE[0].ALBUM[0].ARTIST_ORIGIN[0]) : "", 344 | 'artist_era': (xml.RESPONSE[0].ALBUM[0].ARTIST_ERA[0]) ? me._getOETElem(xml.RESPONSE[0].ALBUM[0].ARTIST_ERA[0]) : "", 345 | 'artist_type': (xml.RESPONSE[0].ALBUM[0].ARTIST_TYPE[0]) ? me._getOETElem(xml.RESPONSE[0].ALBUM[0].ARTIST_TYPE[0]) : "" 346 | } 347 | cb(null, output); 348 | }); 349 | }); 350 | }; 351 | 352 | Gracenote.prototype.albumToc = function(toc, cb) { 353 | var body = '' + toc + ''; 354 | var data = this._constructQueryRequest(body, "ALBUM_TOC"); 355 | this._execute(data, cb); 356 | }; 357 | 358 | 359 | Gracenote.BEST_MATCH_ONLY = 0; 360 | Gracenote.ALL_RESULTS = 1; 361 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-gracenote", 3 | "version": "0.0.3", 4 | "description": "A node.js wrapper for the Gracenote API - https://developer.gracenote.com", 5 | "keywords": [ 6 | "gracenote" 7 | ], 8 | "author": "Dominik Danninger ", 9 | "main": "./lib/Gracenote.js", 10 | "dependencies": { 11 | "memory-cache": "0.0.5", 12 | "request": "^2.36.0", 13 | "xml2js": "^0.4.4" 14 | }, 15 | "engines": { 16 | "node": "*" 17 | }, 18 | "directories": { 19 | "lib": "lib" 20 | }, 21 | "devDependencies": {}, 22 | "repository": { 23 | "type": "git", 24 | "url": "git://github.com/ddanninger/node-gracenote.git" 25 | } 26 | } 27 | --------------------------------------------------------------------------------