I know that this is really late, but for anyone having the same issue...
I am working on something similar was mentioned above so I'll share what I know. I am creating a music player that could act as another device on my Spotify (using: https://developer.spotify.com/documentation/web-playback-sdk/) account as well be controlled by my custom webpage.
I have 3 parts to this: backend server, the SDK player webpage (for me: http://localhost:8080/#/pup/player), the frontend UI webpage
(all the code snippets are a part of a class)
The only way I was able to get it running was like so:
- Start the backend server and initialize puppeteer
async initPup(){
this.browser = await puppeteer.launch({
headless: false, // This is important, because spotify SDK doesn't create the device when using headless
devtools: true,
executablePath: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", //I also have to use Chrome and not Chromium, because Chromium is missing support for EME keySystems (yes, I've tried bruteforcing chromium versions or getting Firefox to work using createBrowserFetcher())
ignoreDefaultArgs: ['--mute-audio'],
args: ['--autoplay-policy=no-user-gesture-required']
});
this.page = (await this.browser.pages())[0]; // create one page
if(this.page == undefined){
this.page = await this.browser.newPage();
}
this.pup_ready = true;
console.log(await this.page.browser().version())
}
- Open your SDK player page with puppeteer and pass the ClientID and ClientSecret of your Spotify project (https://developer.spotify.com/dashboard/):
async openPlayer(){
// const player_page = "http://localhost:8080/#/pup/player"
if(this.pup_ready){
await this.page.goto(player_page + "/?&cid=" + this.client_id + "&csec=" + this.client_secret);
}
}
- On the SDK player webpage save the cid and csec URL params to LocalStorage. This should be done when no ULR parameter named "code" has been given, because that's the authorizations code which will be generated in the next step.
Something like:
var auth_code = url_params_array.find(x=>x.param.includes("code")); // try to get the auth code
var c_id = url_params_array.find(x=>x.param.includes("cid")); //get cid
var c_sec = url_params_array.find(x=>x.param.includes("csec")); //get csec
var token = undefined;
if(auth_code == undefined){ // the auth code is not defined yet and it has to be created
//SAVING CLIENT ID and CLIENT SECRET
c_id = c_id.value;
c_sec = c_sec.value;
window.localStorage.setItem("__cid", c_id)
window.localStorage.setItem("__csec", c_sec)
//GETTING THE AUTH CODE
var scope = "streaming \
user-read-email \
user-read-private"
var state = "";
var auth_query_parameters = new URLSearchParams({
response_type: "code",
client_id: c_id,
scope: scope,
redirect_uri: "http://localhost:8080/#/pup/player/",
state: state
})
window.open('https://accounts.spotify.com/authorize/?' + auth_query_parameters.toString()); // tak the puppeteer to the spotify login page
}
- Login on the spotify page using your credential to create the auth token. I had to use https://www.npmjs.com/package/puppeteer-extra-plugin-stealth to bypass CAPTCHAS
async spotifyLogin(mail="<YOUR_SPOTIFY_MAIL>", pass = "<YOUR_SPOTIFY_PASSWORD") {
var p = this.page = (await this.browser.pages())[1] // get the newly opened page with the spotify
//await p.waitForNavigation({waitUntil: 'networkidle2'})
await p.focus("#login-username"); // put in the credentials
await p.keyboard.type(mail);
await p.focus("#login-password");
await p.keyboard.type(pass);
await p.$eval("#login-button", el => el.click());
(await this.browser.pages())[0].close(); // close the old SDK page
await sleep(1000) // wait to be redirected back to your SDK page
//
this.page = (await this.browser.pages())[0];
this.auth_code = await this.page.evaluate( (varName) => window.localStorage.getItem(varName), ["__auth"] ) // here is ave the auth token as a property of the class instance as well
}
- Once you're redirected to SDK page again you already have cid and csec and now also the auth token.
if(auth_code == undefined)
//... (this is already in step 3)
}else{
// GETTING CID and C SECRET AGAIN
c_id = window.localStorage.getItem("__cid")
c_sec = window.localStorage.getItem("__csec")
// SAVING THE AUTH CODE
auth_code = auth_code.value;
window.localStorage.setItem("__auth", auth_code)
}
- Generate a token on the backend.
async genToken():Promise<void>{
//Pretty much coppied from: https://developer.spotify.com/documentation/web-playback-sdk/guide/
var authOptions = {
url: 'https://accounts.spotify.com/api/token',
headers: {
'Authorization': 'Basic ' + (Buffer.from(this.client_id + ':' + this.client_secret).toString("base64"))
},
form: {
code: this.auth_code,
redirect_uri: "http://localhost:8080/#/pup/player/",
grant_type: 'authorization_code'
},
json: true
};
var token;
var refresh_token;
await request.post(authOptions, function(error, response, body) { // also get the refresh token
if (!error && response.statusCode === 200) {
token = body.access_token;
refresh_token = body.refresh_token;
}
});
while (!token && !refresh_token){ // wait for both of them
await sleep(100)
}
this.token = token; // save them in the class instance properties
this.refresh_token = refresh_token;
}
- Lastly the puppeteer fills in a html field with the token generated in step 6 on the SDK site and presses a button to start the SDK player.
// this function gets called after the button gets pressed
async function main(){
console.log(window.localStorage.getItem("__cid")) // print out all the data
console.log(window.localStorage.getItem("__csec"))
console.log(window.localStorage.getItem("__auth"))
console.log(getToken())
const player = new Spotify.Player({ // start the sporify player
name: 'Home Spotify Player',
getOAuthToken: cb => cb(getToken())
});
player.connect().then(()=>{ // connect the player
console.log(player)
});
window.player = player;
}
function getToken(){
return document.getElementById("token_input").value;
}
- You are done. Next step for me at least was communicating using another UI page to the backend puppeteer to control the SDK page (play/pause/skip etc.) This process is pretty "hacky" and not pretty at all but if you just have a little personal project it should do the job fine.
If anyone would be interested in the whole code I might even upload it somewhere, but I think this read is long-enough and overly detailed anyway.