Permalink
Browse files

node-echoprint-server v0.1.0

  • Loading branch information...
1 parent e69a07a commit d3b4fa079ba2ad5afe880a0ed4037d48724528d4 @jhurliman committed Feb 23, 2012
Showing with 1,311 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +46 −0 config.js
  3. +26 −0 config.local.js.orig
  4. +82 −0 controllers/api.js
  5. +126 −0 controllers/debug.js
  6. +457 −0 controllers/fingerprinter.js
  7. BIN docs/node-echoprint-debug01.png
  8. +28 −0 index.js
  9. 0 logs/.keep
  10. +224 −0 models/mysql.js
  11. +35 −0 mutex.js
  12. +40 −0 mysql.sql
  13. +9 −0 package.json
  14. +135 −0 server.js
  15. +99 −0 views/debug.jade
View
@@ -0,0 +1,4 @@
+
+.DS_Store
+node_modules/
+logs/*.log
View
@@ -0,0 +1,46 @@
+/**
+ * Configuration variables. These can be overridden in the per-system config file
+ */
+
+var log = require('winston');
+
+var settings = {
+ // Port that the web server will bind to
+ web_port: 37760,
+
+ // Database settings
+ db_user: 'root',
+ db_pass: '',
+ db_database: 'echoprint',
+ db_host: 'localhost',
+
+ // Set this to a system username to drop root privileges
+ run_as_user: '',
+
+ // Filename to log to
+ log_path: __dirname + '/logs/echoprint.log',
+ // Log level. Valid values are debug, info, warn, error
+ log_level: 'debug',
+
+ // Minimum number of codes that must be matched to consider a fingerprint
+ // match valid
+ code_threshold: 10
+};
+
+// Override default settings with any local settings
+try {
+ var localSettings = require('./config.local');
+
+ for (var property in localSettings) {
+ if (localSettings.hasOwnProperty(property))
+ settings[property] = localSettings[property];
+ }
+
+ log.info('Loaded settings from config.local.js. Database is ' +
+ settings.db_database + '@' + settings.db_host);
+} catch (err) {
+ log.warn('Using default settings from config.js. Database is ' +
+ settings.db_database + '@' + settings.db_host);
+}
+
+module.exports = settings;
View
@@ -0,0 +1,26 @@
+/**
+ * Local configuration variables
+ */
+
+module.exports = {
+ // Port that the web server will bind to
+ web_port: 37760,
+
+ // Database settings
+ db_user: 'root',
+ db_pass: '',
+ db_database: 'echoprint',
+ db_host: 'localhost',
+
+ // Set this to a system username to drop root privileges
+ run_as_user: '',
+
+ // Filename to log to
+ log_path: __dirname + '/logs/echoprint.log',
+ // Log level. Valid values are debug, info, warn, error
+ log_level: 'debug',
+
+ // Minimum number of codes that must be matched to consider a fingerprint
+ // match valid
+ code_threshold: 10
+};
View
@@ -0,0 +1,82 @@
+var urlParser = require('url');
+var log = require('winston');
+var fingerprinter = require('./fingerprinter');
+var server = require('../server');
+var config = require('../config');
+
+/**
+ * Querying for the closest matching track.
+ */
+exports.query = function(req, res) {
+ var url = urlParser.parse(req.url, true);
+ var code = url.query.code;
+ if (!code)
+ return server.respond(req, res, 500, { error: 'Missing code' });
+
+ var codeVer = url.query.version;
+ if (!codeVer || codeVer.length !== 4)
+ return server.respond(req, res, 500, { error: 'Missing or invalid version' });
+
+ fingerprinter.decodeCodeString(code, function(err, fp) {
+ if (err) {
+ log.error('Failed to decode codes for query: ' + err);
+ return server.respond(req, res, 500, { error: 'Invalid code' });
+ }
+
+ fp.codever = codeVer;
+
+ fingerprinter.bestMatchForQuery(fp, config.code_threshold, function(err, result) {
+ if (err) {
+ log.warn('Failed to complete query: ' + err);
+ return server.respond(req, res, 500, { error: 'Lookup failed' });
+ }
+
+ var duration = new Date() - req.start;
+ log.debug('Completed lookup in ' + duration + 'ms. success=' +
+ !!result.success + ', status=' + result.status);
+
+ return server.respond(req, res, 200, { success: !!result.success,
+ status: result.status, match: result.match || null });
+ });
+ });
+};
+
+/**
+ * Adding a new track to the database.
+ */
+exports.ingest = function(req, res) {
+ var code = req.body.code;
+ var codeVer = req.body.version;
+ var track = req.body.track;
+ var length = req.body.length;
+ var artist = req.body.artist;
+
+ if (!code || !codeVer || codeVer.length !== 4 || isNaN(parseInt(length, 10)))
+ return server.respond(req, res, 500, { error: 'Missing or invalid required fields' });
+
+ fingerprinter.decodeCodeString(code, function(err, fp) {
+ if (err || !fp.codes.length) {
+ log.error('Failed to decode codes for ingest: ' + err);
+ return server.respond(req, res, 500, { error: 'Invalid code' });
+ }
+
+ fp.codever = codeVer;
+ fp.track = track;
+ fp.length = length;
+ fp.artist = artist;
+
+ fingerprinter.ingest(fp, function(err, result) {
+ if (err) {
+ log.error('Failed to ingest track: ' + err);
+ return server.respond(req, res, 500, { error: 'Ingestion failed' });
+ }
+
+ var duration = new Date() - req.start;
+ log.debug('Ingested new track in ' + duration + 'ms. track_id=' +
+ result.track_id + ', artist_id=' + result.artist_id);
+
+ result.success = true;
+ return server.respond(req, res, 200, result);
+ });
+ });
+};
View
@@ -0,0 +1,126 @@
+var urlParser = require('url');
+var log = require('winston');
+var fingerprinter = require('./fingerprinter');
+var server = require('../server');
+var config = require('../config');
+
+/**
+ * Browser-friendly query debugging endpoint.
+ */
+exports.debugQuery = function(req, res) {
+ if (!req.body || !req.body.json)
+ return server.renderView(req, res, 200, 'debug.jade', {});
+
+ var code, codeVer;
+ try {
+ var json = JSON.parse(req.body.json)[0];
+ code = json.code;
+ codeVer = json.metadata.version.toString();
+ } catch (err) {
+ log.warn('Failed to parse JSON debug input: ' + err);
+ }
+
+ if (!code || !codeVer || codeVer.length !== 4) {
+ return server.renderView(req, res, 500, 'debug.jade',
+ { err: 'Unrecognized input' });
+ }
+
+ fingerprinter.decodeCodeString(code, function(err, fp) {
+ if (err) {
+ log.error('Failed to decode codes for debug query: ' + err);
+ return server.renderView(req, res, 500, 'debug.jade',
+ { err: 'Invalid code' });
+ }
+
+ fp.codever = codeVer;
+ fp = fingerprinter.cutFPLength(fp);
+
+ fingerprinter.bestMatchForQuery(fp, config.code_threshold,
+ function(err, result, allMatches)
+ {
+ if (err) {
+ log.warn('Failed to complete debug query: ' + err);
+ return server.renderView(req, res, 500, 'debug.jade',
+ { err: 'Lookup failed', input: req.body.json });
+ }
+
+ var duration = new Date() - req.start;
+ log.debug('Completed debug lookup in ' + duration + 'ms. success=' +
+ !!result.success + ', status=' + result.status);
+
+ // TODO: Determine a useful set of data to return about the query and
+ // each match and return it in an HTML view
+ if (allMatches) {
+ for (var i = 0; i < allMatches.length; i++) {
+ var match = allMatches[i];
+ match.codeLength = Math.ceil(match.length * fingerprinter.SECONDS_TO_TIMESTAMP);
+ // Find each match that contributed to ascore
+ getContributors(fp, match);
+ delete match.codes;
+ delete match.times;
+ }
+ }
+
+ var json = JSON.stringify({ success: !!result.success, status: result.status,
+ queryLen: fp.codes.length, matches: allMatches, queryTime: duration });
+ return server.renderView(req, res, 200, 'debug.jade', { res: json,
+ input: req.body.json });
+ });
+ });
+};
+
+/**
+ * Attach an array called contributors to the match object that contains one
+ * entry for each matched code that is contributing to the final match score.
+ * Used by the client-side JS to draw pretty pictures.
+ */
+function getContributors(fp, match) {
+ var MAX_DIST = 32767;
+ var i, j;
+
+ match.contributors = [];
+
+ if (match.codes.length < config.code_threshold)
+ return;
+
+ // Find the top two entries in the match histogram
+ var keys = Object.keys(match.histogram);
+ var array = new Array(keys.length);
+ for (i = 0; i < keys.length; i++)
+ array[i] = [ keys[i], match.histogram[keys[i]] ];
+ array.sort(function(a, b) { return b[1] - a[1]; });
+ var topOffsets = array.splice(0, 2);
+
+ var matchCodesToTimes = fingerprinter.getCodesToTimes(match, fingerprinter.MATCH_SLOP);
+
+ // Iterate over each {code,time} tuple in the query
+ for (i = 0; i < fp.codes.length; i++) {
+ var code = fp.codes[i];
+ var time = Math.floor(fp.times[i] / fingerprinter.MATCH_SLOP);
+ var minDist = MAX_DIST;
+
+ // Find the distance of the nearest instance of this code in the match
+ var matchTimes = matchCodesToTimes[code];
+ if (matchTimes) {
+ for (j = 0; j < matchTimes.length; j++) {
+ var dist = Math.abs(time - matchTimes[j]);
+ if (dist < minDist)
+ minDist = dist;
+ }
+
+ if (minDist < MAX_DIST) {
+ // If minDist is in topOffsets, add a contributor object
+ for (j = 0; j < topOffsets.length; j++) {
+ if (minDist === parseInt(topOffsets[j][0], 10)) {
+ match.contributors.push({
+ code: code,
+ time: time,
+ dist: minDist
+ });
+ break;
+ }
+ }
+ }
+ }
+ }
+}
Oops, something went wrong. Retry.

0 comments on commit d3b4fa0

Please sign in to comment.