Initial Commit

This commit is contained in:
Jonathan Cremin 2014-11-30 12:21:03 +00:00
commit f8d3ea92b1
12 changed files with 350 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

1
Procfile Normal file
View file

@ -0,0 +1 @@
web: node ./bin/www

66
app.js Normal file
View file

@ -0,0 +1,66 @@
var express = require('express');
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 bodyParser = require('body-parser');
var routes = require('./routes/index');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// uncomment after placing your favicon in /public
//app.use(favicon(__dirname + '/public/favicon.ico'));
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')));
app.use('/', routes);
// 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, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;

9
bin/www Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env node
var debug = require('debug')('unify.audio');
var app = require('../app');
app.set('port', process.env.PORT || 3000);
var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});

59
lib/googleplaymusic.js Normal file
View file

@ -0,0 +1,59 @@
var parse = require("url").parse;
var PlayMusic = require('playmusic');
var pm = new PlayMusic();
if (!process.env.GOOGLE_EMAIL || !process.env.GOOGLE_PASSWORD) {
throw new Error("You need to set GOOGLE_EMAIL and GOOGLE_PASSWORD environment variables");
}
// It's probably ok to not wait for this to finish
pm.init({email: process.env.GOOGLE_EMAIL, password: process.env.GOOGLE_PASSWORD}, function() {});
module.exports.lookupId = function(id, type, next) {
pm.getAlbum(id, true, function(album) {
next({
id: album.albumId,
name: album.name,
url: "https://play.google.com/music/listen#/album/" + album.albumId,
artwork: album.albumArtRef.replace("http:", ""),
artist: {
name: album.artist
},
type: "album"
});
});
}
module.exports.search = function(query, type, next) {
pm.search(query, 5, function(data) { // max 5 results
var result = data.entries.filter(function(result) {
return result.album;
}).sort(function(a, b) { // sort by match score
return a.score < b.score;
}).shift();
module.exports.lookupId(result.album.albumId, "album", next);
});
}
module.exports.parseUrl = function(url, next) {
// https://play.google.com/music/listen#/album/B3lxthejqxjxja2bhzchcw5qaci
// https://play.google.com/music/listen#/album//Underworld/Everything%2C+Everything+(Live)
var parsed = parse(url.replace(/\+/g, "%20"));
var hash = parsed.hash;
var matches = hash.match(/\/album[\/]+([^\/]+)\/([^\/]+)/);
if (matches && matches[2]) {
var artist = decodeURIComponent(matches[1]);
var album = decodeURIComponent(matches[2]);
module.exports.search(artist + " " + album, "album", function(googleAlbum) {
next(googleAlbum);
})
} else {
var matches = hash.match(/\/album[\/]+([\w]+)/);
if (matches && matches[1]) {
return next({id:matches[1], type: "album"})
}
}
}

49
lib/spotify.js Normal file
View file

@ -0,0 +1,49 @@
var parse = require('url').parse;
var spotify = require('spotify');
module.exports.lookupId = function(id, type, next) {
spotify.lookup({id: id, type: type}, function(err, data) {
if ( err ) {
console.log('Error occurred: ' + err);
return;
}
var artist = data.artists[0];
next({
id: data.id,
name: data.name,
url: "https://play.spotify.com/album/" + data.id,
artwork: data.images[0].url.replace("http:", ""),
artist: {
name: artist.name
}
})
});
}
module.exports.search = function(query, type, next) {
spotify.search({query: query, type: type}, function(err, data) {
if ( err ) {
console.log('Error occurred: ' + err);
return;
}
album = data.albums.items[0];
module.exports.lookupId(album.id, "album", next);
});
}
module.exports.parseUrl = function(url, next) {
// https://play.spotify.com/album/3W3ENDBQMJ9bD2qmxWI2f0
// https://play.spotify.com/track/3W3ENDBQMJ9bD2qmxWI2f0
// https://open.spotify.com/album/3W3ENDBQMJ9bD2qmxWI2f0
// https://open.spotify.com/track/3W3ENDBQMJ9bD2qmxWI2f0
var matches = parse(url).path.match(/\/album[\/]+([^\/]+)/);
if (matches && matches[1]) {
return next({id:matches[1], type: "album"})
}
}

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "unify.audio",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"engines": {
"node": "0.10.x"
},
"dependencies": {
"body-parser": "~1.8.1",
"connect-flash": "^0.1.1",
"cookie-parser": "~1.3.3",
"debug": "~2.0.0",
"ejs": "~0.8.5",
"express": "~4.9.0",
"express-session": "^1.9.2",
"morgan": "~1.3.0",
"playmusic": "^1.1.0",
"serve-favicon": "~2.1.3",
"spotify": "^0.3.0"
}
}

