Merge pull request #364 from DanTheMan827/hifi-token

fix(HiFi): enhance token fetching
This commit is contained in:
edidealt 2026-03-20 22:35:29 +02:00 committed by GitHub
commit 98b65509ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,6 +1,6 @@
const API_VERSION = '2.6'; const API_VERSION = '2.6';
const CLIENT_ID = 'txNoH4kkV41MfH25'; const BROWSER_CLIENT_ID = 'txNoH4kkV41MfH25';
const CLIENT_SECRET = 'dQjy0MinCEvxi1O4UmxvxWnDjt4cgHBPw8ll6nYBk98='; const BROWSER_CLIENT_SECRET = 'dQjy0MinCEvxi1O4UmxvxWnDjt4cgHBPw8ll6nYBk98=';
type Params = Record<string, string | number | undefined | null>; type Params = Record<string, string | number | undefined | null>;
@ -18,14 +18,71 @@ export class TidalResponse extends Response {
} }
} }
/** A container for the mock localStorage */
export class HiFiClient { export class HiFiClient {
private static token: string | null;
private static appTokenExpiry = 0;
private static tokenPromise: Promise<string> | null = null; private static tokenPromise: Promise<string> | null = null;
private countryCode: string;
private static albumTracksMax = 20; private static albumTracksMax = 20;
private static albumTracksActive = 0; private static albumTracksActive = 0;
private static albumTracksQueue: Array<() => void> = []; private static albumTracksQueue: Array<() => void> = [];
private countryCode: string;
private clientId: string;
private clientSecret: string;
private static _localStorage: Record<string, string> = {};
private static get localStorage() {
return (
globalThis?.localStorage ??
window?.localStorage ?? {
getItem: (key) => HiFiClient._localStorage[key],
setItem: (key, value) => {
HiFiClient._localStorage[key] = String(value);
},
removeItem: (key) => {
delete HiFiClient._localStorage[key];
},
get length() {
return Object.keys(HiFiClient._localStorage).length;
},
clear() {
for (const key in HiFiClient._localStorage) {
delete HiFiClient._localStorage[key];
}
},
key(index) {
const keys = Object.keys(HiFiClient._localStorage);
return keys[index] || null;
},
}
);
}
private static get token(): string | null {
return HiFiClient.localStorage.getItem('hifi_token') || null;
}
private static set token(value: string | null) {
if (value) {
HiFiClient.localStorage.setItem('hifi_token', value);
} else {
HiFiClient.localStorage.removeItem('hifi_token');
}
}
private static get appTokenExpiry() {
return Number(HiFiClient.localStorage.getItem('hifi_token_expiry') || '0');
}
private static set appTokenExpiry(value: number) {
if (value) {
HiFiClient.localStorage.setItem('hifi_token_expiry', value.toString());
} else {
HiFiClient.localStorage.removeItem('hifi_token_expiry');
}
if (value < Date.now()) {
HiFiClient.localStorage.removeItem('hifi_token');
}
}
private static buildUrl(base: string, params?: Params) { private static buildUrl(base: string, params?: Params) {
if (!params) return base; if (!params) return base;
@ -44,12 +101,12 @@ export class HiFiClient {
return Buffer.from(`${id}:${secret}`).toString('base64'); return Buffer.from(`${id}:${secret}`).toString('base64');
} }
private static async fetchAppToken(signal: AbortSignal = new AbortController().signal) { private static async fetchAppToken(
const now = Date.now(); signal: AbortSignal = new AbortController().signal,
HiFiClient.token ??= localStorage.getItem('hifi_token') || null; clientId: string,
HiFiClient.appTokenExpiry = Number(localStorage.getItem('hifi_token_expiry') || '0'); clientSecret: string
) {
if (HiFiClient.token && now < HiFiClient.appTokenExpiry) return HiFiClient.token; if (HiFiClient.token && Date.now() < HiFiClient.appTokenExpiry) return HiFiClient.token;
return await (HiFiClient.tokenPromise ??= (async () => { return await (HiFiClient.tokenPromise ??= (async () => {
try { try {
@ -57,12 +114,12 @@ export class HiFiClient {
method: 'POST', method: 'POST',
headers: { headers: {
'content-type': 'application/x-www-form-urlencoded', 'content-type': 'application/x-www-form-urlencoded',
authorization: `Basic ${this.encodeBasic(CLIENT_ID, CLIENT_SECRET)}`, authorization: `Basic ${this.encodeBasic(clientId, clientSecret)}`,
}, },
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: 'client_credentials', grant_type: 'client_credentials',
client_id: CLIENT_ID, client_id: clientId,
client_secret: CLIENT_SECRET, client_secret: clientSecret,
}), }),
signal, signal,
}); });
@ -77,8 +134,6 @@ export class HiFiClient {
const expires_in = json.expires_in ?? 3600; const expires_in = json.expires_in ?? 3600;
HiFiClient.token = token; HiFiClient.token = token;
HiFiClient.appTokenExpiry = Date.now() + expires_in * 1000 - 60_000; HiFiClient.appTokenExpiry = Date.now() + expires_in * 1000 - 60_000;
localStorage.setItem('hifi_token', token);
localStorage.setItem('hifi_token_expiry', HiFiClient.appTokenExpiry.toString());
return token; return token;
} finally { } finally {
@ -87,14 +142,18 @@ export class HiFiClient {
})()); })());
} }
constructor(countryCode = 'US') { constructor(clientId = BROWSER_CLIENT_ID, clientSecret = BROWSER_CLIENT_SECRET, countryCode = 'US') {
this.countryCode = countryCode; this.countryCode = countryCode;
this.clientId = clientId;
this.clientSecret = clientSecret;
} }
private async fetchJson(url: string, params?: Params, signal: AbortSignal = new AbortController().signal) { private async fetchJson(url: string, params?: Params, signal: AbortSignal = new AbortController().signal) {
const final = HiFiClient.buildUrl(url, params); const final = HiFiClient.buildUrl(url, params);
const res = await fetch(final, { const res = await fetch(final, {
headers: { authorization: `Bearer ${await HiFiClient.fetchAppToken(signal)}` }, headers: {
authorization: `Bearer ${await HiFiClient.fetchAppToken(signal, this.clientId, this.clientSecret)}`,
},
signal, signal,
}); });