From 599e9b08503347658036dada144c39ca57f8497b Mon Sep 17 00:00:00 2001 From: Jonathan Cremin Date: Mon, 12 Jan 2015 17:38:42 +0000 Subject: [PATCH] Add support for matching from Youtube urls --- app.js | 2 +- lib/lookup.js | 1 - lib/services/rdio/index.js | 10 ++++-- lib/services/spotify/index.js | 4 +-- lib/services/youtube/freebase.js | 20 +++++++++++ lib/services/youtube/index.js | 61 ++++++++++++++++++++++++++++++++ lib/services/youtube/url.js | 10 ++++++ public/stylesheets/style.css | 4 +++ routes/search.js | 15 ++++++-- views/error.jsx | 2 +- views/faq.jsx | 4 +++ views/home.jsx | 11 ++++-- 12 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 lib/services/youtube/freebase.js diff --git a/app.js b/app.js index 9bc11cc..cea09cb 100644 --- a/app.js +++ b/app.js @@ -105,7 +105,7 @@ app.use(function(req, res, next) { // will print stacktrace if (app.get('env') === 'development') { app.use(function(err, req, res, next) { - console.log(err) + console.log(err.stack) res.status(err.status || 500); var content = React.renderToString(ErrorView({status: err.status || 500, message: err.message, error: err})); diff --git a/lib/lookup.js b/lib/lookup.js index 7776710..06ed6dc 100644 --- a/lib/lookup.js +++ b/lib/lookup.js @@ -25,5 +25,4 @@ module.exports = function(url) { }); }); } - return false; }; diff --git a/lib/services/rdio/index.js b/lib/services/rdio/index.js index f227620..1714890 100644 --- a/lib/services/rdio/index.js +++ b/lib/services/rdio/index.js @@ -120,7 +120,11 @@ module.exports.search = function(data) { albumClean = data.name.match(/([^\(\[]+)/)[0]; } else if (type == "track") { query = data.artist.name + " " + data.album.name + " " + data.name; - albumClean = data.album.name.match(/([^\(\[]+)/)[0]; + try { + albumClean = data.album.name.match(/([^\(\[]+)/)[0]; + } catch(e) { + albumClean = ""; + } } return rdio.apiAsync("", "", {query: query, method: 'search', types: type}).then(function(results) { @@ -129,14 +133,14 @@ module.exports.search = function(data) { var result = results.filter(function(result) { if (type == "album" && result.name.match(/([^\(\[]+)/)[0] == albumClean) { return result; - } else if (type == "track" && result.album.match(/([^\(\[]+)/)[0] == albumClean) { + } else if (type == "track" && (result.album.match(/([^\(\[]+)/)[0] == albumClean || !albumClean)) { return result; } }).shift(); if (!result) { var matches = albumClean.match(/^[^\(\[]+/); - if (matches[0] && matches[0] != albumClean) { + if (matches && matches[0] && matches[0] != albumClean) { var cleanedData = JSON.parse(JSON.stringify(data)); if (type == "album") { cleanedData.name = matches[0].trim(); diff --git a/lib/services/spotify/index.js b/lib/services/spotify/index.js index b11fa49..3f4abd3 100644 --- a/lib/services/spotify/index.js +++ b/lib/services/spotify/index.js @@ -72,14 +72,14 @@ module.exports.search = function(data) { query = "artist:" + data.artist.name.replace(":", "") + " album:" + data.name.replace(":", ""); album = data.name; } else if (type == "track") { - query = "artist:" + data.artist.name.replace(":", "") + " album:" + data.album.name.replace(":", "") + " track:" + data.name.replace(":", ""); + query = "artist:" + data.artist.name.replace(":", "") + " track:" + data.name.replace(":", "") + ( data.album.name.length > 0 ? " album: " + data.album.name.replace(":", ""): ""); album = data.album.name; } return spotify.searchAsync({query: query, type: type}).then(function(results) { if (!results[type + "s"].items[0]) { var matches = album.match(/^[^\(\[]+/); - if (matches[0] && matches[0] != album) { + if (matches && matches[0] && matches[0] != album) { var cleanedData = JSON.parse(JSON.stringify(data)); if (type == "album") { cleanedData.name = matches[0].trim(); diff --git a/lib/services/youtube/freebase.js b/lib/services/youtube/freebase.js new file mode 100644 index 0000000..fffc02e --- /dev/null +++ b/lib/services/youtube/freebase.js @@ -0,0 +1,20 @@ +"use strict"; +var parse = require('url').parse; +var Promise = require('bluebird'); +var request = require('superagent'); +require('superagent-bluebird-promise'); + +var credentials = { + key: process.env.YOUTUBE_KEY, +}; + +var apiRoot = "https://www.googleapis.com/freebase/v1/topic"; + +module.exports.get = function(topic) { + return request.get(apiRoot + topic + "?key=" + credentials.key).promise().then(function(res) { + return res.body; + }) +} + + +module.exports.get("/m/0dwcrm_"); \ No newline at end of file diff --git a/lib/services/youtube/index.js b/lib/services/youtube/index.js index b664896..63746af 100644 --- a/lib/services/youtube/index.js +++ b/lib/services/youtube/index.js @@ -1,5 +1,7 @@ "use strict"; var parse = require('url').parse; +var freebase = require('./freebase'); +var querystring = require('querystring'); var Promise = require('bluebird'); var request = require('superagent'); require('superagent-bluebird-promise'); @@ -19,6 +21,65 @@ var apiRoot = "https://www.googleapis.com/youtube/v3"; module.exports.match = require('./url').match; +module.exports.parseUrl = function(url) { + var parsed = parse(url); + var query = querystring.parse(parsed.query); + var id = query.v; + + if (!id) { + id = parsed.path.substr(1); + if (!id) { + throw new Error(); + } + } + return module.exports.lookupId(id, "track"); +} + +module.exports.lookupId = function(id, type) { + + var path = "/videos?part=snippet%2Cstatus%2CtopicDetails&id=" + id + "&key=" + credentials.key; + + return request.get(apiRoot + path).promise().then(function(res) { + var item = res.body.items[0]; + if (item.topicDetails.topicIds) { + var promises = []; + item.topicDetails.topicIds.forEach(function(topicId) { + promises.push(freebase.get(topicId).then(function(topic) { + return topic.property["/music/recording/song"] ? topic : false; + }, function(err) { + console.log(err) + })); + }) + return Promise.all(promises).then(function(topics) { + for (var key in topics) { + var topic = topics[key]; + if (topic) { + console.log(topic.property['/music/recording/song']) + return { + id: id, + service: "youtube", + type: "track", + name: topic.property['/music/recording/song'].values[0].text, + artist: {name: topic.property['/music/recording/artist'].values[0].text}, + album: {name: ""}, + streamUrl: "https://youtu.be/" + id, + purchaseUrl: null, + artwork: { + small: item.snippet.thumbnails.medium.url, + large: item.snippet.thumbnails.high.url, + } + } + } + } + }); + } else { + return {service: "youtube"}; + } + }, function(res) { + return {service: "youtube"}; + }); +}; + module.exports.search = function(data) { var query, album; var type = data.type; diff --git a/lib/services/youtube/url.js b/lib/services/youtube/url.js index 4a82c7e..14fc82f 100644 --- a/lib/services/youtube/url.js +++ b/lib/services/youtube/url.js @@ -1,5 +1,15 @@ "use strict"; +var parse = require("url").parse; +var querystring = require('querystring'); module.exports.match = function(url, type) { + var parsed = parse(url); + + if (parsed.host.match(/youtu\.be$/)) { + return true; + } else if (parsed.host.match(/youtube\.com$/)) { + var query = querystring.parse(parsed.query); + return !!query.v; + } return false; }; diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index 854b605..e424803 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -105,6 +105,10 @@ h3 { margin-bottom: 30px; } +.share-form .alert { + margin-top: 20px; +} + .btn-custom { background-color: #FE4365; } diff --git a/routes/search.js b/routes/search.js index 3102cb4..8f1a5e5 100644 --- a/routes/search.js +++ b/routes/search.js @@ -10,7 +10,16 @@ module.exports = function(req, res, next) { return res.json({error:{message:"You need to submit a url."}}); } - lookup(req.body.url).then(function(item) { + var promise = lookup(req.body.url); + + if (!promise) { + return res.json({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(); var matches = {}; matches[item.service] = item; @@ -30,7 +39,7 @@ module.exports = function(req, res, next) { res.json(item); }); }, function(error) { - console.log(error.stack) - res.json({error: "No matches found for url"}); + console.log(error.stack); + res.json({error: {message: "No matches found for this link, sorry :("}}); }); }; diff --git a/views/error.jsx b/views/error.jsx index 7fef054..4e9fa12 100644 --- a/views/error.jsx +++ b/views/error.jsx @@ -9,7 +9,7 @@ module.exports = React.createClass({ render: function() { return ( - +
diff --git a/views/faq.jsx b/views/faq.jsx index 3488156..955a28d 100644 --- a/views/faq.jsx +++ b/views/faq.jsx @@ -22,6 +22,10 @@ module.exports = React.createClass({

Where do I find a link to paste in the box?

Most music services have a "share" dialog for albums and tracks in their interface. If you have them open in a web browser instead of an app, you can simply copy and paste the address bar and we'll work out the rest.

+
  • +

    Why don't you guys support Bandcamp, Amazon Music, Sony Music Unlimited… ?

    +

    Let me stop you there. Match Audio is open source, that means any capable programmer who wants to add other music services can look at our code and submit changes. If you're not a programmer, you can always submit a request and maybe we'll do it for you.

    +
  • diff --git a/views/home.jsx b/views/home.jsx index f24bd5d..e66e85c 100644 --- a/views/home.jsx +++ b/views/home.jsx @@ -47,7 +47,8 @@ var SearchForm = React.createClass({ getInitialState: function () { return { - submitting: true + submitting: true, + error: false }; }, @@ -69,7 +70,7 @@ var SearchForm = React.createClass({ submitting: false }); if (res.body.error) { - return alert(res.body.error.message) + that.setState({error: res.body.error.message}); } that.transitionTo("share", res.body); }); @@ -77,7 +78,8 @@ var SearchForm = React.createClass({ componentDidMount: function () { this.setState({ - submitting: false + submitting: false, + error: false }); }, @@ -90,6 +92,9 @@ var SearchForm = React.createClass({ +
    + {this.state.error} +
    ); }