View file

@ -0,0 +1,8 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

64
routes/index.js Normal file
View file

@ -0,0 +1,64 @@
var express = require('express');
var router = express.Router();
var googleplaymusic = require('../lib/googleplaymusic');
var spotify = require('../lib/spotify');
//https://play.spotify.com/album/3W3ENDBQMJ9bD2qmxWI2f0
//https://play.google.com/music/listen#/album/B3lxthejqxjxja2bhzchcw5qaci
router.get('/:service/:type/:id', function(req, res) {
var service = req.params.service;
var type = req.params.type;
var id = req.params.id;
switch(service) {
case "spotify":
spotify.lookupId(id, type, function(spotifyAlbum) {
googleplaymusic.search(spotifyAlbum.artist.name + " " + spotifyAlbum.name, "album", function(googleAlbum) {
res.render('album', {googleAlbum: googleAlbum, spotifyAlbum: spotifyAlbum});
});
});
break;
case "google":
googleplaymusic.lookupId(id, type, function(googleAlbum) {
spotify.search(googleAlbum.artist.name + " " + googleAlbum.name, "album", function(spotifyAlbum) {
res.render('album', {googleAlbum: googleAlbum, spotifyAlbum: spotifyAlbum});
});
});
break;
}
});
router.post('/search', function(req, res) {
// determine spotify or google music
var url = req.body.url;
if (url.match(/spotify\.com/)) {
spotify.parseUrl(url, function(result) {
if (!result.id) {
req.flash('search-error', 'No match found for this link');
res.redirect('/');
}
res.redirect("/spotify/" + result.type + "/" + result.id);
});
} else if (url.match(/play\.google\.com/)) {
googleplaymusic.parseUrl(url, function(result) {
if (!result.id) {
req.flash('search-error', 'No match found for this link');
res.redirect('/');
}
res.redirect("/google/" + result.type + "/" + result.id);
});
} else {
req.flash('search-error', 'No match found for this link');
res.redirect('/');
}
});
/* GET home page. */
router.get('/', function(req, res) {
res.render('index', { error: req.flash('search-error') });
});
module.exports = router;

27
views/album.ejs Normal file
View file

@ -0,0 +1,27 @@
<!doctype html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Unify Audio</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<style>
.container {
padding-top: 100px;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<img src="<%= spotifyAlbum.artwork %>" height=200 class="img-rounded" /> <%= spotifyAlbum.id %> <a href="<%= spotifyAlbum.url %>">Play on Spotify</a>
</div>
<div class="row">
<img src="<%= googleAlbum.artwork %>" height=200 class="img-rounded" /> <%= googleAlbum.id %> <a href="<%= googleAlbum.url %>">Play on Google Music</a>
</div>
</div>
</body>
</html>

3
views/error.ejs Normal file
View file

@ -0,0 +1,3 @@
<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>

39
views/index.ejs Normal file
View file

@ -0,0 +1,39 @@
<!doctype html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Unify Audio</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<style>
.container {
padding-top: 100px;
}
.url-search {
text-align: center;
padding-bottom: 50px;
}
</style>
</head>
<body>
<div class="container">
<div class="row url-search">
<%= error %>
<form role="form" method="post" action="/search" class="form-inline">
<div class="form-group form-group-lg">
<input type="text" name="url" placeholder="Paste link here" class="form-control">
<input type="submit" class="btn btn-default btn-lg" value="Find Matches">
</div>
</form>
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<p>Make sharing music from subscription services better. Give us one link and we'll match it with other services and give you back a link with all of them. <a href="/google/album/B3qrtsvk5s3piwyla76sk6qyxny">Here's an example</a>.</p>
</div>
</div>
</div>
</body>
</html>