All projects

Music streaming with an Android app

AhoyMusic

My own music streaming service with a native Android app, my daily driver for two months now.

My daily driver for 2 months

10 TRACKS · 85 MB

Good evening

Liked Songs

A recreation of the real app with made-up tracks and no sound.

Core skills

  • Mobile and native
  • Backend and APIs
  • Real-time systems
  • Infrastructure and ops
  • Reliability
  • Testing and verification

Tech stack

  • Expo
  • React Native
  • Reanimated
  • expo-audio
  • Kotlin
  • Media3
  • Expo Modules API
  • MediaStore
  • NestJS
  • MongoDB
  • Redis
  • BullMQ
  • ffmpeg
  • Linux
  • pm2
  • nginx
  • Let's Encrypt

The hardest part

The lock screen stuck on the first song

What looks frozen can be a loop running too fast.

The whole story
  1. What it looked like

    The music kept playing normally, but the notification on the lock screen showed the first song's title forever.

  2. What was really going on

    An endless loop: every notification restarted the background service, and every restart sent another notification. Android simply threw the flood away.

  3. The fix

    The service now only announces itself on an explicit action. Proven by tapping the real notification on an emulator.

846
tests passed, app 548 and API 298
~24k
lines of code, plus ~10.7k lines of tests
~1,000
lines of Kotlin in 4 files
2 months
in daily use
In plain words

Instead of paying for a music subscription, my music runs on my own server. The app plays it online or offline, in the background and with lock-screen controls, like the big services do. I use it every day.

Simply to save costs. And because a music player is a nasty stress test. Audio, the Android lifecycle and offline sync forgive nothing, and if something breaks, I notice it myself every day.

How it works

Step 1 of 5

Import

A BullMQ worker outside the API handles tracks one by one, so a stuck job can't block requests.

What was built
  • A player with a queue, shuffle, repeat, background playback and lock-screen controls
  • A native Kotlin module with a Media3 session that decodes nothing and mirrors the queue held in JS
  • Offline mode: per-track downloads, sign-in without a network, library snapshots, cleanup of partial files
  • Audio streaming with HTTP Range (206 and 416), so seeking works through nginx too
  • An import worker with BullMQ outside the API, per-track progress, duplicate detection and file upload
  • Separate libraries per account, a deploy script with preflight checks, nightly backups with a documented restore
How those skills were used
Mobile and native
Kotlin, foreground services, Media3 sessions, runtime permissions and config plugins, plus debugging with adb and logcat
Backend and APIs
NestJS with MongoDB and no ORM, Redis and BullMQ, 32 HTTP routes
Real-time systems
Range requests, proxy buffering and async status ticks that must not arrive out of order
Infrastructure and ops
Linux, pm2, nginx, Let's Encrypt, logrotate, a disk guard and backups that warn about empty dumps
Reliability
Silent wrong states become clear refusals, for example a deploy over an old install path
Testing and verification
846 unit tests, and I checked the native part on a real device and an emulator
My role

Concept, architecture, directing the agents and verification. Claude Code wrote most of the code, inside an agent workflow I developed myself.

See the Context Observer project
Next projectBrickUp