Self-hosted service
cwuom/NeriPlayer avatar
cwuom/NeriPlayer

NeriPlayer: A Native Android Player That Keeps Your Library on Your Own Remote

A native Android audio player that combines multi-source streaming, local control, rich lyrics, and self-hosted sync. / ✨ 一个把多源在线播放、本地管理、歌词体验和自建同步做进原生 Android 的音频播放器 🎵

3,417 stars114 forksKotlinGPL-3.0

At a glance

What is it?
NeriPlayer is a Kotlin, Jetpack Compose and Media3 audio player for Android 9 and up that aggregates NetEase Cloud Music, Bilibili and YouTube Music playback while storing playlists and statistics in your own GitHub or WebDAV remote. Its design is unusual in one specific way: it refuses to write your listening history back to the platforms it streams from.
Who is it for?
Adopt NeriPlayer if you already hold accounts on the platforms it integrates, you want your playlists and play counts on a remote you control, and you accept that a GPL-3.0 app in active development ships API-level constraints rather than a polished consumer product. Do not adopt it if you expect a bundled catalogue, if you need a stable tagged release rather than CI-stamped builds, or if you cannot supply your own GitHub repository or WebDAV endpoint for sync.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The gap NeriPlayer is built to fill

Most Android players pick one side. Either they index local files and stop there, or they wrap a single streaming service and treat your library as that service's library. NeriPlayer takes a third position: it treats the device as the source of truth and the streaming platforms as interchangeable suppliers of audio. The README states the project is organised around four goals it lists as multi-source exploration, online playback, local controllability and data self-possession. That last item is the one that shapes everything else.

The audience is narrow and identifiable. You need an Android 9 (API 28) or newer device, an account on at least one of NetEase Cloud Music, Bilibili or YouTube Music, and ideally a GitHub repository or WebDAV endpoint you are willing to point the app at. The project explicitly does not ship media content, keys, DRM workarounds or a public proxy, and the maintainers state they accept no sponsorship or donations. If you want a service that hands you a catalogue, this is the wrong shape of software.

How PlayerManager decides what actually plays

The interesting engineering is not the UI, it is the fallback chain. PlayerManager handles source resolution, queue management and failure recovery. When a NetEase track cannot be played, returns no usable result, or comes back as a preview fragment only, the app first attempts a quality downgrade. If that fails, PlayerManagerNeteaseAutoSourceSwitch scores Bilibili candidates by song title, artist and duration and substitutes one automatically. On a playback error the current link is refreshed, and after repeated failures the track is skipped or playback stops.

YouTube gets a separate, more elaborate path. The README describes maintaining a logged-in identity cookie alongside an anonymous visitor session, with bootstrap and PoToken sessions tracked separately. Cookie rotation results, session parameters, player.js and challenge outputs are cached and reused first. A stale player.js or a playback candidate the platform rejects moves into an EJS or HLS fallback rather than being retried on the same candidate. That is a deliberate choice to stop burning attempts on a known-bad path, and it is the kind of detail that usually only appears after a maintainer has spent a long time watching failures.

Offline behaviour and the local-first data model

NetworkStatusMonitor reads the system default network bearer to decide whether the device is offline, and offlineCachedImageRequest blocks remote image fetches in that state, preferring cached artwork. Screens including the home page, explore, the now-playing view, lyrics, playlists and the download list all receive an offlineMode flag. The practical result is that a disconnected device still works around local files, downloaded audio, playback cache, cached covers and local playlists.

Storage is handled by StorageUsageAnalyzer, which groups usage into audio cache, image cache, download staging, share staging, platform playlist cache, downloaded content, logs, crash reports and core app data. Clearing cache is scoped to regenerable data only, so a song you deliberately downloaded is not swept away as cache. Downloads bypass the system DownloadManager entirely. The app uses a shared OkHttpClient with configurable concurrency, a working file and sidecar metadata, and applies different resume strategies for plain HTTP, transfers that require an explicit Range header, and HLS. Paused queues resume on next launch, and if tag writing fails after the audio lands, the audio file is kept.

Sync, statistics and the deliberate refusal to report back

PlaybackStatsRepository records play counts, listening duration, recent plays and daily buckets against a stable per-song identity, and those records participate in sync merges. TrafficStatsRepository separates playback traffic, download traffic, Wi-Fi, mobile, roaming and cache hits, with batched writes flushed at lifecycle boundaries and a cap on daily bucket count. Sync targets are a GitHub repository, which the README says defaults to private when created in-app, or a WebDAV remote file.

The design decision worth pausing on is what NeriPlayer declines to do. The README states the app is technically capable of writing play history back to the third-party music platforms, and explains that it does not, citing the risk that client-side risk controls and behaviour sampling on those platforms could trigger abnormal login, abnormal playback or account freezing. That is a defensible position, and it is also a limitation: your play counts on NetEase or YouTube Music will not reflect what you actually listened to in NeriPlayer. Remote snapshots are read with tolerance for older or malformed fields, and records without a resolvable song identity are filtered out.

Getting a build running and what the settings schema controls

The README does not document a Gradle command line for building from source, so the concrete install path it does describe is the releases page: https://github.com/cwuom/NeriPlayer/releases. Release tags follow a commit-hash plus timestamp pattern such as NeriPlayer-ac7bdea4.09031001, and the project also runs CI builds through a Telegram channel it links as @neriplayer_ci. Because the README points contributors at CONTRIBUTING.md rather than restating build steps, treat the release APK as the supported route and the source build as something you verify yourself.

