mirror of
https://github.com/markuryy/shark.git
synced 2025-12-13 03:51:02 +00:00
feat(auth): purple app track fetch
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user