From f2bc9519035c398d8ca7562e7c11de02fd6c1ec4 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 17 Sep 2026 16:39:49 +0800 Subject: [PATCH] fix: add A0 path using App Feed API for Douyin extraction --- apps/api/app/api/routes/scripts_ai.py | 65 ++++++++++++++++++--------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/apps/api/app/api/routes/scripts_ai.py b/apps/api/app/api/routes/scripts_ai.py index 2fd9078a1..929560698 100644 --- a/apps/api/app/api/routes/scripts_ai.py +++ b/apps/api/app/api/routes/scripts_ai.py @@ -356,7 +356,7 @@ def _fetch_ttwid(): # 方式一:直接调 ttwid 注册接口 try: with httpx.Client(timeout=8, follow_redirects=True, verify=False) as http: - http.post( + r = http.post( "https://ttwid.bytedance.com/ttwid/union/register/", json={ "region": "cn", @@ -769,7 +769,7 @@ def _ytdlp_download_and_local_asr(page_url, temp_dir, cookiefile=None): cookie_files_to_try.append(cookiefile) # 2. 合成 cookie 重试(复用 _ytdlp_extract 的 cookie 生成逻辑,8次重试) - import time as _time + import secrets, time as _time ttwid = _fetch_ttwid() for i in range(8): if i % 3 == 0: @@ -945,10 +945,9 @@ def _html_scrape_direct_url(page_url, timeout=15): @router.get("/douyin/__debug_diag") def douyin_diag(): """[Staging only] 容器内网络/yt-dlp诊断""" - import tempfile as _tf - import time as _t - import httpx as _httpx + import time as _t + import tempfile as _tf import yt_dlp as _ytdlp results = {} @@ -1085,6 +1084,22 @@ def douyin_diag(): # 6. yt-dlp version results["ytdlp_version"] = _ytdlp.version.__version__ + + # 7. App Feed API 直连测试(路径 A0) + try: + from packages.douyin_parser import fetch_douyin_video_url as _feed_fetch + ft0 = _t.time() + f_url, f_desc = _feed_fetch(page_url, timeout=10, max_retries=2) + results["feed_api"] = { + "ok": bool(f_url), + "desc": (f_desc or "")[:100], + "url_domain": f_url.split("/")[2] if f_url and "/" in f_url else None, + "url_prefix": f_url[:100] if f_url else None, + "time": round(_t.time() - ft0, 2), + } + except Exception as e: + results["feed_api"] = {"ok": False, "error": str(e)[:200]} + results["total_time"] = round(_t.time() - t0, 2) return results @@ -1101,6 +1116,18 @@ def extract_from_douyin( page_url = _canonicalize_douyin_url(page_url) _dbg("page_url", page_url) + # 路径 A0:抖音 App Feed API 直连(零依赖、无需 cookies/签名、最稳定,优先级最高) + # 使用 Android 客户端 aid=1128 接口,直接返回 aweme_list 含无水印直链 + direct_url = None + feed_desc = "" + try: + from packages.douyin_parser import fetch_douyin_video_url as _feed_fetch + direct_url, feed_desc = _feed_fetch(page_url, timeout=15, max_retries=3) + if direct_url: + logger.info("抖音 App Feed API 直连成功: url=%s play_len=%d", page_url, len(direct_url)) + except Exception as exc: # noqa: BLE001 + logger.warning("抖音 App Feed API 异常: %s", exc) + text = "" duration = 0.0 cookiefile = _resolve_cookies_file() @@ -1108,23 +1135,21 @@ def extract_from_douyin( # 整体重试:外层最多 2 轮完整链路,应对第三方 API 瞬时限流/CDN 抖动 max_rounds = 2 - direct_url = None meta_duration = 0.0 for round_idx in range(max_rounds): - direct_url = None - meta_duration = 0.0 - - # 路径 A1:第三方解析 API(最稳定,apizero 付费时 99.88% 可用) - logger.info("抖音解析第%d轮开始: url=%s", round_idx + 1, page_url) - try: - direct_url, meta_duration = _parser_extract_direct_url(page_url) - if direct_url: - logger.info( - "第三方解析 API 成功(第%d轮): url_domain=%s", - round_idx + 1, direct_url.split("/")[2] if "/" in direct_url else "?", - ) - except Exception as exc: # noqa: BLE001 - logger.warning("第三方解析 API 异常(第%d轮): %s", round_idx + 1, exc) + # 路径 A0 已成功则跳过 A1/A2/A3;否则走兜底链路 + if not direct_url: + # 路径 A1:第三方解析 API(apizero 付费时可用) + logger.info("抖音解析第%d轮开始: url=%s a0_ok=%s", round_idx + 1, page_url, bool(direct_url)) + try: + direct_url, meta_duration = _parser_extract_direct_url(page_url) + if direct_url: + logger.info( + "第三方解析 API 成功(第%d轮): url_domain=%s", + round_idx + 1, direct_url.split("/")[2] if "/" in direct_url else "?", + ) + except Exception as exc: # noqa: BLE001 + logger.warning("第三方解析 API 异常(第%d轮): %s", round_idx + 1, exc) # 路径 A2:yt-dlp 拿直链(作为兜底,需要 cookie 概率性成功) if not direct_url: