feat(auth): implement purple music app authentication

This commit is contained in:
2025-09-30 21:56:45 -04:00
parent d9e7e4885d
commit 48d8b4a593
5 changed files with 579 additions and 2 deletions

172
src/lib/services/deezer.ts Normal file
View File

@@ -0,0 +1,172 @@
import { fetch } from '@tauri-apps/plugin-http';
import type { DeezerUser } from '$lib/stores/deezer';
// Deezer API response types
interface DeezerUserData {
USER: {
USER_ID: number;
BLOG_NAME: string;
USER_PICTURE?: string;
MULTI_ACCOUNT?: {
ENABLED: boolean;
IS_SUB_ACCOUNT: boolean;
};
OPTIONS: {
license_token: string;
web_hq?: boolean;
mobile_hq?: boolean;
web_lossless?: boolean;
mobile_lossless?: boolean;
license_country: string;
};
SETTING?: {
global?: {
language?: string;
};
};
LOVEDTRACKS_ID?: number;
};
checkForm?: string;
}
interface GWAPIResponse {
results: DeezerUserData;
error: any;
}
export class DeezerAPI {
private httpHeaders: Record<string, string>;
private arl: string | null = null;
private apiToken: string | null = null;
constructor() {
this.httpHeaders = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
'Content-Type': 'application/json',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9'
};
}
// Set ARL cookie for authentication
setArl(arl: string): void {
this.arl = arl.trim();
}
// Get API token from getUserData
private async getToken(): Promise<string> {
const userData = await this.getUserData();
return userData.checkForm || '';
}
// Call Deezer GW API
private async apiCall(method: string, args: any = {}, params: any = {}): Promise<any> {
if (!this.apiToken && method !== 'deezer.getUserData') {
this.apiToken = await this.getToken();
}
const searchParams = new URLSearchParams({
api_version: '1.0',
api_token: method === 'deezer.getUserData' ? 'null' : (this.apiToken || 'null'),
input: '3',
method,
...params
});
const url = `http://www.deezer.com/ajax/gw-light.php?${searchParams.toString()}`;
try {
const response = await fetch(url, {
method: 'POST',
headers: {
...this.httpHeaders,
'Cookie': this.arl ? `arl=${this.arl}` : ''
},
body: JSON.stringify(args)
});
const resultJson: GWAPIResponse = await response.json();
// Handle errors
if (resultJson.error && (Array.isArray(resultJson.error) ? resultJson.error.length : Object.keys(resultJson.error).length)) {
const errorStr = JSON.stringify(resultJson.error);
// Handle invalid token - retry with new token
if (errorStr.includes('invalid api token') || errorStr.includes('Invalid CSRF token')) {
this.apiToken = await this.getToken();
return this.apiCall(method, args, params);
}
throw new Error(`Deezer API Error: ${errorStr}`);
}
// Set token from getUserData response
if (!this.apiToken && method === 'deezer.getUserData') {
this.apiToken = resultJson.results.checkForm || null;
}
return resultJson.results;
} catch (error) {
console.error('[ERROR] deezer.gw', method, args, error);
throw error;
}
}
// Get user data
async getUserData(): Promise<DeezerUserData> {
return this.apiCall('deezer.getUserData');
}
// Login via ARL token
async loginViaArl(arl: string): Promise<{ success: boolean; user?: DeezerUser; error?: string }> {
try {
this.setArl(arl);
const userData = await this.getUserData();
// Check if user is logged in
if (!userData || !userData.USER || userData.USER.USER_ID === 0) {
return {
success: false,
error: 'Invalid ARL token or not logged in'
};
}
// Build user object
const user: DeezerUser = {
id: userData.USER.USER_ID,
name: userData.USER.BLOG_NAME,
picture: userData.USER.USER_PICTURE || '',
license_token: userData.USER.OPTIONS.license_token,
can_stream_hq: userData.USER.OPTIONS.web_hq || userData.USER.OPTIONS.mobile_hq || false,
can_stream_lossless: userData.USER.OPTIONS.web_lossless || userData.USER.OPTIONS.mobile_lossless || false,
country: userData.USER.OPTIONS.license_country,
language: userData.USER.SETTING?.global?.language || 'en'
};
return {
success: true,
user
};
} catch (error) {
console.error('Login error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
// Test if authentication is working
async testAuth(): Promise<boolean> {
try {
const userData = await this.getUserData();
return userData && userData.USER && userData.USER.USER_ID !== 0;
} catch {
return false;
}
}
}
// Singleton instance
export const deezerAPI = new DeezerAPI();