Linux Media Downloader is a lightweight desktop application for downloading media content (currently YouTube) for personal, educational, or non-commercial use. It pairs a small Flask backend with a PyWebView native window, delegating all downloading to the excellent yt-dlp library.
curl -fsSL https://raw.githubusercontent.com/MensuraMedia/linux-media-downloader/main/install.sh | bashInstalls everything (Python, ffmpeg, the GUI backend), sets up the app, and adds it to your
program menu with its own icon — then launch "Linux Media Downloader" like any other
app. Re-run to update; bash ~/.local/share/linux-media-downloader/install.sh --uninstall to
remove. Other platforms / manual setup: INSTALL.md.
Repository note: This project is maintained at
MensuraMedia/linux-media-downloader. It originated atmikesdatawork/linux-media-downloader(kept as theupstreamremote).Full technical reference: see DOCUMENTATION.md.
- Project Status
- Features
- Screenshot
- Architecture
- Project Structure
- Requirements
- Installation
- Running the App
- Usage
- Configuration
- Development & Testing
- AppImage Packaging
- Roadmap
- Contributing
- License
- Disclaimer
- In Development — this product is still evolving; it's under active development and grows over time, so expect frequent updates and refinements. See ROADMAP.md.
- Current support — YouTube videos and playlists, powered by
yt-dlp. - Setup — full install steps for Linux and Windows are in INSTALL.md.
- YouTube video & playlist download — single videos or entire playlists.
- Audio or video mode — audio extracted to MP3 (192 kbps) or full video as MP4.
- Real-time progress — an overall progress circle (which folds in the current file's fraction for smooth movement) plus a live progress bar centered beneath the current track's title; the download table lists each track's Length (duration). Byte, estimate, and fragment fallbacks keep the bar accurate.
- Cancel in progress — abort an active download cleanly at any time.
- Download history — the last 100 downloads persisted to
data/download_history.json. - Links history — a scrollable, YouTube-style record of every link you've submitted (thumbnails, badges, search, open/copy/re-use), on its own Links tab.
- Skip long files — when a playlist is detected, optionally skip any track over 7 minutes so they're never downloaded.
- Ignore Duplicates — skip tracks you've already downloaded, matched by video id and by
original (pre-rename) title with ≥80% relevancy (so
Beach_Original_music≈Original Beach Music), using a hidden manifest built as you download. - Playlists manager — a Playlists tab listing every downloaded playlist (folders
with more than one track). Rename a playlist (renames its folder), then drill in to a
sortable table (Length / Filename / Size) with bulk operations: delete all long files
(>7 min), clean up names, remove special characters, replace spaces with underscores,
remove filler words (music/mix/remaster/live/4k/hd/months/…), truncate to 35 chars,
normalize to a standard font (ASCII), change case (lower/UPPER/Title/Camel), and add a
number prefix, and abbreviate duplicate strings (
Predator_Soundtrack_Track01→Pred_Soun_Track01). Filler-word removal works even inside camelCase names (SongOfficialVideo→Song). Name collisions are resolved with a 4-character salt, and every operation is undoable (delete moves files to.trash, purgeable via Empty trash). Playlists carry a fixed sequence badge (assigned newest-highest, wraps999→000), sorted latest-added first, with per-playlist badge colours you can pick. - Playlist download controls — a single scope choice (Single Video / All (N) / First 10 / 20 / 30) plus a Skip long files filter, in clean columns. Playlist-only options stay visible but grayed-out until a playlist is detected.
- Player & curation — a Player tab with an audio/video player (play, seek/fast- forward, volume, skip ±10s, auto-next) over a searchable list of every downloaded file. Delete a file you don't want, or add it to a new folder as you listen to build a curated playlist on the fly.
- File Manager — a dense grid of every app-recorded file with stats (duplicate names, same size), the full set of global text operations (one-undo across all folders), and an inline keyboard-controlled player (→ +5s, ← −2s, ↑/↓ volume, Delete = stop & delete).
- Folder selection & open — choose a download directory and open it in your file manager.
- Filename sanitization — output names are normalized (alphanumeric + underscores).
- Two ways to run — native desktop window or an ordinary web browser.
| Layer | Technology | Responsibility |
|---|---|---|
| Native shell | PyWebView (app.py) |
Renders the UI in a desktop window |
| Browser fallback | Flask dev server (browser_app.py) |
Same UI at http://127.0.0.1:5000 |
| Web server | Flask blueprints | Serves UI pages and a JSON API |
| Download engine | yt-dlp | Extraction, download, audio post-processing |
Shared runtime state (the current download and the history list) lives as module-level
singletons in modules/config/settings.py. Because these are mutable and imported by
reference, mutation is visible across modules. Scalar flags (such as the cancellation
flag) are accessed only through accessor functions — request_cancel(),
reset_cancel(), and is_cancel_requested() — so a signal from the API thread is reliably
seen by the download worker thread.
Browser / WebView ──HTTP──▶ Flask (api.py, ui.py)
│
▼
start_download_thread() ──▶ worker thread
│ │
▼ ▼
current_download ◀── yt-dlp + progress_hook
(polled via /api/download-status)
linux-media-downloader/
├── app.py # Entry point: native PyWebView window
├── browser_app.py # Entry point: browser fallback (Flask :5000)
├── requirements.txt # Runtime dependencies
├── requirements-dev.txt # Dev/test dependencies (pytest, pyflakes)
├── modules/
│ ├── config/settings.py # Config, shared state, history, cancel helpers
│ ├── download/media.py # yt-dlp orchestration, progress hook, worker thread
│ ├── routes/api.py # JSON API blueprint (/api/*)
│ ├── routes/ui.py # Page-rendering blueprint (/, /backups, …)
│ └── utils/file_utils.py # Filename sanitization, open-folder
├── templates/ # Jinja2 HTML templates
├── static/ # CSS and JS assets
├── data/download_history.json # Persisted history (last 100)
├── tests/test_app.py # Offline unit + route tests
└── AppDir/ # AppImage packaging staging tree
- Python 3.8+
yt-dlpFlaskpywebview(native window only)ffmpegon yourPATH(required for audio extraction / merging)
Running as a standalone app window (not a browser):
python3 app.pyopens the UI in a native PyWebView window — its own title bar and taskbar icon, no browser chrome. PyWebView needs a GUI backend installed:
- Linux (GTK):
sudo apt install python3-gi gir1.2-webkit2-4.1 libgirepository1.0-dev- Linux (Qt) alternative:
pip install "pywebview[qt]"- Windows: Edge WebView2 (preinstalled on Win 10/11).
On a headless machine (no display) use the browser fallback:
python3 browser_app.py→ openhttp://127.0.0.1:5000. Packaged builds (see docs/packaging.md) bundle the backend so end users get the standalone window with no setup.
Add it to the Linux Mint / Cinnamon program menu (per-user, no root), with its own icon:
./packaging/install-menu.sh # adds "Linux Media Downloader" to the menu
./packaging/install-menu.sh --uninstall # remove itIt launches app.py as a native window (min/maximize/close controls); closing exits the app.
Quick start below. For full Linux + Windows setup (system deps, GUI backend, menu entry, Start-menu shortcut, troubleshooting) see INSTALL.md.
git clone https://github.com/MensuraMedia/linux-media-downloader.git
cd linux-media-downloader
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt # runtime only
# or, for development:
pip install -r requirements-dev.txtInstall ffmpeg via your system package manager, e.g.:
sudo apt install ffmpeg # Debian/Ubuntu/MintNative desktop window:
python3 app.pyBrowser mode (no GUI toolkit required — good for servers/headless):
python3 browser_app.py
# then open http://127.0.0.1:5000Flask binds only to 127.0.0.1 (localhost); the app is not exposed to your network.
- Paste a YouTube video or playlist URL.
- Choose Audio (MP3) or Video (MP4).
- Choose Single (just the video) or Playlist (all videos).
- Pick a download folder (defaults to your XDG Downloads directory).
- Click Download — watch live progress; Cancel stops it cleanly.
- Use Open Folder to reveal your files, and the Backups/History tab to review past downloads.
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/check-url |
Detect whether a URL is a single video or a playlist |
GET |
/api/get-default-path |
Return the default download directory |
POST |
/api/download |
Start a download in a background thread |
POST |
/api/cancel-download |
Signal the active download to stop |
GET |
/api/download-status |
Poll current download status/progress |
GET |
/api/links-history |
Return submitted-links history, newest first |
POST |
/api/clear-links-history |
Clear the links history |
GET |
/api/playlists |
List downloaded playlists (folders with >1 track) |
POST |
/api/playlist-files |
List a playlist's files (length / name / size) |
POST |
/api/rename-playlist |
Rename a playlist folder |
POST |
/api/playlist-operation |
Bulk file op (delete_long / clean / remove_special / replace_spaces / number_prefix) |
POST |
/api/open-folder |
Open a folder in the system file manager |
/api/download also accepts skip_long: true to skip tracks over 7 minutes.
Pages: / (Home), /backups (History), /playlists (Playlists), /links (Links), /about.
| Setting | How | Default |
|---|---|---|
| Flask secret key | SECRET_KEY env var |
random per run (os.urandom) |
| Download folder | Selected in UI / output_dir in /api/download |
XDG Downloads → ~/Downloads → ./downloads |
| History file | data/download_history.json |
last 100 entries |
| Audio codec/quality | modules/download/media.py |
MP3 @ 192 kbps |
# Optional: pin a stable secret key
export SECRET_KEY="your-own-secret"
python3 browser_app.pyThe test suite is offline — it never performs a real download.
pip install -r requirements-dev.txt
pytest -q # run the suite (15 tests)
pyflakes modules tests # lint for unused imports / undefined namesTests cover filename sanitization, the cancellation wiring (including an end-to-end HTTP
cancel through the Flask test client), the progress hook's completion/abort paths,
secret-key hygiene, and the /api/* routes.
Note: When changing runtime code, keep the
AppDir/usr/bin/copy in sync with the top-level sources — it is what an AppImage build ships.
The AppDir/ tree stages the app for packaging as a portable AppImage for Debian,
Ubuntu, Mint, and similar distributions.
Recommended storage location: keep the built AppImage in ~/Applications (create the
folder if needed) to keep user-level apps organized.
Build a .deb (Debian/Ubuntu/Mint):
./packaging/build-deb.sh→ install withsudo apt install ./dist/linux-media-downloader_1.0.0_all.deb.Full packaging guide — Linux (pipx-from-git, AppImage, .deb, Flatpak) and Windows (PyInstaller): see docs/packaging.md.
This product is still evolving. It's under active development and grows over time — features are added and refined based on real use. The full, categorized roadmap (shipped / designed / planned / ideas) lives in ROADMAP.md.
Highlights of what's coming: packaged installers (AppImage / .deb / Windows .exe),
one-line pipx install, Ignore Duplicates, YouTube cookie support, a fixed app-data
directory, more sources (Vimeo, SoundCloud), and CI. See ROADMAP.md for the rest.
Contributions are welcome — please open an issue or pull request to discuss ideas.
Run pytest and pyflakes before submitting, and keep the AppDir/ copy in sync.
Free for personal, educational, or non-commercial use only. Commercial use is strictly prohibited without prior written permission. See LICENSE for details.
This project is not affiliated with YouTube or any other media provider. Please respect the terms of service of every platform you use with this tool, and only download content you are legally permitted to.
