├── .gitignore ├── index.html ├── package.json ├── manifest.json ├── Gruntfile.coffee ├── LICENSE ├── src ├── main.coffee └── server.coffee ├── main.js ├── server.js ├── README.md └── support └── promise-4.0.0.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | main.js 3 | support 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Remote Control 7 | 16 | 17 | 18 | 19 | 20 |

disconnected

21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-desktop-remote", 3 | "version": "0.2.3", 4 | "description": "Control your Spotify Desktop app with a simple HTTP interface or Socket.io", 5 | "main": "server.js", 6 | "author": { 7 | "name": "Louis Acresti", 8 | "email": "louis.acresti@gmail.com", 9 | "url": "https://namuol.github.io" 10 | }, 11 | "scripts": { 12 | "start": "node server.js", 13 | "prepublish": "grunt" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/namuol/spotify-desktop-remote.git" 18 | }, 19 | "license": "MIT", 20 | "dependencies": { 21 | "express": "^4.8.3", 22 | "socket.io": "^1.0.6", 23 | "serve-static": "^1.5.1" 24 | }, 25 | "devDependencies": { 26 | "grunt": "~0.4.2", 27 | "grunt-contrib-coffee": "^0.11.0", 28 | "grunt-contrib-watch": "^0.6.1", 29 | "grunt-express-server": "~0.4.11", 30 | "coffee-script": "^1.7.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "AppDescription": { 4 | "en": "Control Spotify Player Remotely" 5 | }, 6 | "AppIcon": { 7 | "18x18": "images/icons/icon-18x18.png", 8 | "36x18": "images/icons/icon-36x18.png", 9 | "32x32": "images/icons/icon-32x32.png", 10 | "64x64": "images/icons/icon-64x64.png", 11 | "128x128": "images/icons/icon-128x128.png", 12 | "300x300": "images/icons/icon-300x300.png" 13 | }, 14 | "AppName": { 15 | "en": "Spotify Desktop Remote" 16 | }, 17 | "BundleIdentifier": "spotify-desktop-remote", 18 | "BundleType": "Application", 19 | "BundleVersion": "0.2.3", 20 | "Dependencies": { 21 | "api": "1.0.0" 22 | }, 23 | "RequiredPermissions": [ 24 | "https://*", 25 | "http://*", 26 | "http://localhost:3001/*" 27 | ], 28 | "SupportedDeviceClasses": [ 29 | "Web", 30 | "Desktop" 31 | ], 32 | "SupportedLanguages": [ 33 | "en" 34 | ], 35 | "VendorIdentifier": "io.jove" 36 | } 37 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig 3 | watch: 4 | options: 5 | livereload: true 6 | express: 7 | files: 'src/server.coffee' 8 | tasks: ['express:dev'] 9 | options: 10 | spawn: false 11 | coffee: 12 | files: ['src/main.coffee'] 13 | tasks: ['coffee'] 14 | html: 15 | files: '*.html' 16 | js: 17 | files: '*.js' 18 | 19 | express: 20 | dev: 21 | options: 22 | port: process.env.PORT ? 3001 23 | opts: ['node_modules/coffee-script/bin/coffee'] 24 | script: 'src/server.coffee' 25 | 26 | coffee: 27 | build: 28 | files: 29 | 'main.js': 'src/main.coffee' 30 | 'server.js': 'src/server.coffee' 31 | 32 | grunt.loadNpmTasks "grunt-contrib-coffee" 33 | grunt.loadNpmTasks 'grunt-contrib-watch' 34 | grunt.loadNpmTasks "grunt-express-server" 35 | 36 | grunt.registerTask 'default', ['coffee'] 37 | grunt.registerTask 'dev', ['coffee', 'express:dev', 'watch'] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Louis Acresti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/main.coffee: -------------------------------------------------------------------------------- 1 | require ['$api/models'], (models) -> 2 | window.models = models 3 | 4 | Number::clamp = (min, max) -> 5 | Math.min Math.max(this, min), max 6 | 7 | # Extend Spotify's built in Promise to support promises/A+ spec, for sanity: 8 | models.Promise::then = (onResolved, onRejected) -> 9 | (new Promise (resolve, reject) => 10 | @done (value) -> resolve value 11 | @fail (reason) -> reject reason 12 | ).then onResolved, onRejected 13 | 14 | models.player.load('volume') 15 | 16 | available_songs = {} 17 | 18 | socket = io.connect 'http://localhost:3001' 19 | 20 | socket.on 'connect', -> 21 | socket.emit '__player_connected__' 22 | status = document.getElementById 'status' 23 | status.className = status.innerHTML = 'connected' 24 | 25 | socket.on 'disconnect', -> 26 | status = document.getElementById 'status' 27 | status.className = status.innerHTML = 'disconnected' 28 | 29 | socket.on 'volume', (volume, cb) -> 30 | console.log 'volume', volume 31 | models.player.load('volume').then (player) -> 32 | volume ?= player.volume 33 | models.player.setVolume(parseFloat(volume).clamp(0,1)) 34 | .then (player) -> 35 | cb null, currentStatus 36 | , (err) -> 37 | cb err 38 | 39 | socket.on 'stop', (cb) -> 40 | console.log 'stop' 41 | models.player.stop().then -> 42 | cb null, currentStatus 43 | , (err) -> 44 | console.error err 45 | cb 'Failed to stop.' 46 | 47 | socket.on 'pause', (cb) -> 48 | console.log 'pause' 49 | models.player.pause().then -> 50 | cb null, currentStatus 51 | , (err) -> 52 | console.error err 53 | cb 'Failed to pause.' 54 | 55 | socket.on 'play', (cb) -> 56 | console.log 'play' 57 | models.player.pause() 58 | models.player.play().then -> 59 | cb null, currentStatus 60 | , (err) -> 61 | console.error err 62 | cb 'Failed to play.' 63 | 64 | socket.on 'nextTrack', (cb) -> 65 | console.log 'nextTrack' 66 | models.player.skipToNextTrack().then -> 67 | cb null, currentStatus 68 | , (err) -> 69 | console.error err 70 | cb 'Failed to skip to next track.' 71 | 72 | socket.on 'prevTrack', (cb) -> 73 | console.log 'prevTrack' 74 | models.player.skipToPrevTrack().then -> 75 | cb null, currentStatus 76 | , (err) -> 77 | console.error err 78 | cb 'Failed to skip to prev track.' 79 | 80 | socket.on 'playContext', (params, cb) -> 81 | console.log 'playContext', params 82 | {uri, index, ms, duration} = params 83 | models.player.pause() 84 | models.player.playContext(models.Context.fromURI(uri), index, parseFloat(ms), parseFloat(duration)).then -> 85 | cb null, currentStatus 86 | , (err) -> 87 | console.error err 88 | cb 'Failed to play ' + uri 89 | 90 | socket.on 'playTrack', (params, cb) -> 91 | console.log 'playTrack', params 92 | {uri, ms, duration} = params 93 | models.player.pause() 94 | models.player.playTrack(models.Track.fromURI(uri), parseFloat(ms), parseFloat(duration)).then -> 95 | cb null, currentStatus 96 | , (err) -> 97 | console.error err 98 | cb 'Failed to play ' + uri 99 | 100 | socket.on 'sync', (cb) -> 101 | console.log 'sync' 102 | cb null, currentStatus 103 | 104 | socket.on 'seek', (amount, cb) -> 105 | console.log 'seek', amount 106 | models.player.load('volume', 'playing', 'position', 'duration', 'track').then (player) -> 107 | player.seek(player.duration * parseFloat(amount)) 108 | .then (player) -> 109 | cb null, currentStatus 110 | , (err) -> 111 | console.error err 112 | cb 'Failed to seek to ' + amount 113 | 114 | currentStatus = null 115 | models.player.addEventListener 'change', (event) -> 116 | currentStatus = event.data 117 | socket.emit 'player.change', currentStatus 118 | -------------------------------------------------------------------------------- /src/server.coffee: -------------------------------------------------------------------------------- 1 | express = require 'express' 2 | app = express() 3 | server = require('http').createServer app 4 | io = require('socket.io').listen server 5 | 6 | allowCrossDomain = (req, res, next) -> 7 | res.header "Access-Control-Allow-Origin", '*' 8 | res.header "Access-Control-Allow-Methods", "GET,PUT,POST,DELETE" 9 | res.header "Access-Control-Allow-Headers", "Content-Type" 10 | next() 11 | return 12 | 13 | app.use allowCrossDomain 14 | app.use require('serve-static')(__dirname) 15 | app.use (req, res, next) -> 16 | return res.status(400).send 'Not connected to the player! Ensure you are running the app.' if not spotify_socket 17 | next() 18 | 19 | getParams = (req) -> 20 | result = {} 21 | for own k,v of req.params 22 | result[k] = v 23 | return result 24 | 25 | spotify_socket = null 26 | 27 | io.on 'connection', (socket) -> 28 | socket.on '__player_connected__', -> 29 | spotify_socket = socket 30 | spotify_socket.on 'disconnect', -> 31 | spotify_socket = null 32 | spotify_socket.on 'player.change', (data) -> 33 | spotify_socket.broadcast.emit 'player.change', data 34 | 35 | socket.on 'pause', -> spotify_socket?.emit 'pause' 36 | socket.on 'stop', -> spotify_socket?.emit 'stop' 37 | socket.on 'nextTrack', -> spotify_socket?.emit 'nextTrack' 38 | socket.on 'prevTrack', -> spotify_socket?.emit 'prevTrack' 39 | 40 | socket.on 'volume', (level) -> spotify_socket?.emit 'volume', level 41 | socket.on 'seek', (amount) -> spotify_socket?.emit 'seek', amount 42 | 43 | socket.on 'play', (params) -> 44 | if /^spotify:track:[^:]+$/.test params?.uri 45 | spotify_socket.emit 'playTrack', params 46 | else if /^spotify:(user:[^:]+:playlist|album):[^:]+$/.test params?.uri 47 | spotify_socket.emit 'playContext', params 48 | else 49 | spotify_socket.emit 'play' 50 | 51 | app.get '/volume/:volume', (req, res, next) -> 52 | spotify_socket.emit 'volume', getParams(req).volume, (err, data={}) -> 53 | return res.status(500).send err if err 54 | res.send data 55 | 56 | app.get '/stop', (req, res, next) -> 57 | spotify_socket.emit 'stop', (err, data={}) -> 58 | return res.status(500).send err if err 59 | res.send data 60 | 61 | app.get '/pause', (req, res, next) -> 62 | spotify_socket.emit 'pause', (err, data={}) -> 63 | return res.status(500).send err if err 64 | res.send data 65 | 66 | app.get '/play', (req, res, next) -> 67 | spotify_socket.emit 'play', (err, data={}) -> 68 | return res.status(500).send err if err 69 | res.send data 70 | 71 | app.get '/nextTrack', (req, res, next) -> 72 | spotify_socket.emit 'nextTrack', (err, data={}) -> 73 | return res.status(500).send err if err 74 | res.send data 75 | 76 | app.get '/prevTrack', (req, res, next) -> 77 | spotify_socket.emit 'prevTrack', (err, data={}) -> 78 | return res.status(500).send err if err 79 | res.send data 80 | 81 | app.get /^\/play\/(spotify:track:[^:]+)(\/([0-9]+))?(\/([0-9]+))?/, (req, res, next) -> 82 | params = 83 | uri: req.params[0] 84 | ms: req.params[2] 85 | duration: req.params[4] 86 | 87 | spotify_socket.emit 'playTrack', params, (err, data={}) -> 88 | return res.status(500).send err if err 89 | res.send data 90 | 91 | app.get /^\/play\/(spotify:(user:[^:]+:playlist|album):[^:]+)(\/([0-9]+))?(\/([0-9]+))?(\/([0-9]+))?/, (req, res, next) -> 92 | params = 93 | uri: req.params[0] 94 | index: req.params[4] 95 | ms: req.params[6] 96 | duration: req.params[8] 97 | spotify_socket.emit 'playContext', params, (err, data={}) -> 98 | return res.status(500).send err if err 99 | res.send data 100 | 101 | app.get '/sync', (req, res, next) -> 102 | spotify_socket.emit 'sync', (err, data={}) -> 103 | return res.status(500).send err if err 104 | res.send data 105 | 106 | app.get '/seek/:amount', (req, res, next) -> 107 | spotify_socket.emit 'seek', getParams(req).amount, (err, data={}) -> 108 | return res.status(500).send err if err 109 | res.send data 110 | 111 | server.listen process.env.PORT ? 3001 112 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | require(['$api/models'], function(models) { 3 | var available_songs, currentStatus, socket; 4 | window.models = models; 5 | Number.prototype.clamp = function(min, max) { 6 | return Math.min(Math.max(this, min), max); 7 | }; 8 | models.Promise.prototype.then = function(onResolved, onRejected) { 9 | return (new Promise((function(_this) { 10 | return function(resolve, reject) { 11 | _this.done(function(value) { 12 | return resolve(value); 13 | }); 14 | return _this.fail(function(reason) { 15 | return reject(reason); 16 | }); 17 | }; 18 | })(this))).then(onResolved, onRejected); 19 | }; 20 | models.player.load('volume'); 21 | available_songs = {}; 22 | socket = io.connect('http://localhost:3001'); 23 | socket.on('connect', function() { 24 | var status; 25 | socket.emit('__player_connected__'); 26 | status = document.getElementById('status'); 27 | return status.className = status.innerHTML = 'connected'; 28 | }); 29 | socket.on('disconnect', function() { 30 | var status; 31 | status = document.getElementById('status'); 32 | return status.className = status.innerHTML = 'disconnected'; 33 | }); 34 | socket.on('volume', function(volume, cb) { 35 | console.log('volume', volume); 36 | return models.player.load('volume').then(function(player) { 37 | if (volume == null) { 38 | volume = player.volume; 39 | } 40 | return models.player.setVolume(parseFloat(volume).clamp(0, 1)); 41 | }).then(function(player) { 42 | return cb(null, currentStatus); 43 | }, function(err) { 44 | return cb(err); 45 | }); 46 | }); 47 | socket.on('stop', function(cb) { 48 | console.log('stop'); 49 | return models.player.stop().then(function() { 50 | return cb(null, currentStatus); 51 | }, function(err) { 52 | console.error(err); 53 | return cb('Failed to stop.'); 54 | }); 55 | }); 56 | socket.on('pause', function(cb) { 57 | console.log('pause'); 58 | return models.player.pause().then(function() { 59 | return cb(null, currentStatus); 60 | }, function(err) { 61 | console.error(err); 62 | return cb('Failed to pause.'); 63 | }); 64 | }); 65 | socket.on('play', function(cb) { 66 | console.log('play'); 67 | models.player.pause(); 68 | return models.player.play().then(function() { 69 | return cb(null, currentStatus); 70 | }, function(err) { 71 | console.error(err); 72 | return cb('Failed to play.'); 73 | }); 74 | }); 75 | socket.on('nextTrack', function(cb) { 76 | console.log('nextTrack'); 77 | return models.player.skipToNextTrack().then(function() { 78 | return cb(null, currentStatus); 79 | }, function(err) { 80 | console.error(err); 81 | return cb('Failed to skip to next track.'); 82 | }); 83 | }); 84 | socket.on('prevTrack', function(cb) { 85 | console.log('prevTrack'); 86 | return models.player.skipToPrevTrack().then(function() { 87 | return cb(null, currentStatus); 88 | }, function(err) { 89 | console.error(err); 90 | return cb('Failed to skip to prev track.'); 91 | }); 92 | }); 93 | socket.on('playContext', function(params, cb) { 94 | var duration, index, ms, uri; 95 | console.log('playContext', params); 96 | uri = params.uri, index = params.index, ms = params.ms, duration = params.duration; 97 | models.player.pause(); 98 | return models.player.playContext(models.Context.fromURI(uri), index, parseFloat(ms), parseFloat(duration)).then(function() { 99 | return cb(null, currentStatus); 100 | }, function(err) { 101 | console.error(err); 102 | return cb('Failed to play ' + uri); 103 | }); 104 | }); 105 | socket.on('playTrack', function(params, cb) { 106 | var duration, ms, uri; 107 | console.log('playTrack', params); 108 | uri = params.uri, ms = params.ms, duration = params.duration; 109 | models.player.pause(); 110 | return models.player.playTrack(models.Track.fromURI(uri), parseFloat(ms), parseFloat(duration)).then(function() { 111 | return cb(null, currentStatus); 112 | }, function(err) { 113 | console.error(err); 114 | return cb('Failed to play ' + uri); 115 | }); 116 | }); 117 | socket.on('sync', function(cb) { 118 | console.log('sync'); 119 | return cb(null, currentStatus); 120 | }); 121 | socket.on('seek', function(amount, cb) { 122 | console.log('seek', amount); 123 | return models.player.load('volume', 'playing', 'position', 'duration', 'track').then(function(player) { 124 | return player.seek(player.duration * parseFloat(amount)); 125 | }).then(function(player) { 126 | return cb(null, currentStatus); 127 | }, function(err) { 128 | console.error(err); 129 | return cb('Failed to seek to ' + amount); 130 | }); 131 | }); 132 | currentStatus = null; 133 | return models.player.addEventListener('change', function(event) { 134 | currentStatus = event.data; 135 | return socket.emit('player.change', currentStatus); 136 | }); 137 | }); 138 | 139 | }).call(this); 140 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var allowCrossDomain, app, express, getParams, io, server, spotify_socket, _ref, 3 | __hasProp = {}.hasOwnProperty; 4 | 5 | express = require('express'); 6 | 7 | app = express(); 8 | 9 | server = require('http').createServer(app); 10 | 11 | io = require('socket.io').listen(server); 12 | 13 | allowCrossDomain = function(req, res, next) { 14 | res.header("Access-Control-Allow-Origin", '*'); 15 | res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE"); 16 | res.header("Access-Control-Allow-Headers", "Content-Type"); 17 | next(); 18 | }; 19 | 20 | app.use(allowCrossDomain); 21 | 22 | app.use(require('serve-static')(__dirname)); 23 | 24 | app.use(function(req, res, next) { 25 | if (!spotify_socket) { 26 | return res.status(400).send('Not connected to the player! Ensure you are running the app.'); 27 | } 28 | return next(); 29 | }); 30 | 31 | getParams = function(req) { 32 | var k, result, v, _ref; 33 | result = {}; 34 | _ref = req.params; 35 | for (k in _ref) { 36 | if (!__hasProp.call(_ref, k)) continue; 37 | v = _ref[k]; 38 | result[k] = v; 39 | } 40 | return result; 41 | }; 42 | 43 | spotify_socket = null; 44 | 45 | io.on('connection', function(socket) { 46 | socket.on('__player_connected__', function() { 47 | spotify_socket = socket; 48 | spotify_socket.on('disconnect', function() { 49 | return spotify_socket = null; 50 | }); 51 | return spotify_socket.on('player.change', function(data) { 52 | return spotify_socket.broadcast.emit('player.change', data); 53 | }); 54 | }); 55 | socket.on('pause', function() { 56 | return spotify_socket != null ? spotify_socket.emit('pause') : void 0; 57 | }); 58 | socket.on('stop', function() { 59 | return spotify_socket != null ? spotify_socket.emit('stop') : void 0; 60 | }); 61 | socket.on('nextTrack', function() { 62 | return spotify_socket != null ? spotify_socket.emit('nextTrack') : void 0; 63 | }); 64 | socket.on('prevTrack', function() { 65 | return spotify_socket != null ? spotify_socket.emit('prevTrack') : void 0; 66 | }); 67 | socket.on('volume', function(level) { 68 | return spotify_socket != null ? spotify_socket.emit('volume', level) : void 0; 69 | }); 70 | socket.on('seek', function(amount) { 71 | return spotify_socket != null ? spotify_socket.emit('seek', amount) : void 0; 72 | }); 73 | socket.on('play', function(params) { 74 | if (/^spotify:track:[^:]+$/.test(params != null ? params.uri : void 0)) { 75 | return spotify_socket.emit('playTrack', params); 76 | } else if (/^spotify:(user:[^:]+:playlist|album):[^:]+$/.test(params != null ? params.uri : void 0)) { 77 | return spotify_socket.emit('playContext', params); 78 | } else { 79 | return spotify_socket.emit('play'); 80 | } 81 | }); 82 | app.get('/volume/:volume', function(req, res, next) { 83 | return spotify_socket.emit('volume', getParams(req).volume, function(err, data) { 84 | if (data == null) { 85 | data = {}; 86 | } 87 | if (err) { 88 | return res.status(500).send(err); 89 | } 90 | return res.send(data); 91 | }); 92 | }); 93 | app.get('/stop', function(req, res, next) { 94 | return spotify_socket.emit('stop', function(err, data) { 95 | if (data == null) { 96 | data = {}; 97 | } 98 | if (err) { 99 | return res.status(500).send(err); 100 | } 101 | return res.send(data); 102 | }); 103 | }); 104 | app.get('/pause', function(req, res, next) { 105 | return spotify_socket.emit('pause', function(err, data) { 106 | if (data == null) { 107 | data = {}; 108 | } 109 | if (err) { 110 | return res.status(500).send(err); 111 | } 112 | return res.send(data); 113 | }); 114 | }); 115 | app.get('/play', function(req, res, next) { 116 | return spotify_socket.emit('play', function(err, data) { 117 | if (data == null) { 118 | data = {}; 119 | } 120 | if (err) { 121 | return res.status(500).send(err); 122 | } 123 | return res.send(data); 124 | }); 125 | }); 126 | app.get('/nextTrack', function(req, res, next) { 127 | return spotify_socket.emit('nextTrack', function(err, data) { 128 | if (data == null) { 129 | data = {}; 130 | } 131 | if (err) { 132 | return res.status(500).send(err); 133 | } 134 | return res.send(data); 135 | }); 136 | }); 137 | app.get('/prevTrack', function(req, res, next) { 138 | return spotify_socket.emit('prevTrack', function(err, data) { 139 | if (data == null) { 140 | data = {}; 141 | } 142 | if (err) { 143 | return res.status(500).send(err); 144 | } 145 | return res.send(data); 146 | }); 147 | }); 148 | app.get(/^\/play\/(spotify:track:[^:]+)(\/([0-9]+))?(\/([0-9]+))?/, function(req, res, next) { 149 | var params; 150 | params = { 151 | uri: req.params[0], 152 | ms: req.params[2], 153 | duration: req.params[4] 154 | }; 155 | return spotify_socket.emit('playTrack', params, function(err, data) { 156 | if (data == null) { 157 | data = {}; 158 | } 159 | if (err) { 160 | return res.status(500).send(err); 161 | } 162 | return res.send(data); 163 | }); 164 | }); 165 | app.get(/^\/play\/(spotify:(user:[^:]+:playlist|album):[^:]+)(\/([0-9]+))?(\/([0-9]+))?(\/([0-9]+))?/, function(req, res, next) { 166 | var params; 167 | params = { 168 | uri: req.params[0], 169 | index: req.params[4], 170 | ms: req.params[6], 171 | duration: req.params[8] 172 | }; 173 | return spotify_socket.emit('playContext', params, function(err, data) { 174 | if (data == null) { 175 | data = {}; 176 | } 177 | if (err) { 178 | return res.status(500).send(err); 179 | } 180 | return res.send(data); 181 | }); 182 | }); 183 | app.get('/sync', function(req, res, next) { 184 | return spotify_socket.emit('sync', function(err, data) { 185 | if (data == null) { 186 | data = {}; 187 | } 188 | if (err) { 189 | return res.status(500).send(err); 190 | } 191 | return res.send(data); 192 | }); 193 | }); 194 | return app.get('/seek/:amount', function(req, res, next) { 195 | return spotify_socket.emit('seek', getParams(req).amount, function(err, data) { 196 | if (data == null) { 197 | data = {}; 198 | } 199 | if (err) { 200 | return res.status(500).send(err); 201 | } 202 | return res.send(data); 203 | }); 204 | }); 205 | }); 206 | 207 | server.listen((_ref = process.env.PORT) != null ? _ref : 3001); 208 | 209 | }).call(this); 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # No Longer Supported 2 | 3 | Spotify no longer officially supports Desktop Apps, so this probably wont work for you. 4 | 5 | If you can think of an alternative way to control Spotify Desktop with a similar interface, please [let me know](https://github.com/namuol/spotify-desktop-remote/issues)! 6 | 7 | Some possible alternatives: 8 | 9 | - [Spotify Web Helper](https://github.com/onetune/spotify-web-helper) 10 | - [nutgaard/SpotifyHttpJs](https://github.com/nutgaard/SpotifyHttpJs) 11 | - [cgbystrom/spotify-local-http-api](https://github.com/cgbystrom/spotify-local-http-api) 12 | + [Great article about Spotify's built-in HTTP server](http://cgbystrom.com/articles/deconstructing-spotifys-builtin-http-server/) 13 | 14 | ---- 15 | 16 | # Spotify Desktop Remote 17 | 18 | Control your Spotify Desktop app from a simple HTTP interface or with [Socket.IO](http://socket.io). 19 | 20 | ```bash 21 | # Play a track: 22 | curl http://localhost:3001/play/spotify:track:0FutrWIUM5Mg3434asiwkp 23 | 24 | # Seek to the halfway mark of the song: 25 | curl http://localhost:3001/seek/0.5 26 | 27 | # Set the player volume: 28 | curl http://localhost:3001/volume/0.8 29 | 30 | # Play a playlist: 31 | curl http://localhost:3001/play/spotify:album:2YJFLMyzzZ2k4mhfPSiOj2 32 | 33 | # Pause the player: 34 | curl http://localhost:3001/pause 35 | 36 | # Stop the player: 37 | curl http://localhost:3001/stop 38 | ``` 39 | 40 | ```js 41 | // Keep everything in sync: 42 | socket.on('player.change', function (playerStatus) { 43 | console.log('The current player status is', playerStatus); 44 | }); 45 | 46 | // Play a track: 47 | socket.emit('play', {uri: 'spotify:track:0FutrWIUM5Mg3434asiwkp'}); 48 | 49 | // Seek to the halfway mark of the song: 50 | socket.emit('seek', 0.5); 51 | 52 | // Set the player volume: 53 | socket.emit('volume', 0.8); 54 | 55 | // Play a playlist: 56 | socket.emit('play', {uri: 'spotify:album:2YJFLMyzzZ2k4mhfPSiOj2'}); 57 | 58 | // Pause the player: 59 | socket.emit('pause'); 60 | 61 | // Stop the player: 62 | socket.emit('stop'); 63 | ``` 64 | 65 | See the [API reference](#api) for more details. 66 | 67 | ## Requirements 68 | 69 | - [node.js](http://nodejs.org) >= 0.10 70 | - A premium Spotify account, [registered as a developer](https://devaccount.spotify.com/my-account/). 71 | 72 | ## Installation 73 | 74 | There are two parts to the app: 75 | 76 | 1. The HTTP Server that forwards commands to Spotify (`src/server.coffee`) 77 | 2. The Spotify Webapp (runs inside Spotify Desktop) that accepts commands from the server via Websockets (`src/main.coffee` and `index.html`) 78 | 79 | ```bash 80 | # OS X/Linux users: 81 | cd ~/Spotify 82 | 83 | # Windows users: 84 | # cd ~/My\ Documents/Spotify 85 | 86 | git clone https://github.com/namuol/spotify-desktop-remote.git 87 | cd spotify-desktop-remote 88 | 89 | # Start the server on port 3001: 90 | npm start 91 | 92 | # Or run it on a different port: 93 | # PORT=3002 npm start 94 | 95 | # Finally, run spotify and open the app: 96 | spotify -uri spotify:app:spotify-desktop-remote 97 | 98 | # NOTE: You can also run the app by entering 99 | # "spotify:app:spotify-desktop-remote" into Spotify's search bar. 100 | 101 | # Now you can control the Spotify Desktop app by hitting the server: 102 | curl http://localhost:3001/play/spotify:track:0FutrWIUM5Mg3434asiwkp 103 | curl http://localhost:3001/volume/1 104 | ``` 105 | 106 | ## API 107 | 108 | ### Responses 109 | 110 | All GET operations and the [`player.change`](#player.change) socket event return a JSON object representing the current status of the player: 111 | 112 | ```js 113 | { 114 | loading: false, 115 | playing: true, 116 | position: 19450, 117 | duration: 212400, 118 | index: 0, 119 | repeat: false, 120 | shuffle: false, 121 | volume: 0.849988579750061, 122 | context: null, 123 | contexts: [{ 124 | index: 0, 125 | descriptor: { 126 | type: "set" 127 | } 128 | }], 129 | track: { 130 | artists: [{ 131 | name: "Rick Astley", 132 | uri: "spotify:artist:0gxyHStUsqpMadRV0Di1Qt" 133 | }], 134 | disc: 1, 135 | duration: 212000, 136 | image: "spotify:image:938dfdd57d4fe8a864f6148ffb9676395d012720", 137 | images: [ 138 | [ 139 | 64, 140 | "spotify:image:9b87c26f500947d28838ebb2e33c120f6b9a6b1b" 141 | ], 142 | [ 143 | 300, 144 | "spotify:image:938dfdd57d4fe8a864f6148ffb9676395d012720" 145 | ], 146 | [ 147 | 600, 148 | "spotify:image:d6e92c8891f16c1126c6d58f47da81873a17e993" 149 | ] 150 | ], 151 | name: "Never Gonna Give You Up", 152 | number: 1, 153 | playable: true, 154 | popularity: 65, 155 | starred: false, 156 | explicit: false, 157 | availability: "premium", 158 | album: { 159 | uri: "spotify:album:3vGtqTr5he9uQfusQWJ0oC" 160 | }, 161 | local: false, 162 | advertisement: false, 163 | placeholder: false, 164 | uri: "spotify:track:0FutrWIUM5Mg3434asiwkp" 165 | } 166 | } 167 | ``` 168 | 169 | ### Socket.io 170 | 171 | In order to use socket.io, include the following in your ``: 172 | 173 | ```html 174 | 175 | ``` 176 | 177 | Then somewhere after that you can connect: 178 | 179 | ```js 180 | var socket = io.connect(); 181 | socket.on('player.change', function (playerStatus) { 182 | console.log('The current player status is', playerStatus); 183 | }); 184 | ``` 185 | 186 | 187 | #### `socket.on('player.change', callback(playerStatus))` 188 | *socket only* 189 | 190 | Subscribe to this event to be notified whenever anything about the player changes. 191 | 192 | To poll for the status (with sockets or `GET`), see [`sync`](#sync). 193 | 194 | ```js 195 | socket.on('player.change', function (playerStatus) { 196 | console.log('The current volume level is', playerStatus.volume) 197 | }); 198 | ``` 199 | 200 | Parameters: 201 | > **`callback(playerStatus)`** *socket only* 202 | > A callback function that accepts a single argument as the [player's current status](#responses). 203 | 204 | #### `/sync` 205 | #### `socket.emit('sync', callback(playerStatus))` 206 | Perform no action; simply used to retrieve the current status of the player. 207 | 208 | ```bash 209 | curl http://localhost:3001/sync 210 | ``` 211 | 212 | ```js 213 | socket.emit('sync', function (playerStatus) { 214 | console.log('The current volume level is', playerStatus.volume); 215 | }); 216 | ``` 217 | 218 | Parameters: 219 | > **`callback(playerStatus)`** *socket only* 220 | > A callback function that accepts a single argument as the [player's current status](#responses). 221 | 222 | #### `/play` 223 | #### `socket.emit('play')` 224 | Play the current track. 225 | 226 | ```bash 227 | curl http://localhost:3001/play 228 | ``` 229 | 230 | ```js 231 | socket.emit('play'); 232 | ``` 233 | 234 | #### `/play/:track_uri/:ms?/:duration?` 235 | #### `socket.emit('play', {uri[, ms, duration]})` 236 | Play a specific track with a given URI. 237 | 238 | ```bash 239 | curl http://localhost:3001/play/spotify:track:0FutrWIUM5Mg3434asiwkp 240 | 241 | # Play the first 30 seconds: 242 | curl http://localhost:3001/play/spotify:track:0FutrWIUM5Mg3434asiwkp/0/30000 243 | 244 | # Play the first 30 seconds starting one minute into the song: 245 | curl http://localhost:3001/play/spotify:track:0FutrWIUM5Mg3434asiwkp/60000/30000 246 | ``` 247 | 248 | ```js 249 | socket.emit('play', {uri: 'spotify:track:0FutrWIUM5Mg3434asiwkp'}); 250 | 251 | // Play the first 30 seconds: 252 | socket.emit('play', { 253 | uri: 'spotify:track:0FutrWIUM5Mg3434asiwkp', 254 | ms: 0, 255 | duration: 30000 256 | }); 257 | 258 | // Play the first 30 seconds starting one minute into the song: 259 | socket.emit('play', { 260 | uri: 'spotify:track:0FutrWIUM5Mg3434asiwkp', 261 | ms: 60000, 262 | duration: 30000 263 | }); 264 | ``` 265 | 266 | Parameters: 267 | > **`track_uri`** / **`uri`** 268 | > A spotify track URI. 269 | > 270 | > Example: `spotify:track:0FutrWIUM5Mg3434asiwkp` 271 | 272 | > **`ms`** *optional* 273 | > Number of milliseconds to begin playing the track at. 274 | 275 | > **`duration`** *optional* 276 | > Number of milliseconds to play the song for before stopping. 277 | 278 | #### `/play/:playlist_uri/:index?/:ms?/:duration?` 279 | #### `socket.emit('play', {uri[, index, ms, duration]})` 280 | Play a specific album or user playlist with a given URI. 281 | 282 | ```bash 283 | curl http://localhost:3001/play/spotify:album:2YJFLMyzzZ2k4mhfPSiOj2 284 | curl http://localhost:3001/play/spotify:user:spotify:playlist:4BKT5olNFqLB1FAa8OtC8k 285 | 286 | # Start at the third track in the playlist: 287 | curl http://localhost:3001/play/spotify:user:spotify:playlist:4BKT5olNFqLB1FAa8OtC8k/3 288 | 289 | # Start a minute into the third track in the playlist: 290 | curl http://localhost:3001/play/spotify:user:spotify:playlist:4BKT5olNFqLB1FAa8OtC8k/3/60000 291 | 292 | # Start a minute into the third track in the playlist and play the first 30 seconds: 293 | curl http://localhost:3001/play/spotify:user:spotify:playlist:4BKT5olNFqLB1FAa8OtC8k/3/60000/30000 294 | ``` 295 | 296 | ```js 297 | socket.emit('play', {uri: 'spotify:album:2YJFLMyzzZ2k4mhfPSiOj2'}); 298 | socket.emit('play', {uri: 'spotify:user:spotify:playlist:4BKT5olNFqLB1FAa8OtC8'}); 299 | 300 | // Start at the third track in the playlist: 301 | socket.emit('play', { 302 | uri: 'spotify:user:spotify:playlist:4BKT5olNFqLB1FAa8OtC8k', 303 | index: 3 304 | }); 305 | 306 | // Start a minute into the third track in the playlist: 307 | socket.emit('play', { 308 | uri: 'spotify:user:spotify:playlist:4BKT5olNFqLB1FAa8OtC8k', 309 | index: 3, 310 | ms: 60000 311 | }); 312 | 313 | // Start a minute into the third track in the playlist and play the first 30 seconds: 314 | socket.emit('play', { 315 | uri: 'spotify:user:spotify:playlist:4BKT5olNFqLB1FAa8OtC8k', 316 | index: 3, 317 | ms: 60000, 318 | duration: 30000 319 | }); 320 | ``` 321 | 322 | Parameters: 323 | > **`playlist_uri`** / **`uri`** 324 | > A spotify playlist URI (an album or user playlist). 325 | > 326 | > Example: `spotify:album:2YJFLMyzzZ2k4mhfPSiOj2` 327 | > 328 | > Example: `spotify:user:spotify:playlist:4BKT5olNFqLB1FAa8OtC8k` 329 | 330 | > **`index`** *optional* 331 | > The track number to play (starting at zero). 332 | 333 | > **`ms`** *optional* 334 | > Number of milliseconds to begin playing the track at. 335 | 336 | > **`duration`** *optional* 337 | > Number of milliseconds to play the song for before stopping. 338 | 339 | #### `/pause` 340 | #### `socket.emit('pause')` 341 | Pause the player. 342 | 343 | ```bash 344 | curl http://localhost:3001/pause 345 | ``` 346 | 347 | ```js 348 | socket.emit('pause'); 349 | ``` 350 | 351 | #### `/stop` 352 | #### `socket.emit('stop')` 353 | Stop the player. 354 | 355 | ```bash 356 | curl http://localhost:3001/stop 357 | ``` 358 | 359 | ```js 360 | socket.emit('pause'); 361 | ``` 362 | 363 | #### `/volume/:volume` 364 | #### `socket.emit('volume', volume)` 365 | Set the player volume level. 366 | 367 | ```bash 368 | curl http://localhost:3001/volume/1 369 | curl http://localhost:3001/volume/0 370 | curl http://localhost:3001/volume/0.5 371 | ``` 372 | 373 | ```js 374 | socket.emit('volume', 1); 375 | socket.emit('volume', 0); 376 | socket.emit('volume', 0.5); 377 | ``` 378 | 379 | Parameters: 380 | > **`volume`** 381 | > A number representing the volume level, between 0 and 1. 382 | 383 | #### `/seek/:amount` 384 | #### `socket.emit('seek', amount)` 385 | Set the playhead's position. 386 | 387 | ```bash 388 | curl http://localhost:3001/seek/0 389 | curl http://localhost:3001/seek/0.5 390 | ``` 391 | 392 | ```js 393 | socket.emit('seek', 0); 394 | socket.emit('seek', 0.5); 395 | ``` 396 | 397 | 398 | Parameters: 399 | > **`amount`** 400 | > A number representing the position of the seek bar, between 0 and 1. 401 | 402 | ## License 403 | 404 | MIT 405 | 406 | ---- 407 | 408 | [![Analytics](https://ga-beacon.appspot.com/UA-33247419-2/spotify-desktop-remote/README.md)](https://github.com/igrigorik/ga-beacon) 409 | -------------------------------------------------------------------------------- /support/promise-4.0.0.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { 24 | var fn = queue.shift(); 25 | fn(); 26 | } 27 | } 28 | }, true); 29 | 30 | return function nextTick(fn) { 31 | queue.push(fn); 32 | window.postMessage('process-tick', '*'); 33 | }; 34 | } 35 | 36 | return function nextTick(fn) { 37 | setTimeout(fn, 0); 38 | }; 39 | })(); 40 | 41 | process.title = 'browser'; 42 | process.browser = true; 43 | process.env = {}; 44 | process.argv = []; 45 | 46 | function noop() {} 47 | 48 | process.on = noop; 49 | process.once = noop; 50 | process.off = noop; 51 | process.emit = noop; 52 | 53 | process.binding = function (name) { 54 | throw new Error('process.binding is not supported'); 55 | } 56 | 57 | // TODO(shtylman) 58 | process.cwd = function () { return '/' }; 59 | process.chdir = function (dir) { 60 | throw new Error('process.chdir is not supported'); 61 | }; 62 | 63 | },{}],2:[function(require,module,exports){ 64 | 'use strict'; 65 | 66 | var asap = require('asap') 67 | 68 | module.exports = Promise 69 | function Promise(fn) { 70 | if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new') 71 | if (typeof fn !== 'function') throw new TypeError('not a function') 72 | var state = null 73 | var value = null 74 | var deferreds = [] 75 | var self = this 76 | 77 | this.then = function(onFulfilled, onRejected) { 78 | return new Promise(function(resolve, reject) { 79 | handle(new Handler(onFulfilled, onRejected, resolve, reject)) 80 | }) 81 | } 82 | 83 | function handle(deferred) { 84 | if (state === null) { 85 | deferreds.push(deferred) 86 | return 87 | } 88 | asap(function() { 89 | var cb = state ? deferred.onFulfilled : deferred.onRejected 90 | if (cb === null) { 91 | (state ? deferred.resolve : deferred.reject)(value) 92 | return 93 | } 94 | var ret 95 | try { 96 | ret = cb(value) 97 | } 98 | catch (e) { 99 | deferred.reject(e) 100 | return 101 | } 102 | deferred.resolve(ret) 103 | }) 104 | } 105 | 106 | function resolve(newValue) { 107 | try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure 108 | if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.') 109 | if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { 110 | var then = newValue.then 111 | if (typeof then === 'function') { 112 | doResolve(then.bind(newValue), resolve, reject) 113 | return 114 | } 115 | } 116 | state = true 117 | value = newValue 118 | finale() 119 | } catch (e) { reject(e) } 120 | } 121 | 122 | function reject(newValue) { 123 | state = false 124 | value = newValue 125 | finale() 126 | } 127 | 128 | function finale() { 129 | for (var i = 0, len = deferreds.length; i < len; i++) 130 | handle(deferreds[i]) 131 | deferreds = null 132 | } 133 | 134 | doResolve(fn, resolve, reject) 135 | } 136 | 137 | 138 | function Handler(onFulfilled, onRejected, resolve, reject){ 139 | this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null 140 | this.onRejected = typeof onRejected === 'function' ? onRejected : null 141 | this.resolve = resolve 142 | this.reject = reject 143 | } 144 | 145 | /** 146 | * Take a potentially misbehaving resolver function and make sure 147 | * onFulfilled and onRejected are only called once. 148 | * 149 | * Makes no guarantees about asynchrony. 150 | */ 151 | function doResolve(fn, onFulfilled, onRejected) { 152 | var done = false; 153 | try { 154 | fn(function (value) { 155 | if (done) return 156 | done = true 157 | onFulfilled(value) 158 | }, function (reason) { 159 | if (done) return 160 | done = true 161 | onRejected(reason) 162 | }) 163 | } catch (ex) { 164 | if (done) return 165 | done = true 166 | onRejected(ex) 167 | } 168 | } 169 | 170 | },{"asap":4}],3:[function(require,module,exports){ 171 | 'use strict'; 172 | 173 | //This file contains then/promise specific extensions to the core promise API 174 | 175 | var Promise = require('./core.js') 176 | var asap = require('asap') 177 | 178 | module.exports = Promise 179 | 180 | /* Static Functions */ 181 | 182 | function ValuePromise(value) { 183 | this.then = function (onFulfilled) { 184 | if (typeof onFulfilled !== 'function') return this 185 | return new Promise(function (resolve, reject) { 186 | asap(function () { 187 | try { 188 | resolve(onFulfilled(value)) 189 | } catch (ex) { 190 | reject(ex); 191 | } 192 | }) 193 | }) 194 | } 195 | } 196 | ValuePromise.prototype = Object.create(Promise.prototype) 197 | 198 | var TRUE = new ValuePromise(true) 199 | var FALSE = new ValuePromise(false) 200 | var NULL = new ValuePromise(null) 201 | var UNDEFINED = new ValuePromise(undefined) 202 | var ZERO = new ValuePromise(0) 203 | var EMPTYSTRING = new ValuePromise('') 204 | 205 | Promise.from = Promise.cast = function (value) { 206 | if (value instanceof Promise) return value 207 | 208 | if (value === null) return NULL 209 | if (value === undefined) return UNDEFINED 210 | if (value === true) return TRUE 211 | if (value === false) return FALSE 212 | if (value === 0) return ZERO 213 | if (value === '') return EMPTYSTRING 214 | 215 | if (typeof value === 'object' || typeof value === 'function') { 216 | try { 217 | var then = value.then 218 | if (typeof then === 'function') { 219 | return new Promise(then.bind(value)) 220 | } 221 | } catch (ex) { 222 | return new Promise(function (resolve, reject) { 223 | reject(ex) 224 | }) 225 | } 226 | } 227 | 228 | return new ValuePromise(value) 229 | } 230 | Promise.denodeify = function (fn, argumentCount) { 231 | argumentCount = argumentCount || Infinity 232 | return function () { 233 | var self = this 234 | var args = Array.prototype.slice.call(arguments) 235 | return new Promise(function (resolve, reject) { 236 | while (args.length && args.length > argumentCount) { 237 | args.pop() 238 | } 239 | args.push(function (err, res) { 240 | if (err) reject(err) 241 | else resolve(res) 242 | }) 243 | fn.apply(self, args) 244 | }) 245 | } 246 | } 247 | Promise.nodeify = function (fn) { 248 | return function () { 249 | var args = Array.prototype.slice.call(arguments) 250 | var callback = typeof args[args.length - 1] === 'function' ? args.pop() : null 251 | try { 252 | return fn.apply(this, arguments).nodeify(callback) 253 | } catch (ex) { 254 | if (callback === null || typeof callback == 'undefined') { 255 | return new Promise(function (resolve, reject) { reject(ex) }) 256 | } else { 257 | asap(function () { 258 | callback(ex) 259 | }) 260 | } 261 | } 262 | } 263 | } 264 | 265 | Promise.all = function () { 266 | var args = Array.prototype.slice.call(arguments.length === 1 && Array.isArray(arguments[0]) ? arguments[0] : arguments) 267 | 268 | return new Promise(function (resolve, reject) { 269 | if (args.length === 0) return resolve([]) 270 | var remaining = args.length 271 | function res(i, val) { 272 | try { 273 | if (val && (typeof val === 'object' || typeof val === 'function')) { 274 | var then = val.then 275 | if (typeof then === 'function') { 276 | then.call(val, function (val) { res(i, val) }, reject) 277 | return 278 | } 279 | } 280 | args[i] = val 281 | if (--remaining === 0) { 282 | resolve(args); 283 | } 284 | } catch (ex) { 285 | reject(ex) 286 | } 287 | } 288 | for (var i = 0; i < args.length; i++) { 289 | res(i, args[i]) 290 | } 291 | }) 292 | } 293 | 294 | /* Prototype Methods */ 295 | 296 | Promise.prototype.done = function (onFulfilled, onRejected) { 297 | var self = arguments.length ? this.then.apply(this, arguments) : this 298 | self.then(null, function (err) { 299 | asap(function () { 300 | throw err 301 | }) 302 | }) 303 | } 304 | 305 | Promise.prototype.nodeify = function (callback) { 306 | if (callback === null || typeof callback == 'undefined') return this 307 | 308 | this.then(function (value) { 309 | asap(function () { 310 | callback(null, value) 311 | }) 312 | }, function (err) { 313 | asap(function () { 314 | callback(err) 315 | }) 316 | }) 317 | } 318 | 319 | Promise.prototype.catch = function (onRejected) { 320 | return this.then(null, onRejected); 321 | } 322 | 323 | 324 | Promise.resolve = function (value) { 325 | return new Promise(function (resolve) { 326 | resolve(value); 327 | }); 328 | } 329 | 330 | Promise.reject = function (value) { 331 | return new Promise(function (resolve, reject) { 332 | reject(value); 333 | }); 334 | } 335 | 336 | Promise.race = function (values) { 337 | return new Promise(function (resolve, reject) { 338 | values.map(function(value){ 339 | Promise.cast(value).then(resolve, reject); 340 | }) 341 | }); 342 | } 343 | 344 | },{"./core.js":2,"asap":4}],4:[function(require,module,exports){ 345 | (function (process){ 346 | 347 | // Use the fastest possible means to execute a task in a future turn 348 | // of the event loop. 349 | 350 | // linked list of tasks (single, with head node) 351 | var head = {task: void 0, next: null}; 352 | var tail = head; 353 | var flushing = false; 354 | var requestFlush = void 0; 355 | var isNodeJS = false; 356 | 357 | function flush() { 358 | /* jshint loopfunc: true */ 359 | 360 | while (head.next) { 361 | head = head.next; 362 | var task = head.task; 363 | head.task = void 0; 364 | var domain = head.domain; 365 | 366 | if (domain) { 367 | head.domain = void 0; 368 | domain.enter(); 369 | } 370 | 371 | try { 372 | task(); 373 | 374 | } catch (e) { 375 | if (isNodeJS) { 376 | // In node, uncaught exceptions are considered fatal errors. 377 | // Re-throw them synchronously to interrupt flushing! 378 | 379 | // Ensure continuation if the uncaught exception is suppressed 380 | // listening "uncaughtException" events (as domains does). 381 | // Continue in next event to avoid tick recursion. 382 | if (domain) { 383 | domain.exit(); 384 | } 385 | setTimeout(flush, 0); 386 | if (domain) { 387 | domain.enter(); 388 | } 389 | 390 | throw e; 391 | 392 | } else { 393 | // In browsers, uncaught exceptions are not fatal. 394 | // Re-throw them asynchronously to avoid slow-downs. 395 | setTimeout(function() { 396 | throw e; 397 | }, 0); 398 | } 399 | } 400 | 401 | if (domain) { 402 | domain.exit(); 403 | } 404 | } 405 | 406 | flushing = false; 407 | } 408 | 409 | if (typeof process !== "undefined" && process.nextTick) { 410 | // Node.js before 0.9. Note that some fake-Node environments, like the 411 | // Mocha test runner, introduce a `process` global without a `nextTick`. 412 | isNodeJS = true; 413 | 414 | requestFlush = function () { 415 | process.nextTick(flush); 416 | }; 417 | 418 | } else if (typeof setImmediate === "function") { 419 | // In IE10, Node.js 0.9+, or https://github.com/NobleJS/setImmediate 420 | if (typeof window !== "undefined") { 421 | requestFlush = setImmediate.bind(window, flush); 422 | } else { 423 | requestFlush = function () { 424 | setImmediate(flush); 425 | }; 426 | } 427 | 428 | } else if (typeof MessageChannel !== "undefined") { 429 | // modern browsers 430 | // http://www.nonblocking.io/2011/06/windownexttick.html 431 | var channel = new MessageChannel(); 432 | channel.port1.onmessage = flush; 433 | requestFlush = function () { 434 | channel.port2.postMessage(0); 435 | }; 436 | 437 | } else { 438 | // old browsers 439 | requestFlush = function () { 440 | setTimeout(flush, 0); 441 | }; 442 | } 443 | 444 | function asap(task) { 445 | tail = tail.next = { 446 | task: task, 447 | domain: isNodeJS && process.domain, 448 | next: null 449 | }; 450 | 451 | if (!flushing) { 452 | flushing = true; 453 | requestFlush(); 454 | } 455 | }; 456 | 457 | module.exports = asap; 458 | 459 | 460 | }).call(this,require("/Users/forbeslindesay/GitHub/promisejs.org/node_modules/browserify/node_modules/insert-module-globals/node_modules/process/browser.js")) 461 | },{"/Users/forbeslindesay/GitHub/promisejs.org/node_modules/browserify/node_modules/insert-module-globals/node_modules/process/browser.js":1}],5:[function(require,module,exports){ 462 | if (!Promise.prototype.done) { 463 | Promise.prototype.done = function (cb, eb) { 464 | this.then(cb, eb).then(null, function (err) { 465 | setTimeout(function () { 466 | throw err; 467 | }, 0); 468 | }); 469 | }; 470 | } 471 | },{}],6:[function(require,module,exports){ 472 | if (typeof Promise === 'undefined') { 473 | Promise = require('promise'); 474 | } else { 475 | require('./polyfill-done.js'); 476 | } 477 | },{"./polyfill-done.js":5,"promise":3}]},{},[6]) --------------------------------------------------------------------------------