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()

File diff suppressed because it is too large Load Diff

338
tiktok_cookies.json Normal file
View File

@@ -0,0 +1,338 @@
[
{
"name": "tt_csrf_token",
"value": "6g7Ta7cb-46I0WOdZpGkUeAfE7TXrOTBVZ6I",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": true
},
{
"name": "tiktok_webapp_theme_source",
"value": "auto",
"domain": ".www.tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": false,
"expirationDate": 1805101281
},
{
"name": "tiktok_webapp_theme",
"value": "light",
"domain": ".www.tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": false,
"expirationDate": 1805101281
},
{
"name": "s_v_web_id",
"value": "verify_mpcelbdy_c78gS6Nv_2epq_4OKd_9mNS_GZH9mzGOiDJq",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "None",
"secure": true,
"httpOnly": false
},
{
"name": "multi_sids",
"value": "7305397368600757280%3Ae9e0f445e6e6eb00d9293b81d2fb4308",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": true,
"expirationDate": 1784365273
},
{
"name": "cmpl_token",
"value": "AgQQAPOUF-RO0rVfEpTyt10Z8G2_tH5Zv5QfYKfg6Q",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": true,
"expirationDate": 1784365273
},
{
"name": "passport_auth_status",
"value": "bf53172ab2535545b8385b46168a73fc%2C",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": true,
"expirationDate": 1781773273
},
{
"name": "passport_auth_status_ss",
"value": "bf53172ab2535545b8385b46168a73fc%2C",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "None",
"secure": true,
"httpOnly": true,
"expirationDate": 1781773273
},
{
"name": "sid_guard",
"value": "e9e0f445e6e6eb00d9293b81d2fb4308%7C1779181273%7C15551999%7CSun%2C+15-Nov-2026+09%3A01%3A12+GMT",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": true,
"expirationDate": 1810285273
},
{
"name": "uid_tt",
"value": "6a7d327365b2658105574a2fb86db67d615b0936fbc3e8ac823056eb9329c49d",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": true,
"expirationDate": 1794733272
},
{
"name": "uid_tt_ss",
"value": "6a7d327365b2658105574a2fb86db67d615b0936fbc3e8ac823056eb9329c49d",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "None",
"secure": true,
"httpOnly": true,
"expirationDate": 1794733272
},
{
"name": "sid_tt",
"value": "e9e0f445e6e6eb00d9293b81d2fb4308",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": true,
"expirationDate": 1794733272
},
{
"name": "sessionid",
"value": "e9e0f445e6e6eb00d9293b81d2fb4308",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": true,
"expirationDate": 1794733272
},
{
"name": "sessionid_ss",
"value": "e9e0f445e6e6eb00d9293b81d2fb4308",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "None",
"secure": true,
"httpOnly": true,
"expirationDate": 1794733272
},
{
"name": "tt_session_tlb_tag",
"value": "sttt%7C5%7C6eD0Rebm6wDZKTuB0vtDCP________-zK57oABhf8wrlnTz9ZSWMmMCzr-HFLPLPPYEEIs_MI6w%3D",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "None",
"secure": true,
"httpOnly": true,
"expirationDate": 1794733272
},
{
"name": "sid_ucp_v1",
"value": "1.0.1-KDJmNzI2NTMwNmVlYzAxMzYzZmNkNzNiNmUwODQzMWE2ZDMwMjE1NTYKIQigiJbUt4b_sGUQ2c2w0AYYswsgDDDj-IerBjgIQBJIBBAFGgRubzFhIiBlOWUwZjQ0NWU2ZTZlYjAwZDkyOTNiODFkMmZiNDMwODJOCiB87-dWsa2mYgbzW_QlDt-oDfUxvvMe7g8bN5MV9QQDtRIglciTyheWMYEI-nW9oeGw8bdVbBf_RHq90BDz_f4o4BwYAiIGdGlrdG9r",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": true,
"expirationDate": 1794733272
},
{
"name": "ssid_ucp_v1",
"value": "1.0.1-KDJmNzI2NTMwNmVlYzAxMzYzZmNkNzNiNmUwODQzMWE2ZDMwMjE1NTYKIQigiJbUt4b_sGUQ2c2w0AYYswsgDDDj-IerBjgIQBJIBBAFGgRubzFhIiBlOWUwZjQ0NWU2ZTZlYjAwZDkyOTNiODFkMmZiNDMwODJOCiB87-dWsa2mYgbzW_QlDt-oDfUxvvMe7g8bN5MV9QQDtRIglciTyheWMYEI-nW9oeGw8bdVbBf_RHq90BDz_f4o4BwYAiIGdGlrdG9r",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "None",
"secure": true,
"httpOnly": true,
"expirationDate": 1794733272
},
{
"name": "store-idc",
"value": "no1a",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": true,
"expirationDate": 1810717273
},
{
"name": "store-country-code",
"value": "es",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": true,
"expirationDate": 1810717273
},
{
"name": "store-country-code-src",
"value": "uid",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": true,
"expirationDate": 1810717273
},
{
"name": "tt-target-idc",
"value": "eu-ttp2",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": true,
"expirationDate": 1810717273
},
{
"name": "tt-target-idc-sign",
"value": "Y2fRqNK2uTF-G2kfsNy9Z77wH8dc_Z0QOOfz2rmXJC0pLeTahDUB5xZY0af6bWWSYwG_AWMmOdRMMAhfr-KU1jmX0QKbVW3B24X6EoPdaLtkqKtT6wuYEyki1kTifrROJLv-F9FgTfqtvQ_LHyqjmgb6Wkhm-y0_khqrozVFM4g_9mHaLgasQHSF0E-3vVf6Fm9i-nn9UAQb6LqJJCCYn_XNYadkaA_yQZF7SehhhJBBU4xBwtwg6tpNbVmuXU005TLJIj7BIrAYgR9tpYzr_YIjmrjHoBdIL_JayIRfjtR9jiIrBN826F5KZSrjj6vf1h2arsH9-hpX_BLq8lAUoHcT6xXUO_OJMABinxQUExJroj-sIJ6HZ0P1Ixm59z8c_AR535ugRquU_h9n2UQwlqY5rjT9omhikYsmjDMyndR6tNnRqCJV2uemWbE4vX944wL3Q90i7duftetE7T-nAEY0OZieFUmJckShcED3sxN6CXnL4wqlYrNdHizQVIYD",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": true,
"expirationDate": 1810717273
},
{
"name": "last_login_method",
"value": "google",
"domain": "www.tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": false,
"expirationDate": 1786957273
},
{
"name": "store-country-sign",
"value": "MEIEDA_ZltNKQuxxH5tNSAQg8bpxWK_gzlosOKZ_-wliU-OJ9qKiZPdX9qsBJZmcSR8EEFOqR7iIx8mBSOEmYSipgDU",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": true,
"expirationDate": 1810717273
},
{
"name": "tt_chain_token",
"value": "+6JILZyc1Xq4QFg0gz3u8Q==",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": true,
"expirationDate": 1794733281
},
{
"name": "x-web-secsdk-uid",
"value": "ae1efd16-3828-4789-bec6-a425bd6c95f6",
"domain": "www.tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": false
},
{
"name": "delay_guest_mode_vid",
"value": "5",
"domain": ".www.tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": false,
"expirationDate": 1805101278
},
{
"name": "tt_ticket_guard_has_set_public_key",
"value": "1",
"domain": "www.tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": false,
"expirationDate": 1779267680
},
{
"name": "odin_tt",
"value": "dfde42c9888eb5e823f2edd71b75702b22a94eb8f1ff6adbd48778fd10df92580e2cf95c942c8ce87c81aee72aad80b97ff0bf69f02d0fa00afd7015b53c6cc9c5f2057a7042db4be189c89617b62183",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": true,
"expirationDate": 1810717280
},
{
"name": "passport_fe_beating_status",
"value": "false",
"domain": ".www.tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": false
},
{
"name": "perf_feed_cache",
"value": "{%22expireTimestamp%22:0%2C%22itemIds%22:[%227640301163904109855%22%2C%22%22%2C%227626885187447590167%22]}",
"domain": ".www.tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": true,
"httpOnly": false,
"expirationDate": 1779786080
},
{
"name": "msToken",
"value": "qwWR_XX3h1ik1sfccKph_bkbUTDhk1teq9ST6Nu6ii_Tk2n5qMw_ON5OdnFgtw3t7rRNCDXfazZY-9zv_4cIfR5Tp6q7gq7DkeqphBaxnqXHSJmmlS5YZ8Lp_KVLyHH6ayPBUegehccYIx6f1OnZmw==",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "None",
"secure": true,
"httpOnly": false,
"expirationDate": 1780045281
},
{
"name": "ttwid",
"value": "1%7CF_FBmCoYt3MliZMOF9ZNeH_gOHSQ-i3qIM94nwOiDjU%7C1779181281%7Cc6e2178d520785b8741dbba5d68cd4c979d46a4df59272de62a2dc147e7446bb",
"domain": ".tiktok.com",
"path": "/",
"sameSite": "None",
"secure": true,
"httpOnly": true,
"expirationDate": 1810717281
},
{
"name": "msToken",
"value": "AqdTpKNu_rIKnlCkG42TazDTxW-R7NxXEyFxHbGAEC3kp42Bp2IURKWOWScmpeUT4YBXp-7kWFykXYC9YoARN7ynDiGwz8MVGHbuS0cmlTuqZ1jF4Nz-SWdxgB80GgL7fJfUeGfH6NqECptHRKjWGw==",
"domain": "www.tiktok.com",
"path": "/",
"sameSite": "Lax",
"secure": false,
"httpOnly": false,
"expirationDate": 1786957285
}
]

File diff suppressed because one or more lines are too long