diff --git a/README.md b/README.md index 8aaf3d1..d7e1816 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ On MacOS and Linux (debian, arch, fedora and centos, and based on those), you ca This can also be used to update the installation 4. Run `python main.py` -5. Visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps), and set up an app that is a "script". +5. Visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps), and set up an app that is a "script". Paste any URL in redirect URL. Ex:google.com 6. The bot will ask you to fill in your details to connect to the Reddit API, and configure the bot to your liking 7. Enjoy 😎 8. If you need to reconfigure the bot, simply open the `config.toml` file and delete the lines that need to be changed. On the next run of the bot, it will help you reconfigure those options. @@ -61,7 +61,7 @@ https://user-images.githubusercontent.com/66544866/173453972-6526e4e6-c6ef-41c5- ## Contributing & Ways to improve 📈 -In its current state, this bot does exactly what it needs to do. However, lots of improvements can be made. +In its current state, this bot does exactly what it needs to do. However, improvements can always be made! I have tried to simplify the code so anyone can read it and start contributing at any skill level. Don't be shy :) contribute! diff --git a/main.py b/main.py index 6d6f04a..eae5cb2 100755 --- a/main.py +++ b/main.py @@ -2,6 +2,9 @@ from asyncio import run from subprocess import Popen from os import name + +from prawcore import ResponseException + from reddit.subreddit import get_subreddit_threads from utils.cleanup import cleanup from utils.console import print_markdown, print_step @@ -14,8 +17,8 @@ from video_creation.final_video import make_final_video from video_creation.screenshot_downloader import RedditScreenshot from video_creation.voices import save_text_to_mp3 -__VERSION__ = "2.3" -__BRANCH__ = "master" +__VERSION__ = "2.3.1" +__BRANCH__ = "develop" print( """ @@ -48,14 +51,20 @@ async def main( async def run_many(times): 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 {x}{("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")[x % 10]} iteration of {times}' ) # correct 1st 2nd 3rd 4th 5th.... await main() Popen("cls" if name == "nt" else "clear", shell=True).wait() +def shutdown(): + print_markdown("## Clearing temp files") + cleanup() + exit() + + if __name__ == "__main__": - config = settings.check_toml(".config.template.toml", "config.toml") + config = settings.check_toml("utils/.config.template.toml", "config.toml") config is False and exit() try: if config["settings"]["times_to_run"]: @@ -67,7 +76,7 @@ if __name__ == "__main__": 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 {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("+"))}' ) run( main(post_id) @@ -76,6 +85,12 @@ if __name__ == "__main__": else: main() except KeyboardInterrupt: - print_markdown("## Clearing temp files") - cleanup() - exit() + shutdown() + except ResponseException: + # error for invalid credentials + print_markdown("## Invalid credentials") + print_markdown("Please check your credentials in the config.toml file") + + shutdown() + + # todo error diff --git a/requirements.txt b/requirements.txt index 0dc26df..a0fb434 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ toml==0.10.2 translators==5.3.1 pyppeteer==1.0.2 attrs==21.4.0 +Pillow~=9.1.1 \ No newline at end of file diff --git a/.config.template.toml b/utils/.config.template.toml similarity index 98% rename from .config.template.toml rename to utils/.config.template.toml index 877e45d..be2c76f 100644 --- a/.config.template.toml +++ b/utils/.config.template.toml @@ -38,7 +38,7 @@ video_width = { optional = true, default = 1080, example = 1080, explanation = " video_height = { optional = true, default = 1920, example = 1920, explanation = "Final video height", type = "int", nmin = 800, oob_error = "Choose at least 800 pixels long" } [settings.background] -background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", ""], explanation = "Sets the background for the video" } +background_choice = { optional = true, default = "minecraft", example = "minecraft", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""], explanation = "Sets the background for the video" } #background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, # false, #], explaination="Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } diff --git a/utils/CONSTANTS.py b/utils/CONSTANTS.py new file mode 100644 index 0000000..e46ebbc --- /dev/null +++ b/utils/CONSTANTS.py @@ -0,0 +1,45 @@ +# Supported Background. Can add/remove background video here.... +# - : key -> used as keyword for TOML file. value -> background configuration +# Format (value): +# 1. Youtube URI +# 2. filename +# 3. Citation (owner of the video) +# 4. Position of image clips in the background. See moviepy reference for more information. (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position) +background_options = { + "motor-gta": ( # Motor-GTA Racing + "https://www.youtube.com/watch?v=vw5L4xCPy9Q", + "bike-parkour-gta.mp4", + "Achy Gaming", + lambda t: ("center", 480 + t), + ), + "rocket-league": ( # Rocket League + "https://www.youtube.com/watch?v=2X9QGY__0II", + "rocket_league.mp4", + "Orbital Gameplay", + lambda t: ("center", 200 + t), + ), + "minecraft": ( # Minecraft parkour + "https://www.youtube.com/watch?v=n_Dv4JMiwK8", + "parkour.mp4", + "bbswitzer", + "center", + ), + "gta": ( # GTA Stunt Race + "https://www.youtube.com/watch?v=qGa9kWREOnE", + "gta-stunt-race.mp4", + "Achy Gaming", + lambda t: ("center", 480 + t), + ), + "csgo-surf": ( # CSGO Surf + "https://www.youtube.com/watch?v=E-8JlyO59Io", + "csgo-surf.mp4", + "Aki", + "center", + ), + "cluster-truck": ( # Cluster Truck Gameplay + "https://www.youtube.com/watch?v=uVKxtdMgJVU", + "cluster_truck.mp4", + "No Copyright Gameplay", + lambda t: ("center", 480 + t), + ), +} diff --git a/utils/cleanup.py b/utils/cleanup.py index ef4fc44..75074b7 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -2,6 +2,10 @@ import os from os.path import exists +def _listdir(d): # listdir with full path + return [os.path.join(d, f) for f in os.listdir(d)] + + def cleanup() -> int: """Deletes all temporary assets in assets/temp @@ -14,14 +18,12 @@ def cleanup() -> int: count += len(files) for f in files: os.remove(f) - try: - for file in os.listdir("./assets/temp/mp4"): + REMOVE_DIRS = ["./assets/temp/mp3/", "./assets/temp/png/"] + files_to_remove = list(map(_listdir, REMOVE_DIRS)) + for directory in files_to_remove: + for file in directory: count += 1 - os.remove("./assets/temp/mp4/" + file) - except FileNotFoundError: - pass - for file in os.listdir("./assets/temp/mp3"): - count += 1 - os.remove("./assets/temp/mp3/" + file) + os.remove(file) return count + return 0 diff --git a/utils/settings.py b/utils/settings.py index 8acae2a..53d83a4 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -167,4 +167,4 @@ If you see any prompts, that means that you have unset/incorrectly set variables if __name__ == "__main__": - check_toml(".config.template.toml", "config.toml") + check_toml("utils/.config.template.toml", "config.toml") diff --git a/utils/subreddit.py b/utils/subreddit.py index c1efd0c..24f3956 100644 --- a/utils/subreddit.py +++ b/utils/subreddit.py @@ -5,10 +5,11 @@ from utils import settings from utils.console import print_substep -def get_subreddit_undone(submissions: list, subreddit): +def get_subreddit_undone(submissions: list, subreddit, times_checked=0): """_summary_ Args: + times_checked: (int): For internal use, number of times function was called submissions (list): List of posts that are going to potentially be generated into a video subreddit (praw.Reddit.SubredditHelper): Chosen subreddit @@ -36,13 +37,30 @@ def get_subreddit_undone(submissions: list, subreddit): continue if submission.num_comments < int(settings.config["reddit"]["thread"]["min_comments"]): print_substep( - f'This post has under the specified minimum of comments ({settings.config["reddit"]["thread"]["min_comments"]}). Skipping...' + 'This post has under the specified minimum of comments' + f'({settings.config["reddit"]["thread"]["min_comments"]}). Skipping...' ) continue return submission print("all submissions have been done going by top submission order") + VALID_TIME_FILTERS = [ + "hour", + "day", + "month", + "week", + "year", + "all", + ] # set doesn't have __getitem__ + index = times_checked + 1 if times_checked != 0 else times_checked + if index == len(VALID_TIME_FILTERS): + print("all time filters have been checked you absolute madlad ") + return get_subreddit_undone( - subreddit.top(time_filter="hour"), subreddit + subreddit.top( + time_filter=VALID_TIME_FILTERS[index], limit=100 + ), + subreddit, + times_checked=index, ) # all the videos in hot have already been done diff --git a/utils/video.py b/utils/video.py new file mode 100644 index 0000000..0d65e68 --- /dev/null +++ b/utils/video.py @@ -0,0 +1,51 @@ +from typing import Tuple + +from PIL import ImageFont, Image, ImageDraw, ImageEnhance +from moviepy.video.VideoClip import VideoClip, ImageClip +from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip + + +class Video: + def __init__(self, video: VideoClip, *args, **kwargs): + self.video: VideoClip = video + self.fps = self.video.fps + self.duration = self.video.duration + + @staticmethod + def _create_watermark(text, fontsize, opacity=0.5): + path = "./assets/temp/png/watermark.png" + width = int(fontsize * len(text)) + height = int(fontsize * len(text) / 2) + white = (255, 255, 255) + transparent = (0, 0, 0, 0) + + font = ImageFont.load_default() + wm = Image.new("RGBA", (width, height), transparent) + im = Image.new("RGBA", (width, height), transparent) # Change this line too. + + draw = ImageDraw.Draw(wm) + w, h = draw.textsize(text, font) + draw.text(((width - w) / 2, (height - h) / 2), text, white, font) + en = ImageEnhance.Brightness(wm) # TODO allow it to use the fontsize + mask = en.enhance(1 - opacity) + im.paste(wm, (25, 25), mask) + im.save(path) + return ImageClip(path) + + def add_watermark( + self, text, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15 + ): + compensation = round( + (position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])), + ndigits=2, + ) + position = (compensation, position[1]) + img_clip = self._create_watermark(text, opacity=opacity, fontsize=fontsize) + img_clip = img_clip.set_opacity(opacity).set_duration(duration) + img_clip = img_clip.set_position( + position, relative=True + ) # TODO get data from utils/CONSTANTS.py and adapt position accordingly + + # Overlay the img clip on the first video clip + self.video = CompositeVideoClip([self.video, img_clip]) + return self.video diff --git a/video_creation/background.py b/video_creation/background.py index 09daece..73f4251 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -10,42 +10,9 @@ from pytube import YouTube from pytube.cli import on_progress from utils import settings +from utils.CONSTANTS import background_options from utils.console import print_step, print_substep -# Supported Background. Can add/remove background video here.... -# - : key -> used as keyword for TOML file. value -> background configuration -# Format (value): -# 1. Youtube URI -# 2. filename -# 3. Citation (owner of the video) -# 4. Position of image clips in the background. See moviepy reference for more information. (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position) -background_options = { - "motor-gta": ( # Motor-GTA Racing - "https://www.youtube.com/watch?v=vw5L4xCPy9Q", - "bike-parkour-gta.mp4", - "Achy Gaming", - lambda t: ("center", 480 + t), - ), - "rocket-league": ( # Rocket League - "https://www.youtube.com/watch?v=2X9QGY__0II", - "rocket_league.mp4", - "Orbital Gameplay", - lambda t: ("center", 200 + t), - ), - "minecraft": ( # Minecraft parkour - "https://www.youtube.com/watch?v=n_Dv4JMiwK8", - "parkour.mp4", - "bbswitzer", - "center", - ), - "gta": ( # GTA Stunt Race - "https://www.youtube.com/watch?v=qGa9kWREOnE", - "gta-stunt-race.mp4", - "Achy Gaming", - lambda t: ("center", 480 + t), - ), -} - def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int, int]: """Generates a random interval of time to be used as the background of the video. @@ -92,7 +59,7 @@ def download_background(background_config: Tuple[str, str, str, Any]): YouTube(uri, on_progress_callback=on_progress).streams.filter(res="1080p").first().download( "assets/backgrounds", filename=f"{credit}-{filename}" ) - print_substep("Background videos downloaded successfully! 🎉", style="bold green") + print_substep("Background video downloaded successfully! 🎉", style="bold green") def chop_background_video(background_config: Tuple[str, str, str, Any], video_length: int): diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 8904f87..f6e21c3 100755 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -18,6 +18,7 @@ from rich.progress import track from utils.cleanup import cleanup from utils.console import print_step, print_substep +from utils.video import Video from utils.videos import save_data from utils import settings from video_creation.background import download_background, chop_background_video @@ -171,6 +172,8 @@ def make_final_video( # .set_opacity(float(opacity)), # ) # else: story mode stuff + + # Can't use concatenate_videoclips here, it resets clips' start point image_concat = CompositeVideoClip(image_clips).set_position(background_config[3]) download_background(background_config) @@ -219,6 +222,18 @@ def make_final_video( print_substep('The results folder didn\'t exist so I made it') os.makedirs(f'./results/{subreddit}') + # if settings.config["settings"]['background']["background_audio"] and exists(f"assets/backgrounds/background.mp3"): + # audioclip = mpe.AudioFileClip(f"assets/backgrounds/background.mp3").set_duration(final.duration) + # audioclip = audioclip.fx( volumex, 0.2) + # final_audio = mpe.CompositeAudioClip([final.audio, audioclip]) + # # lowered_audio = audio_background.multiply_volume( # todo get this to work + # # VOLUME_MULTIPLIER) # lower volume by background_audio_volume, use with fx + # final.set_audio(final_audio) + + final = Video(final).add_watermark( + text=f"Background credit: {background_config[2]}", opacity=0.4 + ) + final.write_videofile( 'assets/temp/temp.mp4', fps=30,