Configuration is managed by AutoSettingsSchema. The keys it covers include dynamic colour extraction, a seed colour, theme style, automatic/light/dark mode, UI scaling, custom background, lyric font size, lyric blur, the now-playing fluid background, two-level advanced blur, coherent feedback, home card toggles, default start page, haptic feedback, and per-song custom name, artist and cover overrides. Two defaults matter: advanced blur and coherent feedback ship off. Turning coherent feedback off switches detail pages such as playlists to a drawer-style expansion. Advanced blur only becomes available on Android 13 or newer and only when the parent blur toggle is on, and it exposes a 12-64 dp blur range that the README says does not alter text, icons, layout or hit areas. Blur quality has four levels (ultra low, low, default, high); ultra low and low keep the same blur radius as default and reduce cost through local region rendering, dynamic downsampling and RenderNode hardware caching. On MediaTek SoCs with no saved preference, ultra low is selected by default.

USB exclusive output and the hardware it does not cover

PlaybackEffectsController binds speed, pitch, Equalizer and LoudnessEnhancer to the current Media3 audio session, with presets plus manual band control, loudness enhancement, per-song real-time loudness levelling, channel balance, 32-bit high-resolution output, fades and crossfades, Bluetooth disconnect pause, USB exclusive playback and audio focus policy. USB exclusive hands the device to a native driver so system audio and other apps do not share the USB path. Bit-perfect volume mode keeps software gain at 0 dB and leaves volume to the DAC hardware.

The constraint is stated plainly in the README: the feature currently targets UAC1.0 and compatible UAC2.0 Type I PCM devices. UAC2 devices outside that class are not covered. The supporting machinery is extensive (device selection, sample rate, bit depth and buffer policy, 32-bit PCM, PCM float software conversion, background-run warnings, UAC2 clock topology and explicit feedback endpoint parsing, a startup watchdog, foreground and background health audits, dynamic transfer scaling and stall auto-recovery, with feedback clock recapture after long scheduling gaps), but none of it widens the device compatibility list. If your DAC is not UAC1.0 or UAC2.0 Type I PCM, this is a feature you should assume you will not get.

Lyrics, rendering and where the polish stops

SyncedLyricsView and AdvancedLyricsView handle line-level and word or character-level LRC, including line-end timing and bracketed per-character timestamps, plus YRC and TTML. Highlighting, translations, transliteration, lyric offset, tap-to-seek, long-press share, depth-of-field blur, edge fading and full-screen mode are all present. LyricShareSheet lets you pick lines, copy text, share the song or generate a 1080px lyric card. One small detail in the README is telling: when Japanese source text contains kana, the translation line gets extra spacing so the two do not collide. Floating lyrics, status bar lyrics, SuperLyric, Lyricon, Bluetooth lyrics and lyric editing reuse the same playback data path.

The rendering side is more interesting than a blurred cover. BgEffectPainter loads assets/shaders/hyper_background_effect.glsl and renders it per frame through RuntimeShader, generating a fluid background from cover-derived colours, moving colour blocks and light grain noise, with uMusicLevel and uBeat driving audio response. That is a real shader pipeline, not a post-process. The trade-off is that it is GPU work running continuously on the now-playing screen, and the README's blur quality controls exist precisely because the maintainers are managing that cost on weaker hardware.

The alternative, and the honest boundary of this design

The obvious alternative is a self-hosted media server such as Navidrome or Jellyfin paired with a Subsonic-compatible Android client. The difference in approach is fundamental rather than cosmetic. A Subsonic client streams from one server you operate; the catalogue is whatever you put on that server, and the client is a thin remote control. NeriPlayer does the opposite: it aggregates catalogues owned by third parties, then keeps only your metadata local or on your own GitHub or WebDAV remote. NeriPlayer will not serve you a track that no platform will hand over, and a Subsonic client will not play a NetEase track you have no file for. Choosing between them is really choosing whether you want to own the audio or own the index.

Several limits follow from that choice. Multi-source playback depends on your platform accounts continuing to authenticate, and the YouTube fallback chain exists because those sessions break. The app is in active development and releases are stamped with commit hashes rather than semantic versions, so pinning to a known-good build means tracking a hash. The licence is GPL-3.0, which matters if you intend to fork and redistribute; if you are evaluating it for a commercial product, read the licence text rather than this article. And the README itself carries a warning that the project is for study and research, that you should only access content you have the rights or platform permission to access, and that it provides no content, keys or DRM circumvention. That is the boundary the project draws, and it is narrower than the feature list might suggest.

Editorial conclusion

Adopt NeriPlayer if you already hold accounts on the platforms it integrates, you want your playlists and play counts on a remote you control, and you accept that a GPL-3.0 app in active development ships API-level constraints rather than a polished consumer product. Do not adopt it if you expect a bundled catalogue, if you need a stable tagged release rather than CI-stamped builds, or if you cannot supply your own GitHub repository or WebDAV endpoint for sync. Verify first that your device meets Android 9 (API 28), that your USB DAC falls inside the UAC1.0 or UAC2.0 Type I PCM range the README names, and that your third-party platform sessions still authenticate before you migrate a library into it.

Official sources

  1. cwuom/NeriPlayer on GitHub
  2. Issues
  3. License: GPL-3.0
  4. README
  5. Releases
Community notes

Community notes