<repo_name>ahanlon1987/musocracy<file_sep>/voter-app/public/js/routers/mobileRouter.js
// Mobile Router
// =============

// Includes file dependencies
define([ "jquery","backbone", "amplify", "../collections/SearchCollection", "../collections/QueueCollection", "../views/SearchView", "../views/QueueView", "../views/LocationView", "../util/persist"],
function( $, Backbone, Amplify, SearchCollection, QueueCollection, SearchView, QueueView, LocationView, Persist) {

    // Extends Backbone.Router
    var MusocracyRouter = Backbone.Router.extend( {


        // The Router constructor
        initialize: function() {

            //Instantiates a new Location View
            this.locationView = new LocationView({el: "#container-wrapper"});

            // Instantiates a new Search  View
            this.searchView = new SearchView( { el: "#container-wrapper", collection: new SearchCollection () } );

            // Instantiates a new Queue View
            this.queueView= new QueueView( { el: "#votes", collection: new QueueCollection() } );

            // Tells Backbone to start watching for hashchange events
            Backbone.history.start();

            this.persist = Persist;

        },

        amplify:Amplify,

        // Backbone.js Routes
        routes: {

            // When there is no hash bang on the url, the home method is called
            "": "home",

            "queue" : "queue",

            "search" : "search",

            "search?:query" : "search",

            "location" : "enterLocation"

        },

        // Home method
        home: function() {
            if(!amplify.store('locationId')){
                this.enterLocation();
            } else {
                window.location.href = '#queue';
            }
        },

        enterLocation: function(){
            this.locationView.render();
        },

        queue: function() {

            if(!amplify.store('locationId')){
                this.enterLocation();
            } else {
                var currentView = this.queueView;
                currentView.collection.url = '/location/' + amplify.store('locationId') +'/votes';

                //Always refresh the queue
                currentView.collection.fetch().done( function() {

                } );
            }

        },

        search: function(query) {

            if(!amplify.store('locationId')){
                this.enterLocation();
            } else {
                var currentView = this.searchView;
                if(query){

                    currentView.collection.url = '/search/track?q=' + query;
                    currentView.collection.fetch().done( function() {
                    } );
                } else {

                    //No previous search?
                    if (currentView.collection.isEmpty()){
                        currentView.collection.reset(); //re-render a blank view

                        //Previous search in memory, show it
                    } else {
                        currentView.render();
                    }

                }
            }


        }
    } );

// Returns the Router class
    return MusocracyRouter;

} );<file_sep>/voter-app/public/js/collections/SpotifySearchCollection.js
define(['backbone', 'models/TrackModel'],
    function (Backbone, TrackModel) {

        var regex = /[^A-Za-z\d]/g;
        return Backbone.Collection.extend({

            model: TrackModel,

            initialize:function () {

            },

            parse:function (response) {
                return response.playlist;
            },

            getTrackById:function (trackId) {
                return this.find(function (model) {
                    return model.get('trackId') === trackId;
                });
            },

            // filter results by name and artist, ignore case, special chars, and whitespace for now
            getFilteredResults:function (filter) {
                if (filter) {
                    filter = filter.toLowerCase.replace(regex, '');
                }

                return _.filter(this.models, function (model) {
                    var name = model.get('name') || '';
                    name = name.toLowerCase().replace(regex, '');

                    var artists = model.get('artists') || '';
                    artists = artists.toLowerCase().replace(regex, '');
                    return name.contains(filter) || artists.contains(filter);
                });
            }



        });
    });<file_sep>/voter-app/routes/votes.js

var votingService = require('../services/votingService').votingService;

exports.addVote = function(req, resp) {

        var locationId = req.params.locationId;
        var trackId = req.params.trackId;
        var track = req.body;

        votingService.addVote(locationId, track, {
            success:function(result) {
                console.log('successfully added vote');
                resp.writeHead(200, {'Content-Type':'application/json'});
                resp.write(JSON.stringify(result));
                resp.end();
            },
            error:function(err) {
                resp.writeHead(500);
                resp.write('Error adding vote');
                resp.end();
            }
        })
    // }
};

exports.getVotes = function(req, resp) {
    var locationId = req.params.locationId;
    var limit = req.query.limit || 0;
    var excludePlayed = (req.query.excludePlayed && (req.query.excludePlayed.toLowerCase() === 'true')) || false;
    votingService.getVotes({
        locationId: locationId,
        limit: limit,
        excludePlayed:excludePlayed
    }, 
    {
        success:function(results) {
            resp.writeHead(200, {'Content-Type':'application/json'});
            resp.write(JSON.stringify(results));
            resp.end();
        },
        error:function(err) {
            resp.writeHead(500);
            resp.write('Error getting votes');
            resp.end();
        }
    })
};

exports.markAsPlayed = function (req, resp) {
  var locationId = req.params.locationId,
    trackId = req.params.trackId;

    console.log('Marking track as played: locationId=' + locationId);
    console.log('trackId=' + trackId);

  votingService.markAsPlayed({
      locationId:locationId,
      trackId:trackId
    }, 
    {
      success:function(result) {
        console.log('successfully added vote');
        resp.writeHead(200, {'Content-Type':'application/json'});
        resp.write(JSON.stringify(result));
        resp.end();
      },
      error:function(err) {
        resp.writeHead(500);
        resp.write('Error adding vote');
        resp.end();
      }
  });
};

// exports.addVote = addVote;
// exports.getVotes = getVotes;<file_sep>/voter-app/routes/player.js

var playerService = require('../services/playerService').api;

exports.updateNowPlayingAndQueueNext = function (req, resp) {
    var locationId = req.params.locationId;
    var nowPlaying = req.query.nowPlaying;
    var upNext = req.query.upNext;


    console.log('Marking track as played: locationId=' + locationId);
    console.log('nowPlaying=' + nowPlaying);
    console.log('upNext=' + upNext);

    playerService.updateNowPlayingAndQueueNext({
            locationId:locationId,
            nowPlaying:nowPlaying,
            upNext:upNext,
            success:function(result) {
                console.log('successfully added vote');
                resp.writeHead(200, {'Content-Type':'application/json'});
                resp.write(JSON.stringify(result));
                resp.end();
            },
            error:function(err) {
                resp.writeHead(500);
                resp.write('Error adding vote');
                resp.end();
            }
        });
};

// exports.addVote = addVote;
// exports.getVotes = getVotes;<file_sep>/voter-app/public/js/views/ToolbarView.js
define(['jquery', 'underscore', 'backbone', 'templates', 'util/dispatcher'],
    function ($, _, Backbone, templates, dispatcher) {
        return Backbone.View.extend({
            events:{
                'click .icon-refresh':'refresh',
                'click .refresh':'refresh'
            },

            initialize:function () {
                this.title = this.options.title || 'Musocracy';
                this.searchShown = false;
            },

            render:function () {
                var templateJson = {
                    title:this.title
                };

                this.$el.html(templates.toolbar.render(templateJson));
                return this;
            },

            showLocation:function (locationName) {
                this.$('.brand').text(locationName);
                this.$('.back').show();
            },

            showHome:function () {
                this.$('.brand').text('Musocracy');
                this.$('.back').hide();
            },

            refresh:function (e) {
                (e && e.preventDefault());
                this.startRefreshRotation();

                dispatcher.trigger(dispatcher.events.REFRESH);
            },

            startRefreshRotation:function () {
                var degrees = 0;
                var $refreshIcon = this.$('.icon-refresh');
                var rotate = function() {
                    degrees += 10;
                    $refreshIcon.css('transform', 'rotate(' + degrees + 'deg)');

                    if (degrees !== 360) {
                        setTimeout(rotate, 25);
                    }
                };
                rotate();
            },

            toggleSearch:function (e) {
                (e && e.preventDefault());

                this.searchShown ? this.showSearch() : this.hideSearch();
            },

            showSearch:function () {
                this.$('.search').show();
                dispatcher.trigger(dispatcher.events.SHOW_SEARCH);
            },

            hideSearch:function () {
                this.$('.search').hide();
                dispatcher.trigger(dispatcher.events.HIDE_SEARCH);
            },

            clearSearch:function () {
                this.$('.search input').val('');
                dispatcher.trigger(dispatcher.events.CLEAR_SEARCH);
            }
        });
    });<file_sep>/voter-app/public/js/collections/QueueCollection.js
// Category Collection
// ===================

// Includes file dependencies
define([ "jquery", "backbone", "models/QueueModel"], function ($, Backbone, QueueModel) {

    // Extends Backbone.Collection
    var Collection = Backbone.Collection.extend({

        // The Collection constructor
        initialize:function () {
        },

        isEmpty:function () {
            return (this.models.length < 1);
        },

        type:'Queue',

        // Sets the Collection model property to be a Category Model
        model:QueueModel,

        comparator:function (model) {
            return -model.get('votes');
        },

        parse:function (response) {
            if (response) {
                return response.playlist;
            }
        },

        getFilteredResults:function (filter) {
            return _.filter(this.models, function (model) {
                //TODO make this better e.g., to lowercase on everything.
                return model.get('name') == filter;
            });
        }

    });

    // Returns the Collection class
    return Collection;

});<file_sep>/voter-app/public/js/views/ListItemView.js
// Category View
// =============

// Includes file dependencies
define([ "jquery", "backbone", "amplify"], function( $, Backbone, Amplify) {

    var THREE_HOURS_IN_MS = 10800000;

    var ListItemView = Backbone.View.extend({

        tagName : "li",

        initialize: function(){
            _.bindAll(this, "setActive");
        },

        amplify:Amplify,

        render : function(model) {

            var index = $.inArray(model.get('trackId'), _.pluck(amplify.store('previousVotes'), 'trackId'));
            if (index >= 0){
                var oldVote = amplify.store('previousVotes')[index];
                if(oldVote){
                    if((new Date()) - (new Date(oldVote.voteTime)) < THREE_HOURS_IN_MS){
                        model.set('disableVote', true);
                    }
                }
            }

            this.template = _.template( $( "script#songs" ).html(), { "model": model.toJSON()} );

            this.$el.attr('data-name', model.get('trackId'))
                    .addClass('song-list media')
                    .addClass(model.get('disableVote') ? 'disabled': '')
                    .html(this.template);

            this.model = model;

            return this;
        },

        events:{
            "click":"setActive"
        },

        setActive: function(event){

            var listItem = $(event.currentTarget);
            if(listItem[0]){
                if(!this.model.get('disableVote')){
                    $(listItem).animate({
                        opacity:.5
                    });
                    router.persist.vote(this.model);
                }
            }
        }
    });

    return ListItemView;

} );<file_sep>/voter-app/public/js/util/persist.js
// Persist Utils
// =============

// Includes file dependencies
define(["jquery", "underscore", "backbone", "amplify"], function ($, _, Backbone, Amplify) {

    var Persist = {


        amplify:Amplify,

        vote:function (track, options) {

            var trackId = track.get('trackId');
            var trackName = track.get('name');
            var artists = track.get('artists');
            var album = track.get('album');

            console.log('Voting for trackId: ' + trackId + ', trackName: ' + trackName + ', artists: ' + artists + ', album: ' + album);
            var amp = amplify;

            $.ajax({
                type:"POST",
                url:'/location/' + amplify.store('locationId') + '/votes/' + trackId,
                data:{trackId:trackId, name:trackName, artists:artists, album:album},
                success:function (resp) {
                    var previousVotes = (amp.store('previousVotes') || []);
                    previousVotes.push({
                        'trackId':trackId,
                        'voteTime':new Date()
                    });
                    amp.store('previousVotes', previousVotes);

                    if (options && _.isFunction(options.success)) {
                        options.success(resp);
                    }
                },
                failure:function () {
                    console.log('voting failed.');
                },
                dataType:'json'
            });

        },


        lookupLocation:function (location) {

            $.ajax({
                type:"GET",
                url:'/location/' + location,
                data:{},
                success:function (data) {
                    console.log('200 returned from location service, check if locationId is null');
                    if (data) {
                        console.log('found location: ' + data.locationId);
                        amplify.store('locationId', data.locationId, {expires:21600000}); //expire locationId after six hours.
                        $("#location-submit").addClass("hidden");
                        window.location.href = '#';

                    } else {
                        console.log('No such location: ' + location);
                        $('label#location-label').html('Unable to find a location of ' + location + '.');
                    }

                },
                failure:function () {
                    console.log('Error trying to find location ' + location + ', please try again');
                },
                dataType:'json'
            });
        }

    };

    return Persist;

});<file_sep>/voter-app/app.js
/**
 * Module dependencies.
 */

var express = require('express')
    , routes = require('./routes')
    , user = require('./routes/user')
    , search = require('./routes/search')
    , lookup = require('./routes/lookup')
    , http = require('http')
    , path = require('path')
    , votingService = require('./services/votingService').votingService
    , votes = require('./routes/votes')
    , location = require('./routes/location')
    , player = require('./routes/player')
    , locationService = require('./services/locationService').locationService
    , playerService = require('./services/playerService').api;


var app = express();

app.configure(function () {
    app.set('port', process.env.PORT || 3000);
    // app.set('views', __dirname + '/views');
    // app.set('view engine', 'jade');
    app.use(express.static(path.join(__dirname, 'views')));
    app.use(express.favicon());
    app.use(express.logger('dev'));
    app.use(express.bodyParser());
    app.use(express.methodOverride());
    app.use(app.router);
    app.use(require('less-middleware')({
//      src: __dirname + '/public',
        dest:__dirname + '/public/css',
        src:__dirname + '/public/less',
        prefix:'/css'
    }));

});

app.get('/', routes.index);
app.get('/location/:locationId/search/track', search.byTrack);
app.get('/location/:locationId/search/artist', search.byArtist);
app.get('/location/:locationId/search/album', search.byAlbum);
app.get('/lookup', lookup.lookup);
app.get('/location/:locationId', location.getLocation);
app.get('/location/:locationId/votes', votes.getVotes);
app.get('/location/:locationId/votes/:trackId', votes.addVote);
app.post('/location/:locationId/votes/:trackId', votes.addVote);
app.post('/location/:locationId/track/:trackId', votes.markAsPlayed);
app.get('/location/:locationId/track/:trackId', votes.markAsPlayed);
app.get('/location/:locationId/updateQueue', player.updateNowPlayingAndQueueNext);


var config = {};
var mongourl;

var generate_mongo_url = function (obj) {
    obj.hostname = (obj.hostname || 'localhost');
    obj.port = (obj.port || 27017);
    obj.db = (obj.db || 'test');
    if (obj.username && obj.password) {
        return "mongodb://" + obj.username + ":" + obj.password + "@" + obj.hostname + ":" + obj.port + "/" + obj.db;
    }
    else {
        return "mongodb://" + obj.hostname + ":" + obj.port + "/" + obj.db;
    }
}


app.configure('development', function () {
    console.log('Loading development configuration');

    app.use(express.errorHandler());
    app.use(express.static(path.join(__dirname, 'public')));
    config = require('./config/development.json');
    app.set('port', config.port);
    mongourl = generate_mongo_url(config.mongo);
});

app.configure('production', function () {
//mongodb://af_musocracy-musocracyapp:<EMAIL>:27427/af_musocracy-musocracyapp  

    console.log('Loading production configuration');

    app.use(express.static(path.join(__dirname, 'publicbuilt')));
    config = require('./config/production.json');
    mongourl = "mongodb://musocracy:<EMAIL>:27447/musocracy";
});
;

if (!mongourl) {
    console.log("Couldn't find mongolab URI! Exiting now");
    throw new Error('MONGOLAB_URI environment variable not found.');
}

console.log('Mongolab URI: ' + mongourl);

console.error("About to connect to " + mongourl);
require('mongodb').connect(mongourl, function (err, conn) {
    if (err) {
        console.log('Error connecting to Mongo', err);
        throw err;
    }

    console.log('Mongo connection obtained.');
    votingService.setDb(conn);
    locationService.setDb(conn);
    playerService.setDb(conn);

    http.createServer(app).listen(app.get('port'), function () {
        console.log("Express server listening on port " + app.get('port'));
    });

});

<file_sep>/voter-app/services/spotifyService.js

var url = require('url');
var http = require('http');
var Q = require('q');
var _ = require('underscore');

var filter = require('./util/filter').filter;


var BASE_URL = 'http://ws.spotify.com/';

var SEARCH_URLS = {
    track: BASE_URL + 'search/1/track.json',
    artist: BASE_URL + 'search/1/artist.json',
    album: BASE_URL + 'search/1/album.json'
};

var LOOKUP_URL = BASE_URL + 'lookup/1/.json';


/**
 *
 * @param opts
 * {
 *    type: track | artist | album
 *    query: search
 *    success: function
 *    error: function
 * }
 */
var doSearch = function(opts) {
    // console.log('searching by track');
    var locationId = opts.locationId,
        query = opts.query,
        type = opts.type;

    var requestUrl = SEARCH_URLS[type] + '?q=' + query;
    // console.log('Request Url: ' + requestUrl);
    http.get(requestUrl, function(response){
        // console.log('Got Response', response);
        var respData = '';
        response.setEncoding('utf8');
        response.on('data', function(chunk) {
            // console.log('Data Received: ' + chunk);
            respData += chunk;
        });

        response.on('end', function() {
            var respObj = JSON.parse(respData);
//      console.log(respData);

            var info  = respObj.info;

            //Clean up the search result to only bring back what we need: id, name, artist(s), album.
            var formattedTracks = [];
            if (respObj.tracks){
                _.each(respObj.tracks, function(track){
                    var trackId = track.href;
                    var name = track.name;
                    var artists = '';
                    var delim = ' ';
                    _.each(track.artists, function(artist){
                        artists += delim + artist.name;
                        delim = ', ';
                    });
                    var album = track.album ? track.album.name : '';

                    formattedTracks.push({
                        "trackId":trackId,
                        "name":name,
                        "artists":artists,
                        "album":album
                    })

                });
            }

            filter()
            opts.success({info:info, playlist: formattedTracks});
        });
    })
}

exports.searchByTrack = function(opts) {
    doSearch(_.extend(opts, {type:'track'}));
};

exports.searchByArtist = function(opts) {
    doSearch(_.extend(opts, {type:'artist'}));
};

exports.searchByAlbum = function(opts) {
    doSearch(_.extend(opts, {type:'album'}));
};

/**
 *
 * @param opts
 * {
 *     uri: trackId
 * }
 */
exports.lookup = function(opts) {
    // console.log('searching by track');
    var uri = opts.uri;
    if (!uri) {
        opts.error({errorCode: 'error.spotifyService.lookup.noUri'});
        return;
    }

    var requestUrl = LOOKUP_URL + '?uri=' + uri;
    if (uri instanceof String && uri.indexOf('spotify:artist') >= 0) {
        requestUrl += '&extras=album'
    }
    else if (uri instanceof String && uri.indexOf('spotify:album') >= 0) {
        requestUrl += '&extras=track'
    }

    // console.log('Request Url: ' + requestUrl);
    http.get(requestUrl, function(response){
        // console.log('Got Response', response);
        var respData = '';
        response.setEncoding('utf8');
        response.on('data', function(chunk) {
            // console.log('Data Received: ' + chunk);
            respData += chunk;
        });

        response.on('end', function() {
            var respObj = JSON.parse(respData);
            // console.log(respData);

            var track = {
                trackId: respObj.track.href,
                name: respObj.track.name,
                artists:_.reduce(respObj.track.artists, function(memo, artist) {
                        if (memo) memo+= ", ";
                        return (memo += artist.name);
                    }, ""),
                album: respObj.track.album.name
            };

            opts.success(track);
        });
    })
};
<file_sep>/voter-app/services/locationService.js

var _ = require('underscore');
var PLAYLISTS_COLLECTION = 'playlists';

var locationService = {
    setDb:function(db) {
        this.db = db;
    },

    //TODO we could probably use to increment a user count or something
    findLocation:function(locationId, options) {
        console.log('Adding vote for locationId=' + locationId);

        this.db.collection(PLAYLISTS_COLLECTION, function(err, collection) {
            if (err) {
                console.log('Error fetching collection (name=' + PLAYLISTS_COLLECTION + ')');
                (options && options.error && options.error(err));
                return;
            }

            console.log('found collection; searching for location.');

            collection.findOne({locationId:locationId.toLowerCase()}, function(err, location) {
                if (err) {
                    console.log('Error finding collection.', err);
                    options && options.error && options.error(err);
                    return;
                }

                if (!location) {
                    console.log('Unable to find location. Returning null');
                    options && options.success && options.success(null)
                } else {
                    console.log('Found location ' + locationId + '. Returning location');
//                    options && options.success && options.success({"locationId":locationId})
                    options && options.success && options.success(location);
                }
            });
        });
    }
};

exports.locationService = locationService;



<file_sep>/voter-app/routes/location.js

var locationService = require('../services/locationService').locationService;


exports.getLocation = function(req, resp) {

    var locationId = req.params.locationId;

    locationService.findLocation(locationId, {
             success:function(result) {
                 resp.writeHead(200, {'Content-Type':'application/json'});
                 resp.write(JSON.stringify(result));
                 resp.end();
             },
             error:function(err) {
                 resp.writeHead(500);
                 resp.write('Error finding location');
                 resp.end();
             }
         })

};
<file_sep>/voter-app/services/playerService.js
var _ = require('underscore');
var spotifyService = require('../services/spotifyService');
var votingService = require('../services/votingService').votingService;

var PLAYLISTS_COLLECTION = 'playlists';
var MAX_PLAYED_LENGTH = 20;

/**
 *
 * @param opts
 * {
 *    trackId: track ID to load
 *    success: function
 *    error: function
 * }
 */
var deferTrackLoad = function(opts) {
    var trackId = opts.trackId;

    spotifyService.lookup({
        uri: trackId,
        success: opts.success,
        error: opts.error
    });
}


var service = {

    setDb:function(db) {
        this.db = db;
    },

    /**
     *
     * @param opts
     * {
     *     locationId: locationId
     *     nowPlaying: trackId
     *     upNext: trackId
     *     success: function
     *     error: function
     * }
     */
    updateNowPlayingAndQueueNext:function(options) {
        var locationId = options.locationId;
        var nowPlayingTrackId = options.nowPlaying;
        var upNextTrackId = options.upNext;
        if (!locationId) {
            throw new Error("locationId param is required");
        }

        var self = this;
        this.db.collection(PLAYLISTS_COLLECTION, function (err, collection) {
            if (err) {
                console.error('Error retrieving collection.');
                if (options && _.isFunction(options.error)) options.error(err);
                else throw err;
            }

            collection.findOne({locationId: locationId}, function(err, location) {
                if (err) {
                    console.error('Error finding collection.', err);
                    if (options && _.isFunction(options.error)) options.error(err);
                    else throw err;
                }

                if (!location) {
                    location = {
                        locationId: locationId
                    };
                }

                if (!location.votes) {
                    location.votes = [];
                }

                if (!location.played) {
                    location.played = [];
                }

                var nowPlaying, upNext,
                    nowPlayingIndex = -1,
                    upNextIndex = -1;

                if (nowPlayingTrackId) {
                    if (location.upNext && location.upNext.trackId === nowPlayingTrackId) {
                        nowPlaying = location.upNext;
                    }
                    else {
                        nowPlaying = _.find(location.votes, function(track, index) {
                            if (track.trackId === nowPlayingTrackId) {
                                nowPlayingIndex = index;
                                return true;
                            }
                            return false;
                        });
                        if (nowPlaying) {
                            location.votes.splice(nowPlayingIndex, 1);
                        }
                    }

                    // if nowPlaying was found, store it and remove from the votes list
                    if (nowPlaying) {
                        location.nowPlaying = nowPlaying;
                        location.played.push(nowPlaying);
                    }
                    else {
                        deferTrackLoad({
                            trackId: nowPlayingTrackId,
                            success: function(track) {
                                collection.update({locationId:location.locationId}, {$set: {nowPlaying:track}});
                            },
                            error: function(err) {
                                console.error('Error deferring load of up next track', err);
                            }
                        })
                    }
                }
                else {
                    location.nowPlaying = null;
                }

                if (upNextTrackId) {
                    upNext = _.find(location.votes, function(track, index) {
                        if (track.trackId === upNextTrackId) {
                            upNextIndex = index;
                            return true;
                        }
                        return false;
                    });

                    // if upNext was found, store it and remove from the votes list
                    if (upNext) {
                        location.upNext = upNext;
                        location.votes.splice(upNextIndex, 1);
                    }
                    else {
                        deferTrackLoad({
                            trackId: upNextTrackId,
                            success: function(track) {
                                collection.update({locationId:location.locationId}, {$set: {upNext:track}});
                            },
                            error: function(err) {
                                console.error('Error deferring load of up next track', err);
                            }
                        })
                    }
                }
                else {
                    location.upNext = null;
                }

                collection.update({locationId:location.locationId}, location, {upsert:true}, function (err, obj) {
                    if (err) {
                        console.error('Error marking track as played.', err);
                        (options && options.error && options.error(err));
                    }
                    else {
                        votingService.getVotes({
                            locationId:location.locationId,
                            limit:5,
                            excludePlayed:true
                        }, options);
                        // (options && options.success && options.success(location));
                    }
                });

            });
        })

    }


};

