154 lines
5.5 KiB
Python
154 lines
5.5 KiB
Python
# social/tasks/tiktok.py
|
|
from celery import shared_task
|
|
from django.utils import timezone
|
|
from apps.social.models import SocialAccount, SocialContent, SocialComment
|
|
from apps.social.services.tiktok import TikTokService, TikTokAPIError
|
|
import logging
|
|
import time
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def parse_tiktok_timestamp(ts):
|
|
"""Convert TikTok timestamp (seconds) to datetime."""
|
|
if not ts:
|
|
return timezone.now()
|
|
# TikTok timestamps are usually seconds, check if milliseconds
|
|
if ts > 1000000000000:
|
|
ts = ts / 1000
|
|
return timezone.make_aware(timezone.datetime.utcfromtimestamp(ts))
|
|
|
|
@shared_task(bind=True)
|
|
def extract_all_comments_task(self, account_id):
|
|
"""
|
|
FULL SYNC: Fetches ALL Ads and ALL Comments.
|
|
Used for initial connection or monthly backfill.
|
|
"""
|
|
try:
|
|
account = SocialAccount.objects.get(id=account_id, platform_type='TT')
|
|
if not account.is_active:
|
|
return
|
|
|
|
logger.info(f"Starting FULL TikTok Ads sync for {account.name}")
|
|
|
|
page = 1
|
|
has_more_ads = True
|
|
|
|
while has_more_ads:
|
|
try:
|
|
# 1. Fetch Ads (Content)
|
|
data = TikTokService.fetch_ads(account, page=page)
|
|
ads = data.get('list', [])
|
|
|
|
# Check pagination
|
|
page_info = data.get('page_info', {})
|
|
total_ads = page_info.get('total', 0)
|
|
has_more_ads = (page * 20) < total_ads
|
|
page += 1
|
|
time.sleep(1)
|
|
|
|
for ad_data in ads:
|
|
ad_id = str(ad_data['ad_id'])
|
|
|
|
# Store Ad as SocialContent
|
|
content, _ = SocialContent.objects.update_or_create(
|
|
platform_type='TT',
|
|
content_id=ad_id,
|
|
defaults={
|
|
'account': account,
|
|
'text': ad_data.get('ad_name', ''),
|
|
'created_at': parse_tiktok_timestamp(ad_data.get('create_time')),
|
|
'content_data': ad_data
|
|
}
|
|
)
|
|
|
|
# 2. Fetch ALL Comments for this Ad
|
|
_sync_comments_for_content(account, content, ad_id, full_sync=True)
|
|
|
|
except TikTokAPIError as e:
|
|
logger.error(f"Error syncing page {page}: {e}")
|
|
break
|
|
|
|
except Exception as e:
|
|
logger.error(f"Full Sync Critical Error: {e}")
|
|
|
|
@shared_task
|
|
def poll_new_comments_task():
|
|
"""
|
|
DELTA SYNC: Fetches comments but stops when existing comments are found.
|
|
Run frequently (e.g., every 15 mins) via Celery Beat.
|
|
"""
|
|
accounts = SocialAccount.objects.filter(platform_type='TT', is_active=True)
|
|
|
|
for account in accounts:
|
|
# Fetch existing Ads from our DB (we don't re-fetch Ad list every poll to save API calls)
|
|
contents = SocialContent.objects.filter(account=account, platform_type='TT')
|
|
|
|
for content in contents:
|
|
try:
|
|
# Sync comments, but stop early if we hit known data
|
|
_sync_comments_for_content(account, content, content.content_id, full_sync=False)
|
|
except TikTokAPIError as e:
|
|
logger.error(f"Polling Error for Ad {content.content_id}: {e}")
|
|
|
|
def _sync_comments_for_content(account, content, ad_id, full_sync=True):
|
|
"""
|
|
Helper function to sync comments for a specific Ad.
|
|
|
|
Args:
|
|
full_sync (bool):
|
|
True -> Fetch all pages (History).
|
|
False -> Stop fetching pages once we find a comment we already have (Delta).
|
|
"""
|
|
c_page = 1
|
|
has_more_comments = True
|
|
|
|
while has_more_comments:
|
|
c_data = TikTokService.fetch_comments_for_ad(account, ad_id, page=c_page)
|
|
comments = c_data.get('list', [])
|
|
|
|
# Check pagination
|
|
page_info = c_data.get('page_info', {})
|
|
total_comments = page_info.get('total', 0)
|
|
has_more_comments = (c_page * 20) < total_comments
|
|
c_page += 1
|
|
|
|
if not comments:
|
|
break
|
|
|
|
should_stop = False
|
|
|
|
for comm in comments:
|
|
comm_id = comm.get('comment_id')
|
|
|
|
# DELTA LOGIC: If doing a poll (not full sync), check if exists
|
|
if not full_sync:
|
|
exists = SocialComment.objects.filter(
|
|
platform_type='TT',
|
|
comment_id=comm_id
|
|
).exists()
|
|
|
|
if exists:
|
|
# We found a comment we already have.
|
|
# API returns oldest -> newest usually, so we can stop.
|
|
should_stop = True
|
|
break
|
|
|
|
# Create or Update
|
|
comm_time = parse_tiktok_timestamp(comm.get('create_time'))
|
|
SocialComment.objects.update_or_create(
|
|
platform_type='TT',
|
|
comment_id=comm_id,
|
|
defaults={
|
|
'account': account,
|
|
'content': content,
|
|
'author_name': comm.get('user_name', 'Unknown'),
|
|
'text': comm.get('text', ''),
|
|
'created_at': comm_time,
|
|
'comment_data': comm
|
|
}
|
|
)
|
|
|
|
if should_stop:
|
|
break
|
|
|
|
time.sleep(0.5) # Small delay to respect rate limits |