Thank you for contributing to NeriPlayer. This document describes the current Android client and Listen Together Worker implementation. Keep documentation aligned with the source code and runtime behavior.
- NeriPlayer is a native Android audio player, not a public cloud music service.
- Online source capabilities mainly come from NetEase Cloud Music, Bilibili, and YouTube Music.
- Playback metadata and lyrics completion currently use NetEase + QQ Music, with LRCLIB available as an external lyrics source.
- Data is local by default. GitHub / WebDAV sync is optional and syncs metadata such as playlists, favorites, recent plays, and playback stats, not media files.
- The Listen Together server lives in
np-submodule/NeriPlayer-LTWand is based on Cloudflare Workers and Durable Objects.
When maintaining docs, split them by audience:
README.md/README_EN.md- For users and new contributors: project scope, feature boundaries, installation/builds, sync, and privacy.
CONTRIBUTING.md/CONTRIBUTING_EN.md- For developers: module boundaries, extension paths, tests, and PR expectations.
app/src/main/cpp/README.md- Defines the alternative-license scope for NeriPlayer-owned native source, third-party exclusions, and the explicit dual-license statement required for external contributions to enter that scope.
app/src/main/cpp/tests/usb/config/host-gate-contract.md- Defines the public native USB host gate, CI coverage, and real-device boundary.
app/src/main/cpp/tests/usb/corpus/README.mdandapp/src/main/cpp/tests/usb/fixtures/README.md- Define the synthetic/auditable boundary for public USB test corpus and fixtures; device-derived evidence stays in the private evidence tree.
np-submodule/NeriPlayer-LTW/README.md- For Listen Together server deployers: Worker API, event model, deployment, and local checks.
If a behavior change affects user understanding, update the README. If it affects extension paths, tests, or module boundaries, update CONTRIBUTING.
- Android Studio: latest stable version
- JDK: 17
- Kotlin: 2.4.10, JVM target 17
- AGP: 9.2.1
- Gradle: 9.4.1
- compileSdk / targetSdk / minSdk: 37 / 36 / 28
- NDK:
27.0.12077973 - CMake:
3.28.0+ - Node.js: 20, for Listen Together Worker checks
- Version name format:
<git_short_hash>.<MMddHHmm> - Release APK filename:
NeriPlayer-<versionName>[-abi].apk
Additional notes:
- The repository uses Git submodules. Clone with
--recursive, or rungit submodule update --init --recursive. - The build script reads the Git short commit hash to generate the version name, so Git must be installed locally.
- Dependency versions are managed by
gradle/libs.versions.tomland modulebuild.gradle.ktsfiles. - Only
zhandenresources are kept in the app, via the locale filter inbuild-logic.
NeriPlayer covers a broad product surface. Protect these paths first:
- Playback:
PlayerManager, playback resolution, cache, URL refresh, playback fallback, long-form progress memory, BilibiliSponsorBlock skip policy, state recovery, loudness normalization, channel balance, high-resolution output, the USB-exclusive native path, startup watchdogs, and foreground/background health audits. - Downloads:
AudioDownloadManager,GlobalDownloadManager,DownloadTaskStore,DownloadLifecyclePolicies,ManagedDownloadStorage, resume checkpoints, sidecar files, queue recovery, cancellation cleanup, and SAF migration. - Sync: GitHub / WebDAV three-way merge, deletion records, playback stats, missing-field snapshot cleanup, JSON/ProtoBuf/Base64 compatibility, and WebDAV concurrency protection.
- Local data: atomic playlist JSON writes, local metadata hydration, config import/export, encrypted auth storage, and DataStore settings.
- Lyrics and Now Playing UI:
AdvancedLyricsView,SyncedLyricsView,LyricShareSheet, phonetic lyric display, Japanese lyric translation spacing, long-press lyric sharing, and the full-screen Lyrics page. - Navigation and glass UI:
MainTabLayerHost, drawer/coherent detail feedback, interruptible main-tab switching, page-state retention, standardized Snackbar overlays, and Advanced Glass owner handoff. - System entry points and desktop shell:
LauncherShortcuts, home-screen widgets,USB_DEVICE_ATTACHEDhandling, and playback-service control entry points. - Storage and cache UI:
StorageUsageAnalyzer, cache cleanup options, download directory indexes, and SAF snapshots. - Listen Together: Android client, Worker protocol fields, roles, queues, version-gated updates, session-candidate sharing toggles, and controller-offline recovery.
- Diagnostics: safe mode, JVM/native crash logs, ANR capture, and Debug probes.
- Local persistence: debounced playback/traffic-stat writes, lifecycle flushes, atomic file replacement, and local-playlist/SAF initialization readiness.
Related tests live under app/src/test/ and app/src/androidTest/.
When changing these areas, search for neighboring tests first, then add coverage
for the new behavior.
- Clone the repository:
git clone --recursive https://github.com/cwuom/NeriPlayer.git cd NeriPlayer - Build the Debug APK:
./gradlew :app:assembleDebug
- Install it onto a device:
adb install -r app/build/outputs/apk/debug/app-debug.apk
- First launch enters the disclaimer and startup onboarding flow. Notification and local-music permissions are explained first and requested only after the user chooses them; either can be skipped.
- For debugging access, tap the version number 7 times in Settings. A
standalone
Debugtab will appear in the bottom navigation bar.
Release builds enable minification and resource shrinking by default.
A normal assembleRelease builds arm64-v8a only. Multi-ABI output requires an
extra Gradle property.
-
Provide signing config in
~/.gradle/gradle.properties, project Gradle properties, or through command-line-Pproperties:KEYSTORE_FILE=/absolute/path/to/neri.jks KEYSTORE_PASSWORD=your_store_password KEY_ALIAS=key0 KEY_PASSWORD=your_key_password
If
KEYSTORE_FILEis relative, it is resolved against theapp/module directory. The current Release build does not fall back to the debug signing config. Local Android Studio / IntelliJ builds automatically allow unsigned Release packaging so IDEBuild APK/assembleflows keep working. CLI and CI builds still require a usable keystore by default. GitHub PR builds automatically produce an unsigned Release for packaging validation. Other CI/PR environments can pass-PallowUnsignedRelease=true. -
Build the default Release APK:
./gradlew :app:assembleRelease
-
Build multi-ABI Release APKs:
./gradlew :app:assembleRelease -PbuildAllReleaseAbis=true
-
Artifacts are generated in
app/build/outputs/apk/release/:NeriPlayer-<git_short_hash>.<MMddHHmm>[-abi].apk
Security reminders:
- Never commit keystores, passwords, cookies, tokens, or other sensitive data.
- Do not paste full authorization data in Issues or PRs.
- Full config export files contain platform auth and sync credentials. Do not attach them publicly.
:app- Main Android application.
:ksp-annotations/:ksp-processor- KSP-generated settings schema, keys, backup allowlists, and UI metadata.
:accompanist-lyrics-core/:accompanist-lyrics-ui- Lyrics parsing and Compose lyrics UI submodules.
build-logic- Android/Kotlin/Compose convention plugins.
buildSrc- Retained auxiliary Gradle build logic.
np-submodule/NeriPlayer-LTW- Listen Together Cloudflare Workers server.
np-submodule/miuix- Vendored upstream Miuix source/docs tree, not part of the current app module graph.
-
app/src/main/java/moe/ouom/neriplayer/NeriPlayerApplication.kt- Application initialization. Handles language, crash handling,
AppContainer, Lyricon, global downloads, and the shared image loader.
- Application initialization. Handles language, crash handling,
-
app/src/main/java/moe/ouom/neriplayer/activity/MainActivity.kt: the only external entry point. Handles safe mode, startup, disclaimer, onboarding, external audio imports, Listen Together deep links, and the top-level Compose host.- Platform login activities live under
activity/auth/and run in dedicated secondary processes.activity/sync/stores Activity-side sync warning state. UsbDeviceAttachHandling.ktenables or disables the USB attach Activity alias from settings; the playback service reuses the same policy forUSB_DEVICE_ATTACHEDbroadcasts.NeteaseWebLoginActivity.kt,NeteaseQrLoginActivity.kt,BiliWebLoginActivity.kt,BiliQrLoginActivity.kt, andYouTubeWebLoginActivity.kt: internal platform sign-in pages.
-
app/src/main/java/moe/ouom/neriplayer/ui/NeriApp.kt- Top-level Compose app shell. Handles
NavHost, dynamic bottom bar,MiniPlayer,Now Playingoverlay, Debug routes, themes, cache cleanup, and playback service sync. MainTabLayerHost.ktretains outgoing and incoming main-tab scenes, performs interruptible directional transitions, and preserves saveable state plus a glass owner for each scene.ui/feedback/owns app-wide Snackbar/Toast feedback policy. Before adding a new global feedback surface, checkAppFeedbackandViewSnackbarfirst.
- Top-level Compose app shell. Handles
-
app/src/main/java/moe/ouom/neriplayer/ui/component/lyrics/AdvancedLyricsView.ktandSyncedLyricsView.kt: advanced lyric layout, word/character highlighting, translation/phonetic display, click-to-seek, and long-press callbacks.LyricShareSheet.kt: lyric-line selection, copy, song sharing, and lyric card generation.- The old
AppleMusicLyricname exists only as an@Deprecatedwrapper inui/component/LyricsCompatibility.kt. New code should useSyncedLyricsView.
-
app/src/main/java/moe/ouom/neriplayer/ui/component/playback/NeriMiniPlayer.kt: bottom Mini Player, play/pause, and horizontal swipe for previous/next. Playback sound and sleep-timer sheets also live here.- Same-named files in the
ui/component/root are primarily legacy package compatibility entry points. New implementations belong in responsibility-based subpackages such aslyrics/,playback/,download/, andnavigation/.
-
app/src/main/java/moe/ouom/neriplayer/ui/screen/tab/LibraryScreen.kt: top-level Library categories. Local content can switch between playlists and artists, and Favorites can show playlists and followed artists.LocalArtistLibraryGrid.kt: local artist grid, empty state, and artist cards.
-
app/src/main/java/moe/ouom/neriplayer/ui/screen/playlist/LocalArtistDetailScreen.kt: local artist details with play-all, multi-select, playlist export, and batch download for resolvable online songs.
-
app/src/main/java/moe/ouom/neriplayer/ui/screen/artist/- NetEase artist detail screens for artist info, hot songs, paged albums, and follow state.
-
app/src/main/java/moe/ouom/neriplayer/ui/viewmodel/artist/- NetEase artist summaries, JSON parsing, and detail-screen state management.
-
app/src/main/java/moe/ouom/neriplayer/ui/onboarding/- First-run onboarding for language, platform accounts, permission guidance, playback controls, GitHub sync, and personalization.
-
app/src/main/java/moe/ouom/neriplayer/core/api/netease/: NetEase endpoints, crypto, and account capabilities.bili/: Bilibili search, QR login, favorites, collections, playback info, and audio playback resolution. Explore link recognition preserves Bilibili selected parts,cid, andseason_idcontext; changes should check bothExploreLinkRecognizerandExploreViewModel.youtube/: YouTube Music client based on NewPipe Extractor, home/playlist/search/playback, PoToken, and JS Challenge support.search/: playback metadata/lyrics completion APIs. Current implementations:CloudMusicSearchApiandQQMusicSearchApi.lyrics/: external lyrics sources. Current implementation:LrcLibClient.
-
app/src/main/java/moe/ouom/neriplayer/core/player/PlayerManager.kt: unified Media3 ExoPlayer management, playback resolution, queue, cache, state recovery, retry, and playback policy.service/AudioPlayerService.kt: foreground playback service, media notification, MediaSession, and media button handling.download/AudioDownloadManager.kt: resolves platform playback and saves downloads;DownloadParallelism.ktdefines concurrency boundaries.effects/PlaybackEffectsController.kt: speed, pitch, loudness enhancer, and equalizer.engine/: Media3 audio processors, including loudness normalization, channel balance, and high-resolution output processing.playback/PlaybackStatsTracker.kt: playback stats tracking. Playback commands and queue advancement live inplayback/PlayerManagerPlaybackExtensions.kt.timer/SleepTimerManager.kt: sleep timer.engine/datasource/ConditionalHttpDataSourceFactory.kt: adds platform-specific request headers.watchdog/PlayerManagerStartupWatchdogExtensions.ktandlifecycle/PlayerManagerLifecycleExtensions.kt: playback startup watchdogs, foreground/background health audits, failure recovery, and USB-exclusive fallback handling.resolver/netease/PlayerManagerNeteaseAutoSourceSwitch.kt: Bilibili fallback for NetEase tracks that are restricted, have no playable result, or only return previews.resolver/youtube/YouTubeGoogleVideoRangeSupport.kt,YouTubeSeekRefreshPolicy.kt, andprefetch/YouTubePrefetchRunner.kt: YouTube Music playback compatibility policies.metadata/: lyrics, metadata, and external Bluetooth lyrics handling.model/: player-specific state models. Cross-layer song models do not live here. Shuffle display state is represented byPlayerQueueDisplayState; avoid returning to implicit index-remapping semantics for a shuffled queue.usb/: split intodevice/,path/,session/,sink/,system/, andtransport/for USB-exclusive sessions, the native bridge, runtime snapshots, and recovery controls. The current implementation covers UAC1.0 and compatible UAC2.0 Type I PCM devices, with 32-bit PCM, software PCM-float conversion, UAC2 explicit feedback, coordinated AudioSink reconfiguration, dynamic transfer scaling, and backpressure stall recovery.
-
app/src/main/java/moe/ouom/neriplayer/core/download/GlobalDownloadManager.kt: global download tasks and downloaded song list.ManagedDownloadStorage.ktis the facade for app-managed and SAF storage. Implementation details are split acrossstorage/commit/,delete/,lookup/,migration/,recovery/,snapshot/,tree/, andworking/.task/DownloadTaskStore.kt: persisted download tasks, status, progress, and attempt IDs.policy/DownloadLifecyclePolicies.kt: recovery, cancellation cleanup, and fast-settle policies.naming/ManagedDownloadNaming.kt: filename templates and legacy filename compatibility.metadata/DownloadedAudioTagWriter.kt: audio tag writing;catalog/owns downloaded-song catalog models.
-
app/src/main/java/moe/ouom/neriplayer/core/startup/- Startup stages and decisions are split across
app/,crash/,download/,logging/,permission/,player/,safemode/,sync/, andtheme/.MainActivitycoordinates these components with the UI lifecycle.
- Startup stages and decisions are split across
-
app/src/main/java/moe/ouom/neriplayer/data/model/: sharedSongItem,SongIdentity, and media model extensions used by playback, playlists, downloads, sync, Listen Together, and UI.settings/:DataStoresettings, KSP schema, bootstrap snapshot, theme snapshot, and playback preference snapshot.auth/: NetEase, Bilibili, and YouTube cookie/auth storage and validation.platform/netease/: NetEase platform-side caches, currently including playlist detail cache.storage/: storage usage analysis, cache grouping, and extra cache cleanup.local/playlist/: local playlist JSON atomic writes, system playlist compatibility, background metadata hydration, and local artist aggregation models.local/audioimport/,local/media/: local audio import, fast scans, background metadata hydration, cover fallback resolution, and sharing.playlist/favorite/,playlist/usage/: favorite playlists, followed artists, and Home continue-listening data.history/,stats/: recent plays, playback stats, and day/week/month/year/all-time aggregation.backup/: playlist JSON backup/import and diff analysis.config/: full app config import/export.sync/model/: payload and conflict models shared by GitHub and WebDAV.sync/: provider-neutral coordination, preferences, and cover mapping.sync/github/: GitHub transport, three-way merge, serialization, Data Saver, and secure storage.sync/webdav/: WebDAV sync, remote config, Worker, and WebDAV API.
-
app/src/main/java/moe/ouom/neriplayer/listentogether/protocol/defines room, event, and transport models;network/owns HTTP/WebSocket and reconnect behavior;playback/owns queues, authoritative stream links, and position sync.control/,session/,invite/,mapping/, andvalidation/own their corresponding policies and boundaries.- The root retains
ListenTogetherSessionManager.ktand a few compatibility entry points. New protocol logic should not keep accumulating in the root package.
-
app/src/main/cpp/- Native crash handling lives under
crash/. USB code is split acrossusb/exclusive/,usb/feedback/,usb/iso/,usb/pcm/,usb/uac1/, andusb/uac2/, with matching host tests undertests/usb/.
- Native crash handling lives under
-
app/src/main/java/moe/ouom/neriplayer/core/lyricon/- Lyricon integration and SuperLyric output for current song, playback state, position, word-level lyrics, and translations.
-
app/src/main/java/moe/ouom/neriplayer/navigation/LauncherShortcuts.ktmaps app-icon shortcuts to navigation or playback requests.
-
app/src/main/java/moe/ouom/neriplayer/widget/- Home-screen playback widget providers, state snapshots, artwork-derived visuals, and RemoteViews updates.
Exploreis NetEase curated playlists + YouTube Music playlists + platform-specific NetEase/Bilibili/YouTube Music search. It is not mixed search.Homeshows local continue-listening, all available NetEase recommendation sources, and Radar playlists in the default Chinese mode. Refreshing updates each section. International mode prioritizes YouTube Music home shelves.- The QQ Music entry in
Libraryis still a placeholder and does not represent full platform integration. - Local artist categories are aggregated from imported/saved local songs by display artist. They are not an online artist directory.
- NetEase artist detail pages depend on NetEase artist metadata and endpoints; follow state is saved into the local Favorites category.
Bilibilisupports search, favorites, audio playback, and downloads, but is not a full video discovery or comments client. Link recognition supports selected parts, collection shares, andseason_idcontext, but this is still not a full Bilibili client.YouTube Musicsupports login, anonymous playback, home/playlist browsing, details, search, playback, and downloads. Valid identity cookies are preserved and rotated when needed; bootstrap,player.js, PoToken, and challenge-result caches are reused; signature or playback-candidate failures can fall through to EJS/HLS. Cache hits and local tests are not proof of real-account or network stability. Large seeks in long audio use an expedited startup-recovery window and still need real-network qualification.- Status-bar lyrics depend on private vendor support and only work on select devices.
- The RuntimeShader fluid/audio-reactive background is enabled only on Android 13+. Cover blur and advanced blur require Android 12+, so animation changes must preserve the fallback path for older versions.
- Phonetic lyric display depends on phonetic lyrics returned by the platform or phonetic fields embedded in word-level lyrics. When no phonetic data exists, do not synthesize it or render an empty second line.
- Lyric sharing uses
FileProviderto share lyric card files from the app cache. These generated share files are cleanable cache, not user-downloaded content. - Lyricon/SuperLyric position updates use an independent 200 ms feed anchored to elapsed realtime. Changes to playback progress intervals must preserve foreground and background lyric timing.
- NetEase playback tries lower qualities when the current quality is unavailable. For restricted, missing-URL, or preview-only tracks, it can auto-match a Bilibili or local-audio fallback source only when the user enables it; both fallback settings are disabled by default.
- NetEase playlist detail cache is only for playlist detail fast display and failure fallback. Album details still refresh live and should not reuse playlist cache.
- Local "My Favorite Music" can sync recognizable NetEase songs to NetEase Liked Songs. This requires NetEase login and skips unsupported or existing songs.
- Downloads use a shared
OkHttpClientand write to the app directory or a SAF directory. They are not handled by the systemDownloadManager, but they do support automatic resume and startup recovery. - Download queues, cancellation records, and attempt IDs all participate in recovery decisions. When changing recovery, make sure stale requests cannot clear task state for newer requests.
- Resume behavior depends on transport type:
- direct downloads resume through working-file size plus
Range - platform-specific explicit chunked downloads resume by byte offset
- HLS downloads resume from a saved segment checkpoint in
.hls.json
- direct downloads resume through working-file size plus
- Working files live under
cache/download_staging/and also keep.resume.jsonmetadata so unfinished downloads can be reconstructed after app restart or network recovery. - Manual cancellation rolls back partial artifacts and removes working files. Partial data is preserved only for network-policy pauses and recoverable retry paths.
- When the audio body is complete, tag-writing or an unwritable SAF handle still finalizes the download without tags; the completed audio must not be deleted.
- The app-private download directory is usually faster than custom SAF directories. SAF snapshots and indexes reduce directory walking, and an empty scan must not overwrite an existing index. SAF access should not be treated as having the same cost as normal file IO.
- Local audio sharing exposes a controlled URI directly when possible; content URIs that cannot be shared directly are copied to cache staging, which is cleanable.
- Storage cleanup must only delete regenerable cache, download staging, and share staging. Do not delete user-saved audio, downloaded lyrics/covers, or auth data through normal cache cleanup.
- Streaming cache and permanent downloads are separate features: cache uses
SimpleCache; downloads are written byAudioDownloadManagerandManagedDownloadStorage. - GitHub / WebDAV sync only sync metadata. Audio caches, downloaded files, local media files, cookies, and playback tokens are not synced.
- Local and synced playlists use
songOrderVersionto distinguish order semantics:0is the legacy order and1is the current display order. Older data must be migrated compatibly instead of being interpreted directly as the new order. - Sync snapshots may come from older JSON/ProtoBuf payloads or malformed remote
files. Use safe defaults, filter records without resolvable track identity,
valid deletion time, or valid playlist id, and never let songs with missing
addedAtsort ahead of songs that already have timestamps. - Data Saver writes raw
GZIP(ProtoBuf)bytes tobackup-raw.bin; normal mode writesbackup.json. Reads must also accept historicalbackup.binBase64. Do not switch formats on one client before Android and Desktop both support read-both. JSON, compressed, and decompressed payloads are capped at 8 MiB, 12 MiB, and 16 MiB. - GitHub sync writes through Git Data API blob/tree/commit calls and advances the branch with a non-force ref update. Base64 in blob requests is only the API transport envelope; raw content reads and repository bodies remain binary. Do not reintroduce the deprecated asset-manifest transport.
- WebDAV prefers ETag/Last-Modified conditional writes. Without condition tokens, only an unchanged remote SHA-256 fingerprint permits an unconditional retry; otherwise return a concurrency conflict.
- Playback and traffic statistics use delayed batch writes. Playback stats flush at important player/activity lifecycle points, while traffic accumulators flush when a request or download attempt ends; playback daily buckets are bounded by retention window and count. Sync merging must preserve aggregate totals, daily buckets, and legacy bucket-only lifting; do not trim a visible window before lifting totals.
- Platform cookies/auth data, GitHub tokens, and WebDAV passwords are encrypted
with
Android Keystore + EncryptedSharedPreferences. DataStorestores regular settings and non-sensitive state, not platform login credentials.- Long-form progress memory applies only to tracks at least 15 minutes long. Positions
below 5 seconds are ignored, positions within 30 seconds of the end are cleared,
explicit playback positions win, and the persisted field is
resumePositionMs. This is not third-party playback history. - BilibiliSponsorBlock is disabled by default. It sends only a SHA-256 prefix of the current BV ID and performs skips locally; it stays disabled during Listen Together, and its public results must not enter sync or room state.
- 32-bit high-resolution system output preserves the high-precision pipeline on regular Android system output and bypasses loudness normalization, channel balance, audio visualization, and in-app speed processing. Check settings copy and tests together when changing it.
- USB exclusive playback depends on a compatible UAC1.0 or UAC2.0 Type I PCM DAC, the foreground service, wake locks, and the system background policy. The in-app background-permission prompt is not decorative, so screen-off behavior must stay in scope.
- USB settings include bit-perfect volume mode: software gain remains at 0 dB and the DAC hardware controls volume. Do not treat it as ordinary app/system volume.
- USB attach handling is a separate setting. When disabled, both the Activity alias
and playback-service broadcast entry must skip
USB_DEVICE_ATTACHED; do not only hide the setting or change one entry point. - If a foreground/background USB runtime report returns
native_refresh_deferred, the player retries it only within a bounded budget; other invalid reports remain fail-closed. - Local scan results may return quick metadata first and then hydrate richer title/artist/album/cover data in the background. Do not assume the first scan result is the final local metadata shape.
- When
shareAudioLinks=falsein Listen Together, room snapshots and queue items must not exposestreamUrl. Turning the setting off must also clear any cached shared candidates immediately, andREQUEST_LINKmust be rejected. - Listen Together repeat/shuffle changes use
PLAYBACK_MODE/REQUEST_PLAYBACK_MODE. Member controls must validate the target stable track key, reject olderclientInstanceId/clientSequence/clientTimeMsevents, and limitREQUEST_SET_TRACKto the current queue. - The current Listen Together track keeps at most three deduplicated HTTP(S) candidates. Listeners resolve their own quality policy first; candidates are only a session-scoped fallback and must never be written to normal song or offline caches. Server position is projected from track duration, with single-track repeat wrapping by duration; rejoining with the same member credential must not trigger new-member auto-pause.
Use this when integrating a new platform into Explore search or discovery.
- Implement a client or repository under
core/api/. - Add request, pagination, and state mapping in
ExploreViewModel. - Add platform tabs and result UI in
ExploreScreen/ host screens. - If playback is needed, connect the platform to
PlayerManagerplayback resolution. - If downloads are needed, complete
AudioDownloadManagerand download metadata mapping.
Use this for cover, lyrics, and track metadata completion, not for Explore.
- Implement a new
SearchApiundercore/api/search/. - Register the singleton in
AppContainer. - Add routing, matching, and fallback logic in
SearchManager. - Add
MusicPlatform, string resources, and debug probes as needed.
- Use
bili/oryoutube/as a reference for client and playback repository design. - Extend
core/player/engine/datasource/ConditionalHttpDataSourceFactory.ktif special headers are needed. - Add the platform under
core/player/url/and its matchingresolver/path. - Keep downloads, lyrics, covers, and stats separated from transient playback cache.
- If NetEase Liked Songs sync should support the new source, provide stable
NetEase song IDs or a verified mapping and reuse candidate validation in
LocalPlaylistRepository.
- The entry point is the NetEase URL resolution flow in
core/player/url/PlayerManagerUrlExtensions.kt. - Matching and scoring live in
core/player/resolver/netease/PlayerManagerNeteaseAutoSourceSwitch.kt. - Playback fallback is only for restricted, missing-URL, or preview-only NetEase playback. Do not turn it into cross-platform aggregate search.
- When changing matching, consider title, artist, video pages, duration tolerance, and cache key stability.
- Prefer registering keys, defaults, types, and UI metadata in
data/settings/AutoSettingsSchema.kt. - Simple switches can use the generated
AutoSettingsRepositoryandAutoSettingsSwitchItems. - Settings with side effects, mutual exclusion, permissions, or startup snapshot requirements should keep a handwritten setter.
- If a setting affects early startup behavior, update the corresponding snapshot:
BootstrapSettingsSnapshot,ThemePreferenceSnapshot, orPlaybackPreferenceSnapshot. - UI usually belongs in the matching
SettingsPageinSettingsScreen.ktor underui/screen/tab/settings/component/. - When adding or renaming a setting, update localized strings,
SettingsSearchIndex.ktkeywords, Settings page visibility/filtering tests, andAutoSettingsGeneratedTest. - If a setting controls an Activity alias, playback service, system entry point, or pre-startup behavior, verify that generated keys, handwritten setters, startup snapshots, and the actual entry point all read the same preference.
- For complex preference models, prefer extracting normalization, bounds, and layout math into unit-testable functions instead of keeping the behavior implicit inside Compose.
- Explore search history is stored by
ExploreSearchHistoryRepository. Whenexplore_search_history_enabledis off, Explore must hide the history and stop new records without silently deleting existing entries on toggle. - Lyrics font size is now split between cover and lyrics pages, with separate
lyric and translation scales. When touching related UI, update
SettingsRepository.lyricFontScalesFlow,setLyricFontScale(target, scale), and the matching preview/playback call sites together.
- Read
core/player/usb/sink/UsbExclusiveAudioSink.kt,core/player/usb/transport/,core/player/usb/session/,core/player/policy/usb/UsbAudioSinkReconfigurationCoordinator.kt,core/player/watchdog/PlayerManagerStartupWatchdogExtensions.kt,core/player/lifecycle/PlayerManagerLifecycleExtensions.kt, and related tests first. - The current USB-exclusive implementation supports UAC1.0 and compatible UAC2.0 Type I PCM devices. If support expands to more complex UAC2.0 topologies or non-Type-I PCM devices, update the docs, boundaries, diagnostics, and compatibility assumptions together.
- Consider device selection, sample-rate/bit-depth policies, 32-bit PCM, software PCM-float conversion, UAC2 clock topology, explicit-feedback endpoints, foreground/background buffers, wake locks, background-permission prompts, and the system-fallback path together. Implicit feedback is not a supported candidate yet.
- When changing automatic recovery, keep-alive logic, or background audits, validate foreground playback, screen-off background playback, USB attach/detach, and Android system fallback paths.
- When changing feedback clocks, long-gap reacquisition, coordinated
reconfiguration, dynamic transfer scaling, backpressure recovery, or candidate
bit-depth fallback, also check
UsbExclusiveOutputFormatResolverTest,UsbExclusivePcmWritePlannerTest,UsbExclusiveSessionControllerReusePolicyTest,UsbAudioSinkReconfigurationCoordinatorTest, and native USB feedback/PCM/UAC tests. - Runtime Report v2 parsing must remain fail-closed. When changing feedback endpoint, state, holdover, recovery-action, or generation fields, update the Kotlin parser and boundary tests together.
- If error semantics or recovery behavior changes, update the Settings / Debug diagnostics surfaces and the matching tests.
- If USB attach behavior changes, check
UsbDeviceAttachHandling.kt,AudioPlayerService.kt, the Activity alias inAndroidManifest.xml, andAutoSettingsSchema.ktsetting generation together. - Native changes should run all three host gates plus the four-ABI Android compile. Host models, ABI compilation, and real-DAC validation are separate gates.
- Understand
data/sync/model/SyncDataModels.ktanddata/sync/github/SyncDataSerializer.ktcompatibility first. Shared payload models must not move back into the GitHub provider package. - Sync data includes playlists, favorite playlists, recent plays, deletion records,
and playback stats. Data Saver writes raw
GZIP(ProtoBuf)tobackup-raw.bin, normal mode writesbackup.json, and the reader must also accept historicalbackup.binBase64. GitHub Git Data API blob requests use Base64 only as a transport envelope; the repository body remains raw binary. songOrderVersion=0represents legacy order, whilesongOrderVersion=1represents current display order. Serialization, merging, and local restoration must preserve the migration path for older data.- Playlist membership uses
syncMembershipTokens/removedMembershipTokensfor observed-remove semantics. New fields must remain readable when legacy JSON or ProtoBuf payloads omit them; tokenized membership must not fall back to a timestamp-only deletion decision. Deletion undo, backup restore, and cross-device sync should be tested together so stale deletion records cannot remove restored membership again. - Missing-field or malformed snapshots must be cleaned before merging.
SyncSongneeds at least one of id, audioId, or mediaUri; deletion records also need a valid deletion time; songs with missingaddedAtare low-priority display items. CoverUrlMapper.ktlives in provider-neutraldata/sync/. Most merge logic lives inGitHubSyncManager.kt; WebDAV reuses the same data model and much of the merge behavior.- Do not break the delayed sync, periodic sync, validated-network checks, or retry
behavior in
GitHubSyncWorker.kt/WebDavSyncWorker.kt. GitHub writes must use the remote branch head and a non-force update, failing on conflicts rather than overwriting. Large-file reads should use the raw content path, and WebDAV writes without ETag/Last-Modified must revalidate the remote fingerprint before an unconditional retry. - Sensitive data must go through
SecureTokenStorage.ktorWebDavStorage.kt. Do not store it inDataStoreor plaintext JSON.
- Read
ManagedDownloadStorage.kt,naming/ManagedDownloadNaming.kt,task/DownloadTaskStore.kt,policy/DownloadLifecyclePolicies.kt, and related unit tests first. - Consider app-managed storage, SAF custom directories, migration, legacy names,
metadata files, and
.nomedia. - Download tasks write to
cache/download_staging/before being committed to the final directory..resume.jsonand.hls.jsonare part of resume recovery and should not be treated as disposable temp files. - Default download concurrency is 6, configurable from 1-8.
When changing concurrency, retry, or network recovery, check
DownloadParallelism.kt,AudioDownloadManager.kt, andGlobalDownloadManager.kt. - Changes to migration, delete semantics, resume checkpoints, or sidecar writes must update or add unit tests.
- Now Playing lyrics mainly live in
ui/component/lyrics/AdvancedLyricsView.kt,ui/component/lyrics/SyncedLyricsView.kt, andNowPlayingScreen.kt. - The full-screen Lyrics page lives in
LyricsScreen.kt, and lyric sharing reusesLyricShareSheet.kt. - Phonetic display is controlled by the
lyric_translation_use_phoneticsetting, requires lyric translation to be enabled, and only works when the current lyrics include phonetic data. - Japanese lyric translation spacing must distinguish kana from ordinary CJK text.
When changing it, check both app-side
resolveLyricTranslationExtraGapand the:accompanist-lyrics-uisubmodule'sresolveJapaneseLyricTranslationTopPadding. - Long-press opens the lyric sharing sheet. When changing gestures, also check click-to-seek, manual lyric offset, and advanced lyric viewport scrolling.
- Lyric cards are shared through
FileProvidercache files. If the output location changes, updatefile_paths.xmland cache cleanup behavior as well.
- Entry points are
data/storage/StorageUsageAnalyzer.ktandSettingsStorageCacheSection.kt. - When adding a cache directory, decide whether it belongs to cleanable cache, downloaded content, diagnostics, or app data.
- Cleanup actions must target regenerable content only. Downloaded songs, downloaded lyrics, download indexes, and auth data must not be removed by normal cache cleanup.
- When clearing download staging, respect current download task state. Staging files for active tasks should wait until the task ends.
- The cache entry point is
NeteasePlaylistCacheRepository.kt; page state is inNeteaseCollectionDetailViewModel.kt. - The cache signature is based on track count and recent track IDs, mainly to decide whether the track list can be reused.
- Network or parse failures may fall back to cache, but manual refresh should keep force-refresh semantics.
- Album details do not use this playlist cache, so keep the data models separated.
- The integration entry point is
core/lyricon/LyriconManager.kt. - The setting key is
lyricon_enabled, and playback lifecycle keeps it in sync. - Lyrics use
LyricEntry; word-level data comes fromWordTiming, and translations are matched to original lines by timestamp tolerance. - Lyricon/SuperLyric position updates use an independent 200 ms feed anchored to elapsed realtime; preserve foreground/background timing when changing progress.
- Keep Lyricon, SuperLyric, status-bar lyrics, advanced Now Playing lyrics, and external Bluetooth lyrics compatible when changing lyric structures.
- Original and translated Bluetooth lyrics are independent switches. When both are active, update title/artist through one atomic snapshot and keep tests for track identity, field bounds, whitespace normalization, and duplicate suppression.
- Android client logic is under
listentogether/. - Server logic is under
np-submodule/NeriPlayer-LTW. - Protocol field changes must stay compatible across the Android client and Worker, and tests must be updated.
- When
shareAudioLinks=false, HTTP and WebSocket room snapshots must not exposetrack.streamUrlorqueue[*].streamUrl, and turning the setting off must clear any cached shared candidates immediately. REQUEST_LINK/LINK_READY, member control, controller-offline recovery, and version-gated updates must be reviewed together so older state cannot overwrite newer room state.- Repeat/shuffle mode uses
PLAYBACK_MODE/REQUEST_PLAYBACK_MODE. Member requests andLINK_READYmust validate the target stable track key so async results cannot land on the wrong track. - First joins require
joinSecret; member reconnects requirememberSecret. Never persist either secret in sanitized room state or logs, and treat invite URIsecretvalues as sensitive input. - Control events may carry
clientInstanceId,clientSequence, andclientTimeMs. The Worker filters stale order, andREQUEST_SET_TRACKcan only select an item already in the current queue. Keep the Android compatibility path aligned. - Explicit member departure uses
/api/rooms/:roomId/leave; the Worker removes the member and broadcastsMEMBER_LEFT. A normal WebSocket close is transport churn and must preserve the credential-bound member for reconnects; an explicit controller departure closes the room. - Treat the 6-character room ID, 1-24 character nickname, queue limit 2000, and request de-duplication as protocol boundaries, not just UI validation details.
- Settings support custom server URLs and availability tests. Do not hard-code a single server.
- The production main-tab path is shared by
NeriApp.ktandMainTabLayerHost.kt; tab order and direction come fromresolveMainTabTransitionDirection. - Keep both outgoing and incoming scenes alive during transitions and preserve
per-tab state with
SaveableStateHolder. Rapid reverse or repeated requests must not clear the old scene before settlement. - Each scene owns a separate
MainTabGlassOwner. Advanced Glass changes must keep only visible owners in composition while detail-page handoff remains owned by the navigation layer. coherent_feedback_enableddefaults to off. Detail pages use a drawer-style foreground rise over a slightly recessed background until coherent feedback is explicitly enabled.- Cover forward, reverse, interrupted, repeated-request, state-restoration, and
startup-first-frame behavior. Geometry tests must not treat an unlaid-out
Rect(0, 0, 0, 0)scene as a real overlap. - At minimum, check
NeriAppMainTabTransitionPolicyTest,AdvancedGlassNavigationTransitionTest,NeriAppNavigationTransitionTest, andHostNavigationTransitionGeometryTest.
- Widget entry points live in
widget/PlaybackWidgetProviders.kt; state and color extraction live inPlaybackWidgetState.ktandPlaybackWidgetVisuals.kt. - RemoteViews layouts have both regular and API 31 variants. Visual, clipping,
or preview changes should keep
layout/,layout-v31/,xml/, andxml-v31/aligned. - Playback widget actions end in
AudioPlayerService. New actions must check foreground-service start policy, MediaSession refreshes, and empty/buffering feedback behavior. - Launcher shortcuts are mapped by
navigation/LauncherShortcuts.kt. New shortcuts should updateres/xml/shortcuts.xml, localized strings, andLauncherShortcutsTest. - Prefer
AppFeedback/ViewSnackbarfor global Snackbar behavior. Avoid page-local Snackbar hosts that cannot surface above app overlays. - Batch playlist export, deletion undo, and sync deletion records are linked.
Changes in any of these areas should also check
PlaylistExportSheetTest,AppFeedbackPolicyTest,LocalPlaylistRepositoryTest, andSyncPlaylistDeletionPolicyTest.
- Enable Developer Mode by tapping the version number 7 times in Settings.
- A standalone
Debugtab appears after enabling it. - Regular file logging is enabled only in Developer Mode.
- Crash logs are written independently by
ExceptionHandler/NativeCrashHandler. - The Debug tab contains YouTube, Bili, NetEase, Search, and Listen Together probes, plus regular log and crash log viewers.
Common command:
adb logcat | findstr NeriPlayerLinux / macOS:
adb logcat | grep NeriPlayerBefore submitting, consider at least these checks:
- Debug build:
./gradlew :app:assembleDebug
- Unit tests:
./gradlew :app:testDebugUnitTest
- If you changed auth-dependent flows, playback resolution, or other integration-heavy
behavior, optional smoke tests are available:
./gradlew :app:testDebugUnitTest -DrunNeteaseSmoke=true ./gradlew :app:testDebugUnitTest \ -DrunYouTubePlaybackSmoke=true \ -DyoutubeSmokeVideoId=<id> \ [-DyoutubeSmokeForceRefresh=true] \ [-DyoutubeSmokeCookieFile=/absolute/path/to/cookies.json]
- If you changed resources, UI, navigation, settings, sync, or storage logic:
./gradlew :app:lintDebug
- If you changed Compose UI, permissions, Activity, or login flows:
./gradlew :app:connectedDebugAndroidTest
- If you changed native USB code, match the dedicated Android Native CI gates:
The host gate runs one fixed CTest inventory. The Android build must also produce non-empty
for profile in release-werror-asserts asan-ubsan tsan; do tools_pub/usb-async-lab host-test \ --manifest app/src/main/cpp/tests/usb/config/run-manifest.example.yaml \ --profile "$profile" done ./gradlew :app:externalNativeBuildDebug \ --no-daemon \ --warning-mode all \ --stacktrace
lib_neri.sooutputs forarm64-v8a,armeabi-v7a,x86, andx86_64. - If you changed the Listen Together Worker:
npm ci --prefix np-submodule/NeriPlayer-LTW npm run check --prefix np-submodule/NeriPlayer-LTW
npm run checkrunsnode --check, protocol tests, andwrangler deploy --dry-run. Protocol or room-state changes still need real create/join/WebSocket flow verification. - Add unit tests under
app/src/test/. Add device or Compose UI tests underapp/src/androidTest/. - If behavior changes affect README, settings copy, user flows, or sync formats, update documentation in the same PR.
Existing focused tests cover areas such as:
- YouTube login, cookie rotation, anonymous sessions, challenge parsing, PoToken, playback, Range/Seek policy, expedited long-seek recovery, and prefetching
- NetEase lyrics, local smoke tests, playback fallback, and playback response parsing
- USB-exclusive keep-alive, startup watchdogs, foreground/background recovery, 32-bit/float output, UAC2 explicit feedback, long-gap clock reacquisition, coordinated reconfiguration, Runtime Report v2, deferred-refresh retry, backpressure recovery, USB attach handling, and audio-focus policies
- Dual-scene main-tab transitions, rapid reverse switching, drawer/coherent detail feedback, glass-owner isolation, and unlaid-out scene geometry filtering
- Home-screen widget state/color/RemoteViews resources, launcher shortcut mapping, and app-wide Snackbar overlays
- Download metadata, naming, directory migration, snapshot caches,
.nomedia, delete semantics, and startup recovery - Startup stages, notification permission, playback-service startup, history recording, and safe-mode recovery planning
- Local scanning, metadata hydration, cover fallback resolution, system-playlist de-duplication, and stable playlist order
- GitHub/WebDAV sync serialization, missing-field snapshot cleanup, legacy playlist-order migration, deletion policy, playback-stat rolling windows, aggregate-total merging, legacy bucket-only compatibility, WebDAV concurrency fallback, atomic writes, and upload retry
- Long-form progress thresholds, explicit-position precedence, BilibiliSponsorBlock local skips, and its Listen Together disable policy
- Listen Together base URL validation, version gating, repeat/shuffle modes, stable-track-key target validation, session-only playback candidates, invite/member secrets, explicit leave/reconnect behavior, event ordering, playback sync planning, session control/cancellation, and protocol compatibility
- Lyrics UI, Japanese kana translation spacing, word timing, external Bluetooth lyrics, playback sound controls, and playback policies
- Config backup, generated settings, security guards, crash log files, and safe-mode behavior
PRs should include:
- Motivation
- Key implementation details
- Risks and compatibility impact
- Test steps
- Screenshots or recordings for UI changes
Do not commit:
- APKs, signing files, or local IDE config
- Caches, logs, or temporary build outputs
- Auth cookies, tokens, full config backups, or personal data
Commit messages should follow Conventional Commits when possible, for example:
feat: ..., fix: ..., or docs: ....
- This project is for learning and research purposes only. Do not use it for illegal purposes.
- This project is licensed under GPL-3.0.
- By submitting contributions, you agree to distribute your changes at least under GPL-3.0.
- The alternative license in
app/src/main/cpp/README.mdcovers only the listed NeriPlayer-owned native source, not third-party code or other repository content. - A native PR does not itself grant the alternative license. Contributors who agree to dual licensing must record the README's statement in the PR, commit, or another auditable form accepted by the copyright holder.
- An external native contribution without that explicit dual-license grant can still be accepted under GPL-3.0, but it is excluded from the attribution-based closed-source exception.
- Issues: bugs, feature requests, and discussions
- README_EN.md: features and usage
- CODE_OF_CONDUCT.md: community code of conduct
If you plan a large structural change, open an Issue first to align direction.