mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 19:51:01 +00:00
feat(auth): implement purple music app authentication
This commit is contained in:
172
src/lib/services/deezer.ts
Normal file
172
src/lib/services/deezer.ts
Normal 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();
|
||||
86
src/lib/stores/deezer.ts
Normal file
86
src/lib/stores/deezer.ts
Normal file
@@ -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<DeezerAuthState> = writable(defaultState);
|
||||
|
||||
// Load Deezer auth state from store
|
||||
export async function loadDeezerAuth(): Promise<void> {
|
||||
const arl = await store.get<string>('arl');
|
||||
const user = await store.get<DeezerUser>('user');
|
||||
|
||||
deezerAuth.set({
|
||||
arl: arl ?? null,
|
||||
user: user ?? null,
|
||||
loggedIn: !!(arl && user)
|
||||
});
|
||||
}
|
||||
|
||||
// Save ARL token
|
||||
export async function saveArl(arl: string): Promise<void> {
|
||||
await store.set('arl', arl);
|
||||
await store.save();
|
||||
|
||||
deezerAuth.update(s => ({
|
||||
...s,
|
||||
arl
|
||||
}));
|
||||
}
|
||||
|
||||
// Save user data
|
||||
export async function saveUser(user: DeezerUser): Promise<void> {
|
||||
await store.set('user', user);
|
||||
await store.save();
|
||||
|
||||
deezerAuth.update(s => ({
|
||||
...s,
|
||||
user,
|
||||
loggedIn: true
|
||||
}));
|
||||
}
|
||||
|
||||
// Clear auth (logout)
|
||||
export async function clearDeezerAuth(): Promise<void> {
|
||||
await store.delete('arl');
|
||||
await store.delete('user');
|
||||
await store.save();
|
||||
|
||||
deezerAuth.set(defaultState);
|
||||
}
|
||||
|
||||
// Get ARL token
|
||||
export async function getArl(): Promise<string | null> {
|
||||
return (await store.get<string>('arl')) ?? null;
|
||||
}
|
||||
|
||||
// Initialize on module load
|
||||
loadDeezerAuth();
|
||||
Reference in New Issue
Block a user