diff --git a/main.py b/main.py index 849663d..6120860 100755 --- a/main.py +++ b/main.py @@ -4,17 +4,17 @@ import sys from os import name from pathlib import Path from subprocess import Popen -from typing import NoReturn +from typing import Dict, NoReturn, Optional from prawcore import ResponseException from reddit.subreddit import get_subreddit_threads from utils import settings from utils.cleanup import cleanup -from utils.console import print_markdown, print_step, print_substep +from utils.console import print_markdown, print_step, format_ordinal from utils.ffmpeg_install import ffmpeg_install -from utils.id import id -from utils.version import checkversion +from utils.id import extract_id +from utils.version import checkversion, check_python from video_creation.background import ( chop_background, download_background_audio, @@ -38,15 +38,20 @@ print( """ ) print_markdown( - "### Thanks for using this tool! Feel free to contribute to this project on GitHub! If you have any questions, feel free to join my Discord server or submit a GitHub issue. You can find solutions to many common problems in the documentation: https://reddit-video-maker-bot.netlify.app/" + "### Thanks for using this tool! Feel free to contribute to this project on GitHub! If you have any questions," + " feel free to join my Discord server or submit a GitHub issue." + " You can find solutions to many common problems in the documentation: https://reddit-video-maker-bot.netlify.app/" ) checkversion(__VERSION__) +reddit_id: Optional[str] = None +reddit_object: Dict[str, str | list] + def main(POST_ID=None) -> None: global redditid, reddit_object reddit_object = get_subreddit_threads(POST_ID) - redditid = id(reddit_object) + redditid = extract_id(reddit_object) length, number_of_comments = save_text_to_mp3(reddit_object) length = math.ceil(length) get_screenshots_of_reddit_posts(reddit_object, number_of_comments) @@ -63,14 +68,14 @@ def main(POST_ID=None) -> None: def run_many(times) -> None: for x in range(1, times + 1): print_step( - f'on the {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' + f"on the {format_ordinal(x)} iteration of {times}" ) # correct 1st 2nd 3rd 4th 5th.... main() Popen("cls" if name == "nt" else "clear", shell=True).wait() def shutdown() -> NoReturn: - if "redditid" in globals(): + if reddit_id is not None: print_markdown("## Clearing temp files") cleanup(redditid) @@ -79,33 +84,19 @@ def shutdown() -> NoReturn: if __name__ == "__main__": - if sys.version_info.major != 3 or sys.version_info.minor not in [10, 11]: - print( - "Hey! Congratulations, you've made it so far (which is pretty rare with no Python 3.10). Unfortunately, this program only works on Python 3.10. Please install Python 3.10 and try again." - ) - sys.exit() + check_python() ffmpeg_install() directory = Path().absolute() - config = settings.check_toml( - f"{directory}/utils/.config.template.toml", f"{directory}/config.toml" - ) - config is False and sys.exit() + config = settings.get_config(directory) - if ( - not settings.config["settings"]["tts"]["tiktok_sessionid"] - or settings.config["settings"]["tts"]["tiktok_sessionid"] == "" - ) and config["settings"]["tts"]["voice_choice"] == "tiktok": - print_substep( - "TikTok voice requires a sessionid! Check our documentation on how to obtain one.", - "bold red", - ) - sys.exit() try: if config["reddit"]["thread"]["post_id"]: - for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): + for index, post_id in enumerate( + config["reddit"]["thread"]["post_id"].split("+") + ): index += 1 print_step( - f'on the {index}{("st" if index % 10 == 1 else ("nd" if index % 10 == 2 else ("rd" if index % 10 == 3 else "th")))} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' + f'on the {format_ordinal(index)} post of {len(config["reddit"]["thread"]["post_id"].split("+"))}' ) main(post_id) Popen("cls" if name == "nt" else "clear", shell=True).wait() diff --git a/utils/console.py b/utils/console.py index 18c3248..481fd7a 100644 --- a/utils/console.py +++ b/utils/console.py @@ -118,3 +118,15 @@ def handle_input( console.print( "[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "." ) + +def format_ordinal(x): + if 10 <= x % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(x % 10, 'th') + return f"{x}{suffix}" + + +if __name__ == "__main__": + for i in range(20): + print(format_ordinal(i)) \ No newline at end of file diff --git a/utils/id.py b/utils/id.py index 3d76593..0d1ec17 100644 --- a/utils/id.py +++ b/utils/id.py @@ -3,7 +3,7 @@ import re from utils.console import print_substep -def id(reddit_obj: dict): +def extract_id(reddit_obj: dict): """ This function takes a reddit object and returns the post id """ diff --git a/utils/settings.py b/utils/settings.py index 2ebaef3..b2d2607 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -1,11 +1,12 @@ import re from pathlib import Path -from typing import Dict, Tuple +import sys +from typing import Any, Dict, Literal, Tuple import toml from rich.console import Console -from utils.console import handle_input +from utils.console import handle_input, print_substep console = Console() config = dict # autocomplete @@ -53,7 +54,11 @@ def check(value, checks, name): and not hasattr(value, "__iter__") and ( ("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and value > checks["nmax"]) + or ( + "nmax" in checks + and checks["nmax"] is not None + and value > checks["nmax"] + ) ) ): incorrect = True @@ -61,8 +66,16 @@ def check(value, checks, name): not incorrect and hasattr(value, "__iter__") and ( - ("nmin" in checks and checks["nmin"] is not None and len(value) < checks["nmin"]) - or ("nmax" in checks and checks["nmax"] is not None and len(value) > checks["nmax"]) + ( + "nmin" in checks + and checks["nmin"] is not None + and len(value) < checks["nmin"] + ) + or ( + "nmax" in checks + and checks["nmax"] is not None + and len(value) > checks["nmax"] + ) ) ): incorrect = True @@ -70,9 +83,15 @@ def check(value, checks, name): if incorrect: value = handle_input( message=( - (("[blue]Example: " + str(checks["example"]) + "\n") if "example" in checks else "") + ( + ("[blue]Example: " + str(checks["example"]) + "\n") + if "example" in checks + else "" + ) + "[red]" - + ("Non-optional ", "Optional ")["optional" in checks and checks["optional"] is True] + + ("Non-optional ", "Optional ")[ + "optional" in checks and checks["optional"] is True + ] ) + "[#C0CAF5 bold]" + str(name) @@ -107,13 +126,15 @@ def check_vars(path, checks): crawl_and_check(config, path, checks) -def check_toml(template_file, config_file) -> Tuple[bool, Dict]: +def check_toml(template_file, config_file) -> Dict[str, Any] | None | Literal[False]: global config config = None try: template = toml.load(template_file) except Exception as error: - console.print(f"[red bold]Encountered error when trying to to load {template_file}: {error}") + console.print( + f"[red bold]Encountered error when trying to to load {template_file}: {error}" + ) return False try: config = toml.load(config_file) @@ -164,6 +185,25 @@ If you see any prompts, that means that you have unset/incorrectly set variables toml.dump(config, f) return config +def get_config(directory): + config = check_toml( + f"{directory}/utils/.config.template.toml", f"{directory}/config.toml" + ) + if not config: + sys.exit() + + if ( + not config["settings"]["tts"]["tiktok_sessionid"] + or config["settings"]["tts"]["tiktok_sessionid"] == "" + ) and config["settings"]["tts"]["voice_choice"] == "tiktok": + print_substep( + "TikTok voice requires a sessionid! Check our documentation on how to obtain one.", + "bold red", + ) + sys.exit() + + return config + if __name__ == "__main__": directory = Path().absolute() diff --git a/utils/version.py b/utils/version.py index 0818c87..2f6d08c 100644 --- a/utils/version.py +++ b/utils/version.py @@ -1,3 +1,4 @@ +import sys import requests from utils.console import print_step @@ -19,3 +20,11 @@ def checkversion(__VERSION__: str): print_step( f"Welcome to the test version ({__VERSION__}) of the bot. Thanks for testing and feel free to report any bugs you find." ) + +def check_python() -> None: + minor_versions = [10, 11, 12, 13] + if sys.version_info.major != 3 or sys.version_info.minor not in minor_versions: + print( + f"Hey! Congratulations, you've made it so far (which is pretty rare with no Python 3.{minor_versions}). Unfortunately, this program only works on Python 3.{minor_versions}. Please install Python 3.{minor_versions} and try again." + ) + sys.exit() \ No newline at end of file diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 101d0f7..06b6af5 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -2,7 +2,6 @@ import multiprocessing import os import re import tempfile -import textwrap import threading import time from os.path import exists # Needs to be imported specifically @@ -11,16 +10,16 @@ from typing import Dict, Final, Tuple import ffmpeg import translators -from PIL import Image, ImageDraw, ImageFont +from PIL import Image from rich.console import Console from rich.progress import track from utils import settings from utils.cleanup import cleanup from utils.console import print_step, print_substep -from utils.fonts import getheight -from utils.thumbnail import create_thumbnail from utils.videos import save_data +from video_creation.thumbnail import background_thumbnail +from .thumbnail import create_fancy_thumbnail console = Console() @@ -108,63 +107,6 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str: return output_path -def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): - print_step(f"Creating fancy thumbnail for: {text}") - font_title_size = 47 - font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) - image_width, image_height = image.size - lines = textwrap.wrap(text, width=wrap) - y = ( - (image_height / 2) - - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) - + 30 - ) - draw = ImageDraw.Draw(image) - - username_font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 30) - draw.text( - (205, 825), - settings.config["settings"]["channel_name"], - font=username_font, - fill=text_color, - align="left", - ) - - if len(lines) == 3: - lines = textwrap.wrap(text, width=wrap + 10) - font_title_size = 40 - font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) - y = ( - (image_height / 2) - - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) - + 35 - ) - elif len(lines) == 4: - lines = textwrap.wrap(text, width=wrap + 10) - font_title_size = 35 - font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) - y = ( - (image_height / 2) - - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) - + 40 - ) - elif len(lines) > 4: - lines = textwrap.wrap(text, width=wrap + 10) - font_title_size = 30 - font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) - y = ( - (image_height / 2) - - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) - + 30 - ) - - for line in lines: - draw.text((120, y), line, font=font, fill=text_color, align="left") - y += getheight(font, line) + padding - - return image - - def merge_background_audio(audio: ffmpeg, reddit_id: str): """Gather an audio and merge with assets/backgrounds/background.mp3 Args: @@ -266,17 +208,12 @@ def make_final_video( # get the title_template image and draw a text in the middle part of it with the title of the thread title_template = Image.open("assets/title_template.png") - title = reddit_obj["thread_title"] - - title = name_normalize(title) - + title = name_normalize(reddit_obj["thread_title"]) font_color = "#000000" padding = 5 - - # create_fancy_thumbnail(image, text, text_color, padding title_img = create_fancy_thumbnail(title_template, title, font_color, padding) - title_img.save(f"assets/temp/{reddit_id}/png/title.png") + image_clips.insert( 0, ffmpeg.input(f"assets/temp/{reddit_id}/png/title.png")["v"].filter( @@ -362,36 +299,7 @@ def make_final_video( settingsbackground = settings.config["settings"]["background"] if settingsbackground["background_thumbnail"]: - if not exists(f"./results/{subreddit}/thumbnails"): - print_substep( - "The 'results/thumbnails' folder could not be found so it was automatically created." - ) - os.makedirs(f"./results/{subreddit}/thumbnails") - # get the first file with the .png extension from assets/backgrounds and use it as a background for the thumbnail - first_image = next( - (file for file in os.listdir("assets/backgrounds") if file.endswith(".png")), - None, - ) - if first_image is None: - print_substep("No png files found in assets/backgrounds", "red") - - else: - font_family = settingsbackground["background_thumbnail_font_family"] - font_size = settingsbackground["background_thumbnail_font_size"] - font_color = settingsbackground["background_thumbnail_font_color"] - thumbnail = Image.open(f"assets/backgrounds/{first_image}") - width, height = thumbnail.size - thumbnailSave = create_thumbnail( - thumbnail, - font_family, - font_size, - font_color, - width, - height, - title_thumb, - ) - thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png") - print_substep(f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png") + background_thumbnail(reddit_id, title_thumb, subreddit) text = f"Background by {background_config['video'][2]}" background_clip = ffmpeg.drawtext( diff --git a/video_creation/thumbnail.py b/video_creation/thumbnail.py new file mode 100644 index 0000000..6941b01 --- /dev/null +++ b/video_creation/thumbnail.py @@ -0,0 +1,100 @@ +import os +import textwrap +from os.path import exists + +from PIL import Image, ImageDraw, ImageFont + +from utils import settings +from utils.console import print_step, print_substep +from utils.fonts import getheight +from utils.thumbnail import create_thumbnail + + +def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): + print_step(f"Creating fancy thumbnail for: {text}") + font_title_size = 47 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + image_width, image_height = image.size + lines = textwrap.wrap(text, width=wrap) + y = ( + (image_height / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 30 + ) + draw = ImageDraw.Draw(image) + + username_font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 30) + draw.text( + (205, 825), + settings.config["settings"]["channel_name"], + font=username_font, + fill=text_color, + align="left", + ) + + if len(lines) == 3: + lines = textwrap.wrap(text, width=wrap + 10) + font_title_size = 40 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + y = ( + (image_height / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 35 + ) + elif len(lines) == 4: + lines = textwrap.wrap(text, width=wrap + 10) + font_title_size = 35 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + y = ( + (image_height / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 40 + ) + elif len(lines) > 4: + lines = textwrap.wrap(text, width=wrap + 10) + font_title_size = 30 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + y = ( + (image_height / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 30 + ) + + for line in lines: + draw.text((120, y), line, font=font, fill=text_color, align="left") + y += getheight(font, line) + padding + + return image + + +def background_thumbnail(reddit_id, title_thumb, subreddit): + if not exists(f"./results/{subreddit}/thumbnails"): + print_substep( + "The 'results/thumbnails' folder could not be found so it was automatically created." + ) + os.makedirs(f"./results/{subreddit}/thumbnails") + # get the first file with the .png extension from assets/backgrounds and use it as a background for the thumbnail + first_image = next( + (file for file in os.listdir("assets/backgrounds") if file.endswith(".png")), + None, + ) + if first_image is None: + print_substep("No png files found in assets/backgrounds", "red") + + else: + font_family = settings.config["settings"]["background"]["background_thumbnail_font_family"] + font_size = settings.config["settings"]["background"]["background_thumbnail_font_size"] + font_color = settings.config["settings"]["background"]["background_thumbnail_font_color"] + thumbnail = Image.open(f"assets/backgrounds/{first_image}") + width, height = thumbnail.size + thumbnailSave = create_thumbnail( + thumbnail, + font_family, + font_size, + font_color, + width, + height, + title_thumb, + ) + thumbnailSave.save(f"./assets/temp/{reddit_id}/thumbnail.png") + print_substep(f"Thumbnail - Building Thumbnail in assets/temp/{reddit_id}/thumbnail.png") \ No newline at end of file