socialmag v0.1 — 4/5 platforms live (Threads, FB, IG, TikTok)
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""TikTok upload flow (media required).
|
||||
|
||||
1. navigate tiktokstudio/upload
|
||||
2. wait for the upload page
|
||||
3. photos: switch to the Photos tab first
|
||||
4. setFileInputFiles on the matching input[type=file]
|
||||
5. wait for processing
|
||||
6. type the caption
|
||||
7. click Post/Publish
|
||||
8. wait for the success confirmation
|
||||
|
||||
TikTok is the most bot-sensitive of the five (proposal: ⭐⭐⭐⭐). Delays here
|
||||
are deliberately longer than elsewhere.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
|
||||
from ..cdp import CDPError, human_delay
|
||||
from . import guard, require_media
|
||||
from ..selectors import TIKTOK as S
|
||||
|
||||
PLATFORM = "tiktok"
|
||||
|
||||
VIDEO_EXTS = {".mp4", ".mov", ".webm", ".avi", ".mkv"}
|
||||
PHOTO_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".heic"}
|
||||
|
||||
|
||||
async def post(cdp_client, text, media_path=None, options=None):
|
||||
options = options or {}
|
||||
require_media(PLATFORM, media_path)
|
||||
|
||||
ext = os.path.splitext(media_path)[1].lower()
|
||||
if ext in VIDEO_EXTS:
|
||||
kind = "video"
|
||||
elif ext in PHOTO_EXTS:
|
||||
kind = "photo"
|
||||
else:
|
||||
raise CDPError(
|
||||
f"tiktok: unrecognised media extension {ext!r} — "
|
||||
f"videos {sorted(VIDEO_EXTS)}, photos {sorted(PHOTO_EXTS)}"
|
||||
)
|
||||
|
||||
tab, reused = await cdp_client.get_or_open_tab(S["url"], "tiktok.com")
|
||||
if reused:
|
||||
await tab.eval("window.onbeforeunload = null")
|
||||
await tab.navigate(S["url"])
|
||||
return await guard(PLATFORM, tab, _flow(tab, text or "", media_path, kind))
|
||||
|
||||
|
||||
async def _flow(tab, caption, media_path, kind):
|
||||
await tab.wait_for_load(timeout=45.0)
|
||||
await human_delay(1.5, 3.0)
|
||||
|
||||
if kind == "photo":
|
||||
# TODO: VALIDATE LIVE — the Photos tab switch and the input it reveals.
|
||||
await _wait_for_js(tab, S["photos_tab_js"], "the Photos tab", timeout=30.0)
|
||||
await tab.click_js(S["photos_tab_js"])
|
||||
await human_delay(1.0, 2.0)
|
||||
file_input = S["photo_file_input"]
|
||||
else:
|
||||
file_input = S["video_file_input"]
|
||||
|
||||
await tab.wait_for(file_input, visible=False, timeout=45.0)
|
||||
await tab.set_file_input(file_input, media_path)
|
||||
|
||||
# Step 5: TikTok transcodes before it will let you post.
|
||||
# TODO: VALIDATE LIVE — the real "upload finished" marker. Right now we
|
||||
# wait for the Post button to become enabled, which is the behaviour the
|
||||
# flow actually depends on.
|
||||
await _wait_for_js(
|
||||
tab, S["post_button_js"], "an enabled Post button", timeout=300.0
|
||||
)
|
||||
|
||||
if caption:
|
||||
await tab.wait_for(S["caption_box"])
|
||||
# DraftJS: type per-char so its keypress handlers fire.
|
||||
# TODO: VALIDATE LIVE — whether insertText is accepted here.
|
||||
await tab.type_text(S["caption_box"], caption, per_char=True)
|
||||
await human_delay(1.0, 2.0)
|
||||
|
||||
await tab.click_js(S["post_button_js"])
|
||||
|
||||
# TikTok may show a copyright check dialog — dismiss it
|
||||
await asyncio.sleep(3)
|
||||
try:
|
||||
has_continue = await tab.eval("!!(%s)" % S["continue_post_js"])
|
||||
if has_continue:
|
||||
await tab.click_js(S["continue_post_js"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await _wait_for_success(tab)
|
||||
|
||||
|
||||
async def _wait_for_js(tab, expression, what, timeout=30.0):
|
||||
deadline = asyncio.get_running_loop().time() + timeout
|
||||
while asyncio.get_running_loop().time() < deadline:
|
||||
if await tab.eval("!!(%s)" % expression):
|
||||
return
|
||||
await asyncio.sleep(0.5)
|
||||
raise CDPError(f"tiktok: {what} did not appear within {timeout}s")
|
||||
|
||||
|
||||
async def _wait_for_success(tab, timeout=120.0):
|
||||
# TODO: VALIDATE LIVE — the confirmation copy. TikTok also redirects to
|
||||
# /tiktokstudio/content on success, which we accept as the same signal.
|
||||
deadline = asyncio.get_running_loop().time() + timeout
|
||||
marker = json.dumps(S["success_text"])
|
||||
while asyncio.get_running_loop().time() < deadline:
|
||||
if await tab.eval(f"document.body.innerText.includes({marker})"):
|
||||
return
|
||||
if "tiktokstudio/content" in (await tab.url() or ""):
|
||||
return
|
||||
await asyncio.sleep(1.0)
|
||||
raise CDPError(
|
||||
f"tiktok: no success confirmation within {timeout}s of clicking Post"
|
||||
)
|
||||
|
||||
|
||||
async def check_login(cdp_client):
|
||||
tab, reused = await cdp_client.get_or_open_tab(S["url"], "tiktok.com")
|
||||
if reused:
|
||||
await tab.eval("window.onbeforeunload = null")
|
||||
await tab.navigate(S["url"])
|
||||
await asyncio.sleep(5)
|
||||
if await tab.exists(S["login_marker"]):
|
||||
return False
|
||||
return await tab.exists(S["video_file_input"])
|
||||
Reference in New Issue
Block a user