Files
shark/CLAUDE.md

8.6 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Shark is a music management and download application built with Tauri 2, SvelteKit 2, and TypeScript. It integrates with Deezer for music downloads and uses a custom Windows 98-style UI theme.

Development Commands

  • Run dev server: bun run tauri dev (runs both Vite dev server and Tauri)
  • Build for production: bun run tauri build
  • Type checking: bun run check
  • Add packages: bun add <package-name>

Important Notes

  • Always use bun as the package manager, not npm or yarn
  • Tauri capabilities changes require a full restart of bun run tauri dev to take effect
  • The app uses custom window decorations (no native title bar)

Architecture

Tech Stack

  • Frontend: SvelteKit 2 with Svelte 5 (runes-based reactivity)
  • Desktop Runtime: Tauri 2
  • Package Manager: Bun
  • Styling: Custom Windows 98 theme (defined in +layout.svelte)

Directory Structure

shark/
├── src/                          # SvelteKit frontend
│   ├── routes/                   # File-based routing
│   │   ├── +layout.svelte       # Root layout with Windows 98 theme
│   │   ├── library/             # Music library management
│   │   ├── playlists/           # Playlist management
│   │   ├── services/deezer/     # Deezer integration UI
│   │   └── settings/            # App settings
│   └── lib/
│       ├── services/
│       │   └── deezer/          # Deezer service layer
│       │       ├── crypto.ts    # Blowfish decryption for tracks
│       │       ├── downloader.ts # Track download & decryption
│       │       └── paths.ts     # File path generation
│       ├── stores/              # Svelte stores (reactive state)
│       │   ├── deezer.ts       # Deezer auth & user state
│       │   └── settings.ts     # App settings state
│       └── types/              # TypeScript type definitions
└── src-tauri/                   # Tauri backend
    ├── capabilities/
    │   └── default.json        # Security permissions
    └── tauri.conf.json         # Tauri configuration

Key Components

UI Components (src/lib/):

  • MenuBar.svelte - Top menu bar
  • TitleBar.svelte - Custom window title bar with controls
  • ToolBar.svelte - Navigation toolbar

Services (src/lib/services/):

  • deezer.ts - Main Deezer API client with cookie-based session management
  • deezer/crypto.ts - MD5, AES-ECB, and Blowfish decryption utilities
  • deezer/downloader.ts - Handles streaming download and chunk-based Blowfish decryption
  • deezer/paths.ts - Generates file paths with template: <albumartist>/<album>/<tracknumber> - <title>

State Management:

  • Uses Tauri's @tauri-apps/plugin-store for persistent storage
  • Svelte stores for reactive state ($state runes in Svelte 5)
  • Deezer authentication stored persistently (ARL token + user data)

Deezer Integration Details

Authentication:

  • Uses ARL (Authentication Resource Locator) cookie from Deezer website
  • Implements manual cookie jar to maintain session across requests
  • CSRF tokens are dynamically refreshed (stored in DeezerAPI.apiToken)

API Architecture:

  • Gateway API (deezer.gw): Used for track metadata via http://www.deezer.com/ajax/gw-light.php
  • Media API: Used for download URLs via https://media.deezer.com/v1/get_url
  • CDN: Actual track downloads from *.dzcdn.net domains

Cookie Management (Critical):

  • Deezer requires both ARL cookie AND session cookies (sid) from API responses
  • DeezerAPI class implements a Map<string, string> cookie store
  • All cookies are sent with every request to maintain session state
  • This is essential because Deezer's CSRF tokens are session-dependent

Decryption Flow:

  1. Download encrypted FLAC/MP3 from CDN
  2. Every 3rd chunk of 2048 bytes is Blowfish-CBC encrypted
  3. Generate Blowfish key from track ID using MD5 XOR algorithm
  4. Decrypt chunks using blowfish-node library (browser-compatible mode)
  5. Write decrypted file to user-selected music folder

Crypto Implementation:

  • md5() - Custom MD5 implementation (pure JS, no Node.js crypto)
  • ecbEncrypt() - AES-128-ECB using @noble/ciphers
  • decryptChunk() - Blowfish-CBC using blowfish-node with Uint8Array (not Buffer)
  • generateBlowfishKey() - Generates key from track ID + secret

