This is Part 2 of the series.
Part 1 covered extracting profile data and video lists (via HasData + TikTok endpoints). Here, we’ll use those results to download videos.
Table of Contents
Requirements
You’ll need:
- Python 3.8+
- requests
- HasData API key (if you want to refresh links)
pip install requests
Basic downloader
Simple, memory-safe way to download each video file in chunks.
import json
import requests
import os
import time
DOWNLOAD_FOLDER = "tiktok_videos"
os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
HEADERS = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
"accept": "*/*",
"referer": "https://www.tiktok.com",
}
def download_video(url, filepath):
try:
with requests.get(url, headers=HEADERS, stream=True, timeout=30) as r:
r.raise_for_status()
with open(filepath, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"[OK] {filepath}")
return True
except requests.HTTPError as e:
print(f"[HTTP {e.response.status_code}] {filepath}")
return False
except Exception as e:
print(f"[ERROR] {filepath}: {e}")
return False
# Load video list from Part 1
with open("videos_clean_full.json", "r", encoding="utf-8") as f:
videos = json.load(f)
for video in videos:
vid = video.get("id")
url = video.get("downloadAddr")
if not url:
print(f"[WARN] No link for {vid}")
continue
path = os.path.join(DOWNLOAD_FOLDER, f"{vid}.mp4")
if os.path.exists(path):
print(f"[SKIP] {vid}")
continue
download_video(url, path)
time.sleep(0.5)
This handles streaming, writes to disk in chunks, and skips existing files.
Handling expired links
TikTok download links expire fast. If you reuse old links, you’ll get 403 Forbidden. When that happens, request a fresh link using HasData.
from bs4 import BeautifulSoup
import json
def refresh_download_url(video_page_url, api_key, country="DE"):
api_url = "https://api.hasdata.com/scrape/web"
payload = {
"url": video_page_url,
"proxyType": "residential",
"proxyCountry": country,
"blockResources": False,
"wait": 1000,
"jsRendering": True
}
headers = {
"Content-Type": "application/json",
"x-api-key": api_key
}
try:
r = requests.post(api_url, headers=headers, json=payload, timeout=30)
r.raise_for_status()
data = r.json()
html = data.get("content")
if not html:
return None
soup = BeautifulSoup(html, "html.parser")
script = soup.find("script", id="__UNIVERSAL_DATA_FOR_REHYDRATION__")
if not script or not script.string:
return None
raw = json.loads(script.string)
items = raw.get("__DEFAULT_SCOPE__", {}).get("webapp.video-detail", {}).get("itemModule", {})
for v in items.values():
d = v.get("video", {}).get("downloadAddr")
if d:
return d
except Exception as e:
print(f"[ERROR] refresh: {e}")
return None
Retry failed downloads and refresh URLs on 403.
MAX_RETRIES = 3
HASDATA_API_KEY = "YOUR-API-KEY"
for v in videos:
vid = v.get("id")
url = v.get("downloadAddr")
page = f"https://www.tiktok.com/@funny_funny66066/video/{vid}"
path = os.path.join(DOWNLOAD_FOLDER, f"{vid}.mp4")
attempts = 0
while attempts < MAX_RETRIES:
ok = download_video(url, path)
if ok:
break
attempts += 1
print(f"[RETRY] {vid} ({attempts}) — refreshing URL")
url = refresh_download_url(page, HASDATA_API_KEY) or url
time.sleep(1)
if not ok:
print(f"[FAIL] {vid}")
Notes
More useful resources:
Top comments (0)