Refactor more, fix and design 404

This commit is contained in:
Jonathan Cremin 2015-08-21 18:33:50 +01:00
parent 7de374e00b
commit 8521baa6d9
11 changed files with 223 additions and 138 deletions

23
app.js
View file

@ -16,33 +16,14 @@ import share from './routes/share';
import itunesProxy from './routes/itunes-proxy'; import itunesProxy from './routes/itunes-proxy';
import { routes } from './views/app'; import { routes } from './views/app';
import createHandler from './lib/react-handler'; import createHandler from './lib/react-handler';
import errorHandler from './lib/error-handler';
import debuglog from 'debug'; import debuglog from 'debug';
const debug = debuglog('match.audio'); const debug = debuglog('match.audio');
const app = koa(); const app = koa();
app.use(function* (next) { app.use(errorHandler(routes));
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(bodyparser()); app.use(bodyparser());
app.use(compress({flush: zlib.Z_SYNC_FLUSH })); app.use(compress({flush: zlib.Z_SYNC_FLUSH }));

48
lib/error-handler.js Normal file
View file

@ -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';
}
}
}

View file

@ -4,10 +4,13 @@ import request from 'superagent';
import 'superagent-bluebird-promise'; import 'superagent-bluebird-promise';
import { match as urlMatch } from './url'; 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) { 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 = { const credentials = {
@ -15,12 +18,12 @@ const credentials = {
clientSecret: process.env.XBOX_CLIENT_SECRET clientSecret: process.env.XBOX_CLIENT_SECRET
}; };
const apiRoot = "https://music.xboxlive.com/1/content"; const apiRoot = 'https://music.xboxlive.com/1/content';
function* getAccessToken() { function* getAccessToken() {
const authUrl = "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13"; const authUrl = 'https://datamarket.accesscontrol.windows.net/v2/OAuth2-13';
const scope = "http://music.xboxlive.com"; const scope = 'http://music.xboxlive.com';
const grantType = "client_credentials"; const grantType = 'client_credentials';
const data = { const data = {
client_id: credentials.clientId, client_id: credentials.clientId,
@ -28,7 +31,7 @@ function* getAccessToken() {
scope: scope, scope: scope,
grant_type: grantType 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; return result.body.access_token;
} }
@ -39,16 +42,16 @@ function formatResponse(res) {
} else { } else {
result = res.body.Albums.Items[0]; result = res.body.Albums.Items[0];
} }
let item = { const item = {
service: "xbox", service: 'xbox',
type: res.body.Tracks ? "track" : "album", type: res.body.Tracks ? 'track' : 'album',
id: result.Id, id: result.Id,
name: result.Name, name: result.Name,
streamUrl: result.Link, streamUrl: result.Link,
purchaseUrl: null, purchaseUrl: null,
artwork: { artwork: {
small: result.ImageUrl.replace("http://", "https://") + "&w=250&h=250", small: result.ImageUrl.replace('http://', 'https://') + '&w=250&h=250',
large: result.ImageUrl.replace("http://", "https://") + "&w=500&h=250" large: result.ImageUrl.replace('http://', 'https://') + '&w=500&h=250'
}, },
artist: { artist: {
name: result.Artists[0].Artist.Name name: result.Artists[0].Artist.Name
@ -60,43 +63,60 @@ function formatResponse(res) {
return item; 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 const match = urlMatch;
export function* parseUrl(url) { export function* parseUrl(url) {
const parsed = parse(url); const parsed = parse(url);
const parts = parsed.path.split("/"); const parts = parsed.path.split('/');
const type = parts[1]; const type = parts[1];
const idMatches = parts[4].match(/[\w\-]+/); const idMatches = parts[4].match(/[\w\-]+/);
const id = idMatches[0]; const id = idMatches[0];
if (!id) { if (!id) {
return false; return false;
} }
return yield lookupId("music." + id, type); return yield lookupId('music.' + id, type);
} }
export function* lookupId(id, type) { export function* lookupId(id, type) {
const access_token = yield getAccessToken(); const path = '/' + id + '/lookup';
const path = "/" + id + "/lookup"; try {
const result = yield request.get(apiRoot + path).set("Authorization", "Bearer " + access_token).promise(); const result = yield apiCall(path);
return result ? formatResponse(result) : {service: "xbox"}; return formatResponse(result);
} catch (e) {
if (e.status !== 404) {
debug(e.body);
}
return {service: 'xbox'};
}
}; };
export function* search(data) { export function* search(data) {
var cleanParam = function(str) { var cleanParam = function(str) {
return str.replace(/[\:\?\&]+/, ""); return str.replace(/[\:\?\&\(\)\[\]]+/g, '');
} }
let query, album; let query, album;
const type = data.type; const type = data.type;
if (type == "album") { if (type == 'album') {
query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + " " + cleanParam(data.name); query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + ' ' + cleanParam(data.name);
album = data.name; album = data.name;
} else if (type == "track") { } else if (type == 'track') {
query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + " " + cleanParam(data.name); query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + ' ' + cleanParam(data.name);
album = data.album.name album = data.album.name
} }
const access_token = yield getAccessToken(); const path = '/music/search?q=' + encodeURIComponent(query) + '&filters=' + type + 's';
const path = "/music/search?q=" + encodeURIComponent(query) + "&filters=" + type + "s"; try {
const result = yield request.get(apiRoot + path).set("Authorization", "Bearer " + access_token).promise() const result = yield apiCall(path);
return result ? formatResponse(result) : {service: "xbox"}; return formatResponse(result);
} catch (e) {
if (e.status !== 404) {
debug(e.body);
}
return {service: 'xbox'};
}
}; };

View file

@ -6,6 +6,6 @@ export function* match(url, type) {
return false; return false;
} }
const parts = parsed.path.split("/"); const parts = parsed.path.split('/');
return (parts[1] == "album" || parts[1] == "track") && parts[4]; return (parts[1] == 'album' || parts[1] == 'track') && parts[4];
}; };

View file

@ -7,9 +7,9 @@ const credentials = {
key: process.env.YOUTUBE_KEY, 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) { 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; return result.body;
} }

View file

@ -6,6 +6,9 @@ import 'superagent-bluebird-promise';
import { match as urlMatch } from './url'; import { match as urlMatch } from './url';
import freebase from './freebase'; import freebase from './freebase';
import debuglog from 'debug';
const debug = debuglog('match.audio:youtube');
module.exports.id = 'youtube'; module.exports.id = 'youtube';
if (!process.env.YOUTUBE_KEY) { if (!process.env.YOUTUBE_KEY) {
@ -37,14 +40,13 @@ export function parseUrl(url) {
export function* lookupId(id, type) { export function* lookupId(id, type) {
const path = '/videos?part=snippet%2CtopicDetails%2CcontentDetails&id=' + id + '&key=' + credentials.key; const path = '/videos?part=snippet%2CtopicDetails%2CcontentDetails&id=' + id + '&key=' + credentials.key;
try {
const result = yield request.get(apiRoot + path).promise(); const result = yield request.get(apiRoot + path).promise();
const item = res.body.items[0]; const item = result.body.items[0];
if (!item.topicDetails.topicIds) { if (!item.topicDetails.topicIds) {
return {service: 'youtube'}; return {service: 'youtube'};
} }
const promises = [];
const match = { const match = {
id: id, id: id,
service: 'youtube', service: 'youtube',
@ -86,6 +88,10 @@ export function* lookupId(id, type) {
} }
} }
return match; return match;
} catch (e) {
debug(e.body);
return {'service': 'youtube'};
}
}; };
export function* search(data) { 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 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 result = yield request.get(apiRoot + path).promise();
const item = result.body.items[0]; const item = result.body.items[0];

View file

@ -214,38 +214,36 @@ h3 {
margin-bottom: 7px; margin-bottom: 7px;
} }
.js-video { .error {
height: 0; background: #FE4365;
padding-top: 25px; color: #febdc9;
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 h1, .error h2 { .error h1, .error h2 {
font-weight: 300; font-weight: 100;
text-align: center;
} }
.error .main h1 { .error .main h1 {
font-size: 2em; font-size: 2em;
margin-bottom: 50px; margin-bottom: 20px;
color: #fff;
} }
.error h2 { .error h2 {
color: #ff7c94;
font-size: 4em; 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;
} }

View file

@ -3,6 +3,9 @@ import co from 'co';
import lookup from '../lib/lookup'; import lookup from '../lib/lookup';
import services from '../lib/services'; import services from '../lib/services';
import debuglog from 'debug';
const debug = debuglog('match.audio:search');
module.exports = function* () { module.exports = function* () {
const url = parse(this.request.body.url); const url = parse(this.request.body.url);
this.assert(url.host, 400, {error: {message: 'You need to submit a 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}); yield this.db.matches.save({_id: item.service + '$$' + item.id, 'created_at': new Date(), services: matches});
this.body = item; this.body = item;
process.nextTick(co.wrap(function* (){ process.nextTick(() => {
for (let service of services) { for (let service of services) {
if (service.id === item.service) { if (service.id === item.service) {
continue; continue;
} }
matches[service.id] = {service: service.id}; matches[service.id] = {service: service.id};
co(function* (){
const match = yield service.search(item); const match = yield service.search(item);
match.matched_at = new Date(); // eslint-disable-line camelcase match.matched_at = new Date(); // eslint-disable-line camelcase
const update = {}; const update = {};
update['services.' + match.service] = match; update['services.' + match.service] = match;
yield this.db.matches.updateOne({_id: item.service + '$$' + item.id}, {'$set': update}); yield this.db.matches.updateOne({_id: item.service + '$$' + item.id}, {'$set': update});
}.bind(this)).catch((err) => {
debug(err);
});
} }
}.bind(this))); });
}; };

View file

@ -31,15 +31,13 @@ export default function* (serviceId, type, itemId, format, next) {
const shares = formatAndSort(doc.services, serviceId); const shares = formatAndSort(doc.services, serviceId);
if (format === 'json') { if (format === 'json') {
this.body = {shares: shares}; return this.body = {shares: shares};
} else { }
const Handler = yield createHandler(routes, this.request.url);
const Handler = yield createHandler(routes, this.request.url);
const App = React.createFactory(Handler); const App = React.createFactory(Handler);
let content = React.renderToString(new App({shares: shares})); let content = React.renderToString(new App({shares: shares}));
content = content.replace('</body></html>', '<script>var shares = ' + JSON.stringify(shares) + '</script></body></html>'); content = content.replace('</body></html>', '<script>var shares = ' + JSON.stringify(shares) + '</script></body></html>');
this.body = '<!doctype html>\n' + content; this.body = '<!doctype html>\n' + content;
}
}; };

View file

@ -5,6 +5,7 @@ import Home from './home';
import Share from './share'; import Share from './share';
import Head from './head'; import Head from './head';
import ErrorView from './error'; import ErrorView from './error';
import NotFound from './notfound';
const App = React.createClass({ const App = React.createClass({
render: function () { render: function () {
@ -27,7 +28,7 @@ const routes = (
<Route name='home' handler={App} path='/'> <Route name='home' handler={App} path='/'>
<DefaultRoute handler={Home} /> <DefaultRoute handler={Home} />
<Route name='share' path=':service/:type/:id' handler={Share}/> <Route name='share' path=':service/:type/:id' handler={Share}/>
<NotFoundRoute handler={ErrorView}/> <NotFoundRoute handler={NotFound}/>
</Route> </Route>
); );

27
views/notfound.js Normal file
View file

@ -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>
);
}
});