exports.api = service;<file_sep>/voter-app/public/js/views/FooterView.js
define(['jquery', 'underscore', 'backbone', 'templates'], 
  function ($, _, Backbone, templates) {
    return Backbone.View.extend({
      initialize:function() {

      },

      render:function() {
        this.$el.html(templates.footer.render());
        return this;
      }
    });
  });<file_sep>/voter-app/services/notes.txt
Hosting:  appfrog
Mongo: Mongolab
username: musocracyadmin
password: <PASSWORD>

db user: musocracy
password: <PASSWORD><file_sep>/voter-app/public/js/views/HomeView.js
define(['jquery', 'amplify', 'backbone', 'templates'], 
  function ($, amplify, Backbone, templates) {
    return Backbone.View.extend({
      events: {
        'submit #location-submit': 'submitLocation'
      },

      initialize:function () {

      },

      render:function () {
        this.$el.html(templates.home.render());
        return this;
      },

      submitLocation:function (e) {
        (e && e.preventDefault());
        var location = this.$("#location-input").val();
        if (location) {
          // router.persist.lookupLocation(location);
          this.verifyLocation(location);
        } else {
        console.log('No value found, enter a value and try again.');
          this.$('label#location-label').html('Enter a value and try again.');
        }

        return false;
      },

      verifyLocation:function(location) {
        var self = this;
        $.ajax({
            type: "GET",
            url: '/location/' + location,
            contentType: 'application/json',
            success: function(data) {
                console.log('200 returned from location service, check if locationId is null');
                if(data) {
                    console.log('found location: ' + data.locationId);
                    amplify.store('locationId', data.locationId, {expires:21600000}); //expire locationId after six hours.
                    window.location.href = '/#location/' + data.locationId;

                } else {
                    console.log('No such location: ' + location);
                    self.$('label#location-label').html('Unable to find a location of ' + location + '.');
                }

            },
            failure:function(){
                console.log('Error trying to find location ' + location + ', please try again');
            }
        });
      }
    });
  });<file_sep>/voter-app/services/votingService.js
var _ = require('underscore');
var PLAYLISTS_COLLECTION = 'playlists';
var MAX_PLAYED_LENGTH = 20;

/*
{
    locationId: "Kincades",
    votes: [
        { name: "Inside Out", artists: "Eve 6", album: "Album Name", votes: 3 },
        ...
    ]
    played: [
        { name: "Californication", artists: "Red Hot Chili Peppers", album: "Album Name", votes: 5, played: true },
        { name: "Can't Stop", artists: "Red Hot Chili Peppers", album: "Album Name", votes: 3, played: true },
    ],
}
 */

var updatePlaylist = function(location) {
    var votes = location.votes;
    var topFive = votes.slice(0, 5);


};

var votingService = {
    setDb:function (db) {
        this.db = db;
    },

    addVote:function (locationId, obj, options) {
        console.log('Adding vote for locationId=' + locationId);

        this.db.collection(PLAYLISTS_COLLECTION, function (err, collection) {
            if (err) {
                console.error('Error fetching collection (name=' + PLAYLISTS_COLLECTION + ')');
                (options && options.error && options.error(err));
                return;
            }

            console.error('found collection; searching for location.');
            collection.findOne({locationId:locationId.toLowerCase()}, function (err, location) {
                if (err) {
                    console.error('Error finding collection.', err);
                    options && options.error && options.error(err);
                    return;
                }

                if (!location) {
                    location = {
                        locationId:locationId.toLowerCase()
                    };
                }
                if (!location.votes) {
                    location.votes = [];
                }
                if (!location.played) {
                    location.played = [];
                }

                console.error('Search tracks for track.trackId=' + obj.trackId);
                var track = _.find(location.votes, function(track) {
                    return track.trackId === obj.trackId;
                });

                if (track) {
                    track.votes++;
                }
                else {
                    location.votes.push({
                        trackId:obj.trackId,
                        name:obj.name,
                        artists:obj.artists,
                        album:obj.album,
                        votes:1
                    });
                }

                location.votes = _.sortBy(location.votes, function (track) {
                    return track.votes * -1;  // inverse sort order
                });

                collection.update({locationId:location.locationId}, location, {upsert:true}, function (err, obj) {
                    if (err) {
                        console.error('Error saving vote.', err);
                        (options && options.error && options.error(err));
                    }
                    else {
                        (options && options.success && options.success(location));
                    }
                });
            });
            // collection.findAndModify({locationId:locationId }, [['trackName',1]],
            //   {$inc: {voteCount: 1}}, {upsert:true}, function(err, doc) {
            //     if (err) {
            //       console.log('error adding vote for locationId ' + locationId, err);
            //       if (options && options.error) options.error(err);
            //     }
            //     else {
            //       if (options && options.success) options.success(doc);
            //     }
            //   });

        });
    },

    getVotes:function (obj, options) {
        var locationId = obj.locationId;
        var limit = obj.limit || 0;
        var excludePlayed = obj.excludePlayed || false;
        this.db.collection(PLAYLISTS_COLLECTION, function (err, collection) {
            if (err) {
                console.error('Error fetching collection (name=' + PLAYLISTS_COLLECTION + ')');
                (options && options.error && options.error(err));
                return;
            }

            if (!collection) {
                (options && options.success && options.success(collection));
                return;
            }

            collection.findOne({locationId:locationId}, function (err, location) {
                if (err) {
                    console.log('error fetching location', err);
                    if (options && options.error) {
                        options.error(err);
                    }
                }
                else {
                    //TODO throw an error if the location doesn't exist?
                    if (!location) {
                        location = {
                            locationId:locationId,
                            votes:[],
                            played:[]
                        };
                    }

                    if (excludePlayed) {
                        location.votes = _.filter(location.votes, function (track) {
                            return !track.played;
                        });
                    }

                    if (limit && limit > 0) {
                        location.votes = location.votes.slice(0, limit);
                    }
                    if (options && options.success) {
                        options.success(location);
                    }
                }
            });
        })
    },

    markAsPlayed:function (obj, options) {
        var self = this;
        this.db.collection(PLAYLISTS_COLLECTION, function (err, collection) {
            if (err) {
                console.error('Error fetching collection (name=' + PLAYLISTS_COLLECTION + ')');
                (options && options.error && options.error(err));
                return;
            }

            console.log('Receving mark as played request: ' + JSON.stringify(obj));
            collection.findOne({locationId:obj.locationId}, function (err, location) {
                if (err) {
                    console.error('Error finding collection.', err);
                    options && options.error && options.error(err);
                    return;
                }

                if (!location) {
                    location = {
                        locationId:obj.locationId
                    };
                }
                if (!location.played) {
                    location.played = [];
                }
                if (!location.votes) {
                    location.votes = [];
                }

                var trackIndex = -1;
                var track = _.find(location.votes, function (track, i) {
                    if (track.trackId == obj.trackId && !track.played) {
                        trackIndex = i;
                        return true;
                    }
                    else {
                        return false;
                    }
                });



                if (track) {
                    console.log('Found track: ' + JSON.stringify(track));
                    track.played = true;
                    location.votes.splice(trackIndex, 1);
                }
                else {
                    track = {
                        trackId:obj.trackId,
                        played:true
                    };
                }

                console.log('pushing track onto played list: ' + JSON.stringify(track));
                location.played.push(track);
                while (location.played > MAX_PLAYED_LENGTH) {
                    location.played.unshift();
                }


                var topTrack = _.sortBy(location.votes, function (track) {
                    return track.votes * -1;
                })[0];

                location.upNext = topTrack;

                collection.update({locationId:location.locationId}, location, {upsert:true}, function (err, obj) {
                    if (err) {
                        console.error('Error marking track as played.', err);
                        (options && options.error && options.error(err));
                    }
                    else {
                        self.getVotes({
                            locationId:location.locationId,
                            limit:5,
                            excludePlayed:true
                        }, options);
                        // (options && options.success && options.success(location));
                    }
                })
            });
        })
    }
}

