Hacky, but working spotify skipping.

This commit is contained in:
Ian Wijma 2023-11-05 00:08:54 +11:00
parent fecd3db3bc
commit 64f1d76778
6 changed files with 221 additions and 38 deletions

116
package-lock.json generated
View File

@ -13,6 +13,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"nanoid": "^3.3.6", "nanoid": "^3.3.6",
"pkg": "^5.8.1", "pkg": "^5.8.1",
"spotify-web-api-node": "^5.0.2",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"bin": { "bin": {
@ -434,6 +435,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/at-least-node": { "node_modules/at-least-node": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@ -648,6 +654,22 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -699,6 +721,11 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
}, },
"node_modules/cookiejar": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
},
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@ -755,6 +782,14 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -955,6 +990,11 @@
"node": ">=8.6.0" "node": ">=8.6.0"
} }
}, },
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
@ -1018,6 +1058,28 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formidable": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",
"integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==",
"deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau",
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -2143,6 +2205,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/spotify-web-api-node": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/spotify-web-api-node/-/spotify-web-api-node-5.0.2.tgz",
"integrity": "sha512-r82dRWU9PMimHvHEzL0DwEJrzFk+SMCVfq249SLt3I7EFez7R+jeoKQd+M1//QcnjqlXPs2am4DFsGk8/GCsrA==",
"dependencies": {
"superagent": "^6.1.0"
}
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -2199,6 +2269,52 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/superagent": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz",
"integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==",
"deprecated": "Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at <https://github.com/visionmedia/superagent/releases>.",
"dependencies": {
"component-emitter": "^1.3.0",
"cookiejar": "^2.1.2",
"debug": "^4.1.1",
"fast-safe-stringify": "^2.0.7",
"form-data": "^3.0.0",
"formidable": "^1.2.2",
"methods": "^1.1.2",
"mime": "^2.4.6",
"qs": "^6.9.4",
"readable-stream": "^3.6.0",
"semver": "^7.3.2"
},
"engines": {
"node": ">= 7.0.0"
}
},
"node_modules/superagent/node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/superagent/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",

View File

