返回 douyin-downloader
mix_downloader.py
根目录 / core / mix_downloader.py
1 from __future__ import annotations
2
3 from typing import Any, Dict, List, Optional
4
5 from core.downloader_base import BaseDownloader, DownloadResult
6 from core.user_modes.base_strategy import BaseUserModeStrategy
7 from utils.logger import setup_logger
8
9 logger = setup_logger("MixDownloader")
10
11
12 class MixDownloader(BaseDownloader):
13 async def download(self, parsed_url: Dict[str, Any]) -> DownloadResult:
14 result = DownloadResult()
15
16 mix_id = parsed_url.get("mix_id")
17 if not mix_id:
18 logger.error("No mix_id found in parsed URL")
19 return result
20
21 aweme_list = await self._collect_mix_aweme_list(str(mix_id))
22
23 result.total = len(aweme_list)
24 self._progress_set_item_total(result.total, "合集作品待下载")
25 self._progress_update_step("下载合集", f"mix_id={mix_id},待处理 {result.total} 条")
26
27 mix_detail = await self._get_mix_detail(str(mix_id))
28 author_name = (
29 (mix_detail.get("author") or {}).get("nickname")
30 if isinstance(mix_detail, dict)
31 else None
32 ) or "mix"
33
34 async def _process_aweme(item: Dict[str, Any]):
35 aweme_id = item.get("aweme_id")
36 if not aweme_id:
37 self._progress_advance_item("failed", "missing_aweme_id")
38 return {"status": "failed", "aweme_id": None}
39
40 if not await self._should_download(str(aweme_id)):
41 self._progress_advance_item("skipped", str(aweme_id))
42 return {"status": "skipped", "aweme_id": aweme_id}
43
44 success = await self._download_aweme_assets(item, author_name, mode="mix")
45 status = "success" if success else "failed"
46 self._progress_advance_item(status, str(aweme_id))
47 return {"status": status, "aweme_id": aweme_id}
48
49 download_results = await self.queue_manager.download_batch(_process_aweme, aweme_list)
50 for entry in download_results:
51 status = entry.get("status") if isinstance(entry, dict) else None
52 if status == "success":
53 result.success += 1
54 elif status == "skipped":
55 result.skipped += 1
56 else:
57 result.failed += 1
58 return result
59
60 async def _collect_mix_aweme_list(self, mix_id: str) -> List[Dict[str, Any]]:
61 fetch_mix_aweme = getattr(self.api_client, "get_mix_aweme", None)
62 if not callable(fetch_mix_aweme):
63 logger.error("API client has no get_mix_aweme implementation")
64 return []
65
66 aweme_list: List[Dict[str, Any]] = []
67 has_more = True
68 cursor = 0
69 number_limit = int(self.config.get("number", {}).get("mix", 0) or 0)
70
71 while has_more:
72 await self.rate_limiter.acquire()
73 raw_page = await fetch_mix_aweme(mix_id, cursor=cursor, count=20)
74 page = BaseUserModeStrategy._normalize_page_data(raw_page)
75 items = page.get("items", [])
76 if not items:
77 break
78
79 for item in items:
80 aweme = self._extract_aweme_from_item(item)
81 if aweme:
82 aweme_list.append(aweme)
83
84 if number_limit > 0 and len(aweme_list) >= number_limit:
85 aweme_list = aweme_list[:number_limit]
86 break
87
88 has_more = bool(page.get("has_more", False))
89 next_cursor = int(page.get("max_cursor", 0) or 0)
90 if has_more and next_cursor == cursor:
91 logger.warning(
92 "Mix pagination cursor did not advance (%s), stop to avoid loop",
93 cursor,
94 )
95 break
96 cursor = next_cursor
97
98 return aweme_list
99
100 async def _get_mix_detail(self, mix_id: str) -> Optional[Dict[str, Any]]:
101 getter = getattr(self.api_client, "get_mix_detail", None)
102 if not callable(getter):
103 return None
104 try:
105 return await getter(mix_id)
106 except Exception as exc:
107 logger.warning("Get mix detail failed: %s", exc)
108 return None
109
110 @staticmethod
111 def _extract_aweme_from_item(item: Any) -> Optional[Dict[str, Any]]:
112 if not isinstance(item, dict):
113 return None
114 if item.get("aweme_id"):
115 return item
116 for key in ("aweme", "aweme_info", "aweme_detail"):
117 value = item.get(key)
118 if isinstance(value, dict) and value.get("aweme_id"):
119 return value
120 return None
121
121 lines PYTHON