video downloader tool that supports
video downloader tool that supports YouTube.
Paste a link → it works out where the link is from and reads it → pick a quality → pick a folder → Download.
Licensed under GPL-3.0.
- Paste a video URL and fetch the qualities that link actually offers
- Recognises the site as you paste — the name appears in the URL bar immediately, and the link is read without you pressing anything
- TikTok downloads have no watermark, at the highest quality the post has
- Simple quality presets: Best available, per-resolution (1080p, 720p, …), and Audio only (MP3)
- Native folder picker, remembers the last folder you used
- Live progress bar with size, speed and ETA
- Status/log panel showing each stage (reading link, downloading, merging with ffmpeg, done)
- Cancel button that stops cleanly and deletes the half-written files
- Friendly messages for bad links, dead videos, network drops and missing ffmpeg
Deliberately not in v1: playlists, subtitles, trimming.
Anything yt-dlp supports will download. What changes per site is how Spyder presents it.
The moment a link lands in the URL bar its site is named there — no network call, it is a hostname lookup — and half a second later Spyder reads the link by itself. The Fetch button still works; it just rarely needs pressing.
TikTok is the one site with its own rules:
- TikTok serves every video twice, once clean and once stamped with a drifting @handle. Spyder filters the stamped copies out entirely, so the watermark-free version is not a setting to find — it is the only thing offered.
- The top preset reads Best quality, no watermark and is already selected.
- Because TikTok publishes each quality as one finished file, Spyder takes it whole instead of downloading video and audio separately and merging them.
vm.tiktok.comandvt.tiktok.comshort links work; they are resolved on the way through.
Vertical video is named the way people name it. A 1080×1920 TikTok or Short is listed as 1080p, not 1920p.
TikTok turns away any client that does not look like a real browser, so yt-dlp
has to impersonate one. That ability comes from
curl_cffi, which requirements.txt
pulls in via yt-dlp's curl-cffi extra and which is bundled inside the built
exe. Without it TikTok fails before it starts; Spyder notices a TikTok link it
cannot read and says so in the log rather than letting the fetch fail
unexplained.
Grab Spyder.exe and double-click it. Python, Qt, yt-dlp and ffmpeg are all
bundled inside, so there is nothing else to install.
The exe is unsigned, so Windows shows a "Windows protected your PC" screen the first time. Click More info -> Run anyway. First launch takes a few seconds while it unpacks.
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
python run_spyder.pyFrom source, ffmpeg is not bundled. Spyder looks for it in this order:
- Inside the bundle (frozen builds only)
- Next to
Spyder.exe, or in the project root /vendor/when run from source - Anywhere on your
PATH
So either drop an ffmpeg.exe into vendor/ (running python build.py once
fetches it for you), or install ffmpeg system-wide:
winget install Gyan.FFmpegWithout ffmpeg the app still starts and warns you in the log; downloads that need merging or MP3 conversion will fail with a clear message.
pip install -r requirements-dev.txt
python build.pybuild.py downloads a static ffmpeg (~30 MB zipped, once) into vendor/, then
runs PyInstaller. The result is a single self-contained file:
dist\Spyder.exe
Roughly 80 MB, because it contains Python, PyQt6, yt-dlp and ffmpeg.
Options:
| Flag | Effect |
|---|---|
--clean |
delete build/ and dist/ first |
--no-ffmpeg |
skip the ffmpeg download; produces a much smaller exe that needs ffmpeg on the user's machine |
To run PyInstaller directly instead:
pyinstaller Spyder.spec --noconfirmThe spec bundles vendor/ffmpeg.exe if it is present and warns if it is not.
- The exe is windowed (
console=False).spyder/main.pypatchessubprocess.PopenwithCREATE_NO_WINDOWso ffmpeg does not flash a console. yt_dlp.extractor.lazy_extractorsis listed as a hidden import — yt-dlp loads extractors dynamically and PyInstaller cannot see them.- curl_cffi needs the same treatment and then some. PyInstaller ships no hook
for it, so the spec collects its submodules by hand and copies the
libcurl-impersonate-*.dllout of thecurl_cffi.libsdirectory the wheel puts beside the package. That directory name matters:curl_cffi/__init__.pypasses it toos.add_dll_directorybefore loading its extension, so the DLL has to keep it rather than land at the bundle root like everything else. Building without curl_cffi installed prints a warning and produces an exe that cannot read TikTok. - The pixel-art icon comes from
logo.ico, with dedicated 16, 24, 32, 48, 64, 128 and 256px images for the exe, window and taskbar.logo.pngis the large preview andlogo.asepriteis the editable source. Replace bothlogo.icoandlogo.pngand rebuild to change it. Withoutlogo.ico, the build generates icon sizes fromlogo.pnginstead. - Unused Qt modules (QML, Quick, WebEngine, Multimedia, …) are excluded to keep the exe smaller.
- Some antivirus tools flag unsigned PyInstaller one-file exes. Code-signing is
the real fix; a
--onedirbuild (dropruntime_tmpdir, useCOLLECT) trips it less often.
python build.py --clean- Check
dist/contains Spyder.exe and THIRD-PARTY-NOTICES.txt - Tag and push:
git tag v1.0.0 && git push origin v1.0.0 - Create the GitHub release and attach both files from
dist/
Attaching the notices file matters — it is what keeps the GPL ffmpeg binary
inside the exe properly licensed. Bump __version__ in spyder/__init__.py
when you cut a new version.
Tell people the first launch shows a SmartScreen warning (the exe is unsigned): More info -> Run anyway. Signing is the only real fix.
Spyder is licensed under the GNU General Public License v3.0 - see LICENSE.
It has to be. Spyder links PyQt6, which is GPLv3, so any distributed build must also be GPLv3 and ship its source. Publishing this repository satisfies that.
To relicense Spyder under something permissive you would need to drop PyQt6 -
porting ui.py and worker.py to PySide6 (LGPLv3) is mostly import
renames, since neither uses PyQt-specific APIs - or buy a Riverbank commercial
PyQt licence.
THIRD-PARTY-NOTICES.txt ships inside the exe and next to it. It covers:
| Component | License |
|---|---|
| FFmpeg (gyan.dev essentials build) | GPLv3+ |
| PyQt6 / Qt6 | GPLv3 / LGPLv3 |
| yt-dlp | The Unlicense |
| curl_cffi + libcurl-impersonate | MIT (bundles libcurl, BoringSSL, nghttp2) |
| Python | PSF License v2 |
FFmpeg is run as a separate process, so only its own binary is covered by its license.
Spyder is a front-end for yt-dlp; it does not host, bypass, or decrypt anything. Downloading may still breach a site's terms of service or copyright law depending on the content and your country. That is on the person using it.
Removing a watermark does not remove the credit it stood for. A TikTok saved through Spyder is still someone's work, and reposting it as your own is the thing the watermark existed to prevent.
run_spyder.py launcher / PyInstaller entry point
spyder/
main.py app bootstrap, Windows console suppression
ui.py MainWindow — widgets and signal wiring only
theme.py palette, the three type roles, the global stylesheet
widgets.py the blocky pieces Qt does not provide
worker.py QThread wrappers: ProbeWorker, DownloadWorker
downloader.py yt-dlp logic (Qt-free), presets, error translation
platforms.py which site a link is from, and what that changes
icons.py the line-art icon set, drawn with QPainter
win32.py what a frameless window owes Windows
ffmpeg_tools.py finding ffmpeg
resources.py finding bundled assets (icons)
logo.png large source logo / preview
logo.ico authored Windows icon sizes
logo.aseprite editable pixel-art source
assets/ generated Spyder.ico + icon.png (gitignored)
Spyder.spec PyInstaller spec
build.py fetch ffmpeg + build in one command
vendor/ ffmpeg.exe lands here (gitignored)
The design language is kitty's, ported to Qt: neutral dark, rounded, and drawn entirely by the app.
Nothing on screen is drawn by the OS. Spyder has no title bar, because an OS frame above a carefully made app looks like a web page in a picture frame. A strip flush to the top carries the mark, what is downloading, and the three window controls; everything else sits in one rounded canvas inset from the edges, on a 4px dot texture.
Dropping the frame means owing the user everything it gave them, and that debt is paid rather than ignored:
| What the frame did | Who does it now |
|---|---|
| Drag to move, with Aero Snap | TitleBar, via startSystemMove |
| Double-click to maximise | TitleBar.doubleClicked |
| Edge and corner resize | EdgeResizer, via startSystemResize |
| Keeping a maximised window off the taskbar | win32.clamp_maximised |
| Rounded outer corners | win32.round_corners |
No blue in the greys. A tint that reads as merely "cool" on one panel
becomes a colour cast across a window full of them, so the neutrals are true
neutrals and the only hue is the accent (#2b948b), spent on state: a focused
field, the chosen row, the primary button, the recognised platform.
Four radii, not a value per element. Every rounded thing picks one of
R_XS/R_SM/R_MD/R_LG, so two controls of the same weight cannot disagree
about how round they are.
Three consequences show up in the code. Every colour and font decision lives in
theme.py as one style sheet, so no widget carries its own styling. Because the
style allows no floating system windows, widgets.py reimplements the two
things Qt would otherwise float: the quality dropdown (BlockSelect, a menu
built from our own rows) and message boxes (BlockDialog, a panel washed over
the page). The folder picker stays native — that one is the OS's job.
Icons are drawn, not fetched. icons.py strokes each one on its own square
viewbox, the way kitty draws its SVGs, so they are identical on every machine
and take the app's line weight. The icon fonts this replaced — Segoe Fluent on
Windows 11, Segoe MDL2 on 10, neither anywhere else — needed a text fallback
for a case that could not be tested.
Type is Segoe UI Variable Text for the interface and Cascadia Mono for everything technical: links, paths, sizes, speeds and the log.
downloader.py knows nothing about Qt and
ui.py never blocks. Every slow call — reading a link, downloading — happens on
a QThread and reports back through signals, so the window stays responsive.
platforms.py is the same idea applied to per-site behaviour. Rather than
scattering if tiktok: through the downloader and the window, each site is one
frozen Platform record carrying what it changes — the wording of the top
preset, whether watermarked formats are dropped, whether to prefer one finished
file over a merge. The two modules that care read those fields; adding a site is
an entry in a table. It is Qt-free and network-free, which is what lets the
window name a link on every keystroke without lagging.