diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17874d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Deployed apps should consider commenting this line out: +# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git +node_modules +spotify_appkey.key +downloads +config.json + +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..9486de2 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# spotijay-lib + +Download tracks from Spotify in 320kbps. + +# Installation + +1. `npm install` +1. Install [libspotify](https://developer.spotify.com/technologies/libspotify/) +1. Install following libraries: lame sox eye3D + - Ubuntu/Debian: `sudo apt-get install lame sox eyed3` + - Arch: `yaourt -S libspotify python2-eyed3 lame sox` + - OSX: `brew install homebrew/binary/libspotify lame sox eyeD3` +1. Copy your [appkey](https://developer.spotify.com/my-account/keys) in the root of the dir that uses this library (or specify another path, its the `__dirname` below that points to the appkey dir) + +# Usage + +```js +if (args.playlist) { + Spotijay(args.username, args.password, __dirname, function(err, spotify) { + spotify.downloadPlaylistByUrl(args.playlist, args.dir, errorCallback) + }) +} else if (args.track) { + Spotijay(args.username, args.password, __dirname, function(err, spotify) { + spotify.getTrackByUrl(args.track, args.dir, errorCallback) + }) +} +``` + +For more see [Spotijay](https://github.com/Christilut/Spotijay) diff --git a/index.js b/index.js new file mode 100644 index 0000000..3140325 --- /dev/null +++ b/index.js @@ -0,0 +1,29 @@ +var sp = require('libspotify') +var path = require('path') + +module.exports = function(username, password, dir, cb) { + var session = new sp.Session({ + applicationKey: path.join(dir, 'spotify_appkey.key') + }) + + var downloadTrack = require('./lib/download_track') + var downloadPlaylist = require('./lib/download_playlist') + var getTrackByUrl = require('./lib/get_track') + + session.login(username, password) + + session.once('login', function(err) { + + cb(null, { + downloadTrack: downloadTrack(session), + downloadPlaylistByUrl: downloadPlaylist(session), + getTrackByUrl: getTrackByUrl(session) + }) + }) + + session.on('error', function(err) { + cb(err) + session.close() + process.exit() + }) +} diff --git a/lib/download_playlist.js b/lib/download_playlist.js new file mode 100644 index 0000000..0aa2374 --- /dev/null +++ b/lib/download_playlist.js @@ -0,0 +1,45 @@ +var async = require('async') +var sp = require('libspotify') + +module.exports = function(session) { + var downloadTrack = require(__dirname + '/download_track')(session) + + function downloadPlaylist(url, path, callback) { + var args = arguments + var playlist = sp.Playlist.getFromUrl(url) + + console.log('Getting playlist contents from Spotify...') + + playlist.whenReady(function() { + playlist.getTracks(function(tracks) { + var left = tracks.length + + if (left) { + async.mapSeries(tracks, function(track, callback) { + console.log('Tracks left: ' + left) + left-- + downloadTrack(track, path, callback) + + }, function(err) { + if (err) { + callback(err) + } else { + console.log('Downloaded all tracks') + setTimeout(function() { + downloadPlaylist.apply(downloadPlaylist, args) + }, 60 * 1000) + } + }) + + } else { + console.log('There are no tracks on this playlist') + setTimeout(function() { + downloadPlaylist.apply(downloadPlaylist, args) + }, 60 * 1000) + } + }) + }) + } + + return downloadPlaylist +} diff --git a/lib/download_track.js b/lib/download_track.js new file mode 100644 index 0000000..1607a2f --- /dev/null +++ b/lib/download_track.js @@ -0,0 +1,148 @@ +var async = require('async') +var crypto = require('crypto') +var exec = require('child_process').exec +var fs = require('fs') +var punycode = require('punycode') + +module.exports = function(session) { + return function(track, path, callback) { + var tracktitle = track.artist.name + ' - ' + track.title + + if (track.availability === 'UNAVAILABLE') { + console.log('Track unavavailabe, skipping: ' + tracktitle) + return callback(null) + } + + var player = session.getPlayer() + + // var destiny = path + '/' + track.artist.name + '/' + track.album.name + '/' + track.name + var destiny = path + '/' + tracktitle + + var hashedName = crypto.createHash('sha1').update( + tracktitle + ).digest('hex') + + //getting a hash cuz eyeD3 is a motherfucker with special chars. + var destinyHash = path + '/' + hashedName + + // skip if already exists + if (fs.existsSync(destiny + '.mp3')) { + console.log('File already exists, skipping: ' + tracktitle + '.mp3') + return callback(null) + } + + console.log('Downloading: ' + tracktitle) + + if (!fs.existsSync(path)) fs.mkdirSync(path) + var ws = fs.createWriteStream(destinyHash + '.raw') + + player.load(track) + player.play() + player.pipe(ws) + + player.once('track-end', function() { + player.stop() + ws.end() + + async.waterfall([ + + function(callback) { + console.log('Converting raw file to wav') + exec('sox ' + [ + '-r', 44100, + '-b', 16, + '-L', + '-c', 2, + '-e', 'signed-integer', + '-t', 'raw', + '"' + destinyHash + '.raw"', + '"' + destinyHash + '.wav"' + ].join(' '), function(err) { + callback(err) + }) + }, + function(callback) { + console.log('Converting wav file to mp3 at 320kbps') + exec('lame ' + [ + '--preset', 'insane', + '-b', 320, + '-h', + '"' + destinyHash + '.wav"', + '"' + destinyHash + '.mp3"' + ].join(' '), function(err) { + callback(err) + }) + }, + function(callback) { + track.album.coverImage(function(err, buffer) { + if (err) { + return callback(null, false) + } + fs.writeFile(destinyHash + '.png', buffer, function(err) { + callback(null, true) + }) + }) + }, + function(cover, callback) { + console.log('Adding id3 tags') + + var coverCmd = ['--add-image', '"' + destinyHash + '.png:FRONT_COVER"'] + + var args = [ + '-t', '"' + punycode.toASCII(track.title) + '"', + '-a', '"' + punycode.toASCII(track.artist.name) + '"', + '-A', '"' + punycode.toASCII(track.album.name) + '"', + '-Y', '"' + track.album.year + '"', + '"' + destinyHash + '.mp3"' + ] + + // Adds the command to add a ilustration to the file + if (cover) + args = coverCmd.concat(args) + + var cmd = 'eyeD3 ' + args.join(' ') + + exec(cmd, function(err) { + callback(err, cover) + }) + + }, + function(cover, callback) { + // if (config.copy) + // exec('cp ' + destinyHash + '.mp3 ' + config.copy + '/' + destinyHash + '.mp3', function(err) { + // console.log(err) + // }) + // if (config.itunes) + // exec('open -a "itunes" ' + destinyHash + '.mp3 ', function(err) { + // console.log(err) + // }) + + // cleanup + fs.unlinkSync(destinyHash + '.raw') + fs.unlinkSync(destinyHash + '.wav') + fs.unlinkSync(destinyHash + '.png') // use flag? some may want it as cover.png or something + + fs.renameSync(destinyHash + '.mp3', destiny + '.mp3') + + // if (cover) { + // fs.renameSync(destinyHash + '.png', path + '/' + track.artist.name + ' - ' + track.title + '.png') + // } + callback() + } + + ], function(err) { + console.log('Finished: ' + tracktitle) + ws.end() + player.removeAllListeners('error') + delete player, ws + callback(err) + }) + + }) + + player.on('error', function(err) { + ws.end() + callback(err) + }) + } +} diff --git a/lib/get_track.js b/lib/get_track.js new file mode 100644 index 0000000..dec17e7 --- /dev/null +++ b/lib/get_track.js @@ -0,0 +1,14 @@ +var async = require('async') +var sp = require('libspotify') + +var downloadTrack = require(__dirname + '/download_track') + +module.exports = function(session) { + return function(url, path, callback) { + var track = sp.Track.getFromUrl(url) + + track.on('ready', function() { + downloadTrack(session)(track, path, callback) + }) + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1e3bdfb --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "spotijay-lib", + "description": "Spotify downloader library", + "version": "0.1.0", + "keywords": [ + "spotify", + "spotijay", + "download" + ], + "author": { + "name": "Alejandro Perezpayá", + "email": "alejandro@perezpaya.net", + "url": "http://perezpaya.net" + }, + "contributors": [ + { + "name": "Alejandro Perezpayá", + "email": "alejandro@perezpaya.net", + "url": "http://perezpaya.net" + }, + { + "name": "Luis Iván Cuende", + "email": "me@luisivan.net", + "url": "http://luisivan.net" + }, + { + "name": "Christiaan Maks" + } + ], + "main": "index.js", + "engines": { + "node": ">= 0.10.x" + }, + "dependencies": { + "async": "~0.9.0", + "libspotify": "~0.2.2" + } +}