@ -23,6 +23,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"nanoid": "^3.3.6", "nanoid": "^3.3.6",
"pkg": "^5.8.1", "pkg": "^5.8.1",
"spotify-web-api-node": "^5.0.2",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -3,31 +3,36 @@ const {input} = require("@inquirer/prompts");
const querystring = require("querystring"); const querystring = require("querystring");
const {nanoid} = require("nanoid"); const {nanoid} = require("nanoid");
const express = require('express') const express = require('express')
const {setAuth, hasAuth, authFile} = require("../utilities/authentication"); const {spotify, spotifyCredentialsFile, hasSpotifyCredentials, saveSpotifyCredentials} = require("../utilities/spotify");
exports.command = 'authenticate' exports.command = 'authenticate'
exports.description = 'starts the Spotity authentication process' exports.description = 'starts the Spotity authentication process'
exports.builder = function (argv) { exports.builder = (argv) => argv
return argv.option('client-id') .option('client-id', {
} description: 'The Spotify app client id',
; type: 'string'
})
.option('client-secret', {
description: 'The Spotify app client secret',
type: 'string'
})
exports.handler = async function (argv) { exports.handler = async function (argv) {
if (hasAuth()) { if (hasSpotifyCredentials()) {
console.log(`spotify.local is already authenticated, remove "${authFile}" is you want to log out.`); console.log(`spotify.local is already authenticated, remove "${spotifyCredentialsFile}" is you want to log out.`);
return; return;
} }
const port = await findAvailablePort(); const port = await findAvailablePort();
const authenticateRoute = 'authenticate'; const authenticateRoute = 'authenticate';
const authenticateUrl = `http://localhost:${port}/${authenticateRoute}`; const redirectUri = `http://localhost:${port}/${authenticateRoute}`;
const tutorial = [ const tutorial = [
'Visit https://developer.spotify.com/dashboard and click "Create app"', 'Visit https://developer.spotify.com/dashboard and click "Create app"',
'Set the "App name" as "spotify.local"', 'Set the "App name" as "spotify.local"',
'Set the "App description" as "My spotify.local"', 'Set the "App description" as "My spotify.local"',
'Set the "Website" as "https://code.tmp.dev/ian/spotify.local"', 'Set the "Website" as "https://code.tmp.dev/ian/spotify.local"',
`Set the "Redirect URI" as "${authenticateUrl}"`, `Set the "Redirect URI" as "${redirectUri}"`,
'Select under "Which API/SDKs are you planning to use?" the "Web API"', 'Select under "Which API/SDKs are you planning to use?" the "Web API"',
'Sign your life away by agreeing with the "Developer Terms of Service and Design Guidelines"', 'Sign your life away by agreeing with the "Developer Terms of Service and Design Guidelines"',
'Click on the "Save" button', 'Click on the "Save" button',
@ -36,12 +41,18 @@ exports.handler = async function (argv) {
tutorial.forEach((string, index) => console.log(`${index+1} ${string}`)); tutorial.forEach((string, index) => console.log(`${index+1} ${string}`));
const clientId = argv.clientId ?? await input({ message: `${tutorial.length}. Paste the clientId here` }) const clientId = argv.clientId ?? await input({ message: `${tutorial.length}. Paste the client id here` })
const clientSecret = argv.clientSecret ?? await input({ message: `${tutorial.length}. Paste the client secret here` })
const originalState = nanoid(); const originalState = nanoid();
const redirectUrl = new URL('https://accounts.spotify.com/authorize');
redirectUrl.search = querystring.stringify({ spotify.setClientId(clientId);
spotify.setClientSecret(clientSecret);
spotify.setRedirectURI(redirectUri);
const authUrl = new URL('https://accounts.spotify.com/authorize');
authUrl.search = querystring.stringify({
response_type: 'code', response_type: 'code',
client_id: clientId, client_id: spotify.getClientId(),
scope: [ scope: [
'user-library-read', 'user-library-read',
'user-read-private', 'user-read-private',
@ -59,28 +70,35 @@ exports.handler = async function (argv) {
'user-read-currently-playing', 'user-read-currently-playing',
'user-read-playback-position' 'user-read-playback-position'
].join(' '), ].join(' '),
redirect_uri: authenticateUrl, redirect_uri: spotify.getRedirectURI(),
state: originalState state: originalState
}); });
const app = express(); const app = express();
let server; let server;
app.get(`/${authenticateRoute}`, (req, res) => { app.get(`/${authenticateRoute}`, async (req, res) => {
const {state, code} = req.query; const {state, code, error = null} = req.query;
if (originalState === state) { if (!error && originalState === state) {
setAuth(code); const { body } = await spotify.authorizationCodeGrant(code)
const {access_token, refresh_token, expires_in} = body;
spotify.setExpired((new Date).getTime() + expires_in);
spotify.setAccessToken(access_token);
spotify.setRefreshToken(refresh_token);
saveSpotifyCredentials();
res.send('<script>(() => close())()</script>'); res.send('<script>(() => close())()</script>');
server?.close(); server?.close();
console.log(`${tutorial.length+2}. Spotify.local has been authenticated`); console.log(`${tutorial.length+2}. Spotify.local has been authenticated`);
} else {
res.status(400); return;
res.send('NO')
} }
res.status(400);
res.send('NO');
}); });
server = app.listen(port, () => console.log(`${tutorial.length+1}. Follow the following URL to authenticate: ${redirectUrl}`)); server = app.listen(port, () => console.log(`${tutorial.length+1}. Follow the following URL to authenticate: ${authUrl}`));
} }

8
src/commands/next.js Normal file
View File

@ -0,0 +1,8 @@
const { spotify} = require('../utilities/spotify')
exports.command = 'next'
exports.handler = async () => {
spotify.ensure()
.then(spotify => spotify.skipToNext())
.catch(e => console.error(e.message))
};

View File

@ -1,16 +0,0 @@
const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('fs');
const { homedir } = require('os');
const configDir = `${homedir()}/.spotify-local`
const authenticationFile = `${configDir}/authentication`
const ensure = () => {
if (!existsSync(configDir)) mkdirSync(configDir);
if (!existsSync(authenticationFile)) writeFileSync(authenticationFile, '');
return true;
};
exports.authFile = authenticationFile;
exports.setAuth = (code) => ensure() && writeFileSync(authenticationFile, code);
exports.getAuth = () => ensure() && readFileSync(authenticationFile).toString();
exports.hasAuth = () => ensure() && readFileSync(authenticationFile).toString() !== '';

56
src/utilities/spotify.js Normal file
View File

@ -0,0 +1,56 @@
const SpotifyWebApi = require('spotify-web-api-node');
const {homedir} = require("os");
const {existsSync, mkdirSync, writeFileSync, readFileSync} = require("fs");
const configDir = `${homedir()}/.spotify-local`
const spotifyCredentialsFile = `${configDir}/credentials.json`
const ensure = () => {
if (!existsSync(configDir)) mkdirSync(configDir);
if (!existsSync(spotifyCredentialsFile)) writeFileSync(spotifyCredentialsFile, '{}');
return true;
};
SpotifyWebApi.prototype.setExpired = function (expiresIn) {
const expired = (new Date).getTime() + expiresIn;
this._setCredential('expired', expired);
}
SpotifyWebApi.prototype.getExpired = function () {
return this._getCredential('expired') ?? 0;
}
/**
* @returns {Promise<SpotifyWebApi>}
*/
SpotifyWebApi.prototype.ensure = async function () {
const timeExpired = this.getExpired();
const time = (new Date).getTime();
if (time > timeExpired) {
const { body } = await this.refreshAccessToken();
const { access_token, expires_in } = body;
this.setAccessToken(access_token);
this.setExpired((new Date).getTime() + expires_in);
saveSpotifyCredentials();
}
return this;
}
const spotify = new SpotifyWebApi();
const saveSpotifyCredentials = () => ensure() && writeFileSync(spotifyCredentialsFile, JSON.stringify(spotify.getCredentials()));
const restoreSpotifyCredentials = () => ensure() && spotify.setCredentials(JSON.parse(readFileSync(spotifyCredentialsFile).toString()));
const hasSpotifyCredentials = () => existsSync(spotifyCredentialsFile);
if (existsSync(spotifyCredentialsFile)) restoreSpotifyCredentials();
module.exports = {
spotify,
spotifyCredentialsFile,
saveSpotifyCredentials,
restoreSpotifyCredentials,
hasSpotifyCredentials,
}