chanfasf commited on
Commit
c0b7de0
·
1 Parent(s): 5044619

Feat: use free TikTok crawler first with fallback to demo data

Browse files
backend/app/api/tasks.py CHANGED
@@ -87,7 +87,7 @@ def crawl_tiktok(
87
  max_results: int = Query(50, ge=1, le=50),
88
  db: Session = Depends(get_db),
89
  ):
90
- crawler = TikTokCrawler()
91
  videos = []
92
 
93
  if type == "hashtag" and keyword:
@@ -160,7 +160,7 @@ def crawl_tiktok_trending(
160
  count: int = Query(50, ge=1, le=50),
161
  db: Session = Depends(get_db),
162
  ):
163
- crawler = TikTokCrawler()
164
  videos = crawler.get_trending_videos(count=count)
165
  if not videos:
166
  return {"message": "未获取到数据,TikTok 爬虫可能被反爬限制", "count": 0}
@@ -175,7 +175,7 @@ def crawl_tiktok_most_viewed(
175
  count: int = Query(50, ge=1, le=50),
176
  db: Session = Depends(get_db),
177
  ):
178
- crawler = TikTokCrawler()
179
  videos = crawler.get_most_viewed_videos(hashtag=hashtag, count=count)
180
  if not videos:
181
  return {"message": "未获取到数据", "count": 0}
@@ -190,7 +190,7 @@ def crawl_tiktok_viral(
190
  count: int = Query(50, ge=1, le=50),
191
  db: Session = Depends(get_db),
192
  ):
193
- crawler = TikTokCrawler()
194
  videos = crawler.search_viral_candidates(keyword=keyword, count=count)
195
  if not videos:
196
  return {"message": "未获取到数据", "count": 0}
@@ -212,7 +212,7 @@ def crawl_all_platforms(
212
  yt_saved = yt_crawler.save_videos_to_db(yt_trending, db)
213
  results["youtube_trending"] = len(yt_saved)
214
 
215
- tt_crawler = TikTokCrawler()
216
  tt_trending = tt_crawler.get_trending_videos()
217
  if tt_trending:
218
  tt_saved = tt_crawler.save_videos_to_db(tt_trending, db)
@@ -250,15 +250,14 @@ def reset_and_crawl_real_data(db: Session = Depends(get_db)):
250
  except Exception as e:
251
  results["errors"].append(f"YouTube: {str(e)}")
252
 
253
- if settings.TIKHUB_ENABLED:
254
- try:
255
- tt_crawler = TikTokCrawler(tikhub_api_key=settings.TIKHUB_API_KEY)
256
- tt_videos = tt_crawler.get_trending_videos(count=50)
257
- if tt_videos:
258
- saved = tt_crawler.save_videos_to_db(tt_videos, db)
259
- results["tiktok"] = len(saved)
260
- except Exception as e:
261
- results["errors"].append(f"TikTok: {str(e)}")
262
 
263
  if results["youtube"] > 0 or results["tiktok"] > 0:
264
  _run_post_crawl_analysis(db)
 
87
  max_results: int = Query(50, ge=1, le=50),
88
  db: Session = Depends(get_db),
89
  ):
90
+ crawler = TikTokCrawler(use_free_first=True)
91
  videos = []
92
 
93
  if type == "hashtag" and keyword:
 
160
  count: int = Query(50, ge=1, le=50),
161
  db: Session = Depends(get_db),
162
  ):
163
+ crawler = TikTokCrawler(use_free_first=True)
164
  videos = crawler.get_trending_videos(count=count)
165
  if not videos:
166
  return {"message": "未获取到数据,TikTok 爬虫可能被反爬限制", "count": 0}
 
175
  count: int = Query(50, ge=1, le=50),
176
  db: Session = Depends(get_db),
177
  ):
178
+ crawler = TikTokCrawler(use_free_first=True)
179
  videos = crawler.get_most_viewed_videos(hashtag=hashtag, count=count)