Tauri Security Model

Capabilities (src-tauri/capabilities/default.json):

  • HTTP requests are scoped to allowed domains only
  • Required domains: *.deezer.com, media.deezer.com, *.dzcdn.net
  • File system access is scoped (currently ** for user-selected directories)
  • Uses unsafe-headers feature to send custom Cookie headers

Permissions Required:

  • fs:allow-write-file - Write downloaded tracks
  • fs:allow-mkdir - Create artist/album folders
  • fs:allow-remove - Clean up temp files
  • fs:allow-rename - Move from temp to final location
  • http:default with custom scope - Fetch from Deezer APIs/CDN

Common Patterns

Adding New Tauri Permissions:

  1. Edit src-tauri/capabilities/default.json
  2. Add specific permission (e.g., "fs:allow-read-file")
  3. Add scope if needed (e.g., URL patterns for HTTP)
  4. Must restart bun run tauri dev completely (stop and start)

Deezer API Calls:

  • Always use DeezerAPI.apiCall() for GW API endpoints
  • Pass method name and args object
  • Automatic CSRF token refresh with retry logic (max 2 retries)
  • Cookie headers are automatically managed
  • Rate limiting handled automatically (error codes 4, 700 → 5s wait + retry)
  • Network errors (ECONNRESET, ETIMEDOUT, etc.) → 2s wait + retry
  • Download timeout (60s) with automatic retry (max 3 attempts)

Svelte 5 State:

  • Use $state rune for reactive variables
  • Use $derived for computed values
  • Stores use $ prefix in templates (e.g., $deezerAuth.user)

Known Issues & Solutions

Infinite Loop Prevention:

  • Token refresh has retry limit (2 attempts) to prevent infinite loops
  • Chunk decryption errors are sampled (1% logging) to prevent console spam

Error Handling & Resilience:

  • All network operations have automatic retry logic matching deemix behavior
  • API rate limiting detected and handled (5s wait on error codes 4, 700)
  • Network timeouts/errors trigger 2s wait + retry
  • Download timeouts (60s) with up to 3 retry attempts
  • Maintains deemix-compliant behavior to avoid bans

Browser Compatibility:

  • Must use Uint8Array instead of Node.js Buffer
  • Blowfish IV must be Uint8Array, not Buffer
  • @noble/ciphers for AES (browser-compatible)
  • blowfish-node works in browser when using Uint8Array mode

File Path Sanitization:

  • Uses paths.ts to sanitize illegal characters for cross-platform compatibility
  • Handles Windows reserved names (CON, PRN, etc.)
  • Template is hard-coded but can be extended

Testing Deezer Integration

  1. Get ARL Token:

    • Log into deezer.com in browser
    • Open DevTools → Application → Cookies
    • Copy the arl cookie value (192 chars)
  2. Test Track Download:

    • Navigate to Services → Deezer
    • Enter ARL token and login
    • Use track ID 3135556 (Daft Punk - One More Time) for testing
    • Check console for [DEBUG] logs showing API flow
  3. Verify Download:

    • Check that FLAC/MP3 is created in music folder
    • Verify file is playable (decryption worked)
    • Check folder structure matches template

Dependencies Note

  • @noble/ciphers - AES encryption (browser-compatible)
  • blowfish-node - Blowfish decryption (works in browser with Uint8Array)
  • browser-id3-writer - ID3 tag writing
  • music-metadata - Metadata reading
  • @tauri-apps/plugin-* - Tauri official plugins

Do not use Node.js crypto or Buffer - these don't work in browser/Tauri frontend.

UI/Layout Tips

Perfectly Nesting Content Without Page Scroll

When content needs to fit perfectly within a page container without creating scrollbars:

DON'T: Use magic numbers with calc(100vh - ???px) - this requires guessing exact pixel values

DO: Use flexbox with proper height constraints:

.wrapper {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.header {
  flex-shrink: 0; /* Takes natural height */
}

.content {
  flex: 1; /* Fills remaining space */
  display: flex;
  flex-direction: column;
  min-height: 0; /* Allows proper shrinking */
}

.scrollable-area {
  flex: 1;
  overflow-y: auto;
}

This automatically calculates the correct heights without hardcoded values.