mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 19:51:01 +00:00
feat(dz): add local caching and UI for user favorites
This commit is contained in:
@@ -26,7 +26,7 @@ fn tag_audio_file(
|
|||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let migrations = vec![
|
let library_migrations = vec![
|
||||||
Migration {
|
Migration {
|
||||||
version: 1,
|
version: 1,
|
||||||
description: "create_library_tables",
|
description: "create_library_tables",
|
||||||
@@ -65,10 +65,64 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let deezer_migrations = vec![
|
||||||
|
Migration {
|
||||||
|
version: 1,
|
||||||
|
description: "create_deezer_cache_tables",
|
||||||
|
sql: "
|
||||||
|
CREATE TABLE IF NOT EXISTS deezer_playlists (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
nb_tracks INTEGER DEFAULT 0,
|
||||||
|
creator_name TEXT,
|
||||||
|
picture_small TEXT,
|
||||||
|
picture_medium TEXT,
|
||||||
|
cached_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS deezer_albums (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
artist_name TEXT NOT NULL,
|
||||||
|
nb_tracks INTEGER DEFAULT 0,
|
||||||
|
release_date TEXT,
|
||||||
|
picture_small TEXT,
|
||||||
|
picture_medium TEXT,
|
||||||
|
cached_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS deezer_artists (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
nb_album INTEGER DEFAULT 0,
|
||||||
|
picture_small TEXT,
|
||||||
|
picture_medium TEXT,
|
||||||
|
cached_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS deezer_tracks (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
artist_name TEXT NOT NULL,
|
||||||
|
album_title TEXT,
|
||||||
|
duration INTEGER DEFAULT 0,
|
||||||
|
cached_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deezer_playlists_title ON deezer_playlists(title);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deezer_albums_artist ON deezer_albums(artist_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deezer_artists_name ON deezer_artists(name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deezer_tracks_title ON deezer_tracks(title);
|
||||||
|
",
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_sql::Builder::new()
|
tauri_plugin_sql::Builder::new()
|
||||||
.add_migrations("sqlite:library.db", migrations)
|
.add_migrations("sqlite:library.db", library_migrations)
|
||||||
|
.add_migrations("sqlite:deezer.db", deezer_migrations)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
|
|||||||
242
src/lib/library/deezer-database.ts
Normal file
242
src/lib/library/deezer-database.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import Database from '@tauri-apps/plugin-sql';
|
||||||
|
|
||||||
|
export interface DeezerPlaylist {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
nb_tracks: number;
|
||||||
|
creator_name: string;
|
||||||
|
picture_small?: string;
|
||||||
|
picture_medium?: string;
|
||||||
|
cached_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeezerAlbum {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
artist_name: string;
|
||||||
|
nb_tracks: number;
|
||||||
|
release_date?: string;
|
||||||
|
picture_small?: string;
|
||||||
|
picture_medium?: string;
|
||||||
|
cached_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeezerArtist {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
nb_album: number;
|
||||||
|
picture_small?: string;
|
||||||
|
picture_medium?: string;
|
||||||
|
cached_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeezerTrack {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
artist_name: string;
|
||||||
|
album_title: string;
|
||||||
|
duration: number;
|
||||||
|
cached_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let db: Database | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize database connection
|
||||||
|
*/
|
||||||
|
export async function initDeezerDatabase(): Promise<Database> {
|
||||||
|
if (!db) {
|
||||||
|
db = await Database.load('sqlite:deezer.db');
|
||||||
|
}
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached playlists
|
||||||
|
*/
|
||||||
|
export async function getCachedPlaylists(): Promise<DeezerPlaylist[]> {
|
||||||
|
const database = await initDeezerDatabase();
|
||||||
|
const playlists = await database.select<DeezerPlaylist[]>(
|
||||||
|
'SELECT * FROM deezer_playlists ORDER BY title COLLATE NOCASE'
|
||||||
|
);
|
||||||
|
return playlists || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached albums
|
||||||
|
*/
|
||||||
|
export async function getCachedAlbums(): Promise<DeezerAlbum[]> {
|
||||||
|
const database = await initDeezerDatabase();
|
||||||
|
const albums = await database.select<DeezerAlbum[]>(
|
||||||
|
'SELECT * FROM deezer_albums ORDER BY artist_name COLLATE NOCASE, title COLLATE NOCASE'
|
||||||
|
);
|
||||||
|
return albums || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached artists
|
||||||
|
*/
|
||||||
|
export async function getCachedArtists(): Promise<DeezerArtist[]> {
|
||||||
|
const database = await initDeezerDatabase();
|
||||||
|
const artists = await database.select<DeezerArtist[]>(
|
||||||
|
'SELECT * FROM deezer_artists ORDER BY name COLLATE NOCASE'
|
||||||
|
);
|
||||||
|
return artists || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached tracks
|
||||||
|
*/
|
||||||
|
export async function getCachedTracks(): Promise<DeezerTrack[]> {
|
||||||
|
const database = await initDeezerDatabase();
|
||||||
|
const tracks = await database.select<DeezerTrack[]>(
|
||||||
|
'SELECT * FROM deezer_tracks ORDER BY title COLLATE NOCASE'
|
||||||
|
);
|
||||||
|
return tracks || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert playlists
|
||||||
|
*/
|
||||||
|
export async function upsertPlaylists(playlists: any[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('[deezer-database] Upserting playlists, count:', playlists.length);
|
||||||
|
if (playlists.length > 0) {
|
||||||
|
console.log('[deezer-database] First playlist sample:', playlists[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const database = await initDeezerDatabase();
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
// Clear existing playlists
|
||||||
|
await database.execute('DELETE FROM deezer_playlists');
|
||||||
|
console.log('[deezer-database] Cleared existing playlists');
|
||||||
|
|
||||||
|
// Insert new playlists
|
||||||
|
for (const playlist of playlists) {
|
||||||
|
await database.execute(
|
||||||
|
`INSERT INTO deezer_playlists (id, title, nb_tracks, creator_name, picture_small, picture_medium, cached_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||||
|
[
|
||||||
|
String(playlist.PLAYLIST_ID),
|
||||||
|
playlist.TITLE || '',
|
||||||
|
playlist.NB_SONG || 0,
|
||||||
|
playlist.PARENT_USERNAME || 'Unknown',
|
||||||
|
playlist.PLAYLIST_PICTURE || null,
|
||||||
|
playlist.PICTURE_TYPE || null,
|
||||||
|
now
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log('[deezer-database] Inserted', playlists.length, 'playlists');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[deezer-database] Error in upsertPlaylists:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert albums
|
||||||
|
*/
|
||||||
|
export async function upsertAlbums(albums: any[]): Promise<void> {
|
||||||
|
const database = await initDeezerDatabase();
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
// Clear existing albums
|
||||||
|
await database.execute('DELETE FROM deezer_albums');
|
||||||
|
|
||||||
|
// Insert new albums
|
||||||
|
for (const album of albums) {
|
||||||
|
await database.execute(
|
||||||
|
`INSERT INTO deezer_albums (id, title, artist_name, nb_tracks, release_date, picture_small, picture_medium, cached_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||||
|
[
|
||||||
|
String(album.ALB_ID),
|
||||||
|
album.ALB_TITLE || '',
|
||||||
|
album.ART_NAME || 'Unknown',
|
||||||
|
album.NB_SONG || 0,
|
||||||
|
album.PHYSICAL_RELEASE_DATE || null,
|
||||||
|
album.ALB_PICTURE || null,
|
||||||
|
album.PICTURE_TYPE || null,
|
||||||
|
now
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert artists
|
||||||
|
*/
|
||||||
|
export async function upsertArtists(artists: any[]): Promise<void> {
|
||||||
|
const database = await initDeezerDatabase();
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
// Clear existing artists
|
||||||
|
await database.execute('DELETE FROM deezer_artists');
|
||||||
|
|
||||||
|
// Insert new artists
|
||||||
|
for (const artist of artists) {
|
||||||
|
await database.execute(
|
||||||
|
`INSERT INTO deezer_artists (id, name, nb_album, picture_small, picture_medium, cached_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||||
|
[
|
||||||
|
String(artist.ART_ID),
|
||||||
|
artist.ART_NAME || '',
|
||||||
|
artist.NB_ALBUM || 0,
|
||||||
|
artist.ART_PICTURE || null,
|
||||||
|
artist.PICTURE_TYPE || null,
|
||||||
|
now
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert tracks
|
||||||
|
*/
|
||||||
|
export async function upsertTracks(tracks: any[]): Promise<void> {
|
||||||
|
const database = await initDeezerDatabase();
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
// Clear existing tracks
|
||||||
|
await database.execute('DELETE FROM deezer_tracks');
|
||||||
|
|
||||||
|
// Insert new tracks
|
||||||
|
for (const track of tracks) {
|
||||||
|
await database.execute(
|
||||||
|
`INSERT INTO deezer_tracks (id, title, artist_name, album_title, duration, cached_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||||
|
[
|
||||||
|
String(track.SNG_ID),
|
||||||
|
track.SNG_TITLE || '',
|
||||||
|
track.ART_NAME || 'Unknown',
|
||||||
|
track.ALB_TITLE || '',
|
||||||
|
track.DURATION || 0,
|
||||||
|
now
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache timestamp
|
||||||
|
*/
|
||||||
|
export async function getCacheTimestamp(): Promise<number | null> {
|
||||||
|
const database = await initDeezerDatabase();
|
||||||
|
const result = await database.select<{ cached_at: number }[]>(
|
||||||
|
'SELECT cached_at FROM deezer_playlists LIMIT 1'
|
||||||
|
);
|
||||||
|
return result[0]?.cached_at || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all Deezer cache
|
||||||
|
*/
|
||||||
|
export async function clearDeezerCache(): Promise<void> {
|
||||||
|
const database = await initDeezerDatabase();
|
||||||
|
await database.execute('DELETE FROM deezer_playlists');
|
||||||
|
await database.execute('DELETE FROM deezer_albums');
|
||||||
|
await database.execute('DELETE FROM deezer_artists');
|
||||||
|
await database.execute('DELETE FROM deezer_tracks');
|
||||||
|
await database.execute('VACUUM');
|
||||||
|
}
|
||||||
@@ -307,7 +307,7 @@ export class DeezerAPI {
|
|||||||
const response = await this.apiCall('deezer.pageProfile', {
|
const response = await this.apiCall('deezer.pageProfile', {
|
||||||
USER_ID: userId,
|
USER_ID: userId,
|
||||||
tab: 'playlists',
|
tab: 'playlists',
|
||||||
nb: 100
|
nb: -1
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.TAB?.playlists?.data || [];
|
return response.TAB?.playlists?.data || [];
|
||||||
@@ -317,6 +317,86 @@ export class DeezerAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get user albums
|
||||||
|
async getUserAlbums(): 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: 'albums',
|
||||||
|
nb: -1
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.TAB?.albums?.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching albums:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user artists
|
||||||
|
async getUserArtists(): 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: 'artists',
|
||||||
|
nb: -1
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.TAB?.artists?.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching artists:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user favorite tracks (uses the more reliable song.getFavoriteIds method)
|
||||||
|
async getUserTracks(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
// Get favorite track IDs
|
||||||
|
const idsResponse = await this.apiCall('song.getFavoriteIds', {
|
||||||
|
nb: -1,
|
||||||
|
start: 0,
|
||||||
|
checksum: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const trackIds = idsResponse.data?.map((x: any) => x.SNG_ID) || [];
|
||||||
|
|
||||||
|
if (trackIds.length === 0) {
|
||||||
|
console.log('[Deezer] No favorite tracks found');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Deezer] Found ${trackIds.length} favorite track IDs, fetching details...`);
|
||||||
|
|
||||||
|
// Fetch track details in batches (Deezer API might have limits)
|
||||||
|
const batchSize = 100;
|
||||||
|
const tracks: any[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < trackIds.length; i += batchSize) {
|
||||||
|
const batchIds = trackIds.slice(i, i + batchSize);
|
||||||
|
const batchResponse = await this.apiCall('song.getListData', {
|
||||||
|
SNG_IDS: batchIds
|
||||||
|
});
|
||||||
|
|
||||||
|
if (batchResponse.data) {
|
||||||
|
tracks.push(...batchResponse.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Deezer] Fetched ${tracks.length} track details`);
|
||||||
|
return tracks;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching favorite tracks:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get album data
|
// Get album data
|
||||||
async getAlbumData(albumId: string): Promise<any> {
|
async getAlbumData(albumId: string): Promise<any> {
|
||||||
return this.apiCall('album.getData', { alb_id: albumId });
|
return this.apiCall('album.getData', { alb_id: albumId });
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface DeezerAuthState {
|
|||||||
arl: string | null;
|
arl: string | null;
|
||||||
user: DeezerUser | null;
|
user: DeezerUser | null;
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
|
cacheTimestamp: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the store with deezer.json
|
// Initialize the store with deezer.json
|
||||||
@@ -27,7 +28,8 @@ const store = new LazyStore('deezer.json');
|
|||||||
const defaultState: DeezerAuthState = {
|
const defaultState: DeezerAuthState = {
|
||||||
arl: null,
|
arl: null,
|
||||||
user: null,
|
user: null,
|
||||||
loggedIn: false
|
loggedIn: false,
|
||||||
|
cacheTimestamp: null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a writable store for reactive UI updates
|
// Create a writable store for reactive UI updates
|
||||||
@@ -37,11 +39,13 @@ export const deezerAuth: Writable<DeezerAuthState> = writable(defaultState);
|
|||||||
export async function loadDeezerAuth(): Promise<void> {
|
export async function loadDeezerAuth(): Promise<void> {
|
||||||
const arl = await store.get<string>('arl');
|
const arl = await store.get<string>('arl');
|
||||||
const user = await store.get<DeezerUser>('user');
|
const user = await store.get<DeezerUser>('user');
|
||||||
|
const cacheTimestamp = await store.get<number>('cacheTimestamp');
|
||||||
|
|
||||||
deezerAuth.set({
|
deezerAuth.set({
|
||||||
arl: arl ?? null,
|
arl: arl ?? null,
|
||||||
user: user ?? null,
|
user: user ?? null,
|
||||||
loggedIn: !!(arl && user)
|
loggedIn: !!(arl && user),
|
||||||
|
cacheTimestamp: cacheTimestamp ?? null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,5 +86,16 @@ export async function getArl(): Promise<string | null> {
|
|||||||
return (await store.get<string>('arl')) ?? null;
|
return (await store.get<string>('arl')) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save cache timestamp
|
||||||
|
export async function saveCacheTimestamp(timestamp: number): Promise<void> {
|
||||||
|
await store.set('cacheTimestamp', timestamp);
|
||||||
|
await store.save();
|
||||||
|
|
||||||
|
deezerAuth.update(s => ({
|
||||||
|
...s,
|
||||||
|
cacheTimestamp: timestamp
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on module load
|
// Initialize on module load
|
||||||
loadDeezerAuth();
|
loadDeezerAuth();
|
||||||
|
|||||||
@@ -1,44 +1,167 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { deezerAuth, loadDeezerAuth, saveArl, saveUser, clearDeezerAuth, saveCacheTimestamp } from '$lib/stores/deezer';
|
||||||
import { deezerAuth, loadDeezerAuth, saveArl, saveUser, clearDeezerAuth } from '$lib/stores/deezer';
|
|
||||||
import { deezerAPI } from '$lib/services/deezer';
|
import { deezerAPI } from '$lib/services/deezer';
|
||||||
import { addDeezerTrackToQueue } from '$lib/services/deezer/addToQueue';
|
import {
|
||||||
import { settings } from '$lib/stores/settings';
|
getCachedPlaylists,
|
||||||
|
getCachedAlbums,
|
||||||
|
getCachedArtists,
|
||||||
|
getCachedTracks,
|
||||||
|
upsertPlaylists,
|
||||||
|
upsertAlbums,
|
||||||
|
upsertArtists,
|
||||||
|
upsertTracks,
|
||||||
|
type DeezerPlaylist,
|
||||||
|
type DeezerAlbum,
|
||||||
|
type DeezerArtist,
|
||||||
|
type DeezerTrack
|
||||||
|
} from '$lib/library/deezer-database';
|
||||||
|
|
||||||
|
type ViewMode = 'playlists' | 'tracks' | 'artists' | 'albums' | 'user';
|
||||||
|
|
||||||
|
let viewMode = $state<ViewMode>('playlists');
|
||||||
|
let playlists = $state<DeezerPlaylist[]>([]);
|
||||||
|
let albums = $state<DeezerAlbum[]>([]);
|
||||||
|
let artists = $state<DeezerArtist[]>([]);
|
||||||
|
let tracks = $state<DeezerTrack[]>([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
let syncing = $state(false);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let selectedIndex = $state<number | null>(null);
|
||||||
|
|
||||||
|
// Login form state
|
||||||
let arlInput = $state('');
|
let arlInput = $state('');
|
||||||
let isLoading = $state(false);
|
let isLogging = $state(false);
|
||||||
let errorMessage = $state('');
|
let loginError = $state('');
|
||||||
let successMessage = $state('');
|
let loginSuccess = $state('');
|
||||||
let testingAuth = $state(false);
|
|
||||||
let authTestResult = $state<string | null>(null);
|
|
||||||
|
|
||||||
// Track add to queue test
|
// User refresh state
|
||||||
let trackIdInput = $state('3135556'); // Default: Daft Punk - One More Time
|
let refreshingUser = $state(false);
|
||||||
let isFetchingTrack = $state(false);
|
let userRefreshMessage = $state('');
|
||||||
let isAddingToQueue = $state(false);
|
|
||||||
let trackInfo = $state<any>(null);
|
const CACHE_DURATION = 24 * 60 * 60; // 24 hours in seconds
|
||||||
let queueStatus = $state('');
|
|
||||||
let queueError = $state('');
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadDeezerAuth();
|
await loadDeezerAuth();
|
||||||
|
if ($deezerAuth.loggedIn) {
|
||||||
|
await loadFavorites();
|
||||||
|
} else {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadFavorites() {
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if we need to refresh cache
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const cacheAge = $deezerAuth.cacheTimestamp ? now - $deezerAuth.cacheTimestamp : Infinity;
|
||||||
|
const needsRefresh = cacheAge > CACHE_DURATION;
|
||||||
|
|
||||||
|
if (needsRefresh) {
|
||||||
|
await refreshFavorites();
|
||||||
|
} else {
|
||||||
|
// Load from cache
|
||||||
|
const [cachedPlaylists, cachedAlbums, cachedArtists, cachedTracks] = await Promise.all([
|
||||||
|
getCachedPlaylists(),
|
||||||
|
getCachedAlbums(),
|
||||||
|
getCachedArtists(),
|
||||||
|
getCachedTracks()
|
||||||
|
]);
|
||||||
|
|
||||||
|
playlists = cachedPlaylists;
|
||||||
|
albums = cachedAlbums;
|
||||||
|
artists = cachedArtists;
|
||||||
|
tracks = cachedTracks;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = 'Error loading favorites: ' + (e instanceof Error ? e.message : String(e));
|
||||||
|
// Switch to user tab to show error
|
||||||
|
viewMode = 'user';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshFavorites() {
|
||||||
|
if (!$deezerAuth.arl || syncing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncing = true;
|
||||||
|
error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
deezerAPI.setArl($deezerAuth.arl);
|
||||||
|
|
||||||
|
// Fetch all favorites from API
|
||||||
|
console.log('[Deezer] Fetching favorites from API...');
|
||||||
|
const [apiPlaylists, apiAlbums, apiArtists, apiTracks] = await Promise.all([
|
||||||
|
deezerAPI.getUserPlaylists(),
|
||||||
|
deezerAPI.getUserAlbums(),
|
||||||
|
deezerAPI.getUserArtists(),
|
||||||
|
deezerAPI.getUserTracks()
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('[Deezer] Fetched from API:', {
|
||||||
|
playlists: apiPlaylists.length,
|
||||||
|
albums: apiAlbums.length,
|
||||||
|
artists: apiArtists.length,
|
||||||
|
tracks: apiTracks.length
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update database cache
|
||||||
|
console.log('[Deezer] Updating database cache...');
|
||||||
|
await upsertPlaylists(apiPlaylists);
|
||||||
|
await upsertAlbums(apiAlbums);
|
||||||
|
await upsertArtists(apiArtists);
|
||||||
|
await upsertTracks(apiTracks);
|
||||||
|
|
||||||
|
// Update cache timestamp
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
await saveCacheTimestamp(now);
|
||||||
|
|
||||||
|
console.log('[Deezer] Reloading from cache...');
|
||||||
|
// Reload from cache
|
||||||
|
const [cachedPlaylists, cachedAlbums, cachedArtists, cachedTracks] = await Promise.all([
|
||||||
|
getCachedPlaylists(),
|
||||||
|
getCachedAlbums(),
|
||||||
|
getCachedArtists(),
|
||||||
|
getCachedTracks()
|
||||||
|
]);
|
||||||
|
|
||||||
|
playlists = cachedPlaylists;
|
||||||
|
albums = cachedAlbums;
|
||||||
|
artists = cachedArtists;
|
||||||
|
tracks = cachedTracks;
|
||||||
|
|
||||||
|
console.log('[Deezer] Refresh complete!');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error refreshing favorites:', e);
|
||||||
|
error = 'Error refreshing favorites: ' + (e instanceof Error ? e.message : String(e));
|
||||||
|
// Switch to user tab to show error
|
||||||
|
viewMode = 'user';
|
||||||
|
} finally {
|
||||||
|
syncing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
if (!arlInput || arlInput.trim().length === 0) {
|
if (!arlInput || arlInput.trim().length === 0) {
|
||||||
errorMessage = 'Please enter an ARL token';
|
loginError = 'Please enter an ARL token';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arlInput.trim().length !== 192) {
|
if (arlInput.trim().length !== 192) {
|
||||||
errorMessage = 'ARL token should be 192 characters long';
|
loginError = 'ARL token should be 192 characters long';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading = true;
|
isLogging = true;
|
||||||
errorMessage = '';
|
loginError = '';
|
||||||
successMessage = '';
|
loginSuccess = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await deezerAPI.loginViaArl(arlInput.trim());
|
const result = await deezerAPI.loginViaArl(arlInput.trim());
|
||||||
@@ -46,114 +169,76 @@
|
|||||||
if (result.success && result.user) {
|
if (result.success && result.user) {
|
||||||
await saveArl(arlInput.trim());
|
await saveArl(arlInput.trim());
|
||||||
await saveUser(result.user);
|
await saveUser(result.user);
|
||||||
successMessage = `Successfully logged in as ${result.user.name}!`;
|
loginSuccess = `Successfully logged in as ${result.user.name}!`;
|
||||||
arlInput = '';
|
arlInput = '';
|
||||||
|
|
||||||
|
// Load favorites after login
|
||||||
|
await loadFavorites();
|
||||||
} else {
|
} else {
|
||||||
errorMessage = result.error || 'Login failed. Please check your ARL token.';
|
loginError = result.error || 'Login failed. Please check your ARL token.';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage = `Login error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
loginError = `Login error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLogging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await clearDeezerAuth();
|
await clearDeezerAuth();
|
||||||
successMessage = 'Logged out successfully';
|
playlists = [];
|
||||||
errorMessage = '';
|
albums = [];
|
||||||
authTestResult = null;
|
artists = [];
|
||||||
|
tracks = [];
|
||||||
|
loginSuccess = '';
|
||||||
|
loginError = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testAuthentication() {
|
async function handleRefreshUser() {
|
||||||
if (!$deezerAuth.arl) {
|
if (!$deezerAuth.arl || refreshingUser) {
|
||||||
authTestResult = 'Not logged in';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
testingAuth = true;
|
refreshingUser = true;
|
||||||
authTestResult = null;
|
userRefreshMessage = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deezerAPI.setArl($deezerAuth.arl);
|
deezerAPI.setArl($deezerAuth.arl);
|
||||||
const isValid = await deezerAPI.testAuth();
|
const result = await deezerAPI.loginViaArl($deezerAuth.arl);
|
||||||
authTestResult = isValid ? '✓ Authentication is working!' : '✗ Authentication failed';
|
|
||||||
} catch (error) {
|
|
||||||
authTestResult = `✗ Test failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
||||||
} finally {
|
|
||||||
testingAuth = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchTrackInfo() {
|
if (result.success && result.user) {
|
||||||
if (!$deezerAuth.arl || !$deezerAuth.user) {
|
await saveUser(result.user);
|
||||||
queueError = 'Not logged in';
|
userRefreshMessage = 'User info refreshed successfully!';
|
||||||
return;
|
} else {
|
||||||
}
|
userRefreshMessage = 'Failed to refresh user info';
|
||||||
|
|
||||||
isFetchingTrack = true;
|
|
||||||
queueError = '';
|
|
||||||
trackInfo = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
deezerAPI.setArl($deezerAuth.arl);
|
|
||||||
const trackData = await deezerAPI.getTrack(trackIdInput);
|
|
||||||
console.log('Track data:', trackData);
|
|
||||||
|
|
||||||
if (!trackData || !trackData.SNG_ID) {
|
|
||||||
throw new Error('Track not found or invalid track ID');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trackInfo = trackData;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch error:', error);
|
userRefreshMessage = 'Error refreshing user info: ' + (error instanceof Error ? error.message : 'Unknown error');
|
||||||
queueError = error instanceof Error ? error.message : 'Failed to fetch track';
|
|
||||||
} finally {
|
} finally {
|
||||||
isFetchingTrack = false;
|
refreshingUser = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
userRefreshMessage = '';
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addTrackToQueue() {
|
function handleItemClick(index: number) {
|
||||||
if (!trackInfo) {
|
selectedIndex = index;
|
||||||
queueError = 'Please fetch track info first';
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$settings.musicFolder) {
|
function formatDuration(seconds: number): string {
|
||||||
queueError = 'Please set a music folder in Settings first';
|
const mins = Math.floor(seconds / 60);
|
||||||
return;
|
const secs = Math.floor(seconds % 60);
|
||||||
}
|
return `${mins}:${String(secs).padStart(2, '0')}`;
|
||||||
|
|
||||||
isAddingToQueue = true;
|
|
||||||
queueStatus = '';
|
|
||||||
queueError = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use shared utility to add track to queue
|
|
||||||
await addDeezerTrackToQueue(trackIdInput);
|
|
||||||
|
|
||||||
queueStatus = '✓ Added to download queue!';
|
|
||||||
|
|
||||||
// Navigate to downloads page after brief delay
|
|
||||||
setTimeout(() => {
|
|
||||||
goto('/downloads');
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Queue error:', error);
|
|
||||||
queueError = error instanceof Error ? error.message : 'Failed to add to queue';
|
|
||||||
} finally {
|
|
||||||
isAddingToQueue = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="deezer-page">
|
<div class="deezer-wrapper">
|
||||||
<h2>Deezer Authentication</h2>
|
<h2 style="padding: 8px">Deezer</h2>
|
||||||
|
|
||||||
{#if !$deezerAuth.loggedIn}
|
{#if !$deezerAuth.loggedIn}
|
||||||
<!-- Login Form -->
|
<!-- Login Form -->
|
||||||
<section class="window login-section">
|
<section class="window login-section" style="max-width: 600px; margin: 8px;">
|
||||||
<div class="title-bar">
|
<div class="title-bar">
|
||||||
<div class="title-bar-text">Login to Deezer</div>
|
<div class="title-bar-text">Login to Deezer</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,29 +252,28 @@
|
|||||||
type="password"
|
type="password"
|
||||||
bind:value={arlInput}
|
bind:value={arlInput}
|
||||||
placeholder="192 character ARL token"
|
placeholder="192 character ARL token"
|
||||||
disabled={isLoading}
|
disabled={isLogging}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if loginError}
|
||||||
<div class="error-message">
|
<div class="error-message">
|
||||||
⚠ {errorMessage}
|
⚠ {loginError}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if successMessage}
|
{#if loginSuccess}
|
||||||
<div class="success-message">
|
<div class="success-message">
|
||||||
✓ {successMessage}
|
✓ {loginSuccess}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button onclick={handleLogin} disabled={isLoading}>
|
<button onclick={handleLogin} disabled={isLogging}>
|
||||||
{isLoading ? 'Logging in...' : 'Login'}
|
{isLogging ? 'Logging in...' : 'Login'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Instructions -->
|
|
||||||
<details class="instructions">
|
<details class="instructions">
|
||||||
<summary>How to get your ARL token</summary>
|
<summary>How to get your ARL token</summary>
|
||||||
<div class="instructions-content">
|
<div class="instructions-content">
|
||||||
@@ -208,121 +292,233 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{:else if loading}
|
||||||
|
<p style="padding: 8px;">Loading favorites...</p>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Logged In View -->
|
<section class="favorites-content">
|
||||||
<section class="window user-section">
|
<!-- Tabs -->
|
||||||
<div class="title-bar">
|
<!--
|
||||||
<div class="title-bar-text">User Info</div>
|
svelte-ignore a11y_no_noninteractive_element_to_interactive_role
|
||||||
</div>
|
Reason: 98.css library requires <menu role="tablist"> for proper tab styling.
|
||||||
<div class="window-body">
|
-->
|
||||||
<div class="user-info">
|
<menu role="tablist">
|
||||||
<div class="field-row">
|
<li role="tab" aria-selected={viewMode === 'playlists'}>
|
||||||
<span>Name:</span>
|
<button onclick={() => viewMode = 'playlists'}>Playlists</button>
|
||||||
<span>{$deezerAuth.user?.name || 'Unknown'}</span>
|
</li>
|
||||||
</div>
|
<li role="tab" aria-selected={viewMode === 'tracks'}>
|
||||||
<div class="field-row">
|
<button onclick={() => viewMode = 'tracks'}>Tracks</button>
|
||||||
<span>User ID:</span>
|
</li>
|
||||||
<span>{$deezerAuth.user?.id || 'N/A'}</span>
|
<li role="tab" aria-selected={viewMode === 'artists'}>
|
||||||
</div>
|
<button onclick={() => viewMode = 'artists'}>Artists</button>
|
||||||
<div class="field-row">
|
</li>
|
||||||
<span>Country:</span>
|
<li role="tab" aria-selected={viewMode === 'albums'}>
|
||||||
<span>{$deezerAuth.user?.country || 'N/A'}</span>
|
<button onclick={() => viewMode = 'albums'}>Albums</button>
|
||||||
</div>
|
</li>
|
||||||
<div class="field-row">
|
<li role="tab" aria-selected={viewMode === 'user'}>
|
||||||
<span>HQ Streaming:</span>
|
<button onclick={() => viewMode = 'user'}>User</button>
|
||||||
<span>{$deezerAuth.user?.can_stream_hq ? '✓ Yes' : '✗ No'}</span>
|
</li>
|
||||||
</div>
|
</menu>
|
||||||
<div class="field-row">
|
|
||||||
<span>Lossless Streaming:</span>
|
<!-- Tab Content -->
|
||||||
<span>{$deezerAuth.user?.can_stream_lossless ? '✓ Yes' : '✗ No'}</span>
|
<div class="window tab-content" role="tabpanel">
|
||||||
</div>
|
<div class="window-body">
|
||||||
|
{#if syncing}
|
||||||
|
<div class="sync-status">
|
||||||
|
<p>Refreshing favorites from Deezer...</p>
|
||||||
|
</div>
|
||||||
|
{:else if viewMode === 'playlists'}
|
||||||
|
<!-- Playlists View -->
|
||||||
|
<div class="tab-header">
|
||||||
|
<h4>Favorite Playlists</h4>
|
||||||
|
<button onclick={refreshFavorites} disabled={syncing}>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sunken-panel table-container">
|
||||||
|
<table class="interactive">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Playlist</th>
|
||||||
|
<th>Tracks</th>
|
||||||
|
<th>Creator</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each playlists as playlist, i}
|
||||||
|
<tr
|
||||||
|
class:highlighted={selectedIndex === i}
|
||||||
|
onclick={() => handleItemClick(i)}
|
||||||
|
>
|
||||||
|
<td>{playlist.title}</td>
|
||||||
|
<td>{playlist.nb_tracks}</td>
|
||||||
|
<td>{playlist.creator_name}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{:else if viewMode === 'tracks'}
|
||||||
|
<!-- Tracks View -->
|
||||||
|
<div class="tab-header">
|
||||||
|
<h4>Favorite Tracks</h4>
|
||||||
|
<button onclick={refreshFavorites} disabled={syncing}>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sunken-panel table-container">
|
||||||
|
<table class="interactive">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Artist</th>
|
||||||
|
<th>Album</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each tracks as track, i}
|
||||||
|
<tr
|
||||||
|
class:highlighted={selectedIndex === i}
|
||||||
|
onclick={() => handleItemClick(i)}
|
||||||
|
>
|
||||||
|
<td>{track.title}</td>
|
||||||
|
<td>{track.artist_name}</td>
|
||||||
|
<td>{track.album_title}</td>
|
||||||
|
<td class="duration">{formatDuration(track.duration)}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{:else if viewMode === 'artists'}
|
||||||
|
<!-- Artists View -->
|
||||||
|
<div class="tab-header">
|
||||||
|
<h4>Favorite Artists</h4>
|
||||||
|
<button onclick={refreshFavorites} disabled={syncing}>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sunken-panel table-container">
|
||||||
|
<table class="interactive">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Artist</th>
|
||||||
|
<th>Albums</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each artists as artist, i}
|
||||||
|
<tr
|
||||||
|
class:highlighted={selectedIndex === i}
|
||||||
|
onclick={() => handleItemClick(i)}
|
||||||
|
>
|
||||||
|
<td>{artist.name}</td>
|
||||||
|
<td>{artist.nb_album}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{:else if viewMode === 'albums'}
|
||||||
|
<!-- Albums View -->
|
||||||
|
<div class="tab-header">
|
||||||
|
<h4>Favorite Albums</h4>
|
||||||
|
<button onclick={refreshFavorites} disabled={syncing}>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sunken-panel table-container">
|
||||||
|
<table class="interactive">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Album</th>
|
||||||
|
<th>Artist</th>
|
||||||
|
<th>Tracks</th>
|
||||||
|
<th>Year</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each albums as album, i}
|
||||||
|
<tr
|
||||||
|
class:highlighted={selectedIndex === i}
|
||||||
|
onclick={() => handleItemClick(i)}
|
||||||
|
>
|
||||||
|
<td>{album.title}</td>
|
||||||
|
<td>{album.artist_name}</td>
|
||||||
|
<td>{album.nb_tracks}</td>
|
||||||
|
<td>{album.release_date ? new Date(album.release_date).getFullYear() : '—'}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{:else if viewMode === 'user'}
|
||||||
|
<!-- User Info View -->
|
||||||
|
<div class="user-container">
|
||||||
|
{#if error}
|
||||||
|
<fieldset>
|
||||||
|
<legend>Error</legend>
|
||||||
|
<div class="error-message">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>User Information</legend>
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-label">Name:</span>
|
||||||
|
<span>{$deezerAuth.user?.name || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-label">Country:</span>
|
||||||
|
<span>{$deezerAuth.user?.country || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-label">HQ Streaming:</span>
|
||||||
|
<span>{$deezerAuth.user?.can_stream_hq ? '✓ Yes' : '✗ No'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-label">Lossless Streaming:</span>
|
||||||
|
<span>{$deezerAuth.user?.can_stream_lossless ? '✓ Yes' : '✗ No'}</span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{#if userRefreshMessage}
|
||||||
|
<div class="message-box">
|
||||||
|
{userRefreshMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<fieldset style="margin-top: 16px;">
|
||||||
|
<legend>Actions</legend>
|
||||||
|
<div class="button-row">
|
||||||
|
<button onclick={handleRefreshUser} disabled={refreshingUser}>
|
||||||
|
{refreshingUser ? 'Refreshing...' : 'Refresh User Info'}
|
||||||
|
</button>
|
||||||
|
<button onclick={handleLogout}>Logout</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if successMessage}
|
|
||||||
<div class="success-message">
|
|
||||||
✓ {successMessage}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="button-row">
|
|
||||||
<button onclick={handleLogout}>Logout</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Add Track to Queue -->
|
|
||||||
<section class="window test-section">
|
|
||||||
<div class="title-bar">
|
|
||||||
<div class="title-bar-text">Add Track to Download Queue</div>
|
|
||||||
</div>
|
|
||||||
<div class="window-body">
|
|
||||||
<p>Add a track to the download queue:</p>
|
|
||||||
|
|
||||||
<div class="field-row-stacked">
|
|
||||||
<label for="track-id">Track ID (from Deezer URL)</label>
|
|
||||||
<input
|
|
||||||
id="track-id"
|
|
||||||
type="text"
|
|
||||||
bind:value={trackIdInput}
|
|
||||||
placeholder="e.g., 3135556"
|
|
||||||
disabled={isFetchingTrack || isAddingToQueue}
|
|
||||||
/>
|
|
||||||
<small class="help-text">Default: 3135556 (Daft Punk - One More Time)</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if trackInfo}
|
|
||||||
<div class="track-info">
|
|
||||||
<strong>{trackInfo.SNG_TITLE}</strong> by {trackInfo.ART_NAME}
|
|
||||||
<br>
|
|
||||||
<small>Album: {trackInfo.ALB_TITLE} • Duration: {Math.floor(trackInfo.DURATION / 60)}:{String(trackInfo.DURATION % 60).padStart(2, '0')}</small>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if queueStatus}
|
|
||||||
<div class="success-message">
|
|
||||||
{queueStatus}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if queueError}
|
|
||||||
<div class="error-message">
|
|
||||||
⚠ {queueError}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="button-row">
|
|
||||||
<button onclick={fetchTrackInfo} disabled={isFetchingTrack || isAddingToQueue}>
|
|
||||||
{isFetchingTrack ? 'Fetching...' : 'Fetch Track Info'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick={addTrackToQueue} disabled={!trackInfo || isAddingToQueue || !$settings.musicFolder}>
|
|
||||||
{isAddingToQueue ? 'Adding...' : 'Add to Queue'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if !$settings.musicFolder}
|
|
||||||
<p class="help-text" style="margin-top: 8px;">
|
|
||||||
⚠ Please set a music folder in Settings before adding to queue.
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.deezer-page {
|
.deezer-wrapper {
|
||||||
max-width: 600px;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-top: 0;
|
margin: 0;
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-section,
|
.login-section {
|
||||||
.user-section,
|
|
||||||
.test-section {
|
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,23 +539,16 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-row span:first-child {
|
.field-label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="password"],
|
input[type="password"] {
|
||||||
input[type="text"] {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-text {
|
|
||||||
color: var(--text-color, #FFFFFF);
|
|
||||||
opacity: 0.7;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -413,18 +602,76 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.favorites-content {
|
||||||
margin-bottom: 12px;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-info {
|
.tab-content {
|
||||||
|
margin-top: -2px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content .window-body {
|
||||||
|
padding: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid var(--button-shadow, #808080);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-align: center;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
background-color: var(--button-shadow, #2a2a2a);
|
background-color: var(--button-shadow, #2a2a2a);
|
||||||
border: 1px solid var(--button-highlight, #606060);
|
border: 1px solid var(--button-highlight, #606060);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-info strong {
|
.sync-status {
|
||||||
font-weight: bold;
|
padding: 16px 8px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user