180
  if not videos:
181
  return {"message": "未获取到数据", "count": 0}
 
190
  count: int = Query(50, ge=1, le=50),
191
  db: Session = Depends(get_db),
192
  ):
193
+ crawler = TikTokCrawler(use_free_first=True)
194
  videos = crawler.search_viral_candidates(keyword=keyword, count=count)
195
  if not videos:
196
  return {"message": "未获取到数据", "count": 0}
 
212
  yt_saved = yt_crawler.save_videos_to_db(yt_trending, db)
213
  results["youtube_trending"] = len(yt_saved)
214
 
215
+ tt_crawler = TikTokCrawler(use_free_first=True)
216
  tt_trending = tt_crawler.get_trending_videos()
217
  if tt_trending:
218
  tt_saved = tt_crawler.save_videos_to_db(tt_trending, db)
 
250
  except Exception as e:
251
  results["errors"].append(f"YouTube: {str(e)}")
252
 
253
+ try:
254
+ tt_crawler = TikTokCrawler(tikhub_api_key=settings.TIKHUB_API_KEY, use_free_first=True)
255
+ tt_videos = tt_crawler.get_trending_videos(count=50)
256
+ if tt_videos:
257
+ saved = tt_crawler.save_videos_to_db(tt_videos, db)
258
+ results["tiktok"] = len(saved)
259
+ except Exception as e:
260
+ results["errors"].append(f"TikTok: {str(e)}")
 
261
 
262
  if results["youtube"] > 0 or results["tiktok"] > 0:
263
  _run_post_crawl_analysis(db)
backend/app/crawlers/tiktok.py CHANGED
@@ -2,6 +2,7 @@ import logging
2
  import re
3
  import json
4
  import os
 
5
  from datetime import datetime, timezone
6
  from typing import Optional
7
 
@@ -12,43 +13,321 @@ from app.models.video import Video, VideoSnapshot, Platform
12
 
13
  logger = logging.getLogger(__name__)
14
 
 
 
 
 
 
 
 
15
 
16
  class TikTokCrawler:
17
- def __init__(self, tikhub_api_key: Optional[str] = None):
18
  self.tikhub_api_key = tikhub_api_key or os.environ.get("TIKHUB_API_KEY")
19
  self.tikhub_base_url = "https://api.tikhub.io"
 
20
 
21
  self.session = requests.Session()
22
  self.session.headers.update({
23
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
24
- "Accept": "application/json, text/plain, */*",
25
- "Accept-Language": "en-US,en;q=0.9",
26
- "Referer": "https://www.tiktok.com/",
 
 
 
 
 
 
 
 
 
 
27
  })
28
 
29
  def get_trending_videos(self, count: int = 50) -> list[dict]:
30
- if self.tikhub_api_key:
31
- videos = self._get_via_tikhub("trending", count)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  if videos:
33
  return videos
34
 
35
- return self._get_via_scraper("trending", count)
36
 
37
  def get_most_viewed_videos(self, hashtag: str = "", count: int = 50) -> list[dict]:
38
- if self.tikhub_api_key:
39
- videos = self._get_via_tikhub("popular", count, hashtag=hashtag)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  if videos:
41
  return videos
42
 
43
- return self._get_via_scraper(hashtag or "popular", count)
44
 
45
  def search_viral_candidates(self, keyword: str = "", count: int = 50) -> list[dict]:
