diff --git a/Dockerfile b/Dockerfile index bca6aa6..534334f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ WORKDIR /app ADD requirements.txt /app/requirements.txt RUN pip install -r requirements.txt RUN python3 -m spacy download en_core_web_sm +RUN playwright install && playwright install-deps # tricks for pytube : https://github.com/elebumm/RedditVideoMakerBot/issues/142 # (NOTE : This is no longer useful since pytube was removed from the dependencies) diff --git a/utils/.config.template.toml b/utils/.config.template.toml index 98b65a2..296cd33 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -36,6 +36,7 @@ opacity = { optional = false, default = 0.9, example = 0.8, explanation = "Sets storymode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only read out title and post content, great for subreddits with stories" } storymodemethod= { optional = true, default = 1, example = 1, explanation = "Style that's used for the storymode. Set to 0 for single picture display in whole video, set to 1 for fancy looking video ", type = "int", nmin = 0, oob_error = "It's very hard to run something less than once.", options = [0, 1] } storymode_max_length = { optional = true, default = 1000, example = 1000, explanation = "Max length of the storymode video in characters. 200 characters are approximately 50 seconds.", type = "int", nmin = 1, oob_error = "It's very hard to make a video under a second." } +title_screenshot = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Display reddit screenshot for title" } resolution_w = { optional = false, default = 1080, example = 1440, explantation = "Sets the width in pixels of the final video" } resolution_h = { optional = false, default = 1920, example = 2560, explantation = "Sets the height in pixels of the final video" } text_size = { optional = false, default = 56, example = 75, explantation = "Sets the font size for the captions" } diff --git a/utils/imagenarator.py b/utils/imagenarator.py index 779e208..090c6ec 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -2,6 +2,7 @@ import re import textwrap import os import json +from typing import Final from utils import settings @@ -9,6 +10,169 @@ from PIL import Image, ImageDraw, ImageFont from rich.progress import track from TTS.engine_wrapper import process_text from utils.console import print_substep +from utils.videos import save_data + + +from pathlib import Path + +import translators +from playwright.async_api import async_playwright # pylint: disable=unused-import +from playwright.sync_api import ViewportSize, sync_playwright +from rich.progress import track + +from utils import settings +from utils.console import print_substep +from utils.playwright import clear_cookie_by_name + +from utils.videos import save_data + + +def get_title_screenshot(reddit_object: dict): + W: Final[int] = int(settings.config["settings"]["resolution_w"]) + H: Final[int] = int(settings.config["settings"]["resolution_h"]) + lang: Final[str] = settings.config["reddit"]["thread"]["post_lang"] + + reddit_id = re.sub(r"[^\w\s-]", "", reddit_object["thread_id"]) + # ! Make sure the reddit screenshots folder exists + Path(f"assets/temp/{reddit_id}/png").mkdir(parents=True, exist_ok=True) + + if settings.config["settings"]["theme"] == "dark": + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + elif settings.config["settings"]["theme"] == "transparent": + cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8") + else: + cookie_file = open("./video_creation/data/cookie-light-mode.json", encoding="utf-8") + + with sync_playwright() as p: + print_substep("Launching Headless Browser...") + + browser = p.chromium.launch( + headless=True + ) # headless=False will show the browser for debugging purposes + # Device scale factor (or dsf for short) allows us to increase the resolution of the screenshots + # When the dsf is 1, the width of the screenshot is 600 pixels + # so we need a dsf such that the width of the screenshot is greater than the final resolution of the video + dsf = (W // 600) + 1 + + context = browser.new_context( + locale=lang or "en-us", + color_scheme="dark", + viewport=ViewportSize(width=W, height=H), + device_scale_factor=dsf, + ) + cookies = json.load(cookie_file) + cookie_file.close() + + context.add_cookies(cookies) # load preference cookies + + # Login to Reddit + print_substep("Logging in to Reddit...") + page = context.new_page() + page.goto("https://www.reddit.com/login", timeout=0) + page.set_viewport_size(ViewportSize(width=1920, height=1080)) + page.wait_for_load_state() + + page.locator('[name="username"]').fill(settings.config["reddit"]["creds"]["username"]) + page.locator('[name="password"]').fill(settings.config["reddit"]["creds"]["password"]) + page.locator("button[class$='m-full-width']").click() + page.wait_for_timeout(5000) + + login_error_div = page.locator(".AnimatedForm__errorMessage").first + if login_error_div.is_visible(): + login_error_message = login_error_div.inner_text() + if login_error_message.strip() == "": + # The div element is empty, no error + pass + else: + # The div contains an error message + print_substep( + "Your reddit credentials are incorrect! Please modify them accordingly in the config.toml file.", + style="red", + ) + exit() + else: + pass + + page.wait_for_load_state() + # Handle the redesign + # Check if the redesign optout cookie is set + if page.locator("#redesign-beta-optin-btn").is_visible(): + # Clear the redesign optout cookie + clear_cookie_by_name(context, "redesign_optout") + # Reload the page for the redesign to take effect + page.reload() + # Get the thread screenshot + page.goto(reddit_object["thread_url"], timeout=0) + page.set_viewport_size(ViewportSize(width=W, height=H)) + page.wait_for_load_state() + page.wait_for_timeout(5000) + + if page.locator( + "#t3_12hmbug > div > div._3xX726aBn29LDbsDtzr_6E._1Ap4F5maDtT1E1YuCiaO0r.D3IL3FD0RFy_mkKLPwL4 > div > div > button" + ).is_visible(): + # This means the post is NSFW and requires to click the proceed button. + + print_substep("Post is NSFW. You are spicy...") + page.locator( + "#t3_12hmbug > div > div._3xX726aBn29LDbsDtzr_6E._1Ap4F5maDtT1E1YuCiaO0r.D3IL3FD0RFy_mkKLPwL4 > div > div > button" + ).click() + page.wait_for_load_state() # Wait for page to fully load + + # translate code + if page.locator( + "#SHORTCUT_FOCUSABLE_DIV > div:nth-child(7) > div > div > div > header > div > div._1m0iFpls1wkPZJVo38-LSh > button > i" + ).is_visible(): + page.locator( + "#SHORTCUT_FOCUSABLE_DIV > div:nth-child(7) > div > div > div > header > div > div._1m0iFpls1wkPZJVo38-LSh > button > i" + ).click() # Interest popup is showing, this code will close it + + if lang: + print_substep("Translating post...") + texts_in_tl = translators.translate_text( + reddit_object["thread_title"], + to_language=lang, + translator="google", + ) + + page.evaluate( + "tl_content => document.querySelector('[data-adclicklocation=\"title\"] > div > div > h1').textContent = tl_content", + texts_in_tl, + ) + else: + print_substep("Skipping translation...") + + postcontentpath = f"assets/temp/{reddit_id}/png/title.png" + try: + if settings.config["settings"]["zoom"] != 1: + # store zoom settings + zoom = settings.config["settings"]["zoom"] + # zoom the body of the page + page.evaluate("document.body.style.zoom=" + str(zoom)) + # as zooming the body doesn't change the properties of the divs, we need to adjust for the zoom + location = page.locator('[data-test-id="post-content"]').bounding_box() + for i in location: + location[i] = float("{:.2f}".format(location[i] * zoom)) + page.screenshot(clip=location, path=postcontentpath) + else: + page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath) + except Exception as e: + print_substep("Something went wrong!", style="red") + resp = input( + "Something went wrong with making the screenshots! Do you want to skip the post? (y/n) " + ) + + if resp.casefold().startswith("y"): + save_data("", "", "skipped", reddit_id, "") + print_substep( + "The post is successfully skipped! You can now restart the program and this post will skipped.", + "green", + ) + + resp = input("Do you want the error traceback for debugging purposes? (y/n)") + if not resp.casefold().startswith("y"): + exit() + + raise e def load_text_replacements(): text_replacements = {} @@ -91,12 +255,14 @@ def imagemaker(theme, reddit_obj: dict, txtclr, transparent=False) -> None: size = (int(settings.config["settings"]["resolution_w"]), int(settings.config["settings"]["resolution_h"])) - image = Image.new("RGBA", size, theme) - - # for title - draw_multiple_line_text(image, perform_text_replacements(title), tfont, txtclr, int(settings.config["settings"]["text_padding"]), wrap=int(settings.config["settings"]["text_wrap"]), transparent=transparent) - image.save(f"assets/temp/{id}/png/title.png") + if bool(settings.config['settings']['title_screenshot']): + get_title_screenshot(reddit_obj) + else: + image = Image.new("RGBA", size, theme) + # for title + draw_multiple_line_text(image, perform_text_replacements(title), tfont, txtclr, int(settings.config["settings"]["text_padding"]), wrap=int(settings.config["settings"]["text_wrap"]), transparent=transparent) + image.save(f"assets/temp/{id}/png/title.png") for idx, text in track(enumerate(texts), "💬 Rendering captions...", total=len(texts)): image = Image.new("RGBA", size, theme)