mirror of
https://github.com/vndangkhoa/purestream.git
synced 2026-04-05 01:17:58 +07:00
Add dynamic suggested Vietnamese TikTokers API with caching
This commit is contained in:
parent
587a83fe0d
commit
62b78d4700
3 changed files with 192 additions and 3 deletions
|
|
@ -156,3 +156,76 @@ async def search_videos(
|
||||||
print(f"Error searching for {query}: {e}")
|
print(f"Error searching for {query}: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(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"},
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -773,6 +773,100 @@ class PlaywrightManager:
|
||||||
print(f"DEBUG: Total captured search videos: {len(captured_videos)}")
|
print(f"DEBUG: Total captured search videos: {len(captured_videos)}")
|
||||||
return 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
|
# Singleton instance
|
||||||
playwright_manager = PlaywrightManager()
|
playwright_manager = PlaywrightManager()
|
||||||
|
|
|
||||||
|
|
@ -199,11 +199,33 @@ export const Feed: React.FC = () => {
|
||||||
const loadSuggestedProfiles = async () => {
|
const loadSuggestedProfiles = async () => {
|
||||||
setLoadingProfiles(true);
|
setLoadingProfiles(true);
|
||||||
try {
|
try {
|
||||||
const usernames = SUGGESTED_ACCOUNTS.map(a => a.username.replace('@', '')).join(',');
|
// Try the dynamic suggested API first (auto-updates from TikTok Vietnam)
|
||||||
const res = await axios.get(`${API_BASE_URL}/user/profiles?usernames=${usernames}`);
|
const res = await axios.get(`${API_BASE_URL}/user/suggested?limit=50`);
|
||||||
setSuggestedProfiles(res.data);
|
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) {
|
} catch (err) {
|
||||||
console.error('Failed to load profiles:', 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 {
|
} finally {
|
||||||
setLoadingProfiles(false);
|
setLoadingProfiles(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue