aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Burwell <ben@benburwell.com>2015-08-02 17:31:36 -0400
committerBen Burwell <ben@benburwell.com>2015-08-02 17:31:36 -0400
commit3d0f989cee9d7c426c759588539dd06c14fa70ea (patch)
tree798fb7c7f10282be06f85814e20ba062a9d6f635
parent2cde3ec173c57d24e6fb8a31700a7db4eb31564d (diff)
let's get v1 going here
-rw-r--r--db/schema.sql36
-rw-r--r--package.json4
-rw-r--r--src/assets/index.html25
-rw-r--r--src/assets/thanks.html11
-rw-r--r--src/index.js207
-rw-r--r--src/templates/search_results.html34
-rw-r--r--src/templates/submit_song_new.html29
-rw-r--r--src/templates/submit_song_picklist.html15
-rw-r--r--src/utils.js25
9 files changed, 360 insertions, 26 deletions
diff --git a/db/schema.sql b/db/schema.sql
new file mode 100644
index 0000000..a7509da
--- /dev/null
+++ b/db/schema.sql
@@ -0,0 +1,36 @@
+-- Create the phrase table
+-- This will hold the phrases with an associated ID
+-- Phrases will be varchars of solfege syllables normalized as:
+-- a = do
+-- b = do+/re-
+-- c = re
+-- d = re+/mi-
+-- e = mi
+-- f = fa
+-- g = fa+/so-
+-- h = so
+-- i = so+/la-
+-- j = la
+-- k = la+/ti-
+-- l = ti
+CREATE TABLE IF NOT EXISTS phrases (
+ phrase_id BIGSERIAL PRIMARY KEY,
+ solfege VARCHAR(30) NOT NULL UNIQUE
+);
+
+-- Create a table that allows us to have a many-to-many
+-- relationship between phrases and songs (i.e., a phrase can be
+-- contained within multiple songs, and a song can have multiple phrases).
+CREATE TABLE IF NOT EXISTS phrase_song (
+ phrase_id BIGINT NOT NULL,
+ song_id BIGINT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS songs (
+ song_id BIGSERIAL PRIMARY KEY,
+ title VARCHAR(255),
+ artist_name VARCHAR(255)
+);
+
+-- Now we need an index that will allow us to search more quickly for phrases
+CREATE UNIQUE INDEX phrases_solfege ON phrases (solfege);
diff --git a/package.json b/package.json
index 876842d..fb3d90b 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,9 @@
"author": "Ben Burwell <ben@benburwell.com>",
"license": "MIT",
"dependencies": {
- "pg": "^4.4.1"
+ "handlebars": "^3.0.3",
+ "hapi": "^8.8.1",
+ "hapi-postgres": "^1.0.2"
},
"devDependencies": {
"mocha": "^2.2.5",
diff --git a/src/assets/index.html b/src/assets/index.html
new file mode 100644
index 0000000..ada309b
--- /dev/null
+++ b/src/assets/index.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <title>Solfege search</title>
+ </head>
+ <body>
+ <header>
+ <h1>Solfege search</h1>
+ </header>
+ <main>
+ <p>Enter some movable-do solfege syllables to search for:</p>
+ <p>
+ <form action="/search" method="get">
+ <input type="text" autofocus name="q">
+ <input type="submit" value="Search">
+ </form>
+ </p>
+ <p>The search engine will recognize different forms, such as ti and si, and will understand both <kbd>+</kbd> and <kbd>#</kbd> as sharp, and <kbd>-</kbd> and <kbd>b</kbd> as flat.</p>
+ <!-- ♯♭ -->
+ </main>
+ <footer>
+ <p>By <a href="https://www.benburwell.com/">Ben Burwell</a> | <a href="https://github.com/benburwell/solfege">GitHub</a> | &copy; 2015</p>
+ </footer>
+ </body>
+</html>
diff --git a/src/assets/thanks.html b/src/assets/thanks.html
new file mode 100644
index 0000000..e6f6ff4
--- /dev/null
+++ b/src/assets/thanks.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <title>Thanks for being awesome!</title>
+ </head>
+ <body>
+ <h1>You rock!</h1>
+ <p>Thanks for helping out the solfege project! You gain &infin; life points! :D</p>
+ <p><a href="/">Head home</a></p>
+ </body>
+</html>
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..7edcfdb
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,207 @@
+// require built-in modules
+var fs = require('fs');
+var path = require('path');
+
+// require external libs
+var Hapi = require('hapi');
+
+// require custom libs
+var utils = require('./utils');
+
+var server = new Hapi.Server();
+server.connection({
+ host: 'localhost',
+ port: process.env.PORT || 8000
+});
+
+server.views({
+ engines: {
+ html: require('handlebars')
+ },
+ path: path.join(__dirname, 'templates')
+});
+
+server.register({
+ register: require('hapi-postgres'),
+ options: {
+ uri: process.env.DATABASE_URL || 'postgres://postgres:postgres@127.0.0.1/solfege'
+ }
+}, function(err) {
+ if (err) {
+ console.error('Failed to load hapi-postgres', err);
+ }
+});
+
+server.method('search', function(query, next) {
+ var client = server.plugins['hapi-postgres'].client;
+ var done = server.plugins['hapi-postgres'].done;
+
+ var norm = utils.normalize(query);
+
+ var sql = 'SELECT title, artist_name FROM songs JOIN phrase_song ON (songs.song_id = phrase_song.song_id) WHERE phrase_song.phrase_id IN (SELECT phrase_id FROM phrases WHERE solfege LIKE \'%' + norm + '%\') GROUP BY songs.song_id;';
+ return client.query(sql, function(err, results) {
+ done();
+
+ if (err) {
+ next(err);
+ } else {
+ next(null, results.rows);
+ }
+ });
+});
+
+server.method('getSongsWithSimilarTitle', function(title, next) {
+ var client = server.plugins['hapi-postgres'].client;
+ var done = server.plugins['hapi-postgres'].done;
+
+ var sql = 'SELECT song_id, title, artist_name FROM songs WHERE title @@ plainto_tsquery(\'' + title + '\');';
+ return client.query(sql, function(err, results) {
+ done();
+ if (err) {
+ next(err);
+ } else {
+ next(null, results.rows);
+ }
+ });
+});
+
+server.method('addSongWithPhrase', function(options, next) {
+ var client = server.plugins['hapi-postgres'].client;
+ var done = server.plugins['hapi-postgres'].done;
+
+ if (!options.title || !options.artist_name || !options.solfege) {
+ done();
+ return next(new Error('Missing required title, artist_name, or solfege'));
+ }
+
+ options.solfege = utils.normalize(options.solfege);
+ var sql = 'INSERT INTO songs (title, artist_name) VALUES (\'' + options.title + '\', \'' + options.artist_name + '\'); INSERT INTO phrases (solfege) VALUES (\'' + options.solfege + '\'); INSERT INTO phrase_song (song_id, phrase_id) VALUES ((SELECT CURRVAL(\'songs_song_id_seq\')), (SELECT CURRVAL(\'phrases_phrase_id_seq\')));';
+ return client.query(sql, function(err, results) {
+ done();
+ if (err) {
+ next(err);
+ } else {
+ next(null);
+ }
+ });
+});
+
+server.method('addPhraseToSong', function(options, next) {
+ var client = server.plugins['hapi-postgres'].client;
+ var done = server.plugins['hapi-postgres'].done;
+
+ if (!options.song_id || !options.phrase) {
+ done();
+ return next(new Error('Missing required song_id or phrase'));
+ } else {
+ var solfege = utils.normalize(options.phrase);
+ var sql = 'INSERT INTO phrases (solfege) VALUES (\'' + solfege + '\'); INSERT INTO phrase_song (song_id, phrase_id) VALUES (' + options.song_id + ', (SELECT phrase_id FROM phrases WHERE solfege=\'' + solfege + '\'));';
+ return client.query(sql, function(err, results) {
+ done();
+
+ if (err) {
+ next(new Error('Error adding phrase to song'));
+ } else {
+ next(null);
+ }
+ });
+ }
+});
+
+// search results
+server.route({
+ method: 'GET',
+ path: '/search',
+ handler: function(request, reply) {
+
+ var q = request.query.q;
+
+ server.methods.search(request.query.q, function(err, results) {
+ if (err) {
+ reply('Error processing query: ' + err);
+ } else {
+ reply.view('search_results', {
+ original_query: q,
+ normalized_query: utils.normalize(q),
+ denormalized_query: utils.denormalize(utils.normalize(q)),
+ results: results
+ });
+ }
+ });
+ }
+});
+
+// submit a song
+server.route({
+ method: 'GET',
+ path: '/submit_song',
+ handler: function(request, reply) {
+ server.methods.getSongsWithSimilarTitle(request.query.title, function(err, songs) {
+ if (err) {
+ console.error(err)
+ reply('Error');
+ } else {
+ if (songs.length > 0 && request.query.new != 1) {
+ reply.view('submit_song_picklist', { phrase: request.query.phrase, songs: songs });
+ } else {
+ reply.view('submit_song_new', {
+ title: request.query.title,
+ phrase: request.query.phrase
+ });
+ }
+ }
+ });
+ }
+});
+
+server.route({
+ method: 'POST',
+ path: '/submit_song',
+ handler: function(request, reply) {
+ server.methods.addSongWithPhrase({
+ title: request.payload.title,
+ artist_name: request.payload.artist,
+ solfege: request.payload.phrase
+ }, function(err) {
+ if (err) {
+ console.log(err);
+ reply('Error processing request');
+ } else {
+ reply.redirect('/thanks.html');
+ }
+ });
+ }
+});
+
+server.route({
+ method: 'GET',
+ path: '/add_phrase',
+ handler: function(request, reply) {
+ server.methods.addPhraseToSong({
+ song_id: request.query.id,
+ phrase: request.query.phrase
+ }, function(err) {
+ if (err) {
+ console.error(err);
+ reply('Error processing request');
+ } else {
+ reply.redirect('/thanks.html');
+ }
+ });
+ }
+});
+
+// serve static files
+server.route({
+ method: 'GET',
+ path: '/{filename*}',
+ handler: {
+ directory: {
+ path: path.join(__dirname, 'assets')
+ }
+ }
+});
+
+server.start(function() {
+ console.log('Server running!');
+});
diff --git a/src/templates/search_results.html b/src/templates/search_results.html
new file mode 100644
index 0000000..9bac74f
--- /dev/null
+++ b/src/templates/search_results.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <title>Solfege search results - {{original_query}}</title>
+ </head>
+ <body>
+ <!-- Normalized: {{normalized_query}} -->
+ <!-- Denormalized: {{denormalized_query}} -->
+
+ <h1>Solfege search results</h1>
+ <p>
+ <form action="/search" method="get">
+ <input type="text" name="q" value="{{original_query}}">
+ <input type="submit" value="Search">
+ </form>
+ </p>
+
+ <ol>
+ {{#each results}}
+ <li><b>{{title}} - {{artist_name}}</b></li>
+ {{/each}}
+ </ol>
+
+ <h2>None of these right? Remebered the song?</h2>
+ <p>Submit the song this phrase is from!</p>
+ <p>
+ <form action="/submit_song">
+ <input type="hidden" name="phrase" value="{{denormalized_query}}">
+ <input type="text" name="title" placeholder="Song title">
+ <input type="submit" value="Improve the world">
+ </form>
+ </p>
+ </body>
+</html>
diff --git a/src/templates/submit_song_new.html b/src/templates/submit_song_new.html
new file mode 100644
index 0000000..29d7529
--- /dev/null
+++ b/src/templates/submit_song_new.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Submit a new song</title>
+ </head>
+ <body>
+ <h1>Submit a new song</h1>
+ <p>Tell us about the song (all fields required):</p>
+ <form action="/submit_song" method="post">
+ <table>
+ <tr>
+ <td>Title:</td>
+ <td><input type="text" name="title" value="{{title}}"></td>
+ </tr>
+ <tr>
+ <td>Artist:</td>
+ <td><input type="text" name="artist" value="{{artist}}"></td>
+ </tr>
+ <tr>
+ <td>Phrase:</td>
+ <td><input type="text" name="phrase" value="{{phrase}}"></td>
+ </tr>
+ <tr>
+ <td colspan="2"><input type="submit" value="Win at life"></td>
+ </tr>
+ </table>
+ </form>
+ </body>
+</html>
diff --git a/src/templates/submit_song_picklist.html b/src/templates/submit_song_picklist.html
new file mode 100644
index 0000000..b56c43f
--- /dev/null
+++ b/src/templates/submit_song_picklist.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <title>Submit a song</title>
+ </head>
+ <body>
+ <h1>Did you mean one of these?</h1>
+ <ol>
+ {{#each songs}}
+ <li><a href="/add_phrase?phrase={{../phrase}}&amp;id={{song_id}}">{{title}} by {{artist_name}}</a></li>
+ {{/each}}
+ </ol>
+ <a href="/submit_song?phrase={{phrase}}&amp;title={{title}}&amp;new=1">Nope, it's none of these</a>
+ </body>
+</html>
diff --git a/src/utils.js b/src/utils.js
index f2d1dc1..4dc3458 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,7 +1,4 @@
-var fs = require('fs');
-
module.exports = {
-
// Take some solfege input and normalize it
normalize: function(str) {
var tokens = str.toLowerCase().split(/\s+/);
@@ -80,27 +77,5 @@ module.exports = {
}
return ret.trim();
- },
-
- // Initialize the SQL database
- initializeDatabase: function(db, done) {
-
- // Fetch the SQL we need to run
- fs.readFile('../db/schema.sql', function(err, sql) {
- if (err) {
- return console.error('Error reading SQL from file', err);
- }
-
- // Execute the query
- db.query(sql, function(err, result) {
-
- if (err) {
- console.error('Error executing SQL query', err);
- }
-
- // Run the callback
- done();
- });
- });
}
};