├── client ├── src │ ├── img │ │ ├── favicon.png │ │ ├── rdio-logo.png │ │ ├── seatgeek102x31.png │ │ ├── GooglePlayBadge_45.png │ │ ├── bandsintown135x33.png │ │ ├── soundcloud105x66.png │ │ ├── spotify-logo-primary-horizontal-light-background-rgb2.png │ │ ├── rdio.svg │ │ ├── twitter_alt.svg │ │ ├── loading.svg │ │ ├── spotify.svg │ │ ├── instagram.svg │ │ ├── bkgr.svg │ │ └── iTunesBadge135x40.svg │ ├── bower.json │ ├── index.css │ ├── about.html │ └── index.html ├── config │ └── _example.json ├── package.json └── gulpfile.js ├── mobile ├── resources │ ├── icon.png │ ├── splash.png │ ├── ios │ │ ├── icon │ │ │ ├── icon.png │ │ │ ├── icon-40.png │ │ │ ├── icon-50.png │ │ │ ├── icon-60.png │ │ │ ├── icon-72.png │ │ │ ├── icon-76.png │ │ │ ├── icon@2x.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-50@2x.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-72@2x.png │ │ │ ├── icon-76@2x.png │ │ │ ├── icon-small.png │ │ │ └── icon-small@2x.png │ │ └── splash │ │ │ ├── Default-667h.png │ │ │ ├── Default-736h.png │ │ │ ├── Default~iphone.png │ │ │ ├── Default@2x~iphone.png │ │ │ ├── Default-568h@2x~iphone.png │ │ │ ├── Default-Landscape-736h.png │ │ │ ├── Default-Landscape~ipad.png │ │ │ ├── Default-Portrait~ipad.png │ │ │ ├── Default-Landscape@2x~ipad.png │ │ │ └── Default-Portrait@2x~ipad.png │ └── android │ │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ │ └── splash │ │ ├── drawable-land-hdpi-screen.png │ │ ├── drawable-land-ldpi-screen.png │ │ ├── drawable-land-mdpi-screen.png │ │ ├── drawable-land-xhdpi-screen.png │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-land-xxhdpi-screen.png │ │ ├── drawable-land-xxxhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png └── config.xml ├── .gitignore ├── README.md ├── server ├── config │ └── _example.json ├── package.json └── index.js └── LICENSE /client/src/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/client/src/img/favicon.png -------------------------------------------------------------------------------- /mobile/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/icon.png -------------------------------------------------------------------------------- /mobile/resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/splash.png -------------------------------------------------------------------------------- /client/src/img/rdio-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/client/src/img/rdio-logo.png -------------------------------------------------------------------------------- /client/src/img/seatgeek102x31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/client/src/img/seatgeek102x31.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | *~ 4 | mobile/platforms 5 | mobile/plugins 6 | mobile/www 7 | client/dist 8 | 9 | -------------------------------------------------------------------------------- /client/src/img/GooglePlayBadge_45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/client/src/img/GooglePlayBadge_45.png -------------------------------------------------------------------------------- /client/src/img/bandsintown135x33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/client/src/img/bandsintown135x33.png -------------------------------------------------------------------------------- /client/src/img/soundcloud105x66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/client/src/img/soundcloud105x66.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /mobile/resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /mobile/resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /mobile/resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /mobile/resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /mobile/resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /mobile/resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /mobile/resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /mobile/resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /mobile/resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /mobile/resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /mobile/resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /mobile/resources/ios/splash/Default-Landscape-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/splash/Default-Landscape-736h.png -------------------------------------------------------------------------------- /mobile/resources/ios/splash/Default-Landscape~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/splash/Default-Landscape~ipad.png -------------------------------------------------------------------------------- /mobile/resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /mobile/resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /mobile/resources/ios/splash/Default-Landscape@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/splash/Default-Landscape@2x~ipad.png -------------------------------------------------------------------------------- /mobile/resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-land-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-land-hdpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-land-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-land-ldpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-land-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-land-mdpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-land-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-land-xhdpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-land-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-land-xxhdpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-land-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-land-xxxhdpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /mobile/resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/mobile/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # music-tonight 2 | The Music Tonight web and mobile applications help you discover local musicians. 3 | 4 | Check out the [live version here](http://musictonight.millstonecw.com). 5 | -------------------------------------------------------------------------------- /client/config/_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "GOOGLE_ANALYTICS_ID": "REPLACEME", 3 | "SPOTIFY_CLIENT_ID": "REPLACEME", 4 | "GOOGLE_API_KEY": "REPLACEME", 5 | "APP_SERVER_URL": "REPLACEME" 6 | } 7 | -------------------------------------------------------------------------------- /client/src/img/spotify-logo-primary-horizontal-light-background-rgb2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschanely/music-tonight/HEAD/client/src/img/spotify-logo-primary-horizontal-light-background-rgb2.png -------------------------------------------------------------------------------- /server/config/_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "MYSQL": { 3 | "host": "REPLACEME", 4 | "user": "REPLACEME", 5 | "password": "REPLACEME", 6 | "database": "REPLACEME", 7 | "connectionLimit": 400, 8 | "waitForConnections": false 9 | }, 10 | "SEATGEEK_EVENTS_PREFIX": "http://api.seatgeek.com/2/events?", 11 | "SPOTIFY_ARTIST_SEARCH_PREFIX": "https://api.spotify.com/v1/search?type=artist&", 12 | "SPOTIFY_ARTIST_PREFIX": "https://api.spotify.com/v1/artists/" 13 | } 14 | -------------------------------------------------------------------------------- /client/src/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "musictonight", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "Phillip Schanely " 6 | ], 7 | "license": "MIT", 8 | "private": true, 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "dependencies": { 17 | "angularjs": "~1.3.14", 18 | "angular-animate": "~1.3.14", 19 | "animate.css": "~3.2.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "musictonight", 3 | "version": "0.0.0", 4 | "description": "MusicTonight Server", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "dependencies": { 11 | "bit-array": "^0.2.2", 12 | "mysql2": "^0.15.2", 13 | "node-uuid": "^1.4.3", 14 | "q": "^1.0.1", 15 | "q-io": "^1.12.0", 16 | "rdio": "^3.1.0", 17 | "request": "^2.34.0", 18 | "require-dir": "^0.1.0", 19 | "restify": "^2.7.0", 20 | "restify-cookies": "^0.1.1", 21 | "soundclouder": "^0.8.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/src/img/rdio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "musictonight-client", 3 | "version": "0.0.0", 4 | "description": "Web and mobile client for Music Tonight", 5 | "main": "gulpfile.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/pschanely/music-tonight" 12 | }, 13 | "author": "Phillip Schanely", 14 | "license": "BSD 2-clause", 15 | "bugs": { 16 | "url": "https://github.com/pschanely/music-tonight/issues" 17 | }, 18 | "homepage": "https://github.com/pschanely/music-tonight", 19 | "devDependencies": { 20 | "gulp": "^3.8.11", 21 | "gulp-base64": "^0.1.2", 22 | "gulp-css-inline-images": "^0.1.1", 23 | "gulp-frep": "^0.1.3", 24 | "gulp-imagemin": "^2.2.1", 25 | "gulp-inline": "0.0.10", 26 | "gulp-inline-image": "0.0.4", 27 | "gulp-inline-image-html": "^0.2.1", 28 | "gulp-inline-source": "^1.2.0", 29 | "gulp-minify-css": "^1.0.0", 30 | "gulp-print": "^1.1.0", 31 | "gulp-uglify": "^1.1.0", 32 | "main-bower-files": "^2.6.2", 33 | "require-dir": "^0.1.0" 34 | }, 35 | "dependencies": { 36 | "soundclouder": "^0.8.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Phillip Schanely 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /client/src/img/twitter_alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /client/src/img/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var requireDir = require('require-dir'); 3 | var configFiles = requireDir('./config'); 4 | var frep = require('gulp-frep'); 5 | var imagemin = require('gulp-imagemin'); 6 | var mainBowerFiles = require('main-bower-files'); 7 | var print = require('gulp-print'); 8 | var inline = require('gulp-inline'); 9 | var uglify = require('gulp-uglify'); 10 | var minifyCss = require('gulp-minify-css'); 11 | var inlinesource = require('gulp-inline-source'); 12 | var inlineimg = require('gulp-inline-image-html'); 13 | 14 | var config = {} 15 | for(var fileName in configFiles) { 16 | var configObj = configFiles[fileName]; 17 | for (var attrname in configObj) { config[attrname] = configObj[attrname]; } 18 | } 19 | 20 | var config_patterns = [ 21 | { pattern: /GOOGLE_ANALYTICS_ID/g, replacement: config.GOOGLE_ANALYTICS_ID }, 22 | { pattern: /APP_SERVER_URL/g, replacement: config.APP_SERVER_URL }, 23 | { pattern: /SPOTIFY_CLIENT_ID/g, replacement: config.SPOTIFY_CLIENT_ID }, 24 | { pattern: /GOOGLE_API_KEY/g, replacement: config.GOOGLE_API_KEY } 25 | ]; 26 | 27 | function buildto(dir, isweb) { 28 | var html; 29 | if (isweb) { 30 | html = gulp.src('./src/*.html'); 31 | html = html.pipe(inlinesource({compress:true})); 32 | } else { 33 | html = gulp.src(['./src/*.html', './src/*.css']); 34 | } 35 | html.pipe(frep(config_patterns)) 36 | .pipe(gulp.dest(dir)); 37 | 38 | gulp.src(mainBowerFiles({paths:'./src', debugging:false}), { base:'./src/bower_components'}) 39 | .pipe(gulp.dest(dir + '/bower_components')); 40 | 41 | gulp.src('./src/img/*') 42 | .pipe(gulp.dest(dir + '/img')); 43 | 44 | } 45 | 46 | gulp.task('default', function() { 47 | buildto('./dist', true); 48 | }); 49 | 50 | gulp.task('app', function() { 51 | buildto('../mobile/www', false); 52 | }); 53 | 54 | gulp.task('watch', function() { 55 | gulp.watch('./src/*.css', ['default','app']); 56 | gulp.watch('./src/*.html', ['default','app']); 57 | gulp.watch('./src/img/*', ['default','app']); 58 | }); 59 | -------------------------------------------------------------------------------- /client/src/img/spotify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 20 | 21 | -------------------------------------------------------------------------------- /client/src/img/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 53 | 58 | 59 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | /* CSS reset */ 2 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td { 3 | margin:0; 4 | padding:0; 5 | } 6 | html { 7 | margin:0; 8 | padding:0; 9 | background: #eee url(img/bkgr.svg) no-repeat top left; 10 | background-size: 100%; 11 | font-size: 100%; 12 | font-family: Arial, Helvetica, sans-serif; 13 | } 14 | form { 15 | display:inline-block; 16 | } 17 | input { 18 | display:inline-block; 19 | } 20 | a { 21 | display:inline-block; 22 | } 23 | a:hover, button:hover { 24 | background-color: transparent; 25 | } 26 | a:visited, a:link { 27 | color: #353535; 28 | } 29 | .outlined:hover { 30 | background-color: #fff; 31 | } 32 | .outlined { 33 | color: #000000; 34 | border: solid #000000 5px; 35 | text-decoration: none; 36 | } 37 | .big { 38 | font-size: 130%; 39 | } 40 | input.outlined { 41 | padding: 0.3em 0.6em 0.3em 0.6em; 42 | -webkit-border-radius: 0.2em; 43 | -moz-border-radius: 0.2em; 44 | border-radius: 0.2em; 45 | } 46 | button.outlined, a.outlined { 47 | padding: 0.2em 0.4em 0.2em 0.4em; 48 | -webkit-border-radius: 1em; 49 | -moz-border-radius: 1em; 50 | border-radius: 1em; 51 | text-decoration: none; 52 | opacity: 1; 53 | overflow-x: hidden; 54 | } 55 | .bouncy.ng-hide-remove, .bouncy.ng-hide-remove { 56 | -webkit-animation: bounceInRight 1s; 57 | -moz-animation: bounceInRight 1s; 58 | -o-animation: bounceInRight 1s; 59 | animation: bounceInRight 1s; 60 | } 61 | .callout { 62 | display: inline-block; 63 | position: absolute; 64 | margin-top: -1.8em; 65 | margin-left: -1.3em; 66 | font-style: italic; 67 | font-size: 83%; 68 | border: solid 1px #e2e2e2; 69 | border-top-left-radius: 0.8em; 70 | border-top-right-radius: 0.8em; 71 | border-bottom-right-radius: 0.8em; 72 | padding: 0.1em 0.4em 0.1em 0.3em; 73 | background-color: #f8f8f8; 74 | color: #353535; 75 | } 76 | li.ng-enter, li.ng-move { 77 | -webkit-animation: fadeIn 1s; 78 | -moz-animation: fadeIn 1s; 79 | -o-animation: fadeIn 1s; 80 | animation: fadeIn 1s; 81 | } 82 | .optionlabel { 83 | display: inline-block; 84 | width: 12em; 85 | text-align: left; 86 | } 87 | #options.ng-hide-remove { 88 | -webkit-animation: bounceInRight 1s; 89 | -moz-animation: bounceInRight 1s; 90 | -o-animation: bounceInRight 1s; 91 | animation: bounceInRight 1s; 92 | } 93 | #options.ng-hide-add { 94 | -webkit-animation: bounceOutRight 1s; 95 | -moz-animation: bounceOutRight 1s; 96 | -o-animation: bounceOutRight 1s; 97 | animation: bounceOutRight 1s; 98 | } 99 | .serviceicon { 100 | width:1.4em; 101 | height:1.2em; 102 | display: inline-block; 103 | position: relative; 104 | margin-bottom: -0.3em; 105 | } 106 | #options { 107 | z-index: 50; 108 | background-color: #f8f8f8; 109 | box-shadow: 4px 4px 4px #444; 110 | border: solid #000000 0px; 111 | -webkit-border-radius: 1.5em; 112 | -moz-border-radius: 1.5em; 113 | border-radius: 1.5em; 114 | padding: 3em 0.5em; 115 | position: absolute; 116 | width: 85%; 117 | left: 5%; 118 | top: 2em; 119 | } 120 | h2 { 121 | margin-top: 1em; 122 | margin-bottom: 0.1em; 123 | } 124 | p { 125 | margin-top: 0.8em; 126 | line-height: 135%; 127 | } 128 | ul { 129 | list-style-type: none; 130 | margin: 0em 1em 0em 1em; 131 | } 132 | li { 133 | line-height: 135%; 134 | } 135 | .dtlist { 136 | display: table; 137 | } 138 | .dtlist > li { 139 | display: table-row; 140 | } 141 | .dtlist > li > span { 142 | display: table-cell; 143 | padding-right: 0.6em; 144 | } 145 | span.newsdt { 146 | text-align: right; 147 | } 148 | .results li { 149 | border-top: 1px solid #ddd; 150 | padding: 0px; 151 | } 152 | .results a { 153 | text-decoration: none; 154 | } 155 | .results a:hover { 156 | background: #fff; 157 | transition: background-color 0.4s ease; 158 | } 159 | .results li:last-child { 160 | border-bottom: 1px solid #ddd; 161 | } 162 | .event_date { 163 | font-size: 80% 164 | } 165 | @media (min-width: 701px) { 166 | #allcontent { 167 | display:block; 168 | width:700px; 169 | margin: 3em auto; 170 | } 171 | } 172 | @media (max-width: 700px) { 173 | #allcontent { 174 | display:block; 175 | margin: 4px auto; 176 | } 177 | #discussion { 178 | clear:both; 179 | padding: 1em; 180 | } 181 | } 182 | .results { 183 | clear:both; 184 | font-size: 90%; 185 | display:block; 186 | } 187 | 188 | .mtsays { 189 | font-size:80px; 190 | padding-top:10px; 191 | font-family: Georgia, serif; 192 | } 193 | -------------------------------------------------------------------------------- /mobile/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | Music Tonight 17 | 18 | Music Tonight, the mobile app. 19 | 20 | 21 | Phil Schanely 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /client/src/img/bkgr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 33 | 37 | 38 | 41 | 45 | 49 | 53 | 57 | 58 | 61 | 65 | 69 | 70 | 81 | 91 | 101 | 102 | 125 | 127 | 128 | 130 | image/svg+xml 131 | 133 | 134 | 135 | 136 | 137 | 142 | 149 | 154 | 155 | 160 | 165 | 166 | 171 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /client/src/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | About Music Tonight 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 |
23 | 30 | 33 | 34 | 35 |
36 | 37 |
38 |

About

39 |

40 | There is amazing live music happening all around us; we just need better ways to find it. 41 |

42 |

43 | Launched in March of 2015, Music Tonight creates playlists of bands playing near you, wherever you are in the world. 44 | Partnering with Bandsintown, Spotify, SoundCloud, and Rdio, Music Tonight makes playlists of nearby artists tonight, or up two weeks in the future. 45 |

46 |

47 | You won't like most of the songs on the playlist we make for you. You probably won't like one track out of ten. But, equipped with a skip button and a sense of adventure, we hope you love it half as much as we do. 48 |

49 |

Go out.

50 |
51 | 52 |
53 |

Announcements

54 |
    55 |
  • 56 | [4/10/2015] 57 | 58 | And ... now make playlists for SoundCloud! Try it out. 59 | 60 |
  • 61 |
  • 62 | [4/6/2015] 63 | 64 | You can now create playlists using Rdio or Spotify. Take a look! 65 | 66 |
  • 67 |
  • 68 | [3/25/2015] 69 | 70 | Just released today, Music Tonight for iOS! Check it out on iTunes. 71 | 72 |
  • 73 |
  • 74 | [3/20/2015] 75 | 76 | We switched our concert data provider to Bandsintown. Let us know what you think. 77 | 78 |
  • 79 |