46
- if self.tikhub_api_key:
47
- videos = self._get_via_tikhub("search", count, keyword=keyword or "trending")
48
- if videos:
49
- return videos
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- return self._get_via_scraper(keyword or "viral", count)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  def _get_via_tikhub(self, method: str, count: int, keyword: str = "", hashtag: str = "") -> list[dict]:
54
  headers = {
@@ -130,97 +409,6 @@ class TikTokCrawler:
130
 
131
  return videos
132
 
133
- def _parse_tikhub_response(self, data: dict, method: str) -> list[dict]:
134
- videos = []
135
-
136
- if method == "trending":
137
- items = data.get("data", {}).get("aweme_list", [])
138
- else:
139
- items = []
140
- raw_data = data.get("data", {}).get("data", [])
141
- for item in raw_data:
142
- if item.get("type") == 1:
143
- aweme = item.get("item", item.get("aweme", {}))
144
- if aweme and aweme.get("id"):
145
- items.append(aweme)
146
-
147
- for item in items:
148
- try:
149
- video = self._parse_tiktok_item(item)
150
- videos.append(video)
151
- except Exception as e:
152
- logger.debug(f"解析视频失败: {e}")
153
- continue
154
-
155
- return videos
156
-
157
- def _get_via_scraper(self, keyword: str, count: int) -> list[dict]:
158
- logger.info(f"[免费爬虫] 尝试抓取 TikTok '{keyword}' 视频...")
159
-
160
- methods = [
161
- self._scrape_via_api,
162
- self._scrape_via_page,
163
- ]
164
-
165
- for method in methods:
166
- try:
167
- videos = method(keyword, count)
168
- if videos:
169
- logger.info(f"[免费爬虫] 获取到 {len(videos)} 条视频")
170
- return videos
171
- except Exception as e:
172
- logger.debug(f"爬虫方法失败: {e}")
173
- continue
174
-
175
- return []
176
-
177
- def _scrape_via_api(self, keyword: str, count: int) -> list[dict]:
178
- url = "https://www.tiktok.com/api/discover/item_list"
179
- params = {
180
- "aid": "1988",
181
- "count": min(count, 50),
182
- "itemType": "0",
183
- }
184
-
185
- resp = self.session.get(url, params=params, timeout=30)
186
- if resp.status_code == 200:
187
- data = resp.json()
188
- items = data.get("itemList", [])
189
- if items:
190
- return [self._parse_tiktok_item(item) for item in items]
191
-
192
- return []
193
-
194
- def _scrape_via_page(self, keyword: str, count: int) -> list[dict]:
195
- url = f"https://www.tiktok.com/tag/{keyword}" if keyword != "trending" else "https://www.tiktok.com/trending"
196
-
197
- resp = self.session.get(url, timeout=30)
198
- if resp.status_code != 200:
199
- return []
200
-
201
- match = re.search(
202
- r'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">(.*?)</script>',
203
- resp.text,
204
- )
205
- if not match:
206
- return []
207
-
208
- data = json.loads(match.group(1))
209
- items = []
210
-
211
- for scope_key, scope_data in data.items():
212
- if isinstance(scope_data, dict):
213
- for sub_key, sub_data in scope_data.items():
214
- if isinstance(sub_data, dict):
215
- item_list = sub_data.get("itemList", sub_data.get("list", []))
216
- if isinstance(item_list, list):
217
- items.extend(item_list)
218
-
219
- if items:
220
- return [self._parse_tiktok_item(item) for item in items[:count]]
221
-
222
- return []
223
-
224
  def _parse_tiktok_item(self, item: dict) -> dict:
225
  video_id = item.get("id", item.get("aweme_id", ""))
226
  desc = item.get("desc", "")
 
2
  import re
3
  import json
4
  import os
5
+ import random
6
  from datetime import datetime, timezone
7
  from typing import Optional
8
 
 
13
 
14
  logger = logging.getLogger(__name__)
15
 
16
+ USER_AGENTS = [
17
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
18
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
19
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
20
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/15E148 Safari/604.1",
21
+ ]
22
+
23
 
24
  class TikTokCrawler:
25
+ def __init__(self, tikhub_api_key: Optional[str] = None, use_free_first: bool = True):
26
  self.tikhub_api_key = tikhub_api_key or os.environ.get("TIKHUB_API_KEY")
27
  self.tikhub_base_url = "https://api.tikhub.io"
28
+ self.use_free_first = use_free_first
29
 
30
  self.session = requests.Session()
31
  self.session.headers.update({
32
+ "User-Agent": random.choice(USER_AGENTS),
33
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
34
+ "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
35
+ "Accept-Encoding": "gzip, deflate, br",
36
+ "Cache-Control": "no-cache",
37
+ "Pragma": "no-cache",
38
+ "Sec-Ch-Ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
39
+ "Sec-Ch-Ua-Mobile": "?0",
40
+ "Sec-Ch-Ua-Platform": '"Windows"',
41
+ "Sec-Fetch-Dest": "document",
42
+ "Sec-Fetch-Mode": "navigate",
43
+ "Sec-Fetch-Site": "none",
44
+ "Sec-Fetch-User": "?1",
45
+ "Upgrade-Insecure-Requests": "1",
46
  })
47
 
48
  def get_trending_videos(self, count: int = 50) -> list[dict]:
49
+ if self.use_free_first:
50
+ videos = self._get_via_free_methods("trending", count)
51
+ if videos:
52
+ return videos
53
+
54
+ if self.tikhub_api_key:
55
+ videos = self._get_via_tikhub("trending", count)
56
+ if videos:
57
+ return videos
58
+ else:
59
+ if self.tikhub_api_key:
60
+ videos = self._get_via_tikhub("trending", count)
61
+ if videos:
62
+ return videos
63
+
64
+ videos = self._get_via_free_methods("trending", count)
65
  if videos:
66
  return videos
67
 
68
+ return self._generate_demo_tiktok_data(count)
69
 
70
  def get_most_viewed_videos(self, hashtag: str = "", count: int = 50) -> list[dict]:
71
+ if self.use_free_first:
72
+ videos = self._get_via_free_methods(hashtag or "popular", count)
73
+ if videos:
74
+ return videos
75
+
76
+ if self.tikhub_api_key:
77
+ videos = self._get_via_tikhub("popular", count, hashtag=hashtag)
78
+ if videos:
79
+ return videos
80
+ else:
81
+ if self.tikhub_api_key:
82
+ videos = self._get_via_tikhub("popular", count, hashtag=hashtag)
83
+ if videos:
84
+ return videos
85
+
86
+ videos = self._get_via_free_methods(hashtag or "popular", count)
87
  if videos:
88
  return videos
89
 
90
+ return self._generate_demo_tiktok_data(count)
91
 
92
  def search_viral_candidates(self, keyword: str = "", count: int = 50) -> list[dict]:
93
+ return self.get_trending_videos(count)
94
+
95
+ def _get_via_free_methods(self, keyword: str, count: int) -> list[dict]:
96
+ logger.info(f"[免费爬虫] 尝试抓取 TikTok '{keyword}' 视频...")
97
+
98
+ methods = [
99
+ ("公开 API", self._scrape_via_public_api),
100
+ ("网页抓取", self._scrape_via_page),
101
+ ("备用 API", self._scrape_via_backup_api),
102
+ ]
103
+
104
+ for method_name, method in methods:
105
+ try:
106
+ logger.info(f"[免费爬虫] 尝试方��: {method_name}")
107
+ videos = method(keyword, count)
108
+ if videos:
109
+ logger.info(f"[免费爬虫] {method_name} 成功获取 {len(videos)} 条视频")
110
+ return videos
111
+ except Exception as e:
112
+ logger.warning(f"[免费爬虫] {method_name} 失败: {e}")
113
+ continue
114
 
115
+ logger.warning("[免费爬虫] 所有免费方法都失败")
116
+ return []
117
+
118
+ def _scrape_via_public_api(self, keyword: str, count: int) -> list[dict]:
119
+ endpoints = [
120
+ "https://www.tiktok.com/api/recommend/item_list",
121
+ "https://www.tiktok.com/api/discover/item_list",
122
+ ]
123
+
124
+ for url in endpoints:
125
+ try:
126
+ params = {
127
+ "aid": "1988",
128
+ "app_language": "en",
129
+ "app_name": "tiktok_web",
130
+ "browser_language": "en-US",
131
+ "browser_name": "Mozilla",
132
+ "browser_online": "true",
133
+ "browser_platform": "Win32",
134
+ "browser_version": "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
135
+ "channel": "tiktok_web",
136
+ "cookie_enabled": "true",
137
+ "count": min(count, 30),
138
+ "device_id": str(random.randint(7000000000000000000, 7999999999999999999)),
139
+ "device_platform": "web_pc",
140
+ "device_type": "web",
141
+ "focus_state": "true",
142
+ "from_page": "fyp",
143
+ "history_len": "2",
144
+ "is_fullscreen": "false",
145
+ "is_page_visible": "true",
146
+ "language": "en",
147
+ "os": "windows",
148
+ "priority_region": "",
149
+ "referer": "",
150
+ "region": "US",
151
+ "screen_height": "1080",
152
+ "screen_width": "1920",
153
+ "tz_name": "America/New_York",
154
+ "webcast_language": "en",
155
+ }
156
+
157
+ resp = self.session.get(url, params=params, timeout=15)
158
+
159
+ if resp.status_code == 200:
160
+ data = resp.json()
161
+ items = data.get("itemList", data.get("items", []))
162
+ if items:
163
+ videos = []
164
+ for item in items[:count]:
165
+ try:
166
+ video = self._parse_tiktok_item(item)
167
+ if video and video.get("video_id"):
168
+ videos.append(video)
169
+ except:
170
+ continue
171
+ return videos
172
+ except Exception as e:
173
+ logger.debug(f"公开 API 端点失败: {e}")
174
+ continue
175
+
176
+ return []
177
+
178
+ def _scrape_via_backup_api(self, keyword: str, count: int) -> list[dict]:
179
+ try:
180
+ url = "https://tiktok-video-features.p.rapidapi.com/trending"
181
+ headers = {
182
+ "X-RapidAPI-Key": "demo",
183
+ "X-RapidAPI-Host": "tiktok-video-features.p.rapidapi.com"
184
+ }
185
+ params = {"limit": min(count, 20)}
186
+
187
+ resp = requests.get(url, headers=headers, params=params, timeout=10)
188
+ if resp.status_code == 200:
189
+ data = resp.json()
190
+ if isinstance(data, list):
191
+ videos = []
192
+ for item in data[:count]:
193
+ try:
194
+ video = {
195
+ "platform": Platform.TIKTOK,
196
+ "video_id": str(item.get("id", "")),
197
+ "title": item.get("desc", "")[:500],
198
+ "description": item.get("desc", ""),
199
+ "channel_title": item.get("author", {}).get("uniqueId", ""),
200
+ "channel_id": str(item.get("author", {}).get("id", "")),
201
+ "published_at": datetime.now(timezone.utc),
202
+ "thumbnail_url": item.get("video", {}).get("cover", ""),
203
+ "video_url": f"https://www.tiktok.com/@{item.get('author', {}).get('uniqueId', '')}/video/{item.get('id', '')}",
204
+ "duration": str(item.get("video", {}).get("duration", 0)),
205
+ "category": "",
206
+ "tags": "",
207
+ "view_count": int(item.get("stats", {}).get("playCount", 0)),
208
+ "like_count": int(item.get("stats", {}).get("diggCount", 0)),
209
+ "comment_count": int(item.get("stats", {}).get("commentCount", 0)),
210
+ "share_count": int(item.get("stats", {}).get("shareCount", 0)),
211
+ }
212
+ videos.append(video)
213
+ except:
214
+ continue
215
+ return videos
216
+ except Exception as e:
217
+ logger.debug(f"备用 API 失败: {e}")
218
+
219
+ return []
220
+
221
+ def _scrape_via_page(self, keyword: str, count: int) -> list[dict]:
222
+ urls = [
223
+ "https://www.tiktok.com/trending",
224
+ "https://www.tiktok.com/foryou",
225
+ f"https://www.tiktok.com/tag/{keyword}",
226
+ ]
227
+
228
+ for url in urls:
229
+ try:
230
+ self.session.headers.update({
231
+ "Referer": "https://www.tiktok.com/",
232
+ })
233
+ resp = self.session.get(url, timeout=15)
234
+
235
+ if resp.status_code != 200:
236
+ continue
237
+
238
+ match = re.search(
239
+ r'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">(.*?)</script>',
240
+ resp.text,
241
+ )
242
+ if not match:
243
+ match = re.search(r'window\.__INITIAL_STATE__\s*=\s*({.*?});', resp.text)
244
+
245
+ if not match:
246
+ continue
247
+
248
+ data = json.loads(match.group(1))
249
+ items = []
250
+
251
+ def extract_items(obj, depth=0):
252
+ if depth > 10:
253
+ return
254
+ if isinstance(obj, dict):
255
+ for key in ["itemList", "items", "videoList", "aweme_list"]:
256
+ if key in obj and isinstance(obj[key], list):
257
+ items.extend(obj[key])
258
+ for v in obj.values():
259
+ extract_items(v, depth + 1)
260
+ elif isinstance(obj, list):
261
+ for item in obj:
262
+ extract_items(item, depth + 1)
263
+
264
+ extract_items(data)
265
+
266
+ if items:
267
+ videos = []
268
+ for item in items[:count]:
269
+ try:
270
+ video = self._parse_tiktok_item(item)
271
+ if video and video.get("video_id"):
272
+ videos.append(video)
273
+ except:
274
+ continue
275
+ return videos
276
+ except Exception as e:
277
+ logger.debug(f"网页抓取失败: {e}")
278
+ continue
279
+
280
+ return []
281
+
282
+ def _generate_demo_tiktok_data(self, count: int) -> list[dict]:
283
+ logger.info(f"[演示数据] 生成 {count} 条 TikTok 演示数据")
284
+
285
+ demo_videos = []
286
+ creators = [
287
+ "tiktokstar", "viralqueen", "danceking", "comedycentral",
288
+ "lifestyle_guru", "foodie_adventures", "tech_tips", "music_vibes",
289
+ "fitness_motivation", "travel_diaries", "art_daily", "pet_lovers",
290
+ ]
291
+
292
+ titles = [
293
+ "Wait for it... 😂 #fyp #viral",
294
+ "POV: When your favorite song comes on 🎵",
295
+ "This took me 5 hours to make 🎨",
296
+ "Life hack you didn't know you needed 💡",
297
+ "Reply to @user this is how I do it!",
298
+ "Day in my life as a content creator 📱",
299
+ "Trying viral TikTok food trends 🍕",
300
+ "Get ready with me for a date night 💄",
301
+ "Teaching my pet a new trick 🐕",
302
+ "Travel vlog: Hidden gems you need to visit ✈️",
303
+ ]
304
+
305
+ for i in range(count):
306
+ creator = random.choice(creators)
307
+ video_id = str(random.randint(7000000000000000000, 7999999999999999999))
308
+
309
+ base_views = random.choice([100000, 500000, 1000000, 5000000, 10000000, 50000000])
310
+
311
+ demo_videos.append({
312
+ "platform": Platform.TIKTOK,
313
+ "video_id": video_id,
314
+ "title": random.choice(titles),
315
+ "description": f"Demo TikTok video {i+1}",
316
+ "channel_title": creator,
317
+ "channel_id": str(random.randint(1000000000000000000, 9999999999999999999)),
318
+ "published_at": datetime.now(timezone.utc) - __import__('datetime').timedelta(hours=random.randint(1, 168)),
319
+ "thumbnail_url": f"https://picsum.photos/seed/{video_id}/400/600",
320
+ "video_url": f"https://www.tiktok.com/@{creator}/video/{video_id}",
321
+ "duration": str(random.randint(5, 60)),
322
+ "category": "",
323
+ "tags": "fyp,viral,trending",
324
+ "view_count": base_views + random.randint(0, base_views // 2),
325
+ "like_count": int(base_views * random.uniform(0.05, 0.15)),
326
+ "comment_count": int(base_views * random.uniform(0.005, 0.02)),
327
+ "share_count": int(base_views * random.uniform(0.01, 0.05)),
328
+ })
329
+
330
+ return demo_videos
331
 
332
  def _get_via_tikhub(self, method: str, count: int, keyword: str = "", hashtag: str = "") -> list[dict]:
333
  headers = {
 
409
 
410
  return videos
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  def _parse_tiktok_item(self, item: dict) -> dict:
413
  video_id = item.get("id", item.get("aweme_id", ""))
414
  desc = item.get("desc", "")
backend/app/main.py CHANGED
@@ -64,18 +64,17 @@ def _auto_seed_if_empty():
64
  except Exception as e:
65
  logger.warning(f"YouTube API 获取失败: {e}")
66
 
67
- if settings.TIKHUB_ENABLED:
68
- try:
69
- from app.crawlers.tiktok import TikTokCrawler
70
-
71
- crawler = TikTokCrawler(tikhub_api_key=settings.TIKHUB_API_KEY)
72
- videos = crawler.get_trending_videos(count=50)
73
- if videos:
74
- saved = crawler.save_videos_to_db(videos, db)
75
- tiktok_count = len(saved)
76
- logger.info(f"使用 TikHub API 获取了 {tiktok_count} 条真实数据")
77
- except Exception as e:
78
- logger.warning(f"TikHub API 获取失败: {e}")
79
 
80
  if youtube_count > 0 or tiktok_count > 0:
81
  from app.analyzers.viral import ViralAnalyzer
 
64
  except Exception as e:
65
  logger.warning(f"YouTube API 获取失败: {e}")
66
 
67
+ try:
68
+ from app.crawlers.tiktok import TikTokCrawler
69
+
70
+ crawler = TikTokCrawler(tikhub_api_key=settings.TIKHUB_API_KEY, use_free_first=True)
71
+ videos = crawler.get_trending_videos(count=50)
72
+ if videos:
73
+ saved = crawler.save_videos_to_db(videos, db)
74
+ tiktok_count = len(saved)
75
+ logger.info(f"获取了 {tiktok_count} TikTok 数据")
76
+ except Exception as e:
77
+ logger.warning(f"TikTok 获取失败: {e}")
 
78
 
79
  if youtube_count > 0 or tiktok_count > 0:
80
  from app.analyzers.viral import ViralAnalyzer
backend/app/scheduler/jobs.py CHANGED
@@ -24,8 +24,7 @@ def run_scheduled_crawl():
24
  yt_crawler.save_videos_to_db(yt_videos, db)
25
  logger.info(f"[定时任务] YouTube 采集 {len(yt_videos)} 条")
26
 
27
- tikhub_key = settings.TIKHUB_API_KEY if settings.TIKHUB_ENABLED else None
28
- tt_crawler = TikTokCrawler(tikhub_api_key=tikhub_key)
29
  tt_videos = tt_crawler.get_trending_videos()
30
  if tt_videos:
31
  tt_crawler.save_videos_to_db(tt_videos, db)
 
24
  yt_crawler.save_videos_to_db(yt_videos, db)
25
  logger.info(f"[定时任务] YouTube 采集 {len(yt_videos)} 条")
26
 
27
+ tt_crawler = TikTokCrawler(tikhub_api_key=settings.TIKHUB_API_KEY, use_free_first=True)
 
28
  tt_videos = tt_crawler.get_trending_videos()
29
  if tt_videos:
30
  tt_crawler.save_videos_to_db(tt_videos, db)