A Spotify-style web app where picking any song queues up only songs by the same artist next. Pick a Darshan Raval song → the whole queue is Darshan Raval. Pick an Arijit Singh song → the whole queue is Arijit Singh. No mixing, no surprises.
Built with Flask + vanilla JS + CSS. Uses the iTunes Search API for album art and the YouTube catalog (via yt-dlp) for full-length audio playback.
🔗 Repo: https://github.com/aditidrdz/tunify
**On a phone** |
**Tap ☰ for the nav drawer** |
- 🎯 Strict same-artist recommendations — once you pick a song, every recommendation is by that same artist.
- 🌐 Live iTunes catalog search — works for any song/artist in the world, not just a hardcoded list.
- 🖼️ Real album art via the iTunes Search API (no key required).
▶️ Full-length audio playback by extracting direct audio streams from YouTube usingyt-dlp(audio-only — no video popup).- 🔁 Smart fallback — if YouTube audio fails, it gracefully drops to the iTunes 30-second preview.
- 👤 User accounts — sign up / log in / log out. Passwords are hashed with Werkzeug (never plaintext).
- ❤️ Liked Songs — heart any song and find it later in a dedicated sidebar tab. Liked songs are stored per-user, server-side.
- 🗃️ Pluggable user storage — uses MongoDB if
MONGODB_URIis set, otherwise falls back to a JSON file. No code change required. - 🎨 Spotify-inspired dark UI — sidebar, top search, hero card, song grid, slide-in queue panel, and a bottom player bar.
- Go to the Releases page
- Download
Tunify.exe(~25 MB) - Double-click it. Your browser opens automatically at
http://127.0.0.1:5000. Sign up and start listening.
A tunify_data/ folder will appear next to the .exe — that's where your user accounts and caches live. Delete the folder to reset everything; copy it elsewhere to back up.
Windows SmartScreen may warn you because the .exe is unsigned. Click More info → Run anyway. (It's just Python + Flask + your repo; the source is right here for anyone to verify.)
Prerequisite: Python 3.10 or newer (download here — during install tick "Add Python to PATH").
Then just:
- Download or clone this repo
- Double-click
run.bat(Windows) or run./run.sh(macOS/Linux) - Your browser opens automatically at
http://127.0.0.1:5000. Sign up and start listening.
That's it. The script creates a virtual environment, installs everything, starts the server, and opens your browser — all in one go. Subsequent launches skip the install step and start in ~3 seconds.
First-run note: The first launch takes ~1–2 minutes (downloading deps + fetching album art from iTunes API). Every launch after that is instant.
If you'd rather drive it yourself:
git clone https://github.com/aditidrdz/tunify.git
cd tunify
python -m venv .venv
# Windows:
.\.venv\Scripts\Activate.ps1
# macOS / Linux:
source .venv/bin/activate
pip install -r requirements.txt
python app.pyThen open http://127.0.0.1:5000.
If you want to rebuild the standalone executable yourself:
pip install pyinstaller
pyinstaller Tunify.spec --noconfirmThe .exe lands in dist/Tunify.exe (~25 MB).
Tunify also works as a mobile web app — same UI, optimized for touch.
When you start Tunify (any method), the console prints something like:
Starting Tunify...
- On this computer: http://127.0.0.1:5000
- On any device on the same WiFi: http://192.168.1.42:5000
Just open the second URL on your phone's browser. Tap the ☰ button in the top-left to access Home / Search / Liked Songs / Artists.
First time only: Windows Firewall may pop up asking whether to allow the app on private networks. Click Allow access. If you missed the popup, manually allow
Tunify.exe(orpython.exe) for "Private networks" in Settings → Privacy & Security → Windows Security → Firewall.
Two options:
- Quick & temporary: Install ngrok, run
ngrok http 5000while Tunify is running, and you get a publichttps://*.ngrok-free.appURL that works on any phone anywhere. - Permanent: Deploy to Render.com (free tier supports Flask). I haven't pre-built this — ping me / open an issue if you want a deployment guide.
Copy .env.example → .env and set MONGODB_URI to your MongoDB Atlas connection string. The app auto-detects it on next launch. See Switching to MongoDB Atlas below.
tunify/
├── run.bat # One-click launcher (Windows)
├── run.sh # One-click launcher (macOS/Linux)
├── launch.py # Auto-opens browser when server is ready
├── app.py # Flask app + routes + recommendation logic
├── requirements.txt # Python dependencies
├── .env.example # Template for environment variables
├── .gitignore # Excludes secrets, caches, __pycache__
├── README.md
│
├── data/
│ ├── songs.py # 56-song seed dataset + similar-artist map
│ ├── enrich.py # iTunes API → album art + 30s previews
│ ├── youtube.py # yt-dlp → YouTube video IDs + audio URLs
│ ├── users.py # User auth + liked songs (Mongo OR JSON)
│ ├── migrate_users_to_mongo.py # One-time JSON → Mongo migration script
│ └── (generated at runtime) # enriched.json, youtube_ids.json, users.json
│
├── templates/
│ ├── index.html # Main player UI
│ └── login.html # Login / signup page
│
├── static/
│ ├── css/style.css # Spotify-style dark theme
│ └── js/app.js # Frontend logic (fetch, render, playback)
│
├── tools/
│ └── take_screenshots.py # Playwright script that regenerates README images
│
└── docs/
└── screenshots/ # PNGs embedded in this README
When you click a song, the frontend calls GET /api/recommend/<song_id>:
- The seed song's artist is identified (handles multi-artist credits like "A, B & C" by splitting them).
- The local 56-song catalog is searched for other songs by the same artist.
- The iTunes Search API is queried for more songs by that same artist to top up the queue (target: ~20+ songs).
- Results are deduplicated by
(title, artist)and returned. - The queue UI labels it "More from <Artist>" and plays them in sequence.
No similar-artist fallback — by user requirement, the queue is strictly same-artist. When the artist's catalog is exhausted, the queue simply ends.
User clicks a song
↓
Backend: GET /api/play/<song_id>
↓
1. Look up the song's title + artist
2. yt-dlp searches YouTube for "<title> <artist> audio"
3. yt-dlp extracts the direct audio stream URL (m4a preferred)
4. Return the URL to the frontend
↓
Frontend: <audio src="..."> plays it natively in the browser
↓
If YouTube audio fails → fall back to iTunes 30-second preview
No YouTube IFrame player, no visible video popup — just clean audio.
All /api/* routes require an authenticated session.
| Method | Path | Description |
|---|---|---|
| GET | / |
Main UI (redirects to /login if not signed in) |
| GET | /login |
Login / signup page |
| POST | /login |
Submit credentials |
| POST | /register |
Create a new account |
| GET | /logout |
End session |
| GET | /api/songs?q=<query> |
List or search local + iTunes catalog |
| GET | /api/artists |
Distinct artists with song counts |
| GET | /api/recommend/<song_id> |
Same-artist recommendations for a song |
| POST | /api/rediscover |
Re-find an iTunes song by {title, artist} |
| GET | /api/play/<song_id> |
Get a direct YouTube audio stream URL |
| GET | /api/likes |
Current user's liked songs |
| POST | /api/likes |
Like a song (body: full song object) |
| DELETE | /api/likes/<song_id> |
Unlike a song |
By default, user accounts and liked songs are stored in data/users.json. To use MongoDB instead:
- Sign up at https://www.mongodb.com/cloud/atlas/register (free M0 tier).
- Create a cluster, a database user, and add
0.0.0.0/0to Network Access. - Copy your connection string (looks like
mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/). - Create a
.envfile in the project root:MONGODB_URI=mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority MONGODB_DB=tunify TUNIFY_SECRET=some-random-string
- (Optional) Migrate existing users from the JSON file:
python -m data.migrate_users_to_mongo
- Restart the app:
On startup you should see
python app.py
[users] Using MongoDB backend (db=tunify).
If the URI is wrong or Mongo is unreachable, the app automatically falls back to JSON storage so it never crashes.
- Backend: Flask 3.0, Werkzeug (password hashing),
yt-dlp,pymongo,python-dotenv - Frontend: Vanilla JavaScript, CSS Grid + Flexbox, Inter font, HTML5
<audio> - External APIs: iTunes Search API (no key), YouTube (via
yt-dlp) - Storage: MongoDB Atlas or local JSON file (auto-detected)
| Symptom | Fix |
|---|---|
ModuleNotFoundError on startup |
Activate your virtualenv, then pip install -r requirements.txt |
| First request hangs ~60 seconds | Normal — iTunes enrichment runs on first launch. Watch the terminal. |
| "Audio failed, using preview" | YouTube extraction failed for that song. The 30-second iTunes preview is being played instead. |
| Can't log in | Delete data/users.json to reset all accounts, then sign up again |
| Want a fresh iTunes cache | python -m data.enrich --force |
| Port 5000 already in use | Edit the last line of app.py to use a different port |
MIT — do whatever you want with this code.








