fetch_video.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. #!/usr/bin/env python3
  2. """fetch_video.py — 拉片第一步: 拿到完整视频.
  3. 用法:
  4. # 抖音分享链接 / 视频ID → VOC 网关取详情+完整下载
  5. python3 fetch_video.py --url "https://v.douyin.com/xxxx/" --out-dir /tmp/lapian
  6. python3 fetch_video.py --aweme-id 7677548065384877346 --platform douyin --out-dir /tmp/lapian
  7. # 已有本地视频 → 直接登记
  8. python3 fetch_video.py --local /path/video.mp4 --out-dir /tmp/lapian --platform douyin
  9. 产物: <out-dir>/video.mp4 (完整) + meta.json (作者/统计/标签/时长)
  10. 铁律: 视频必须完整下载, 用 ffprobe 验证时长, 不许 Range 截断。
  11. """
  12. import argparse
  13. import json
  14. import os
  15. import re
  16. import subprocess
  17. import sys
  18. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  19. from common import (aweme_summary, download_file, extract_aweme_detail, # noqa: E402
  20. ffprobe_duration, get_api_token, slugify_platform, voc_call)
  21. def resolve_share_url(url: str) -> str:
  22. """短链接 → 最终 URL(带重定向链)。"""
  23. import urllib.request
  24. req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
  25. with urllib.request.urlopen(req, timeout=30) as r:
  26. return r.geturl()
  27. def extract_aweme_id(final_url: str) -> str:
  28. m = re.search(r"/(?:video|note)/(\d+)", final_url) or re.search(r"aweme_id=(\d+)", final_url)
  29. if not m:
  30. raise SystemExit(f"无法从 URL 提取视频ID: {final_url}")
  31. return m.group(1)
  32. def fetch_meta(aweme_id: str, platform: str) -> dict:
  33. """VOC 网关取视频详情(抖音)。其他平台留 TODO。"""
  34. if platform != "douyin":
  35. print(f"[fetch] WARN 平台 {platform} 详情接口未接入, 仅下载/登记")
  36. return {}
  37. resp = voc_call("douyin/web/fetch_one_video", {"aweme_id": aweme_id})
  38. if resp.get("error"):
  39. raise SystemExit(f"[fetch] VOC fetch_one_video 失败: {resp['error']}")
  40. return aweme_summary(extract_aweme_detail(resp))
  41. def main():
  42. ap = argparse.ArgumentParser(description="下载/登记待拉片视频")
  43. ap.add_argument("--url", help="抖音分享短链或含视频ID的页面URL")
  44. ap.add_argument("--aweme-id", help="视频ID(数字)")
  45. ap.add_argument("--local", help="已有本地视频文件路径(跳过下载)")
  46. ap.add_argument("--platform", default="douyin",
  47. help="平台英文简称: douyin/xiaohongshu/weixin/bilibili/tiktok/shortdrama")
  48. ap.add_argument("--out-dir", required=True, help="工作目录(产物放这里)")
  49. args = ap.parse_args()
  50. os.makedirs(args.out_dir, exist_ok=True)
  51. platform = slugify_platform(args.platform)
  52. meta, video_path = {}, os.path.join(args.out_dir, "video.mp4")
  53. if args.local:
  54. video_path = os.path.abspath(args.local)
  55. print(f"[fetch] 使用本地视频: {video_path}")
  56. else:
  57. if not (args.url or args.aweme_id):
  58. ap.error("需要 --url / --aweme-id / --local 之一")
  59. aweme_id = args.aweme_id
  60. if args.url:
  61. final = resolve_share_url(args.url)
  62. aweme_id = extract_aweme_id(final)
  63. print(f"[fetch] 视频ID: {aweme_id} (平台: {platform})")
  64. meta = fetch_meta(aweme_id, platform)
  65. urls = meta.get("play_urls") or meta.get("download_urls")
  66. if not urls:
  67. raise SystemExit("[fetch] 拿不到播放地址(可能视频被删/私密/风控)")
  68. if os.path.exists(video_path) and ffprobe_duration(video_path) > 1:
  69. print(f"[fetch] 复用已缓存 {video_path}")
  70. else:
  71. size = download_file(urls[0], video_path)
  72. print(f"[fetch] 下载完成: {size/1e6:.1f} MB")
  73. meta["aweme_id"] = meta.get("aweme_id") or aweme_id
  74. dur = ffprobe_duration(video_path)
  75. if dur <= 1:
  76. raise SystemExit(f"[fetch] 视频时长异常({dur}s), 文件可能损坏")
  77. print(f"[fetch] 时长验证通过: {dur:.1f}s")
  78. meta.update({"video_path": os.path.abspath(video_path),
  79. "duration_s": round(dur, 2), "platform": platform})
  80. meta_path = os.path.join(args.out_dir, "meta.json")
  81. json.dump(meta, open(meta_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
  82. print(f"[fetch] meta.json 已写入: 作者={meta.get('author')} 统计={meta.get('stats')}")
  83. if __name__ == "__main__":
  84. main()