From 8521baa6d909515fbc0e2d5dd47d85e214d45812 Mon Sep 17 00:00:00 2001 From: Jonathan Cremin <jonathan@crem.in> Date: Fri, 21 Aug 2015 18:33:50 +0100 Subject: [PATCH] Refactor more, fix and design 404 --- app.js | 23 +------- lib/error-handler.js | 48 ++++++++++++++++ lib/services/xbox/index.js | 74 ++++++++++++++++--------- lib/services/xbox/url.js | 4 +- lib/services/youtube/freebase.js | 4 +- lib/services/youtube/index.js | 95 +++++++++++++++++--------------- public/stylesheets/style.css | 44 +++++++-------- routes/search.js | 21 ++++--- routes/share.js | 18 +++--- views/app.js | 3 +- views/notfound.js | 27 +++++++++ 11 files changed, 223 insertions(+), 138 deletions(-) create mode 100644 lib/error-handler.js create mode 100644 views/notfound.js diff --git a/app.js b/app.js index 756b95c..6f8898f 100644 --- a/app.js +++ b/app.js @@ -16,33 +16,14 @@ import share from './routes/share'; import itunesProxy from './routes/itunes-proxy'; import { routes } from './views/app'; import createHandler from './lib/react-handler'; +import errorHandler from './lib/error-handler'; import debuglog from 'debug'; const debug = debuglog('match.audio'); const app = koa(); -app.use(function* (next) { - this.set('Server', 'Nintendo 64'); - try { - yield next; - } catch (err) { - if (!err.status) { - debug('Error: %o', err); - throw err; - } else if (err.status === 404) { - let Handler = yield createHandler(routes, this.request.url); - - let App = React.createFactory(Handler); - let content = React.renderToString(new App()); - - this.body = '<!doctype html>\n' + content; - } else { - debug('Error: %o', err); - throw err; - } - } -}); +app.use(errorHandler(routes)); app.use(bodyparser()); app.use(compress({flush: zlib.Z_SYNC_FLUSH })); diff --git a/lib/error-handler.js b/lib/error-handler.js new file mode 100644 index 0000000..1bad98a --- /dev/null +++ b/lib/error-handler.js @@ -0,0 +1,48 @@ +import React from 'react'; +import createHandler from './react-handler'; + +import debuglog from 'debug'; +const debug = debuglog('match.audio'); + +export default function (routes) { + return function* (next) { + this.set('Server', 'Nintendo 64'); + try { + yield next; + } catch (err) { + if (err.status === 404) { + let Handler = yield createHandler(routes, this.request.url); + + let App = React.createFactory(Handler); + let content = React.renderToString(new App()); + + this.body = '<!doctype html>\n' + content; + } else { + debug('Error: %o', err); + throw err; + } + } + + if (404 != this.status) return; + + switch (this.accepts('html', 'json')) { + case 'html': + this.type = 'html'; + let Handler = yield createHandler(routes, this.request.url); + + let App = React.createFactory(Handler); + let content = React.renderToString(new App()); + + this.body = '<!doctype html>\n' + content; + break; + case 'json': + this.body = { + message: 'Page Not Found' + }; + break; + default: + this.type = 'text'; + this.body = 'Page Not Found'; + } + } +} diff --git a/lib/services/xbox/index.js b/lib/services/xbox/index.js index f95886b..1997fee 100644 --- a/lib/services/xbox/index.js +++ b/lib/services/xbox/index.js @@ -4,10 +4,13 @@ import request from 'superagent'; import 'superagent-bluebird-promise'; import { match as urlMatch } from './url'; -export let id = "xbox"; +import debuglog from 'debug'; +const debug = debuglog('match.audio:xbox'); + +export let 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."); + console.warn('XBOX_CLIENT_ID and XBOX_CLIENT_SECRET environment variables not found, deactivating Xbox Music.'); } const credentials = { @@ -15,12 +18,12 @@ const credentials = { clientSecret: process.env.XBOX_CLIENT_SECRET }; -const apiRoot = "https://music.xboxlive.com/1/content"; +const apiRoot = 'https://music.xboxlive.com/1/content'; function* getAccessToken() { - const authUrl = "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13"; - const scope = "http://music.xboxlive.com"; - const grantType = "client_credentials"; + const authUrl = 'https://datamarket.accesscontrol.windows.net/v2/OAuth2-13'; + const scope = 'http://music.xboxlive.com'; + const grantType = 'client_credentials'; const data = { client_id: credentials.clientId, @@ -28,7 +31,7 @@ function* getAccessToken() { scope: scope, grant_type: grantType }; - const result = yield request.post(authUrl).send(data).set('Content-type', 'application/x-www-form-urlencoded').promise(); + const result = yield request.post(authUrl).timeout(10000).send(data).set('Content-type', 'application/x-www-form-urlencoded').promise(); return result.body.access_token; } @@ -39,16 +42,16 @@ function formatResponse(res) { } else { result = res.body.Albums.Items[0]; } - let item = { - service: "xbox", - type: res.body.Tracks ? "track" : "album", + const item = { + service: 'xbox', + type: res.body.Tracks ? 'track' : 'album', id: result.Id, name: result.Name, streamUrl: result.Link, purchaseUrl: null, artwork: { - small: result.ImageUrl.replace("http://", "https://") + "&w=250&h=250", - large: result.ImageUrl.replace("http://", "https://") + "&w=500&h=250" + small: result.ImageUrl.replace('http://', 'https://') + '&w=250&h=250', + large: result.ImageUrl.replace('http://', 'https://') + '&w=500&h=250' }, artist: { name: result.Artists[0].Artist.Name @@ -60,43 +63,60 @@ function formatResponse(res) { return item; } +function* apiCall(path) { + const access_token = yield getAccessToken(); + return request.get(apiRoot + path).timeout(10000).set('Authorization', 'Bearer ' + access_token).promise(); +} + export const match = urlMatch; export function* parseUrl(url) { const parsed = parse(url); - const parts = parsed.path.split("/"); + const parts = parsed.path.split('/'); const type = parts[1]; const idMatches = parts[4].match(/[\w\-]+/); const id = idMatches[0]; if (!id) { return false; } - return yield lookupId("music." + id, type); + return yield lookupId('music.' + id, type); } export function* lookupId(id, type) { - const access_token = yield getAccessToken(); - const path = "/" + id + "/lookup"; - const result = yield request.get(apiRoot + path).set("Authorization", "Bearer " + access_token).promise(); - return result ? formatResponse(result) : {service: "xbox"}; + const path = '/' + id + '/lookup'; + try { + const result = yield apiCall(path); + return formatResponse(result); + } catch (e) { + if (e.status !== 404) { + debug(e.body); + } + return {service: 'xbox'}; + } }; export function* search(data) { var cleanParam = function(str) { - return str.replace(/[\:\?\&]+/, ""); + return str.replace(/[\:\?\&\(\)\[\]]+/g, ''); } let query, album; const type = data.type; - if (type == "album") { - query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + " " + cleanParam(data.name); + if (type == 'album') { + query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + ' ' + cleanParam(data.name); album = data.name; - } else if (type == "track") { - query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + " " + cleanParam(data.name); + } else if (type == 'track') { + query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + ' ' + cleanParam(data.name); album = data.album.name } - const access_token = yield getAccessToken(); - const path = "/music/search?q=" + encodeURIComponent(query) + "&filters=" + type + "s"; - const result = yield request.get(apiRoot + path).set("Authorization", "Bearer " + access_token).promise() - return result ? formatResponse(result) : {service: "xbox"}; + const path = '/music/search?q=' + encodeURIComponent(query) + '&filters=' + type + 's'; + try { + const result = yield apiCall(path); + return formatResponse(result); + } catch (e) { + if (e.status !== 404) { + debug(e.body); + } + return {service: 'xbox'}; + } }; diff --git a/lib/services/xbox/url.js b/lib/services/xbox/url.js index d116d5d..fb54a36 100644 --- a/lib/services/xbox/url.js +++ b/lib/services/xbox/url.js @@ -6,6 +6,6 @@ export function* match(url, type) { return false; } - const parts = parsed.path.split("/"); - return (parts[1] == "album" || parts[1] == "track") && parts[4]; + const parts = parsed.path.split('/'); + return (parts[1] == 'album' || parts[1] == 'track') && parts[4]; }; diff --git a/lib/services/youtube/freebase.js b/lib/services/youtube/freebase.js index 2dd7793..ef2377d 100644 --- a/lib/services/youtube/freebase.js +++ b/lib/services/youtube/freebase.js @@ -7,9 +7,9 @@ const credentials = { key: process.env.YOUTUBE_KEY, }; -const apiRoot = "https://www.googleapis.com/freebase/v1/topic"; +const apiRoot = 'https://www.googleapis.com/freebase/v1/topic'; export function* get(topic) { - const result = yield request.get(apiRoot + topic + "?key=" + credentials.key).promise(); + const result = yield request.get(apiRoot + topic + '?key=' + credentials.key).promise(); return result.body; } diff --git a/lib/services/youtube/index.js b/lib/services/youtube/index.js index 1446b09..2aa93c2 100644 --- a/lib/services/youtube/index.js +++ b/lib/services/youtube/index.js @@ -6,6 +6,9 @@ import 'superagent-bluebird-promise'; import { match as urlMatch } from './url'; import freebase from './freebase'; +import debuglog from 'debug'; +const debug = debuglog('match.audio:youtube'); + module.exports.id = 'youtube'; if (!process.env.YOUTUBE_KEY) { @@ -37,55 +40,58 @@ export function parseUrl(url) { export function* lookupId(id, type) { const path = '/videos?part=snippet%2CtopicDetails%2CcontentDetails&id=' + id + '&key=' + credentials.key; - - const result = yield request.get(apiRoot + path).promise(); - const item = res.body.items[0]; - if (!item.topicDetails.topicIds) { - return {service: 'youtube'}; - } - - const promises = []; - const match = { - id: id, - service: 'youtube', - name: item.snippet.title, - type: 'track', - album: {name: ''}, - streamUrl: 'https://youtu.be/' + id, - purchaseUrl: null, - artwork: { - small: item.snippet.thumbnails.medium.url, - large: item.snippet.thumbnails.high.url, + try { + const result = yield request.get(apiRoot + path).promise(); + const item = result.body.items[0]; + if (!item.topicDetails.topicIds) { + return {service: 'youtube'}; } - }; - for (let topic of yield freebase.get(topicId)) { - const musicalArtist = topic.property['/type/object/type'].values.some((value) => { - return value.text == 'Musical Artist'; - }); - - const musicalRecording = topic.property['/type/object/type'].values.some(function(value) { - return value.text == 'Musical Recording'; - }); - - const musicalAlbum = topic.property['/type/object/type'].values.some(function(value) { - return value.text == 'Musical Album'; - }) - - if (musicalArtist) { - match.artist = {name: topic.property['/type/object/name'].values[0].text}; - } else if (musicalRecording) { - match.name = topic.property['/type/object/name'].values[0].text; - if (topic.property['/music/recording/releases']) { - match.type = 'album'; - match.album.name = topic.property['/music/recording/releases'].values[0].text; + const match = { + id: id, + service: 'youtube', + name: item.snippet.title, + type: 'track', + album: {name: ''}, + streamUrl: 'https://youtu.be/' + id, + purchaseUrl: null, + artwork: { + small: item.snippet.thumbnails.medium.url, + large: item.snippet.thumbnails.high.url, + } + }; + + for (let topic of yield freebase.get(topicId)) { + const musicalArtist = topic.property['/type/object/type'].values.some((value) => { + return value.text == 'Musical Artist'; + }); + + const musicalRecording = topic.property['/type/object/type'].values.some(function(value) { + return value.text == 'Musical Recording'; + }); + + const musicalAlbum = topic.property['/type/object/type'].values.some(function(value) { + return value.text == 'Musical Album'; + }) + + if (musicalArtist) { + match.artist = {name: topic.property['/type/object/name'].values[0].text}; + } else if (musicalRecording) { + match.name = topic.property['/type/object/name'].values[0].text; + if (topic.property['/music/recording/releases']) { + match.type = 'album'; + match.album.name = topic.property['/music/recording/releases'].values[0].text; + } + } else if (musicalAlbum) { + match.name = topic.property['/type/object/name'].values[0].text; + match.type = 'album'; } - } else if (musicalAlbum) { - match.name = topic.property['/type/object/name'].values[0].text; - match.type = 'album'; } + return match; + } catch (e) { + debug(e.body); + return {'service': 'youtube'}; } - return match; }; export function* search(data) { @@ -101,7 +107,6 @@ export function* search(data) { } const path = '/search?part=snippet&q=' + encodeURIComponent(query) + '&type=video&videoCaption=any&videoCategoryId=10&key=' + credentials.key; - const result = yield request.get(apiRoot + path).promise(); const item = result.body.items[0]; diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index 58a44ad..49b9f51 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -214,38 +214,36 @@ h3 { margin-bottom: 7px; } -.js-video { - height: 0; - padding-top: 25px; - padding-bottom: 67.5%; - margin-bottom: 10px; - position: relative; - overflow: hidden; -} - -.js-video.widescreen { - padding-bottom: 57.25%; -} - -.js-video embed, .js-video iframe, .js-video object, .js-video video { - top: 0; - left: 0; - width: 100%; - height: 100%; - position: absolute; +.error { + background: #FE4365; + color: #febdc9; } .error h1, .error h2 { - font-weight: 300; - text-align: center; + font-weight: 100; } .error .main h1 { font-size: 2em; - margin-bottom: 50px; + margin-bottom: 20px; + color: #fff; } .error h2 { + color: #ff7c94; font-size: 4em; - margin-top: 50px; + margin-top: 0px; + margin-bottom: 20px; +} + +.error a { + color: #fff; +} + +.vertical-center { + min-height: 100%; /* Fallback for browsers do NOT support vh unit */ + min-height: 100vh; /* These two lines are counted as one :-) */ + + display: flex; + align-items: center; } diff --git a/routes/search.js b/routes/search.js index 7092e59..3bd020e 100644 --- a/routes/search.js +++ b/routes/search.js @@ -3,6 +3,9 @@ import co from 'co'; import lookup from '../lib/lookup'; import services from '../lib/services'; +import debuglog from 'debug'; +const debug = debuglog('match.audio:search'); + module.exports = function* () { const url = parse(this.request.body.url); this.assert(url.host, 400, {error: {message: 'You need to submit a url.'}}); @@ -26,17 +29,21 @@ module.exports = function* () { yield this.db.matches.save({_id: item.service + '$$' + item.id, 'created_at': new Date(), services: matches}); this.body = item; - process.nextTick(co.wrap(function* (){ + process.nextTick(() => { for (let service of services) { if (service.id === item.service) { continue; } matches[service.id] = {service: service.id}; - const match = yield service.search(item); - match.matched_at = new Date(); // eslint-disable-line camelcase - const update = {}; - update['services.' + match.service] = match; - yield this.db.matches.updateOne({_id: item.service + '$$' + item.id}, {'$set': update}); + co(function* (){ + const match = yield service.search(item); + match.matched_at = new Date(); // eslint-disable-line camelcase + const update = {}; + update['services.' + match.service] = match; + yield this.db.matches.updateOne({_id: item.service + '$$' + item.id}, {'$set': update}); + }.bind(this)).catch((err) => { + debug(err); + }); } - }.bind(this))); + }); }; diff --git a/routes/share.js b/routes/share.js index 1999072..af4593f 100644 --- a/routes/share.js +++ b/routes/share.js @@ -31,15 +31,13 @@ export default function* (serviceId, type, itemId, format, next) { const shares = formatAndSort(doc.services, serviceId); if (format === 'json') { - this.body = {shares: shares}; - } else { - const Handler = yield createHandler(routes, this.request.url); - - const App = React.createFactory(Handler); - let content = React.renderToString(new App({shares: shares})); - - content = content.replace('</body></html>', '<script>var shares = ' + JSON.stringify(shares) + '</script></body></html>'); - - this.body = '<!doctype html>\n' + content; + return this.body = {shares: shares}; } + + const Handler = yield createHandler(routes, this.request.url); + const App = React.createFactory(Handler); + let content = React.renderToString(new App({shares: shares})); + content = content.replace('</body></html>', '<script>var shares = ' + JSON.stringify(shares) + '</script></body></html>'); + + this.body = '<!doctype html>\n' + content; }; diff --git a/views/app.js b/views/app.js index 25b80b3..9396e89 100644 --- a/views/app.js +++ b/views/app.js @@ -5,6 +5,7 @@ import Home from './home'; import Share from './share'; import Head from './head'; import ErrorView from './error'; +import NotFound from './notfound'; const App = React.createClass({ render: function () { @@ -27,7 +28,7 @@ const routes = ( <Route name='home' handler={App} path='/'> <DefaultRoute handler={Home} /> <Route name='share' path=':service/:type/:id' handler={Share}/> - <NotFoundRoute handler={ErrorView}/> + <NotFoundRoute handler={NotFound}/> </Route> ); diff --git a/views/notfound.js b/views/notfound.js new file mode 100644 index 0000000..baae22c --- /dev/null +++ b/views/notfound.js @@ -0,0 +1,27 @@ +import React from 'react'; +import Head from './head'; +import Foot from './foot'; + +export default React.createClass({ + + render: function() { + return ( + <html> + <Head {...this.props} /> + <body> + <div className='error vertical-center'> + <div className='container main'> + <div className='row'> + <div className='col-md-12'> + <h2>404</h2> + <h1>Sorry, it looks like the page you asked for is gone.</h1> + <a href='/'>Take Me Home</a> or <a href='https://www.youtube.com/watch?v=gnnIrTLlLyA' target='_blank'>Show Me the Wubs</a> + </div> + </div> + </div> + </div> + </body> + </html> + ); + } +});