douyin.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. """Douyin formal acquisition adapter."""
  2. from __future__ import annotations
  3. from typing import Any, Iterable
  4. from acquisition.content_mode import infer_content_mode
  5. from acquisition.crawler import fetch_post_detail
  6. from acquisition.platforms.base import PlatformCandidate, PlatformItem, PlatformSearchPage
  7. from acquisition.search import SearchHit, search_douyin_hits, search_douyin_page
  8. from core.config import Settings
  9. class DouyinAdapter:
  10. platform = "douyin"
  11. def search(
  12. self,
  13. query: str,
  14. *,
  15. settings: Settings,
  16. limit: int,
  17. rate_limiter: Any,
  18. ) -> list[PlatformCandidate]:
  19. hits = search_douyin_hits(
  20. query,
  21. content_type="视频",
  22. limit=limit,
  23. settings=settings,
  24. rate_limiter=rate_limiter,
  25. )
  26. return [
  27. PlatformCandidate(
  28. rank=i,
  29. platform=self.platform,
  30. provider=hit.provider,
  31. source_id=hit.content_id,
  32. raw={
  33. "id": hit.content_id,
  34. "search_provider": hit.provider,
  35. "original": hit.raw,
  36. "request_payload": hit.request_payload,
  37. },
  38. )
  39. for i, hit in enumerate(hits, start=1)
  40. ]
  41. def search_pages(
  42. self,
  43. query: str,
  44. *,
  45. settings: Settings,
  46. limit: int,
  47. rate_limiter: Any,
  48. max_pages: int = 2,
  49. ) -> Iterable[PlatformSearchPage]:
  50. provider = "piaoquantv"
  51. cursors: dict[str, str] = {}
  52. rank = 1
  53. seen: set[str] = set()
  54. for page_index in range(1, max_pages + 1):
  55. page = search_douyin_page(
  56. query,
  57. provider=provider,
  58. cursors=cursors,
  59. content_type="视频",
  60. limit=limit,
  61. settings=settings,
  62. rate_limiter=rate_limiter,
  63. )
  64. candidates: list[PlatformCandidate] = []
  65. for page_rank, hit in enumerate(page.rows, start=1):
  66. if not isinstance(hit, SearchHit):
  67. continue
  68. if not hit.content_id or hit.content_id in seen:
  69. continue
  70. seen.add(hit.content_id)
  71. candidates.append(
  72. PlatformCandidate(
  73. rank=rank,
  74. platform=self.platform,
  75. provider=hit.provider,
  76. source_id=hit.content_id,
  77. raw={
  78. "id": hit.content_id,
  79. "search_provider": hit.provider,
  80. "original": hit.raw,
  81. "request_payload": hit.request_payload,
  82. "page_index": page_index,
  83. "page_rank": page_rank,
  84. "source_cursor": page.cursor,
  85. },
  86. )
  87. )
  88. rank += 1
  89. yield PlatformSearchPage(
  90. page_index=page_index,
  91. candidates=candidates,
  92. raw_count=page.raw_count,
  93. cursor=page.cursor,
  94. next_cursor=page.next_cursor,
  95. has_more=page.has_more,
  96. provider=page.provider,
  97. request_payload=page.request_payload,
  98. raw_metadata=page.raw_metadata,
  99. )
  100. provider = page.provider
  101. cursors = dict(page.raw_metadata.get("cursors") or cursors)
  102. current_cursor = page.cursor
  103. if not page.has_more or not page.next_cursor or page.next_cursor == current_cursor:
  104. break
  105. def fetch_detail(
  106. self,
  107. candidate: PlatformCandidate,
  108. *,
  109. settings: Settings,
  110. rate_limiter: Any,
  111. ) -> PlatformItem:
  112. if not candidate.provider:
  113. raise RuntimeError("douyin candidate missing search provider")
  114. post = fetch_post_detail(
  115. candidate.source_id,
  116. platform=self.platform,
  117. provider=candidate.provider,
  118. settings=settings,
  119. rate_limiter=rate_limiter,
  120. )
  121. content_mode = infer_content_mode(
  122. platform=self.platform,
  123. content_type=post.content_type,
  124. body_text=post.body_text,
  125. image_urls=post.image_urls,
  126. video_urls=post.video_urls,
  127. raw=post.raw if isinstance(post.raw, dict) else {},
  128. )
  129. return PlatformItem(
  130. platform=self.platform,
  131. provider=post.provider or candidate.provider,
  132. source_id=post.content_id or candidate.source_id,
  133. url=post.url or candidate.url,
  134. content_type=post.content_type or "",
  135. content_mode=content_mode,
  136. title=post.title or candidate.title,
  137. author=post.author_name or candidate.author,
  138. body_text=post.body_text or "",
  139. image_urls=post.image_urls,
  140. video_urls=post.video_urls,
  141. raw=post.raw if isinstance(post.raw, dict) else {},
  142. )