CLAUDE.md
CLAUDE.mdCLAUDE.mdroot
Quality
57/100
Scores the file, not the repository.Length
5,604 words
86 headings · 5 code blocksRepository
10k
— · pushed 0 days agoLast changed
today
First indexed 2 days ago.1# CLAUDE.md - SimpMusic Project Guide for AI Agents23## 🌐 Language Rule45**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?)"78This applies to all conversations in this project. The user is using Max plan so token cost is not a concern.910## 📋 Project Overview1112**SimpMusic** is a FOSS (Free and Open Source Software) YouTube Music client for Android and Desktop, built with Compose Multiplatform.1314### Main Purpose15- Stream music from YouTube Music and YouTube for free, ad-free, with background playback16- Provide advanced features like Spotify Canvas, AI song suggestions, synced lyrics17- Support both Android and Desktop (Windows, macOS, Linux)1819### Basic Information20- **Package name**: `com.maxrave.simpmusic`21- **Primary language**: Kotlin22- **UI Framework**: Jetpack Compose / Compose Multiplatform23- **Architecture**: Clean Architecture + MVVM24- **Build system**: Gradle (Kotlin DSL)2526## 🏗️ Architecture2728### Clean Architecture Layers2930```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```5556## 📁 Module Structure5758### Root Modules5960#### 1. **composeApp/**61- **Shared Compose Multiplatform module** - main module containing shared code62- Supports: Android, Desktop (JVM), iOS (future)63- Contains all UI (Compose) and business logic64- Source sets:65 - `commonMain/`: Shared code for all platforms66 - `androidMain/`: Android-specific code67 - `desktopMain/`: Desktop-specific code68- Can run **Desktop app directly** from this module6970#### 2. **androidApp/**71- **Android-specific module** to build Android app72- Depends on `composeApp` as a shared module73- Contains Android-specific configuration:74 - AndroidManifest.xml75 - Android build configuration76 - Android resources (if needed)77 - Entry point for Android app7879#### 3. **core/**80Contains core modules organized by functionality:8182##### **core/common/**83- Shared utilities84- Extension functions85- Constants86- Helper classes8788##### **core/domain/**89- Domain models90- Use cases91- Repository interfaces92- Business logic rules9394##### **core/data/**95- Repository implementations96- Data sources (Remote & Local)97- Database schemas (Room)98- Data mappers99100##### **core/media/**101- **media3/**: Media3 ExoPlayer integration (includes `CrossfadeExoPlayerAdapter` for DJ-style crossfade on Android)102- **media3-ui/**: Media3 UI components103- **media-jvm/**: JVM media playback (libmpv via JNA — replaced VLCJ, which replaced GStreamer post-1.0.4)104- **media-jvm-ui/**: JVM media UI components105106##### **core/service/**107Service modules:108109- **kotlinYtmusicScraper/**: YouTube Music API scraper110- **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 Presence114- **ktorExt/**: Ktor extensions for networking115116#### 4. **crashlytics/** & **crashlytics-empty/**117- **crashlytics/**: Full version with Sentry crash reporting118- **crashlytics-empty/**: FOSS version without tracking119120#### 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 POST123- **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 either124- 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 redirect126- 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 exists127128#### 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 builds131- Selected via the `isFullBuild` Gradle property (same pattern as crashlytics) in `core/media/media3/build.gradle.kts` and `composeApp/build.gradle.kts` androidMain132- 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 casting133134## 🛠️ Key Technologies135136### Android/Mobile137- **Jetpack Compose**: Modern UI toolkit138- **Material Design 3**: Design system139- **Media3 (ExoPlayer)**: Media playback140- **Room**: Local database141- **Coroutines & Flow**: Async programming142- **Hilt/Koin**: Dependency injection143144### Desktop145- **Compose for Desktop**: UI146- **libmpv** (mpv's C client API, bound with JNA): audio + video playback. Replaced VLCJ, which had replaced GStreamer post-1.0.4147- libmpv natives are bundled per platform via `./gradlew :composeApp:mpvSetupAll` into `mpv-natives/<os>-<arch>/`148149### Networking & APIs150- **Ktor Client**: HTTP client151- **Kotlin Serialization**: JSON parsing152- **YouTube Music hidden API**: Data source153- **Spotify Web API**: Canvas and lyrics154- **OpenAI/Gemini API**: AI features155156### Data & Storage157- **Room Database**: Local persistence158- **DataStore**: Preferences159- **Caching**: Offline playback support160161### Third-party Integrations162- **SponsorBlock**: Skip sponsors163- **ReturnYouTubeDislike**: Vote information164- **LRCLIB**: Lyrics provider165- **BetterLyrics**: Additional lyrics provider (added in v1.0.4)166- **Sentry**: Crash reporting (Full version only)167168## 📝 Development Guidelines169170### Code Style171- **Kotlin coding conventions**: Follow Kotlin official guidelines172- **Compose best practices**: Single source of truth, unidirectional data flow173- **Clean Architecture**: Strict layer separation, dependency rule174175### Module Dependencies176```177UI Layer (composeApp)178 ↓179Domain Layer (core/domain)180 ↓181Data Layer (core/data)182 ↓183Service Layer (core/service/*)184 ↓185Common (core/common)186```187188**Dependency Rule**: Higher layer modules can only depend on lower layer modules, NOT vice versa.189190### Working with UI191- Use **Jetpack Compose** for all new UI192- Follow **Material Design 3** guidelines193- State management with **StateFlow** or **State\<T>**194- Side effects with **LaunchedEffect**, **DisposableEffect**195196### Working with Data197- Repository pattern for all data operations198- Use cases for complex business logic199- Mapping between Data models ↔ Domain models ↔ UI models200- Room for local persistence201- Ktor for network requests202203### Research Before Implementation (MANDATORY)204205Before implementing code, researching code, or answering technical questions, the AI agent **MUST** follow this research workflow:206207#### Step 1: Look up official documentation208- Use **MCP Context7** (`resolve-library-id` → `query-docs`) to fetch up-to-date documentation for any library/framework about to be used209- Understand the latest API surface, breaking changes, and recommended usage patterns210211#### Step 2: Evaluate pros, cons, and alternatives212- Use **WebSearch** to research:213 - Pros and cons of the library/approach214 - Alternative libraries or approaches that solve the same problem215 - Known issues, performance concerns, or deprecation notices216- Compare and evaluate whether the chosen library/approach is the best fit for this project217218#### Step 3: Study OSS best practices219- Use **Grep** (on GitHub via web search) or **WebSearch** to find how well-known open-source projects implement similar features220- Verify the approach follows established best practices before adopting it221- Pay attention to patterns used in projects with similar architecture (Clean Architecture, Compose Multiplatform, etc.)222223#### Step 4: Make a decision and justify224- Only proceed with implementation after completing steps 1-3225- If a library/approach has significant drawbacks or better alternatives exist, recommend the better option to the user before proceeding226- Document the rationale briefly when introducing new dependencies or patterns227228**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.229230**This workflow does NOT apply to**: Simple bug fixes in existing code, minor refactoring, or tasks using libraries already well-established in the project.231232### Verification After Code Changes233- **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.235236### Testing237- Unit tests for Domain layer (Use cases)238- Repository tests with fake data sources239- UI tests with Compose Testing240241## 🎯 Common Tasks242243### 1. Add New UI Feature244**Location**: `composeApp/src/commonMain/kotlin/`245- Create Composable function in appropriate package246- Use ViewModel for state management247- Follow Material 3 design patterns248249### 2. Add New API Endpoint250**Location**: `core/service/kotlinYtmusicScraper/`251- Implement endpoint in corresponding service252- Create data model for response253- Map to domain model254255### 3. Add New Database Entity256**Location**: `core/data/src/main/java/.../database/`257- Define Entity with Room annotations258- Create DAO interface259- Update Database class260- Create migration if needed261262### 4. Add New Use Case263**Location**: `core/domain/src/main/java/.../usecase/`264- Create use case class265- Inject repository dependencies266- Implement business logic267- Return Result/Flow268269### 5. Work with Media Playback270**Location**: `core/media/media3/` (Android) or `core/media/media-jvm/` (Desktop)271- Media3/ExoPlayer + CrossfadeExoPlayerAdapter for Android272- libmpv (MpvPlayerAdapter / MpvPlayer / MpvLibrary) for Desktop273- Queue management in `core/data/src/.../mediaservice/`274- Playback controls275276### 6. Add New Lyrics Provider277**Location**: `core/service/lyricsService/`278- Implement lyrics fetcher interface279- Add fallback logic280- Handle synced/unsynced lyrics281282### 7. AI Features283**Location**: `core/service/aiService/`284- OpenAI integration285- Gemini integration286- AI lyrics translation287- Song recommendations288289### 8. Add a New Icon290291**Location**: `composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/icon/`292293All icons are **Material Symbols Rounded** generated as Compose `ImageVector`s. There is no294`material-icons-extended` dependency and no XML icon drawable — do not add either back.295296**Fetch it from Google's own generator** (it returns a ready `.kt` file, gzipped):297298```bash299curl -sfL --compressed \300 "https://fonts.gstatic.com/render/v1/Material+Symbols+Rounded/24dp/<symbol_name>.kt?var=opsz,wght,FILL,GRAD,ROND@24,400,1,0,50" \301 -o <PascalName>.kt302```303304Keep 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 and307filled states render identically.308309**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`314315**Use it:** `SimpIcons.PlayArrow` — plus a per-icon import, `import com.maxrave.simpmusic.ui.icon.PlayArrow`.316317#### Traps that have already cost time here318319- **Each icon needs its own import.** `val SimpIcons.X` is an *extension property*, so importing the320 `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 typed324 `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 at330 `google/material-design-icons` → `variablefont/MaterialSymbolsRounded[...].codepoints`. Legacy331 names like `favorite_border` and `thumb_up_alt` do still exist; `person_add_alt_1` does not.332333## 📍 Important Files and Locations334335### Configuration336- `build.gradle.kts` (root): Root build configuration337- `gradle/libs.versions.toml`: Version catalog for dependencies338- `settings.gradle.kts`: Module inclusion339340### Main Application341- `composeApp/src/commonMain/kotlin/`: Shared Compose code342- `composeApp/src/androidMain/kotlin/`: Android-specific code343- `composeApp/src/desktopMain/kotlin/`: Desktop-specific code344345### Database346- `core/data/src/main/java/.../database/`: Room database schemas347- Migrations in Database class348349### Network350- `core/service/kotlinYtmusicScraper/`: YouTube Music API351- `core/service/spotify/`: Spotify API352- `core/service/ktorExt/`: Ktor utilities353354### Resources355- `composeApp/src/commonMain/composeResources/`: Shared resources356- `composeApp/src/androidMain/res/`: Android resources357- Crowdin integration for translations358359## 🔧 Build Variants360361### Android362- **Full**: With Sentry crash reporting (module: `crashlytics`)363- **FOSS**: No tracking (module: `crashlytics-empty`)364365### Desktop366- **Windows**: `.msi` installer367- **macOS**: `.dmg` (ARM and x86-64)368- **Linux**: `.AppImage` (DEB and RPM removed post-1.0.4)369370## 🚨 Important Notes371372### Privacy & Data Collection373- FOSS version: NO tracking374- Full version: Only Sentry crash reporting375- "Send back to Google" feature: Optional, only when user enables376377### Platform-specific Considerations378379#### Android380- Min SDK: Check `androidApp/build.gradle.kts`381- Target SDK: Latest stable382- Android Auto support383- Background playback with MediaSession384385#### Desktop386- **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 dialog393 - Custom title bar (disabled in VM environments)394- **Limitations**:395 - No offline playback396397### External APIs398- YouTube Music: Hidden/unofficial API (may change anytime)399- Spotify: Requires login for lyrics400- OpenAI/Gemini: User must provide API key401- SponsorBlock: Public API402- LRCLIB: Public lyrics API403404## 🎵 Media Playback Architecture405406### Desktop Player (libmpv — replaced VLCJ 2026-07-27)407408**Location**: `core/media/media-jvm/src/main/java/com/simpmusic/media_jvm/mpv/`409410- `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-verified411- `MpvPlayer.kt` — one handle per media item; `vo=libmpv` + software render context412- `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-01413- `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 approach416417#### 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-01420- Settings persisted via DataStore421422### Android Player (Media3/ExoPlayer)423424#### Crossfade & DJ-style Transition (added in v1.0.4)425426**Location**: `core/media/media3/src/main/java/com/maxrave/media3/exoplayer/CrossfadeExoPlayerAdapter.kt`427428- DJ-style crossfade with adjustable duration429- Requires 320kbps stream preference to enable DJ mode430- Auto crossfade mode (like AutoMix)431- `CrossfadeFilterAudioProcessor` for audio processing432- Edge cases: disabled for video, repeat one, last track433434## 🤝 Contributing435436### Code of Conduct437See `CODE_OF_CONDUCT.md`438439### Pull Request Guidelines4401. Fork and create branch from `dev`4412. Follow coding conventions4423. Test thoroughly before submitting4434. Update documentation if needed4445. PR title: Clear and descriptive4456. PR description: Explain changes and reasoning446447### Translation448- Use Crowdin: https://crowdin.com/project/simpmusic449- Don't edit translation files directly450451## 📚 References452453### Inspiration & Credits454- **InnerTune**: YouTube Music data extraction inspiration455- **SmartTube**: YouTube streaming URL extraction456- **SponsorBlock**: Sponsor skip functionality457- **LRCLIB**: Lyrics provider458459### External Documentation460- [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)467468### Community469- Website: https://simpmusic.org470- Discord: https://discord.gg/Rq5tWVM9Hg471- GitHub Issues: Bug reports and feature requests472473---474475## 🎯 Quick Start for AI Agents476477When working with this project:4784791. **Always check layer dependencies**: Don't violate Clean Architecture rules4802. **Use existing patterns**: Review current code to follow established patterns4813. **Platform-aware**: Code in `commonMain` must work for both Android and Desktop4824. **Test thoroughly**: Especially critical for media playback and network code4835. **Consider privacy**: FOSS version must NOT have tracking4846. **Check external API stability**: YouTube Music API may change at any time485486### When Encountering Issues487- Check Discord server for known issues488- Review recent commits and PRs489- View dependency graph: `asset/dependencies_graph.svg`490- Test on both Android and Desktop if code is in commonMain491492### Platform-Specific Code Patterns493494**Example: Desktop-only UI settings**495```kotlin496if (getPlatform() == Platform.Desktop) {497 // Desktop-specific UI or logic498}499```500501**Example: Android-only features**502```kotlin503if (getPlatform() == Platform.Android) {504 // Android-specific UI or logic505}506```507508## 📜 Changelog Summary (post-1.0.4)509510### Architecture Changes511- **Desktop: GStreamer → VLCJ**: Completely replaced GStreamer with VLCJ for desktop audio playback512- **DEB/RPM builds removed**: Desktop Linux now only ships AppImage513514### 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 lyricsService517- **320kbps audio stream option**: Higher quality streaming preference518- **Parallel download**: Improved download speed519- **Character-level animated lyrics**: Word-by-word lyrics with spring animations520- **SimpMusic Chart**: Chart playlists integrated into Library screen521- **Favorites**: Liked songs feature with UI integration522- **Custom OpenAI base URL**: Support for compatible API endpoints523524### New Features (v1.0.1 - v1.0.3)525- **Desktop Mini Player**: Always-on-top, resizable, draggable mini player window with volume/like controls526- **Analytics/Local Tracking**: Track top artists, albums, and tracks locally (no remote tracking)527- **Auto Backup**: Automatic backup settings528- **Custom Title Bar**: Desktop window control with transparency support529- **SimpMusic Lyrics voting**: Vote functionality for community lyrics530531### 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 schemes534- **Desktop Crash dialog**: Error reporting UI for desktop535- **Playback speed/pitch controls**: Redesigned UI with improved animations536- **VM environment detection**: Disable transparency and custom titlebar in VMs537- **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-free538- **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`.562563- **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.571572## 🔄 CLAUDE.md Auto-Update Rule (MANDATORY)573574After completing any of the following types of changes, the AI agent **MUST** update this CLAUDE.md file:5755761. **Architecture changes**: Module additions/removals, dependency changes (e.g., library swaps like GStreamer → VLCJ), build system changes5772. **New major features**: New modules, new service integrations, new platform capabilities5783. **API/Technology migrations**: Swapping core libraries, changing data flow patterns5794. **Build/CI changes**: New build variants, changed packaging formats, CI workflow changes5805. **Module structure changes**: Adding/removing modules in settings.gradle.kts581582**What to update**:583- Relevant sections in this document (Module Structure, Key Technologies, etc.)584- Add entry to Changelog Summary section with date/version context585- Update "Last updated" date at the bottom586587**What NOT to update for**:588- Bug fixes, minor UI tweaks, translation updates589- Simple refactoring within existing patterns590- Dependency version bumps without API changes591592---593594*This document helps AI Agents quickly understand the SimpMusic project. Update regularly when there are major changes to architecture or structure.*595596**Last updated**: 2026-08-03597**Project version**: Check latest release on GitHub598**Maintained by**: maxrave-dev and contributors599
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| livewire/livewireCLAUDE.md · 24k | CLAUDE.md | setupbuildteststyle+4 | 100/100 | 3 days ago | |
| filamentphp/filamentCLAUDE.md · 32k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 3 days ago | |
| stacklok/toolhiveCLAUDE.md · 2.0k | CLAUDE.md | buildteststylearch+4 | 100/100 | 3 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 3 days ago | |
| Adit-Jain-srm/NightmareNetCLAUDE.md · 45 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 3 days ago | |
| microsoft/playwrightCLAUDE.md · 94k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 3 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.4k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 3 days ago | |
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 3 days ago |
