douyin-video-downloader
抖音账号视频批量下载器 — 输入抖音号,自动拉取账号全部作品并解析无水印下载链接,支持批量一键下载到本地。适用场景:竞品账号内容拆解与逐帧学习、收藏喜欢的创作者全部作品避免被删、批量搬运素材二次剪辑创作、保存自己账号作品做本地备份防丢失、课程/教程类视频离线囤货反复看、甲方要求提供无水印源文件、达人合作前快速拉取对方作品做背调分析。触发词:抖音主页下载、抖音视频下载、抖音批量下载、抖音无水印下载、下载抖音作品、抖音账号视频提取、抖音视频保存、抖音去水印、抖音视频备份、竞品视频下载。
Install
npx skills add https://github.com/redfox-data/redfox-community/tree/main/skills/douyin-video-downloader
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install redfox-data-redfox-community@llmmart
git clone https://github.com/redfox-data/redfox-community.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole redfox-data/redfox-community collection as a plugin from our marketplace. Git is the plain clone.
README
抖音账号视频批量下载器 / douyin-video-downloader
简介
输入抖音号,自动拉取账号近期作品并解析无水印下载链接,支持一键批量下载到本地。
核心价值
- 无水印直链:解析后的视频/图文链接已去除抖音水印,可直接用于二次创作或备份。
- 批量高效:一次输入抖音号,自动完成作品拉取、链接解析、文件下载全流程。
- 灵活筛选:支持按发布时间范围、分页浏览,精准定位目标作品。
适用对象
- 🎬 内容创作者 — 收藏学习竞品账号的优质内容,逐帧拆解拍摄与剪辑技巧。
- 📦 运营 / MCN — 批量备份达人合作作品,快速完成背调分析。
- 🎓 知识学习者 — 离线囤积教程类视频,随时反复观看。
功能特性
核心功能
- 作品拉取:通过抖音号获取账号近期作品,包含标题、互动数据和作品链接。
- 链接解析:逐条解析视频/图文的无水印下载直链,支持视频、封面、音频多种资源类型。
- 批量下载:一键下载全部作品到本地,视频和图文均支持。
- 分页浏览:支持翻页查看更多作品,每页最多 50 条。
- 日期筛选:按作品发布时间范围精确筛选,支持自然语言识别(如「最近一周」「7月份的作品」)。
- 多账号支持:可同时输入多个抖音号,批量处理。
密钥获取与安全说明
- 本技能需要使用环境变量:
REDFOX_API_KEY。 REDFOX_API_KEY由 红狐 hub (https://redfox.hk)提供。- 请前往 红狐 hub 注册账号,获取
REDFOX_API_KEY。 - 配置设备环境变量
REDFOX_API_KEY后使用本技能。 - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
- 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。
使用指南
直接用自然语言描述需求,无需记忆命令。
常用说法速查
| 意图 | 示例话术 | 效果 |
|---|---|---|
| 下载单个账号视频 | 「帮我下载 Fish688688 的抖音视频」 | 拉取作品列表并解析下载链接 |
| 批量下载多个账号 | 「把 Fish688688 和 YuZhouXiaoLi1220 的视频都下载下来」 | 多账号依次拉取并下载 |
| 按时间筛选 | 「下载这个账号 7 月份的作品」 | 自动识别日期范围,精准筛选 |
| 查看更多作品 | 「继续看下一页」 | 翻页获取更多作品 |
输出示例
解析完成后,你将看到类似以下格式的作品表格(示意):
| # | 发布时间 | 作品 | 赞 | 评论 | 收藏 | 分享 | 资源下载 |
|---|---|---|---|---|---|---|---|
| 1 | 07-28 | 作品标题 | 5.2k | 67 | 689 | 5.8k | 🎬视频 · 🖼封面 · 🎵音频 |
表格底部会显示可下载作品数和失败数,并提示是否批量下载到本地。
使用场景
| 场景 | 角色 | 示例问法 | 收益 |
|---|---|---|---|
| 竞品内容拆解 | 创作者 / 编导 | 「帮我下载这个竞品账号的视频,逐帧学习」 | 离线反复观看,拆解拍摄手法 |
| 作品备份防丢 | 博主 / 运营 | 「把我账号的视频都备份一份到本地」 | 防止平台删除或账号异常丢失内容 |
| 达人背调分析 | MCN / 品牌 | 「把这个达人的作品全部拉下来看看」 | 合作前快速了解内容风格与数据表现 |
| 教程离线囤货 | 学习者 | 「把这个教程账号的视频下载下来慢慢学」 | 无需联网,随时反复观看学习 |
Skill manifest
抖音账号视频批量下载器
输入抖音号 → 拉取作品列表 → 解析下载链接 → 一键下载到本地
简介
抖音账号主页下视频批量下载工具。只需提供抖音号,自动拉取该账号近期作品列表,逐条解析无水印视频/图文下载直链,支持一键批量下载到本地。
功能特性
| 功能 | 说明 |
|---|---|
| 📋 作品拉取 | 通过抖音号获取账号近期作品(标题、互动数据、作品链接) |
| 🔗 链接解析 | 逐条调用解析接口,获取视频/图文无水印下载直链 |
| 📥 批量下载 | 一键下载全部作品(视频+图文)到本地 output/ 目录 |
| 📊 数据展示 | Markdown 表格 / JSON 双格式输出,含完整互动数据 |
| 📄 分页翻页 | 支持翻页查看更多作品 |
| 📅 日期筛选 | 可按作品发布时间挑选 |
| ⚡ 速率保护 | 内置请求间隔,避免触发频率限制 |
API 说明
本 Skill 调用两个 redfox.hk 接口:
| 步骤 | 接口 | 说明 |
|---|---|---|
| 1. 拉取作品 | POST /story/api/dy/data/listWorkByAccount |
根据抖音号获取作品列表 |
| 2. 解析下载 | POST /story/api/parseWork/videoDownload/douyin |
根据作品链接获取无水印下载直链 |
认证
前往 红狐hub 获取 API Key。
# 环境变量配置(推荐)
export REDFOX_API_KEY=ak_你的密钥
# Windows PowerShell
$env:REDFOX_API_KEY="ak_你的密钥"
使用方式
CLI 命令行
# 基础用法:拉取作品并解析下载链接(不下载文件)
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688"
# 下载视频到本地
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --download
# 指定下载目录
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --download --output-dir ./my_videos
# JSON 格式输出
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --json
# 多账号(逗号分隔)
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --accounts "Fish688688,YuZhouXiaoLi1220" --download
# 指定作品数量(默认10,最多50)
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --count 20 --download
# 翻页查看更多作品
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --page 2
# 按日期范围筛选作品
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --date-start 2026-07-01 --date-end 2026-07-31
# 组合使用:第2页 + 日期过滤 + 下载
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --page 2 --count 20 --date-start 2026-06-01 --download
参数说明
| 参数 | 说明 |
|---|---|
--account |
单个抖音号 |
--accounts |
多个抖音号,逗号分隔 |
--count |
拉取作品数量(默认10,最多50) |
--page |
页码(默认1) |
--date-start |
起始日期 YYYY-MM-DD |
--date-end |
结束日期 YYYY-MM-DD |
--download |
下载视频文件到本地 |
--output-dir |
下载目录(默认 output/) |
--json |
JSON 格式输出 |
--rate-limit |
请求间隔秒数(默认 1.0) |
--api-key |
指定 API Key |
依赖
| 依赖 | 安装命令 |
|---|---|
requests |
pip3 install requests |
抖音号要求
接口通过 uniqueName(抖音展示ID)查询,脚本将用户提供的抖音号统一按 uniqueName 传入,userId 和 shortId 固定为空字符串。必须提供抖音号。
重要: 若用户只输入账号名称(如"李佳琦""老高與小茉")而未提供抖音号,必须提示:
"抖音账号名称存在多个重名情况,请提供准确的抖音号以便精准查询。"
并附上获取抖音号的示例图供用户参考:

抖音号是抖音 APP → 目标账号主页 → 昵称下方显示的唯一 ID(如
Fish688688、YuZhouXiaoLi1220),非中文昵称。
数据规则
失败阈值保护
同一抖音号在 6 小时内累计 5 次 API 调用失败后,后续请求将被拒绝
拒绝时提示:「当前账号下载已超过失败阈值,请联系客服邮箱 redfoxdata@proton.me 处理」
距上次失败超过 6 小时,计数自动归零,恢复正常调用
同一账号查询成功(API 返回正常数据),计数归零,恢复正常调用
失败计数持久化至
~/.qoder/douyin_video_downloader_failures.json
注意: 仅记录因网络超时、API 错误码(3108/3106/3107/400 等)导致的调用失败;账号暂无作品数据(空数据)不计入失败次数。
Agent 集成指南
触发词
抖音下载 / 抖音视频下载
抖音批量下载 / 抖音无水印下载
下载抖音作品 / 抖音账号视频提取
帮我下载 xxx 的抖音视频
Agent 执行流程
Step 1: 确认用户意图与抖音号
- 若用户提供了抖音号(如 Fish688688),直接执行
- 若用户只提供了账号名称(中文昵称,如"李佳琦"),必须提示:
"抖音账号名称存在多个重名情况,请提供准确的抖音号以便精准查询。"
并展示获取抖音号的示例图:https://lyy.redfox.hk/page/ljq.png
Step 2: 识别用户输入的时间范围(如有)
- 若用户提到了作品时间范围,自动识别并转换为 --date-start / --date-end:
- "7.1~7.20" → --date-start 2026-07-01 --date-end 2026-07-20
- "7月1日到7月20日" → --date-start 2026-07-01 --date-end 2026-07-20
- "最近一周" → 当前日期往前推 7 天
- "上个月" → 上个月 1 日到上个月最后一天
- "7月份的作品" → --date-start 2026-07-01 --date-end 2026-07-31
- 日期格式固定为 YYYY-MM-DD,年份默认为当前年份
- 若用户要求"继续"/"下一页",添加 --page N
Step 3: 调用脚本拉取作品 + 解析下载链接
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "抖音号"
Step 4: 展示结果 + 询问翻页
- 展示 Markdown 表格,含可点击的作品链接和下载链接
- 若有失败:提示「可能是用户已删除该视频,如需数据核查可联系工作人员邮箱 redfoxdata@proton.me 处理」
- 若可翻页:告诉用户「还有更多作品,是否需要翻看下一页?」,输入下一页翻页
- 提示用户支持按时间范围提取作品
Step 5: 询问用户是否需要批量下载
- 使用 AskUserQuestion:「是否需要将能下载的视频批量下载到本地?」
- 选项:「下载到本地」/「只看链接即可」
Step 6: 用户确认后执行下载
python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "抖音号" --download
- 告知用户文件保存路径
Agent 输出规范
解析完成后按以下格式输出(自然语言风格,面向普通用户)。
⛔ 强制规则:禁止简化输出
Agent 必须原样展示脚本返回的完整表格,禁止以下行为:
- ❌ 禁止删除或合并任何列(发布时间、作品、赞、评论、收藏、分享、资源下载 缺一不可)
- ❌ 禁止省略翻页提示行("还有更多作品,输入
--page 2翻看下一页")- ❌ 禁止省略时间范围筛选提示("💡 支持输入想提取的作品时间范围…")
- ❌ 禁止省略下载询问提示("💾 需要将这 X 条作品批量下载到本地吗?")
- ❌ 禁止用「...」或摘要代替完整表格内容
- ✅ 必须逐行展示所有作品的完整信息(含资源下载链接)
输出模板
## 📥 抖音视频下载 — @账号名(粉丝: X)
当前是**第 1 页**,共 10 条作品 | 还有更多作品,输入 `--page 2` 翻看下一页
| # | 发布时间 | 作品 | 赞 | 评论 | 收藏 | 分享 | 资源下载 |
| --- | -------- | ------------------------------------------ | ---- | ---- | ---- | ---- | --------------------------------------------- |
| 1 | 07-28 | [作品标题](https://www.iesdouyin.com/...) | 5.2k | 67 | 689 | 5.8k | [🎬视频](...) · [🖼封面](...) · [🎵音频](...) |
| 2 | 07-24 | [作品标题2](https://www.iesdouyin.com/...) | 3.1k | 22 | 136 | 865 | [🖼封面](...) |
| 3 | … | … | … | … | … | … | … |
**合计:** 10 条作品,8 条可下载,2 条下载失败
> ⚠️ 下载失败的视频可能是用户已删除该视频,如需数据核查可联系工作人员邮箱 **redfoxdata@proton.me** 处理。
> 💡 需要提取特定时间范围的作品?直接告诉我时间范围即可,如「7.1~7.20」「最近一周」
> 💾 需要将这 X 条作品批量下载到本地吗?直接告诉我即可。
常见问题
Q:如何获取 API Key? A:前往 redfox.hk 注册获取。
Q:下载的视频有水印吗? A:无水印。API 返回的视频/图文直链已去除抖音水印。
Q:图文作品能下载吗? A:可以。图文作品(幻灯片、相册类)会自动下载首张无水印图片,格式为 JPG/PNG/WebP。
Q:为什么提示「调用频率超限」?
A:API 返回 code=3108 时表示请求过快触发了限流。可增加 --rate-limit 2.0 延长间隔。
Q:支持哪些抖音号格式?
A:支持抖音展示 ID(uniqueName),如 Fish688688、YuZhouXiaoLi1
Files (redfox-community)
-
scripts
-
douyin_video_downloader.py 38.8 KB
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 抖音账号视频下载器 — 根据抖音号拉取作品列表,解析无水印下载链接,批量下载到本地 ================================================================================ 基于 redfox.hk API: 1. POST /story/api/dy/data/listWorkByAccount — 拉取账号作品列表 2. POST /story/api/parseWork/videoDownload/douyin — 解析视频下载链接 Usage: # 拉取作品并展示下载链接(不下载文件) python3 douyin_video_downloader.py --account "Fish688688" # 下载视频到本地 python3 douyin_video_downloader.py --account "Fish688688" --download # 多账号 + 指定数量 python3 douyin_video_downloader.py --accounts "Fish688688,id2" --count 20 --download # JSON 输出 python3 douyin_video_downloader.py --account "Fish688688" --json """ import argparse import json import os import re import sys import time import warnings from datetime import datetime from pathlib import Path from typing import Optional from urllib.parse import unquote, urlparse # Windows 终端 UTF-8 编码修复 if sys.platform == "win32": try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") sys.stderr.reconfigure(encoding="utf-8", errors="replace") except Exception: pass import requests warnings.filterwarnings("ignore", category=Warning) warnings.filterwarnings("ignore", message=".*NotOpenSSLWarning.*") # ─── 配置 ───────────────────────────────────────────────────────────────────────── API_BASE = "https://redfox.hk" WORKS_ENDPOINT = "/story/api/dy/data/listWorkByAccount" DOWNLOAD_ENDPOINT = "/story/api/parseWork/videoDownload/douyin" ENV_KEY = "REDFOX_API_KEY" CONFIG_FILE = Path.home() / ".qoder" / "apis" / "redfox.json" DEFAULT_PAGE_SIZE = 10 MAX_PAGE_SIZE = 50 DEFAULT_RATE_LIMIT = 1.0 # ─── 失败阈值保护配置 ──────────────────────────────────────────────────────────── FAILURES_FILE = Path.home() / ".qoder" / "douyin_video_downloader_failures.json" RATE_LIMIT_MAX_FAILURES = 5 # 6 小时内累计失败次数上限 RATE_LIMIT_WINDOW_HOURS = 6 # 失败计数窗口 SUPPORT_EMAIL = "redfoxdata@proton.me" # ─── 终端颜色 ────────────────────────────────────────────────────────────────────── GREEN = "\033[92m" YELLOW = "\033[93m" RED = "\033[91m" CYAN = "\033[96m" BOLD = "\033[1m" RESET = "\033[0m" def info(msg: str): print(f"{GREEN}[OK]{RESET} {msg}") def warn(msg: str): print(f"{YELLOW}[!!]{RESET} {msg}") def error(msg: str): print(f"{RED}[XX]{RESET} {msg}") def step(msg: str): print(f"{CYAN}[>>]{RESET} {msg}") # ─── API Key 管理 ────────────────────────────────────────────────────────────────── def get_api_key(cli_key: Optional[str] = None) -> Optional[str]: """获取 API Key:CLI > 环境变量 > 配置文件""" if cli_key: return cli_key env_key = os.environ.get(ENV_KEY) if env_key: return env_key if CONFIG_FILE.exists(): try: data = json.loads(CONFIG_FILE.read_text(encoding="utf-8")) key = data.get("api_key") if key: return key except (json.JSONDecodeError, OSError): pass return None # ─── 数字格式化 ──────────────────────────────────────────────────────────────────── def format_number(n) -> str: """格式化数字: ≥1亿→x.x亿, ≥1万→x.xw, ≥1千→x.xk, 其余逗号分隔""" if n is None: return "-" try: n = int(n) except (ValueError, TypeError): return str(n) if n >= 100000000: return f"{n / 100000000:.1f}亿" if n >= 10000: return f"{n / 10000:.1f}w" if n >= 1000: return f"{n / 1000:.1f}k" return f"{n:,}" # ─── 抖音号识别 ──────────────────────────────────────────────────────────────────── def _is_chinese(text: str) -> bool: """判断输入是否包含中文字符(说明可能是昵称而非抖音号)""" return bool(re.search(r'[\u4e00-\u9fff]', text)) def validate_account_id(account_id: str) -> tuple[bool, str]: """ 验证输入是否为合法的抖音号格式。 若包含中文字符,说明用户输入的是昵称而非抖音号,需要提示。 Returns: (is_valid, message) """ if _is_chinese(account_id): return False, ( f"「{account_id}」看起来是账号昵称而非抖音号。\n" "抖音账号名称存在多个重名情况,请提供准确的抖音号以便精准查询。\n" "抖音号是抖音 APP → 目标账号主页 → 昵称下方显示的唯一 ID(如 Fish688688)。" ) if not account_id or len(account_id) < 2: return False, f"「{account_id}」不是有效的抖音号,请提供正确的抖音号。" return True, "" # ─── 失败计数 / 频率限制 ──────────────────────────────────────────────────────── def _load_failures() -> dict: """加载失败记录""" if FAILURES_FILE.exists(): try: return json.loads(FAILURES_FILE.read_text(encoding="utf-8")) except Exception: pass return {} def _save_failures(failures: dict): """保存失败记录""" FAILURES_FILE.parent.mkdir(parents=True, exist_ok=True) FAILURES_FILE.write_text(json.dumps(failures, ensure_ascii=False, indent=2), encoding="utf-8") def _check_rate_limit(account_id: str) -> tuple[bool, str]: """ 检查是否超过失败阈值(6h 内 5 次失败)。 Returns: (blocked: bool, message: str) """ failures = _load_failures() key = account_id.strip().lower() record = failures.get(key) if not record: return False, "" last_fail_time = record.get("lastFailTime", 0) fail_count = record.get("count", 0) # 距上次失败超过窗口时间,计数归零 if time.time() - last_fail_time > RATE_LIMIT_WINDOW_HOURS * 3600: del failures[key] _save_failures(failures) return False, "" # 窗口内失败达上限,拒绝调用 if fail_count >= RATE_LIMIT_MAX_FAILURES: return True, ( f"当前账号「{account_id}」下载已超过失败阈值," f"请联系客服邮箱 {SUPPORT_EMAIL} 处理" ) return False, "" def _record_failure(account_id: str): """记录一次失败""" failures = _load_failures() key = account_id.strip().lower() record = failures.get(key, {"count": 0, "lastFailTime": 0}) # 距上次失败超过窗口,重置计数 if time.time() - record.get("lastFailTime", 0) > RATE_LIMIT_WINDOW_HOURS * 3600: record = {"count": 0, "lastFailTime": 0} record["count"] += 1 record["lastFailTime"] = time.time() failures[key] = record _save_failures(failures) def _record_success(account_id: str): """成功后计数归零""" failures = _load_failures() key = account_id.strip().lower() if key in failures: del failures[key] _save_failures(failures) # ─── 文件名清理 ──────────────────────────────────────────────────────────────────── def safe_filename(text: str) -> str: """将文本转为安全的文件名(去除非法字符、限制长度)""" text = re.sub(r'[\\/:*?"<>|\n\r\t]', '_', text) text = text.strip().strip('.') if len(text) > 80: text = text[:80] return text or "video" # ─── 核心下载器类 ────────────────────────────────────────────────────────────────── class DouyinVideoDownloader: """抖音账号视频下载器""" def __init__(self, api_key: str): self.api_key = api_key # ── 作品拉取 ──────────────────────────────────────────────────────────────── def fetch_works(self, account_id: str, page_size: int = DEFAULT_PAGE_SIZE, page_num: int = 1) -> dict: """ 拉取抖音账号作品列表 Args: account_id: 抖音号(uniqueName) page_size: 拉取数量(默认10,最多50) page_num: 页码(默认1) Returns: dict: { "success": bool, "account": dict | None, "works": list, "error": str | None, "rate_limited": bool # 是否因失败阈值被拒绝 } """ # 规则:失败阈值检查 blocked, block_msg = _check_rate_limit(account_id) if blocked: return {"success": False, "account": None, "works": [], "error": block_msg, "rate_limited": True} payload = { "uniqueName": account_id, "userId": "", "shortId": "", "pageNum": page_num, "pageSize": min(page_size, MAX_PAGE_SIZE), "source": "抖音主页作品提取-GitHub", } url = f"{API_BASE}{WORKS_ENDPOINT}" headers = { "Content-Type": "application/json", "REDFOX_API_KEY": self.api_key, } try: resp = requests.post(url, json=payload, headers=headers, timeout=30) result = resp.json() except requests.exceptions.Timeout: _record_failure(account_id) return {"success": False, "account": None, "works": [], "error": "请求超时,请稍后重试"} except requests.exceptions.RequestException as e: _record_failure(account_id) return {"success": False, "account": None, "works": [], "error": f"网络请求失败: {e}"} except json.JSONDecodeError: _record_failure(account_id) return {"success": False, "account": None, "works": [], "error": f"API 返回无效数据"} code = result.get("code") msg = result.get("msg", "") # 成功码:200 或 2000 if code in (200, 2000): data_raw = result.get("data", {}) if not data_raw: return {"success": False, "account": None, "works": [], "error": "未查询到该账号的作品数据,可能账号尚未收录"} # data 可能是 list 或 dict if isinstance(data_raw, list): works = data_raw elif isinstance(data_raw, dict): works = data_raw.get("list") or data_raw.get("articles") or data_raw.get("records") or [] else: works = [] # 提取账号信息(从第一条作品或 data 中) account_info = {"accountId": account_id} if isinstance(data_raw, dict): account_info["accountName"] = data_raw.get("authorName") or data_raw.get("nickname") or account_id if works: first = works[0] account_info["accountName"] = first.get("authorName") or first.get("nickname") or account_info.get("accountName", account_id) account_info["followerCount"] = first.get("authorFansCount") or first.get("followerCount", 0) account_info["secUid"] = first.get("authorSecUid") or first.get("secUid", "") # 映射字段 for w in works: w["title"] = w.get("content") or w.get("title") or "" w["workUrl"] = w.get("opusUrl") or w.get("workUrl") or "" w["accountName"] = w.get("authorName") or w.get("accountName") or account_id # 成功:失败计数归零 _record_success(account_id) return { "success": True, "account": account_info, "works": works, "error": None, } # 错误码处理 _record_failure(account_id) if code == 3108: return {"success": False, "account": None, "works": [], "error": "调用频率超限,请稍后重试或增加 --rate-limit 参数"} if code in (3106, 3107): return {"success": False, "account": None, "works": [], "error": f"API Key 无效 (code {code}),请检查配置"} if code == 400: return {"success": False, "account": None, "works": [], "error": f"请求参数错误: {msg}"} return {"success": False, "account": None, "works": [], "error": f"API 错误 (code {code}): {msg}"} # ── 视频解析 ──────────────────────────────────────────────────────────────── def get_download_info(self, work_url: str) -> dict: """ 解析视频下载链接 Args: work_url: 作品链接(opusUrl) Returns: dict: {"success": bool, "download_url": str|None, "title": str|None, "cover": str|None, "duration": int|None, "resources": list, "error": str|None} """ payload = {"url": work_url, "source": "抖音主页作品提取-GitHub"} url = f"{API_BASE}{DOWNLOAD_ENDPOINT}" headers = { "Content-Type": "application/json", "X-API-KEY": self.api_key, } try: resp = requests.post(url, json=payload, headers=headers, timeout=30) data = resp.json() except requests.exceptions.Timeout: return {"success": False, "download_url": None, "title": None, "cover": None, "duration": None, "resources": [], "error": "解析超时"} except requests.exceptions.RequestException as e: return {"success": False, "download_url": None, "title": None, "cover": None, "duration": None, "resources": [], "error": f"网络请求失败: {e}"} except json.JSONDecodeError: return {"success": False, "download_url": None, "title": None, "cover": None, "duration": None, "resources": [], "error": "API 返回无效数据"} code = data.get("code") msg = data.get("msg", "") if not str(code).startswith("2"): if code == 3108: err = "调用频率超限" elif code in (3106, 3107): err = "API Key 无效" elif code == 400: err = f"参数错误: {msg}" else: err = f"API 错误 (code {code}): {msg}" return {"success": False, "download_url": None, "title": None, "cover": None, "duration": None, "resources": [], "error": err} payload_data = data.get("data") if not payload_data: return {"success": False, "download_url": None, "title": None, "cover": None, "duration": None, "resources": [], "error": "API 返回空数据"} result = { "success": True, "download_url": None, "title": None, "cover": None, "duration": None, "resources": [], "error": None, } if isinstance(payload_data, dict): result["title"] = payload_data.get("desc") or payload_data.get("title") result["cover"] = payload_data.get("cover") or payload_data.get("coverUrl") # 提取时长 dur = payload_data.get("duration") or payload_data.get("durationSeconds") if isinstance(dur, (int, float)): result["duration"] = int(dur) # 提取 resources 列表 resources = payload_data.get("resources", []) if isinstance(resources, list): result["resources"] = resources for res in resources: if isinstance(res, dict): rtype = res.get("type", "") dl = res.get("downloadUrl") or res.get("url") # 视频类型:提取下载链接 if dl and rtype == "video" and not result["download_url"]: result["download_url"] = dl # 图片类型:提取封面 + 下载链接(图文作品可下载首图) if dl and rtype == "image": if not result["cover"]: result["cover"] = dl if not result["download_url"]: result["download_url"] = dl res_dur = res.get("durationSeconds") if isinstance(res_dur, (int, float)) and not result["duration"]: result["duration"] = int(res_dur) # 如果 resources 中没有,使用顶层字段 if not result["download_url"]: result["download_url"] = ( payload_data.get("videoUrl") or payload_data.get("video_url") or payload_data.get("downloadUrl") or payload_data.get("download_url") or payload_data.get("playUrl") ) return result # ── 视频下载 ──────────────────────────────────────────────────────────────── def download_video(self, download_url: str, output_path: str) -> bool: """ 下载视频文件到本地 Args: download_url: 视频下载直链 output_path: 保存路径 Returns: bool: 是否下载成功 """ try: resp = requests.get(download_url, timeout=120, stream=True) resp.raise_for_status() total = int(resp.headers.get("content-length", 0)) downloaded = 0 with open(output_path, "wb") as f: for chunk in resp.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) # 验证文件大小 if total > 0 and downloaded < total * 0.9: warn(f" 文件可能不完整: {downloaded}/{total} bytes") return False return True except requests.exceptions.RequestException as e: error(f" 下载失败: {e}") return False except OSError as e: error(f" 文件写入失败: {e}") return False # ─── Markdown 输出 ──────────────────────────────────────────────────────────────── def print_markdown_table(account: dict, results: list[dict], page_num: int = 1, page_size: int = DEFAULT_PAGE_SIZE): """ 输出 Markdown 格式的结果表格 ⛔ 强制规则:Agent 调用方必须原样展示本函数的完整输出,禁止以下行为: - 禁止删除或合并任何列(#、发布时间、作品、赞、评论、收藏、分享、资源下载 缺一不可) - 禁止省略翻页提示行 - 禁止省略时间范围筛选提示和下载询问提示 - 禁止用「...」或摘要代替完整表格内容 - 必须逐行展示所有作品信息(含完整资源下载链接) """ name = account.get("accountName", account.get("accountId", "未知")) fans = format_number(account.get("followerCount")) print(f"\n## 📥 抖音视频下载 — @{name}(粉丝: {fans})") print() # 翻页信息 total = len(results) page_info = f"当前是**第 {page_num} 页**,共 {total} 条作品" if total >= page_size: next_page = page_num + 1 page_info += f" | 还有更多作品,输入 `--page {next_page}` 翻看下一页" print(page_info) print() print("| # | 发布时间 | 作品 | 赞 | 评论 | 收藏 | 分享 | 资源下载 |") print("|---|----------|------|-----|------|------|------|------|") success_count = 0 fail_count = 0 for i, r in enumerate(results, 1): title_raw = (r.get("title") or "无标题")[:25] title = title_raw.replace("|", "\\|").replace("\n", " ") work_url = r.get("work_url", "") # 作品标题作为可点击链接 if work_url: title_display = f"[{title}]({work_url})" else: title_display = title pub_time = r.get("publishTime", "")[:10] if r.get("publishTime") else "-" likes = format_number(r.get("likeCount")) comments = format_number(r.get("commentCount")) collects = format_number(r.get("collectCount")) shares = format_number(r.get("shareCount")) # 资源下载列:合并展示视频/封面/音频所有可点击下载链接 resources = r.get("resources", []) resource_parts = [] seen_types = set() has_video = False for res in resources: if not isinstance(res, dict): continue rtype = res.get("type", "") rurl = res.get("downloadUrl") or res.get("url") or "" if rtype and rurl and rtype not in seen_types: seen_types.add(rtype) if rtype == "video": has_video = True resource_parts.append(f"[视频]({rurl})") elif rtype == "image": resource_parts.append(f"[封面]({rurl})") elif rtype == "audio": resource_parts.append(f"[音频]({rurl})") else: resource_parts.append(f"[{rtype}]({rurl})") # 如果 resources 没有封面但顶层有 cover 字段,补充 cover = r.get("cover") if cover and "image" not in seen_types: resource_parts.append(f"[封面]({cover})") resource_display = " · ".join(resource_parts) if resource_parts else "-" if r.get("download_success") and has_video: success_count += 1 elif r.get("download_success"): # API 成功,无视频但有图片(图文作品,也可下载) success_count += 1 else: fail_count += 1 print(f"| {i} | {pub_time} | {title_display} | {likes} | {comments} | {collects} | {shares} | {resource_display} |") print() # 汇总 parts = [f"{success_count} 条可下载"] if fail_count > 0: parts.append(f"{fail_count} 条下载失败") print(f"**合计:** {total} 条作品,{','.join(parts)}") if fail_count > 0: print(f"\n> ⚠️ 下载失败的视频可能是用户已删除该视频,如需数据核查可联系工作人员邮箱 **redfoxdata@proton.me** 处理。") print(f"\n> 💡 支持输入想提取的作品时间范围,如 `--date-start 2026-07-01 --date-end 2026-07-20`") if success_count > 0: print(f"> 💾 需要将这 {success_count} 条作品批量下载到本地吗?直接告诉我即可。") # ─── JSON 输出 ──────────────────────────────────────────────────────────────────── def print_json_output(account: dict, results: list[dict]): """输出 JSON 格式结果""" output = { "account": account, "total": len(results), "success": sum(1 for r in results if r.get("download_success")), "failed": sum(1 for r in results if not r.get("download_success")), "results": results, "generated_at": datetime.now().isoformat(), } print(json.dumps(output, ensure_ascii=False, indent=2)) # ─── 主处理流程 ──────────────────────────────────────────────────────────────────── def process_account( downloader: DouyinVideoDownloader, account_id: str, page_size: int, page_num: int = 1, date_start: str = "", date_end: str = "", rate_limit: float = DEFAULT_RATE_LIMIT, do_download: bool = False, output_dir: str = "", ) -> tuple[dict, list[dict]]: """ 处理单个账号:拉取作品 → 日期过滤 → 解析下载链接 → 下载视频 Returns: (account_info, results_list) """ # Step 1: 拉取作品 step(f"拉取账号作品: {account_id}" + (f" (日期 {date_start}~{date_end})" if date_start or date_end else "")) works_result = downloader.fetch_works(account_id, page_size, page_num=page_num) if not works_result["success"]: error(f"拉取失败: {works_result['error']}") return works_result.get("account") or {"accountId": account_id}, [] account = works_result["account"] works = works_result["works"] info(f"拉取到 {len(works)} 条作品 — {account.get('accountName', account_id)}") if not works: warn("该账号暂无作品数据") return account, [] # Step 1.5: 客户端日期过滤(在解析下载链接之前,避免浪费 API 配额) if date_start or date_end: original_count = len(works) filtered = [] for w in works: pt = w.get("publishTime", "") if not pt: filtered.append(w) # 无发布时间的不筛选 continue if date_start and pt[:10] < date_start: continue if date_end and pt[:10] > date_end: continue filtered.append(w) works = filtered skipped = original_count - len(works) if skipped > 0: info(f"日期过滤:{original_count} → {len(works)} 条(跳过 {skipped} 条)") if not works: warn("日期范围内暂无作品") return account, [] # Step 2: 逐条解析下载链接 results = [] total = len(works) for i, work in enumerate(works, 1): work_url = work.get("workUrl") or work.get("opusUrl") or "" title = work.get("title") or "无标题" print(f"\n [{i}/{total}] {CYAN}解析:{RESET} {title[:50]}{'...' if len(title) > 50 else ''}") if not work_url: warn(f" 无作品链接,跳过") results.append({ "title": title, "work_url": "", "likeCount": work.get("likeCount", 0), "shareCount": work.get("shareCount", 0), "commentCount": work.get("commentCount", 0), "collectCount": work.get("collectCount", 0), "publishTime": work.get("publishTime", ""), "download_success": False, "download_error": "无作品链接", "download_url": None, "cover": None, "duration": None, "resources": [], "local_path": None, }) continue dl_result = downloader.get_download_info(work_url) result_entry = { "title": title, "work_url": work_url, "likeCount": work.get("likeCount", 0), "shareCount": work.get("shareCount", 0), "commentCount": work.get("commentCount", 0), "collectCount": work.get("collectCount", 0), "publishTime": work.get("publishTime", ""), "download_success": dl_result["success"], "download_error": dl_result.get("error"), "download_url": dl_result.get("download_url"), "cover": dl_result.get("cover"), "duration": dl_result.get("duration"), "resources": dl_result.get("resources", []), "local_path": None, } if dl_result["success"] and dl_result.get("download_url"): # 判断媒体类型:视频 or 图片 resources = dl_result.get("resources", []) is_image_post = ( not any(r.get("type") == "video" for r in resources if isinstance(r, dict)) and any(r.get("type") == "image" for r in resources if isinstance(r, dict)) ) media_label = "图片" if is_image_post else "视频" info(f" 解析成功({media_label})" + (f" | 时长: {dl_result['duration']}s" if dl_result.get("duration") else "")) # Step 3: 下载 if do_download: dl_url = dl_result["download_url"] # 生成文件名 pub_time = work.get("publishTime", "") time_part = pub_time[:10].replace("-", "") if pub_time else datetime.now().strftime("%Y%m%d") safe_title = safe_filename(title) # 确定扩展名:视频优先 mp4,图片优先 jpg VIDEO_EXTS = ("mp4", "mov", "webm", "avi") IMAGE_EXTS = ("jpg", "jpeg", "png", "webp", "gif", "bmp") ext = ".jpg" if is_image_post else ".mp4" parsed = urlparse(dl_url) path_part = unquote(parsed.path) if "." in path_part.rsplit("/", 1)[-1]: url_ext = path_part.rsplit(".", 1)[-1].split("?")[0].lower() if is_image_post and url_ext in IMAGE_EXTS: ext = f".{url_ext}" elif not is_image_post and url_ext in VIDEO_EXTS: ext = f".{url_ext}" filename = f"{time_part}_{safe_title}{ext}" out_path = os.path.join(output_dir, filename) print(f" {CYAN}下载中...{RESET}", end="", flush=True) if downloader.download_video(dl_url, out_path): file_size = os.path.getsize(out_path) size_mb = file_size / (1024 * 1024) info(f"\r 下载完成: {filename} ({size_mb:.1f} MB)") result_entry["local_path"] = out_path else: warn(f"\r 下载失败") result_entry["download_success"] = False result_entry["download_error"] = "文件下载失败" else: warn(f" 解析失败: {dl_result.get('error', '未知错误')}") results.append(result_entry) # 速率限制 if i < total: time.sleep(rate_limit) return account, results # ─── 主入口 ──────────────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description="抖音账号视频下载器 — 拉取作品 + 解析下载链接 + 批量下载", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 示例: python3 douyin_video_downloader.py --account "Fish688688" python3 douyin_video_downloader.py --account "Fish688688" --download python3 douyin_video_downloader.py --accounts "id1,id2" --count 20 --download python3 douyin_video_downloader.py --account "Fish688688" --json """, ) parser.add_argument("--account", "-a", help="单个抖音号") parser.add_argument("--accounts", help="多个抖音号,逗号分隔") parser.add_argument("--count", "-c", type=int, default=DEFAULT_PAGE_SIZE, help=f"拉取作品数量(默认{DEFAULT_PAGE_SIZE},最多{MAX_PAGE_SIZE})") parser.add_argument("--page", "-p", type=int, default=1, help="页码(默认1)") parser.add_argument("--date-start", help="起始日期 YYYY-MM-DD,筛选此日期之后的作品") parser.add_argument("--date-end", help="结束日期 YYYY-MM-DD,筛选此日期之前的作品") parser.add_argument("--download", "-d", action="store_true", help="下载视频文件到本地") parser.add_argument("--output-dir", "-o", default="", help="下载目录(默认 output/)") parser.add_argument("--json", "-j", action="store_true", help="JSON 格式输出") parser.add_argument("--rate-limit", "-r", type=float, default=DEFAULT_RATE_LIMIT, help=f"请求间隔秒数(默认{DEFAULT_RATE_LIMIT})") parser.add_argument("--api-key", "-k", help="API Key") args = parser.parse_args() # ── 收集账号列表 ── account_ids = [] if args.account: account_ids.append(args.account.strip()) if args.accounts: account_ids.extend([a.strip() for a in args.accounts.split(",") if a.strip()]) if not account_ids: error("请提供至少一个抖音号(--account 或 --accounts)") sys.exit(1) # ── 抖音号格式验证(拒绝中文昵称输入)── invalid_ids = [] for aid in account_ids: valid, msg = validate_account_id(aid) if not valid: invalid_ids.append((aid, msg)) if invalid_ids: print(f"\n{RED}{BOLD}⚠️ 以下输入不是有效的抖音号:{RESET}\n") for aid, msg in invalid_ids: print(f" {YELLOW}• {msg}{RESET}\n") print(f"{CYAN}💡 抖音号是抖音 APP → 目标账号主页 → 昵称下方显示的唯一 ID(如 Fish688688)。{RESET}") print(f"{CYAN} 获取方式参考:https://lyy.redfox.hk/page/ljq.png{RESET}\n") sys.exit(2) # ── API Key ── api_key = get_api_key(cli_key=args.api_key) if not api_key: error("未找到 API Key,请设置 REDFOX_API_KEY 环境变量或使用 --api-key 参数") print(f" 获取 Key: https://redfox.hk/settings/api-keys?source=github") sys.exit(1) # ── 下载目录 ── output_dir = args.output_dir or os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "output") if args.download: os.makedirs(output_dir, exist_ok=True) # ── Banner ── print(f"""{CYAN}{BOLD} ╔══════════════════════════════════════════╗ ║ 抖音账号视频下载器 ║ ║ Douyin Account Video Downloader ║ ╚══════════════════════════════════════════╝{RESET} """) info(f"API Key 已加载 | 账号数: {len(account_ids)} | 作品数/账号: {min(args.count, MAX_PAGE_SIZE)}") downloader = DouyinVideoDownloader(api_key) all_accounts = [] all_results = [] for idx, account_id in enumerate(account_ids): if idx > 0: print(f"\n{CYAN}{'─' * 50}{RESET}\n") account, results = process_account( downloader, account_id, page_size=args.count, page_num=args.page, date_start=args.date_start or "", date_end=args.date_end or "", rate_limit=args.rate_limit, do_download=args.download, output_dir=output_dir, ) all_accounts.append(account) all_results.append((account, results)) # ── 输出结果 ── print(f"\n{CYAN}{BOLD}{'=' * 50}{RESET}\n") if args.json: # JSON 聚合输出 output = { "accounts": [], "total_works": 0, "total_success": 0, "total_failed": 0, "generated_at": datetime.now().isoformat(), } for account, results in all_results: output["accounts"].append({ "account": account, "total": len(results), "success": sum(1 for r in results if r.get("download_success")), "failed": sum(1 for r in results if not r.get("download_success")), "results": results, }) output["total_works"] += len(results) output["total_success"] += sum(1 for r in results if r.get("download_success")) output["total_failed"] += sum(1 for r in results if not r.get("download_success")) print(json.dumps(output, ensure_ascii=False, indent=2)) else: # Markdown 表格输出 for account, results in all_results: if results: print_markdown_table(account, results, page_num=args.page, page_size=args.count) else: name = account.get("accountName", account.get("accountId", "未知")) warn(f"@{name}:暂无作品或拉取失败") # 汇总 total_works = sum(len(r) for _, r in all_results) total_success = sum(sum(1 for x in r if x.get("download_success")) for _, r in all_results) total_failed = total_works - total_success print(f"\n{BOLD}总计:{RESET}{len(account_ids)} 个账号,{total_works} 条作品,成功 {total_success} 条,失败 {total_failed} 条") if args.download: info(f"视频文件保存至: {os.path.abspath(output_dir)}") # 退出码 total_success_final = sum(sum(1 for x in r if x.get("download_success")) for _, r in all_results) total_works_final = sum(len(r) for _, r in all_results) sys.exit(0 if total_success_final == total_works_final else 1) if __name__ == "__main__": main()
-
-
README.en.md 4.7 KB
# Douyin Account Video Batch Downloader / douyin-video-downloader --- ## Introduction Enter a Douyin ID to automatically fetch the account's recent videos, parse watermark-free download links, and batch download them to your local device. **Core Value** - **Watermark-Free Links**: Parsed video and image links are free of Douyin watermarks, ready for re-editing or backup. - **Efficient Batch Processing**: One Douyin ID handles the entire workflow — fetching, parsing, and downloading. - **Flexible Filtering**: Filter by publish date range or browse page by page to find exactly what you need. **Who Is It For** - 🎬 **Content Creators** — Save and study competitors' best content frame by frame. - 📦 **Operations / MCN** — Batch back up creator collaborations and run quick due diligence. - 🎓 **Learners** — Download tutorial videos for offline, anytime rewatching. --- ## Features ### Core Features - **Video Fetching**: Retrieve an account's recent videos by Douyin ID, including titles, engagement data, and video links. - **Link Parsing**: Parse watermark-free download links for each video, cover image, and audio track. - **Batch Download**: Download all videos and images to your local device in one go. - **Pagination**: Browse more videos page by page, up to 50 items per page. - **Date Filtering**: Filter videos by publish date range, with natural language support (e.g., "last week", "July videos"). - **Multi-Account Support**: Process multiple Douyin IDs at once. --- ## API Key Acquisition & Security - This skill requires the environment variable: `REDFOX_API_KEY`. - `REDFOX_API_KEY` is provided by [RedFoxHub](https://redfox.hk/settings/api-keys?source=github) (`https://redfox.hk`). - Please visit [RedFoxHub](https://redfox.hk?source=github) to register and obtain your `REDFOX_API_KEY`. - Configure the `REDFOX_API_KEY` environment variable on your device before using this skill. - Before providing a key, verify its source, scope, validity period, and whether it supports reset/revocation. - Never hardcode or expose the key in plain text in code, prompts, logs, or output files. --- ## Usage Guide Just describe what you need in natural language — no commands to memorize. ### Quick Reference | Intent | Example | Result | | -------------------------------- | ------------------------------------------------------ | ----------------------------------------------- | | Download one account's videos | "Download Fish688688's Douyin videos" | Fetch videos and parse download links | | Batch download multiple accounts | "Download videos from Fish688688 and YuZhouXiaoLi1220" | Process multiple accounts in sequence | | Filter by time | "Download this account's July videos" | Auto-recognize date range for precise filtering | | Browse more | "Show me the next page" | Flip to the next page of videos | ### Output Example After parsing, you'll see a video table like this (illustrative): | # | Date | Video | Likes | Comments | Saves | Shares | Downloads | | --- | ----- | ------------------- | ----- | -------- | ----- | ------ | --------------------------- | | 1 | 07-28 | [Video Title](link) | 5.2k | 67 | 689 | 5.8k | 🎬Video · 🖼Cover · 🎵Audio | Below the table, you'll see how many videos are downloadable and how many failed, with an option to batch download them all. --- ## Use Cases | Scenario | Role | Example Request | Benefit | | --------------------- | ------------------ | ------------------------------------------------------------ | ---------------------------------------------------------------------- | | Competitor analysis | Creator / Director | "Download this competitor's videos for frame-by-frame study" | Offline review of shooting and editing techniques | | Content backup | Blogger / Operator | "Back up all my account's videos locally" | Protect against platform deletion or account issues | | Creator due diligence | MCN / Brand | "Pull all of this creator's videos for review" | Quick assessment of content style and performance before collaboration | | Tutorial archiving | Learner | "Download this tutorial account's videos for later" | No internet needed — rewatch anytime | --- -
README.md 3.7 KB
# 抖音账号视频批量下载器 / douyin-video-downloader --- ## 简介 输入抖音号,自动拉取账号近期作品并解析无水印下载链接,支持一键批量下载到本地。 **核心价值** - **无水印直链**:解析后的视频/图文链接已去除抖音水印,可直接用于二次创作或备份。 - **批量高效**:一次输入抖音号,自动完成作品拉取、链接解析、文件下载全流程。 - **灵活筛选**:支持按发布时间范围、分页浏览,精准定位目标作品。 **适用对象** - 🎬 **内容创作者** — 收藏学习竞品账号的优质内容,逐帧拆解拍摄与剪辑技巧。 - 📦 **运营 / MCN** — 批量备份达人合作作品,快速完成背调分析。 - 🎓 **知识学习者** — 离线囤积教程类视频,随时反复观看。 --- ## 功能特性 ### 核心功能 - **作品拉取**:通过抖音号获取账号近期作品,包含标题、互动数据和作品链接。 - **链接解析**:逐条解析视频/图文的无水印下载直链,支持视频、封面、音频多种资源类型。 - **批量下载**:一键下载全部作品到本地,视频和图文均支持。 - **分页浏览**:支持翻页查看更多作品,每页最多 50 条。 - **日期筛选**:按作品发布时间范围精确筛选,支持自然语言识别(如「最近一周」「7月份的作品」)。 - **多账号支持**:可同时输入多个抖音号,批量处理。 --- ## 密钥获取与安全说明 - 本技能需要使用环境变量:`REDFOX_API_KEY`。 - `REDFOX_API_KEY` 由 [红狐 hub](https://redfox.hk/settings/api-keys?source=github) (`https://redfox.hk`)提供。 - 请前往 [红狐 hub](https://redfox.hk?source=github) 注册账号,获取 `REDFOX_API_KEY`。 - 配置设备环境变量 `REDFOX_API_KEY` 后使用本技能。 - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。 - 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。 --- ## 使用指南 直接用自然语言描述需求,无需记忆命令。 ### 常用说法速查 | 意图 | 示例话术 | 效果 | | ---- | -------- | ---- | | 下载单个账号视频 | 「帮我下载 Fish688688 的抖音视频」 | 拉取作品列表并解析下载链接 | | 批量下载多个账号 | 「把 Fish688688 和 YuZhouXiaoLi1220 的视频都下载下来」 | 多账号依次拉取并下载 | | 按时间筛选 | 「下载这个账号 7 月份的作品」 | 自动识别日期范围,精准筛选 | | 查看更多作品 | 「继续看下一页」 | 翻页获取更多作品 | ### 输出示例 解析完成后,你将看到类似以下格式的作品表格(示意): | # | 发布时间 | 作品 | 赞 | 评论 | 收藏 | 分享 | 资源下载 | |---|----------|------|-----|------|------|------|------| | 1 | 07-28 | [作品标题](链接) | 5.2k | 67 | 689 | 5.8k | 🎬视频 · 🖼封面 · 🎵音频 | 表格底部会显示可下载作品数和失败数,并提示是否批量下载到本地。 --- ## 使用场景 | 场景 | 角色 | 示例问法 | 收益 | | ---- | ---- | -------- | ---- | | 竞品内容拆解 | 创作者 / 编导 | 「帮我下载这个竞品账号的视频,逐帧学习」 | 离线反复观看,拆解拍摄手法 | | 作品备份防丢 | 博主 / 运营 | 「把我账号的视频都备份一份到本地」 | 防止平台删除或账号异常丢失内容 | | 达人背调分析 | MCN / 品牌 | 「把这个达人的作品全部拉下来看看」 | 合作前快速了解内容风格与数据表现 | | 教程离线囤货 | 学习者 | 「把这个教程账号的视频下载下来慢慢学」 | 无需联网,随时反复观看学习 | --- -
SKILL.md 11.3 KB
--- name: douyin-video-downloader description: 抖音账号视频批量下载器 — 输入抖音号,自动拉取账号全部作品并解析无水印下载链接,支持批量一键下载到本地。适用场景:竞品账号内容拆解与逐帧学习、收藏喜欢的创作者全部作品避免被删、批量搬运素材二次剪辑创作、保存自己账号作品做本地备份防丢失、课程/教程类视频离线囤货反复看、甲方要求提供无水印源文件、达人合作前快速拉取对方作品做背调分析。触发词:抖音主页下载、抖音视频下载、抖音批量下载、抖音无水印下载、下载抖音作品、抖音账号视频提取、抖音视频保存、抖音去水印、抖音视频备份、竞品视频下载。 --- # 抖音账号视频批量下载器 > 输入抖音号 → 拉取作品列表 → 解析下载链接 → 一键下载到本地 --- ## 简介 抖音账号主页下视频批量下载工具。只需提供抖音号,自动拉取该账号近期作品列表,逐条解析无水印视频/图文下载直链,支持一键批量下载到本地。 --- ## 功能特性 | 功能 | 说明 | | ----------- | ------------------------------------------------------ | | 📋 作品拉取 | 通过抖音号获取账号近期作品(标题、互动数据、作品链接) | | 🔗 链接解析 | 逐条调用解析接口,获取视频/图文无水印下载直链 | | 📥 批量下载 | 一键下载全部作品(视频+图文)到本地 `output/` 目录 | | 📊 数据展示 | Markdown 表格 / JSON 双格式输出,含完整互动数据 | | 📄 分页翻页 | 支持翻页查看更多作品 | | 📅 日期筛选 | 可按作品发布时间挑选 | | ⚡ 速率保护 | 内置请求间隔,避免触发频率限制 | --- ## API 说明 本 Skill 调用两个 redfox.hk 接口: | 步骤 | 接口 | 说明 | | ----------- | ------------------------------------------------ | ------------------------------ | | 1. 拉取作品 | `POST /story/api/dy/data/listWorkByAccount` | 根据抖音号获取作品列表 | | 2. 解析下载 | `POST /story/api/parseWork/videoDownload/douyin` | 根据作品链接获取无水印下载直链 | ### 认证 前往 [红狐hub](https://redfox.hk/settings/api-keys?source=github) 获取 API Key。 ```bash # 环境变量配置(推荐) export REDFOX_API_KEY=ak_你的密钥 # Windows PowerShell $env:REDFOX_API_KEY="ak_你的密钥" ``` --- ## 使用方式 ### CLI 命令行 ```bash # 基础用法:拉取作品并解析下载链接(不下载文件) python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" # 下载视频到本地 python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --download # 指定下载目录 python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --download --output-dir ./my_videos # JSON 格式输出 python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --json # 多账号(逗号分隔) python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --accounts "Fish688688,YuZhouXiaoLi1220" --download # 指定作品数量(默认10,最多50) python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --count 20 --download # 翻页查看更多作品 python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --page 2 # 按日期范围筛选作品 python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --date-start 2026-07-01 --date-end 2026-07-31 # 组合使用:第2页 + 日期过滤 + 下载 python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "Fish688688" --page 2 --count 20 --date-start 2026-06-01 --download ``` ### 参数说明 | 参数 | 说明 | | -------------- | ------------------------------ | | `--account` | 单个抖音号 | | `--accounts` | 多个抖音号,逗号分隔 | | `--count` | 拉取作品数量(默认10,最多50) | | `--page` | 页码(默认1) | | `--date-start` | 起始日期 YYYY-MM-DD | | `--date-end` | 结束日期 YYYY-MM-DD | | `--download` | 下载视频文件到本地 | | `--output-dir` | 下载目录(默认 `output/`) | | `--json` | JSON 格式输出 | | `--rate-limit` | 请求间隔秒数(默认 1.0) | | `--api-key` | 指定 API Key | ### 依赖 | 依赖 | 安装命令 | | ---------- | ----------------------- | | `requests` | `pip3 install requests` | --- ## 抖音号要求 接口通过 `uniqueName`(抖音展示ID)查询,脚本将用户提供的抖音号统一按 `uniqueName` 传入,`userId` 和 `shortId` 固定为空字符串。**必须提供抖音号**。 **重要:** 若用户只输入账号名称(如"李佳琦""老高與小茉")而未提供抖音号,必须提示: > "抖音账号名称存在多个重名情况,请提供准确的**抖音号**以便精准查询。" 并附上获取抖音号的示例图供用户参考:  > **抖音号**是抖音 APP → 目标账号主页 → 昵称下方显示的唯一 ID(如 `Fish688688`、`YuZhouXiaoLi1220`),非中文昵称。 --- ## 数据规则 ### 失败阈值保护 - **同一抖音号在 6 小时内累计 5 次 API 调用失败后,后续请求将被拒绝** - 拒绝时提示:「当前账号下载已超过失败阈值,请联系客服邮箱 <redfoxdata@proton.me> 处理」 - **距上次失败超过 6 小时**,计数自动归零,恢复正常调用 - **同一账号查询成功**(API 返回正常数据),计数归零,恢复正常调用 - 失败计数持久化至 `~/.qoder/douyin_video_downloader_failures.json` > **注意:** 仅记录因网络超时、API 错误码(3108/3106/3107/400 等)导致的调用失败;账号暂无作品数据(空数据)不计入失败次数。 --- ## Agent 集成指南 ### 触发词 - 抖音下载 / 抖音视频下载 - 抖音批量下载 / 抖音无水印下载 - 下载抖音作品 / 抖音账号视频提取 - 帮我下载 xxx 的抖音视频 ### Agent 执行流程 ``` Step 1: 确认用户意图与抖音号 - 若用户提供了抖音号(如 Fish688688),直接执行 - 若用户只提供了账号名称(中文昵称,如"李佳琦"),必须提示: "抖音账号名称存在多个重名情况,请提供准确的抖音号以便精准查询。" 并展示获取抖音号的示例图:https://lyy.redfox.hk/page/ljq.png Step 2: 识别用户输入的时间范围(如有) - 若用户提到了作品时间范围,自动识别并转换为 --date-start / --date-end: - "7.1~7.20" → --date-start 2026-07-01 --date-end 2026-07-20 - "7月1日到7月20日" → --date-start 2026-07-01 --date-end 2026-07-20 - "最近一周" → 当前日期往前推 7 天 - "上个月" → 上个月 1 日到上个月最后一天 - "7月份的作品" → --date-start 2026-07-01 --date-end 2026-07-31 - 日期格式固定为 YYYY-MM-DD,年份默认为当前年份 - 若用户要求"继续"/"下一页",添加 --page N Step 3: 调用脚本拉取作品 + 解析下载链接 python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "抖音号" Step 4: 展示结果 + 询问翻页 - 展示 Markdown 表格,含可点击的作品链接和下载链接 - 若有失败:提示「可能是用户已删除该视频,如需数据核查可联系工作人员邮箱 redfoxdata@proton.me 处理」 - 若可翻页:告诉用户「还有更多作品,是否需要翻看下一页?」,输入下一页翻页 - 提示用户支持按时间范围提取作品 Step 5: 询问用户是否需要批量下载 - 使用 AskUserQuestion:「是否需要将能下载的视频批量下载到本地?」 - 选项:「下载到本地」/「只看链接即可」 Step 6: 用户确认后执行下载 python3 "$SKILL_PATH/scripts/douyin_video_downloader.py" --account "抖音号" --download - 告知用户文件保存路径 ``` ### Agent 输出规范 解析完成后按以下格式输出(自然语言风格,面向普通用户)。 #### ⛔ 强制规则:禁止简化输出 > **Agent 必须原样展示脚本返回的完整表格,禁止以下行为:** > > - ❌ 禁止删除或合并任何列(发布时间、作品、赞、评论、收藏、分享、资源下载 缺一不可) > - ❌ 禁止省略翻页提示行("还有更多作品,输入 `--page 2` 翻看下一页") > - ❌ 禁止省略时间范围筛选提示("💡 支持输入想提取的作品时间范围…") > - ❌ 禁止省略下载询问提示("💾 需要将这 X 条作品批量下载到本地吗?") > - ❌ 禁止用「...」或摘要代替完整表格内容 > - ✅ 必须逐行展示所有作品的完整信息(含资源下载链接) #### 输出模板 ```markdown ## 📥 抖音视频下载 — @账号名(粉丝: X) 当前是**第 1 页**,共 10 条作品 | 还有更多作品,输入 `--page 2` 翻看下一页 | # | 发布时间 | 作品 | 赞 | 评论 | 收藏 | 分享 | 资源下载 | | --- | -------- | ------------------------------------------ | ---- | ---- | ---- | ---- | --------------------------------------------- | | 1 | 07-28 | [作品标题](https://www.iesdouyin.com/...) | 5.2k | 67 | 689 | 5.8k | [🎬视频](...) · [🖼封面](...) · [🎵音频](...) | | 2 | 07-24 | [作品标题2](https://www.iesdouyin.com/...) | 3.1k | 22 | 136 | 865 | [🖼封面](...) | | 3 | … | … | … | … | … | … | … | **合计:** 10 条作品,8 条可下载,2 条下载失败 > ⚠️ 下载失败的视频可能是用户已删除该视频,如需数据核查可联系工作人员邮箱 **redfoxdata@proton.me** 处理。 > 💡 需要提取特定时间范围的作品?直接告诉我时间范围即可,如「7.1~7.20」「最近一周」 > 💾 需要将这 X 条作品批量下载到本地吗?直接告诉我即可。 ``` --- ## 常见问题 **Q:如何获取 API Key?** A:前往 [redfox.hk](https://redfox.hk/settings/api-keys?source=github) 注册获取。 **Q:下载的视频有水印吗?** A:无水印。API 返回的视频/图文直链已去除抖音水印。 **Q:图文作品能下载吗?** A:可以。图文作品(幻灯片、相册类)会自动下载首张无水印图片,格式为 JPG/PNG/WebP。 **Q:为什么提示「调用频率超限」?** A:API 返回 code=3108 时表示请求过快触发了限流。可增加 `--rate-limit 2.0` 延长间隔。 **Q:支持哪些抖音号格式?** A:支持抖音展示 ID(uniqueName),如 `Fish688688`、`YuZhouXiaoLi1`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.