This commit is contained in:
Guillem Hernandez Sola
2026-05-19 11:01:51 +02:00
parent dea94476a9
commit 5cf0b6e915
4 changed files with 1354 additions and 1075 deletions

295
generate_tiktok_cookies.py Normal file
View File

@@ -0,0 +1,295 @@
#!/usr/bin/env python3
"""
generate_tiktok_cookies.py
──────────────────────────
Opens a real (headed) Chromium browser, navigates to TikTok,
and waits for you to:
1. Log in manually
2. Solve any CAPTCHA
3. Reach the TikTok home feed
Then it saves the session cookies to tiktok_cookies.json
so tiktok2bsky.py can reuse them without a browser.
Usage:
python generate_tiktok_cookies.py
python generate_tiktok_cookies.py --output my_cookies.json
python generate_tiktok_cookies.py --handle jijantesfc
"""
import argparse
import json
import logging
import os
import sys
import time
# ─────────────────────────────────────────────
# Config
# ─────────────────────────────────────────────
DEFAULT_OUTPUT_PATH = "tiktok_cookies.json"
TIKTOK_LOGIN_URL = "https://www.tiktok.com/login"
TIKTOK_HOME_URL = "https://www.tiktok.com"
POLL_INTERVAL_S = 2.0 # how often to check if login is complete
LOGIN_TIMEOUT_S = 300 # max seconds to wait for manual login (5 min)
# Selectors that indicate a successful login
LOGGED_IN_SELECTORS = [
'[data-e2e="profile-icon"]',
'[data-e2e="nav-profile"]',
'a[href*="/profile"]',
'[class*="DivAvatarContainer"]',
'[class*="avatar-wrapper"]',
'button:has-text("Upload")',
'button:has-text("Cargar")',
'[data-e2e="upload-icon"]',
]
# ─────────────────────────────────────────────
# Logging
# ─────────────────────────────────────────────
logging.basicConfig(
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
level=logging.INFO,
)
# ─────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────
def is_logged_in(page) -> bool:
"""Check if any known post-login selector is visible."""
for sel in LOGGED_IN_SELECTORS:
try:
if page.locator(sel).first.is_visible(timeout=1500):
logging.info(f"✅ Login detected via: {sel}")
return True
except Exception:
pass
return False
def wait_for_login(page, timeout_s: int = LOGIN_TIMEOUT_S) -> bool:
"""
Poll the page every POLL_INTERVAL_S seconds until a logged-in
selector appears or the timeout is reached.
"""
elapsed = 0
logging.info(
f"⏳ Waiting up to {timeout_s}s for you to log in "
"and solve any CAPTCHA..."
)
while elapsed < timeout_s:
if is_logged_in(page):
return True
time.sleep(POLL_INTERVAL_S)
elapsed += POLL_INTERVAL_S
remaining = timeout_s - elapsed
if elapsed % 30 < POLL_INTERVAL_S: # log reminder every ~30s
logging.info(
f" Still waiting... {remaining:.0f}s remaining. "
"Complete the login in the browser window."
)
return False
def normalise_cookies(raw_cookies: list) -> list:
"""
Normalise Playwright cookies to a clean JSON format
compatible with both tiktok2bsky.py and yt-dlp (Netscape-like).
Removes internal Playwright fields that cause issues.
"""
cleaned = []
for c in raw_cookies:
entry = {
"name": c.get("name", ""),
"value": c.get("value", ""),
"domain": c.get("domain", ".tiktok.com"),
"path": c.get("path", "/"),
"sameSite": c.get("sameSite", "None"),
"secure": c.get("secure", False),
"httpOnly": c.get("httpOnly", False),
}
if c.get("expires") and c["expires"] > 0:
entry["expirationDate"] = int(c["expires"])
cleaned.append(entry)
return cleaned
def save_cookies(cookies: list, output_path: str):
with open(output_path, "w", encoding="utf-8") as f:
json.dump(cookies, f, indent=2, ensure_ascii=False)
logging.info(f"💾 Saved {len(cookies)} cookies → {output_path}")
def navigate_to_profile(page, handle: str):
"""After login, optionally navigate to the target profile to warm up cookies."""
profile_url = f"https://www.tiktok.com/@{handle.lstrip('@')}"
logging.info(f"🌐 Navigating to target profile: {profile_url}")
try:
page.goto(profile_url, wait_until="domcontentloaded", timeout=30000)
time.sleep(3.0)
logging.info("✅ Profile page loaded — cookies are now profile-warmed.")
except Exception as e:
logging.warning(f"⚠️ Could not navigate to profile: {e}")
# ─────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Generate tiktok_cookies.json by logging in manually."
)
parser.add_argument(
"--output", "-o",
default=DEFAULT_OUTPUT_PATH,
help=f"Output path for cookies JSON (default: {DEFAULT_OUTPUT_PATH})",
)
parser.add_argument(
"--handle",
default=None,
help=(
"TikTok handle to visit after login (e.g. jijantesfc). "
"Warms up the session cookies for that profile."
),
)
parser.add_argument(
"--timeout",
type=int,
default=LOGIN_TIMEOUT_S,
help=f"Seconds to wait for manual login (default: {LOGIN_TIMEOUT_S})",
)
args = parser.parse_args()
# ── Safety check: warn if output already exists ───────────────────
if os.path.exists(args.output):
logging.warning(
f"⚠️ '{args.output}' already exists and will be overwritten."
)
try:
from playwright.sync_api import sync_playwright
except ImportError:
logging.error(
"❌ playwright is not installed. Run: pip install playwright"
)
sys.exit(1)
logging.info("🚀 Launching headed Chromium browser...")
logging.info("=" * 60)
logging.info(" 👉 A browser window will open.")
logging.info(" 👉 Log in to TikTok manually.")
logging.info(" 👉 Solve any CAPTCHA or verification that appears.")
logging.info(" 👉 Once you reach the home feed, this script")
logging.info(" will detect it and save your cookies automatically.")
logging.info(" 👉 Do NOT close the browser window yourself.")
logging.info("=" * 60)
with sync_playwright() as p:
# ── Launch HEADED browser (visible window) ─────────────────────
# [[2]](#__2): Playwright headless=False for interactive login sessions
browser = p.chromium.launch(
headless=False, # ← must be False so you can interact
slow_mo=50, # slight slowdown for stability
args=[
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-blink-features=AutomationControlled",
"--window-size=1280,900",
],
)
context = browser.new_context(
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
),
viewport={"width": 1280, "height": 900},
locale="es-ES",
timezone_id="Europe/Madrid",
)
page = context.new_page()
# ── Navigate to TikTok login ───────────────────────────────────
logging.info(f"🌐 Opening TikTok login page: {TIKTOK_LOGIN_URL}")
try:
page.goto(TIKTOK_LOGIN_URL, wait_until="domcontentloaded",
timeout=30000)
except Exception as e:
logging.error(f"❌ Failed to open TikTok login page: {e}")
browser.close()
sys.exit(1)
# ── Wait for manual login ──────────────────────────────────────
# [[3]](#__3): Playwright storage_state / context.cookies() for session persistence
logged_in = wait_for_login(page, timeout_s=args.timeout)
if not logged_in:
logging.error(
f"❌ Login not detected within {args.timeout}s. "
"Cookies NOT saved. Please try again."
)
browser.close()
sys.exit(1)
logging.info("🎉 Login confirmed!")
# ── Optional: warm up cookies on target profile ────────────────
if args.handle:
navigate_to_profile(page, args.handle)
# ── Give TikTok a moment to set all session cookies ───────────
logging.info("⏳ Waiting 3s for all session cookies to settle...")
time.sleep(3.0)
# ── Extract and save cookies ───────────────────────────────────
raw_cookies = context.cookies()
if not raw_cookies:
logging.error("❌ No cookies found in context. Something went wrong.")
browser.close()
sys.exit(1)
tiktok_cookies = [
c for c in raw_cookies
if "tiktok.com" in c.get("domain", "")
]
logging.info(
f"🍪 Extracted {len(tiktok_cookies)} TikTok cookies "
f"(out of {len(raw_cookies)} total)."
)
normalised = normalise_cookies(tiktok_cookies)
save_cookies(normalised, args.output)
# ── Also save Playwright storage_state as backup ───────────────
storage_path = args.output.replace(".json", "_storage_state.json")
context.storage_state(path=storage_path)
logging.info(f"💾 Full storage state (backup) → {storage_path}")
browser.close()
logging.info("")
logging.info("=" * 60)
logging.info(f"✅ Done! Cookies saved to: {args.output}")
logging.info(f" Storage state saved to: {storage_path}")
logging.info("")
logging.info("Next steps:")
logging.info(f" 1. Copy '{args.output}' to your Jenkins workspace")
logging.info(" or add it as a Jenkins Secret File credential.")
logging.info(
" 2. Run tiktok2bsky.py — it will load cookies automatically."
)
logging.info(
" 3. Cookies typically last 3090 days. Re-run this script"
)
logging.info(" when the bot starts hitting CAPTCHAs again.")
logging.info("=" * 60)
if __name__ == "__main__":
main()