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