├── .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 | [](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])
--------------------------------------------------------------------------------