exports.votingService = votingService;



<file_sep>/voter-app/public/js/collections/SearchCollection.js
// Search Collection
// ===================

// Includes file dependencies
define([ "jquery","backbone","models/SearchModel"], function( $, Backbone, SearchModel) {

    // Extends Backbone.Collection
    var Collection = Backbone.Collection.extend( {

        // The Collection constructor
        initialize: function() {

        },

        isEmpty:function(){
            return (this.models.length < 1);
        },

        type:"Search",

        // Sets the Collection model property to be a Search Model
        model: SearchModel,

        parse: function(response){
            return response.playlist;
        }
    } );

    // Returns the Collection class
    return Collection;

} );<file_sep>/README.md
musocracy
=========
A simple node-backed mobile first Web Applciation that allows patrons to search for, and vote on the music being played.  


# Node Application
1. Install nodejs
2. Install nodemon as global dependency
    * `npm install -g nodemon`
3. Install app dependencies: from inside voter-app directory:
    * `npm install`
    * This will install all package dependencies defined in the packages.json file into a node_modules folder
4. Run application
    * `node app.js`
    * Starts an http server on port 3000
5. Monitor Hogan templates for changes
    * `grunt watch:templates`

# Voter Application Structure
* *app.js*: root app server
* */public/*: all client side resources
* */views*: views to be rendered server-side (static HTML for right now, but could be Mustache/Jade templates)
* */routes/*: all request handlers

# API
* `GET /search/track?q=QUERY`: search for tracks
* `GET /search/artist?q=QUERY`: search for artists
* `GET /search/album?q=QUERY`: search for albums* 
* `GET /lookup?uri=SPOTIFY_URI`: lookup metadata for specific artist, album, or track
    * ex: `GET /lookup?uri=spotify:track:6NmXV4o6bmp704aPGyTVVG`
* `POST /location/{id}/votes/{trackId}` : vote for a track at a specific location
* `GET /location/{id}/votes` : returns all votes for a given location
* `GET /location/{id}` : returns locationId if it exists
<file_sep>/voter-app/public/js/models/TrackModel.js
define(['backbone'],
    function (Backbone) {
        return Backbone.Model.extend({
            initialize:function() {
                if (this.attributes.trackId) {
                    this.set('domId', this.attributes.trackId.replace(/:/g, '_'));
                }
            }
        });
    }
);<file_sep>/voter-app/public/js/models/LocationModel.js
define(['underscore', 'backbone', 'collections/VotesCollection'],
    function (_, Backbone, VotesCollection) {
        return Backbone.Model.extend({
            initialize:function (attributes, options) {
                this.locationId = options.locationId;
                this.on('reset', this.onReset, this);
            },

            url:function () {
                return '/location/' + this.locationId;
            },

            // parse:function (response) {

            // },

            onReset:function () {
                this.set('votes', new VotesCollection(this.attributes.votes));
            },

            getVotes:function () {
                var votes = this.get('votes');
                if (!(votes instanceof VotesCollection)) {
                    votes = new VotesCollection(votes);
                    this.set('votes', votes);
                }
                return votes;
            },

            getNowPlaying:function () {
                return this.get('nowPlaying');
            },

            getUpNext:function () {
                return this.get('upNext');
            }

        });

    });<file_sep>/voter-app/public/js/views/QueueView.js
// Category View
// =============

// Includes file dependencies
define(["jquery", "underscore", "backbone", "templates", "models/QueueModel", "views/ListItemView", "collections/VotesCollection",
"collections/SpotifySearchCollection", "models/LocationModel", "util/persist", "util/dispatcher"],
function( $, _, Backbone, templates, QueueModel, ListItemView, VotesCollection, SpotifySearchCollection, LocationModel, persist, dispatcher) {

    // Extends Backbone.View
    var THREE_HOURS_IN_MS = 10800000;

    var timeoutId;
    return Backbone.View.extend({
        events: {
            'keyup .search input':'onKeyPress',
            'click .location-queue .track:not(.disabled)':'onTrackClick',
            'click .spotify-results .track:not(.disabled)':'onSearchResultClick',
            'click .clear-search':'onClearSearch'
        },

        // The View Constructor
        initialize: function() {
            this.locationId = this.options.locationId;
            this.locationModel = this.options.locationModel;

            dispatcher.on(dispatcher.events.REFRESH, this.onRefresh, this);
        },

        // Renders all of the Category models on the UI
        render: function() {
            this.$el.html(templates.queue.render());

            this.searchCollection = new SpotifySearchCollection();

//            this.locationModel = new LocationModel({}, {
//                locationId: this.locationId
//            });

            this.locationModel.fetch({
                success:$.proxy(this.onLocationFetched, this)
            });

            // Maintains chainability
            return this;
        },

        renderQueue:function (tracks) {
            var html = '';
            _.each(tracks, function (track) {

//                track.set('disabled', '');
                var index = $.inArray(track.get('trackId'), _.pluck(amplify.store('previousVotes'), 'trackId'));
                if (index >= 0){
                    var oldVote = amplify.store('previousVotes')[index];
                    if(oldVote){
                        if((new Date()) - (new Date(oldVote.voteTime)) < THREE_HOURS_IN_MS){
                            track.set('disabled', 'disabled');
                        }
                    }
                }
//                track.set('domId', track.get('trackId').replace(":", "_"));

                html += templates.track.render(track.attributes);
            });

            this.$('.location-queue').html(html);
        },

        onRefresh:function () {
            this.locationModel.fetch({
                success:$.proxy(this.onLocationFetched, this)
            });
        },

        onLocationFetched: function() {
            var votesCollection = this.locationModel.getVotes();
            var query = this.$('.search input').val();
            if (query) {
                this.filterVotesCollection(query);
            }
            else {
                this.renderQueue(votesCollection.models);
            }
        },

        onKeyPress:function(e) {
            window.clearTimeout(timeoutId);
            var query = this.$('.search input').val();
            if (e && e.which === 13) {
                this.search();
            } else if (e && e.which === 8 && query == ''){
                console.log('backspace pressed with no no content in search field, clearing search results');
                this.onClearSearch();
            }
            else {
                timeoutId = setTimeout($.proxy(this.search, this), 500);
            }

            this.filterVotesCollection(query);
        },

        onTrackClick:function(e) {
            (e && e.preventDefault());

            var $currentTarget = this.$(e.currentTarget);
            var trackId = $currentTarget.data('trackid');
            var votes = this.locationModel.getVotes();
            var track = votes.getTrackById(trackId);

            if (track) {
                this.addVote(track);
            }
        },

        onSearchResultClick:function(e) {
            (e && e.preventDefault());
            var $currentTarget = this.$(e.currentTarget);
            $currentTarget.remove();

            var trackId = $currentTarget.data('trackid');
            var track = this.searchCollection.getTrackById(trackId);

            if (track) {
                var votesCollection = this.locationModel.getVotes();
                votesCollection.add(track);
                this.addVote(track);
            }
        },

        addVote:function(track) {
            var self = this;

            var votes = track.get('votes');
            if (!votes) {
                votes = 1;
            }
            else{
                votes++;
            }
            track.set('votes', votes);
            track.set('disabled', 'disabled');

            this.onLocationFetched();
            this.highlightTrack(track);
            this.disableTrack(track);

            persist.vote(track, {
                success:function(resp) {
//                    console.log('Voting complete.', resp);
//                        dispatcher.trigger(dispatcher.events.REFRESH);
                    self.locationModel = new LocationModel(resp, {locationId: self.locationId});
//                    self.onLocationFetched();
//                    self.highlightTrack(track);
                }
            });

        },

        highlightTrack:function(track) {
            var domId = track.get('domId');
            var $track = this.$('#' + domId);
            var origColor = $track.css('background-color');
            $track.animate({
                'background-color': "#C7F5C1"
            }, 250);

            setTimeout(function() {
                $track.animate({
                    'background-color': origColor
                }, 750);
            }, 500);
        },

        disableTrack:function(track) {
            var domId = track.get('domId');
            var $track = this.$('#' + domId);

            $track.addClass('disabled');
        },

        onClearSearch:function(e){

            //Clear the search text
            this.$('.search input').val('');
            //clear the spotify results div
            this.$('.spotify-results').empty();
            //Redraw the queue
            this.renderQueue(this.locationModel.getVotes().models);

        },

        search:function(){
            var query = this.$('.search input').val();
            console.log('Executing search on: ' + query);

            var $spotifyResults = this.$('.spotify-results');

            if(query){
                   $spotifyResults.html('Loading....');
                this.searchCollection.url = '/location/' + this.locationId + '/search/track?q=' + query;
                var self = this;
                this.searchCollection.fetch()
                    .done(function(){
                        var html = '';
                        if(!self.searchCollection.isEmpty()){
                            self.searchCollection.each(function(model) {
                                html += templates.track.render(model.attributes);
                            });
                        } else {
                            html = 'No Results Found, please try again.'
                        }
                        $spotifyResults.html(html);
                    }).fail(function(){
                        console.log('Handling failed spotify search, redrawing...');
                        $spotifyResults.html('Search failed, please try again.');

                    });
            } else {
                $spotifyResults.empty();
            }

        },

        filterVotesCollection:function (query) {
            var votesCollection = this.locationModel.getVotes();
            var models = votesCollection.getFilteredResults(query);
            this.renderQueue(models);
        }

    } );

} );<file_sep>/voter-app/public/js/mobile.js
// Sets the require.js configuration for your application.
require.config({

    // 3rd party script alias names (Easier to type "jquery" than "libs/jquery-1.8.2.min")
    paths:{

        // Core Libraries
        "jquery":"lib/jquery-1.9.1",
        "underscore":"lib/lodash",
        "backbone":"lib/backbone",
        "amplify":"lib/amplify.min",
        "bootstrap":"lib/bootstrap"
    },

    // Sets the configuration for your third party scripts that are not AMD compatible
    shim:{

        "backbone":{
            "deps":[ "underscore", "jquery" ],
            "exports":"Backbone"  //attaches "Backbone" to the window object
        }

    } // end Shim Configuration

});

// Includes File Dependencies
require([ "jquery", "backbone", "routers/mobileRouter", "collections/QueueCollection"], function ($, Backbone, MobileRouter, QueueCollection) {


    this.router = new MobileRouter();

    $("#header-search").click(function () {
        $("#header-search-form").removeClass('hidden');
        $("#home-link").css('display', 'none');
        $("#header-song-search").focus();
    });


    //TODO make this on search clear
    $("#header-song-search").blur(function () {
        $("#header-search-form").addClass('hidden');
        $("#home-link").css('display', 'block');
    });

});


<file_sep>/voter-app/public/js/app.js
// Sets the require.js configuration for your application.
require.config({

    // 3rd party script alias names (Easier to type "jquery" than "libs/jquery-1.8.2.min")
    paths:{

        // Core Libraries
        "hogan":"lib/hogan/hogan-2.0.0.amd",
        "jquery":"lib/jquery-1.9.1",
        "jqueryui":"lib/jquery-ui-1.10.2.custom.min",
        "underscore":"lib/lodash",
        "backbone":"lib/backbone",
        "amplify":"lib/amplify.min",
        "bootstrap":"lib/bootstrap"
    },

    // Sets the configuration for your third party scripts that are not AMD compatible
    shim:{
        jqueryui: {
            deps: ["jquery"]
        },
        backbone:{
            "deps":[ "underscore", "jquery" ],
            "exports":"Backbone"  //attaches "Backbone" to the window object
        },
        amplify:{
            exports:"amplify"
        }

    } // end Shim Configuration

});

// Includes File Dependencies
require([ "jquery", "jqueryui", "backbone", "hogan", "routers/Router" ],
    function ($,$ui, Backbone, Hogan, Router) {


        this.router = new Router({
            el:$('body')
        });
    });


<file_sep>/voter-app/public/js/views/InfoFooterView.js
define(['jquery', 'underscore', 'backbone', 'templates'],
    function ($, _, Backbone, templates) {
        return Backbone.View.extend({
            initialize:function() {
//                this.nowPlaying = this.options.nowPlaying;
//                this.upNext = this.options.upNext;

                this.locationModel = this.options.locationModel;
                _.each(['change'], function(eventName) {
                    this.locationModel.on(eventName, this.render, this);
                }, this);
            },

            render:function() {
                var nowPlaying = this.locationModel.getNowPlaying(),
                    upNext = this.locationModel.getUpNext();
                this.$el.html(templates.infoFooter.render({
                    nowPlayingTrackName:(nowPlaying && nowPlaying.name) || '',
                    upNextTrackName:(upNext && upNext.name) || ''
                }));
                return this;
            }
        });
    });<file_sep>/voter-app/routes/lookup.js

var url = require('url');
var http = require('http');
var Q = require('q');

var BASE_URL = 'http://ws.spotify.com/';
var LOOKUP_URL = BASE_URL + 'lookup/1/.json';

exports.lookup = function(req, resp) {
  // console.log('searching by track');
  var uri = new String(req.query.uri);
  if (!uri) {
    resp.writeHead(400, {'Content-Type':'application/json'});
    resp.write(JSON.stringify({errorCode:'error.request.nouri'}));
    return;
  }

  var requestUrl = LOOKUP_URL + '?uri=' + uri;
  if (uri instanceof String && uri.indexOf('spotify:artist') >= 0) {
    requestUrl += '&extras=album'
  }
  else if (uri instanceof String && uri.indexOf('spotify:album') >= 0) {
    requestUrl += '&extras=track'
  }

  // console.log('Request Url: ' + requestUrl);
  http.get(requestUrl, function(response){
    // console.log('Got Response', response);
    var respData = '';
    response.setEncoding('utf8');
    response.on('data', function(chunk) {
      // console.log('Data Received: ' + chunk);
      respData += chunk;
    });

    response.on('end', function() {
      var respObj = JSON.parse(respData);
      // console.log(respData);

      resp.writeHead(200, {'Content-Type':'application/json'});
      resp.write(JSON.stringify(respObj));
      resp.end();
    });
  })
}<file_sep>/voter-app/public/js/views/TracksView.js
define(['jquery', 'underscore', 'backbone', 'templates'],
  function($, _, Backbone, templates) {
    return Backbone.View.extend({
      initialize:function() {
        this.tracks = new Backbone.Collection();
      },

      render:function() {
        var html = '';
        _.each(this.tracks, function(track) {
          html += templates.track(track.attributes);
        });

        this.$el.html(html);

        return this;
      }
    })
  })<file_sep>/voter-app/services/util/filter.js
var locationService = require('../locationService').locationService;
var _ = require('underscore');

/**
 *
 * @param opts
 */
