feat(Apple Music): Automated Authentication (#623)

* apple_music_token

* apple_music_token
This commit is contained in:
KHADIJAH WURAOLA AMUSAT 2024-01-02 23:06:11 +04:00 committed by GitHub
parent 3a20b68a4b
commit 92228262ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 52 additions and 18 deletions

7
cli.js
View File

@ -1537,7 +1537,12 @@ async function init(packageJson, queries, options) {
const authQueue = new AsyncQueue('cli:authQueue', 1, async (service, logger) => { const authQueue = new AsyncQueue('cli:authQueue', 1, async (service, logger) => {
async function coreAuth(loginLogger) { async function coreAuth(loginLogger) {
if (!Config.opts.attemptAuth) return; if (!Config.opts.attemptAuth) return;
const authHandler = service.newAuth(); let authHandler;
try {
authHandler = service.newAuth();
} catch {
return;
}
const url = await authHandler.getUrl; const url = await authHandler.getUrl;
if (Config.opts.autoOpenBrowser) if (Config.opts.autoOpenBrowser)
await processPromise(open(url), loginLogger, { await processPromise(open(url), loginLogger, {

View File

@ -53,8 +53,7 @@
"refreshToken": "AQCbeNs5NiGfHa6He0BlOdoQwEIdo2lwBmefEvpvqVy8WlL2HV7rmGbb30_oaZHMDf9MgXtGjI0gV_QukL33PE8c2bGqJHeMqdHBUjKWhxOW19snJkGUiyxen8UvmDG-OP4" "refreshToken": "AQCbeNs5NiGfHa6He0BlOdoQwEIdo2lwBmefEvpvqVy8WlL2HV7rmGbb30_oaZHMDf9MgXtGjI0gV_QukL33PE8c2bGqJHeMqdHBUjKWhxOW19snJkGUiyxen8UvmDG-OP4"
}, },
"apple_music": { "apple_music": {
"storefront": "us", "storefront": "us"
"developerToken": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IldlYlBsYXlLaWQifQ.eyJpc3MiOiJBTVBXZWJQbGF5IiwiaWF0IjoxNzAyNTAyMjM0LCJleHAiOjE3MDk3NTk4MzQsInJvb3RfaHR0cHNfb3JpZ2luIjpbImFwcGxlLmNvbSJdfQ.zzeMLmez71PLinP9GozYSQnF7NYyCiXHB9tKL3-cyu3LzyeRnYz0ejLj4CrNJs0dlNkFg9_mwKmMLueUAR-KRg"
}, },
"deezer": { "deezer": {
"retries": 2 "retries": 2

View File

@ -2,6 +2,7 @@
import xurl from 'url'; import xurl from 'url';
import path from 'path'; import path from 'path';
import got from 'got';
import Promise from 'bluebird'; import Promise from 'bluebird';
import NodeCache from 'node-cache'; import NodeCache from 'node-cache';
import {Client} from '@yujinakayama/apple-music'; import {Client} from '@yujinakayama/apple-music';
@ -22,7 +23,9 @@ export default class AppleMusic {
// https://www.debuggex.com/r/Pv_Prjinkz1m2FOB // https://www.debuggex.com/r/Pv_Prjinkz1m2FOB
VALID_URL: VALID_URL:
/(?:(?:(?:(?:https?:\/\/)?(?:www\.)?)(?:(?:music|(?:geo\.itunes))\.apple.com)\/([a-z]{2})\/(song|album|artist|playlist)\/(?:([^/]+)\/)?\w+)|(?:apple_music:(track|album|artist|playlist):([\w.]+)))/, /(?:(?:(?:(?:https?:\/\/)?(?:www\.)?)(?:(?:music|(?:geo\.itunes))\.apple.com)\/([a-z]{2})\/(song|album|artist|playlist)\/(?:([^/]+)\/)?\w+)|(?:apple_music:(track|album|artist|playlist):([\w.]+)))/,
PROP_SCHEMA: {}, PROP_SCHEMA: {
developerToken: {type: 'string'},
},
}; };
[symbols.meta] = AppleMusic[symbols.meta]; [symbols.meta] = AppleMusic[symbols.meta];
@ -30,6 +33,8 @@ export default class AppleMusic {
#store = { #store = {
cache: new NodeCache(), cache: new NodeCache(),
core: null, core: null,
axiosInstance: null,
expiry: null,
defaultStorefront: null, defaultStorefront: null,
isAuthenticated: false, isAuthenticated: false,
}; };
@ -37,23 +42,35 @@ export default class AppleMusic {
constructor(config) { constructor(config) {
if (!config) throw new Error(`[AppleMusic] Please define a configuration object`); if (!config) throw new Error(`[AppleMusic] Please define a configuration object`);
if (typeof config !== 'object') throw new Error(`[AppleMusic] Please define a configuration as an object`); if (typeof config !== 'object') throw new Error(`[AppleMusic] Please define a configuration as an object`);
if (!config.developerToken) this.#store.core = new Client({});
throw new Error(`[AppleMusic] Please define [developerToken] as a property within the configuration`); this.#store.axiosInstance = this.#store.core.songs.axiosInstance;
this.#store.core = new Client({developerToken: config.developerToken}); for (let instance of [this.#store.core.albums, this.#store.core.artists, this.#store.core.playlists])
for (let instance of [this.#store.core.albums, this.#store.core.artists, this.#store.core.playlists, this.#store.core.songs]) instance.axiosInstance = this.#store.axiosInstance;
instance.axiosInstance.interceptors.request.use(conf => ((conf.headers.origin = 'https://music.apple.com'), conf)); this.#store.axiosInstance.defaults.headers['Origin'] = 'https://music.apple.com';
this.#store.defaultStorefront = config.storefront; this.#store.defaultStorefront = config.storefront;
this.#store.isAuthenticated = !!config.developerToken;
} }
loadConfig(_config) {} expiresAt(developerToken) {
let segments = developerToken.split('.');
let payload = Buffer.from(segments[1] || '', 'base64');
let parsed = JSON.parse(payload.toString());
return parsed.exp * 1000;
}
loadConfig(config) {
if (config.developerToken) {
this.#store.expiry = this.expiresAt(config.developerToken);
this.#store.core.configuration.developerToken = config.developerToken;
this.#store.axiosInstance.defaults.headers['Authorization'] = `Bearer ${config.developerToken}`;
}
}
hasOnceAuthed() { hasOnceAuthed() {
throw Error('Unimplemented: [AppleMusic:hasOnceAuthed()]'); return this.#store.isAuthenticated;
} }
isAuthed() { isAuthed() {
return this.#store.isAuthenticated; return Date.now() < this.#store.expiry;
} }
newAuth() { newAuth() {
@ -61,19 +78,32 @@ export default class AppleMusic {
} }
canTryLogin() { canTryLogin() {
return !!this.#store.core.configuration.developerToken; return true;
} }
hasProps() { hasProps() {
return false; return this.#store.isAuthenticated;
} }
getProps() { getProps() {
throw Error('Unimplemented: [AppleMusic:getProps()]'); return {
developerToken: this.#store.core.configuration.developerToken,
};
} }
async login() { async login() {
throw Error('Unimplemented: [AppleMusic:login()]'); let browsePage = await got('https://music.apple.com/us/browse').text();
let scriptUri;
if (!(scriptUri = browsePage.match(/assets\/index-[a-z0-9]{8}\.js/)?.[0]))
throw new Error('Unable to extract core script from Apple Music');
let script = await got(`https://music.apple.com/${scriptUri}`).text();
let developerToken;
if (!(developerToken = script.match(/eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IldlYlBsYXlLaWQifQ[^"]+/)?.[0]))
throw new Error('Unable to extract developerToken from Apple Music core script');
this.#store.expiry = this.expiresAt(developerToken);
this.#store.core.configuration.developerToken = developerToken;
this.#store.axiosInstance.defaults.headers['Authorization'] = `Bearer ${developerToken}`;
return (this.#store.isAuthenticated = true);
} }
validateType(uri) { validateType(uri) {
@ -94,7 +124,7 @@ export default class AppleMusic {
const id = isURI ? match[5] : parsedURL.query.i || path.basename(parsedURL.pathname); const id = isURI ? match[5] : parsedURL.query.i || path.basename(parsedURL.pathname);
const type = isURI ? match[4] : collection_type == 'album' && parsedURL.query.i ? 'track' : collection_type; const type = isURI ? match[4] : collection_type == 'album' && parsedURL.query.i ? 'track' : collection_type;
const scope = collection_type == 'track' || (collection_type == 'album' && parsedURL.query.i) ? 'song' : collection_type; const scope = collection_type == 'track' || (collection_type == 'album' && parsedURL.query.i) ? 'song' : collection_type;
storefront = match[1] || storefront || (#store in this ? this.#store.defaultStorefront : 'us'); storefront = match[1] || storefront || (#store in this ? this.#store.defaultStorefront : null) || 'us';
return { return {
id, id,
type, type,