feat(auth): purple app track fetch

This commit is contained in:
2025-09-30 22:38:16 -04:00
parent 48d8b4a593
commit 4ebb77f341
9 changed files with 1094 additions and 20 deletions

View File

@@ -38,6 +38,7 @@ export class DeezerAPI {
private httpHeaders: Record<string, string>;
private arl: string | null = null;
private apiToken: string | null = null;
private cookies: Map<string, string> = new Map();
constructor() {
this.httpHeaders = {
@@ -51,6 +52,28 @@ export class DeezerAPI {
// Set ARL cookie for authentication
setArl(arl: string): void {
this.arl = arl.trim();
this.cookies.set('arl', this.arl);
}
// Parse and store cookies from Set-Cookie header
private parseCookies(setCookieHeaders: string[]): void {
for (const header of setCookieHeaders) {
const parts = header.split(';')[0].split('=');
if (parts.length === 2) {
const [name, value] = parts;
this.cookies.set(name.trim(), value.trim());
console.log(`[DEBUG] Stored cookie: ${name.trim()}`);
}
}
}
// Build cookie header from stored cookies
private getCookieHeader(): string {
const cookies: string[] = [];
this.cookies.forEach((value, name) => {
cookies.push(`${name}=${value}`);
});
return cookies.join('; ');
}
// Get API token from getUserData
@@ -60,7 +83,7 @@ export class DeezerAPI {
}
// Call Deezer GW API
private async apiCall(method: string, args: any = {}, params: any = {}): Promise<any> {
private async apiCall(method: string, args: any = {}, params: any = {}, retryCount: number = 0): Promise<any> {
if (!this.apiToken && method !== 'deezer.getUserData') {
this.apiToken = await this.getToken();
}
@@ -75,36 +98,74 @@ export class DeezerAPI {
const url = `http://www.deezer.com/ajax/gw-light.php?${searchParams.toString()}`;
const cookieHeader = this.getCookieHeader();
console.log(`[DEBUG] API Call: ${method}`, { url, args, cookie: cookieHeader });
try {
const response = await fetch(url, {
method: 'POST',
headers: {
...this.httpHeaders,
'Cookie': this.arl ? `arl=${this.arl}` : ''
'Cookie': cookieHeader
},
body: JSON.stringify(args)
body: JSON.stringify(args),
connectTimeout: 30000
});
// Parse and store cookies from response
const setCookieHeader = response.headers.get('set-cookie');
if (setCookieHeader) {
// Handle multiple Set-Cookie headers
const setCookies = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
this.parseCookies(setCookies);
}
console.log(`[DEBUG] Response status: ${response.status}`);
console.log(`[DEBUG] Response headers:`, Object.fromEntries(response.headers.entries()));
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const resultJson: GWAPIResponse = await response.json();
console.log(`[DEBUG] Response JSON for ${method}:`, resultJson);
// Handle errors
if (resultJson.error && (Array.isArray(resultJson.error) ? resultJson.error.length : Object.keys(resultJson.error).length)) {
// Handle errors - check if error exists and is not empty
const hasError = resultJson.error && (
Array.isArray(resultJson.error)
? resultJson.error.length > 0
: Object.keys(resultJson.error).length > 0
);
if (hasError) {
const errorStr = JSON.stringify(resultJson.error);
console.error(`[ERROR] API returned error for ${method}:`, errorStr);
// Handle invalid token - retry with new token
if (errorStr.includes('invalid api token') || errorStr.includes('Invalid CSRF token')) {
// Handle invalid token - retry with new token (max 2 retries)
if ((errorStr.includes('invalid api token') || errorStr.includes('Invalid CSRF token')) && retryCount < 2) {
console.log(`[DEBUG] Invalid token, fetching new token... (retry ${retryCount + 1}/2)`);
this.apiToken = null; // Clear the invalid token
this.apiToken = await this.getToken();
return this.apiCall(method, args, params);
console.log('[DEBUG] New token acquired, retrying API call...');
return this.apiCall(method, args, params, retryCount + 1);
}
throw new Error(`Deezer API Error: ${errorStr}`);
}
// Set token from getUserData response
if (!this.apiToken && method === 'deezer.getUserData') {
this.apiToken = resultJson.results.checkForm || null;
// Set token from getUserData response (always update it)
if (method === 'deezer.getUserData' && resultJson.results?.checkForm) {
console.log('[DEBUG] Updating API token from getUserData');
this.apiToken = resultJson.results.checkForm;
}
// Validate response has results
if (!resultJson.results) {
console.error(`[ERROR] No results in response for ${method}:`, resultJson);
throw new Error(`Invalid API response: missing results field`);
}
console.log(`[DEBUG] Returning results for ${method}`);
return resultJson.results;
} catch (error) {
console.error('[ERROR] deezer.gw', method, args, error);
@@ -166,6 +227,84 @@ export class DeezerAPI {
return false;
}
}
// Get track data
async getTrack(trackId: string): Promise<any> {
return this.apiCall('song.getData', { SNG_ID: trackId });
}
// Get playlist data
async getPlaylist(playlistId: string): Promise<any> {
return this.apiCall('deezer.pagePlaylist', {
PLAYLIST_ID: playlistId,
lang: 'en',
header: true,
tab: 0
});
}
// Get playlist tracks
async getPlaylistTracks(playlistId: string): Promise<any[]> {
const response = await this.apiCall('playlist.getSongs', {
PLAYLIST_ID: playlistId,
nb: -1
});
return response.data || [];
}
// Get user playlists
async getUserPlaylists(): Promise<any[]> {
try {
const userData = await this.getUserData();
const userId = userData.USER.USER_ID;
const response = await this.apiCall('deezer.pageProfile', {
USER_ID: userId,
tab: 'playlists',
nb: 100
});
return response.TAB?.playlists?.data || [];
} catch (error) {
console.error('Error fetching playlists:', error);
return [];
}
}
// Get track download URL
async getTrackDownloadUrl(trackToken: string, format: string, licenseToken: string): Promise<string | null> {
try {
const response = await fetch('https://media.deezer.com/v1/get_url', {
method: 'POST',
headers: {
...this.httpHeaders,
'Cookie': this.arl ? `arl=${this.arl}` : ''
},
body: JSON.stringify({
license_token: licenseToken,
media: [{
type: 'FULL',
formats: [{ cipher: 'BF_CBC_STRIPE', format }]
}],
track_tokens: [trackToken]
})
});
const result = await response.json();
if (result.data && result.data.length > 0) {
const trackData = result.data[0];
if (trackData.media && trackData.media.length > 0) {
return trackData.media[0].sources[0].url;
}
}
return null;
} catch (error) {
console.error('Error getting track URL:', error);
return null;
}
}
}
// Singleton instance