exports.filter = function(opts) {
    var locationId = opts.locationId;
    var results = opts.results;
    var fnSuccess = opts.success;
    var fnError = opts.error;

    locationService.findLocation(locationId, {
        success:function(location) {
            var votes = location.votes;
            var votesByTrackId = _.reduce(votes, function(memo, vote) {
                memo[vote.trackId] = vote;
                return memo;
            }, {});

            var filteredResults = _.filter(results, function(result) {
                if (votesByTrackId[result.trackId]) {
                    return false;
                }
                return true;
            });

            fnSuccess(filteredResults);
        },
        error:fnError
    });
};

<file_sep>/voter-app/public/js/util/dispatcher.js
define(['underscore', 'backbone'],
    function (_, Backbone) {
        return _.extend({}, Backbone.Events, {
            events:{
                SEARCH:'SEARCH',
                CLEAR_SEARCH:'CLEAR_SEARCH',
                REFRESH: 'REFRESH',
                LOCATION_UPDATED: 'LOCATION_UPDATED'
            }
        });
    });<file_sep>/voter-app/public/js/views/LocationView.js
// Category View
// =============

// Includes file dependencies
define([ "jquery", "backbone","amplify" ], function( $, Backbone, Amplify) {


    var QueueView = Backbone.View.extend( {

        // The View Constructor
        initialize: function() {

        },

        amplify:Amplify,

        render: function() {

            $("#location-submit").removeClass("hidden");
            $("#song-search").addClass('hidden');
            $("#results").empty();

            var curLocation = amplify.store('locationId');

            if (curLocation){
                $('label#location-label').html('Currently at: ' + curLocation);
            }

            $("#location-submit").submit(function(){
                var location = $("#location-input").val();
                if (location) {
                    router.persist.lookupLocation(location);
                } else {
                    console.log('No value found, enter a value and try again.');
                    $('label#location-label').html('Enter a value and try again.');
                }

                return false;
            });

            var queueView = this;

            // Maintains chainability
            return this;

        }

    } );

    // Returns the View class
    return QueueView;

} );