From 48d8b4a593e86cbb67de6fab589900d265dec3b8 Mon Sep 17 00:00:00 2001 From: Markury Date: Tue, 30 Sep 2025 21:56:45 -0400 Subject: [PATCH] feat(auth): implement purple music app authentication --- src-tauri/Cargo.toml | 2 +- src-tauri/capabilities/default.json | 18 +- src/lib/services/deezer.ts | 172 ++++++++++++++ src/lib/stores/deezer.ts | 86 +++++++ src/routes/services/deezer/+page.svelte | 303 ++++++++++++++++++++++++ 5 files changed, 579 insertions(+), 2 deletions(-) create mode 100644 src/lib/services/deezer.ts create mode 100644 src/lib/stores/deezer.ts create mode 100644 src/routes/services/deezer/+page.svelte diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 26f110d..9ddbec8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,5 +25,5 @@ tauri-plugin-dialog = "2" tauri-plugin-fs = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -tauri-plugin-http = "2" +tauri-plugin-http = { version = "2", features = ["unsafe-headers"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index fc9545d..a7b7e32 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -27,6 +27,22 @@ } ] }, - "http:default" + { + "identifier": "http:default", + "allow": [ + { + "url": "http://www.deezer.com/**" + }, + { + "url": "https://www.deezer.com/**" + }, + { + "url": "https://*.deezer.com/**" + }, + { + "url": "http://*.deezer.com/**" + } + ] + } ] } \ No newline at end of file diff --git a/src/lib/services/deezer.ts b/src/lib/services/deezer.ts new file mode 100644 index 0000000..46e1f00 --- /dev/null +++ b/src/lib/services/deezer.ts @@ -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; + 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 { + const userData = await this.getUserData(); + return userData.checkForm || ''; + } + + // Call Deezer GW API + private async apiCall(method: string, args: any = {}, params: any = {}): Promise { + 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 { + 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 { + 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(); diff --git a/src/lib/stores/deezer.ts b/src/lib/stores/deezer.ts new file mode 100644 index 0000000..358895d --- /dev/null +++ b/src/lib/stores/deezer.ts @@ -0,0 +1,86 @@ +import { LazyStore } from '@tauri-apps/plugin-store'; +import { writable, type Writable } from 'svelte/store'; + +// Deezer User interface +export interface DeezerUser { + id: number; + name: string; + picture?: string; + license_token?: string; + can_stream_hq?: boolean; + can_stream_lossless?: boolean; + country?: string; + language?: string; +} + +// Deezer auth state +export interface DeezerAuthState { + arl: string | null; + user: DeezerUser | null; + loggedIn: boolean; +} + +// Initialize the store with deezer.json +const store = new LazyStore('deezer.json'); + +// Default state +const defaultState: DeezerAuthState = { + arl: null, + user: null, + loggedIn: false +}; + +// Create a writable store for reactive UI updates +export const deezerAuth: Writable = writable(defaultState); + +// Load Deezer auth state from store +export async function loadDeezerAuth(): Promise { + const arl = await store.get('arl'); + const user = await store.get('user'); + + deezerAuth.set({ + arl: arl ?? null, + user: user ?? null, + loggedIn: !!(arl && user) + }); +} + +// Save ARL token +export async function saveArl(arl: string): Promise { + await store.set('arl', arl); + await store.save(); + + deezerAuth.update(s => ({ + ...s, + arl + })); +} + +// Save user data +export async function saveUser(user: DeezerUser): Promise { + await store.set('user', user); + await store.save(); + + deezerAuth.update(s => ({ + ...s, + user, + loggedIn: true + })); +} + +// Clear auth (logout) +export async function clearDeezerAuth(): Promise { + await store.delete('arl'); + await store.delete('user'); + await store.save(); + + deezerAuth.set(defaultState); +} + +// Get ARL token +export async function getArl(): Promise { + return (await store.get('arl')) ?? null; +} + +// Initialize on module load +loadDeezerAuth(); diff --git a/src/routes/services/deezer/+page.svelte b/src/routes/services/deezer/+page.svelte new file mode 100644 index 0000000..606688a --- /dev/null +++ b/src/routes/services/deezer/+page.svelte @@ -0,0 +1,303 @@ + + +
+

Deezer Authentication

+ + {#if !$deezerAuth.loggedIn} + + + {:else} + +
+
+
User Info
+
+
+ + + {#if successMessage} +
+ ✓ {successMessage} +
+ {/if} + +
+ +
+
+
+ + +
+
+
Test Authentication
+
+
+

Test if your authentication is working:

+ + {#if authTestResult} +
+ {authTestResult} +
+ {/if} + +
+ +
+
+
+ {/if} +
+ +