QueueUp is a collaborative playlist streaming service. Anybody can create a playlist, use Spotify to stream from QueueUp, and invite friends to contribute in real time.
A QueueUp Player is requried to stream from QueueUp. This repository is for the QueueUp Server. Read below about the Android and iOS Players.
The /spotify.key
configuration file is required to run the server properly. An example configuration file is located in /spotify.key.example
. Most of the requried parameters can be obtained by creating Spotify Developer account, and then a Spotify Application. The encryptionSecret
is your password to encrypt refresh tokens on the server side.
The /env.json
configuration file is required (env.json.example is provided)with the fields:
- name (environment)
- host (current hostname, like queueup.louiswilliams.org)
- port (server listen port)
- database (name of MongoDB database to use)
A MongoDB Server should be running on localhost:27017
. This is configurable in server.js
.
npm install
npm start
A QueueUp Player is required to play from QueueUp. It connects to the server, subscribes to a playlist, and updates automatically to play music from a playlist.
Available Players:
- Android Player: Get it on the Google Play Store
- iOS: Get it on the iOS App store
- Node.js Player: Requires some setup, but effectively the same as the Android player, just on a desktop platform.
Notes:
- All players require Spotify Premium accounts. This is a result of Spotify's streaming licensing, and there is no legal way around it. Consider buying one. As a student ($5/mo), it is one of the best decisions I've made in my adult life.
- No web streaming API exists, again, because of music licensing issues with Spotify. Currently, the streaming APIs are limited to Android, iOS, and C (personal use developer accounts only).
A Player can be implemented using a mixture of REST and Socket.IO APIs.
In terms of the API, a Client is a read-only listener that subscribes to playlist updates. A Player is a Client that can also send updates about the current state of the playing track. Only one Player is allowed to play at a time for a given playlist.
For requests that do not require event-based socketed connections, like searching for and updating playlist information. See Objects section for schema.
Note: All responses send 200 codes on success, 400 on client errors, 403 on unauthorized access, and 500 on server errors. 4xx errors contain an error
attribute, with an error description, error.message
Authenticated routes require the HMAC scheme described below. An attempt to access an authenticated route without the Authorization
header or if the user isn't found return 403 errors, all other bad requests return 400 (like an invalid hash).
Beacause of the the desire to have anonymous users, call this route to obtain a user_id
and client_token
, which must be used in step 1.
- POST
/api/v2/auth/init
: Register an anonymous account- Input:
{device: {id: String}}
: Register with a unique device identifier - Returns:
{user_id: String, client_token: String}
: Save these for later requests
- Input:
Note: a request to both of these routes REASSIGNS a client_token
and invalidates the current one, if it exists.
- POST
/api/v2/auth/register
: Register an account for the first time (without Facebook)- Input:
{user_id: String, client_token: String, email: String, password: String, name: String}
: Register with an name/email/password - Returns:
{user_id: String, client_token: String}
: Save these for API requests
- Input:
- POST
/api/v2/auth/login
: Log in to receive aclient_token
for API requests- Input: Choose ONE:
{email: String, password: String}
: Log in with an existing email/password{user_id: String, client_token: String, facebook_access_token: String}
: Log in with a valid FB access token.- Note: a user_id/client_token are only required for the very first login attempt
- Returns:
{user_id: String, client_token: String}
: Save these for API requests
- Input: Choose ONE:
To authenticate, the server uses an HMAC-SHA1 scheme. There are 2 requried HTTP headers:
Date
: RFC2822 or ISO 8601 formatted dateAuthorization
: Basic HTTP authentication using the base64 encoded string in the formuser_id:HMAC_HASH
Where user_id
is received from logging in. The HMAC_HASH
is the output of using client_token
as the key of the HMAC algorithm with the following as input:
HTTP_METHOD+HOSTNAME+URI+UNIX_SECONDS
Assume the following request by the user_id cafebabecafebabe
, and client_token secret
sent Saturday, 11-Jul-15 21:00:03 UTC (RFC2822 time):
POST http://queueup.louiswilliams.org/api/v2/playlists/c0ffeec0ffee/rename
This message is hashed:
POST+queueup.louiswilliams.org+/api/v2/playlists/c0ffeec0ffee/rename+1436648403
which yields 2871715b0c9fbf688de5104f83d6c800f30cbe34
. The string
cafebabecafebabe:2871715b0c9fbf688de5104f83d6c800f30cbe34
is Base64 encoded to yield
Y2FmZWJhYmVjYWZlYmFiZToyODcxNzE1YjBjOWZiZjY4OGRlNTEwNGY4M2Q2YzgwMGYzMGNiZTM0
The appropriate headers are then:
Date: Saturday, 11-Jul-15 21:00:03 UTC
Authorization: Basic Y2FmZWJhYmVjYWZlYmFiZToyODcxNzE1YjBjOWZiZjY4OGRlNTEwNGY4M2Q2YzgwMGYzMGNiZTM0
Note: Dates must be withing 5 minutes of server time to prevent replay
Spotify Token Routes (to obtain access tokens)
- POST
/api/v2/spotify/swap
: Swap an authorization code for access tokens- Input:
{code: String}
: Authorization code - Returns:
{access_token: String, refresh_token: String, expires_in: Number}
: An access token and (encrypted) refresh token to be stored for later retrieval.
- Input:
- POST
/api/v2/spotify/refresh
: Exchange encrypted refresh token for an access token- Input:
{refresh_token}
: Encrypted refreh token obtained from the swap step - Returns:
{access_token: String, expires_in: Number}
: New access token
- Input:
- GET
/api/v2/search/tracks/:query/[:offset]
: Search for tracks with a page offset- Returns:
{tracks: [Track]}
: Array (max 10) of Spotify Track objects. Use the offset at multiples of 10 to get more results.
- Returns:
- GET
/api/v2/search/playlists/:query
: Search for playlists- Returns:
{playlists: [Playlist]}
: Array of top 10 matches to Playlist objects (by name)
- Returns:
- GET
/api/v2/playlists
: Get a list of playlists- Returns:
{playlists: [Playlist]}
: Array of Playlist objects (without tracks).
- Returns:
- GET
/api/v2/playlists/:playlist_id
: Get details for a playlist, by_id
.- Returns:
{playlist: Playlist}
: A Playlist object.
- Returns:
- POST
/api/v2/playlists/nearby
: Show playlists within 2.0 mi radius- Input:
{location: {latitude: Double, longitude: Double}}
: Coordinates of the user creating the playlist - Returns:
{playlist: Playlist}
: New Playlist object. This object contains thedistance
field, which is equal to the distance to the playlist in meters.
- Input:
- POST
/api/v2/playlists/new
: Create new playlist- Input:
{playlist: {name: String}}
: New playlist object (with name) - Returns:
{playlist: Playlist}
: New Playlist object.
- Input:
- POST
/api/v2/playlists/:playlist_id/rename
: Rename the current track- Input:
{name: String}
: New name of playlist - Returns:
{playlist: Playlist}
: An updated Playlist object.
- Input:
- POST
/api/v2/playlists/:playlist_id/vote
: Vote on a track- Input:
{track_id: String, vote: Boolean}
: True to vote, false to unvote - Returns:
{playlist: Playlist}
: An updated Playlist object.
- Input:
- GET
/api/v2/users/:user_id
: Get User information- Returns:
user: User
: A User object.
- Returns:
- GET
/api/v2/users/:user_id/playlists:
: Get User playlists- Returns:
playlists: [Playlist]
: Arraw of Playlist Objects (without tracks).
- Returns:
- POST
/api/v2/users/friends/playlists:
: Get a list of a user's friends' playlists- Input:
{fbids: [String]}
: An array of Facebok Ids - Returns:
playlists: [Playlist]
: Arraw of Playlist Objects (without tracks).
- Input:
For clients and players subscribing to playlist updates
- on
auth
: Initialize authentication by passing API credentials- Parameters:
client_token
: Client token from the REST API/auth/login
user_id
: UserId of client from Step 1
- Emits:
auth_response
: On result. No error is a success.error
: Sent only if there was an errormessage: String
: Description of problem
- Parameters:
- on
client_subscribe
: Subscribe to updates from a playlist- Parameters:
playlist_id: String
: Playlist ID to subscribe to
- Emits:
state_change
: On every playlist update until disconnect or unsubscribe- State object
- Parameters:
- on
client_unsubscribe
: Stop receiving state change updates- Parameters: None
- Emits:
client_unsubscribe_response
: Stops receivingstate_change
error
: Sent only if there was an errormessage: String
: Description of problem
This registers as a client inherently
- on
player_subscribe
: Subscribe to updates to play from a playlist- Parameters:
playlist_id: String
: Playlist ID to play from
- Emits:
state_change
: (every playlist update until disconnect or unsubscribe)- State object
- Emits:
player_subscribe_response
: Result of subscriptionerror
: Sent only if there was an errormessage: String
: Description of problem
- Parameters:
- on
player_unsubscribe
: Stop acting as a player.- Parameters: None
- Emits:
player_unsubscribe_response
: Stops receivingstate_change
error
: Sent only if there was an errormessage: String
: Description of problem
As a Player, the following events are now available (and should be implemented):
- on
track_finished
: The local track finished.- Parameters: None
- Broadcasts:
state_change: State
: State object with new track.
- on
track_progress
: An ratio to update to the track's progression- Parameters: Send this no less frequently than once per second
progress: Number
: Track progress (ms)duration: Number
: Track duration (ms)
- Parameters: Send this no less frequently than once per second
- on
track_play_pause
:{playing: true/false}
: The track was paused- Parameters:
play: Boolean
: Play state to update the server with (true = playing)
- Broadcasts:
state_change: State
: State object
- Parameters:
-
Playlist: Playlist object that represents the entire playlist. Only used in the REST API.
_id
String: Internal ID. Used for Player authentication.name
String Name of the playlistcurrent
Track: Currently playing Trackplay
Boolean:true
if playing,false
otherwisevolume
Number [0-100]: Volume percentagetracks
[QueueItem]: Ordered items in the queue.admin
Interal ID associate with the Adminisator user (creator)admin_name
Display name of admindate_created
Number: Date created (UNIX)last_updated
Number: Date last updated (UNIX)key
String: Non-unique short name for the playlistdistance
Double: If a location was sent to/playlists/nearby
, this field exists and is the distance in meters to the playlist
-
User: User object that stores basic information
_id
: String: Internal ID.name
: String: Full namefacebook
(If user is connected with Facebook)id
: String: Fabook profile ID
spotify
(If user is connected with Spotify)id
: String: Spotify profile ID
-
State: The following fields are always sent:
play
Boolean:true
if playing,false
otherwisetrack
Track: Currently playing track.queue
[QueueItem]: Ordered Array of QueueItems.trigger
String: Mostly for debugging. Identifies what action caused this broadcast.
-
Track: Simplified version of Spotify's Track (full).
votes
Number: Number of votes on the trackvoters
[User]: Array of User objects, with _id and nameparametersname
String: Track nameid
String: Spotify IDuri
String: Spotify URIartists
[Object]: Array of Spotify's Artist objectsid
String: Spotify IDname
String: Artist nametype
String: Artist typeuri
String: Spotify URIhref
String: Spotify URLsexternal_urls
[String]: Extra URLs
album
Object: Spotify's Album objectid
String: Spotify IDname
String: Album nameuri
String: Spotify URIimages
[Object]: Array of Imagesheight
: Number: Image heightwidth
: Number: Image widthurl
: String: Image URL
-
QueueItem: Item in the queue
_id
: String: Internal IDtrack
: [Track]: Array of Track objects
Because I only get to spend so much of my time on this, I am opening this project up to contribution.
If there is a feature you want to see, you have a problem with the app, or you have a problem with me, send me message or open an Issue, thanks!