diff --git a/.env.template b/.env.template index e00c242..9e75c22 100644 --- a/.env.template +++ b/.env.template @@ -1,4 +1,6 @@ REDDIT_CLIENT_ID="" REDDIT_CLIENT_SECRET="" REDDIT_USERNAME="" -REDDIT_PASSWORD="" \ No newline at end of file +REDDIT_PASSWORD="" +SUBREDDIT="AskReddit" +ALLOW_NSFW="False" diff --git a/.gitignore b/.gitignore index a4589e5..c1074a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ assets/ .env -reddit-bot-351418-5560ebc49cac.json \ No newline at end of file +reddit-bot-351418-5560ebc49cac.json +/.idea diff --git a/main.py b/main.py index 8fb3d50..0c03b56 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ from utils.console import print_markdown import time -from reddit.askreddit import get_askreddit_threads +from reddit.subreddit import get_subreddit_threads from video_creation.background import download_background, chop_background_video from video_creation.voices import save_text_to_mp3 from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts @@ -10,13 +10,17 @@ print_markdown( "### Thanks for using this tool! 😊 [Feel free to contribute to this project on GitHub!](https://lewismenelaws.com). If you have any questions, feel free to reach out to me on Twitter or submit a GitHub issue." ) -time.sleep(3) +time.sleep(2) +def main(): + reddit_object = get_subreddit_threads() -reddit_object = get_askreddit_threads() + length, number_of_comments = save_text_to_mp3(reddit_object) + download_screenshots_of_reddit_posts(reddit_object, number_of_comments) + download_background() + chop_background_video(length) + final_video = make_final_video(number_of_comments) -length, number_of_comments = save_text_to_mp3(reddit_object) -download_screenshots_of_reddit_posts(reddit_object, number_of_comments) -download_background() -chop_background_video(length) -final_video = make_final_video(number_of_comments) + +if __name__ == '__main__': + main() diff --git a/reddit/subreddit.py b/reddit/subreddit.py new file mode 100644 index 0000000..d78bbb3 --- /dev/null +++ b/reddit/subreddit.py @@ -0,0 +1,61 @@ +import re + +from utils.console import print_step, print_substep +import praw +import random +from dotenv import load_dotenv +import os + + +def ascifi(text): + regrex_pattern = re.compile(pattern="[" + u"\U0001F600-\U0001F64F" # emoticons + u"\U0001F300-\U0001F5FF" # symbols & pictographs + u"\U0001F680-\U0001F6FF" # transport & map symbols + u"\U0001F1E0-\U0001F1FF" # flags (iOS) + "]+", flags=re.UNICODE) + return regrex_pattern.sub(r'', text) + + +def get_subreddit_threads(): + """ + Returns a list of threads from the AskReddit subreddit. + """ + + print_step("Getting subreddit threads...") + + content = {} + load_dotenv() + reddit = praw.Reddit(client_id=os.getenv("REDDIT_CLIENT_ID"), client_secret=os.getenv("REDDIT_CLIENT_SECRET"), + user_agent="Accessing AskReddit threads", username=os.getenv("REDDIT_USERNAME"), + password=os.getenv("REDDIT_PASSWORD"), ) + """ + Ask user for subreddit input + """ + if not os.getenv("SUBREDDIT"): + subreddit = reddit.subreddit( + input("What subreddit would you like to pull from? ")) # if the env isnt set, ask user + else: + print_substep(f"Using subreddit: r/{os.getenv('SUBREDDIT')} from environment variable config") + subreddit = reddit.subreddit( + os.getenv("SUBREDDIT")) # Allows you to specify in .env. Done for automation purposes. + + threads = subreddit.hot(limit=25) + submission = list(threads)[random.randrange(0, 25)] + os.environ["VIDEO_TITLE"] = str(ascifi(submission.title)) + print_substep(f"Video will be: {os.getenv('VIDEO_TITLE')} :thumbsup:") + try: + + content["thread_url"] = submission.url + content["thread_title"] = submission.title + content["comments"] = [] + + for top_level_comment in submission.comments: + content["comments"].append( + {"comment_body": top_level_comment.body, "comment_url": top_level_comment.permalink, + "comment_id": top_level_comment.id, }) + + except AttributeError as e: + pass + print_substep("Received subreddit threads Successfully.", style="bold green") + return content diff --git a/utils/cleanup.py b/utils/cleanup.py new file mode 100644 index 0000000..1703e69 --- /dev/null +++ b/utils/cleanup.py @@ -0,0 +1,8 @@ +import os + + +def cleanup(): + files = [f for f in os.listdir('.') if f.endswith('.mp4') and 'temp' in f.lower()] + for f in files: + os.remove(f) + diff --git a/video_creation/background.py b/video_creation/background.py index 20dbe73..c478124 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -1,46 +1,50 @@ +import random +from os import listdir, environ +from pathlib import Path from random import randrange from pytube import YouTube -from pathlib import Path from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip from moviepy.editor import VideoFileClip +from rich.progress import track + from utils.console import print_step, print_substep def get_start_and_end_times(video_length, length_of_clip): - random_time = randrange(180, int(length_of_clip) - int(video_length)) return random_time, random_time + video_length def download_background(): - """Downloads the background video from youtube. + """Downloads the backgrounds/s video from youtube. Shoutout to: bbswitzer (https://www.youtube.com/watch?v=n_Dv4JMiwK8) + Shoutout to: Orbital Gameplay (https://www.youtube.com/watch?v=2X9QGY__0II) """ - - if not Path("assets/mp4/background.mp4").is_file(): - print_step( - "We need to download the Minecraft background video. This is fairly large but it's only done once. 😎" - ) - print_substep("Downloading the background video... please be patient 🙏") - YouTube("https://www.youtube.com/watch?v=n_Dv4JMiwK8").streams.filter( - res="720p" - ).first().download( - "assets/mp4", - filename="background.mp4", - ) - print_substep("Background video downloaded successfully! 🎉", style="bold green") + Path("assets/backgronds/").mkdir(parents=True, exist_ok=True) + background_options = [ # uri , filename , credit + ("https://www.youtube.com/watch?v=n_Dv4JMiwK8", "parkour.mp4", 'bbswitzer'), + # note: make sure the file name doesn't include a - in it + ("https://www.youtube.com/watch?v=2X9QGY__0II", "rocket_league.mp4", 'Orbital Gameplay'), ] + if listdir('assets/backgrounds/') != len(background_options): + print_step("We need to download the backgrounds videos. they are fairly large but it's only done once. 😎") + print_substep("Downloading the backgrounds video... please be patient 🙏 ") + + for uri, filename, credit in track(background_options, description="Downloading..."): + print_step(f"Downloading {filename} from {uri}") + YouTube(uri).streams.filter(res="720p").first().download("assets/backgrounds", + filename=f"{credit}-{filename}") + print_substep("Background videos downloaded successfully! 🎉", style="bold green") def chop_background_video(video_length): - print_step("Finding a spot in the background video to chop...✂️") - background = VideoFileClip("assets/mp4/background.mp4") + print_step("Finding a spot in the backgrounds video to chop...✂️") + choice = random.choice(listdir('assets/backgrounds')) + environ["background_credit"] = choice.split('-')[0] + + background = VideoFileClip(f"assets/backgrounds/{choice}") start_time, end_time = get_start_and_end_times(video_length, background.duration) - ffmpeg_extract_subclip( - "assets/mp4/background.mp4", - start_time, - end_time, - targetname="assets/mp4/clip.mp4", - ) + ffmpeg_extract_subclip(f'assets/backgrounds/{choice}', start_time, end_time, + targetname="assets/temp/backgrounds.mp4", ) print_substep("Background video chopped successfully! 🎉", style="bold green") diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 947ab04..c056218 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -1,14 +1,8 @@ -from moviepy.editor import ( - VideoFileClip, - AudioFileClip, - ImageClip, - concatenate_videoclips, - concatenate_audioclips, - CompositeAudioClip, - CompositeVideoClip, -) -from utils.console import print_step +import os +from moviepy.editor import (VideoFileClip, AudioFileClip, ImageClip, concatenate_videoclips, concatenate_audioclips, + CompositeAudioClip, CompositeVideoClip) +from utils.console import print_step W, H = 1080, 1920 @@ -19,16 +13,13 @@ def make_final_video(number_of_clips): VideoFileClip.reH = lambda clip: clip.resize(width=H) background_clip = ( - VideoFileClip("assets/mp4/clip.mp4") - .without_audio() - .resize(height=H) - .crop(x1=1166.6, y1=0, x2=2246.6, y2=1920) - ) + VideoFileClip("assets/temp/backgrounds.mp4").without_audio().resize(height=H).crop(x1=1166.6, y1=0, x2=2246.6, + y2=1920)) # Gather all audio clips audio_clips = [] for i in range(0, number_of_clips): - audio_clips.append(AudioFileClip(f"assets/mp3/{i}.mp3")) - audio_clips.insert(0, AudioFileClip(f"assets/mp3/title.mp3")) + audio_clips.append(AudioFileClip(f"assets/temp/mp3/{i}.mp3")) + audio_clips.insert(0, AudioFileClip(f"assets/temp/mp3/title.mp3")) audio_concat = concatenate_audioclips(audio_clips) audio_composite = CompositeAudioClip([audio_concat]) @@ -36,26 +27,23 @@ def make_final_video(number_of_clips): image_clips = [] for i in range(0, number_of_clips): image_clips.append( - ImageClip(f"assets/png/comment_{i}.png") - .set_duration(audio_clips[i + 1].duration) - .set_position("center") - .resize(width=W - 100), - ) - image_clips.insert( - 0, - ImageClip(f"assets/png/title.png") - .set_duration(audio_clips[0].duration) - .set_position("center") - .resize(width=W - 100), - ) - image_concat = concatenate_videoclips(image_clips).set_position( - ("center", "center") - ) + ImageClip(f"assets/temp/png/comment_{i}.png").set_duration(audio_clips[i + 1].duration).set_position( + "center").resize(width=W - 100), ) + image_clips.insert(0, ImageClip(f"assets/temp/png/title.png").set_duration(audio_clips[0].duration).set_position( + "center").resize(width=W - 100), ) + image_concat = concatenate_videoclips(image_clips).set_position(("center", "center")) image_concat.audio = audio_composite final = CompositeVideoClip([background_clip, image_concat]) - final.write_videofile( - "assets/final_video.mp4", fps=30, audio_codec="aac", audio_bitrate="192k" - ) - for i in range(0, number_of_clips): - pass + def get_video_title() -> str: + title = os.getenv("VIDEO_TITLE") or "final_video" + if len(title) <= 35: + return title + else: + return title[0:30] + "..." + + final.write_videofile(f"assets/{get_video_title()}.mp4", fps=30, audio_codec="aac", audio_bitrate="192k") + + + + print_step(f"Reddit title: {os.getenv('VIDEO_TITLE')} | Background Credit {os.getenv('background_credit')}") diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index 66c96f1..9023d82 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -1,8 +1,10 @@ +from os import getenv + from playwright.sync_api import sync_playwright from pathlib import Path from rich.progress import track -from utils.console import print_step, print_substep +from utils.console import print_step, print_substep def download_screenshots_of_reddit_posts(reddit_object, screenshot_num): """Downloads screenshots of reddit posts as they are seen on the web. @@ -14,7 +16,7 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num): print_step("Downloading Screenshots of Reddit Posts 📷") # ! Make sure the reddit screenshots folder exists - Path("assets/png").mkdir(parents=True, exist_ok=True) + Path("assets/temp/png").mkdir(parents=True, exist_ok=True) with sync_playwright() as p: print_substep("Launching Headless Browser...") @@ -27,17 +29,18 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num): if page.locator('[data-testid="content-gate"]').is_visible(): # This means the post is NSFW and requires to click the proceed button. + if getenv("ALLOW_NSFW").casefold() == "false": + print_substep("NSFW Post Detected. Skipping...") + from subprocess import call + call(["python", "main.py"]) + exit(1) print_substep("Post is NSFW. You are spicy... :fire:") page.locator('[data-testid="content-gate"] button').click() - page.locator('[data-test-id="post-content"]').screenshot( - path="assets/png/title.png" - ) + page.locator('[data-test-id="post-content"]').screenshot(path="assets/temp/png/title.png") - for idx, comment in track( - enumerate(reddit_object["comments"]), "Downloading screenshots..." - ): + for idx, comment in track(enumerate(reddit_object["comments"]), "Downloading screenshots..."): # Stop if we have reached the screenshot_num if idx >= screenshot_num: @@ -47,7 +50,5 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num): page.locator('[data-testid="content-gate"] button').click() page.goto(f'https://reddit.com{comment["comment_url"]}') - page.locator(f"#t1_{comment['comment_id']}").screenshot( - path=f"assets/png/comment_{idx}.png" - ) + page.locator(f"#t1_{comment['comment_id']}").screenshot(path=f"assets/temp/png/comment_{idx}.png") print_substep("Screenshots downloaded Successfully.", style="bold green") diff --git a/video_creation/test.py b/video_creation/test.py deleted file mode 100644 index e69de29..0000000 diff --git a/video_creation/voices.py b/video_creation/voices.py index d719ff9..70f8a17 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -15,19 +15,19 @@ def save_text_to_mp3(reddit_obj): length = 0 # Create a folder for the mp3 files. - Path("assets/mp3").mkdir(parents=True, exist_ok=True) + Path("assets/temp/mp3").mkdir(parents=True, exist_ok=True) tts = gTTS(text=reddit_obj["thread_title"], lang="en", slow=False, tld="co.uk") - tts.save(f"assets/mp3/title.mp3") - length += MP3(f"assets/mp3/title.mp3").info.length + tts.save(f"assets/temp/mp3/title.mp3") + length += MP3(f"assets/temp/mp3/title.mp3").info.length for idx, comment in track(enumerate(reddit_obj["comments"]), "Saving..."): # ! Stop creating mp3 files if the length is greater than 50 seconds. This can be longer, but this is just a good starting point if length > 50: break tts = gTTS(text=comment["comment_body"], lang="en") - tts.save(f"assets/mp3/{idx}.mp3") - length += MP3(f"assets/mp3/{idx}.mp3").info.length + tts.save(f"assets/temp/mp3/{idx}.mp3") + length += MP3(f"assets/temp/mp3/{idx}.mp3").info.length print_substep("Saved Text to MP3 files Successfully.", style="bold green") # ! Return the index so we know how many screenshots of comments we need to make.