RuleStack

Configs

Stacks

Compare

Diff

RuleStack

Configs

Stacks

Compare

Diff

Read API

RuleStack

Configs

Stacks

Compare

Diff

Read API

Configs/CLAUDE.md/maxrave-dev/SimpMusic

CLAUDE.md

CLAUDE.md
CLAUDE.mdroot

Quality

57/100

Scores the file, not the repository.

Length

5,604 words

86 headings · 5 code blocks

Repository

10k

— · pushed 0 days ago

Last changed

today

First indexed 2 days ago.
maxrave-dev/SimpMusic/CLAUDE.mdRawGitHub
1# CLAUDE.md - SimpMusic Project Guide for AI Agents
2 
3## 🌐 Language Rule
4 
5**Response language**: Always respond in **English**, and after each sentence, add a **Vietnamese translation in parentheses**.
6Example: "Hello, how are you? (Xin chào, bạn khỏe không?)"
7 
8This applies to all conversations in this project. The user is using Max plan so token cost is not a concern.
9 
10## 📋 Project Overview
11 
12**SimpMusic** is a FOSS (Free and Open Source Software) YouTube Music client for Android and Desktop, built with Compose Multiplatform.
13 
14### Main Purpose
15- Stream music from YouTube Music and YouTube for free, ad-free, with background playback
16- Provide advanced features like Spotify Canvas, AI song suggestions, synced lyrics
17- Support both Android and Desktop (Windows, macOS, Linux)
18 
19### Basic Information
20- **Package name**: `com.maxrave.simpmusic`
21- **Primary language**: Kotlin
22- **UI Framework**: Jetpack Compose / Compose Multiplatform
23- **Architecture**: Clean Architecture + MVVM
24- **Build system**: Gradle (Kotlin DSL)
25 
26## 🏗️ Architecture
27 
28### Clean Architecture Layers
29 
30```
31┌─────────────────────────────────────┐
32│ Presentation Layer (UI) │
33│ - Jetpack Compose / Compose MP │
34│ - ViewModels (MVVM) │
35│ - UI States │
36├─────────────────────────────────────┤
37│ Domain Layer │
38│ - Use Cases │
39│ - Domain Models │
40│ - Repository Interfaces │
41├─────────────────────────────────────┤
42│ Data Layer │
43│ - Repository Implementations │
44│ - Data Sources (Remote/Local) │
45│ - Database (Room) │
46├─────────────────────────────────────┤
47│ Service Layer │
48│ - YouTube Music Scraper │
49│ - Spotify Service │
50│ - AI Service │
51│ - Lyrics Service │
52│ - Discord RPC (Kizzy) │
53└─────────────────────────────────────┘
54```
55 
56## 📁 Module Structure
57 
58### Root Modules
59 
60#### 1. **composeApp/**
61- **Shared Compose Multiplatform module** - main module containing shared code
62- Supports: Android, Desktop (JVM), iOS (future)
63- Contains all UI (Compose) and business logic
64- Source sets:
65 - `commonMain/`: Shared code for all platforms
66 - `androidMain/`: Android-specific code
67 - `desktopMain/`: Desktop-specific code
68- Can run **Desktop app directly** from this module
69 
70#### 2. **androidApp/**
71- **Android-specific module** to build Android app
72- Depends on `composeApp` as a shared module
73- Contains Android-specific configuration:
74 - AndroidManifest.xml
75 - Android build configuration
76 - Android resources (if needed)
77 - Entry point for Android app
78 
79#### 3. **core/**
80Contains core modules organized by functionality:
81 
82##### **core/common/**
83- Shared utilities
84- Extension functions
85- Constants
86- Helper classes
87 
88##### **core/domain/**
89- Domain models
90- Use cases
91- Repository interfaces
92- Business logic rules
93 
94##### **core/data/**
95- Repository implementations
96- Data sources (Remote & Local)
97- Database schemas (Room)
98- Data mappers
99 
100##### **core/media/**
101- **media3/**: Media3 ExoPlayer integration (includes `CrossfadeExoPlayerAdapter` for DJ-style crossfade on Android)
102- **media3-ui/**: Media3 UI components
103- **media-jvm/**: JVM media playback (libmpv via JNA — replaced VLCJ, which replaced GStreamer post-1.0.4)
104- **media-jvm-ui/**: JVM media UI components
105 
106##### **core/service/**
107Service modules:
108 
109- **kotlinYtmusicScraper/**: YouTube Music API scraper
110- **spotify/**: Spotify Web API integration (Canvas, Lyrics)
111- **aiService/**: AI features (OpenAI, Gemini integration)
112- **lyricsService/**: Lyrics fetching (LRCLIB, SimpMusic Lyrics, BetterLyrics)
113- **kizzy/**: Discord Rich Presence
114- **ktorExt/**: Ktor extensions for networking
115 
116#### 4. **crashlytics/** & **crashlytics-empty/**
117- **crashlytics/**: Full version with Sentry crash reporting
118- **crashlytics-empty/**: FOSS version without tracking
119 
120#### 5. **cast/** & **cast-empty/**
121#### 6. **lastfm/** & **lastfm-empty/**
122- **lastfm/**: direct Last.fm scrobbling for the Full build. KMP (android + jvm + ios), package `org.simpmusic.lastfm`. Signs `api_sig` with okio's MD5; talks to `ws.audioscrobbler.com/2.0/` over form-urlencoded POST
123- **lastfm-empty/**: FOSS no-op stub with the identical public API — `isLastfmAvailable()` returns `false`, which hides the whole settings block. A FOSS build ships no API secret, so it ships no Last.fm code either
124- Selected via `isFullBuild` in `core/data/build.gradle.kts` (playback hooks) and `composeApp/build.gradle.kts` (UI); credentials come from `LASTFM_API_KEY`/`LASTFM_SECRET` in `local.properties` via BuildKonfig, and are handed in with `configLastfm(key, secret)` at startup — the same shape as `configCrashlytics(context, dsn)`
125- Auth is Last.fm's **web flow** on every platform: open `last.fm/api/auth/?api_key=X` with **no token**, the user approves in their own browser, Last.fm redirects to the callback with `?token=`, then `auth.getSession`. The app never sees a password. **Do not switch to the desktop flow** (`auth.getToken` first, then open the same URL with `&token=` on it): that tells Last.fm the app already holds the token, so it renders a "return to the application" page and the callback is never called — which looks exactly like a broken redirect
126- The callback registered on the API account is `wordbyword://lastfm-auth`, handled by an intent-filter on Android and by Conveyor `url-schemes` + `WindowsProtocolRegistrar` on Desktop; the login screen also accepts the callback URL pasted by hand, for hosts where no scheme handler exists
127 
128#### 7. **cast/** & **cast-empty/**
129- **cast/**: Google Cast support for the Full build (`media3-cast` + `play-services-cast-framework`, `CastOptionsProvider`, `CastIconButton` Compose wrapper for `MediaRouteButton`)
130- **cast-empty/**: FOSS no-op stub with identical public API (package `org.simpmusic.cast`), keeping GMS out of F-Droid builds
131- Selected via the `isFullBuild` Gradle property (same pattern as crashlytics) in `core/media/media3/build.gradle.kts` and `composeApp/build.gradle.kts` androidMain
132- Playback handoff lives in `core/media/media3` (`cast/CastHandoffManager.kt` + `cast/CastStreamResolver.kt`): the session player is `CastPlayer.Builder().setLocalPlayer(forwardingPlayer).build()`; while remote, `CrossfadeExoPlayerAdapter` routes transport/getters to the receiver and pushes a resolved-URL queue window (googlevideo URLs resolved up-front via `StreamRepository`); crossfade/EQ/precache are force-disabled while casting
133 
134## 🛠️ Key Technologies
135 
136### Android/Mobile
137- **Jetpack Compose**: Modern UI toolkit
138- **Material Design 3**: Design system
139- **Media3 (ExoPlayer)**: Media playback
140- **Room**: Local database
141- **Coroutines & Flow**: Async programming
142- **Hilt/Koin**: Dependency injection
143 
144### Desktop
145- **Compose for Desktop**: UI
146- **libmpv** (mpv's C client API, bound with JNA): audio + video playback. Replaced VLCJ, which had replaced GStreamer post-1.0.4
147- libmpv natives are bundled per platform via `./gradlew :composeApp:mpvSetupAll` into `mpv-natives/<os>-<arch>/`
148 
149### Networking & APIs
150- **Ktor Client**: HTTP client
151- **Kotlin Serialization**: JSON parsing
152- **YouTube Music hidden API**: Data source
153- **Spotify Web API**: Canvas and lyrics
154- **OpenAI/Gemini API**: AI features
155 
156### Data & Storage
157- **Room Database**: Local persistence
158- **DataStore**: Preferences
159- **Caching**: Offline playback support
160 
161### Third-party Integrations
162- **SponsorBlock**: Skip sponsors
163- **ReturnYouTubeDislike**: Vote information
164- **LRCLIB**: Lyrics provider
165- **BetterLyrics**: Additional lyrics provider (added in v1.0.4)
166- **Sentry**: Crash reporting (Full version only)
167 
168## 📝 Development Guidelines
169 
170### Code Style
171- **Kotlin coding conventions**: Follow Kotlin official guidelines
172- **Compose best practices**: Single source of truth, unidirectional data flow
173- **Clean Architecture**: Strict layer separation, dependency rule
174 
175### Module Dependencies
176```
177UI Layer (composeApp)
178 ↓
179Domain Layer (core/domain)
180 ↓
181Data Layer (core/data)
182 ↓
183Service Layer (core/service/*)
184 ↓
185Common (core/common)
186```
187 
188**Dependency Rule**: Higher layer modules can only depend on lower layer modules, NOT vice versa.
189 
190### Working with UI
191- Use **Jetpack Compose** for all new UI
192- Follow **Material Design 3** guidelines
193- State management with **StateFlow** or **State\<T>**
194- Side effects with **LaunchedEffect**, **DisposableEffect**
195 
196### Working with Data
197- Repository pattern for all data operations
198- Use cases for complex business logic
199- Mapping between Data models ↔ Domain models ↔ UI models
200- Room for local persistence
201- Ktor for network requests
202 
203### Research Before Implementation (MANDATORY)
204 
205Before implementing code, researching code, or answering technical questions, the AI agent **MUST** follow this research workflow:
206 
207#### Step 1: Look up official documentation
208- Use **MCP Context7** (`resolve-library-id` → `query-docs`) to fetch up-to-date documentation for any library/framework about to be used
209- Understand the latest API surface, breaking changes, and recommended usage patterns
210 
211#### Step 2: Evaluate pros, cons, and alternatives
212- Use **WebSearch** to research:
213 - Pros and cons of the library/approach
214 - Alternative libraries or approaches that solve the same problem
215 - Known issues, performance concerns, or deprecation notices
216- Compare and evaluate whether the chosen library/approach is the best fit for this project
217 
218#### Step 3: Study OSS best practices
219- Use **Grep** (on GitHub via web search) or **WebSearch** to find how well-known open-source projects implement similar features
220- Verify the approach follows established best practices before adopting it
221- Pay attention to patterns used in projects with similar architecture (Clean Architecture, Compose Multiplatform, etc.)
222 
223#### Step 4: Make a decision and justify
224- Only proceed with implementation after completing steps 1-3
225- If a library/approach has significant drawbacks or better alternatives exist, recommend the better option to the user before proceeding
226- Document the rationale briefly when introducing new dependencies or patterns
227 
228**This workflow applies to**: Adding new libraries, choosing architectural patterns, implementing new features with unfamiliar APIs, answering "how should we do X?" questions, and evaluating technical approaches.
229 
230**This workflow does NOT apply to**: Simple bug fixes in existing code, minor refactoring, or tasks using libraries already well-established in the project.
231 
232### Verification After Code Changes
233- **Do NOT build the app** to verify code changes. Instead, use **JetBrains MCP** tools (`get_file_problems`, `getDiagnostics`) to check for compile errors and warnings in real-time.
234- Only run Gradle build when explicitly requested by the user or for final release verification.
235 
236### Testing
237- Unit tests for Domain layer (Use cases)
238- Repository tests with fake data sources
239- UI tests with Compose Testing
240 
241## 🎯 Common Tasks
242 
243### 1. Add New UI Feature
244**Location**: `composeApp/src/commonMain/kotlin/`
245- Create Composable function in appropriate package
246- Use ViewModel for state management
247- Follow Material 3 design patterns
248 
249### 2. Add New API Endpoint
250**Location**: `core/service/kotlinYtmusicScraper/`
251- Implement endpoint in corresponding service
252- Create data model for response
253- Map to domain model
254 
255### 3. Add New Database Entity
256**Location**: `core/data/src/main/java/.../database/`
257- Define Entity with Room annotations
258- Create DAO interface
259- Update Database class
260- Create migration if needed
261 
262### 4. Add New Use Case
263**Location**: `core/domain/src/main/java/.../usecase/`
264- Create use case class
265- Inject repository dependencies
266- Implement business logic
267- Return Result/Flow
268 
269### 5. Work with Media Playback
270**Location**: `core/media/media3/` (Android) or `core/media/media-jvm/` (Desktop)
271- Media3/ExoPlayer + CrossfadeExoPlayerAdapter for Android
272- libmpv (MpvPlayerAdapter / MpvPlayer / MpvLibrary) for Desktop
273- Queue management in `core/data/src/.../mediaservice/`
274- Playback controls
275 
276### 6. Add New Lyrics Provider
277**Location**: `core/service/lyricsService/`
278- Implement lyrics fetcher interface
279- Add fallback logic
280- Handle synced/unsynced lyrics
281 
282### 7. AI Features
283**Location**: `core/service/aiService/`
284- OpenAI integration
285- Gemini integration
286- AI lyrics translation
287- Song recommendations
288 
289### 8. Add a New Icon
290 
291**Location**: `composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/icon/`
292 
293All icons are **Material Symbols Rounded** generated as Compose `ImageVector`s. There is no
294`material-icons-extended` dependency and no XML icon drawable — do not add either back.
295 
296**Fetch it from Google's own generator** (it returns a ready `.kt` file, gzipped):
297 
298```bash
299curl -sfL --compressed \
300 &quot;https://fonts.gstatic.com/render/v1/Material+Symbols+Rounded/24dp/&lt;symbol_name&gt;.kt?var=opsz,wght,FILL,GRAD,ROND@24,400,1,0,50&quot; \
301 -o &lt;PascalName&gt;.kt
302```
303 
304Keep the axes identical for every icon so the set stays consistent: **Rounded, opsz 24, wght 400,
305GRAD 0, ROND 50**, `FILL=1`. Use `FILL=0` only for the "off" half of a state pair (e.g.
306`FavoriteBorder`, `AddCircleOutline`, `DownloadForOfflineOutlined`) — otherwise the empty and
307filled states render identically.
308 
309**Then edit the downloaded file:**
3101. `package com.example.test` → `package com.maxrave.simpmusic.ui.icon`
3112. `public val <symbol_name>: ImageVector` → `val SimpIcons.<PascalName>: ImageVector`
3123. Rename the backing field `_<symbol_name>` → `_<PascalName>`, and `name = "<symbol_name>"` → `"<PascalName>"`
3134. For an icon that must flip in RTL, add `autoMirror = true,` to `ImageVector.Builder`
314 
315**Use it:** `SimpIcons.PlayArrow` — plus a per-icon import, `import com.maxrave.simpmusic.ui.icon.PlayArrow`.
316 
317#### Traps that have already cost time here
318 
319- **Each icon needs its own import.** `val SimpIcons.X` is an *extension property*, so importing the
320 `SimpIcons` object alone does not bring it into scope. This is also what lets R8 drop unused icons —
321 do not "simplify" it into a map or a `when`, that would ship all of them.
322- **`ImageVector` is not a `Painter`.** `Icon`/`Image` have overloads for both, but `AsyncImage`
323 (`placeholder`/`error`), anything drawing inside a `DrawScope`, and custom composables typed
324 `Painter` do not — wrap with `rememberVectorPainter(SimpIcons.X)` there.
325- **The response is gzipped** even when the request asks for `identity`; decompress by magic bytes.
326- **Do not replace an icon whose colour carries meaning.** `baseline_downloaded.xml` (`#FF00A0CB`),
327 `baseline_favorite_24.xml` (`#D10000`), `mono.xml`, `monochrome.xml` and the `holder*.png`
328 placeholders stay as resources; a tinted neutral symbol is not equivalent.
329- Verify a name exists before assuming: the Symbols codepoint list is at
330 `google/material-design-icons` → `variablefont/MaterialSymbolsRounded[...].codepoints`. Legacy
331 names like `favorite_border` and `thumb_up_alt` do still exist; `person_add_alt_1` does not.
332 
333## 📍 Important Files and Locations
334 
335### Configuration
336- `build.gradle.kts` (root): Root build configuration
337- `gradle/libs.versions.toml`: Version catalog for dependencies
338- `settings.gradle.kts`: Module inclusion
339 
340### Main Application
341- `composeApp/src/commonMain/kotlin/`: Shared Compose code
342- `composeApp/src/androidMain/kotlin/`: Android-specific code
343- `composeApp/src/desktopMain/kotlin/`: Desktop-specific code
344 
345### Database
346- `core/data/src/main/java/.../database/`: Room database schemas
347- Migrations in Database class
348 
349### Network
350- `core/service/kotlinYtmusicScraper/`: YouTube Music API
351- `core/service/spotify/`: Spotify API
352- `core/service/ktorExt/`: Ktor utilities
353 
354### Resources
355- `composeApp/src/commonMain/composeResources/`: Shared resources
356- `composeApp/src/androidMain/res/`: Android resources
357- Crowdin integration for translations
358 
359## 🔧 Build Variants
360 
361### Android
362- **Full**: With Sentry crash reporting (module: `crashlytics`)
363- **FOSS**: No tracking (module: `crashlytics-empty`)
364 
365### Desktop
366- **Windows**: `.msi` installer
367- **macOS**: `.dmg` (ARM and x86-64)
368- **Linux**: `.AppImage` (DEB and RPM removed post-1.0.4)
369 
370## 🚨 Important Notes
371 
372### Privacy & Data Collection
373- FOSS version: NO tracking
374- Full version: Only Sentry crash reporting
375- "Send back to Google" feature: Optional, only when user enables
376 
377### Platform-specific Considerations
378 
379#### Android
380- Min SDK: Check `androidApp/build.gradle.kts`
381- Target SDK: Latest stable
382- Android Auto support
383- Background playback with MediaSession
384 
385#### Desktop
386- **Required Dependencies**:
387 - libmpv: audio + video playback (bundled via `mpvSetupAll`; falls back to a system-wide libmpv when `mpv-natives/` has not been staged)
388- **Minimum macOS: 15.0** — raised from 11.0 when VLC was replaced by mpv. mpv's macOS release builds target macOS 15 (96/98 arm64 dylibs declare `minos 15.0`; on Intel `libmpv` itself does), and Conveyor rejects a lower `LSMinimumSystemVersion`. No mpv artifact covers both architectures below 15.
389- **Features**:
390 - Deep link support (`simpmusic://` and `simpmusic.org`)
391 - Mini Player window (always-on-top, resizable, draggable)
392 - Crash dialog
393 - Custom title bar (disabled in VM environments)
394- **Limitations**:
395 - No offline playback
396 
397### External APIs
398- YouTube Music: Hidden/unofficial API (may change anytime)
399- Spotify: Requires login for lyrics
400- OpenAI/Gemini: User must provide API key
401- SponsorBlock: Public API
402- LRCLIB: Public lyrics API
403 
404## 🎵 Media Playback Architecture
405 
406### Desktop Player (libmpv — replaced VLCJ 2026-07-27)
407 
408**Location**: `core/media/media-jvm/src/main/java/com/simpmusic/media_jvm/mpv/`
409 
410- `MpvLibrary.kt` — JNA binding for libmpv's C client API, hand-mapped against client API 2.x. Struct layouts are read by raw offset, so a MAJOR client-API bump needs them re-verified
411- `MpvPlayer.kt` — one handle per media item; `vo=libmpv` + software render context
412- `MpvVideoFrameSource.kt` — mpv SW render API → immutable `BufferedImage` snapshots published via `StateFlow`, drawn by plain Compose `Image` (`MpvVideoFrames` in `media-jvm-ui`); replaced the `SwingPanel`-embedded `MpvVideoSurfacePanel` on 2026-08-01
413- `MpvPlayerAdapter.kt` — the `MediaPlayerInterface` implementation; separate YouTube audio/video URLs are merged into ONE source with an `edl://...;!new_stream;...` URL (mpv's equivalent of Android's `MergingMediaSource`)
414- Natives bundled per platform in `mpv-natives/<os>-<arch>/`, staged by `mpvSetupAll` (Linux slice is compiled from source — `scripts/mpv-linux/`)
415- Supports crossfade transition with dual-player approach
416 
417#### Crossfade Transition (Desktop)
418- Configurable duration: 1-15 seconds (default: 5 seconds)
419- Skipped when the NEXT track will play as video (`isVideo()` + watch-video setting on) — same rule as Android since 2026-08-01
420- Settings persisted via DataStore
421 
422### Android Player (Media3/ExoPlayer)
423 
424#### Crossfade & DJ-style Transition (added in v1.0.4)
425 
426**Location**: `core/media/media3/src/main/java/com/maxrave/media3/exoplayer/CrossfadeExoPlayerAdapter.kt`
427 
428- DJ-style crossfade with adjustable duration
429- Requires 320kbps stream preference to enable DJ mode
430- Auto crossfade mode (like AutoMix)
431- `CrossfadeFilterAudioProcessor` for audio processing
432- Edge cases: disabled for video, repeat one, last track
433 
434## 🤝 Contributing
435 
436### Code of Conduct
437See `CODE_OF_CONDUCT.md`
438 
439### Pull Request Guidelines
4401. Fork and create branch from `dev`
4412. Follow coding conventions
4423. Test thoroughly before submitting
4434. Update documentation if needed
4445. PR title: Clear and descriptive
4456. PR description: Explain changes and reasoning
446 
447### Translation
448- Use Crowdin: https://crowdin.com/project/simpmusic
449- Don't edit translation files directly
450 
451## 📚 References
452 
453### Inspiration & Credits
454- **InnerTune**: YouTube Music data extraction inspiration
455- **SmartTube**: YouTube streaming URL extraction
456- **SponsorBlock**: Sponsor skip functionality
457- **LRCLIB**: Lyrics provider
458 
459### External Documentation
460- [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/)
461- [Material Design 3](https://m3.material.io/)
462- [Media3 (ExoPlayer)](https://developer.android.com/guide/topics/media/media3)
463- [Room Database](https://developer.android.com/training/data-storage/room)
464- [Ktor Client](https://ktor.io/docs/client.html)
465- [libmpv client API](https://github.com/mpv-player/mpv/blob/master/include/mpv/client.h)
466- [mpv EDL format](https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst)
467 
468### Community
469- Website: https://simpmusic.org
470- Discord: https://discord.gg/Rq5tWVM9Hg
471- GitHub Issues: Bug reports and feature requests
472 
473---
474 
475## 🎯 Quick Start for AI Agents
476 
477When working with this project:
478 
4791. **Always check layer dependencies**: Don't violate Clean Architecture rules
4802. **Use existing patterns**: Review current code to follow established patterns
4813. **Platform-aware**: Code in `commonMain` must work for both Android and Desktop
4824. **Test thoroughly**: Especially critical for media playback and network code
4835. **Consider privacy**: FOSS version must NOT have tracking
4846. **Check external API stability**: YouTube Music API may change at any time
485 
486### When Encountering Issues
487- Check Discord server for known issues
488- Review recent commits and PRs
489- View dependency graph: `asset/dependencies_graph.svg`
490- Test on both Android and Desktop if code is in commonMain
491 
492### Platform-Specific Code Patterns
493 
494**Example: Desktop-only UI settings**
495```kotlin
496if (getPlatform() == Platform.Desktop) {
497 // Desktop-specific UI or logic
498}
499```
500 
501**Example: Android-only features**
502```kotlin
503if (getPlatform() == Platform.Android) {
504 // Android-specific UI or logic
505}
506```
507 
508## 📜 Changelog Summary (post-1.0.4)
509 
510### Architecture Changes
511- **Desktop: GStreamer → VLCJ**: Completely replaced GStreamer with VLCJ for desktop audio playback
512- **DEB/RPM builds removed**: Desktop Linux now only ships AppImage
513 
514### New Features (v1.0.4)
515- **Android Crossfade & DJ-style transition**: `CrossfadeExoPlayerAdapter` with auto mode (like AutoMix)
516- **BetterLyrics provider**: Additional lyrics source integrated into lyricsService
517- **320kbps audio stream option**: Higher quality streaming preference
518- **Parallel download**: Improved download speed
519- **Character-level animated lyrics**: Word-by-word lyrics with spring animations
520- **SimpMusic Chart**: Chart playlists integrated into Library screen
521- **Favorites**: Liked songs feature with UI integration
522- **Custom OpenAI base URL**: Support for compatible API endpoints
523 
524### New Features (v1.0.1 - v1.0.3)
525- **Desktop Mini Player**: Always-on-top, resizable, draggable mini player window with volume/like controls
526- **Analytics/Local Tracking**: Track top artists, albums, and tracks locally (no remote tracking)
527- **Auto Backup**: Automatic backup settings
528- **Custom Title Bar**: Desktop window control with transparency support
529- **SimpMusic Lyrics voting**: Vote functionality for community lyrics
530 
531### New Features (post-1.0.4, dev branch)
532- **Icons unified on Material Symbols (2026-08-03)**: `material-icons-core`/`material-icons-extended` are gone, and so are the XML icon drawables — every icon is now a generated `ImageVector` under `ui/icon/`, addressed as `SimpIcons.<Name>`. Two migrations fed into this: 59 icons replacing `Icons.*` (117 call sites), then 25 more replacing `painterResource(Res.drawable.baseline_*)` (167 call sites, 44 XML files deleted). `RippleIconButton`, `LiquidGlassIconButton` and `ActionButton` changed from taking `DrawableResource`/`Painter` to `ImageVector`. See **Common Tasks → Add a New Icon** for how to add one and which traps to avoid. Icons whose colour carries meaning (`baseline_downloaded`, `baseline_favorite_24`), the logos (`mono`, `monochrome`) and the `holder*` bitmaps deliberately stay as resources.
533- **Deep link support**: `simpmusic://` and `simpmusic.org` URL schemes
534- **Desktop Crash dialog**: Error reporting UI for desktop
535- **Playback speed/pitch controls**: Redesigned UI with improved animations
536- **VM environment detection**: Disable transparency and custom titlebar in VMs
537- **Google Cast (2026-07, Full build only)**: `cast`/`cast-empty` module pair gated by `isFullBuild`; unified Media3 `CastPlayer` wraps the session `ForwardingPlayer`; `CastHandoffManager` pushes resolved-URL queue windows to the receiver with 403/expiry retry; Cast button in Now Playing top bar, "Playing on <device>" pill, crossfade/DJ/EQ settings gray out while casting; FOSS build stays GMS-free
538- **Windows SMTC (2026-07)**: System Media Transport Controls on Windows via `jmtc`/`nowplayingcenter` 0.0.3 (forked JMTC). The native `SMTCAdapter.dll` was hardened against the 1.0.x crash (Sentry SIMPMUSIC-DESKTOP-7, ~95k events): COM apartment tolerates `RPC_E_CHANGED_MODE`, `MediaPlayer` kept alive process-wide, and every exported call is exception-guarded so nothing crosses the JNA boundary as "Invalid memory access". JMTC is confined to a dedicated thread (off the AWT EDT), and `MediaType.Music` is set before display properties so title/artist render (not just the app name). Enabled in `JvmMediaPlayerHandlerImpl` for `Platform.Windows` (Linux MPRIS unchanged; macOS uses NowPlayingCenter). DLL built by GitHub Actions (`windows-latest`) in the NowPlayingCenter repo.
539- **VLC removed entirely (2026-07-27)**: `VlcPlayerAdapter`, `DefaultVlcDiscoverer`, `MacOsVlcDiscoverer` and `VlcModule` are deleted; `VlcModule.kt` became `DesktopPlayerModule.kt` (`loadVlcModule()` → `loadDesktopPlayerModule()`). The `vlcj` dependency, the `vlc-setup` Gradle plugin, every `vlcSetup*` task, the `vlc-natives/` tree and the VLC Conveyor inputs are all gone. `appResourcesRootDir` now points at `mpv-natives/`. libmpv is the only desktop backend.
540- **Bundled libmpv (2026-07-27)**: two entry points, deliberately split. `:composeApp:mpvBundleAll` runs **on a Mac, once per mpv bump** — it turns upstream mpv builds into loadable slices in `mpv-natives/<os>-<arch>/`, packs them into tarballs and prints their SHA-256. Those are published to `maxrave-dev/simpmusic-files`. `:composeApp:mpvSetupAll` is what **CI** runs: it downloads those tarballs, verifies them against the digests pinned in `mpvNativesChecksums`, and unpacks them — no toolchain needed on the runner. Both workflows must call it before Conveyor, which is invoked by its own action and so never triggers the Gradle `dependsOn`.
541 - Sources: shinchiro `mpv-dev-*.7z` (Windows — the only one shipping a real `libmpv-2.dll`), mpv's own release `.zip` (macOS), and **for Linux a from-source container build** (see below). On macOS **libmpv is statically linked into the `mpv` executable**; that PIE binary exports the full client API and is renamed to `libmpv.dylib`, with load-command paths repointed to `@loader_path`.
542 - Do NOT lift `IINA.app/Contents/Frameworks` instead: IINA 1.4.4 ships a version-skewed pair (libmpv needs `_pl_log_create_349`, bundled `libplacebo.338.dylib` exports `_pl_log_create_338`) and that libmpv fails `dlopen` under both RTLD_NOW and RTLD_LAZY.
543 - **Every `._*` sidecar must be stripped after unpacking** (`mpvSetupAll` does this). Tarring a slice on macOS writes each file's xattrs out as a companion `._name`; Conveyor then signs them as ordinary bundle members and seals them in `_CodeSignature/CodeResources`, but macOS folds `._name` back into the xattrs of `name` and deletes the sidecar the moment Finder touches the app — unzipping it **or** dragging it out of the DMG. The launched bundle is then missing every sidecar the seal expects and Gatekeeper reports "SimpMusic is damaged and can't be opened" (`codesign --strict`: `a sealed resource is missing or invalid`). Only macOS is affected: it alone seals the whole app directory and re-checks it at launch.
544 - `MpvLibrary.bundledLibraryDirs()` resolves the staged folder: `mpv.bundled.path` → `compose.application.resources.dir` → `mpv/` found by walking up from the JAR → `mpv-natives/<os>-<arch>`.
545- **Linux libmpv built from source (2026-07-28)**: the AppImage route is gone — `scripts/mpv-linux/Dockerfile` now compiles libplacebo 7.351 + FFmpeg 7.1.1 + mpv 0.41.0 on **Ubuntu 22.04**, and `mpvSetupLinuxCi` runs that container and copies `/out`. This deleted ~186 lines of DwarFS extraction, closure pruning and rpath rewriting from `composeApp/build.gradle.kts`.
546 - **Why the AppImage could never work**: every prebuilt Linux mpv targets "run mpv as its own process". `mpv-AppImage` ships its own glibc + `ld-linux`, and its "libmpv.so.2" was really the `mpv` **PIE executable** — glibc refuses to `dlopen` a PIE outright (`DF_1_PIE`), and even patched past that, its glibc 2.43 collides with the one the JVM already mapped. It only ever appeared to work on dev machines because JNA silently fell back to a system-wide libmpv. **Always log the resolved path (`NativeLibrary.getInstance(name).file`)** — that is the only thing distinguishing "using the bundle" from "quietly using /usr/lib".
547 - The container build targets glibc **2.34** → runs on Ubuntu 22.04 / Debian 11 and newer. Vulkan/shaderc/glslang/D3D11 are disabled in libplacebo and X11/Wayland/GPU in mpv, since playback goes through the software render API; that also drops `libshaderc`/`libglslang`/`libSPIRV-Tools` (the bulk of the old bundle) and removes libsixel entirely, which had been aborting the JVM.
548 - `stage.sh` deliberately does **not** bundle `libc`/`libm`/`libstdc++`/`ld-linux`, sets `DT_RPATH` (not `DT_RUNPATH` — RUNPATH is not inherited by transitive dependencies), and fails the build unless a `dlopen` + `mpv_initialize` smoke test passes.
549 - mpv built with `-Dlua=disabled` has no `ytdl_hook`, so the `ytdl` option genuinely does not exist there; `MpvPlayer` uses `optionalOption()` to treat `MPV_ERROR_OPTION_NOT_FOUND` as success.
550- **Last.fm scrobbling (2026-07-30, Full build only)**: `lastfm`/`lastfm-empty` module pair gated by `isFullBuild`, following the `cast`/`crashlytics` shape. `LastfmScrobbler` (in `core/data/.../lastfm/`) lives in `commonMain` and is driven by both player handlers, because Android and Desktop run entirely separate ones. It sends `track.updateNowPlaying` where the Discord RPC is updated, and `track.scrobble` off the existing 5-second position-persist tick — a track over 30s scrobbles at half its length or 4 minutes, whichever comes first.
551 - **`status="ok"` does not mean accepted.** Last.fm answers OK while discarding a scrobble and only says so in `ignoredMessage`: code 1 = artist name filtered, 2 = track name filtered, 3/4 = timestamp too far past/future, 5 = daily limit. Codes 1 and 2 are how bad metadata surfaces, so they are logged loudly rather than dropped.
552 - **The two auth flows are not interchangeable, and picking the wrong one silently kills the callback.** Web flow: send the user to `last.fm/api/auth/?api_key=X` with no token; Last.fm mints it and redirects to the registered callback with `?token=`. Desktop flow: call `auth.getToken`, then open that URL with `&token=` already on it; Last.fm then shows "return to the application" and never redirects. SimpMusic uses the **web** flow because it has a registered callback and deep-link handlers on every platform.
553 - **The callback token does NOT travel through navigation.** `App.kt` hands it straight to `SharedViewModel.completeLastfmLogin()`, and `LastfmLoginScreen` closes itself by watching the stored session key. Navigating to the login screen with the token instead pushes a *second* copy on top of the one the user opened their browser from, so the `navigateUp()` after a successful login only peels off that copy and lands back on a login screen — it looks exactly like "logged in but still stuck on the login screen". The other three login screens never hit this because they embed a WebView and never leave the app; Desktop has no real WebView (`Cookies.jvm.kt` is a placeholder), which is why Last.fm uses the system browser at all.
554 - **`toSortedMap()` does not exist in common Kotlin** (it is a JDK collection) — sort the signature parameters with `entries.sortedBy { it.key }`.
555 - **`format` must be excluded from `api_sig`.** Parameters are sorted by name, concatenated `<name><value>`, secret appended, MD5'd — but signing `format` (or `callback`) yields "Invalid method signature supplied" (code 13) on every request.
556 - Error codes worth branching on: `9` invalid session key → clear the stored session and make the user log in again; `11`/`16`/`29` → transient, retryable; everything else is a malformed request.
557 - Two places where Last.fm's own docs contradict themselves, resolved conservatively: `timestamp` is the time the track **started** (the method page says started, the scrobbling guide says finished — every scrobbler in the wild sends the start), and `duration` is **always sent** (optional on one page, required on the other).
558 - Responses are parsed as loose `JsonObject`s, not `@Serializable` classes: Last.fm's JSON is a translation of its XML, so numbers arrive as strings, attributes hide under `@attr`, and a field is an object with one entry but an array with several.
559- **JNA open flags are POSIX-only (2026-07-28)**: `MpvLibrary` passes `OPTION_OPEN_FLAGS = 2` (RTLD_NOW without RTLD_GLOBAL) **only when not on Windows**. JNA forwards the value verbatim to `LoadLibraryEx`, where `2` means `LOAD_LIBRARY_AS_DATAFILE`: the DLL maps as plain data, imports never resolve, and `GetProcAddress` returns nothing — surfacing as the misleading `Error looking up function 'mpv_client_api_version': The specified module could not be found`.
560- **Desktop URL schemes were never actually registered (2026-07-31)**: `url-schemes` belongs at the **top level** of `app` in `conveyor.conf`. Conveyor binds it on `AppConfig`, not on `MacConfig`/`WindowsConfig`/`LinuxConfig` — compare `MacConfigAccess.getUrlSchemes()` (reads `appConfig`) with the `getFileAssociations()` beside it (reads `mac`). It had been written as `mac.url-schemes` / `windows.url-schemes` / `linux.url-schemes` since May 2026; HOCON accepts unknown keys silently, so all three sat inert and **every packaged build shipped with no `CFBundleURLTypes` at all** — macOS never routed `simpmusic://` either, not just the Last.fm callback. Proven by diffing two `mac-app` builds that differed only in where the key was written. The same misplacement had parked `desktop-file.Categories` / `Comment[en]` / `StartupWMClass` *beside* the `"Desktop Entry"` group instead of inside it, so those never reached the generated `.desktop` either. Two more links in the same chain: the argv filter in `runDesktopApp` matched a fixed list (`simpmusic://`, `http://`, `https://`) and therefore discarded `wordbyword://lastfm-auth?token=…` on Windows and Linux — it now matches any `scheme://` — and the AppImage's own `.desktop` (written by `packageConveyorAppImage`, which is what actually reaches users since AppRun installs it into `~/.local/share/applications`) now declares `x-scheme-handler/wordbyword` alongside `simpmusic`.
561- **Bundled glib disabled `java.awt.Desktop` on Linux (2026-07-31)**: `mpv-natives/linux-x64/lib/libglib-2.0.so.0` is glib 2.72 (built on Ubuntu 22.04) and is missing from `SYSTEM_LIBS` in `scripts/mpv-linux/stage.sh`, so it ships in the bundle and claims the glib soname the moment JNA loads libmpv at startup. AWT's `XDesktopPeer.init()` can then no longer dlopen the **system** `libgio-2.0.so.0`: on a glib 2.80 host (Ubuntu 24.04) it dies with `libgobject-2.0.so.0: undefined symbol: g_dir_unref`, and the JDK reports the whole Desktop API unsupported for the rest of the process. All 23 external-link call sites broke at once — `openUrl()` was an `if` with no `else` so it silently did nothing, while Compose's `LocalUriHandler` calls `Desktop.getDesktop()` on its first line and threw `UnsupportedOperationException` straight out of the click handler, crashing the app. Arrived with the from-source Linux mpv build (2026-07-28); before that JNA quietly fell back to a system-wide libmpv, so the system glib was the only one mapped and links worked. Worked around by calling `Desktop.isDesktopSupported()` at the top of `runDesktopApp` — `XDesktopPeer` caches that probe, so running it before libmpv loads lets the system gio/gobject win the soname race. `OpenUrl.jvm.kt` additionally gained a per-OS launcher fallback (`xdg-open` → `gio open` → `$BROWSER`) and a toast, so it can no longer fail in silence. **The actual cure is to stop bundling glib** — add it to `SYSTEM_LIBS`, which needs the Linux tarball rebuilt, republished and re-pinned in `mpvNativesChecksums`.
562 
563- **Crossfade skips video tracks (2026-08-01)**: both `CrossfadeExoPlayerAdapter` (Android) and `MpvPlayerAdapter` (Desktop) skip the crossfade path when the NEXT track will play as video (`isVideo()` + watch-video setting on — the same condition that builds a merged audio+video source). The merged two-URL source is error-prone to prepare mid-fade and used to cut the outgoing song short or jump straight to the video at 0:00; such transitions now take the normal (non-crossfade) path. The Android check for the CURRENT track being video stays removed (commit `9da155d7`).
564- **Desktop video renders through Compose, SwingPanel removed (2026-08-01)**: `MpvVideoSurfacePanel` (JPanel + `SwingPanel` embedding) became `MpvVideoFrameSource` — the mpv SW render loop is unchanged, but finished frames are published as immutable `BufferedImage` snapshots on a `StateFlow` and drawn by a plain Compose `Image` (`MpvVideoFrames` in `media-jvm-ui`, converted with `toComposeImageBitmap()` off the UI thread; the UI reports its size via `setTargetSize()`). This kills the whole SwingPanel bug class: always-on-top z-order, one-frame-late repositioning while scrolling (the flicker that exposed the transparent window), and AWT's single-parent rule that made NowPlaying/Fullscreen/Artist screens fight over the one panel (video "randomly missing until next/prev"). `MpvPlayerAdapter.currentVideoSurface: StateFlow<Component?>` is now `currentVideoFrames: StateFlow<MpvVideoFrameSource?>` and is set unconditionally during crossfade — the old null-guard kept a dead panel from a released player on screen (the "black video" bug).
565- **macOS desktop audio moved to `ao=avfoundation` (2026-08-01)**: `MpvPlayer` now pins `ao` to `"avfoundation,"` when `Platform.isMac()`, because **`ao_coreaudio` leaks a process-wide CoreAudio listener onto a freed `struct ao`** and takes the whole JVM down the next time an audio device appears or disappears — Sentry-visible as `EXC_BAD_ACCESS` on the `HALC_ProxyNotification Call Listener Queue`, reproduced by simply plugging in headphones. Windows (wasapi) and Linux (pulse/pipewire) are untouched.
566 - The chain, all upstream and **still present in mpv master as of 0.41.0**: `ao_coreaudio.c` `init()` registers `AudioObjectAddPropertyListener(kAudioObjectSystemObject, …, hotplug_cb, (void *)ao)` on the *system* object, but its failure label is bare (`coreaudio_error: return CONTROL_ERROR;`). An init that fails any later step (`ca_init_chmap`, `init_audiounit`) therefore leaves the listener registered. `ao.c` then does `goto fail` → `ao_uninit()`, and `buffer.c`'s `ao_uninit()` calls `driver->uninit()` **only when `driver_initialized` is set** — a flag `ao.c` sets only *after* a successful init. So `unregister_hotplug_cb()` never runs while `talloc_free(ao)` does, and the orphaned listener outlives the handle for the rest of the process.
567 - **Why SimpMusic hits this and plain mpv does not**: mpv initialises one ao per session; SimpMusic creates one handle per media item and runs two at once during a crossfade, so a single failed audio init anywhere in a session arms the crash. The crash then waits for an unrelated hotplug event, which is why the process can look healthy for an hour first.
568 - Diagnosing it: the faulting address decodes as ASCII (`0x65636e6174736e49` = "Instance"), the signature of a freed allocation already handed to another object. No thread was tearing an ao down at crash time, which is what ruled out a teardown race and pointed at a listener leaked much earlier.
569 - Accepted trade-offs, neither reproducible in testing on macOS 27: delayed mute (mpv#15014) and audio desync on playback-speed changes (mpv#14483). The trailing comma in `"avfoundation,"` keeps mpv's auto-probe as a fallback, so a failure degrades audio instead of silencing it. Remove the whole workaround once upstream frees the listener on the error path.
570 - Related blind spot, still open: nothing calls `mpv_request_log_messages()`, so libmpv's own warnings (including failed audio init) never surface anywhere.
571 
572## 🔄 CLAUDE.md Auto-Update Rule (MANDATORY)
573 
574After completing any of the following types of changes, the AI agent **MUST** update this CLAUDE.md file:
575 
5761. **Architecture changes**: Module additions/removals, dependency changes (e.g., library swaps like GStreamer → VLCJ), build system changes
5772. **New major features**: New modules, new service integrations, new platform capabilities
5783. **API/Technology migrations**: Swapping core libraries, changing data flow patterns
5794. **Build/CI changes**: New build variants, changed packaging formats, CI workflow changes
5805. **Module structure changes**: Adding/removing modules in settings.gradle.kts
581 
582**What to update**:
583- Relevant sections in this document (Module Structure, Key Technologies, etc.)
584- Add entry to Changelog Summary section with date/version context
585- Update "Last updated" date at the bottom
586 
587**What NOT to update for**:
588- Bug fixes, minor UI tweaks, translation updates
589- Simple refactoring within existing patterns
590- Dependency version bumps without API changes
591 
592---
593 
594*This document helps AI Agents quickly understand the SimpMusic project. Update regularly when there are major changes to architecture or structure.*
595 
596**Last updated**: 2026-08-03
597**Project version**: Check latest release on GitHub
598**Maintained by**: maxrave-dev and contributors
599 

Commands it names

  • ./gradlew :composeApp:mpvSetupAll
  • gradle/libs.versions.toml

Sections

  • CLAUDE.md - SimpMusic Project Guide for AI Agents
  • 🌐 Language Rule
  • 📋 Project Overview
  • Main Purpose
  • Basic Information
  • 🏗️ Architecture
  • Clean Architecture Layers
  • 📁 Module Structure
  • Root Modules
  • 🛠️ Key Technologies
  • Android/Mobile
  • Desktop
  • Networking & APIs
  • Data & Storage
  • Third-party Integrations
  • 📝 Development Guidelines
  • Code Style
  • Module Dependencies
  • Working with UI
  • Working with Data
  • Research Before Implementation (MANDATORY)
  • Verification After Code Changes
  • Testing
  • 🎯 Common Tasks
  • 1. Add New UI Feature
  • 2. Add New API Endpoint
  • 3. Add New Database Entity
  • 4. Add New Use Case
  • 5. Work with Media Playback
  • 6. Add New Lyrics Provider
  • 7. AI Features
  • 8. Add a New Icon
  • 📍 Important Files and Locations
  • Configuration
  • Main Application
  • Database
  • Network
  • Resources
  • 🔧 Build Variants
  • Android
  • Desktop
  • 🚨 Important Notes
  • Privacy & Data Collection
  • Platform-specific Considerations
  • External APIs
  • 🎵 Media Playback Architecture
  • Desktop Player (libmpv — replaced VLCJ 2026-07-27)
  • Android Player (Media3/ExoPlayer)
  • 🤝 Contributing
  • Code of Conduct
  • Pull Request Guidelines
  • Translation
  • 📚 References
  • Inspiration & Credits
  • External Documentation
  • Community
  • 🎯 Quick Start for AI Agents
  • When Encountering Issues
  • Platform-Specific Code Patterns
  • 📜 Changelog Summary (post-1.0.4)

What it covers

buildtestlint-formatcode-stylearchitecturegit-prdependenciesdatabaseapiuiagent-behaviourdocs

Stack — with the evidence

kotlin

(1.00)

java

(0.60)

github-actions

(0.60)

Format

CLAUDE.md

Claude Code's memory file. Shaped like AGENTS.md but with two things it lacks: @path imports, so shared rules live in one place, and a user-scope layer that follows the developer across repos rather than shipping with the code.

What the corpus says about it

Repository

Owner
maxrave-dev
Language
—
License
—
Archived
no

All configs in this repo

Similar configs

Same format, overlapping stack, ranked by quality.

Same format, overlapping stack, ranked by quality
RepositoryFormatStackCoversScoreChanged
livewire/livewireCLAUDE.md · 24kCLAUDE.mdphpvitest+4setupbuildteststyle+4100/1003 days ago
filamentphp/filamentCLAUDE.md · 32kCLAUDE.mdphplaravel+5buildtestlint-formatstyle+7100/1003 days ago
stacklok/toolhiveCLAUDE.md · 2.0kCLAUDE.mdgogithub-actionsbuildteststylearch+4100/1003 days ago
dotCMS/corecore-web/CLAUDE.md · 949CLAUDE.mdjavanode+13teststylearchtesting-strategy+3100/1003 days ago
Adit-Jain-srm/NightmareNetCLAUDE.md · 45CLAUDE.mdtypescriptpython+18buildtestlint-formatstyle+6100/1003 days ago
microsoft/playwrightCLAUDE.md · 94kCLAUDE.mdtypescriptjavascript+10buildtestlint-formatstyle+7100/1003 days ago
nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.4kCLAUDE.mdtypescriptnode+16setupbuildstylearch+2100/1003 days ago
bagisto/bagistoCLAUDE.md · 28kCLAUDE.mdphplaravel+8setupbuildteststyle+5100/1003 days ago
RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack