A self-contained desktop music streaming app for Windows, macOS, and Linux — and an installable PWA for Android & iOS — powered by YouTube, built with Flask + NeutralinoJS.
PX7.FM is a music streaming player that streams audio directly from YouTube — no browser tabs, no ads, no account. It runs as a native desktop app and as an installable Progressive Web App on your phone.
You get:
- Full-text search across YouTube's music catalogue
- Genre-filtered trending home feed
- Queue management with shuffle & repeat
- Liked songs and recently played history (persisted in localStorage)
- A clean, responsive UI that works at any window size — including a compact mobile-style layout
- OS-level Now Playing card with artwork, hardware button support, and lock-screen controls (via Web Media Session API)
- Installable on Android & iOS — appears on your home screen like a native app
Under the hood it's a Flask HTTP server wrapped in a NeutralinoJS window for desktop, and served as a PWA for mobile.
Disclaimer: PX7.FM is an independent open-source project and is not affiliated with, endorsed by, or connected to YouTube or Google in any way. Use responsibly and in accordance with YouTube's Terms of Service.
Most desktop music apps built in Python reach for Electron — which ships a full Chromium runtime and adds 150–200MB to the binary. PX7 takes a leaner approach:
- Flask is the natural fit because the core logic is already Python (
yt-dlp). A thin local REST server is all that's needed to bridge it to a browser-side UI — no IPC layer, no native bindings. - NeutralinoJS wraps that server in a proper desktop window using the system WebView (Edge WebView2 on Windows, WebKit on macOS/Linux) rather than bundling its own browser engine. Unlike Electron apps, PX7 stays lightweight (~30MB) while still providing a native desktop experience.
- PyInstaller
--onedirbundles the Python runtime and all dependencies alongside the Neutralino binary into one portable folder. No installer, no registry entries — unzip and run.
The tradeoff: you're dependent on the system WebView being present (it is by default on Windows 10+ and most modern macOS/Linux systems), and the frontend can't use Node.js APIs. For a music player, neither matters.
┌─────────────────────────────────────────────┐
│ PX7.exe │
│ (PyInstaller bundle — launcher.py entry) │
└───────────────┬─────────────────────────────┘
│ spawns
┌───────────▼──────────┐ ┌─────────────────────────┐
│ Flask backend │◄────►│ NeutralinoJS window │
│ localhost:5000 │ HTTP │ native system WebView │
│ │ │ loads http://127.0.0.1 │
└───────────┬──────────┘ └─────────────────────────┘
│
┌───────────▼──────────┐
│ yt-dlp │
│ (search / stream) │
└──────────────────────┘
| Layer | Tech | Role |
|---|---|---|
| Launcher | launcher.py |
Entry point. Detects the OS and selects the correct Neutralino binary. Starts Flask in a background thread, waits for it to be ready, then spawns the Neutralino window process. If Flask is already running (second launch), skips straight to the window. On non-Windows, sets the binary executable before spawning. |
| Backend | Flask (app.py) |
Thin REST API server. Also runnable standalone (python app.py) to serve the PWA on the local network for mobile access. |
| API module | api/ package |
Wraps yt-dlp for search and stream URL extraction. Handles title cleaning, duration formatting, and yt-dlp option configs. |
| Frontend | Vanilla JS (ES modules) + Jinja2 HTML | Single-page app served by Flask. Modular JS files handle state, player, queue, search, and views. |
| Media Session | mediasession.js |
Pushes track metadata and artwork to the OS Now Playing widget. Wires hardware prev/pause/next buttons and lock-screen controls. |
| Desktop shell | NeutralinoJS v6.7.0 |
Lightweight native window using the system WebView. Points to http://127.0.0.1:5000/. No Electron bloat. |
| Build | PyInstaller --onedir |
Bundles Python runtime, all deps, and the platform-appropriate Neutralino binary into a portable PX7/ directory. |
px7.fm/
├── app.py # Flask app — also runnable standalone for PWA/mobile
├── launcher.py # Entry point — boots Flask + Neutralino
├── requirements.txt # Python dependencies
├── CHANGELOG
├── LICENSE
│
├── .github/
│ └── workflows/
│ └── build.yml # CI — builds for Windows, macOS, Linux on push to main
│
├── api/
│ ├── __init__.py # yt-dlp options, shared helpers (clean_title, format_duration)
│ ├── search.py # YouTube search via yt-dlp
│ ├── stream.py # Stream URL extractor
│ └── trending.py # Curated "trending" feed (randomised seed queries)
│
├── templates/
│ └── index.html # Full SPA shell — layout, player bar, sidebar, views
│
├── static/
│ ├── site.webmanifest # PWA manifest — name, icons, display mode
│ ├── css/
│ │ └── styles.css # All styling — dark theme, responsive breakpoints
│ ├── js/
│ │ ├── index.js # App init, global __px7 bridge
│ │ ├── state.js # Single shared state object S + localStorage persistence
│ │ ├── player.js # Audio engine, playback controls, like/recent logic
│ │ ├── queue.js # Queue data + rendering
│ │ ├── search.js # Search fetch + result rendering
│ │ ├── views.js # View switching, home grids, genre chips, mobile drawer
│ │ ├── utils.js # fmt(), esc(), showToast(), cleanTitle()
│ │ └── mediasession.js # Web Media Session API — Now Playing card, hardware keys
│ └── images/icons/
│ ├── favicon.ico
│ ├── favicon.svg
│ ├── favicon-96x96.png
│ ├── apple-touch-icon.png
│ ├── web-app-manifest-192x192.png
│ └── web-app-manifest-512x512.png
│
└── neutralino/
├── neutralino.config.json # Window config (1200×800, min 380×600, loads localhost:5000)
├── bin/
│ ├── neutralino-win_x64.exe
│ ├── neutralino-linux_x64
│ └── neutralino-mac_universal
└── resources/
├── js/neutralino.js # Neutralino client runtime
└── icons/
├── PX7.ico
└── PX7.png
All endpoints are served by Flask on http://127.0.0.1:5000.
Search YouTube for tracks matching query.
Response
{
"results": [
{
"id": "dQw4w9WgXcQ",
"title": "Never Gonna Give You Up",
"channel": "Rick Astley",
"thumb": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
"duration": "3:33"
}
]
}Returns a curated list of tracks from randomised seed queries (lo-fi, indie, jazz, etc.), filtered to tracks ≤ 4 minutes.
Response — same shape as /api/search.
Extracts a direct audio stream URL for the given video ID using yt-dlp.
Response
{ "stream_url": "https://..." }Returns 500 with { "error": "Could not extract stream" } on failure.
Liveness check used internally by the launcher. Returns 200 when Flask is up.
The frontend is pure vanilla JS using ES modules — no framework, no bundler.
| Module | Responsibility |
|---|---|
state.js |
Single source of truth (S object). Tracks current track, queue, playback flags, liked/recent lists. Persists liked & recent to localStorage. |
player.js |
Owns the <audio> element. Handles play/pause, seek, volume (drag + touch), mute, shuffle, repeat, prev/next, like toggle, loading spinner state, and Media Session sync. |
queue.js |
Queue array CRUD and DOM rendering. Supports drag-and-drop reordering. |
search.js |
Fetches /api/search, renders result rows, exposes getLastResults() for auto-queue. |
views.js |
SPA view switching, home featured/recent grids, liked/recent list views, genre chip filter, mobile search drawer. |
utils.js |
Tiny pure helpers: fmt() (seconds → m:ss), esc() (HTML escape), showToast(), cleanTitle(). |
mediasession.js |
Web Media Session API. Updates the OS Now Playing card with track metadata and artwork. Wires hardware prev/pause/next action handlers and seek support. |
index.js |
Bootstraps the app on DOMContentLoaded. Wires everything into window.__px7 for inline onclick handlers. |
No Python or dependencies required — just download and run.
- Go to the Releases page
- Download the ZIP for your platform (
PX7-windows,PX7-macos, orPX7-linux) from the latest release - Extract the ZIP anywhere on your machine
- Run the
PX7executable inside the extracted folder
The app is fully portable. Nothing is written to the registry (Windows) and no installer is needed. To uninstall, delete the folder.
Windows: Requires Windows 10 or later (Edge WebView2 is included by default).
macOS / Linux: Requires a system WebKit/WebView. Most modern desktop installations include this out of the box.
No desktop needed — your Android phone runs both the server and the client.
Step 1 — Install Termux
Install Termux from F-Droid. The Play Store version is outdated and no longer receives updates.
Step 2 — Install dependencies
pkg update && pkg upgrade -y
pkg install python ffmpeg git -y
pip install flask yt-dlp
ffmpegis required by yt-dlp for certain audio formats. Skipping it will cause silent stream failures on some tracks.
Step 3 — Clone the repo
git clone --depth 1 https://github.com/px7nn/px7.fm.git
cd px7.fmOr copy the source folder to your phone over USB and cd into it.
Step 4 — Start the server
python app.pyStep 5 — Open in Chrome
Open Chrome on the same device and go to http://127.0.0.1:5000. Install to your home screen from the browser menu — the app will open full-screen with lock-screen playback controls.
Keeping the server alive when the screen is off
Android's battery optimiser kills background Termux sessions by default. Two things to do:
Go to Settings → Apps → Termux → Battery and set to Unrestricted.
Then run the server inside tmux so it survives Termux being backgrounded:
pkg install tmux -y
tmux new -s px7
python app.py
# Detach: Ctrl+B then D
# Reattach later: tmux attach -t px7iOS does not support running a local Python server from any app in a way that's reliable enough to recommend. The best approach is to run the server on another device (desktop or Android/Termux) and access PX7 over Wi-Fi from Safari on your iPhone, then add it to your home screen.
- Python 3.10+
requirements.txtcovers runtime deps; add PyInstaller for the build:pip install -r requirements.txt pyinstaller
- The appropriate Neutralino binary in
neutralino/bin/for your target platform
pyinstaller ^
--onedir ^
--noconsole ^
--noupx ^
--name PX7 ^
--icon "neutralino/resources/icons/PX7.ico" ^
--add-data "templates;templates" ^
--add-data "static;static" ^
--add-binary "neutralino/bin/neutralino-win_x64.exe;neutralino/bin" ^
--add-data "neutralino/neutralino.config.json;neutralino" ^
--add-data "neutralino/resources/icons/PX7.ico;neutralino/resources/icons" ^
--collect-all yt_dlp ^
--collect-all flask ^
--collect-all requests ^
launcher.pypyinstaller \
--onedir \
--noconsole \
--noupx \
--name PX7 \
--icon "neutralino/resources/icons/PX7.png" \
--add-data "templates:templates" \
--add-data "static:static" \
--add-binary "neutralino/bin/neutralino-mac_universal:neutralino/bin" \
--add-data "neutralino/neutralino.config.json:neutralino" \
--add-data "neutralino/resources/icons/PX7.png:neutralino/resources/icons" \
--collect-all yt_dlp \
--collect-all flask \
--collect-all requests \
launcher.pyReplace neutralino-mac_universal with neutralino-linux_x64 on Linux.
Output will be in dist/PX7/. The entire folder is portable — copy it anywhere and run PX7 (or PX7.exe on Windows).
The GitHub Actions workflow (.github/workflows/build.yml) builds for all three platforms automatically on every push to main, using Python 3.11. Artifacts are uploaded as PX7-windows, PX7-macos, and PX7-linux.
python app.py # server only — open http://127.0.0.1:5000 in any browser
python launcher.py # full desktop experience — Flask + Neutralino window- User clicks a track → frontend calls
/api/stream?id=<id> - Flask passes the YouTube video URL to
yt-dlpwithformat: bestaudio/bestandskip_download: True - yt-dlp resolves the best available audio-only stream URL (from YouTube's CDN)
- The URL is returned to the frontend and set as the
<audio>element'ssrc - The browser's native audio engine streams directly from YouTube's CDN
mediasession.jspushes track metadata and artwork to the OS Now Playing widget
No audio data ever passes through the Flask server — it only resolves the URL.
- Stream URLs expire. YouTube's CDN URLs are time-limited. If a track fails after sitting paused for a long time, re-clicking it will re-fetch a fresh URL.
- Trending feed is seeded, not live. The "trending" content is randomly picked from a small set of curated seed queries in
trending.py— not a real trending API. - No downloads. PX7 is a streaming player only.
- yt-dlp currency. YouTube frequently changes its extraction logic. If streams stop working, update yt-dlp:
pip install -U yt-dlpand rebuild. - PWA on iOS requires Safari. Chrome and Firefox on iOS cannot install PWAs due to Apple's browser restrictions.
- Termux battery optimisation. Android will kill the Termux session if battery optimisation is not disabled for Termux. See the Termux section above.
- Live trending via YouTube Music API or RSS
- Playlist creation & persistence
- System media key support (via Neutralino native API)
- Album/artist page views
- Mini-player / always-on-top compact mode
GPLv3 — you're free to use, modify, and distribute PX7.FM, but any derivative work must also be released under GPLv3 with source available. See LICENSE for the full terms.
