The brief was small on paper. Collect a few hundred short vertical clips for a captioning experiment, no watermarks, consistent MP4, and the whole thing reproducible from a Python script so a teammate could rerun it next quarter. TikTok was the source. I write Python for a living, so my instinct was the obvious one. Write the script, point it at a list of URLs, walk away, come back to a folder of files.
It did not go that way. This is the log of what the code handled, what it choked on, and the unglamorous fallback that finished the job.
Starting where any Python dev would: yt-dlp as a library
Most people meet yt-dlp on the command line. The better fit for a pipeline is its Python API, because you get structured exceptions instead of scraping stdout. The core loop was about this small:
import yt_dlp
opts = {"format": "mp4", "outtmpl": "clips/%(id)s.%(ext)s"}
failed = []
with yt_dlp.YoutubeDL(opts) as ydl:
for url in urls:
try:
ydl.download([url])
except yt_dlp.utils.DownloadError as err:
failed.append((url, str(err)))
Wrapping each download in try/except and pushing failures into a list was the single most useful line I wrote. TikTok reshapes its frontend on its own schedule, and an extractor that worked on Monday throws a signature error on Thursday. Over two weeks I bumped the pinned yt-dlp version in requirements.txt three times. When the extractor was current it was excellent. When it lagged, the DownloadError count climbed, and without that failures queue the whole run would have died on the first bad URL instead of finishing the good ones.
Attempt two: the unofficial API wrappers
Then I reached for the community TikTok API wrappers and a couple of tikwm-style endpoints, called straight from Python with requests. Convenient, right up until they are rate limited. I spent an afternoon tuning time.sleep intervals and backoff logic before admitting the obvious. An unofficial endpoint is a guest in someone else's house, and it can ask you to leave mid-request. Roughly a fifth of the links came back empty or still watermarked through that route. Not something to anchor a reproducible dataset on.
The long tail the script could not close
About sixty clips were left that no amount of Python cleverness would finish. A few were region-flagged, several were photo slideshows the extractor flattened to a single frame, and a handful had licensed audio that came back silent. I did those by hand, and while I was in there I tested four browser tools on the same stubborn set, scoring them the way I would score anything: pass rate, and how much babysitting each one demanded.
The one that cleared all sixty without drama was tiksaver. Clean MP4, no logo, audio intact, and the slideshows came back as full image sets instead of one frame. No login, which mattered, because I was not about to attach an identity to a throwaway data task. I kept the tab open and fed it the stragglers one at a time, and it simply did not miss.
For the record, the other three. cobalt.tools came closest, honest about its method, no ads, fine on most of the awkward clips, but it stalled twice on the longest videos, which in a manual session means watching a spinner. snaptik handled plain clips but routed each download through a sponsored page and returned video only on the slideshows, useless for my set. A tikwm web frontend was fast and free on simple video, then muted two of the licensed-audio clips and choked on the region-flagged ones, the exact cases I needed it for.
The numbers, on the sixty hard clips
| Tool | Finished clean | Slideshows intact | Audio kept | Manual friction |
|---|---|---|---|---|
| tiksaver | 60 / 60 | yes | yes | low |
| cobalt.tools | 56 / 60 | yes | yes | medium, stalls |
| snaptik | 49 / 60 | no | yes | medium, ad pages |
| tikwm frontend | 44 / 60 | partial | no | low |
What I would put in the README for the next dev
Script the bulk with the yt-dlp Python API. Pin the version, wrap every download in try/except, and keep a failures list so one bad URL never sinks the run. Do not build anything load-bearing on unofficial endpoints. And accept that there is always a long tail the code will not reach, so keep one reliable manual tool within arm's reach. For me that was tiksaver, and the reason was dull in the best way. Paste a link, get a clean file, every time, and it never once asked who I was.
The experiment shipped. The dataset was clean, and the script sits in the repo for whoever runs it next in 2026. The lesson I keep relearning held again: good Python gets you eighty percent of the way, and the boring manual step gets you the rest.
