Imported from HuuTrucNguyen0508/Kainos-Player (
AGENTS.md). Install upstream withnpx skills add HuuTrucNguyen0508/Kainos-Player. Copyright stays with the author.
Learned User Preferences
- Prefer in-app headless playback on Linux (no separate player window such as Tauon)
- Want in-app local library folder pickers in Settings on desktop and Android, not only env-based roots or MediaStore-only; MediaStore-only is a poor fit when custom SAF folders are intended because it indexes everything
- When launching the desktop app for testing, keep run logs for later debugging; the user tests Android remotely and drops phone screenshots and shared playback logs into the home directory (
~) for the agent to read - Want Now Playing to show audio quality for the current track; expect Spotify tracks as CD-quality 16-bit / 44.1 kHz; in-player artwork should match island/media-session art; shuffle must clearly show active state; with shuffle on, starting from a chosen queue track or wrapping Repeat All should build a fresh shuffle order (not reuse one sticky permutation across short sessions); Back should not sit cramped against artwork
- Want Spotify Connect via developer Client ID and Settings → Connect; prefer in-app librespot decode on Linux and Android (personal-use) without requiring the Spotify app or desktop client (Web Playback / desktop Spotify only as fallback); want Spotify Loud volume normalisation (+3 dB pregain); want disk cache of hearted YouTube audio (priority) and Spotify-via-YouTube mirrors so hearts play without network preload; no protected Spotify DRM offline audio cache; want hearts and a shared music vault to sync over home Wi-Fi between Android and desktop (phone initiates while app open: foreground auto-sync when paired, plus Sync now; no background while app killed; PC hub may listen at login then quit; no cloud/Proton SoT; no per-sync Tailscale toggle)
- Prefer in-app YouTube audio (yt-dlp on desktop, NewPipe-style on Android) instead of only opening the browser
- Want Search to be Spotify + YouTube only (exclude local files), with case-insensitive matching; Library keeps its own search; want an explicit Search autoplay toggle (default off) that can append one continuation batch without faking Spotify Liked shuffle
- Want Library Local files only and Favorites only filters (app hearts) plus Play favorites / Play all; Library text search is display-only (chips define the play queue, not the filtered match list); when not local-only, surface hearted Spotify and YouTube alongside local folders; preserve Library list scroll when returning from a song; maximize list scroll height with Sort/Play all above the list (no row overlap); no sample/placeholder albums or playlists
- Want Settings → Appearance color scheme picker with multiple light/dark schemes, not a single fixed palette; prefer a more atmospheric Home landing than dense Library chrome
- When shipping builds (only when explicitly asked), publish an official GitHub Release (Latest, not prerelease) with both Linux (
kainos-player-linux-*.tar.gz) and Android debug APK assets; copy the APK to repo-rootrelease/and install on the phone over wireless adb when asked - Want Android back / edge-swipe and desktop Escape to dismiss Now Playing / Queue overlays instead of exiting the app, with Spotify-style main-tab history (pop previous destination; leave app only at root Home); prefer collapsing top chrome on downward scroll like a mobile browser; playback should continue
- Want system media controls (Android notification/island/lock screen/Bluetooth and Linux MPRIS/playerctl) to show track metadata and drive the same in-app queue for local, Spotify, and YouTube; island/media controls should stay stable across skips; on desktop, Space always toggles play/pause (not the last-focused control) and mouse-wheel over the volume control adjusts volume
Learned Workspace Facts
- Kainos Player is a Kotlin Multiplatform Compose app (
androidApp,desktopApp,shared) under packagecom.universalmusic.player; storage is JSON/files (not SQLite/Room): desktop~/.universal-music-player/, Android SharedPreferences plus app files; shared Compose UI is Material 3 with multi-scheme Appearance settings; sharedPlayerSessionowns queue, metadata, and transport; shuffle uses a separateshuffleOrder(queue-entry identity for Play next);playQueueIndexand Repeat All wrap reshuffle with the chosen/wrap track first while skip next/prev walk the current order;docs/CURRENT_STATE.mdtracks playback/Spotify/YouTube status - Android test phone (Poco F7 Ultra, HyperOS) is reached over wireless adb:
adb devices -llists_adb-tls-connectentries; install withadb -t <transport_id> install -r release/<apk>(in-place update keeps settings, library folders, hearts and logs) - Desktop runs need JDK 17 (commonly
~/.jdks/temurin-17) via./gradlew :desktopApp:run; installable launcher iskainos-player(Caelestia /.desktopentry) which refreshes:desktopApp:createDistributablethen starts Temurinjavaon the packaged jars (the Compose/jpackage native stub SIGSEGVs here because jlink uses--strip-native-commands); run logs go underlogs/desktop-run-*.log, withlogs/desktop-run-latest.logas the latest symlink - Linux local/HTTP playback uses headless mpv with JSON IPC; Spotify uses Connect plus a Linux librespot receiver named
Kainos Player(Web Playback / desktop Spotify only as fallback); MPRIS viaMprisController(dbus-java) for playerctl; librespot AP traffic (often port 4070) can fail through Tailscale exit nodes or VPNs that block non-HTTP ports - Android URL playback uses Media3/ExoPlayer with a foreground MediaSession bridged to shared
PlayerSession(KainosForwardingPlayer); MediaController/ExoPlayer calls (including the position ticker) must stay on Main and mutations go throughAndroidPlaybackEngine's FIFOmediaCommandsqueue (launchMediaCommand/runMediaCommand), never a barewithContext(Main), sostop()'s clear cannot land after the next track'ssetMediaItem; track changes go throughPlaybackEngine.stopForTransition()(Android pauses ExoPlayer and keeps the item instead ofstop(), because IDLE makes Media3 abandon audio focus and HyperOS can refuse the re-request, which showed up as random pauses at transitions; one guarded focus retry after our ownplay()); local library uses SAF folder picks (persistable tree URIs) viaDocumentsContractcursors (not per-fileDocumentFile), optional MediaStore with dedupe (first SAF folder pick turns MediaStore off), andlocal-library-cache.jsonhydrate; YouTube via NewPipe Extractor; Spotify via librespot-android 0.2.0 (AndroidLibrespotPlaybackHost); aggressive Spotify refresh or librespot state polling can HTTP 429; Keymaster OAuthhttp://127.0.0.1:5588/loginis handled bySpotifyAuthActivity(full-screen WebView, not Compose) for HyperOS; androidApp keepsprotobuf-javaand excludesprotobuf-javaliteso NewPipe and librespot coexist - Playback transition trace: shared
PlaybackTrace(tags Session/Engine/Forwarding/Exo/Session3/Librespot) logs transport decisions, Media3playWhenReadyreasons, suppression, which MediaSession controller package sent a command, and librespot pause/halt events; Android writes logcatKainosTraceplus rotatingAndroid/data/com.universalmusic.player/files/logs/playback-trace.log, shareable from Settings → Advanced → Playback log via aFileProvider; desktop printsTRACElines into the run log - Local library roots:
localMusicFoldersConfigureddistinguishes “use platform default” from “explicit list (may be empty)”; empty explicit list does not reintroduce~/Music. Desktop also honorsKAINOS_MUSIC_DIRS. Paths/URIs persist in settings; Hyprland folder picker uses zenity and must discard stderr. Desktop has no persistent local scan cache (createLocalLibraryScanCache()is null), so every cold start re-walks folders and ffprobes all tracks (Android hydrates fromlocal-library-cache.json); that scan often dominates “slow open” after thekainos-playerGradle gate. Local artwork policy is embedded → sidecar cover/folder.* → MediaStore album art; Android SAF scans never open files, so embedded covers are extracted lazily (LocalEmbeddedArtworkExtractor/MediaMetadataRetriever) intometa-cache/local-art/(.nonemarker = no picture), on first play (PlayerSession.updateCurrentTrackArtwork) and in a background pass after each scan, then written back tolocal-library-cache.json; desktop ffprobe supplies sample rate / bit depth for Now Playing Nyquist and PCM dynamic range - App favorites / remembered / recents persist in versioned
user-library.json(desktop~/.universal-music-player/, Android app files); Spotify-scoped hearts are account-keyed and distinct from Spotify Liked; metadata/artwork cache lives undermeta-cache/with Settings clear-cache; hearted audio files live underaudio-cache/(Settings clear hearted audio cache); never store resolved stream URLs; librespot DRM audio cache stays disabled; Spotify hearts may enqueue a YouTube-matched download (lower priority than YouTube hearts); home-LAN hearts+vault sync uses HTTPS on hub port43822(desktop Netty hub; CIO cannot serve HTTPS) with a self-signed hub cert pinned by SHA-256 inkainos-homesync:2pairing URI copy/paste (Settings → Home sync; cleartext:1rejected; Rotate pairing restarts the hub so the pin matches the live cert); heart-ops merge both ways for YouTube and portablelocalfile:<basename>local hearts (Spotify app hearts are not LAN-mirrored; same Spotify account already covers that); devicelocal:ids stay on-device and rematch by basename after file present; Android SAF locations must decode the document id so basename is the real filename (not the last URI path segment ofprimary:…/file.flac); Sync now plans vault blob copies as pending Transfer/Skip confirms when a side is missing the file (no silent auto-copy); vault defaults to hearted local files only (basename match; Settings toggle for full vault); AES-GCM under TLS; AndroiddataSyncFGS; optionalkainos-player --hub-only+ login autostart (~30 min idle quit); phone→hub is the intended trigger (foreground auto when enabled/paired plus Sync now; not while app killed); desktop JVM hub-client still does hostname checks (Android disables them), so reverse-DNS names missing from hub cert SANs can fail PC-as-client Sync now even when inbound phone sync succeeded; PC Sync now should start the hub first; host firewall on 43822 should use an allow that survives phone DHCP churn (LAN subnet, DHCP-reserved IP with device MAC on home Wi-Fi, or always-on Tailscale node allow), not a one-shot ephemeral phone IP; plan indocs/plans/home-lan-library-sync.md(in-app QR still deferred) - Library has in-app search plus persisted Local files only (
libraryLocalOnly) and Favorites only chips; Favorites filters to app hearts and supports Play favorites on both platforms; Songs content is Column + weighted LazyColumn so Sort/Play all sit above the list (not Box-stacked over rows); empty Library tabs use empty state with no sample album/playlist/artist/track fallbacks - Spotify OAuth redirect is
http://127.0.0.1:43821/callback; putSPOTIFY_CLIENT_IDin gitignoredsecrets.properties; Web Playback needs thestreamingscope; librespot receivers do not; Spotify Loud normalisation uses +3 dB pregain (--normalisation-pregain 3/ AndroidsetNormalisationPregain(3f)); in-app librespot starts at Connect soft-volume 100% (--initial-volume 100/setInitialVolume(65536)) and playback bumps the receiver to 100% so it matches local full-gain paths (librespot’s default 50% log scale is much quieter) - Spotify playlist tracks load via
/v1/playlists/{id}/itemsmappingitem(/trackscan 403 for this Client ID); liked songs stay on/me/tracks; Discover Weekly is blocked on Web API for this Client ID — desktop uses librespot helpertools/librespot-runtime/bin/kainos-discover-weeklyplus Settings → Discover Weekly playlist link (or a library playlist named Discover Weekly), surfaced on Home when available; Android uses librespot-javaApiClient.getPlaylistwith the same Settings link and stored librespot credentials; Gradle:desktopApp:runcwd isdesktopApp/, so find helper binaries under parenttools/librespot-runtime/bin/and~/.local/bin - YouTube search uses the Data API; desktop playback resolves audio with yt-dlp (
KAINOS_YT_DLP,~/.local/bin/yt-dlp, ortools/yt-dlp) into mpv; Android uses NewPipe Extractor; SoundCloud was removed as a provider; Search autoplay usessearchAutoplayEnabled(default false) withSearchAutoplayController/SearchContinuationFetcher; at natural end of a Search-started queue it may append one continuation batch (YouTube search-based when eligible); Spotify/v1/recommendationsis gated for typical post–2024-11-27 Client IDs and is not replaced with Liked Songs shuffle; Discover Weekly is unchanged