80 | 81 |

Media

82 | 88 | 89 |

Contact Us

90 |
91 |

92 | love@musictonightapp.com 93 |

94 |

95 | 96 | 97 | 98 | 99 |

100 |
101 |
102 |
103 | 104 | 105 | -------------------------------------------------------------------------------- /client/src/img/iTunesBadge135x40.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 11 | 12 | 13 | 18 | 20 | 21 | 22 | 23 | 26 | 32 | 38 | 45 | 48 | 54 | 57 | 63 | 64 | 65 | 66 | 71 | 77 | 81 | 85 | 86 | 92 | 98 | 104 | 110 | 114 | 117 | 120 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | TODO: 5 | uniquify on track (multiple incarnations of same artist) 6 | */ 7 | 8 | var fs = require('fs'); 9 | var Q = require('q'); 10 | var rdiolib = require('rdio'); 11 | var request = require('request'); 12 | var restify = require('restify'); 13 | var mysql = require('mysql2'); 14 | var url = require('url'); 15 | var cookieparser = require('restify-cookies'); 16 | var requireDir = require('require-dir'); 17 | var uuid = require('node-uuid'); 18 | var configFiles = requireDir('./config'); 19 | var soundclouder = require("soundclouder"); 20 | 21 | var os = require('os'); 22 | 23 | function linear_scorer(graph) { 24 | return function(value) { 25 | if (value < graph[0][0]) { return graph[0][1]; } 26 | for(var idx=1; idx < graph.length; idx++) { 27 | var next_x = graph[idx][0]; 28 | var prev_x = graph[idx - 1][0]; 29 | if (prev_x <= value && value < next_x) { 30 | var prev_y = graph[idx - 1][1]; 31 | var next_y = graph[idx][1]; 32 | var progress = (value - prev_x) / (next_x - prev_x); 33 | return progress * next_y + (1.0 - progress) * prev_y; 34 | } 35 | } 36 | return graph[graph.length - 1][1]; 37 | }; 38 | } 39 | 40 | function getLocalIps() { 41 | var ifaces = os.networkInterfaces(); 42 | var addrs = []; 43 | Object.keys(ifaces).forEach(function (ifname) { 44 | var alias = 0; 45 | ifaces[ifname].forEach(function (iface) { 46 | if ('IPv4' !== iface.family || iface.internal !== false) { 47 | // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses 48 | return; 49 | } 50 | // this interface has only one ipv4 adress 51 | addrs.push(iface.address); 52 | }); 53 | }); 54 | return addrs; 55 | } 56 | 57 | 58 | var config = {}; 59 | var fileNames = Object.keys(configFiles); 60 | fileNames.sort(); 61 | fileNames.forEach(function(fileName) { 62 | var configObj = configFiles[fileName]; 63 | for (var attrname in configObj) { config[attrname] = configObj[attrname]; } 64 | }); 65 | 66 | getLocalIps().forEach(function(ip) { 67 | var host = config.HOST_PREFIXES[ip]; 68 | if (host) { 69 | config.HOST_PREFIX = host; 70 | } 71 | }); 72 | console.log('My host prefix is ', config.HOST_PREFIX); 73 | 74 | var callback_url = config.HOST_PREFIX + '/api/music_svc_callback'; 75 | 76 | config.RDIO.callback_url = callback_url; 77 | config.SPOTIFY.callback_url = callback_url; 78 | 79 | var Rdio = rdiolib({'rdio': config.RDIO}); 80 | soundclouder.init(config.SOUNDCLOUD.client_id, config.SOUNDCLOUD.client_secret, callback_url); 81 | 82 | var pool = mysql.createPool(config.MYSQL); 83 | 84 | 85 | pool.boundQuery = function() { 86 | var deferred = Q.defer(); 87 | var qArgs = Array.prototype.slice.call(arguments, 0); 88 | var cb = function(err, connection) { 89 | if (err) { 90 | deferred.reject(err); 91 | } else { 92 | qArgs.push(function(err, rows) { 93 | if (err) { 94 | deferred.reject(err); 95 | } else { 96 | deferred.resolve(rows); 97 | } 98 | connection.release(); 99 | }); 100 | connection.query.apply(connection, qArgs); 101 | } 102 | } 103 | pool.getConnection(cb); 104 | return deferred.promise; 105 | }; 106 | 107 | function mysqlStore(pool, table) { 108 | var sql = 'CREATE TABLE IF NOT EXISTS '+table+' (k VARCHAR(255) PRIMARY KEY, v VARCHAR(21000)) ENGINE=innodb' 109 | return pool.boundQuery(sql).then(function () { 110 | var openRequests = {}; 111 | return { 112 | 'get': function(key) { 113 | if (! openRequests[key]) { 114 | openRequests[key] = pool.boundQuery('SELECT k,v FROM ' + table + ' WHERE k=? COLLATE utf8_unicode_ci', key).then(function(rows) { 115 | if (rows.length == 0) return undefined; 116 | var ret = JSON.parse(rows[0].v); 117 | return ret; 118 | }).fin(function() { 119 | delete openRequests[key]; 120 | }); 121 | } 122 | return openRequests[key]; 123 | }, 124 | 'all': function() { 125 | return pool.boundQuery('SELECT k,v FROM ' + table).then(function(rows) { 126 | var result = {}; 127 | rows.forEach(function(row){result[row.k] = JSON.parse(row.v);}); 128 | return result; 129 | }); 130 | }, 131 | 'mget': function(keys) { 132 | if (keys.length === 0) return Q.fcall(function(){return {};}); 133 | var clauses = keys.map(function(k){return 'k=?';}).join(' OR '); 134 | return pool.boundQuery('SELECT k,v FROM ' + table + ' WHERE '+clauses+' COLLATE utf8_unicode_ci', keys).then(function(rows) { 135 | var result = {}; 136 | rows.forEach(function(row){result[row.k] = JSON.parse(row.v);}); 137 | return result; 138 | }); 139 | }, 140 | 'set': function(key, val) { 141 | var sql = 'INSERT INTO ' + table + ' (k,v) VALUES (?,?) ON DUPLICATE KEY UPDATE v=VALUES(v)'; 142 | return pool.boundQuery(sql, [key, JSON.stringify(val)]); 143 | }, 144 | 'mset': function(bindings) { 145 | var sql = ''; 146 | var params = []; 147 | for(var key in bindings) { 148 | if (sql) { sql +=';'; } 149 | sql += 'INSERT INTO ' + table + ' (k,v) VALUES (?,?) ON DUPLICATE KEY UPDATE v=VALUES(v)'; 150 | params.push(key); 151 | params.push(JSON.stringify(bindings[key])); 152 | } 153 | return pool.boundQuery(sql, params); 154 | } 155 | }; 156 | }); 157 | } 158 | 159 | function http(options) { 160 | options.encoding = 'utf8'; 161 | var deferred = Q.defer(); 162 | request(options, function(err, httpResponse, body) { 163 | if (err) { 164 | deferred.reject(err); 165 | } else { 166 | deferred.resolve(body); 167 | } 168 | }); 169 | return deferred.promise; 170 | } 171 | 172 | function clientCookie(req, res) { 173 | var myKey = req.cookies.svc_auth_key; 174 | if (! myKey) { 175 | myKey = uuid.v4(); 176 | res.setCookie('svc_auth_key', myKey); 177 | } 178 | return myKey; 179 | } 180 | 181 | function fetchUserInfo(authStore, myKey) { 182 | return authStore.get(myKey).then(function(info) { 183 | if (! info) { 184 | info = {}; 185 | } 186 | if (! info.div) { 187 | info.div = {}; 188 | } 189 | return info; 190 | }); 191 | } 192 | 193 | function titlePlaylist() { 194 | return formatDate(new Date()).substring(0,10) + '-music-tonight'; 195 | } 196 | 197 | function parse_hash_query(url) { 198 | var args = {}; 199 | var hash = url.replace(/^[^\#]*#/g, ''); 200 | var all = hash.split('&'); 201 | all.forEach(function(keyvalue) { 202 | var idx = keyvalue.indexOf('='); 203 | var key = keyvalue.substring(0, idx); 204 | var val = keyvalue.substring(idx + 1); 205 | args[key] = val; 206 | }); 207 | return args; 208 | } 209 | 210 | function getDiversityChecker(divblock) { 211 | var daypart = Math.floor(new Date().getTime() / (12 * 3600 * 1000)); 212 | var old = {}; 213 | var today = {}; 214 | var now = {}; 215 | for(var k in divblock) { 216 | if (k > daypart - 14) { 217 | divblock[k].forEach(function(hsh) { 218 | if (k == daypart) { 219 | today[hsh] = true; 220 | } else { 221 | old[hsh] = true; 222 | } 223 | }); 224 | } else { 225 | delete divblock[k]; 226 | } 227 | } 228 | return { 229 | check: function(key) { 230 | var idx = hashCode(key) + ''; 231 | return (old[idx] || now[idx]); 232 | }, 233 | add: function(key) { 234 | var idx = hashCode(key) + ''; 235 | old[idx] = true; 236 | today[idx] = true; 237 | now[idx] = true; 238 | }, 239 | commit: function() { 240 | divblock[daypart] = Object.keys(today); 241 | if (divblock[daypart].length > 500) { 242 | divblock[daypart] = []; 243 | } 244 | return divblock; 245 | } 246 | }; 247 | } 248 | 249 | function score_result(track) { 250 | var dtstring = track.event.datetime_local; 251 | var year = dtstring.substring(0, 4); 252 | var month = dtstring.substring(5, 7); 253 | var day = dtstring.substring(8, 10); 254 | var dtscore = parseInt(year + month + day); 255 | return dtscore * 10 + Math.random(); 256 | } 257 | 258 | function score_track(track, divchecker) { 259 | var novel = divchecker.check(track.name) ? 0.0 : 1.0; 260 | var popularity = 0.0; 261 | if (track.popularity) { // spotify 262 | popularity = track.popularity / 100.0; 263 | } else if (track.playCount) { // rdio 264 | popularity = 1.0 - (100.0 / (track.playCount + 1)); 265 | popularity = Math.min(1.0, Math.max(0.0, popularity)); // clamp to unit value 266 | } 267 | var score = novel + popularity / 2.0; 268 | return score 269 | } 270 | 271 | function formatDate(dt) { 272 | return dt.toISOString().substring(0, 19); 273 | } 274 | 275 | function rdio_get_access_token(code, callback_url) { 276 | var deferred = Q.defer(); 277 | var rdio = new Rdio(); 278 | rdio.getAccessToken( 279 | {code: code, redirect: callback_url}, 280 | function(error) { 281 | if (error) { 282 | console.log('could not get access_token'); 283 | deferred.reject(error); 284 | } else { 285 | console.log('rdio access_token ok', rdio.getTokens()); 286 | var tokens = rdio.getTokens(); 287 | deferred.resolve({'token':tokens.accessToken, 'refresh':tokens.refreshToken}); 288 | } 289 | } 290 | ); 291 | return deferred.promise; 292 | } 293 | 294 | function soundcloud_get_access_token(code) { 295 | var deferred = Q.defer(); 296 | soundclouder.auth(code, function(error, access_token) { 297 | if (error) { 298 | console.log('could not get access_token'); 299 | deferred.reject(error); 300 | } else { 301 | console.log('access_token ok', access_token); 302 | deferred.resolve({'token': access_token}); 303 | } 304 | }); 305 | return deferred.promise; 306 | } 307 | 308 | function make_rdio_api_fn(access_token, refresh_token) { 309 | var deferred = Q.defer(); 310 | var rdio; 311 | if (access_token !== undefined) { 312 | rdio = new Rdio({accessToken:access_token, refreshToken:refresh_token}); 313 | } else { 314 | rdio = new Rdio(); 315 | } 316 | var api = function(args) { 317 | function make_promise() { 318 | var deferred = Q.defer(); 319 | var authType = (args.method === 'search' || args.method == 'get') ? false : 'protected'; 320 | rdio.request(args, authType, function(err, data) { 321 | if (err) { 322 | deferred.reject(err); 323 | } else { 324 | deferred.resolve(data['result']); 325 | } 326 | }); 327 | return deferred.promise; 328 | } 329 | return backoff(make_promise, 'Developer Over Qps'); 330 | }; 331 | if (access_token !== undefined) { 332 | deferred.resolve(api); 333 | } else { 334 | rdio.getClientToken(function(err) { 335 | if (err) { 336 | console.log('failed to get unauthed client token for rdio:'+err); 337 | deferred.reject(error); 338 | } else { 339 | deferred.resolve(api); 340 | } 341 | }); 342 | } 343 | return deferred.promise; 344 | } 345 | 346 | function isRealVenue(venue) { 347 | if (venue.name.length < 12) return true; 348 | if (venue.name.match(/radio/i) || venue.name.length > 120) { 349 | return false; 350 | } 351 | return true; 352 | } 353 | 354 | function make_spotify_api_fn(access_token) { 355 | return { 356 | 'getUsername': function() { 357 | var url = 'https://api.spotify.com/v1/me'; 358 | return http({'method':'get', 'url':url, 'headers': {'Authorization': 'Bearer ' + access_token}, 'json':true}). 359 | then(function(r) { return r.id; }); 360 | }, 361 | 'createPlaylist': function(username, name) { 362 | var url = 'https://api.spotify.com/v1/users/' + username + '/playlists'; 363 | var data = {'name': name, 'public': false}; 364 | var headers = { 365 | 'Authorization': 'Bearer ' + access_token, 366 | 'Content-Type': 'application/json' 367 | } 368 | return http({'method':'post', 'url':url, 'body':data, json:true, headers: headers}). 369 | then(function(r) {return r.id;}); 370 | }, 371 | 'addTracksToPlaylist': function(username, playlist, tracks) { 372 | var url = 'https://api.spotify.com/v1/users/' + username + 373 | '/playlists/' + playlist + 374 | '/tracks'; 375 | var headers = { 376 | 'Authorization': 'Bearer ' + access_token, 377 | 'Content-Type': 'application/json' 378 | }; 379 | return http({method:'post', url:url, body:tracks, json:true, headers: headers}). 380 | then(function(r) {return r.id;}); 381 | } 382 | }; 383 | } 384 | 385 | function allPages(reqGenerator, resultProcessor, pageSize) { 386 | function onePage(pageNum) { 387 | return http(reqGenerator(pageSize, pageNum)).then(function(response) { 388 | var results = resultProcessor(response); 389 | console.log('page '+pageNum+' had '+results.length+' results'); 390 | if (results.length < pageSize || pageNum == 1) { 391 | return results; 392 | } else { 393 | return onePage(pageNum + 1).then(function(subresults) { 394 | results.forEach(function(r){subresults.push(r);}); 395 | return subresults; 396 | }); 397 | } 398 | }); 399 | } 400 | return onePage(1); 401 | } 402 | 403 | function fetchEvents(opts) { 404 | var zipcode = opts.zipcode; 405 | var latlon = opts.latlon; 406 | var daysout = opts.daysout; 407 | var maxmiles = opts.maxmiles; 408 | var onlyavailable = opts.onlyavailable; 409 | var dt = new Date(); 410 | var startdt = formatDate(new Date(dt.getTime() - 2 * 3600 * 1000)); 411 | var enddt = formatDate(new Date(dt.getTime() + ((daysout - 1) * 24 - 2) * 3600 * 1000)); 412 | 413 | console.log('fetchEvents input:', zipcode, opts.clientIp, latlon, startdt, enddt); 414 | 415 | var uri = 'http://api.bandsintown.com/events/search?app_id=musictonight.millstonecw.com&format=json'; 416 | if (latlon) { 417 | uri += '&location=' + latlon; 418 | } else { 419 | if (opts.clientIp !== '127.0.0.1') { 420 | uri += '&location=' + opts.clientIp; 421 | } else { 422 | uri += '&location=40.7436300,-73.9906270'; 423 | } 424 | } 425 | uri += '&radius=' + maxmiles; 426 | uri += '&date=' + startdt.substring(0, 10) + ',' + enddt.substring(0, 10); 427 | 428 | function reqGenerator(pageSize, pageNum) { 429 | var cur = uri + '&per_page=' + pageSize + '&page=' + pageNum; 430 | console.log(cur); 431 | return {method:'get', json:true, uri:cur}; 432 | } 433 | function resultProcessor(response) { 434 | if (response.errors) { 435 | var errors = response.errors; 436 | if (errors[0] === 'Unknown Location') { 437 | throw new Error('client error: cannot_geo_ip'); 438 | } else { 439 | throw new Error(errors); 440 | } 441 | } else { 442 | return response; 443 | } 444 | } 445 | var promise = allPages(reqGenerator, resultProcessor, 100).then(function(events) { 446 | if (onlyavailable) { 447 | events = events.filter(function(e){return e.ticket_status === 'available'}); 448 | } 449 | events.forEach(function(event) { 450 | var month = parseInt(event.datetime.substring(5, 7)); 451 | var day = parseInt(event.datetime.substring(8, 10)); 452 | event.datestring = month + '-' + day; 453 | event.datetime_local = event.datetime; 454 | event.performers = event.artists; 455 | delete event.artists; 456 | }); 457 | return events; 458 | }); 459 | return promise.then(function(events) { 460 | var performer_map = {}; 461 | events.forEach(function(event) { 462 | if (isRealVenue(event.venue)) { 463 | event.performers = event.performers.slice(0, 3); 464 | event.performers.forEach(function(performer) { 465 | performer_map[performer.name]=event; 466 | }); 467 | } 468 | }); 469 | return performer_map; 470 | }); 471 | } 472 | 473 | function redirectOnCreate(res, links) { 474 | console.log('sending playlist redirect', new Date().getTime()); 475 | res.header('Location', '/#http=' + encodeURIComponent(links.http) + 476 | '&app=' + encodeURIComponent(links.app)); 477 | res.send(302); 478 | } 479 | 480 | function redirectToAuth(myKey, info, res, authStore) { 481 | var service = info.service; 482 | var trackKeys = info.track_keys; 483 | if (service === 'spotify') { 484 | var uri = 'https://accounts.spotify.com/authorize?client_id=' + config.SPOTIFY.client_id + 485 | '&state=' + myKey + 486 | '&response_type=code' + 487 | '&scope=playlist-read-private%20playlist-modify%20playlist-modify-private' + 488 | '&redirect_uri=' + config.SPOTIFY.callback_url; 489 | return authStore.set(myKey, info).then(function(){ 490 | console.log('spotify auth redirect', new Date().getTime()); 491 | res.header('Location', uri); 492 | res.send(302); 493 | }).done(); 494 | } else if (service === 'rdio') { 495 | var rdio = new Rdio(); 496 | var uri = 'https://www.rdio.com/oauth2/authorize?response_type=code&client_id=' + config.RDIO.clientId + '&redirect_uri=' + config.RDIO.callback_url; 497 | return authStore.set(myKey, info).then(function(){ 498 | res.header('Location', uri); 499 | res.send(302); 500 | }); 501 | } else if (service === 'soundcloud') { 502 | var uri = 'https://soundcloud.com/connect?client_id=' + config.SOUNDCLOUD.client_id + 503 | '&state=' + myKey + '&response_type=code&redirect_uri=' + encodeURIComponent(callback_url); 504 | return authStore.set(myKey, info).then(function(){ 505 | res.header('Location', uri); 506 | res.send(302); 507 | }).done(); 508 | } else { 509 | res.send(400, 'Invalid service'); 510 | } 511 | } 512 | 513 | function hashCode(string) { 514 | var hash = 0, i, chr, len; 515 | if (string.length == 0) return hash; 516 | for (i = 0, len = string.length; i < len; i++) { 517 | chr = string.charCodeAt(i); 518 | hash = ((hash << 5) - hash) + chr; 519 | hash |= 0; // Convert to 32bit integer 520 | } 521 | return hash; 522 | } 523 | 524 | var NUM_TRACKS_CACHED = 7; 525 | 526 | function spotifyArtist(performer) { 527 | var uri = config.SPOTIFY.artist_search_prefix + 'limit=' + NUM_TRACKS_CACHED + '&q='+encodeURIComponent('"'+performer+'"'); 528 | return http({uri:uri, method:'get', json:true}).then( 529 | function(response) { 530 | if (!response.artists) { 531 | console.log('response has no artists?: ', uri, response); 532 | return null; 533 | } 534 | var artists = response.artists.items; 535 | if (artists.length == 0) { 536 | console.log('no artists found for: '+performer); 537 | return null; 538 | } 539 | artists = artists.filter(function(artist) { return artist.name === performer }); 540 | if (artists.length == 0) { 541 | console.log('no name match for artist: '+performer); 542 | return null; 543 | } 544 | return artists[0]; 545 | } 546 | ).then( 547 | function(artist) { 548 | if (artist === null) return null; 549 | uri = config.SPOTIFY.artist_prefix + artist.id + '/top-tracks?country=US'; 550 | return http({uri:uri, method:'get', json:true}).then(function(tracks_response) { 551 | var tracks = tracks_response.tracks; 552 | if (tracks.length === 0) { 553 | console.log('no tracks for artist: '+performer); 554 | return null; 555 | } 556 | function score_track(t) { return (t.popularity + 10.0) / (t.artists.length * t.artists.length); } 557 | tracks.sort(function(a,b) {return score_track(b) - score_track(a);}); 558 | tracks = tracks.slice(0, NUM_TRACKS_CACHED); 559 | tracks = tracks.map(function(item) { 560 | return {name: item.name, artist: performer, uri: item.uri, popularity: item.popularity, key: item.uri}; 561 | }); 562 | artist.tracks = tracks; 563 | return artist; 564 | }); 565 | } 566 | ); 567 | } 568 | 569 | function backoff(fn, errstr) { 570 | return fn()['catch'](function(err) { 571 | if (JSON.stringify(err).indexOf(errstr) === -1) { 572 | console.log('err without backoff', err); 573 | return Q.reject(err); 574 | } else { 575 | console.log('backoff err', err); 576 | return Q.delay(Math.round(1000 * Math.random())).then(function(){ 577 | return backoff(fn, errstr); 578 | }); 579 | } 580 | }); 581 | } 582 | 583 | function rdioArtists(performers) { 584 | return make_rdio_api_fn(undefined, undefined).then(function(api) { 585 | var SLEEP = 50; 586 | var cur_delay = -SLEEP; 587 | return Q.all(performers.map(function(performer) { 588 | cur_delay += SLEEP; 589 | return Q.delay(cur_delay).then(function() { 590 | return api({'method':'search','query':performer,'types':'artist','extras':'-*,key,name,trackKeys'}).then( 591 | function(artist_result) { 592 | var hits = artist_result.results.filter(function(a){return a.name === performer;}); 593 | if (hits) { 594 | return hits[0]; 595 | } else { 596 | console.log('Artist not found on rdio ', performer, ' options ', artist_result.results); 597 | return null; 598 | } 599 | } 600 | ); 601 | }); 602 | })).then(function(artists) { 603 | var track_key_to_artist = {}; 604 | var name_to_artist = {}; 605 | artists.forEach(function(artist) { 606 | if (artist) { 607 | name_to_artist[artist.name] = artist; 608 | artist.tracks = []; 609 | artist.trackKeys.slice(0, NUM_TRACKS_CACHED).forEach(function(track_key) { 610 | track_key_to_artist[track_key] = artist; 611 | }); 612 | } 613 | }); 614 | return api( 615 | {'method': 'get', 616 | 'keys': Object.keys(track_key_to_artist).join(','), 617 | 'extras':'-*,key,name,playCount' 618 | } 619 | ).then(function(resp) { 620 | for(var track_key in resp) { 621 | var track = resp[track_key]; 622 | var artist = track_key_to_artist[track.key]; 623 | artist.tracks.push({ 624 | name: track.name, 625 | artist: artist.name, 626 | key: track.key, 627 | playCount: track.playCount}); 628 | } 629 | return name_to_artist; 630 | }); 631 | }); 632 | }); 633 | } 634 | 635 | function soundcloudArtist(performer) { 636 | var base = 'http://api.soundcloud.com'; 637 | var uri = base + '/users?limit=1&q='+encodeURIComponent(performer) + '&client_id=' + config.SOUNDCLOUD.client_id; 638 | return http({uri: uri, method: 'get', json: true}).then(function(response) { 639 | if (!(response.length > 0)) return null; 640 | var artistid = response[0].id; 641 | var uri = base + '/users/' + artistid + '/tracks?limit=50&client_id=' + config.SOUNDCLOUD.client_id; 642 | return http({uri: uri, method: 'get', json: true}).then(function(trackResponse) { 643 | var tracks = trackResponse.map(function(item) { 644 | return { 645 | name: item.title, 646 | artist: performer, 647 | key: item.id, 648 | playCount: 1.0 - (1/(1+item.favoritings_count)) * (1/(1+item.playback_count)) 649 | }; 650 | }); 651 | tracks.sort(function(a, b) {return b.playCount - a.playCount}); 652 | tracks = tracks.slice(0, NUM_TRACKS_CACHED); 653 | return { 654 | 'name': performer, 655 | 'tracks': tracks 656 | }; 657 | }); 658 | }); 659 | } 660 | 661 | function makePlaylist(service, authInfo, trackKeys) { 662 | var access_token = authInfo.token; 663 | if (service == 'spotify') { 664 | var api = make_spotify_api_fn(access_token); 665 | return api.getUsername().then(function(username) { 666 | if (username === undefined) throw new Error('access_token not working'); 667 | return api.createPlaylist(username, titlePlaylist()).then(function(playlist_id) { 668 | function sendBucket() { 669 | var page = trackKeys.splice(0, 99); 670 | return api.addTracksToPlaylist(username, playlist_id, page).then(function() { 671 | if (trackKeys.length > 0) return sendBucket(); 672 | }); 673 | } 674 | return sendBucket().then(function() { 675 | return {http: 'https://play.spotify.com/user/'+username+'/playlist/'+playlist_id, 676 | app: 'spotify:user:'+username+':playlist:'+playlist_id}; 677 | }); 678 | }); 679 | }); 680 | } else if (service == 'rdio') { 681 | return make_rdio_api_fn(access_token, authInfo.refresh).then(function(api) { 682 | var payload = {'method': 'createPlaylist', 683 | 'name': titlePlaylist(), 684 | 'description': 'A playlist of local artists playing near you, now.', 685 | 'isPublished': 'true', 686 | 'tracks': trackKeys.join(',')}; 687 | return api(payload).then( 688 | function(result) { 689 | return {http: 'http://www.rdio.com' + result.url, 690 | app: 'rdio://www.rdio.com' + result.url}; 691 | } 692 | ); 693 | }); 694 | } else if (service === 'soundcloud') { 695 | var title = titlePlaylist(); 696 | var authbody = ('oauth_token=' + encodeURIComponent(access_token) + 697 | '&client_id=' + encodeURIComponent(config.SOUNDCLOUD.client_id)) 698 | var formbody = (authbody + 699 | '&playlist[sharing]=public' + 700 | '&playlist[title]=' + encodeURIComponent(title)); 701 | var req = { 702 | method: 'POST', 703 | url: 'http://api.soundcloud.com/playlists', 704 | json: true, 705 | form: formbody 706 | }; 707 | console.log('url', req); 708 | return http(req).then(function(response) { 709 | if (! response.permalink_url) { 710 | throw new Error('Unable to create playlist:' + JSON.stringify(response.errors)); 711 | } 712 | function sendBucket() { 713 | var page = trackKeys.splice(0, 9999); 714 | var body = authbody + page.map(function(k){ return '&playlist[tracks][][id]=' + encodeURIComponent(k); }).join(''); 715 | var url = 'http://api.soundcloud.com/playlists/' + response.id; 716 | return http({method:'PUT', json:true, url: url, form: body}).then(function(resp) { 717 | if (trackKeys.length > 0) return sendBucket(); 718 | }); 719 | } 720 | return sendBucket().then(function() { 721 | return { 722 | http: response.permalink_url, 723 | app: 'soundcloud:playlists:' + response.id 724 | }; 725 | }); 726 | }) 727 | } else { 728 | throw new Error('Invalid service'); 729 | } 730 | } 731 | 732 | function mapq(fn) { 733 | return function(items) { 734 | var map = {}; 735 | var delay = 0; 736 | return Q.all(items.map(function(item){ 737 | delay += 100; 738 | return Q.delay(delay).then(function(){return fn(item);}).then(function(val) { 739 | map[item] = val; 740 | }); 741 | })).then( function() { 742 | return map; 743 | }); 744 | }; 745 | } 746 | 747 | 748 | function getMusic(eventOptions, trackOptions, artistStore, divchecker) { 749 | var maxTracksPerArtist = trackOptions.maxTracksPerArtist; 750 | return fetchEvents(eventOptions).then(function(performer_map) { 751 | var service = trackOptions.service; 752 | var playlistName = formatDate(new Date()).substring(0, 10) + '-music-tonight'; 753 | var performers = Object.keys(performer_map); 754 | var tracksPerArtist = Math.round(22.0 / performers.length); 755 | var performerCacheKeys = performers.map(function(p){ return service+':'+p; }); 756 | tracksPerArtist = Math.max(1, Math.min(maxTracksPerArtist, tracksPerArtist)); 757 | return artistStore.mget(performerCacheKeys).then(function(cacheResults) { 758 | var cached = []; 759 | var uncached = []; 760 | performers.forEach(function(performer) { 761 | var artistCacheKey = service + ':' + performer; 762 | if (cacheResults[artistCacheKey] !== undefined) { 763 | cached.push(cacheResults[artistCacheKey]); 764 | } else { 765 | uncached.push(performer); 766 | } 767 | }); 768 | console.log(cached.length + ' cached, ' + uncached.length + ' to be fetched.'); 769 | var promise; 770 | if (uncached.length === 0) { 771 | promise = Q.fcall(function(){ return cached; }); 772 | } else { 773 | var fetchfn = {'spotify': mapq(spotifyArtist), 'rdio': rdioArtists, 'soundcloud':mapq(soundcloudArtist)}[service]; 774 | promise = fetchfn(uncached).then(function(results) { 775 | var result_list = []; 776 | var cacheUpdate = {}; 777 | uncached.forEach(function(key) { 778 | var result = results[key]; 779 | result_list.push(result); 780 | cacheUpdate[service + ':' + key] = result; 781 | }); 782 | artistStore.mset(cacheUpdate).done(); 783 | return cached.concat(result_list); 784 | }); 785 | } 786 | return promise.then(function(artists) { 787 | var all_tracks = []; 788 | artists.forEach(function(artist) { 789 | if (! artist) {return;} 790 | var event = performer_map[artist.name]; 791 | var tracks = artist.tracks; 792 | tracks.sort(function(a, b) { return score_track(b, divchecker) - score_track(a, divchecker); }); 793 | artist.tracks.slice(0, tracksPerArtist).forEach(function(track){ 794 | track.event = event; 795 | divchecker.add(track.name); 796 | all_tracks.push(track); 797 | }); 798 | }); 799 | var result_tracks = all_tracks.sort(function(a, b) { return score_result(a) - score_result(b); }); 800 | 801 | return {name: playlistName, tracks:result_tracks}; 802 | }); 803 | }); 804 | }); 805 | } 806 | 807 | function promised(fn) { 808 | return function(req, res, next) { 809 | fn(req, res, next).then(function(result) { 810 | res.send(200, result); 811 | }, function(err) { 812 | console.log('returning error', err); 813 | if ((err+'').match(/client error/)) { 814 | res.send(400, err); 815 | } else { 816 | console.log(err.stack); 817 | res.send(500, err); 818 | } 819 | }).done(); 820 | }; 821 | } 822 | 823 | function clientError(desc) { 824 | throw new Error('client error: ' + desc); 825 | } 826 | 827 | function makeServer(artistStore, authStore) { 828 | 829 | var server = restify.createServer(); 830 | 831 | server.on('uncaughtException', function(req, res, route, err) { 832 | console.log(err.stack); 833 | res.send(err); 834 | }); 835 | 836 | server.use(restify.gzipResponse()); 837 | server.use(cookieparser.parse); 838 | 839 | server.use( // CORS 840 | function crossOrigin(req,res,next){ 841 | res.header("Access-Control-Allow-Origin", "*"); 842 | res.header("Access-Control-Allow-Headers", "X-Requested-With"); 843 | return next(); 844 | } 845 | ); 846 | 847 | server.use(restify.bodyParser()); 848 | server.use(restify.queryParser()); 849 | 850 | server.get('/api/playlist', promised(function(req, res) { 851 | console.log('/api/playlist', req.params, new Date().getTime()); 852 | 853 | var clientIp = req.headers['x-forwarded-for'] || 854 | req.connection.remoteAddress || 855 | req.socket.remoteAddress || 856 | req.connection.socket.remoteAddress; 857 | clientIp = clientIp.split(',')[0]; 858 | 859 | var language = 'en-US'; 860 | var acceptLanguages = req.headers['accept-language']; 861 | if (acceptLanguages) { 862 | language = acceptLanguages.split(/[\,\;]/)[0]; 863 | } 864 | 865 | var daysout = (req.params.daysout) ? parseInt(req.params.daysout) : 1; 866 | var maxmiles = (req.params.maxmiles) ? parseInt(req.params.maxmiles) : 125; 867 | var onlyavailable = (req.params.onlyavailable) ? (req.params.onlyavailable === 'true') : false; 868 | var eventOptions = { 869 | zipcode: req.params.zip_code, 870 | clientIp: clientIp, 871 | latlon: req.params.latlon, 872 | daysout: daysout, 873 | maxmiles: maxmiles, 874 | onlyavailable: onlyavailable 875 | }; 876 | var trackOptions = { 877 | service: (req.params.service) ? req.params.service : 'spotify', 878 | maxTracksPerArtist: (req.params.maxartisttracks) ? parseInt(req.params.maxartisttracks) : 2 879 | }; 880 | 881 | var myKey = clientCookie(req, res); 882 | return fetchUserInfo(authStore, myKey).then(function(info) { 883 | var divchecker = getDiversityChecker(info.div); 884 | 885 | return getMusic(eventOptions, trackOptions, artistStore, divchecker).then(function(result) { 886 | result.language = language; 887 | info.div = divchecker.commit(); 888 | authStore.set(myKey, info).done(); 889 | return result; 890 | }); 891 | }); 892 | })); 893 | 894 | server.get('/api/music_svc_auth', function(req, res) { 895 | console.log('/api/music_svc_auth', req.params, new Date().getTime()); 896 | var service = req.params.service; 897 | var trackKeys = JSON.parse(req.params.track_keys); 898 | var myKey = clientCookie(req, res); 899 | authStore.get(myKey).then(function(info) { 900 | if (info && info.auth && info.auth[service] && info.auth[service].token) { 901 | var auth = info.auth[service]; 902 | info.service = service; 903 | info.track_keys = trackKeys; 904 | return makePlaylist(info.service, auth, trackKeys).then(function(links) { 905 | redirectOnCreate(res, links); 906 | }).then(function(){}, function(err) { 907 | console.log('Could not use saved access info (', auth, '):', err); 908 | return redirectToAuth(myKey, info, res, authStore); 909 | }); 910 | } else { 911 | var info = {'track_keys': trackKeys, 'service': service}; 912 | return redirectToAuth(myKey, info, res, authStore); 913 | } 914 | }).done(); 915 | }); 916 | 917 | server.get('/api/music_svc_callback', function(req, res) { 918 | console.log('/api/music_svc_callback', req.params, new Date().getTime()); 919 | var myKey = clientCookie(req, res); 920 | authStore.get(myKey).then(function(info) { 921 | var trackKeys = info.track_keys; 922 | delete info.track_keys; 923 | var openlink = ''; 924 | 925 | var promise; 926 | if (info.service === 'spotify') { 927 | var code = req.query.code || null; 928 | var state = req.query.state || null; 929 | if (state === null || state !== myKey) { 930 | throw new Error('state mismatch: state:'+state+' vs cookie:'+myKey); 931 | } 932 | var authOptions = { 933 | url: 'https://accounts.spotify.com/api/token', 934 | form: { 935 | code: code, 936 | redirect_uri: config.SPOTIFY.callback_url, 937 | grant_type: 'authorization_code' 938 | }, 939 | headers: { 940 | 'Authorization': 'Basic ' + (new Buffer(config.SPOTIFY.client_id + ':' + config.SPOTIFY.client_secret).toString('base64')) 941 | }, 942 | method: 'post', 943 | json: true 944 | }; 945 | promise = http(authOptions).then(function(response) { 946 | return {'token': response.access_token}; 947 | }); 948 | 949 | 950 | } else if (info.service === 'rdio') { 951 | promise = rdio_get_access_token(req.query.code, config.RDIO.callback_url); 952 | } else if (info.service === 'soundcloud') { 953 | promise = soundcloud_get_access_token(req.params.code); 954 | } else { 955 | res.send(400, 'Invalid service'); 956 | return; 957 | } 958 | promise = promise.then(function(accessdata) { 959 | if (!info.auth) { 960 | info.auth = {}; 961 | } 962 | info.auth[info.service] = accessdata; 963 | return makePlaylist(info.service, accessdata, trackKeys); 964 | }).then(function(links) { 965 | redirectOnCreate(res, links); 966 | }); 967 | return promise.then( 968 | function() { 969 | return authStore.set(myKey, info); 970 | }); 971 | }).done(); 972 | }); 973 | 974 | return server; 975 | } 976 | 977 | function upgrade1(authStore) { 978 | return authStore.all().then(function(map) { 979 | console.log(map); 980 | var promises = []; 981 | for(var key in map) { 982 | console.log('key', key); 983 | var info = map[key]; 984 | if (info.access && info.access.token) { 985 | var token = info.access.token; 986 | var service = ''; 987 | if (token.toLowerCase() === token) { // rdio tokens are lowercase 988 | service = 'rdio'; 989 | } else { 990 | service = 'spotify'; 991 | } 992 | if (! info.auth) { 993 | info.auth = {}; 994 | info.auth[service] = info.access; 995 | } 996 | console.log(key); 997 | console.log(info); 998 | promises.push(authStore.set(key, info)); 999 | } 1000 | } 1001 | return Q.all(promises).then(function() { console.log('upgrade complete'); }).done(); 1002 | }); 1003 | } 1004 | 1005 | var lastarg = process.argv[process.argv.length - 1]; 1006 | mysqlStore(pool, 'artists2').then(function(artistStore) { 1007 | return mysqlStore(pool, 'auth').then(function(authStore) { 1008 | if (lastarg == 'upgrade') { 1009 | return upgrade1(authStore); 1010 | } else { 1011 | var server = makeServer(artistStore, authStore); 1012 | server.listen(11810, function() { 1013 | console.log('%s listening at %s', server.name, server.url); 1014 | }); 1015 | } 1016 | }); 1017 | }).done(); 1018 | -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Music Tonight 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 | 31 |
32 |   33 |
34 | 35 | up to {{opts.daysout}} day{{(opts.daysout>1)?'s':''}} from now 36 |
37 |   38 |
39 | 40 | up to {{opts.maxmiles}} {{(lang=='en-US') ? 'miles' : 'km'}} away 41 |
42 |   43 |
44 | 45 | up to {{opts.maxartisttracks}} tracks per artist 46 |
47 |   48 |
49 | 50 | only with available tickets 51 |
52 |   53 | 64 |
65 |
66 | 67 | 68 | 76 | 79 | 80 | 81 |
{{(just_authed()) ? ((is_loading) ? '...' : '♡') : 'hi'}}
82 |
83 |
84 |
85 | I make {{service_name()}} 86 | 87 | use 88 | 89 | {{service_name(svc)}}? 90 | 91 | 92 | playlists of bands playing near you, tonight. 93 |
94 |   95 | 182 |
183 |
184 |
185 | Creating Playlist... 186 |
187 | 198 |
199 |
200 |
201 |
202 |
203 |
loading...
204 |
{{load_error}}
205 |
206 | 215 |
216 | Hmm. I couldn't find any results near you; maybe check your location under "options" above? 217 |
218 |
219 |
 
220 |
221 | 222 | 223 | 224 | 226 | 227 | 229 | 231 | 232 | 233 | 238 | 240 | 241 | 242 | 243 | 246 | 252 | 258 | 265 | 268 | 274 | 277 | 283 | 284 | 285 | 286 | 291 | 297 | 301 | 305 | 306 | 312 | 318 | 324 | 330 | 334 | 337 | 340 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | Get it on Google Play 353 | 354 |
355 |
356 |
357 | 358 | About Music Tonight 359 | 360 |
361 |
362 | powered by: 363 | 364 | 365 | 366 | 367 |
368 |
369 |
370 | 703 | 704 | 705 | --------------------------------------------------------------------------------