Streamify is a modern, installable music streaming web app. Search, play, like, download and build playlists across millions of songs β free and ad-free. Built with Next.js (App Router) for the frontend and Django REST Framework for the backend, and shipped as one Docker image so it deploys as a single web service.
All music data comes from the unofficial JioSaavn API by @sumitkolhe, which this project runs inside its own container on loopback (see below).
This project is for educational purposes only. It does not store or redistribute music files, and the API it uses is unofficial.
Listening
- π Search songs, artists and albums with type-ahead suggestions
βΆοΈ Continuous playback with a queue, shuffle, repeat-one/repeat-all- ποΈ Seek, volume, mute, and sleep timer
- π» Endless radio mode β when the queue runs out, Streamify keeps playing similar tracks
- β―οΈ Resumes each track where you left off
- π± Media Session integration (lock screen, notification and hardware media keys)
- β¨οΈ Keyboard shortcuts (see below)
- β¬οΈ 1-click download, streamed through the backend so files arrive properly named
Your library
- β€οΈ Like/unlike, synced across every screen instantly
- π Playlists: create, rename, delete, add/remove tracks, reorder
- π Listening history with play counts, and a "jump back in" shelf
- π€ Profile with listening stats and your top artists
Discovery
- β¨ Personalised "For you" recommendations that learn from what you play and like
- π Home shelves: trending, chill, workout, romance, lo-fi, party
- π Browse by mood
App
- π§ Frosted-glass design language (translucent panels that blur the artwork behind them)
- π Dark and light appearance
- π² Installable PWA β home-screen icon, standalone window, offline shell
- π± Mobile-first: safe-area aware, swipe-to-dismiss player, bottom sheets, big touch targets
| Layer | Stack |
|---|---|
| Frontend | Next.js 15 (App Router), React 19, Tailwind CSS v4, HeroUI, React Query |
| Backend | Django 5, Django REST Framework, Gunicorn, WhiteNoise |
| Auth | Firebase Authentication (Google sign-in) |
| Database | SQLite locally, PostgreSQL in production (DATABASE_URL) |
| Music API | Bundled unofficial JioSaavn API (in-container) |
| Delivery | One Docker image β Next.js + Django + JioSaavn API in the same container |
The browser only ever talks to one origin. Next.js serves the PWA and proxies /api/* to
Django, which runs next to it on loopback β so there is no CORS, no second service to deploy and
nothing extra to pay for.
βββββββββββββββββ one Render web service (one Docker image) βββββββββββββββββββ
β β
browser βββββββΆβ Next.js :$PORT ββββ /api/* ββββΆ Django + Gunicorn 127.0.0.1:8000 β
(PWA) HTTPS β β’ pages, player β’ likes, playlists, history β
β β’ /api/config (runtime FB cfg) β’ recommendations, caching β
β β’ service worker, icons β β
β β loopback β
β βΌ β
β JioSaavn API 127.0.0.1:8123 (bundled) β
βββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
βΌ
www.jiosaavn.com
Three processes share the container and only one is public. The music catalogue is bundled, not
a third-party call: the shared public instance (saavn.sumit.co) fronts itself with a WAF that bans
whole networks, and it answered this project's network with Cloudflare error code: 1027 on every
route β including the bare domain. Self-hosting also pins the response shape to a commit we control.
Two more details worth knowing:
/api/configserves the Firebase keys to the browser at runtime, read from the sameVITE_FIREBASE_*variables the Vite app used. Render injects env vars when the container starts, not when the image is built, so baking them in at build time would not work.- Trailing slashes are optional on every API route. Next's proxy strips them, so the Django
URLconf accepts both
/api/healthand/api/health/and never redirects a POST body away.
Streamify/
βββ frontend/ # Next.js app (the PWA)
β βββ app/
β β βββ (app)/ # signed-in shell: home, search, for-you, library, profile
β β βββ api/config/ # runtime Firebase config
β β βββ manifest.js # PWA manifest
β β βββ offline/ # offline fallback page
β β βββ globals.css # design tokens + glass utilities
β β βββ hero.mjs # HeroUI theme
β βββ components/ # layout, player, song, playlist, common
β βββ hooks/ # usePlayer, useAuth, useLike, useKeyboardShortcuts
β βββ lib/ # api client, queries, firebase, storage, format
β βββ public/ # service worker, logo, generated icons
β βββ scripts/generate-icons.mjs
βββ backend/ # Django API
β βββ music/ # views, models, upstream client, urls, tests
β βββ streamify_api/ # settings, root urls
βββ Dockerfile # single image for a single web service
βββ docker-entrypoint.sh # migrations + Django + Next
βββ render.yaml # optional Render blueprint
You need Node 20+ and Python 3.10+. Two terminals, no Docker required.
cd backend
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # set DJANGO_SECRET_KEY and keep DEBUG=True
python manage.py migrate
python manage.py runserver # http://127.0.0.1:8000cd frontend
npm install
cp .env.example .env.local # add your Firebase keys
npm run dev # http://localhost:3000Open http://localhost:3000. The dev server proxies /api/* to 127.0.0.1:8000 for you.
In the container Django calls a bundled JioSaavn API on 127.0.0.1:8123. Running locally you need
that process too, or search and playback return empty:
cd /tmp && mkdir jiosaavn-api && cd jiosaavn-api
curl -sL https://codeload.github.com/ShirshenduR/jiosaavn/tar.gz/6dc24cfb1ec444cdfcea9de1e59afd1146a51547 \
| tar xz --strip-components=1
npm install --ignore-scripts && npm run build
node /path/to/streamify/saavn-api/serve.mjs # serves 127.0.0.1:8123Django does not need to be told about it β the address is a constant in backend/music/upstream.py.
- Create a project in the Firebase console.
- Enable Authentication β Google.
- Copy the web app config into
frontend/.env.local.
If the keys are missing the app says so on screen instead of failing silently.
Every key name is unchanged from the original setup β nothing to rename, and no new secret is required.
| Variable | Where | Required | Purpose |
|---|---|---|---|
VITE_FIREBASE_API_KEY |
frontend | yes | Firebase web config (public by design) |
VITE_FIREBASE_AUTH_DOMAIN |
frontend | yes | " |
VITE_FIREBASE_PROJECT_ID |
frontend | yes | " |
VITE_FIREBASE_STORAGE_BUCKET |
frontend | yes | " |
VITE_FIREBASE_MESSAGING_SENDER_ID |
frontend | yes | " |
VITE_FIREBASE_APP_ID |
frontend | yes | " |
VITE_BACKEND_API_URL |
frontend | no | Proxy target for /api/*. Defaults to http://127.0.0.1:8000 |
DJANGO_SECRET_KEY (or SECRET_KEY) |
backend | prod | Django secret. Either name is accepted |
DEBUG |
backend | no | Defaults to True; parsed as a real boolean |
DATABASE_URL |
backend | prod | Postgres URL. Omit locally to use SQLite |
NEXT_PUBLIC_* equivalents are also accepted for the Firebase values if you prefer that
convention, but the VITE_* names are what the app reads at runtime.
The repository contains everything needed: Dockerfile, docker-entrypoint.sh and an optional
render.yaml.
Option A β Blueprint. Render β New β Blueprint β pick this repo. It creates one web service plus a free Postgres, and prompts you for the secrets listed above.
Option B β manual. Render β New β Web Service β connect the repo β set Runtime: Docker,
Dockerfile path: ./Dockerfile, and add the variables from the table.
Notes:
- Set
DATABASE_URL(the blueprint wires it automatically). A container filesystem is ephemeral, so without Postgres your library resets on every deploy. - The health check path is
/api/health. - Render terminates TLS and provides
PORT; the entrypoint starts Django on loopback, waits for it, then starts Next on$PORT. One container, one URL. - The Django admin is proxied at
/admin/on the same URL. Create an account once withpython manage.py createsuperuser(inside the container shell) to use it.
Build and run it locally the same way:
docker build -t streamify .
docker run -p 3000:3000 -e DJANGO_SECRET_KEY=dev-secret -e DEBUG=False streamify
# open http://localhost:3000Recommendations are built from a per-user artist affinity score, computed on demand:
history play β 1.0 + 1.5 Γ min(play_count, 20) per artist
liked song β +3.0 per artist
The top artists are searched in parallel, the results are interleaved round-robin so no single
artist can flood the list, anything already liked or played is filtered out, and each track carries
a reason ("Because you like β¦") that the UI displays. A brand-new account gets seeded shelves
instead of an empty page, and the page says so.
Every play is recorded by the player, which is what makes the scores meaningful over time.
Trailing slashes are optional everywhere.
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/api/health |
Liveness probe used by Render |
GET |
/api/search?q=&limit= |
Search songs |
GET |
/api/discover?limit= |
Home shelves + mood list |
GET |
/api/radio?artist=&title=&exclude= |
Endless playback suggestions |
GET |
/api/recommendations?user_id=&limit= |
Personalised picks |
GET |
/api/songs/<id> |
Track details |
GET |
/api/songs/<id>/stream |
Resolved stream URL |
GET |
/api/songs/<id>/download |
Track as an attachment |
GET |
/api/liked?user_id= |
Liked songs |
POST |
/api/like |
Like a song |
POST DELETE |
/api/unlike |
Unlike a song |
GET POST |
/api/history |
Recent plays / record a play |
POST |
/api/history/clear |
Clear history |
GET POST |
/api/playlists |
List / create playlists |
GET PATCH DELETE |
/api/playlists/<id> |
Read / rename / delete a playlist |
POST |
/api/playlists/<id>/songs |
Add a track |
DELETE POST |
/api/playlists/<id>/songs/<song_id> |
Remove a track |
POST |
/api/playlists/<id>/reorder |
Reorder tracks |
Older clients keep working: /api/song?id=, /api/download?id= and /api/search/combined?q= are
still served.
Responses are normalised to one shape, so the UI never has to know what the upstream API looked like:
{
"id": "3IoDK8qI",
"title": "Kesariya",
"artist": "Arijit Singh, Amitabh Bhattacharya",
"cover": "https://c.saavncdn.com/...500x500.jpg",
"duration": 268,
"album": "Brahmastra",
"source": "jiosaavn",
"streamUrl": "https://aac.saavncdn.com/....mp4"
}| Key | Action |
|---|---|
Space / K |
Play / pause |
β / β |
Seek 5 seconds |
Shift + β β |
Previous / next track |
β / β |
Volume |
M |
Mute |
S |
Shuffle |
R |
Repeat mode |
L |
Like the current track |
cd backend
python manage.py test music # 26 tests: parsing, routing, library, recommendationsThe suite stubs the upstream API, so it runs offline and never depends on a rate limit.
cd frontend
npm run lint
npm run build- Identity is the Firebase UID, trusted as sent. Verifying an ID token server-side needs a Firebase service-account secret, and this project deliberately ships with none. Treat the API as you would any single-user demo: anyone who knows a UID can read and write that library.
- The music catalogue runs in the same container. It is fetched at image build time from
ShirshenduR/jiosaavn, pinned to a commit (SAAVN_API_SHAin theDockerfile). Bump that ARG to move to a newer upstream; if that repository disappears the image build breaks. - If the catalogue does go down, the API reports
unavailable: trueand the UI says the music service is unreachable rather than claiming there are no results. A short cooldown stops the app hammering a blocked service, and "Try again" bypasses it. - Downloads are proxied through the backend, so a large library download costs server bandwidth.
- Playback URLs point at a third-party CDN and expire; they are resolved on demand and never stored.
db.sqlite3is a local development convenience and is not part of the deployment.- No new environment variables. Everything above works with the keys that were already there.
- π§ API: Sumit Kolhe's JioSaavn API, bundled in the container from a pinned fork (MIT, Β© Sumit Kolhe)
- π¨ UI kit: HeroUI Β· icons: Lucide
- β‘ Framework: Next.js Β· Django
MIT License.
Built with β€οΈ by Shirshendu for fun, learning & passion for music.