Add dynamic suggested Vietnamese TikTokers API with caching

This commit is contained in:
Khoa.vo 2025-12-19 20:53:33 +07:00
parent 587a83fe0d
commit 62b78d4700
3 changed files with 192 additions and 3 deletions

View file

@ -156,3 +156,76 @@ async def search_videos(
print(f"Error searching for {query}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Cache for suggested accounts
_suggested_cache = {
"accounts": [],
"updated_at": 0
}
CACHE_TTL = 3600 # 1 hour cache
@router.get("/suggested")
async def get_suggested_accounts(
limit: int = Query(50, description="Max accounts to return", ge=10, le=100)
):
"""
Fetch trending/suggested Vietnamese TikTok creators.
Uses TikTok's discover API and caches results for 1 hour.
"""
import time
# Check cache
if _suggested_cache["accounts"] and (time.time() - _suggested_cache["updated_at"]) < CACHE_TTL:
print("Returning cached suggested accounts")
return {"accounts": _suggested_cache["accounts"][:limit], "cached": True}
# Load stored credentials
cookies, user_agent = PlaywrightManager.load_stored_credentials()
if not cookies:
# Return fallback static list if not authenticated
return {"accounts": get_fallback_accounts()[:limit], "cached": False, "fallback": True}
print("Fetching fresh suggested accounts from TikTok...")
try:
accounts = await PlaywrightManager.fetch_suggested_accounts(cookies, user_agent, limit)
if accounts:
_suggested_cache["accounts"] = accounts
_suggested_cache["updated_at"] = time.time()
return {"accounts": accounts[:limit], "cached": False}
else:
# Fallback if API fails
return {"accounts": get_fallback_accounts()[:limit], "cached": False, "fallback": True}
except Exception as e:
print(f"Error fetching suggested accounts: {e}")
return {"accounts": get_fallback_accounts()[:limit], "cached": False, "fallback": True}
def get_fallback_accounts():
"""Static fallback list of popular Vietnamese TikTokers."""
return [
{"username": "ciin_rubi", "nickname": "👑 CiiN - Lisa of Vietnam", "region": "VN"},
{"username": "hoaa.hanassii", "nickname": "💃 Hoa Hanassii", "region": "VN"},
{"username": "hoa_2309", "nickname": "🔥 Ngô Ngọc Hòa", "region": "VN"},
{"username": "minah.ne", "nickname": "🎵 Minah", "region": "VN"},
{"username": "lebong95", "nickname": "💪 Lê Bống", "region": "VN"},
{"username": "po.trann77", "nickname": "✨ Trần Thanh Tâm", "region": "VN"},
{"username": "gamkami", "nickname": "🎱 Gấm Kami", "region": "VN"},
{"username": "quynhalee", "nickname": "🎮 Quỳnh Alee", "region": "VN"},
{"username": "tieu_hy26", "nickname": "👰 Tiểu Hý", "region": "VN"},
{"username": "changmie", "nickname": "🎤 Changmie", "region": "VN"},
{"username": "vuthuydien", "nickname": "😄 Vũ Thụy Điển", "region": "VN"},
{"username": "thienantv", "nickname": "😂 Thiên An TV", "region": "VN"},
{"username": "amee_official", "nickname": "🎵 AMEE", "region": "VN"},
{"username": "sontungmtp_official", "nickname": "🎤 Sơn Tùng M-TP", "region": "VN"},
{"username": "hieuthuhai_", "nickname": "🎧 HIEUTHUHAI", "region": "VN"},
{"username": "mck.99", "nickname": "🔥 MCK", "region": "VN"},
{"username": "tranducbo", "nickname": "😄 Trần Đức Bo", "region": "VN"},
{"username": "call.me.duy", "nickname": "🎭 Call Me Duy", "region": "VN"},
{"username": "mai_ngok", "nickname": "💕 Mai Ngok", "region": "VN"},
{"username": "thanhtrungdam", "nickname": "🎤 Đàm Thanh Trung", "region": "VN"},
]

View file

@ -773,6 +773,100 @@ class PlaywrightManager:
print(f"DEBUG: Total captured search videos: {len(captured_videos)}")
return captured_videos
@staticmethod
async def fetch_suggested_accounts(cookies: list, user_agent: str = None, limit: int = 50) -> list:
"""
Fetch trending/suggested accounts from TikTok Vietnam.
Uses the discover/creators API.
"""
from playwright.async_api import async_playwright, Response
if not user_agent:
user_agent = PlaywrightManager.DEFAULT_USER_AGENT
captured_accounts = []
async def handle_response(response: Response):
"""Capture suggested accounts from API responses."""
nonlocal captured_accounts
url = response.url
# Look for suggest/discover APIs
if any(x in url for x in ["suggest", "discover", "recommend/user", "creator"]):
try:
data = await response.json()
# Different API formats
users = data.get("userList", []) or data.get("users", []) or data.get("data", [])
for item in users:
user_data = item.get("user", item) if isinstance(item, dict) else item
if isinstance(user_data, dict):
username = user_data.get("uniqueId") or user_data.get("unique_id")
if username:
captured_accounts.append({
"username": username,
"nickname": user_data.get("nickname", username),
"avatar": user_data.get("avatarThumb") or user_data.get("avatar"),
"followers": user_data.get("followerCount", 0),
"verified": user_data.get("verified", False),
"region": "VN"
})
if users:
print(f"DEBUG: Captured {len(users)} suggested accounts")
except Exception as e:
pass # Ignore parse errors
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
args=PlaywrightManager.BROWSER_ARGS
)
context = await browser.new_context(
user_agent=user_agent,
locale="vi-VN", # Vietnamese locale
timezone_id="Asia/Ho_Chi_Minh"
)
await context.add_cookies(cookies)
page = await context.new_page()
await stealth_async(page)
page.on("response", handle_response)
try:
# Navigate to TikTok explore/discover page (Vietnam)
await page.goto("https://www.tiktok.com/explore?lang=vi-VN", wait_until="networkidle", timeout=30000)
await asyncio.sleep(3)
# Also try the For You page to capture suggested
await page.goto("https://www.tiktok.com/foryou?lang=vi-VN", wait_until="domcontentloaded", timeout=15000)
await asyncio.sleep(2)
# Scroll to trigger more suggestions
for _ in range(3):
await page.evaluate("window.scrollBy(0, 800)")
await asyncio.sleep(1)
except Exception as e:
print(f"DEBUG: Error fetching suggested accounts: {e}")
await browser.close()
# Remove duplicates by username
seen = set()
unique_accounts = []
for acc in captured_accounts:
if acc["username"] not in seen:
seen.add(acc["username"])
unique_accounts.append(acc)
print(f"DEBUG: Total unique suggested accounts: {len(unique_accounts)}")
return unique_accounts[:limit]
# Singleton instance
playwright_manager = PlaywrightManager()

View file

@ -199,11 +199,33 @@ export const Feed: React.FC = () => {
const loadSuggestedProfiles = async () => {
setLoadingProfiles(true);
try {
const usernames = SUGGESTED_ACCOUNTS.map(a => a.username.replace('@', '')).join(',');
const res = await axios.get(`${API_BASE_URL}/user/profiles?usernames=${usernames}`);
setSuggestedProfiles(res.data);
// Try the dynamic suggested API first (auto-updates from TikTok Vietnam)
const res = await axios.get(`${API_BASE_URL}/user/suggested?limit=50`);
const accounts = res.data.accounts || [];
if (accounts.length > 0) {
// Map API response to our profile format
setSuggestedProfiles(accounts.map((acc: any) => ({
username: acc.username,
nickname: acc.nickname || acc.username,
avatar: acc.avatar || null,
followers: acc.followers || 0,
verified: acc.verified || false
})));
} else {
// Fallback to static list if API returns empty
setSuggestedProfiles(SUGGESTED_ACCOUNTS.map(a => ({
username: a.username.replace('@', ''),
nickname: a.label
})));
}
} catch (err) {
console.error('Failed to load profiles:', err);
// Fallback to static list on error
setSuggestedProfiles(SUGGESTED_ACCOUNTS.map(a => ({
username: a.username.replace('@', ''),
nickname: a.label
})));
} finally {
setLoadingProfiles(false);
}