Abdullah Masood
HomeProjectsAboutContact

Lahore, Pakistan · --:--:-- -- PKT

Currently building: my-windhawk-mods-media

© 2026 Abdullah Masood. Built with Next.js & Tailwind.

All projects
NoirPlayer screenshot
Desktop & MobileFlutterDartaudio_servicejust_audioLast.fm API

NoirPlayer

Dark-themed Flutter music player for Android — local library plus Last.fm-powered discovery, MP3 downloads, background playback with full media-control integration, a native equalizer, and rich playback settings.

Source Download APK

Problem

Most lightweight Android music players get one of two things wrong: playback dies the moment the app leaves the foreground, or the library is a flat file list with no sense of the device's actual media. NoirPlayer is a Flutter player built to behave like a real music app — background playback with lock-screen and notification controls, a library queried straight from the device's media store with a tabbed UI, gapless transitions, and a proper Now Playing screen — wrapped in a dark, noir-styled interface. It began as a strictly local player and has since grown an online Discover tab — trending tracks and search that stream and download straight into the local library — without disturbing that offline core.

Architecture

Flutter UIaudio_serviceon_audio_queryjust_audioDevice MediaStore

The Flutter UI never talks to the audio engine directly. It sends commands to audio_service, which owns the Android media session — that's what keeps music playing when the app is backgrounded and what powers the notification and lock-screen controls. audio_service delegates actual decoding and playback to just_audio (queue management, gapless transitions, seeking). On the library side, on_audio_query reads songs, albums, and artists from the device MediaStore, feeding the tabbed library views. Online discovery and downloads run through a separate MusicDiscoveryService (covered below), so the playback core stays untouched whether a track came from disk or the Discover tab.

Tech decisions & trade-offs

Why audio_service + just_audio instead of one plugin

The split mirrors how Android actually works: a media session (what the OS sees — notification, lock screen, headset buttons) is a different concern from a media player (decoding and output). audio_service handles the first, just_audio the second, and they're designed to compose. The trade-off is boilerplate — an AudioHandler bridging the two — but the payoff is playback that survives backgrounding, audio focus changes, and headset events the way users expect. Committing to just_audio for the player layer kept paying off: its AndroidEqualizer later gave a native per-band equalizer — frequency-labelled sliders with Flat / Bass / Vocal / Treble presets — without bolting on a separate audio-effects plugin.

Why query the MediaStore instead of scanning files

Walking the filesystem for .mp3s is the naive approach: it's slow, needs broad storage permissions, and misses the metadata Android already indexed. on_audio_query reads the system's own media database — instant library loads, album art and tags included, with narrower permissions. The cost is fidelity to whatever the MediaStore knows: files with broken tags show as <unknown>, which is the honest state of the data rather than a parsing failure.

Why the download path chains three APIs

No single API hands you a downloadable MP3 for an arbitrary track, so the Discover tab composes three. Last.fm supplies trending lists, search, and album art; the chosen track's title is resolved to a YouTube video id via the YouTube Data API; and a RapidAPI youtube-mp36 endpoint turns that id into an MP3 URL, which is stream-downloaded with dio (progress-aware) and written to the device Music folder through media_store_plus, with duplicate-download protection. The trade-off is a multi-hop pipeline with three keys to manage — but each hop uses the best source for its job, and because the keys load from a gitignored .env, the whole module is optional: without them the local player runs exactly as before and only the Discover tab goes dark.

Why a local-first core

Streaming apps live and die by their backend; a player that owns the user's local collection works on a plane and has zero ongoing cost. NoirPlayer keeps that local core as the foundation and layers online discovery on top rather than the other way around — the library, playback, and controls all work with no network and no API keys, and Discover is purely additive. That ordering kept the architecture testable: the playback path never depends on a network call, so the online features could ship without putting the offline experience at risk.

Branches

Online discovery and downloads started as experiments and have since shipped to main (v1.0.0 / v1.1.0). The repo keeps the earlier spikes around:

  • main — the full, released app: local library and background playback plus online discovery and MP3 downloads.
  • download-music — the original prototype for online search and downloading, where the Last.fm → YouTube → MP3 flow was first worked out before landing in the releases.
  • firebase — a Firebase-backed variant exploring cloud auth and features, alongside UI work like the system-theme option that follows the device's light/dark setting.

Repositories

  • NoirPlayerSource · pushed Jun 27, 20264