From b3abff99aec873993281e89735cb08bee4d94682 Mon Sep 17 00:00:00 2001 From: Jonathan Cremin Date: Wed, 3 Jun 2015 21:45:54 -0700 Subject: [PATCH] Switch to Koa, more es6 --- .eslintrc | 6 + app.js | 189 +- bin/www | 10 +- config/db.js | 17 + lib/lookup.js | 8 +- lib/react-handler.js | 19 + lib/services.js | 9 +- lib/services/beats/index.js | 150 - lib/services/beats/url.js | 11 - lib/services/deezer/index.js | 173 +- lib/services/itunes/index.js | 196 +- lib/services/rdio/index.js | 5 +- lib/services/xbox/index.js | 5 +- lib/services/youtube/index.js | 5 +- package.json | 63 +- public/images/logo-inverse.png | Bin 0 -> 4220 bytes public/javascript/bundle.js | 24968 +++++++++++++++++++++++++++++++ routes/index.js | 48 +- routes/itunes-proxy.js | 20 +- routes/search.js | 68 +- routes/share.js | 84 +- test/.eslintrc | 6 + test/lookup.js | 18 +- test/services/beats.js | 57 - test/services/deezer.js | 55 +- test/services/google.js | 55 +- test/services/itunes.js | 55 +- test/services/rdio.js | 39 +- test/services/spotify.js | 39 +- test/services/xbox.js | 39 +- test/services/youtube.js | 15 +- views/.eslintrc | 3 +- views/app.jsx | 48 +- views/faq.jsx | 12 +- views/home.jsx | 4 +- views/share.jsx | 2 +- 36 files changed, 25573 insertions(+), 928 deletions(-) create mode 100644 config/db.js create mode 100644 lib/react-handler.js delete mode 100644 lib/services/beats/index.js delete mode 100644 lib/services/beats/url.js create mode 100644 public/images/logo-inverse.png create mode 100644 public/javascript/bundle.js create mode 100644 test/.eslintrc delete mode 100644 test/services/beats.js diff --git a/.eslintrc b/.eslintrc index 7793c77..b95dfb3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,10 @@ { + "ecmaFeatures": { + "modules": true + }, + "rules": { + "quotes": [2, "single", "avoid-escape"] + }, "env": { "node": true, "es6": true diff --git a/app.js b/app.js index a559657..4dca616 100644 --- a/app.js +++ b/app.js @@ -1,127 +1,102 @@ -"use strict"; -var express = require("express"); -var helmet = require("helmet"); -var path = require("path"); -var favicon = require("serve-favicon"); -var logger = require("morgan"); -var session = require("express-session"); -var cookieParser = require("cookie-parser"); -var flash = require("connect-flash"); -var compress = require("compression"); -var bodyParser = require("body-parser"); -var pmongo = require("promised-mongo"); +import path from 'path'; +import koa from 'koa'; +import route from 'koa-route'; +import logger from 'koa-logger'; +import favicon from 'koa-favicon'; +import compress from 'koa-compress'; +import staticHandler from 'koa-static'; +import bodyparser from 'koa-bodyparser'; +import React from 'react'; +import co from 'co'; +import db from './config/db'; +import index from './routes/index'; +import search from './routes/search'; +import share from './routes/share'; +import itunesProxy from './routes/itunes-proxy'; +import {routes} from './views/app.jsx'; +import zlib from 'zlib'; +import createHandler from './lib/react-handler'; -var index = require("./routes/index"); -var search = require("./routes/search"); -var share = require("./routes/share"); -var itunesProxy = require("./routes/itunes-proxy"); +import debuglog from 'debug'; +const debug = debuglog('match.audio'); -var React = require("react"); -require("node-jsx").install({extension: ".jsx"}); +const app = koa(); -var ErrorView = React.createFactory(require("./views/error.jsx")); +app.use(function* (next) { + this.set('Server', 'Nintendo 64'); + try { + yield next; + } catch (err) { + if (!err.status) { + console.error(err.stack); + } else if (err.status === 404) { + let Handler = yield createHandler(routes, this.request.url); -var browserify = require("connect-browserify"); + let App = React.createFactory(Handler); + let content = React.renderToString(new App()); -var app = express(); - -var development = process.env.NODE_ENV !== "production"; - -// view engine setup -app.set("views", path.join(__dirname, "views")); -app.set("view engine", "ejs"); - -app.use(compress()); -app.use(favicon(path.join(__dirname, "/public/images/favicon.png"))); -app.use(helmet()); -app.use(logger("dev")); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: false })); -app.use(cookieParser()); -app.use(session({ - secret: "keyboard catz", - resave: false, - saveUninitialized: true -})); -app.use(flash()); -app.use(express.static(path.join(__dirname, "public"))); - -var dbUrl = process.env.MONGOHQ_URL || "mongodb://localhost/match-audio"; -app.use(function(req, res, next) { - req.db = res.db = pmongo(dbUrl, ["matches"]); - next(); + this.body = '\n' + content; + } else { + throw err; + } + } }); -if (development) { - app.get("/javascript/bundle.js", - browserify("./views/app.jsx", { - debug: true, - watch: true - })); -} +app.use(bodyparser()); +app.use(compress({flush: zlib.Z_SYNC_FLUSH })); +app.use(favicon(path.join(__dirname, '/public/images/favicon.png'))); +app.use(logger()); +app.use(staticHandler(path.join(__dirname, 'public'))); -app.get("*", function(req, res, next) { +let mongo = {}; + +co(function*() { + mongo = yield db(); +}); + +app.use(function* (next){ + this.db = mongo; + yield next; +}); + +app.use(function* (next) { // force SSL - if (req.headers["cf-visitor"] && req.headers["cf-visitor"] !== "{\"scheme\":\"https\"}") { - return res.redirect("https://" + req.headers.host + req.url); - } else if (req.headers["cf-visitor"]) { - req.userProtocol = "https"; + if (this.headers['cf-visitor'] && this.headers['cf-visitor'] !== '{"scheme":"https"}') { + return this.redirect('https://' + this.headers.host + this.url); + } else if (this.headers['cf-visitor']) { + this.userProtocol = 'https'; } else { - req.userProtocol = "http"; + this.userProtocol = 'http'; } // redirect www - if (req.headers.host.match(/^www/) !== null ) { - return res.redirect(req.userProtocol + "://" + req.headers.host.replace(/^www\./, "") + req.url); + if (this.headers.host.match(/^www/) !== null ) { + return this.redirect(this.userProtocol + '://' + this.headers.host.replace(/^www\./, '') + this.url); } else { - next(); + yield next; } }); -app.get("/", index); -app.post("/search", search); -app.get("/:service/:type/:id.:format?", share); -app.get("/itunes/*", itunesProxy); -app.get("/recent", function(req, res, next) { - req.db.matches.find().sort({"created_at": -1}).limit(6).toArray().then(function(docs){ - var recents = []; - docs.forEach(function(doc) { - recents.push(doc.services[doc._id.split("$$")[0]]); // eslint-disable-line no-underscore-dangle - }); - res.json({recents: recents}); - }).catch(function (error) { - return next(error); +app.use(route.get('/', index)); +app.use(route.post('/search', search)); +app.use(route.get('/itunes/(.*)', itunesProxy)); +app.use(route.get('/:service/:type/:id.:format?', share)); +app.use(route.get('/recent', function* () { + let recents = []; + let docs = yield this.db.matches.find().sort({'created_at': -1}).limit(6).toArray(); + docs.forEach(function(doc) { + recents.push(doc.services[doc._id.split('$$')[0]]); // eslint-disable-line no-underscore-dangle }); -}); - -// catch 404 and forward to error handler -app.use(function(req, res, next) { - var err = new Error("Not Found"); - err.status = 404; - next(err); -}); - -// error handlers - -// development error handler -// will print stacktrace -if (app.get("env") === "development") { - app.use(function(err, req, res) { - console.log(err.stack); - res.status(err.status || 500); - - var content = React.renderToString(new ErrorView({status: err.status || 500, message: err.message, error: err})); - res.send("\n" + content); - }); -} - -// production error handler -// no stacktraces leaked to user -app.use(function(err, req, res) { - res.status(err.status || 500); - - var content = React.renderToString(new ErrorView({status: err.status || 500, message: err.message, error: {status: err.status || 500}})); - res.send("\n" + content); -}); + this.body = {recents: recents}; +})); +app.use(route.get('*', function* () { + this.throw(404); +})); module.exports = app; + +if (!module.parent) { + app.listen(process.env.PORT || 3000, function() { + debug('Koa server listening on port ' + (process.env.PORT || 3000)); + }); +} diff --git a/bin/www b/bin/www index b446065..720b602 100755 --- a/bin/www +++ b/bin/www @@ -1,10 +1,10 @@ #!/usr/bin/env node "use strict"; -var debug = require('debug')('match.audio'); -var app = require('../app'); +var debug = require("debug")("match.audio"); +var app = require("../app"); -app.set('port', process.env.PORT || 3000); +app.set("port", process.env.PORT || 3000); -var server = app.listen(app.get('port'), function() { - debug('Express server listening on port ' + server.address().port); +var server = app.listen(app.get("port"), function() { + debug("Express server listening on port " + server.address().port); }); diff --git a/config/db.js b/config/db.js new file mode 100644 index 0000000..c1b5403 --- /dev/null +++ b/config/db.js @@ -0,0 +1,17 @@ +'use strict'; +const debug = require('debug')('match.audio'); + +// Shut mongodb-promisified up. +const dir = console.dir; +console.dir = function() {}; +const MongoClient = require('mongodb-promisified')().MongoClient; +console.dir = dir; + +const uristring = process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || 'mongodb://localhost:27017/match-audio'; + +module.exports = function*() { + const client = yield MongoClient.connect(uristring); + debug('Successfully connected to Mongodb'); + client.matches = client.collection('matches'); + return client; +}; diff --git a/lib/lookup.js b/lib/lookup.js index 25dc68d..e298b84 100644 --- a/lib/lookup.js +++ b/lib/lookup.js @@ -1,10 +1,10 @@ -"use strict"; -var path = require("path"); +import path from 'path'; +import fs from 'fs'; var services = []; -require("fs").readdirSync(path.join(__dirname, "services")).forEach(function(file) { - var service = require(path.join(__dirname, "services", file)); +fs.readdirSync(path.join(__dirname, 'services')).forEach(function(file) { + var service = require(path.join(__dirname, 'services', file)); if (service.search) { services.push(service); } diff --git a/lib/react-handler.js b/lib/react-handler.js new file mode 100644 index 0000000..98a04b6 --- /dev/null +++ b/lib/react-handler.js @@ -0,0 +1,19 @@ +var Router = require('react-router'); + +export default function* (routes, url) { + let router = Router.create({ + location: url, + routes: routes, + onAbort(aborted) { + let { to, params, query } = aborted; + + this.redirect(Router.makePath(to, params, query)); + } + }); + + return new Promise(function(resolve) { + router.run((Handler) => { + resolve(Handler); + }); + }); +} diff --git a/lib/services.js b/lib/services.js index e8e6110..4555747 100644 --- a/lib/services.js +++ b/lib/services.js @@ -1,12 +1,11 @@ -"use strict"; -var path = require("path"); +import path from 'path'; +import fs from 'fs'; module.exports = []; -require("fs").readdirSync(path.join(__dirname, "services")).forEach(function(file) { - var service = require(path.join(__dirname, "services", file)); +fs.readdirSync(path.join(__dirname, 'services')).forEach(function(file) { + var service = require(path.join(__dirname, 'services', file)); if (service.search) { module.exports.push(service); } }); - diff --git a/lib/services/beats/index.js b/lib/services/beats/index.js deleted file mode 100644 index 385012c..0000000 --- a/lib/services/beats/index.js +++ /dev/null @@ -1,150 +0,0 @@ -"use strict"; -var parse = require("url").parse; -var request = require("superagent"); -require("superagent-bluebird-promise"); - -module.exports.id = "beats"; - -if (!process.env.BEATS_KEY || !process.env.BEATS_SECRET) { - console.warn("BEATS_KEY or BEATS_SECRET environment variables not found, deactivating Beats."); - return; -} - -var credentials = { - key: process.env.BEATS_KEY, - secret: process.env.BEATS_SECRET -}; - -var apiRoot = "https://partner.api.beatsmusic.com/v1/api"; - -module.exports.match = require("./url").match; - -module.exports.parseUrl = function(url) { - var matches = parse(url).path.match(/\/albums[\/]+([^\/]+)(\/tracks\/)?([^\/]+)?/); - if (matches && matches[3]) { - return module.exports.lookupId(matches[3], "track"); - } else if (matches && matches[1]) { - return module.exports.lookupId(matches[1], "album"); - } else { - throw new Error("Url does not match"); - } -}; - -module.exports.lookupId = function(id, type) { - if (type === "album") { - return request.get(apiRoot + "/albums/" + id + "/images/default?size=large&client_id=" + credentials.key).redirects(0).promise().then(function(res) { - var artwork = {large: res.headers.location.replace("http:", "https:")}; - return request.get(apiRoot + "/albums/" + id + "/images/default?client_id=" + credentials.key).redirects(0).promise().then(function(res) { - artwork.small = res.headers.location.replace("http:", "https:"); - return request.get(apiRoot + "/albums/" + id + "?client_id=" + credentials.key).promise().then(function(res) { - if (!res.body.data) { - var error = new Error("Not Found"); - error.status = 404; - throw error; - } - var result = res.body.data; - return { - service: "beats", - type: "album", - id: result.id, - name: result.title, - streamUrl: "https://listen.beatsmusic.com/albums/" + result.id, - purchaseUrl: null, - artwork: artwork, - artist: { - name: result.artist_display_name - } - }; - }); - }); - }); - } else if (type === "track") { - return request.get(apiRoot + "/tracks/" + id + "?client_id=" + credentials.key).promise().then(function(res) { - if (!res.body.data) { - var error = new Error("Not Found"); - error.status = 404; - throw error; - } - var result = res.body.data; - return request.get(apiRoot + "/albums/" + result.refs.album.id + "/images/default?size=large&client_id=" + credentials.key).redirects(0).promise().then(function(res) { - var artwork = {large: res.headers.location.replace("http:", "https:")}; - return request.get(apiRoot + "/albums/" + result.refs.album.id + "/images/default?client_id=" + credentials.key).redirects(0).promise().then(function(res) { - artwork.small = res.headers.location.replace("http:", "https:"); - return { - service: "beats", - type: "track", - id: result.id, - name: result.title, - streamUrl: "https://listen.beatsmusic.com/albums/" + result.refs.album.id + "/tracks/" + result.id, - purchaseUrl: null, - artwork: artwork, - artist: { - name: result.artist_display_name - }, - album: { - name: result.refs.album.display - } - }; - }); - }); - }); - } else { - var error = new Error("Not Found"); - error.status = 404; - return error; - } -}; - -module.exports.search = function(data) { - var cleanParam = function(str) { - return str.replace(/[\:\?\&]+/, ""); - }; - var query, album; - var type = data.type; - - if (type === "album") { - query = "'" + cleanParam(data.artist.name) + "' '" + cleanParam(data.name) + "'"; - album = data.name; - } else if (type === "track") { - query = "'" + cleanParam(data.artist.name) + "' '" + cleanParam(data.name) + "'"; - if (data.album) { - album = data.album.name; - } else { - album = ""; - } - } - - var path = "/search?q=" + encodeURIComponent(query) + "&type=" + type + "&client_id=" + credentials.key; - - return request.get(apiRoot + path).promise().then(function(res) { - if (!res.body.data[0]) { - return {service: "beats"}; - } else { - var found; - var choppedAlbum = data.type === "album" ? cleanParam(data.name) : cleanParam(data.album.name); - var choppedArtist = cleanParam(data.artist.name); - - res.body.data.forEach(function(item) { - var matches = item.detail.match(/^[^\(\[]+/); - if(choppedArtist.indexOf(matches[0]) >= 0) { - found = item; - } - }); - - if (!found && !choppedAlbum.length) { - return module.exports.lookupId(res.body.data[0].id, type); - } - - res.body.data.forEach(function(item) { - var matches = item.related.display.match(/^[^\(\[]+/); - if(choppedAlbum.indexOf(matches[0]) >= 0) { - found = item; - } - }); - if (!found) { - return {service: "beats"}; - } - return module.exports.lookupId(found.id, type); - } - }); -}; diff --git a/lib/services/beats/url.js b/lib/services/beats/url.js deleted file mode 100644 index a8fda32..0000000 --- a/lib/services/beats/url.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; -var parse = require("url").parse; - -module.exports.match = function(url) { - var parsed = parse(url); - if (!parsed.host.match(/beatsmusic\.com$/)) { - return false; - } - var matches = parsed.path.match(/\/albums[\/]+([^\/]+)(\/tracks\/)?([^\/]+)?/); - return matches.length > 1; -}; diff --git a/lib/services/deezer/index.js b/lib/services/deezer/index.js index 30adcaa..8c3b894 100644 --- a/lib/services/deezer/index.js +++ b/lib/services/deezer/index.js @@ -1,109 +1,110 @@ -"use strict"; -var parse = require('url').parse; -var Promise = require('bluebird'); -var request = require('superagent'); -require('superagent-bluebird-promise'); +import {parse} from 'url'; +import request from 'superagent'; +import 'superagent-bluebird-promise'; -module.exports.id = "deezer"; +module.exports.id = 'deezer'; -var apiRoot = "https://api.deezer.com"; +const apiRoot = 'https://api.deezer.com'; module.exports.match = require('./url').match; module.exports.parseUrl = function(url) { - var matches = parse(url).path.match(/\/(album|track)[\/]+([^\/]+)/); + let matches = parse(url).path.match(/\/(album|track)[\/]+([^\/]+)/); if (matches && matches[2]) { return module.exports.lookupId(matches[2], matches[1]); } else { throw new Error(); } -} - -module.exports.lookupId = function(id, type) { - var path = "/" + type + "/" + id; - - return request.get(apiRoot + path).promise().then(function(res) { - var result = res.body; - if (res.body.error) { - var error = new Error("Not Found"); - error.status = 404; - throw error; - } - var cover = result.cover || result.album.cover; - return request.get(cover).redirects(0).promise().then(function(res) { - var artwork = { - small: res.headers.location.replace("120x120", "200x200"), - large: res.headers.location.replace("120x120", "800x800") - }; - if (type == "album") { - return { - service: "deezer", - type: type, - id: result.id, - name: result.title, - streamUrl: result.link, - purchaseUrl: null, - artwork: artwork, - artist: { - name: result.artist.name - }, - }; - } else if (type == "track") { - return { - service: "deezer", - type: type, - id: result.id, - name: result.title, - streamUrl: result.album.link, - purchaseUrl: null, - artwork: artwork, - artist: { - name: result.artist.name - }, - album: { - name: result.album.title - } - }; - } else { - throw new Error(); - } - }); - }); }; -module.exports.search = function(data) { - var cleanParam = function(str) { - return str.replace(/[\:\?\&]+/, ""); - } - var query, album; - var type = data.type; +module.exports.lookupId = function* (id, type) { + let path = '/' + type + '/' + id; - if (type == "album") { - query = cleanParam(data.artist.name) + " " + cleanParam(data.name); + let {body} = yield request.get(apiRoot + path).promise(); + if (!body || body.error) { + let error = new Error('Not Found'); + error.status = 404; + return Promise.reject(error); + } + let item = body; + let coverUrl = item.cover || item.album.cover; + let cover = 'test'; + // nasty hacks for superagent-bluebird-promise + try { + cover = yield request.get(coverUrl).redirects(0); + } catch(err) { + cover = err.message.response.res; + } + let artwork = { + small: cover.headers.location.replace('120x120', '200x200'), + large: cover.headers.location.replace('120x120', '800x800') + }; + if (type === 'album') { + return Promise.resolve({ + service: 'deezer', + type: type, + id: item.id, + name: item.title, + streamUrl: item.link, + purchaseUrl: null, + artwork: artwork, + artist: { + name: item.artist.name + } + }); + } else if (type === 'track') { + return Promise.resolve({ + service: 'deezer', + type: type, + id: item.id, + name: item.title, + streamUrl: item.album.link, + purchaseUrl: null, + artwork: artwork, + artist: { + name: item.artist.name + }, + album: { + name: item.album.title + } + }); + } else { + return Promise.reject(new Error()); + } +}; + +module.exports.search = function* (data) { + let cleanParam = function(str) { + return str.replace(/[\:\?\&]+/, ''); + }; + let query, album; + let {type} = data; + + if (type === 'album') { + query = cleanParam(data.artist.name) + ' ' + cleanParam(data.name); album = data.name; - } else if (type == "track") { - query = cleanParam(data.artist.name) + " " + cleanParam(data.album.name) + " " + cleanParam(data.name); + } else if (type === 'track') { + query = cleanParam(data.artist.name) + ' ' + cleanParam(data.album.name) + ' ' + cleanParam(data.name); album = data.album.name; } - var path = "/search/" + type + "?q=" + encodeURIComponent(query); - return request.get(apiRoot + path).promise().then(function(res) { - if (!res.body.data[0]) { - var matches = album.match(/^[^\(\[]+/); - if (matches && matches[0] && matches[0] != album) { - var cleanedData = JSON.parse(JSON.stringify(data)); - if (type == "album") { - cleanedData.name = matches[0].trim(); - } else if (type == "track") { - cleanedData.album.name = matches[0].trim(); - } - return module.exports.search(cleanedData); - } else { - return {service: "deezer"}; + var path = '/search/' + type + '?q=' + encodeURIComponent(query); + let response = yield request.get(apiRoot + path); + if (response.body.data[0]) { + return yield module.exports.lookupId(response.body.data[0].id, type); + } else { + var matches = album.match(/^[^\(\[]+/); + if (matches && matches[0] && matches[0] !== album) { + var cleanedData = JSON.parse(JSON.stringify(data)); + if (type === 'album') { + cleanedData.name = matches[0].trim(); + } else if (type === 'track') { + cleanedData.album.name = matches[0].trim(); } + return yield module.exports.search(cleanedData); } else { - return module.exports.lookupId(res.body.data[0].id, type); + return Promise.resolve({service: 'deezer'}); } - }); + } }; diff --git a/lib/services/itunes/index.js b/lib/services/itunes/index.js index ba37136..6801930 100644 --- a/lib/services/itunes/index.js +++ b/lib/services/itunes/index.js @@ -1,138 +1,134 @@ -"use strict"; -var parse = require("url").parse; -var Promise = require('bluebird'); -var querystring = require('querystring'); -var request = require('superagent'); -require('superagent-bluebird-promise'); +import {parse} from 'url'; +import querystring from 'querystring'; +import request from 'superagent'; +import 'superagent-bluebird-promise'; -module.exports.id = "itunes"; +module.exports.id = 'itunes'; -var apiRoot = "https://itunes.apple.com"; +const apiRoot = 'https://itunes.apple.com'; module.exports.match = require('./url').match; -module.exports.parseUrl = function(url) { - var parsed = parse(url); - var matches = parsed.path.match(/[\/]?([\/]?[a-z]{2}?)?[\/]+album[\/]+([^\/]+)[\/]+([^\?]+)/); - var query = querystring.parse(parsed.query); +module.exports.parseUrl = function* (url) { + let parsed = parse(url); + let matches = parsed.path.match(/[\/]?([\/]?[a-z]{2}?)?[\/]+album[\/]+([^\/]+)[\/]+([^\?]+)/); + let query = querystring.parse(parsed.query); if (matches) { - var type = "album"; - var id = matches[3].substr(2); + let type = 'album'; + let id = matches[3].substr(2); if (query.i) { - type = "track"; + type = 'track'; id = query.i; } - return module.exports.lookupId(id, type, matches[1] || "us"); + return yield module.exports.lookupId(id, type, matches[1] || 'us'); } else { - throw new Error(); + return Promise.reject(new Error()); } }; -module.exports.lookupId = function(id, type, cc) { +module.exports.lookupId = function* (id, type, cc) { if (String(id).match(/^[a-z]{2}/)) { - cc = id.substr(0,2); + cc = id.substr(0, 2); id = id.substr(2); } - var path = "/lookup?id=" + id; + let path = '/lookup?id=' + id; if (cc) { - path = "/" + cc + path; + path = '/' + cc + path; } - return request.get(apiRoot + path).promise().then(function(res) { - var data = JSON.parse(res.text); + let response = yield request.get(apiRoot + path); + let result = JSON.parse(response.text); - if (!data.results || data.resultCount == 0 || !data.results[0].collectionId) { - var error = new Error("Not Found"); - error.status = 404; - throw error; - } else { - var result = data.results[0]; + if (!result.results || result.resultCount === 0 || !result.results[0].collectionId) { + let error = new Error('Not Found'); + error.status = 404; + return Promise.reject(error); + } else { + result = result.results[0]; - var item = { - service: "itunes", - type: type, - id: cc + id, - name: result.trackName ? result.trackName : result.collectionName, - streamUrl: null, - purchaseUrl: result.collectionViewUrl, - artwork: { - small: "https://match.audio/itunes/" + result.artworkUrl100.replace("100x100", "200x200").replace("http://", ""), - large: "https://match.audio/itunes/" + result.artworkUrl100.replace("100x100", "600x600").replace("http://", ""), - }, - artist: { - name: result.artistName - } - }; - - if (type == "track") { - item.album = { - name: result.collectionName - }; + let item = { + service: 'itunes', + type: type, + id: cc + id, + name: result.trackName ? result.trackName : result.collectionName, + streamUrl: null, + purchaseUrl: result.collectionViewUrl, + artwork: { + small: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '200x200').replace('http://', ''), + large: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '600x600').replace('http://', '') + }, + artist: { + name: result.artistName } + }; - return item; + if (type === 'track') { + item.album = { + name: result.collectionName + }; } - }); + + return Promise.resolve(item); + } }; -module.exports.search = function(data) { - var query, album, entity; - var type = data.type; +module.exports.search = function* (data) { + let query, album, entity; + let type = data.type; - if (type == "album") { - query = data.artist.name + " " + data.name; + if (type === 'album') { + query = data.artist.name + ' ' + data.name; album = data.name; - entity = "album"; - } else if (type == "track") { - query = data.artist.name + " " + data.album.name + " " + data.name; + entity = 'album'; + } else if (type === 'track') { + query = data.artist.name + ' ' + data.album.name + ' ' + data.name; album = data.album.name; - entity = "musicTrack"; + entity = 'musicTrack'; } - var path = "/search?term=" + encodeURIComponent(query) + "&media=music&entity=" + entity; - return request.get(apiRoot + path).promise().then(function(res) { - var result = JSON.parse(res.text); + let path = '/search?term=' + encodeURIComponent(query) + '&media=music&entity=' + entity; + let response = yield request.get(apiRoot + path); + let result = JSON.parse(response.text); - if (!result.results[0]) { - var matches = album.match(/^[^\(\[]+/); - if (matches && matches[0] && matches[0] != album) { - var cleanedData = JSON.parse(JSON.stringify(data)); - if (type == "album") { - cleanedData.name = matches[0].trim(); - } else if (type == "track") { - cleanedData.album.name = matches[0].trim(); - } - return module.exports.search(cleanedData); - } else { - return {service: "itunes"}; + if (!result.results[0]) { + let matches = album.match(/^[^\(\[]+/); + if (matches && matches[0] && matches[0] !== album) { + let cleanedData = JSON.parse(JSON.stringify(data)); + if (type === 'album') { + cleanedData.name = matches[0].trim(); + } else if (type === 'track') { + cleanedData.album.name = matches[0].trim(); } + return yield module.exports.search(cleanedData); } else { - var result = result.results[0]; - - var item = { - service: "itunes", - type: type, - id: "us" + result.collectionId, - name: result.trackName ? result.trackName : result.collectionName, - streamUrl: null, - purchaseUrl: result.collectionViewUrl, - artwork: { - small: "https://match.audio/itunes/" + result.artworkUrl100.replace("100x100", "200x200").replace("http://", ""), - large: "https://match.audio/itunes/" + result.artworkUrl100.replace("100x100", "600x600").replace("http://", ""), - }, - artist: { - name: result.artistName - } - }; - - if (type == "track") { - item.album = { - name: result.collectionName - }; - } - return item; + return Promise.resolve({service: 'itunes'}); } - }); + } else { + result = result.results[0]; + + let item = { + service: 'itunes', + type: type, + id: 'us' + result.collectionId, + name: result.trackName ? result.trackName : result.collectionName, + streamUrl: null, + purchaseUrl: result.collectionViewUrl, + artwork: { + small: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '200x200').replace('http://', ''), + large: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '600x600').replace('http://', '') + }, + artist: { + name: result.artistName + } + }; + + if (type === 'track') { + item.album = { + name: result.collectionName + }; + } + return Promise.resolve(item); + } }; diff --git a/lib/services/rdio/index.js b/lib/services/rdio/index.js index 1714890..606b0fa 100644 --- a/lib/services/rdio/index.js +++ b/lib/services/rdio/index.js @@ -6,8 +6,7 @@ module.exports.id = "rdio"; if (!process.env.RDIO_API_KEY || !process.env.RDIO_API_SHARED) { console.warn("RDIO_API_KEY or RDIO_API_SHARED environment variables not found, deactivating Rdio."); - return; -} +} else { var rdio = require('rdio')({ rdio_api_key: process.env.RDIO_API_KEY, @@ -178,3 +177,5 @@ module.exports.search = function(data) { } }); }; + +} \ No newline at end of file diff --git a/lib/services/xbox/index.js b/lib/services/xbox/index.js index 1b43174..0f5b791 100644 --- a/lib/services/xbox/index.js +++ b/lib/services/xbox/index.js @@ -8,8 +8,7 @@ module.exports.id = "xbox"; if (!process.env.XBOX_CLIENT_ID || !process.env.XBOX_CLIENT_SECRET) { console.warn("XBOX_CLIENT_ID and XBOX_CLIENT_SECRET environment variables not found, deactivating Xbox Music."); - return; -} +} else { var credentials = { clientId: process.env.XBOX_CLIENT_ID, @@ -104,3 +103,5 @@ module.exports.search = function(data) { }); }); }; + +} diff --git a/lib/services/youtube/index.js b/lib/services/youtube/index.js index f273697..9141ca3 100644 --- a/lib/services/youtube/index.js +++ b/lib/services/youtube/index.js @@ -11,8 +11,7 @@ module.exports.id = "youtube"; if (!process.env.YOUTUBE_KEY) { console.warn("YOUTUBE_KEY environment variable not found, deactivating Youtube."); - return; -} +} else { var credentials = { key: process.env.YOUTUBE_KEY, @@ -136,3 +135,5 @@ module.exports.search = function(data) { return {service: "youtube"}; }); }; + +} diff --git a/package.json b/package.json index 72132c9..f36e660 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,13 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "node ./bin/www", - "test": "./node_modules/mocha/bin/mocha test/**/*.js --timeout=10000", - "build": "browserify ./views/app.jsx | uglifyjs -cm 2>/dev/null > ./public/javascript/bundle.js", + "start": "babel-node app.js", + "test": "mocha --require co-mocha --compilers js:babel/register test/**/*.js --timeout=10000", + "build": "browserify -t babelify ./views/app.jsx > ./public/javascript/bundle.js", "clean": "rm -f ./public/javascript/bundle.js" }, "engines": { - "node": "0.12.x" + "iojs": "2.2.1" }, "browserify": { "transform": [ @@ -22,38 +22,39 @@ ] }, "dependencies": { - "bluebird": "^2.4.2", - "body-parser": "~1.10.0", - "browserify": "^7.0.0", - "compression": "^1.2.2", - "connect-browserify": "^3.2.1", - "connect-flash": "^0.1.1", - "cookie-parser": "~1.3.3", - "crypto-js": "^3.1.2-5", - "debug": "~2.1.0", - "envify": "^3.2.0", - "express": "~4.10.6", - "express-session": "^1.9.2", - "helmet": "^0.5.2", - "moment": "^2.9.0", - "morgan": "~1.5.0", - "node-jsx": "^0.12.4", + "babel": "^5.2.17", + "babelify": "^6.0.2", + "bluebird": "^2.9.25", + "browserify": "^10.1.3", + "co": "^4.5.4", + "debug": "^2.1.1", + "koa": "^0.21.0", + "koa-bodyparser": "^2.0.0", + "koa-compress": "^1.0.8", + "koa-favicon": "^1.2.0", + "koa-logger": "^1.2.2", + "koa-route": "^2.4.0", + "koa-static": "^1.4.9", + "moment": "^2.10.3", + "mongodb-promisified": "^1.0.2", "node-uuid": "^1.4.2", - "playmusic": "^2.0.2", - "promised-mongo": "^0.11.1", + "playmusic": "^2.0.0", "rdio": "^1.5.2", - "react": "^0.12.1", + "react": "^0.13.3", "react-google-analytics": "^0.2.0", - "react-router": "^0.11.6", - "reactify": "^0.17.1", - "serve-favicon": "~2.2.0", + "react-router": "^0.13.3", + "reactify": "^1.1.1", "spotify": "^0.3.0", - "superagent": "^0.21.0", - "superagent-bluebird-promise": "^0.5.1", - "uglify-js": "^2.4.16" + "superagent": "^1.2.0", + "superagent-bluebird-promise": "^2.0.2" }, "devDependencies": { - "should": "^4.4.1", - "mocha": "^2.0.1" + "co-mocha": "^1.1.0", + "eslint": "^0.22.1", + "eslint-plugin-react": "^2.5.0", + "mocha": "^2.1.0", + "nodemon": "^1.3.7", + "parallelshell": "^1.1.1", + "should": "^6.0.3" } } diff --git a/public/images/logo-inverse.png b/public/images/logo-inverse.png new file mode 100644 index 0000000000000000000000000000000000000000..ee5e28605b486883bfb1739c1e117b345e931ba5 GIT binary patch literal 4220 zcmZ`+2{=^y8y;D*?|Wzr(Pjoinz3XXTZT!xQ6>#DrZEe57F*c{l_i;*o3fOp>}3m4 zxrk5+B_Ye$LuJjb{-}Gee@}OwIdjf=zwiCM-~0W}H_vk-QFfL>{PO$&06@sv%G_b& zjM_YSc{YAy#b)mT031F9Gc%O6nHdm8_3|K)Z~(ww79+0ZfcY`Wh9f*zWIarZ3;ht% z@^56t^`X~zoX??x$bANLX3a70lGL#o2T2+pVI-?HZj$P^5C?vNuFlh_Ly5O&_;#xv z_+rrK@$*$FwCa&JNa%5i5YnqMx3udW_bDWbID<&r?Xuf)mND#kp@Gk)xpD4y=NZnm8ib9a+wAZ%_n>kdWGd8=)q6uwH>Ta9#RKb0LzVy;PbX}rU3u~5}OAHATe1M z0N?@<9G&P+NL#qO7a5Gfdb#1ieq`^BYyiN(5593p#?diAKQf6zgZmkRwld%w*PAc| z1l&rY6AeL5NEFb_i;4s4f_1=}AR~St5NJTfdcYmbEq>59?hHZa=yY#51mf%K3-*PA zy{LGI77PZ1Xlg^WwKX;}G-&=5I>t|fLR0y^$lvRj<7n}KENqLi4f;8IOUC)XWVQvi z5wY%Y8pF*K=T6_YTN`_}_3WEs@Q`h=0pyq21{+TT9O;E6c=(%R=r|*7O)VV_Eo}`= zJx6VAxYow^kOAbUoNX0sM$D)<4Bdg1 zc*-m!ap&&4$1D^s)DzS0`%}3y^Ggjgwlr?!{z1;PopE515FKtg0e`7t83)f+9`col zJ5ONEz7p&U+{0>(7FM*-vT-xSoR-)d?c`5@3eIAoKcre^R_eg2k3zzZ154xb)1_mP zO$uS&xgx{p#WfzJK33#Kf7yIy zO?_OOwGjSbzs(Yqd#+*SVNkuA=uy}l?^keZD~#zW)3}GavA2tOx%^$IfxP32 z4UNa>pQM-Ie5%wOC&?(a)_eZ>6C*aaEM=_~&?F>jWF)G)GEgarTyf1(-s#z0%FT-1 zkxVXAG0VNevLhX12(hQdV4oLC4D^5%Z~V*9i(RZ!w#oVZ2a4ByS>0r}d#519grr70 z!6r6##&2J6)adHY6EPXjJ72L(Y4y@Mpac7`M#o6 zs}nm}>pLa#Sh1fF7Q#{tzS`)pRrPlgDKrr~5zfe@FmNv_=b z-~+Q$19hJAIMN#)T3AU%dh8=Wfp|eqHtVdaM zT}3=Jq~1B4GSmv`3)Gl5PLLyAO8lA~@im2zE!QC!w{uU&0oi2%*1JV4OF1hV&Z%-v z_j4DY>@=*ntf2=z(C2W_fS#eS)*x5(tvYpS(#!4M`@OS6kQ03Mz)IEG$1#|%?#kfrL!H>^I6L=Bl2Nl4KXI%k!p>A-N6sX% zer5V7ODfM?cu7hnOH|LKS6?Dy9#-K#?m$Y*N|PSE@7aPiVLN>ct_{mUl^@OTvXA^I zd_&Tm?wH_uSqjnOsI~*@KH~OdnZpeMEKzn680!fui#QP@mn^d5$ld*cq7~iY6yn#_ zD@}O@ukLeC+?up8Q&KGWOjz`tsG3B?PQ|;%8?M#`aL(D&rYnm1=vm+!#&_#lNyWCg5f!Y_mQ|*HP7`LW~ix24{02pd-*WY@x^c(=;_W;DlPu28-sEL{ajg;T(N6j z&^k*1di{jUdIh^3^Guc1(YHQhr!ymcjix)a18+2*Smu5u`DA8;D78U6He_k8#ykLq zTjriI5P9+_TJuZ-x8Uyh-O&85ZolhsFYT#^m|V4{Hih5bj0HYzCGGFdboAV-zuuap zm8s)1{61zSR24O;D(*Rgew!HaMCWLBu4Grc+&a~B9Ivsa8)jm``(Rb7SD6M4LKA1_ zo18+u(VuEheJM$>eN;*W!t>)>r}nDs7Uae9rHGCY?$z@wS3ei5EaCYrvm@e5fyl+w z*Ic|85Ihp(Hj%QoylX;%Wpa**%F{HTX{J#q)=S?>$943FRA&y!TlOhMfKJyNrTHp$kdtQC4N1X>E-EY(CLXa zvyMS~L@z_gjhP%IU;BJ2Mbt>8Bg7DSX0{^Rk|Ks_Eek&@@vN>9C`-Z9YUnkwHpJns z-%wV>&!-Dl%H=XvQ>PFgXial}QhJp;Uik5UKo-2zJ>AbGzSm(R{?Wn5t zziz?|atqN1FoG|W>RfDZvw2*8Ygevn6FsyyKToV{H31qzuXk!Ht?ZML#sOzRx2qfT z_){F`lo(*l`vD*mkoz&)SBWYO)R4F0iOie*Y1Omw3pz z@P=FBp}}mmevjDPeP*omDMmjYI0$3J41#*D(>T3p*3t4kY@SHu68{QO7KHI&-FN(WvoBu;vDY$SECe+c1s{bg9g7E;cluK zGQsD1jQD3!+KsQRkd}sdj{b7=tMEBvkph+9Q_3aqd1rzS`@$-Qh@Ovp#$t8xQ8ibt za~JK#mOpxHR)LwHCSpQg9iMW4c!1l{IA=Y#{N^XyZJ?n-66in{=2 zE|`J+K?-44aWU-nK2V@3GxLH`vk2{FXiuEVZhtOjm)YUD3|2J9cCJzL30#`C`J-aW$}QD%&DvBr5de;?Mpghajl0KH_v^;*d)nb{1=Jk5rR zE~O#QNGyO6y670XK&X;3yLLp?w+_kOVSLL>13OmA^E#C_(B;m59@X7?Rfv-a9OoGN z%ME&M++6`$Q$MOHY|4k_!Ma>-p2OU{8alMbPE-g7J>WYJYmJ9VtjjVNaaZ@{n-dyB ztAo>R&~e5dJdr0lFYq)q60_!W@33_!f{l3)?A^UPa=28f)J)E$y3`cUT=I45NEd}&)@cXSUtl&=#hrNEQ>|SzS`>p&kq&L2=u6SEDuyKp zI7u(~Bip^W zSPi!T`}vx>TYzuWGdOA=xFAL^m1rEPQ*1)9)J?F?-y|=z9jyQ@M?HQ?HoaW>!tdmg z2kF%AgCCp(2J)oi)zClick me! + * ); + * } + * }); + */ +var Navigation = { + + contextTypes: { + router: PropTypes.router.isRequired + }, + + /** + * Returns an absolute URL path created from the given route + * name, URL parameters, and query values. + */ + makePath: function makePath(to, params, query) { + return this.context.router.makePath(to, params, query); + }, + + /** + * Returns a string that may safely be used as the href of a + * link to the route with the given name. + */ + makeHref: function makeHref(to, params, query) { + return this.context.router.makeHref(to, params, query); + }, + + /** + * Transitions to the URL specified in the arguments by pushing + * a new URL onto the history stack. + */ + transitionTo: function transitionTo(to, params, query) { + this.context.router.transitionTo(to, params, query); + }, + + /** + * Transitions to the URL specified in the arguments by replacing + * the current URL in the history stack. + */ + replaceWith: function replaceWith(to, params, query) { + this.context.router.replaceWith(to, params, query); + }, + + /** + * Transitions to the previous URL. + */ + goBack: function goBack() { + return this.context.router.goBack(); + } + +}; + +module.exports = Navigation; +},{"./PropTypes":7}],6:[function(require,module,exports){ +'use strict'; + +var invariant = require('react/lib/invariant'); +var assign = require('object-assign'); +var qs = require('qs'); + +var paramCompileMatcher = /:([a-zA-Z_$][a-zA-Z0-9_$]*)|[*.()\[\]\\+|{}^$]/g; +var paramInjectMatcher = /:([a-zA-Z_$][a-zA-Z0-9_$?]*[?]?)|[*]/g; +var paramInjectTrailingSlashMatcher = /\/\/\?|\/\?\/|\/\?/g; +var queryMatcher = /\?(.*)$/; + +var _compiledPatterns = {}; + +function compilePattern(pattern) { + if (!(pattern in _compiledPatterns)) { + var paramNames = []; + var source = pattern.replace(paramCompileMatcher, function (match, paramName) { + if (paramName) { + paramNames.push(paramName); + return '([^/?#]+)'; + } else if (match === '*') { + paramNames.push('splat'); + return '(.*?)'; + } else { + return '\\' + match; + } + }); + + _compiledPatterns[pattern] = { + matcher: new RegExp('^' + source + '$', 'i'), + paramNames: paramNames + }; + } + + return _compiledPatterns[pattern]; +} + +var PathUtils = { + + /** + * Returns true if the given path is absolute. + */ + isAbsolute: function isAbsolute(path) { + return path.charAt(0) === '/'; + }, + + /** + * Joins two URL paths together. + */ + join: function join(a, b) { + return a.replace(/\/*$/, '/') + b; + }, + + /** + * Returns an array of the names of all parameters in the given pattern. + */ + extractParamNames: function extractParamNames(pattern) { + return compilePattern(pattern).paramNames; + }, + + /** + * Extracts the portions of the given URL path that match the given pattern + * and returns an object of param name => value pairs. Returns null if the + * pattern does not match the given path. + */ + extractParams: function extractParams(pattern, path) { + var _compilePattern = compilePattern(pattern); + + var matcher = _compilePattern.matcher; + var paramNames = _compilePattern.paramNames; + + var match = path.match(matcher); + + if (!match) { + return null; + }var params = {}; + + paramNames.forEach(function (paramName, index) { + params[paramName] = match[index + 1]; + }); + + return params; + }, + + /** + * Returns a version of the given route path with params interpolated. Throws + * if there is a dynamic segment of the route path for which there is no param. + */ + injectParams: function injectParams(pattern, params) { + params = params || {}; + + var splatIndex = 0; + + return pattern.replace(paramInjectMatcher, function (match, paramName) { + paramName = paramName || 'splat'; + + // If param is optional don't check for existence + if (paramName.slice(-1) === '?') { + paramName = paramName.slice(0, -1); + + if (params[paramName] == null) return ''; + } else { + invariant(params[paramName] != null, 'Missing "%s" parameter for path "%s"', paramName, pattern); + } + + var segment; + if (paramName === 'splat' && Array.isArray(params[paramName])) { + segment = params[paramName][splatIndex++]; + + invariant(segment != null, 'Missing splat # %s for path "%s"', splatIndex, pattern); + } else { + segment = params[paramName]; + } + + return segment; + }).replace(paramInjectTrailingSlashMatcher, '/'); + }, + + /** + * Returns an object that is the result of parsing any query string contained + * in the given path, null if the path contains no query string. + */ + extractQuery: function extractQuery(path) { + var match = path.match(queryMatcher); + return match && qs.parse(match[1]); + }, + + /** + * Returns a version of the given path without the query string. + */ + withoutQuery: function withoutQuery(path) { + return path.replace(queryMatcher, ''); + }, + + /** + * Returns a version of the given path with the parameters in the given + * query merged into the query string. + */ + withQuery: function withQuery(path, query) { + var existingQuery = PathUtils.extractQuery(path); + + if (existingQuery) query = query ? assign(existingQuery, query) : existingQuery; + + var queryString = qs.stringify(query, { arrayFormat: 'brackets' }); + + if (queryString) { + return PathUtils.withoutQuery(path) + '?' + queryString; + }return PathUtils.withoutQuery(path); + } + +}; + +module.exports = PathUtils; +},{"object-assign":35,"qs":36,"react/lib/invariant":175}],7:[function(require,module,exports){ +'use strict'; + +var assign = require('react/lib/Object.assign'); +var ReactPropTypes = require('react').PropTypes; +var Route = require('./Route'); + +var PropTypes = assign({}, ReactPropTypes, { + + /** + * Indicates that a prop should be falsy. + */ + falsy: function falsy(props, propName, componentName) { + if (props[propName]) { + return new Error('<' + componentName + '> should not have a "' + propName + '" prop'); + } + }, + + /** + * Indicates that a prop should be a Route object. + */ + route: ReactPropTypes.instanceOf(Route), + + /** + * Indicates that a prop should be a Router object. + */ + //router: ReactPropTypes.instanceOf(Router) // TODO + router: ReactPropTypes.func + +}); + +module.exports = PropTypes; +},{"./Route":9,"react":195,"react/lib/Object.assign":66}],8:[function(require,module,exports){ +/** + * Encapsulates a redirect to the given route. + */ +"use strict"; + +function Redirect(to, params, query) { + this.to = to; + this.params = params; + this.query = query; +} + +module.exports = Redirect; +},{}],9:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var assign = require('react/lib/Object.assign'); +var invariant = require('react/lib/invariant'); +var warning = require('react/lib/warning'); +var PathUtils = require('./PathUtils'); + +var _currentRoute; + +var Route = (function () { + function Route(name, path, ignoreScrollBehavior, isDefault, isNotFound, onEnter, onLeave, handler) { + _classCallCheck(this, Route); + + this.name = name; + this.path = path; + this.paramNames = PathUtils.extractParamNames(this.path); + this.ignoreScrollBehavior = !!ignoreScrollBehavior; + this.isDefault = !!isDefault; + this.isNotFound = !!isNotFound; + this.onEnter = onEnter; + this.onLeave = onLeave; + this.handler = handler; + } + + _createClass(Route, [{ + key: 'appendChild', + + /** + * Appends the given route to this route's child routes. + */ + value: function appendChild(route) { + invariant(route instanceof Route, 'route.appendChild must use a valid Route'); + + if (!this.childRoutes) this.childRoutes = []; + + this.childRoutes.push(route); + } + }, { + key: 'toString', + value: function toString() { + var string = ''; + + return string; + } + }], [{ + key: 'createRoute', + + /** + * Creates and returns a new route. Options may be a URL pathname string + * with placeholders for named params or an object with any of the following + * properties: + * + * - name The name of the route. This is used to lookup a + * route relative to its parent route and should be + * unique among all child routes of the same parent + * - path A URL pathname string with optional placeholders + * that specify the names of params to extract from + * the URL when the path matches. Defaults to `/${name}` + * when there is a name given, or the path of the parent + * route, or / + * - ignoreScrollBehavior True to make this route (and all descendants) ignore + * the scroll behavior of the router + * - isDefault True to make this route the default route among all + * its siblings + * - isNotFound True to make this route the "not found" route among + * all its siblings + * - onEnter A transition hook that will be called when the + * router is going to enter this route + * - onLeave A transition hook that will be called when the + * router is going to leave this route + * - handler A React component that will be rendered when + * this route is active + * - parentRoute The parent route to use for this route. This option + * is automatically supplied when creating routes inside + * the callback to another invocation of createRoute. You + * only ever need to use this when declaring routes + * independently of one another to manually piece together + * the route hierarchy + * + * The callback may be used to structure your route hierarchy. Any call to + * createRoute, createDefaultRoute, createNotFoundRoute, or createRedirect + * inside the callback automatically uses this route as its parent. + */ + value: function createRoute(options, callback) { + options = options || {}; + + if (typeof options === 'string') options = { path: options }; + + var parentRoute = _currentRoute; + + if (parentRoute) { + warning(options.parentRoute == null || options.parentRoute === parentRoute, 'You should not use parentRoute with createRoute inside another route\'s child callback; it is ignored'); + } else { + parentRoute = options.parentRoute; + } + + var name = options.name; + var path = options.path || name; + + if (path && !(options.isDefault || options.isNotFound)) { + if (PathUtils.isAbsolute(path)) { + if (parentRoute) { + invariant(path === parentRoute.path || parentRoute.paramNames.length === 0, 'You cannot nest path "%s" inside "%s"; the parent requires URL parameters', path, parentRoute.path); + } + } else if (parentRoute) { + // Relative paths extend their parent. + path = PathUtils.join(parentRoute.path, path); + } else { + path = '/' + path; + } + } else { + path = parentRoute ? parentRoute.path : '/'; + } + + if (options.isNotFound && !/\*$/.test(path)) path += '*'; // Auto-append * to the path of not found routes. + + var route = new Route(name, path, options.ignoreScrollBehavior, options.isDefault, options.isNotFound, options.onEnter, options.onLeave, options.handler); + + if (parentRoute) { + if (route.isDefault) { + invariant(parentRoute.defaultRoute == null, '%s may not have more than one default route', parentRoute); + + parentRoute.defaultRoute = route; + } else if (route.isNotFound) { + invariant(parentRoute.notFoundRoute == null, '%s may not have more than one not found route', parentRoute); + + parentRoute.notFoundRoute = route; + } + + parentRoute.appendChild(route); + } + + // Any routes created in the callback + // use this route as their parent. + if (typeof callback === 'function') { + var currentRoute = _currentRoute; + _currentRoute = route; + callback.call(route, route); + _currentRoute = currentRoute; + } + + return route; + } + }, { + key: 'createDefaultRoute', + + /** + * Creates and returns a route that is rendered when its parent matches + * the current URL. + */ + value: function createDefaultRoute(options) { + return Route.createRoute(assign({}, options, { isDefault: true })); + } + }, { + key: 'createNotFoundRoute', + + /** + * Creates and returns a route that is rendered when its parent matches + * the current URL but none of its siblings do. + */ + value: function createNotFoundRoute(options) { + return Route.createRoute(assign({}, options, { isNotFound: true })); + } + }, { + key: 'createRedirect', + + /** + * Creates and returns a route that automatically redirects the transition + * to another route. In addition to the normal options to createRoute, this + * function accepts the following options: + * + * - from An alias for the `path` option. Defaults to * + * - to The path/route/route name to redirect to + * - params The params to use in the redirect URL. Defaults + * to using the current params + * - query The query to use in the redirect URL. Defaults + * to using the current query + */ + value: function createRedirect(options) { + return Route.createRoute(assign({}, options, { + path: options.path || options.from || '*', + onEnter: function onEnter(transition, params, query) { + transition.redirect(options.to, options.params || params, options.query || query); + } + })); + } + }]); + + return Route; +})(); + +module.exports = Route; +},{"./PathUtils":6,"react/lib/Object.assign":66,"react/lib/invariant":175,"react/lib/warning":194}],10:[function(require,module,exports){ +'use strict'; + +var invariant = require('react/lib/invariant'); +var canUseDOM = require('react/lib/ExecutionEnvironment').canUseDOM; +var getWindowScrollPosition = require('./getWindowScrollPosition'); + +function shouldUpdateScroll(state, prevState) { + if (!prevState) { + return true; + } // Don't update scroll position when only the query has changed. + if (state.pathname === prevState.pathname) { + return false; + }var routes = state.routes; + var prevRoutes = prevState.routes; + + var sharedAncestorRoutes = routes.filter(function (route) { + return prevRoutes.indexOf(route) !== -1; + }); + + return !sharedAncestorRoutes.some(function (route) { + return route.ignoreScrollBehavior; + }); +} + +/** + * Provides the router with the ability to manage window scroll position + * according to its scroll behavior. + */ +var ScrollHistory = { + + statics: { + + /** + * Records curent scroll position as the last known position for the given URL path. + */ + recordScrollPosition: function recordScrollPosition(path) { + if (!this.scrollHistory) this.scrollHistory = {}; + + this.scrollHistory[path] = getWindowScrollPosition(); + }, + + /** + * Returns the last known scroll position for the given URL path. + */ + getScrollPosition: function getScrollPosition(path) { + if (!this.scrollHistory) this.scrollHistory = {}; + + return this.scrollHistory[path] || null; + } + + }, + + componentWillMount: function componentWillMount() { + invariant(this.constructor.getScrollBehavior() == null || canUseDOM, 'Cannot use scroll behavior without a DOM'); + }, + + componentDidMount: function componentDidMount() { + this._updateScroll(); + }, + + componentDidUpdate: function componentDidUpdate(prevProps, prevState) { + this._updateScroll(prevState); + }, + + _updateScroll: function _updateScroll(prevState) { + if (!shouldUpdateScroll(this.state, prevState)) { + return; + }var scrollBehavior = this.constructor.getScrollBehavior(); + + if (scrollBehavior) scrollBehavior.updateScrollPosition(this.constructor.getScrollPosition(this.state.path), this.state.action); + } + +}; + +module.exports = ScrollHistory; +},{"./getWindowScrollPosition":25,"react/lib/ExecutionEnvironment":60,"react/lib/invariant":175}],11:[function(require,module,exports){ +'use strict'; + +var PropTypes = require('./PropTypes'); + +/** + * A mixin for components that need to know the path, routes, URL + * params and query that are currently active. + * + * Example: + * + * var AboutLink = React.createClass({ + * mixins: [ Router.State ], + * render() { + * var className = this.props.className; + * + * if (this.isActive('about')) + * className += ' is-active'; + * + * return React.DOM.a({ className: className }, this.props.children); + * } + * }); + */ +var State = { + + contextTypes: { + router: PropTypes.router.isRequired + }, + + /** + * Returns the current URL path. + */ + getPath: function getPath() { + return this.context.router.getCurrentPath(); + }, + + /** + * Returns the current URL path without the query string. + */ + getPathname: function getPathname() { + return this.context.router.getCurrentPathname(); + }, + + /** + * Returns an object of the URL params that are currently active. + */ + getParams: function getParams() { + return this.context.router.getCurrentParams(); + }, + + /** + * Returns an object of the query params that are currently active. + */ + getQuery: function getQuery() { + return this.context.router.getCurrentQuery(); + }, + + /** + * Returns an array of the routes that are currently active. + */ + getRoutes: function getRoutes() { + return this.context.router.getCurrentRoutes(); + }, + + /** + * A helper method to determine if a given route, params, and query + * are active. + */ + isActive: function isActive(to, params, query) { + return this.context.router.isActive(to, params, query); + } + +}; + +module.exports = State; +},{"./PropTypes":7}],12:[function(require,module,exports){ +/* jshint -W058 */ + +'use strict'; + +var Cancellation = require('./Cancellation'); +var Redirect = require('./Redirect'); + +/** + * Encapsulates a transition to a given path. + * + * The willTransitionTo and willTransitionFrom handlers receive + * an instance of this class as their first argument. + */ +function Transition(path, retry) { + this.path = path; + this.abortReason = null; + // TODO: Change this to router.retryTransition(transition) + this.retry = retry.bind(this); +} + +Transition.prototype.abort = function (reason) { + if (this.abortReason == null) this.abortReason = reason || 'ABORT'; +}; + +Transition.prototype.redirect = function (to, params, query) { + this.abort(new Redirect(to, params, query)); +}; + +Transition.prototype.cancel = function () { + this.abort(new Cancellation()); +}; + +Transition.from = function (transition, routes, components, callback) { + routes.reduce(function (callback, route, index) { + return function (error) { + if (error || transition.abortReason) { + callback(error); + } else if (route.onLeave) { + try { + route.onLeave(transition, components[index], callback); + + // If there is no callback in the argument list, call it automatically. + if (route.onLeave.length < 3) callback(); + } catch (e) { + callback(e); + } + } else { + callback(); + } + }; + }, callback)(); +}; + +Transition.to = function (transition, routes, params, query, callback) { + routes.reduceRight(function (callback, route) { + return function (error) { + if (error || transition.abortReason) { + callback(error); + } else if (route.onEnter) { + try { + route.onEnter(transition, params, query, callback); + + // If there is no callback in the argument list, call it automatically. + if (route.onEnter.length < 4) callback(); + } catch (e) { + callback(e); + } + } else { + callback(); + } + }; + }, callback)(); +}; + +module.exports = Transition; +},{"./Cancellation":2,"./Redirect":8}],13:[function(require,module,exports){ +/** + * Actions that modify the URL. + */ +'use strict'; + +var LocationActions = { + + /** + * Indicates a new location is being pushed to the history stack. + */ + PUSH: 'push', + + /** + * Indicates the current location should be replaced. + */ + REPLACE: 'replace', + + /** + * Indicates the most recent entry should be removed from the history stack. + */ + POP: 'pop' + +}; + +module.exports = LocationActions; +},{}],14:[function(require,module,exports){ +'use strict'; + +var LocationActions = require('../actions/LocationActions'); + +/** + * A scroll behavior that attempts to imitate the default behavior + * of modern browsers. + */ +var ImitateBrowserBehavior = { + + updateScrollPosition: function updateScrollPosition(position, actionType) { + switch (actionType) { + case LocationActions.PUSH: + case LocationActions.REPLACE: + window.scrollTo(0, 0); + break; + case LocationActions.POP: + if (position) { + window.scrollTo(position.x, position.y); + } else { + window.scrollTo(0, 0); + } + break; + } + } + +}; + +module.exports = ImitateBrowserBehavior; +},{"../actions/LocationActions":13}],15:[function(require,module,exports){ +/** + * A scroll behavior that always scrolls to the top of the page + * after a transition. + */ +"use strict"; + +var ScrollToTopBehavior = { + + updateScrollPosition: function updateScrollPosition() { + window.scrollTo(0, 0); + } + +}; + +module.exports = ScrollToTopBehavior; +},{}],16:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +/** + * This component is necessary to get around a context warning + * present in React 0.13.0. It sovles this by providing a separation + * between the "owner" and "parent" contexts. + */ + +var React = require('react'); + +var ContextWrapper = (function (_React$Component) { + function ContextWrapper() { + _classCallCheck(this, ContextWrapper); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(ContextWrapper, _React$Component); + + _createClass(ContextWrapper, [{ + key: 'render', + value: function render() { + return this.props.children; + } + }]); + + return ContextWrapper; +})(React.Component); + +module.exports = ContextWrapper; +},{"react":195}],17:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var PropTypes = require('../PropTypes'); +var RouteHandler = require('./RouteHandler'); +var Route = require('./Route'); + +/** + * A component is a special kind of that + * renders when its parent matches but none of its siblings do. + * Only one such route may be used at any given level in the + * route hierarchy. + */ + +var DefaultRoute = (function (_Route) { + function DefaultRoute() { + _classCallCheck(this, DefaultRoute); + + if (_Route != null) { + _Route.apply(this, arguments); + } + } + + _inherits(DefaultRoute, _Route); + + return DefaultRoute; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +DefaultRoute.propTypes = { + name: PropTypes.string, + path: PropTypes.falsy, + children: PropTypes.falsy, + handler: PropTypes.func.isRequired +}; + +DefaultRoute.defaultProps = { + handler: RouteHandler +}; + +module.exports = DefaultRoute; +},{"../PropTypes":7,"./Route":21,"./RouteHandler":22}],18:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var React = require('react'); +var assign = require('react/lib/Object.assign'); +var PropTypes = require('../PropTypes'); + +function isLeftClickEvent(event) { + return event.button === 0; +} + +function isModifiedEvent(event) { + return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); +} + +/** + * components are used to create an element that links to a route. + * When that route is active, the link gets an "active" class name (or the + * value of its `activeClassName` prop). + * + * For example, assuming you have the following route: + * + * + * + * You could use the following component to link to that route: + * + * + * + * In addition to params, links may pass along query string parameters + * using the `query` prop. + * + * + */ + +var Link = (function (_React$Component) { + function Link() { + _classCallCheck(this, Link); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(Link, _React$Component); + + _createClass(Link, [{ + key: 'handleClick', + value: function handleClick(event) { + var allowTransition = true; + var clickResult; + + if (this.props.onClick) clickResult = this.props.onClick(event); + + if (isModifiedEvent(event) || !isLeftClickEvent(event)) { + return; + }if (clickResult === false || event.defaultPrevented === true) allowTransition = false; + + event.preventDefault(); + + if (allowTransition) this.context.router.transitionTo(this.props.to, this.props.params, this.props.query); + } + }, { + key: 'getHref', + + /** + * Returns the value of the "href" attribute to use on the DOM element. + */ + value: function getHref() { + return this.context.router.makeHref(this.props.to, this.props.params, this.props.query); + } + }, { + key: 'getClassName', + + /** + * Returns the value of the "class" attribute to use on the DOM element, which contains + * the value of the activeClassName property when this is active. + */ + value: function getClassName() { + var className = this.props.className; + + if (this.getActiveState()) className += ' ' + this.props.activeClassName; + + return className; + } + }, { + key: 'getActiveState', + value: function getActiveState() { + return this.context.router.isActive(this.props.to, this.props.params, this.props.query); + } + }, { + key: 'render', + value: function render() { + var props = assign({}, this.props, { + href: this.getHref(), + className: this.getClassName(), + onClick: this.handleClick.bind(this) + }); + + if (props.activeStyle && this.getActiveState()) props.style = props.activeStyle; + + return React.DOM.a(props, this.props.children); + } + }]); + + return Link; +})(React.Component); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +Link.contextTypes = { + router: PropTypes.router.isRequired +}; + +Link.propTypes = { + activeClassName: PropTypes.string.isRequired, + to: PropTypes.oneOfType([PropTypes.string, PropTypes.route]).isRequired, + params: PropTypes.object, + query: PropTypes.object, + activeStyle: PropTypes.object, + onClick: PropTypes.func +}; + +Link.defaultProps = { + activeClassName: 'active', + className: '' +}; + +module.exports = Link; +},{"../PropTypes":7,"react":195,"react/lib/Object.assign":66}],19:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var PropTypes = require('../PropTypes'); +var RouteHandler = require('./RouteHandler'); +var Route = require('./Route'); + +/** + * A is a special kind of that + * renders when the beginning of its parent's path matches + * but none of its siblings do, including any . + * Only one such route may be used at any given level in the + * route hierarchy. + */ + +var NotFoundRoute = (function (_Route) { + function NotFoundRoute() { + _classCallCheck(this, NotFoundRoute); + + if (_Route != null) { + _Route.apply(this, arguments); + } + } + + _inherits(NotFoundRoute, _Route); + + return NotFoundRoute; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +NotFoundRoute.propTypes = { + name: PropTypes.string, + path: PropTypes.falsy, + children: PropTypes.falsy, + handler: PropTypes.func.isRequired +}; + +NotFoundRoute.defaultProps = { + handler: RouteHandler +}; + +module.exports = NotFoundRoute; +},{"../PropTypes":7,"./Route":21,"./RouteHandler":22}],20:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var PropTypes = require('../PropTypes'); +var Route = require('./Route'); + +/** + * A component is a special kind of that always + * redirects to another route when it matches. + */ + +var Redirect = (function (_Route) { + function Redirect() { + _classCallCheck(this, Redirect); + + if (_Route != null) { + _Route.apply(this, arguments); + } + } + + _inherits(Redirect, _Route); + + return Redirect; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +Redirect.propTypes = { + path: PropTypes.string, + from: PropTypes.string, // Alias for path. + to: PropTypes.string, + handler: PropTypes.falsy +}; + +// Redirects should not have a default handler +Redirect.defaultProps = {}; + +module.exports = Redirect; +},{"../PropTypes":7,"./Route":21}],21:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var React = require('react'); +var invariant = require('react/lib/invariant'); +var PropTypes = require('../PropTypes'); +var RouteHandler = require('./RouteHandler'); + +/** + * components specify components that are rendered to the page when the + * URL matches a given pattern. + * + * Routes are arranged in a nested tree structure. When a new URL is requested, + * the tree is searched depth-first to find a route whose path matches the URL. + * When one is found, all routes in the tree that lead to it are considered + * "active" and their components are rendered into the DOM, nested in the same + * order as they are in the tree. + * + * The preferred way to configure a router is using JSX. The XML-like syntax is + * a great way to visualize how routes are laid out in an application. + * + * var routes = [ + * + * + * + * + * + * ]; + * + * Router.run(routes, function (Handler) { + * React.render(, document.body); + * }); + * + * Handlers for Route components that contain children can render their active + * child route using a element. + * + * var App = React.createClass({ + * render: function () { + * return ( + *
+ * + *
+ * ); + * } + * }); + * + * If no handler is provided for the route, it will render a matched child route. + */ + +var Route = (function (_React$Component) { + function Route() { + _classCallCheck(this, Route); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(Route, _React$Component); + + _createClass(Route, [{ + key: 'render', + value: function render() { + invariant(false, '%s elements are for router configuration only and should not be rendered', this.constructor.name); + } + }]); + + return Route; +})(React.Component); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +Route.propTypes = { + name: PropTypes.string, + path: PropTypes.string, + handler: PropTypes.func, + ignoreScrollBehavior: PropTypes.bool +}; + +Route.defaultProps = { + handler: RouteHandler +}; + +module.exports = Route; +},{"../PropTypes":7,"./RouteHandler":22,"react":195,"react/lib/invariant":175}],22:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var React = require('react'); +var ContextWrapper = require('./ContextWrapper'); +var assign = require('react/lib/Object.assign'); +var PropTypes = require('../PropTypes'); + +var REF_NAME = '__routeHandler__'; + +/** + * A component renders the active child route handler + * when routes are nested. + */ + +var RouteHandler = (function (_React$Component) { + function RouteHandler() { + _classCallCheck(this, RouteHandler); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(RouteHandler, _React$Component); + + _createClass(RouteHandler, [{ + key: 'getChildContext', + value: function getChildContext() { + return { + routeDepth: this.context.routeDepth + 1 + }; + } + }, { + key: 'componentDidMount', + value: function componentDidMount() { + this._updateRouteComponent(this.refs[REF_NAME]); + } + }, { + key: 'componentDidUpdate', + value: function componentDidUpdate() { + this._updateRouteComponent(this.refs[REF_NAME]); + } + }, { + key: 'componentWillUnmount', + value: function componentWillUnmount() { + this._updateRouteComponent(null); + } + }, { + key: '_updateRouteComponent', + value: function _updateRouteComponent(component) { + this.context.router.setRouteComponentAtDepth(this.getRouteDepth(), component); + } + }, { + key: 'getRouteDepth', + value: function getRouteDepth() { + return this.context.routeDepth; + } + }, { + key: 'createChildRouteHandler', + value: function createChildRouteHandler(props) { + var route = this.context.router.getRouteAtDepth(this.getRouteDepth()); + + if (route == null) { + return null; + }var childProps = assign({}, props || this.props, { + ref: REF_NAME, + params: this.context.router.getCurrentParams(), + query: this.context.router.getCurrentQuery() + }); + + return React.createElement(route.handler, childProps); + } + }, { + key: 'render', + value: function render() { + var handler = this.createChildRouteHandler(); + // ")); - }); - }).catch(function(error) { - next(error); }); + + let Handler = yield createHandler(routes, this.request.url); + + let App = React.createFactory(Handler); + let content = React.renderToString(new App({recents: recents})); + + content = content.replace('', ''); + + this.body = '\n' + content; }; diff --git a/routes/itunes-proxy.js b/routes/itunes-proxy.js index 1c5225c..b96cc93 100644 --- a/routes/itunes-proxy.js +++ b/routes/itunes-proxy.js @@ -1,14 +1,14 @@ -"use strict"; -var parse = require("url").parse; -var request = require("superagent"); +import {parse} from 'url'; +import request from 'superagent'; -module.exports = function(req, res) { - var url = "http://" + req.url.substr(8); - var parsed = parse(url); +module.exports = function* (next) { + let url = 'http://' + this.request.url.substr(8); + let parsed = parse(url); if (parsed.host.match(/mzstatic\.com/)) { - request.get(url, function(response){ - res.set(response.headers); - res.send(response.body); - }); + let proxyResponse = yield request.get(url); + this.set(proxyResponse.headers); + this.body = proxyResponse.body; + } else { + yield next; } }; diff --git a/routes/search.js b/routes/search.js index 69bb64d..b336d20 100644 --- a/routes/search.js +++ b/routes/search.js @@ -1,44 +1,42 @@ -"use strict"; -var parse = require("url").parse; -var lookup = require("../lib/lookup"); -var services = require("../lib/services"); +import {parse} from 'url'; +import co from 'co'; +import lookup from '../lib/lookup'; +import services from '../lib/services'; -module.exports = function(req, res) { - var url = parse(req.body.url); - if (!url.host) { - return res.json({error: {message: "You need to submit a url."}}); - } +module.exports = function* () { + let url = parse(this.request.body.url); + this.assert(url.host, 400, {error: {message: 'You need to submit a url.'}}); - var promise = lookup(req.body.url); + let item = yield lookup(this.request.body.url); - if (!promise) { - return res.json({error: {message: "No supported music found at that link :("}}); - } + this.assert(item, 400, {error: {message: 'No supported music found at that link :('}}); - promise.then(function(item) { - if (!item) { - return res.json({error: {message: "No supported music found at that link :("}}); + item.matched_at = new Date(); // eslint-disable-line camelcase + let matches = {}; + matches[item.service] = item; + + + for (let service of services) { + if (service.id === item.service) { + continue; } - item.matched_at = new Date(); // eslint-disable-line camelcase - var matches = {}; - matches[item.service] = item; - services.forEach(function(service) { + matches[service.id] = {service: service.id}; + } + + yield this.db.matches.save({_id: item.service + '$$' + item.id, 'created_at': new Date(), services: matches}); + this.body = item; + + process.nextTick(co.wrap(function* (){ + for (let service of services) { if (service.id === item.service) { - return; + continue; } matches[service.id] = {service: service.id}; - service.search(item).then(function(match) { - match.matched_at = new Date(); // eslint-disable-line camelcase - var update = {}; - update["services." + match.service] = match; - req.db.matches.update({_id: item.service + "$$" + item.id}, {"$set": update}); - }); - }); - return req.db.matches.save({_id: item.service + "$$" + item.id, "created_at": new Date(), services: matches}).then(function() { - res.json(item); - }); - }, function(error) { - console.log(error.stack); - res.json({error: {message: "No matches found for this link, sorry :("}}); - }); + let match = yield service.search(item); + match.matched_at = new Date(); // eslint-disable-line camelcase + let update = {}; + update['services.' + match.service] = match; + yield this.db.matches.updateOne({_id: item.service + '$$' + item.id}, {'$set': update}); + } + }.bind(this))); }; diff --git a/routes/share.js b/routes/share.js index f50655d..5806b71 100644 --- a/routes/share.js +++ b/routes/share.js @@ -1,13 +1,9 @@ -"use strict"; +import React from 'react'; +import createHandler from '../lib/react-handler'; +import {routes} from '../views/app.jsx'; +import services from '../lib/services'; -var React = require("react"); -var Router = require("react-router"); -require("node-jsx").install(); -var routes = require("../views/app.jsx").routes; - -var services = require("../lib/services"); - -var formatAndSort = function(matches, serviceId) { +let formatAndSort = function(matches, serviceId) { matches = Object.keys(matches).map(function (key) {return matches[key]; }); matches.sort(function(a, b) { return a.id && !b.id; @@ -17,60 +13,34 @@ var formatAndSort = function(matches, serviceId) { return matches; }; -module.exports = function(req, res, next) { - var serviceId = req.params.service; - var type = req.params.type; - var itemId = req.params.id; - - var matchedService; +module.exports = function* (serviceId, type, itemId, format, next) { + let matchedService; services.some(function(service) { matchedService = serviceId === service.id ? service : null; return matchedService; }); - if (!matchedService || (type !== "album" && type !== "track")) { - return next(); + if (!matchedService || (type !== 'album' && type !== 'track')) { + return yield next; } - return req.db.matches.findOne({_id: serviceId + "$$" + itemId}).then(function(doc) { - if (!doc) { - return matchedService.lookupId(itemId, type).then(function(item) { - var matches = {}; - item.matched_at = new Date(); // eslint-disable-line camelcase - matches[item.service] = item; - services.forEach(function(service) { - if (service.id === item.service) { - return; - } - matches[service.id] = {service: service.id}; - service.search(item).then(function(match) { - match.matched_at = new Date(); // eslint-disable-line camelcase - var update = {}; - update["services." + match.service] = match; - req.db.matches.update({_id: item.service + "$$" + item.id}, {"$set": update}); - }); - }); - return req.db.matches.save({_id: item.service + "$$" + item.id, "created_at": new Date(), services: matches}).then(function() { - var newShares = formatAndSort(matches, serviceId); - Router.run(routes, req.url, function (Handler) { - var App = React.createFactory(Handler); - var content = React.renderToString(new App({shares: newShares})); - res.send("\n" + content.replace("", "")); - }); - }); - }); - } - var shares = formatAndSort(doc.services, serviceId); - if (req.params.format === "json") { - return res.json({shares: shares}); - } - Router.run(routes, req.url, function (Handler) { - var App = React.createFactory(Handler); - var content = React.renderToString(new App({shares: shares})); - res.send("\n" + content.replace("", "")); - }); - }).catch(function (error) { - return next(error); - }); + let shares = []; + let doc = yield this.db.matches.findOne({_id: serviceId + '$$' + itemId}); + this.assert(doc, 404, 'Not Found'); + + shares = formatAndSort(doc.services, serviceId); + + if (format === 'json') { + this.body = {shares: shares}; + } else { + let Handler = yield createHandler(routes, this.request.url); + + let App = React.createFactory(Handler); + let content = React.renderToString(new App({shares: shares})); + + content = content.replace('', ''); + + this.body = '\n' + content; + } }; diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 0000000..bf83ab8 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,6 @@ +{ + "env": { + "node": true, + "mocha": true + } +} diff --git a/test/lookup.js b/test/lookup.js index 0f63a36..27c7c5b 100644 --- a/test/lookup.js +++ b/test/lookup.js @@ -1,15 +1,9 @@ -"use strict"; -require('should'); - -var lookup = require("../lib/lookup"); +import 'should'; +import lookup from '../lib/lookup'; describe('Search with url', function(){ - - it('should find album by url', function(done){ - lookup("https://play.google.com/music/listen#/album/Bz6wrjczddcj5hurijsv6ohdoay").then(function(result) { - result.name.should.equal("Phase 5"); - done(); - }); + it('should find album by url', function* (){ + let result = yield lookup('https://play.google.com/music/listen#/album/Bz6wrjczddcj5hurijsv6ohdoay'); + result.name.should.equal('Phase 5'); }); - -}); \ No newline at end of file +}); diff --git a/test/services/beats.js b/test/services/beats.js deleted file mode 100644 index af990d9..0000000 --- a/test/services/beats.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; -var assert = require("assert"); -var should = require('should'); - -var beats = require("../../lib/services/beats"); - -describe('Beats Music', function(){ - describe('lookupId', function(){ - it('should find album by ID', function(done){ - beats.lookupId("al920431", "album").then(function(result) { - result.name.should.equal("Deftones"); - done(); - }); - }); - - it('should find track by ID', function(done){ - beats.lookupId("tr6910289", "track").then(function(result) { - result.name.should.equal("Californication"); - done(); - }); - }); - }); - - describe('search', function(){ - it('should find album by search', function(done){ - beats.search({type: "album", artist: {name: "Deftones"}, name: "Deftones"}).then(function(result) { - result.name.should.equal("Deftones"); - done(); - }); - }); - - it('should find track by search', function(done){ - beats.search({type: "track", artist: {name: "Deftones"}, album: {name: "Deftones"}, name: "Hexagram"}).then(function(result) { - result.name.should.equal("Hexagram"); - done(); - }); - }); - }); - - describe('lookupUrl', function(){ - describe('parseUrl', function(){ - it('should parse album url into ID', function(done){ - beats.parseUrl("https://listen.beatsmusic.com/albums/al920431").then(function(result) { - result.id.should.equal("al920431"); - done(); - }); - }); - - it('should parse track url into ID', function(done){ - beats.parseUrl("https://listen.beatsmusic.com/albums/al6910269/tracks/tr6910289").then(function(result) { - result.id.should.equal("tr6910289"); - done(); - }); - }); - }); - }); -}); diff --git a/test/services/deezer.js b/test/services/deezer.js index 0eae11f..136ee32 100644 --- a/test/services/deezer.js +++ b/test/services/deezer.js @@ -1,56 +1,41 @@ -"use strict"; -var assert = require("assert"); -var should = require('should'); - -var deezer = require("../../lib/services/deezer"); +import 'should'; +import deezer from '../../lib/services/deezer'; describe('Deezer', function(){ describe('lookupId', function(){ - it('should find album by ID', function(done){ - deezer.lookupId("302127", "album").then(function(result) { - result.name.should.equal("Discovery"); - done(); - }); + it('should find album by ID', function* (){ + let result = yield deezer.lookupId('302127', 'album'); + result.name.should.equal('Discovery'); }); - it('should find track by ID', function(done){ - deezer.lookupId("3135554", "track").then(function(result) { - result.name.should.equal("Aerodynamic"); - done(); - }); + it('should find track by ID', function* (){ + let result = yield deezer.lookupId('3135554', 'track'); + result.name.should.equal('Aerodynamic'); }); }); describe('search', function(){ - it('should find album by search', function(done){ - deezer.search({type: "album", artist: {name: "David Guetta"}, name: "Listen (Deluxe)"}).then(function(result) { - result.name.should.startWith("Listen"); - done(); - }); + it('should find album by search', function* (){ + let result = yield deezer.search({type: 'album', artist: {name: 'David Guetta'}, name: 'Listen (Deluxe)'}); + result.name.should.startWith('Listen'); }); - it('should find track by search', function(done){ - deezer.search({type: "track", artist: {name: "Deftones"}, album: {name: "Deftones"}, name: "Hexagram"}).then(function(result) { - result.name.should.equal("Hexagram"); - done(); - }); + it('should find track by search', function* (){ + let result = yield deezer.search({type: 'track', artist: {name: 'Deftones'}, album: {name: 'Deftones'}, name: 'Hexagram'}); + result.name.should.equal('Hexagram'); }); }); describe('lookupUrl', function(){ describe('parseUrl', function(){ - it('should parse album url into ID', function(done){ - deezer.parseUrl("http://www.deezer.com/album/302127").then(function(result) { - result.id.should.equal(302127); - done(); - }); + it('should parse album url into ID', function* (){ + let result = yield deezer.parseUrl('http://www.deezer.com/album/302127'); + result.id.should.equal(302127); }); - it('should parse track url into ID', function(done){ - deezer.parseUrl("http://www.deezer.com/track/3135554").then(function(result) { - result.id.should.equal(3135554); - done(); - }); + it('should parse track url into ID', function* (){ + let result = yield deezer.parseUrl('http://www.deezer.com/track/3135554'); + result.id.should.equal(3135554); }); }); }); diff --git a/test/services/google.js b/test/services/google.js index b9a1eeb..02ecd64 100644 --- a/test/services/google.js +++ b/test/services/google.js @@ -1,55 +1,40 @@ -"use strict"; -var assert = require("assert"); -var should = require('should'); - -var google = require("../../lib/services/google"); +import 'should'; +import google from '../../lib/services/google'; describe('Google Play Music', function(){ describe('lookupId', function(){ - it('should find album by ID', function(done){ - google.lookupId("Byp6lvzimyf74wxi5634ul4tgam", "album").then(function(result) { - result.name.should.equal("Listen (Deluxe)"); - done(); - }); + it('should find album by ID', function* (){ + let result = yield google.lookupId('Byp6lvzimyf74wxi5634ul4tgam', 'album'); + result.name.should.equal('Listen (Deluxe)'); }); - it('should find track by ID', function(done){ - google.lookupId("Tjosptub24g2dft37lforqnudpe", "track").then(function(result) { - result.name.should.equal("Cherub Rock"); - done(); - }); + it('should find track by ID', function* (){ + let result = yield google.lookupId('Tjosptub24g2dft37lforqnudpe', 'track'); + result.name.should.equal('Cherub Rock'); }); }); describe('search', function(){ - it('should find album by search', function(done){ - google.search({type: "album", artist: {name: "David Guetta"}, name: "Listen (Deluxe)"}).then(function(result) { - result.name.should.equal("Listen (Deluxe)"); - done(); - }); + it('should find album by search', function* (){ + let result = yield google.search({type: 'album', artist: {name: 'David Guetta'}, name: 'Listen (Deluxe)'}); + result.name.should.equal('Listen (Deluxe)'); }); }); describe('lookupUrl', function(){ - it('should parse regular url into album ID', function(done){ - google.parseUrl("https://play.google.com/music/listen#/album/Byp6lvzimyf74wxi5634ul4tgam/David+Guetta/Listen+(Deluxe)").then(function(result) { - result.id.should.equal("Byp6lvzimyf74wxi5634ul4tgam"); - done(); - }); + it('should parse regular url into album ID', function* (){ + let result = yield google.parseUrl('https://play.google.com/music/listen#/album/Byp6lvzimyf74wxi5634ul4tgam/David+Guetta/Listen+(Deluxe)'); + result.id.should.equal('Byp6lvzimyf74wxi5634ul4tgam'); }); - it('should parse url without ID into album ID', function(done){ - google.parseUrl("https://play.google.com/music/listen#/album//David+Guetta/Listen+(Deluxe)").then(function(result) { - result.id.should.equal("Byp6lvzimyf74wxi5634ul4tgam"); - done(); - }); + it('should parse url without ID into album ID', function* (){ + let result = yield google.parseUrl('https://play.google.com/music/listen#/album//David+Guetta/Listen+(Deluxe)'); + result.id.should.equal('Byp6lvzimyf74wxi5634ul4tgam'); }); - it('should parse share url into album ID', function(done){ - google.parseUrl("https://play.google.com/music/m/Byp6lvzimyf74wxi5634ul4tgam").then(function(result) { - result.id.should.equal("Byp6lvzimyf74wxi5634ul4tgam"); - done(); - }); + it('should parse share url into album ID', function* (){ + let result = yield google.parseUrl('https://play.google.com/music/m/Byp6lvzimyf74wxi5634ul4tgam'); + result.id.should.equal('Byp6lvzimyf74wxi5634ul4tgam'); }); }); }); diff --git a/test/services/itunes.js b/test/services/itunes.js index 8144a13..c5c5191 100644 --- a/test/services/itunes.js +++ b/test/services/itunes.js @@ -1,56 +1,41 @@ -"use strict"; -var assert = require("assert"); -var should = require('should'); - -var itunes = require("../../lib/services/itunes"); +import 'should'; +import itunes from '../../lib/services/itunes'; describe('iTunes Music', function(){ describe('lookupId', function(){ - it('should find album by ID', function(done){ - itunes.lookupId("id215206912", "album").then(function(result) { - result.name.should.equal("Peace Orchestra"); - done(); - }); + it('should find album by ID', function* (){ + let result = yield itunes.lookupId('id215206912', 'album'); + result.name.should.equal('Peace Orchestra'); }); - it('should find track by ID', function(done){ - itunes.lookupId("id215206958", "track").then(function(result) { - result.name.should.equal("Double Drums"); - done(); - }); + it('should find track by ID', function* (){ + let result = yield itunes.lookupId('id215206958', 'track'); + result.name.should.equal('Double Drums'); }); }); describe('search', function(){ - it('should find album by search', function(done){ - itunes.search({type: "album", artist: {name: "Deftones"}, name: "Deftones"}).then(function(result) { - result.name.should.equal("Deftones"); - done(); - }); + it('should find album by search', function* (){ + let result = yield itunes.search({type: 'album', artist: {name: 'Deftones'}, name: 'Deftones'}); + result.name.should.equal('Deftones'); }); - it('should find track by search', function(done){ - itunes.search({type: "track", artist: {name: "Deftones"}, album: {name: "Deftones"}, name: "Hexagram"}).then(function(result) { - result.name.should.equal("Hexagram"); - done(); - }); + it('should find track by search', function* (){ + let result = yield itunes.search({type: 'track', artist: {name: 'Deftones'}, album: {name: 'Deftones'}, name: 'Hexagram'}); + result.name.should.equal('Hexagram'); }); }); describe('lookupUrl', function(){ describe('parseUrl', function(){ - it('should parse album url into ID', function(done){ - itunes.parseUrl("https://itunes.apple.com/us/album/double-drums/id215206912").then(function(result) { - result.id.should.equal("us215206912"); - done(); - }); + it('should parse album url into ID', function* (){ + let result = yield itunes.parseUrl('https://itunes.apple.com/us/album/double-drums/id215206912'); + result.id.should.equal('us215206912'); }); - it('should parse track url into ID', function(done){ - itunes.parseUrl("https://itunes.apple.com/us/album/double-drums/id215206912?i=215206958&uo=4").then(function(result) { - result.id.should.equal("us215206958"); - done(); - }); + it('should parse track url into ID', function* (){ + let result = yield itunes.parseUrl('https://itunes.apple.com/us/album/double-drums/id215206912?i=215206958&uo=4'); + result.id.should.equal('us215206958'); }); }); }); diff --git a/test/services/rdio.js b/test/services/rdio.js index 81994bd..0ec2e74 100644 --- a/test/services/rdio.js +++ b/test/services/rdio.js @@ -1,41 +1,30 @@ -"use strict"; -var assert = require("assert"); -var should = require('should'); - -var rdio = require("../../lib/services/rdio"); +import 'should'; +import rdio from '../../lib/services/rdio'; describe('Rdio', function(){ describe('lookupId', function(){ - it('should find album by ID', function(done){ - rdio.lookupId("Qj4NXr0").then(function(result) { - result.name.should.equal("Listen (Deluxe)"); - done(); - }); + it('should find album by ID', function* (){ + let result = yield rdio.lookupId('Qj4NXr0'); + result.name.should.equal('Listen (Deluxe)'); }); }); describe('search', function(){ - it('should find album by search', function(done){ - rdio.search({type: "album", artist: {name: "David Guetta"}, name: "Listen (Deluxe)"}).then(function(result) { - result.name.should.equal("Listen (Deluxe)"); - done(); - }); + it('should find album by search', function* (){ + let result = yield rdio.search({type: 'album', artist: {name: 'David Guetta'}, name: 'Listen (Deluxe)'}); + result.name.should.equal('Listen (Deluxe)'); }); }); describe('parseUrl', function(){ - it('should parse regular url into album object', function(done){ - rdio.parseUrl("https://www.rdio.com/artist/David_Guetta/album/Listen_(Deluxe)/").then(function(result) { - result.name.should.equal("Listen (Deluxe)"); - done(); - }); + it('should parse regular url into album object', function* (){ + let result = yield rdio.parseUrl('https://www.rdio.com/artist/David_Guetta/album/Listen_(Deluxe)/'); + result.name.should.equal('Listen (Deluxe)'); }); - it('should parse short url into album object', function(done){ - rdio.parseUrl("http://rd.io/x/Qj4NXr0/").then(function(result) { - result.name.should.equal("Listen (Deluxe)"); - done(); - }); + it('should parse short url into album object', function* (){ + let result = yield rdio.parseUrl('http://rd.io/x/Qj4NXr0/'); + result.name.should.equal('Listen (Deluxe)'); }); }); }); diff --git a/test/services/spotify.js b/test/services/spotify.js index 54a06a7..63d8800 100644 --- a/test/services/spotify.js +++ b/test/services/spotify.js @@ -1,41 +1,30 @@ -"use strict"; -var assert = require("assert"); -var should = require('should'); - -var spotify = require("../../lib/services/spotify"); +import 'should'; +import spotify from '../../lib/services/spotify'; describe('Spotify', function(){ describe('lookupId', function(){ - it('should find album by ID', function(done){ - spotify.lookupId("77UW17CZFyCaRLHdHeofZu", "album").then(function(result) { - result.name.should.equal("Listen (Deluxe)"); - done(); - }); + it('should find album by ID', function* (){ + let result = yield spotify.lookupId('77UW17CZFyCaRLHdHeofZu', 'album'); + result.name.should.equal('Listen (Deluxe)'); }); - it('should find track by ID', function(done){ - spotify.lookupId("7dS5EaCoMnN7DzlpT6aRn2", "track").then(function(result) { - result.name.should.equal("Take Me To Church"); - done(); - }); + it('should find track by ID', function* (){ + let result = yield spotify.lookupId('7dS5EaCoMnN7DzlpT6aRn2', 'track'); + result.name.should.equal('Take Me To Church'); }); }); describe('search', function(){ - it('should find album by search', function(done){ - spotify.search({type: "album", artist: {name: "David Guetta"}, name: "Listen (Deluxe)"}).then(function(result) { - result.name.should.equal("Listen (Deluxe)"); - done(); - }); + it('should find album by search', function* (){ + let result = yield spotify.search({type: 'album', artist: {name: 'David Guetta'}, name: 'Listen (Deluxe)'}); + result.name.should.equal('Listen (Deluxe)'); }); }); describe('parseUrl', function(){ - it('should parse url into ID', function(done){ - spotify.parseUrl("https://play.spotify.com/album/77UW17CZFyCaRLHdHeofZu").then(function(result) { - result.id.should.equal("77UW17CZFyCaRLHdHeofZu"); - done(); - }); + it('should parse url into ID', function* (){ + let result = yield spotify.parseUrl('https://play.spotify.com/album/77UW17CZFyCaRLHdHeofZu'); + result.id.should.equal('77UW17CZFyCaRLHdHeofZu'); }); }); }); diff --git a/test/services/xbox.js b/test/services/xbox.js index 6da89c6..0ece79f 100644 --- a/test/services/xbox.js +++ b/test/services/xbox.js @@ -1,41 +1,30 @@ -"use strict"; -var assert = require("assert"); -var should = require('should'); - -var google = require("../../lib/services/xbox"); +import 'should'; +import xbox from '../../lib/services/xbox'; describe('Xbox Music', function(){ describe('lookupId', function(){ - it('should find album by ID', function(done){ - google.lookupId("music.8b558d00-0100-11db-89ca-0019b92a3933", "album").then(function(result) { - result.name.should.equal("Muchas Gracias: The Best Of Kyuss"); - done(); - }); + it('should find album by ID', function* (){ + let result = yield xbox.lookupId('music.8b558d00-0100-11db-89ca-0019b92a3933', 'album'); + result.name.should.equal('Muchas Gracias: The Best Of Kyuss'); }); - it('should find track by ID', function(done){ - google.lookupId("music.8f558d00-0100-11db-89ca-0019b92a3933", "track").then(function(result) { - result.name.should.equal("Shine"); - done(); - }); + it('should find track by ID', function* (){ + let result = yield xbox.lookupId('music.8f558d00-0100-11db-89ca-0019b92a3933', 'track'); + result.name.should.equal('Shine'); }); }); describe('search', function(){ - it('should find album by search', function(done){ - google.search({type: "album", artist: {name: "Kyuss"}, name: "Muchas Gracias: The Best Of Kyuss"}).then(function(result) { - result.name.should.equal("Muchas Gracias: The Best Of Kyuss"); - done(); - }); + it('should find album by search', function* (){ + let result = yield xbox.search({type: 'album', artist: {name: 'Kyuss'}, name: 'Muchas Gracias: The Best Of Kyuss'}); + result.name.should.equal('Muchas Gracias: The Best Of Kyuss'); }); }); describe('lookupUrl', function(){ - it('should parse regular url into album ID', function(done){ - google.parseUrl("https://music.xbox.com/album/kyuss/muchas-gracias-the-best-of-kyuss/8b558d00-0100-11db-89ca-0019b92a3933").then(function(result) { - result.id.should.equal("music.8B558D00-0100-11DB-89CA-0019B92A3933"); - done(); - }); + it('should parse regular url into album ID', function* (){ + let result = yield xbox.parseUrl('https://music.xbox.com/album/kyuss/muchas-gracias-the-best-of-kyuss/8b558d00-0100-11db-89ca-0019b92a3933'); + result.id.should.equal('music.8B558D00-0100-11DB-89CA-0019B92A3933'); }); }); }); diff --git a/test/services/youtube.js b/test/services/youtube.js index 843e504..e2fc2b1 100644 --- a/test/services/youtube.js +++ b/test/services/youtube.js @@ -1,16 +1,11 @@ -"use strict"; -var assert = require("assert"); -var should = require('should'); - -var youtube = require("../../lib/services/youtube"); +import 'should'; +import youtube from '../../lib/services/youtube'; describe('Youtube', function(){ describe('search', function(){ - it('should find album by search', function(done){ - youtube.search({type: "track", artist: {name: "Aesop Rock"}, album: {name: "Skelethon"}, name: "Zero Dark Thirty"}).then(function(result) { - result.name.should.equal("Aesop Rock - Zero Dark Thirty"); - done(); - }); + it('should find album by search', function* (){ + let result = yield youtube.search({type: 'track', artist: {name: 'Aesop Rock'}, album: {name: 'Skelethon'}, name: 'Zero Dark Thirty'}); + result.name.should.equal('Aesop Rock - Zero Dark Thirty'); }); }); }); diff --git a/views/.eslintrc b/views/.eslintrc index 9c66146..2dafe84 100644 --- a/views/.eslintrc +++ b/views/.eslintrc @@ -6,6 +6,7 @@ "react" ], "env": { - "browser": true + "browser": true, + "es6": true } } \ No newline at end of file diff --git a/views/app.jsx b/views/app.jsx index 1f811bf..730146a 100644 --- a/views/app.jsx +++ b/views/app.jsx @@ -1,15 +1,15 @@ -"use strict"; +'use strict'; -var React = require("react"); -var Router = require("react-router"); -var Route = require("react-router").Route; -var DefaultRoute = require("react-router").DefaultRoute; -var NotFoundRoute = require("react-router").NotFoundRoute; -var RouteHandler = require("react-router").RouteHandler; -var Home = require("./home.jsx"); -var Share = require("./share.jsx"); -var Head = require("./head.jsx"); -var ga = require("react-google-analytics"); +var React = require('react'); +var Router = require('react-router'); +var Route = require('react-router').Route; +var DefaultRoute = require('react-router').DefaultRoute; +var NotFoundRoute = require('react-router').NotFoundRoute; +var RouteHandler = require('react-router').RouteHandler; +var Home = require('./home.jsx'); +var Share = require('./share.jsx'); +var Head = require('./head.jsx'); +var ga = require('react-google-analytics'); var GAInitiailizer = ga.Initializer; var App = React.createClass({ @@ -17,42 +17,36 @@ var App = React.createClass({ return ( - + -