From 290042a0437b8f94cd67d6f5a31a54ef4e5efc09 Mon Sep 17 00:00:00 2001 From: Hong Phuc Date: Mon, 25 May 2026 13:22:15 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20improve=20Threads=20screenshot=20quality?= =?UTF-8?q?=20=E2=80=94=20sizing,=20login=20overlay,=20dead=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Platform-aware overlay sizing: 82% width for Threads (was 45% for all) so comment screenshots fill more of the output video - scale_h: -1 → -2 across all overlay items for yuv420p compatibility - Cookie validation in auth.py: detect stale Threads sessions and re-login automatically instead of screenshotting the login modal - Remove ("button", "Log in") from popup dismissals — clicking Log in opens the auth flow, not a dismissal - Fail-fast guard after popup dismissal: raise RuntimeError if login overlay is still visible, preventing bad screenshots - Remove dead bgcolor/txtcolor assignments in screenshot.py Co-Authored-By: Claude Opus 4.7 --- platforms/threads/auth.py | 31 +++++++++++++++++++++++ platforms/threads/screenshot.py | 45 +++++++++++++++++++++++++++------ video_creation/final_video.py | 13 +++++----- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/platforms/threads/auth.py b/platforms/threads/auth.py index 62a1b51..eb051af 100644 --- a/platforms/threads/auth.py +++ b/platforms/threads/auth.py @@ -13,6 +13,7 @@ from utils import settings from utils.console import emit_scraper_event, print_substep THREADS_LOGIN_URL = "https://www.threads.com/login" +THREADS_AUTH_CHECK_URL = "https://www.threads.net/" THREADS_COOKIE_FILE = "./video_creation/data/cookie-threads.json" DEFAULT_USER_AGENT = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " @@ -45,6 +46,22 @@ def _is_logged_in_threads_url(url: str) -> bool: ) +def _has_login_gate(page: Page) -> bool: + """Detect Threads login modal overlaying the page.""" + gate_selectors = [ + 'input[autocomplete="username"]', + 'input[autocomplete="current-password"]', + 'div[role="dialog"] button:has-text("Log in")', + ] + for selector in gate_selectors: + try: + if page.locator(selector).first.is_visible(): + return True + except Exception: + continue + return False + + def login_to_threads(page: Page, _context: BrowserContext) -> None: """Log into Threads via Instagram credentials and persist session cookies.""" username = settings.config["threads"]["creds"].get("username", "").strip() @@ -131,6 +148,20 @@ def ensure_authenticated_context(browser: Browser, **kwargs) -> BrowserContext: context.add_cookies(saved_cookies) print_substep("Loaded saved Threads session cookies.") emit_scraper_event("login", {"message": "Loaded saved session cookies"}) + + # Verify loaded cookies are still valid + page = context.new_page() + try: + page.goto(THREADS_AUTH_CHECK_URL, timeout=0) + page.wait_for_load_state("domcontentloaded") + page.wait_for_timeout(1500) + if _has_login_gate(page): + print_substep("Saved Threads cookies expired. Re-logging in...", style="yellow") + context.clear_cookies() + cookie_path.unlink(missing_ok=True) + login_to_threads(page, context) + finally: + page.close() except (json.JSONDecodeError, IOError): print_substep("Saved cookies corrupted. Logging in fresh...") page = context.new_page() diff --git a/platforms/threads/screenshot.py b/platforms/threads/screenshot.py index b24128e..4321bc8 100644 --- a/platforms/threads/screenshot.py +++ b/platforms/threads/screenshot.py @@ -4,7 +4,7 @@ import re from pathlib import Path from typing import Final -from playwright.sync_api import ViewportSize +from playwright.sync_api import Page, ViewportSize from platforms.threads.auth import ensure_authenticated_context from utils import settings @@ -12,6 +12,28 @@ from utils.browser_backend import launch_browser from utils.console import print_step, print_substep +def _dismiss_popups(page: Page) -> None: + """Dismiss common Threads/Instagram overlays that block screenshots.""" + dismissals = [ + ("[role='button']", "Not now"), + ("button", "Allow all cookies"), + ("[aria-label='Close']", None), + ("div[role='dialog'] button", "Not Now"), + ] + for selector, text in dismissals: + try: + if text: + el = page.locator(selector, has_text=text).first + else: + el = page.locator(selector).first + if el.count() and el.is_visible(): + el.click() + page.wait_for_timeout(500) + print_substep(f"Dismissed popup: {selector}", style="dim") + except Exception: + pass + + def get_screenshots_of_threads_posts(content_object: dict, screenshot_num: int) -> None: """ Downloads screenshots of Threads posts via Playwright. @@ -29,14 +51,7 @@ def get_screenshots_of_threads_posts(content_object: dict, screenshot_num: int) thread_id = re.sub(r"[^\w\s-]", "", content_object["thread_id"]) Path(f"assets/temp/{thread_id}/png").mkdir(parents=True, exist_ok=True) - # Theme colors theme = settings.config["settings"]["theme"] - if theme == "dark": - bgcolor = (33, 33, 36, 255) - txtcolor = (240, 240, 240) - else: - bgcolor = (255, 255, 255, 255) - txtcolor = (0, 0, 0) # Device scale factor (higher resolution screenshots) dsf = (W // 600) + 1 @@ -56,6 +71,14 @@ def get_screenshots_of_threads_posts(content_object: dict, screenshot_num: int) page.wait_for_load_state("networkidle") page.wait_for_timeout(3000) + _dismiss_popups(page) + + if page.locator('div[role="dialog"] button:has-text("Log in")').first.is_visible(): + raise RuntimeError( + "Threads login overlay is blocking the page — saved cookies are stale. " + "Delete video_creation/data/cookie-threads.json and re-run." + ) + postcontentpath = f"assets/temp/{thread_id}/png/title.png" try: # Threads.net uses div-based cards, not
elements. @@ -98,6 +121,12 @@ def get_screenshots_of_threads_posts(content_object: dict, screenshot_num: int) if num_replies > 0: # Scroll to load all replies inline on the post page print_substep("Loading replies on post page...", style="dim") + _dismiss_popups(page) + if page.locator('div[role="dialog"] button:has-text("Log in")').first.is_visible(): + raise RuntimeError( + "Threads login overlay is blocking the page — saved cookies are stale. " + "Delete video_creation/data/cookie-threads.json and re-run." + ) last_count = 0 stable_count = 0 for _ in range(20): diff --git a/video_creation/final_video.py b/video_creation/final_video.py index e86cd17..b6907a2 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -307,10 +307,9 @@ def make_final_video( console.log(f"[bold green] Video Will Be: {length} Seconds Long") # --- Step 4: Build overlay items --- - screenshot_width = int((W * 45) // 100) - Path(f"assets/temp/{reddit_id}/png").mkdir(parents=True, exist_ok=True) - platform = settings.config["settings"].get("platform", "reddit") + screenshot_width = int(W * (0.82 if platform == "threads" else 0.45)) + Path(f"assets/temp/{reddit_id}/png").mkdir(parents=True, exist_ok=True) # Use actual screenshot for non-Reddit platforms (Threads etc.), Reddit template for Reddit title_img_path = f"assets/temp/{reddit_id}/png/title.png" @@ -330,7 +329,7 @@ def make_final_video( "duration": audio_clips_durations[0], "opacity": opacity, "scale_w": screenshot_width, - "scale_h": -1, + "scale_h": -2, }) current_time += audio_clips_durations[0] @@ -344,7 +343,7 @@ def make_final_video( "duration": audio_clips_durations[1] if len(audio_clips_durations) > 1 else 5, "opacity": opacity, "scale_w": screenshot_width, - "scale_h": -1, + "scale_h": -2, }) elif settings.config["settings"]["storymodemethod"] == 1: for i in range(number_of_clips): @@ -359,7 +358,7 @@ def make_final_video( "duration": audio_clips_durations[dur_idx], "opacity": opacity, "scale_w": screenshot_width, - "scale_h": -1, + "scale_h": -2, }) current_time += audio_clips_durations[dur_idx] else: @@ -375,7 +374,7 @@ def make_final_video( "duration": audio_clips_durations[dur_idx], "opacity": opacity, "scale_w": screenshot_width, - "scale_h": -1, + "scale_h": -2, }) current_time += audio_clips_durations[dur_idx]