├── Procfile
├── screen.png
├── config.json
├── routes
└── index.js
├── package.json
├── README.md
├── .gitignore
├── newrelic.js
├── LICENSE
├── views
├── index.jade
└── layout.jade
├── app.js
├── helpers
└── songify.js
└── public
├── javascripts
└── app.js
└── stylesheets
└── style.css
/Procfile:
--------------------------------------------------------------------------------
1 | web: node app.js
2 |
--------------------------------------------------------------------------------
/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karan/Singular/master/screen.png
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "twitter": {
3 | "key": "",
4 | "secret": "",
5 | "oauth_token": "",
6 | "oauth_token_secret": ""
7 | },
8 | "maps": ""
9 | }
10 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 |
2 | /*
3 | * GET home page.
4 | */
5 |
6 | var config = require('./../config.json');
7 |
8 | exports.index = function(req, res){
9 | res.render('index', { api: config.maps });
10 | };
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "application-name",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "start": "node app.js"
7 | },
8 | "dependencies": {
9 | "express": "3.5.0",
10 | "jade": "*",
11 | "request": "^2.39.0",
12 | "socket.io": "",
13 | "twit": "*",
14 | "newrelic": "*"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Singular
2 | ========
3 |
4 | 
5 |
6 | Maps the location of tweets that contain songs in real time.
7 |
8 | Node.js app with express.js. No db needed.
9 |
10 | ### Heroku
11 |
12 | ```bash
13 | # edit stuff, add keys to config.json
14 | $ git add .
15 | $ git commit -m "helpful commit message"
16 | $ git push heroku master
17 | ```
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Deployed apps should consider commenting this line out:
24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
25 | node_modules
26 |
--------------------------------------------------------------------------------
/newrelic.js:
--------------------------------------------------------------------------------
1 | /**
2 | * New Relic agent configuration.
3 | *
4 | * See lib/config.defaults.js in the agent distribution for a more complete
5 | * description of configuration variables and their potential values.
6 | */
7 | exports.config = {
8 | /**
9 | * Array of application names.
10 | */
11 | app_name : ['Singular'],
12 | /**
13 | * Your New Relic license key.
14 | */
15 | license_key : '',
16 | logging : {
17 | /**
18 | * Level at which to log. 'trace' is most useful to New Relic when diagnosing
19 | * issues with the agent, 'info' and higher will impose the least overhead on
20 | * production applications.
21 | */
22 | level : 'info'
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Karan Goel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/views/index.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 |
5 | #map-canvas
6 | #allsongs
7 | #footer
8 | #logo
9 | #tagline
10 | span Mapping the World's Sounds
11 | #social
12 | a(href="#")
13 | span.fa.fa-question.share.sbg-help
14 | //- a(href="http://github.com/karan/Singular", target="_blank")
15 | //- span.fa.fa-github.share.sbg-github
16 | a(href="https://twitter.com/intent/tweet?text=Look+at+the+map+of+world's+sounds&url=http://singular.goel.im&via=KaranGoel", target="_blank")
17 | span.fa.fa-twitter.share.sbg-twitter
18 | a(href="", onclick="window.open('https://www.facebook.com/sharer/sharer.php?u='+encodeURIComponent(location.href),'facebook-share-dialog','width=626,height=436');return false;", target="_blank")
19 | span.fa.fa-facebook.share.sbg-facebook
20 | .md-modal.md-effect
21 | .md-content
22 | h3 Singular - Mapping the World's Sounds
23 | p Realtime mapping of songs shared on Twitter.
24 | button.md-close Try Singular now
25 | .modal-footer
26 | | Project by
27 | a(href='http://www.goel.im', target='_blank') Karan Goel
28 | | .
29 | br
30 | a(href='http://eepurl.com/SRIPT', target='_blank') Leave your e-mail here and stay tuned about my projects.
31 |
--------------------------------------------------------------------------------
/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang="en")
3 | head
4 | title Singular - Mapping the World's Sounds
5 | meta(charset="utf-8")
6 |
7 | // Stylesheets
8 | link(rel='stylesheet', href='/stylesheets/style.css')
9 | link(rel='stylesheet', href='//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css')
10 |
11 | // Scripts
12 | script(src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js")
13 | script(src="/socket.io/socket.io.js")
14 | script(src="https://maps.googleapis.com/maps/api/js?sensor=false")
15 | script(src='/javascripts/app.js')
16 |
17 | //- Facebook open graph
18 | meta(property="og:url", content="http://singular.goel.im/")
19 | meta(property="og:title", content="Singular - Mapping the World's Sounds")
20 | meta(property="og:description", content="Singular lets you look at the map of music shared by tweeps. A project by @KaranGoel.")
21 | meta(property="og:image", content="http://singular.goel.im/screen.png")
22 |
23 | meta(name="viewport", content="width=device-width, initial-scale=1.0")
24 |
25 | body
26 | block content
27 |
28 | script.
29 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
30 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
31 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
32 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
33 |
34 | ga('create', 'UA-9509587-32', 'auto');
35 | ga('send', 'pageview');
36 | script.
37 | !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
38 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | require('newrelic');
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var express = require('express');
8 | var routes = require('./routes');
9 | var path = require('path');
10 | var config = require('./config.json');
11 |
12 | var twit = require('twit');
13 |
14 | var app = express();
15 |
16 | var server = require('http').createServer(app),
17 | io = require('socket.io').listen(server);
18 |
19 |
20 | // all environments
21 | app.set('port', process.env.PORT || 8888);
22 | app.set('views', path.join(__dirname, 'views'));
23 | app.set('view engine', 'jade');
24 | app.use(express.favicon());
25 | app.use(express.logger('dev'));
26 | app.use(express.json());
27 | app.use(express.urlencoded());
28 | app.use(express.methodOverride());
29 | app.use(app.router);
30 | app.use(express.static(path.join(__dirname, 'public')));
31 |
32 |
33 | // development only
34 | if ('development' == app.get('env')) {
35 | app.use(express.errorHandler());
36 | }
37 |
38 | // socket hang up exception
39 | process.on('uncaughtException', function (exception) {
40 | console.log(exception);
41 | });
42 |
43 |
44 | app.get('/', routes.index);
45 |
46 |
47 | var T = new twit({
48 | consumer_key: config.twitter.key,
49 | consumer_secret: config.twitter.secret,
50 | access_token: config.twitter.oauth_token,
51 | access_token_secret: config.twitter.oauth_token_secret
52 | });
53 |
54 | io.sockets.on('connection', function (socket) {
55 |
56 | console.log("client connected");
57 |
58 | var stream = T.stream('statuses/filter', {
59 | track: ['snd.sc', 'soundcloud', 'listen', 'hear', 'song', 'youtu.be',
60 | 'y2u.be', 'goo.gl', 'youtube', 'music', 'artist', 'playlist',
61 | 'album', 'stream'],
62 | locations: ['-180', '-90', '180', '90']
63 | });
64 |
65 | stream.on('tweet', function(tweet) {
66 | require('./helpers/songify').songify(tweet, function(obj) {
67 | if (obj) {
68 | console.log(obj);
69 | io.sockets.emit('newTweet', obj);
70 | }
71 | });
72 | });
73 |
74 | });
75 |
76 |
77 | server.listen(app.get('port'));
78 | console.log("Server starter on port " + app.get('port'));
79 |
--------------------------------------------------------------------------------
/helpers/songify.js:
--------------------------------------------------------------------------------
1 | // Takes a tweets object and calls the callback with modified tweet object if
2 | // it's a valid SC/YT tweets, null otherwise.
3 |
4 | var request = require('request');
5 |
6 | module.exports.songify = function(tweet, callback) {
7 | if (tweet === undefined || tweet.entities.urls.length === 0) {
8 | return callback(null); // no urls
9 | }
10 |
11 | if (tweet.coordinates === null && tweet.place === null) {
12 | return callback(null);
13 | }
14 |
15 | var url = tweet.entities.urls[0].expanded_url.toLowerCase();
16 |
17 | if ((url.indexOf("soundcloud.com") === -1) &&
18 | (url.indexOf("snd.sc") === -1) &&
19 | (url.indexOf("youtube.com") === -1) &&
20 | (url.indexOf("youtu.be") === -1)) {
21 | return callback(null);
22 | }
23 |
24 | var obj = {};
25 | obj.tweet_id = tweet.id_str;
26 | obj.tweeted_by = tweet.user.screen_name;
27 | obj.tweeted_by_photo = tweet.user.profile_image_url || '';
28 | obj.song_url = tweet.entities.urls[0].expanded_url.toLowerCase();
29 |
30 | if (tweet.coordinates) {
31 | obj.coordinates = tweet.coordinates.coordinates;
32 | } else if (tweet.place) {
33 | obj.coordinates = centerPoint(tweet.place.bounding_box.coordinates[0]);
34 | } else {
35 | return callback(null);
36 | }
37 |
38 | if ((obj.song_url.indexOf("soundcloud.com") === -1) &&
39 | (obj.song_url.indexOf("snd.sc") === -1)) {
40 | obj.song_source = 'yt';
41 | } else {
42 | obj.song_source = 'sc';
43 | }
44 |
45 | if (obj.song_source === 'sc') {
46 | var url = 'https://api.soundcloud.com/resolve.json?consumer_key=c2c242fd7d60a1165e6a0924f8e70138&url='+obj.song_url;
47 | request({url: url, timeout: 3000, followRedirect: true},
48 | function(error, response, track) {
49 | if (!error && response.statusCode == 200) {
50 | track = JSON.parse(track);
51 | obj.artwork_url = track.artwork_url;
52 | obj.song_id = track.id;
53 | obj.song_title = track.title;
54 | return callback(obj);
55 | }
56 | });
57 | } else {
58 | var ytMatches = obj.song_url.match(/(youtu\.be\/|v=)([^&]+)/);
59 | if (ytMatches) {
60 | ytID = ytMatches[2];
61 | var url = 'http://gdata.youtube.com/feeds/api/videos?q='+ytID+'&max-results=1&v=2&alt=jsonc';
62 | request({url: url, timeout: 3000, followRedirect: true},
63 | function(error, response, track) {
64 | if (!error && response.statusCode == 200) {
65 | try {
66 | track = JSON.parse(track);
67 | } catch(err) {
68 | console.log(err);
69 | return callback(null);
70 | }
71 | if (track && track.data && track.data.items &&
72 | track.data.items[0].category === 'Music') {
73 |
74 | track = track.data.items[0];
75 |
76 | obj.artwork_url = track.thumbnail.hqDefault;
77 | obj.song_id = track.id;
78 | obj.song_title = track.title;
79 | return callback(obj);
80 | }
81 | }
82 | });
83 | }
84 | }
85 | }
86 |
87 | var centerPoint = function(coords) {
88 | var centerPointX, centerPointY, coord, i, _len;
89 | centerPointX = centerPointY = 0;
90 | for (i = 0, _len = coords.length; i < _len; i++) {
91 | coord = coords[i];
92 | centerPointX += coord[0];
93 | centerPointY += coord[1];
94 | }
95 | return [centerPointX / coords.length, centerPointY / coords.length];
96 | };
97 |
--------------------------------------------------------------------------------
/public/javascripts/app.js:
--------------------------------------------------------------------------------
1 | var map;
2 |
3 | $(document).ready(function() {
4 |
5 | $('.md-modal').addClass('md-show');
6 | $('.sbg-help').on('click', function(e) {
7 | $('.md-modal').addClass('md-show');
8 | });
9 | $('.md-close').on('click', function(e) {
10 | $('.md-modal').removeClass('md-show');
11 | });
12 |
13 | var mapOptions;
14 | mapOptions = void 0;
15 | mapOptions = {
16 | zoom: 2,
17 | center: new google.maps.LatLng(5, -30),
18 | mapTypeId: google.maps.MapTypeId.ROADMAP,
19 | zoomControl: true,
20 | disableDefaultUI: true
21 | };
22 |
23 | map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions);
24 |
25 | // var host = location.origin.replace(/^http/, 'ws');
26 | var socket = io();
27 | console.log(socket);
28 |
29 | socket.on('newTweet', function (data) {
30 | console.log(data);
31 | var marker = addToMap(data);
32 | addToSidebar(data, marker);
33 | });
34 |
35 | });
36 |
37 |
38 | function addToMap(data) {
39 | var coords = data.coordinates; // lng, lat
40 | var lat = coords[1];
41 | var lng = coords[0];
42 |
43 | var zoom = map.getZoom();
44 | var marker = new google.maps.Marker({
45 | position: new google.maps.LatLng(lat, lng),
46 | title: data.song_title,
47 | raiseOnDrag: false,
48 | draggable: false,
49 | animation: google.maps.Animation.DROP,
50 | map: map
51 | });
52 |
53 | marker.setIcon(new google.maps.MarkerImage(
54 | "http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|FF0000",
55 | null, null, null, new google.maps.Size(15, 24)
56 | ));
57 |
58 | // create the tooltip
59 | createInfoWindow(marker, data);
60 | return marker;
61 | }
62 |
63 | function addToSidebar(data, marker) {
64 | console.log("adding to sidebar");
65 | var userImg = '';
66 | var username = '' + data.tweeted_by + '';
67 | var playerCode;
68 | if (data.song_source === 'sc') {
69 | playerCode = '';
70 | } else {
71 | playerCode = '';
72 | }
73 | var html = '
@' + data.tweeted_by + '
' + data.song_title + '