Converted spaces into tabs for all files

pull/558/head
ART3MISTICAL 3 years ago
parent 365ebef0d3
commit a56e818e26

@ -5,7 +5,7 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "pip" # See documentation for possible values - package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "daily" interval: "daily"

@ -12,61 +12,61 @@
name: "CodeQL" name: "CodeQL"
on: on:
push: push:
branches: [ "master" ] branches: [ "master" ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ "master" ] branches: [ "master" ]
schedule: schedule:
- cron: '16 14 * * 3' - cron: '16 14 * * 3'
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
actions: read actions: read
contents: read contents: read
security-events: write security-events: write
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'python' ] language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality # queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines. # If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: | # - run: |
# echo "Run, Build Application using script" # echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v2

@ -2,12 +2,12 @@ from gtts import gTTS
class GTTS: class GTTS:
def tts( def tts(
self, self,
req_text: str = "Google Text To Speech", req_text: str = "Google Text To Speech",
filename: str = "title.mp3", filename: str = "title.mp3",
random_speaker=False, random_speaker=False,
censor=False, censor=False,
): ):
tts = gTTS(text=req_text, lang="en", slow=False) tts = gTTS(text=req_text, lang="en", slow=False)
tts.save(f"{filename}") tts.save(f"{filename}")

@ -9,21 +9,21 @@ from moviepy.audio.io.AudioFileClip import AudioFileClip
from requests.exceptions import JSONDecodeError from requests.exceptions import JSONDecodeError
voices = [ voices = [
"Brian", "Brian",
"Emma", "Emma",
"Russell", "Russell",
"Joey", "Joey",
"Matthew", "Matthew",
"Joanna", "Joanna",
"Kimberly", "Kimberly",
"Amy", "Amy",
"Geraint", "Geraint",
"Nicole", "Nicole",
"Justin", "Justin",
"Ivy", "Ivy",
"Kendra", "Kendra",
"Salli", "Salli",
"Raveena", "Raveena",
] ]
@ -31,78 +31,78 @@ voices = [
class POLLY: class POLLY:
def __init__(self): def __init__(self):
self.url = "https://streamlabs.com/polly/speak" self.url = "https://streamlabs.com/polly/speak"
def tts( def tts(
self, self,
req_text: str = "Amazon Text To Speech", req_text: str = "Amazon Text To Speech",
filename: str = "title.mp3", filename: str = "title.mp3",
random_speaker=False, random_speaker=False,
censor=False, censor=False,
): ):
if random_speaker: if random_speaker:
voice = self.randomvoice() voice = self.randomvoice()
else: else:
if not os.getenv("VOICE"): if not os.getenv("VOICE"):
return ValueError( return ValueError(
"Please set the environment variable VOICE to a valid voice. options are: {}".format( "Please set the environment variable VOICE to a valid voice. options are: {}".format(
voices voices
) )
) )
voice = str(os.getenv("VOICE")).capitalize() voice = str(os.getenv("VOICE")).capitalize()
body = {"voice": voice, "text": req_text, "service": "polly"} body = {"voice": voice, "text": req_text, "service": "polly"}
response = requests.post(self.url, data=body) response = requests.post(self.url, data=body)
try: try:
voice_data = requests.get(response.json()["speak_url"]) voice_data = requests.get(response.json()["speak_url"])
with open(filename, "wb") as f: with open(filename, "wb") as f:
f.write(voice_data.content) f.write(voice_data.content)
except (KeyError, JSONDecodeError): except (KeyError, JSONDecodeError):
if response.json()["error"] == "Text length is too long!": if response.json()["error"] == "Text length is too long!":
chunks = [ chunks = [
m.group().strip() for m in re.finditer(r" *((.{0,499})(\.|.$))", req_text) m.group().strip() for m in re.finditer(r" *((.{0,499})(\.|.$))", req_text)
] ]
audio_clips = [] audio_clips = []
cbn = sox.Combiner() cbn = sox.Combiner()
chunkId = 0 chunkId = 0
for chunk in chunks: for chunk in chunks:
body = {"voice": voice, "text": chunk, "service": "polly"} body = {"voice": voice, "text": chunk, "service": "polly"}
resp = requests.post(self.url, data=body) resp = requests.post(self.url, data=body)
voice_data = requests.get(resp.json()["speak_url"]) voice_data = requests.get(resp.json()["speak_url"])
with open(filename.replace(".mp3", f"-{chunkId}.mp3"), "wb") as out: with open(filename.replace(".mp3", f"-{chunkId}.mp3"), "wb") as out:
out.write(voice_data.content) out.write(voice_data.content)
audio_clips.append(filename.replace(".mp3", f"-{chunkId}.mp3")) audio_clips.append(filename.replace(".mp3", f"-{chunkId}.mp3"))
chunkId = chunkId + 1 chunkId = chunkId + 1
try: try:
if len(audio_clips) > 1: if len(audio_clips) > 1:
cbn.convert(samplerate=44100, n_channels=2) cbn.convert(samplerate=44100, n_channels=2)
cbn.build(audio_clips, filename, "concatenate") cbn.build(audio_clips, filename, "concatenate")
else: else:
os.rename(audio_clips[0], filename) os.rename(audio_clips[0], filename)
except ( except (
sox.core.SoxError, sox.core.SoxError,
FileNotFoundError, FileNotFoundError,
): # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339 ): # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339
for clip in audio_clips: for clip in audio_clips:
i = audio_clips.index(clip) # get the index of the clip i = audio_clips.index(clip) # get the index of the clip
audio_clips = ( audio_clips = (
audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1 :] audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1 :]
) # replace the clip with an AudioFileClip ) # replace the clip with an AudioFileClip
audio_concat = concatenate_audioclips(audio_clips) audio_concat = concatenate_audioclips(audio_clips)
audio_composite = CompositeAudioClip([audio_concat]) audio_composite = CompositeAudioClip([audio_concat])
audio_composite.write_audiofile(filename, 44100, 2, 2000, None) audio_composite.write_audiofile(filename, 44100, 2, 2000, None)
def make_readable(self, text): def make_readable(self, text):
""" """
Amazon Polly fails to read some symbols properly such as '& (and)'. Amazon Polly fails to read some symbols properly such as '& (and)'.
So we normalize input text before passing it to the service So we normalize input text before passing it to the service
""" """
text = text.replace("&", "and") text = text.replace("&", "and")
return text return text
def randomvoice(self): def randomvoice(self):
return random.choice(voices) return random.choice(voices)

@ -15,49 +15,49 @@ from requests.adapters import HTTPAdapter, Retry
# https://twitter.com/scanlime/status/1512598559769702406 # https://twitter.com/scanlime/status/1512598559769702406
nonhuman = [ # DISNEY VOICES nonhuman = [ # DISNEY VOICES
"en_us_ghostface", # Ghost Face "en_us_ghostface", # Ghost Face
"en_us_chewbacca", # Chewbacca "en_us_chewbacca", # Chewbacca
"en_us_c3po", # C3PO "en_us_c3po", # C3PO
"en_us_stitch", # Stitch "en_us_stitch", # Stitch
"en_us_stormtrooper", # Stormtrooper "en_us_stormtrooper", # Stormtrooper
"en_us_rocket", # Rocket "en_us_rocket", # Rocket
# ENGLISH VOICES # ENGLISH VOICES
] ]
human = [ human = [
"en_au_001", # English AU - Female "en_au_001", # English AU - Female
"en_au_002", # English AU - Male "en_au_002", # English AU - Male
"en_uk_001", # English UK - Male 1 "en_uk_001", # English UK - Male 1
"en_uk_003", # English UK - Male 2 "en_uk_003", # English UK - Male 2
"en_us_001", # English US - Female (Int. 1) "en_us_001", # English US - Female (Int. 1)
"en_us_002", # English US - Female (Int. 2) "en_us_002", # English US - Female (Int. 2)
"en_us_006", # English US - Male 1 "en_us_006", # English US - Male 1
"en_us_007", # English US - Male 2 "en_us_007", # English US - Male 2
"en_us_009", # English US - Male 3 "en_us_009", # English US - Male 3
"en_us_010", "en_us_010",
] ]
voices = nonhuman + human voices = nonhuman + human
noneng = [ noneng = [
"fr_001", # French - Male 1 "fr_001", # French - Male 1
"fr_002", # French - Male 2 "fr_002", # French - Male 2
"de_001", # German - Female "de_001", # German - Female
"de_002", # German - Male "de_002", # German - Male
"es_002", # Spanish - Male "es_002", # Spanish - Male
# AMERICA VOICES # AMERICA VOICES
"es_mx_002", # Spanish MX - Male "es_mx_002", # Spanish MX - Male
"br_001", # Portuguese BR - Female 1 "br_001", # Portuguese BR - Female 1
"br_003", # Portuguese BR - Female 2 "br_003", # Portuguese BR - Female 2
"br_004", # Portuguese BR - Female 3 "br_004", # Portuguese BR - Female 3
"br_005", # Portuguese BR - Male "br_005", # Portuguese BR - Male
# ASIA VOICES # ASIA VOICES
"id_001", # Indonesian - Female "id_001", # Indonesian - Female
"jp_001", # Japanese - Female 1 "jp_001", # Japanese - Female 1
"jp_003", # Japanese - Female 2 "jp_003", # Japanese - Female 2
"jp_005", # Japanese - Female 3 "jp_005", # Japanese - Female 3
"jp_006", # Japanese - Male "jp_006", # Japanese - Male
"kr_002", # Korean - Male 1 "kr_002", # Korean - Male 1
"kr_003", # Korean - Female "kr_003", # Korean - Female
"kr_004", # Korean - Male 2 "kr_004", # Korean - Male 2
] ]
@ -66,74 +66,74 @@ noneng = [
class TikTok: # TikTok Text-to-Speech Wrapper class TikTok: # TikTok Text-to-Speech Wrapper
def __init__(self): def __init__(self):
self.URI_BASE = "https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker=" self.URI_BASE = "https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker="
def tts( def tts(
self, self,
req_text: str = "TikTok Text To Speech", req_text: str = "TikTok Text To Speech",
filename: str = "title.mp3", filename: str = "title.mp3",
random_speaker: bool = False, random_speaker: bool = False,
censor=False, censor=False,
): ):
req_text = req_text.replace("+", "plus").replace(" ", "+").replace("&", "and") req_text = req_text.replace("+", "plus").replace(" ", "+").replace("&", "and")
if censor: if censor:
# req_text = pf.censor(req_text) # req_text = pf.censor(req_text)
pass pass
voice = ( voice = (
self.randomvoice() if random_speaker else (os.getenv("VOICE") or random.choice(human)) self.randomvoice() if random_speaker else (os.getenv("VOICE") or random.choice(human))
) )
chunks = [m.group().strip() for m in re.finditer(r" *((.{0,299})(\.|.$))", req_text)] chunks = [m.group().strip() for m in re.finditer(r" *((.{0,299})(\.|.$))", req_text)]
audio_clips = [] audio_clips = []
cbn = sox.Combiner() cbn = sox.Combiner()
# cbn.set_input_format(file_type=["mp3" for _ in chunks]) # cbn.set_input_format(file_type=["mp3" for _ in chunks])
chunkId = 0 chunkId = 0
for chunk in chunks: for chunk in chunks:
try: try:
r = requests.post(f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0") r = requests.post(f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0")
except requests.exceptions.SSLError: except requests.exceptions.SSLError:
# https://stackoverflow.com/a/47475019/18516611 # https://stackoverflow.com/a/47475019/18516611
session = requests.Session() session = requests.Session()
retry = Retry(connect=3, backoff_factor=0.5) retry = Retry(connect=3, backoff_factor=0.5)
adapter = HTTPAdapter(max_retries=retry) adapter = HTTPAdapter(max_retries=retry)
session.mount("http://", adapter) session.mount("http://", adapter)
session.mount("https://", adapter) session.mount("https://", adapter)
r = session.post(f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0") r = session.post(f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0")
print(r.text) print(r.text)
vstr = [r.json()["data"]["v_str"]][0] vstr = [r.json()["data"]["v_str"]][0]
b64d = base64.b64decode(vstr) b64d = base64.b64decode(vstr)
with open(filename.replace(".mp3", f"-{chunkId}.mp3"), "wb") as out: with open(filename.replace(".mp3", f"-{chunkId}.mp3"), "wb") as out:
out.write(b64d) out.write(b64d)
audio_clips.append(filename.replace(".mp3", f"-{chunkId}.mp3")) audio_clips.append(filename.replace(".mp3", f"-{chunkId}.mp3"))
chunkId = chunkId + 1 chunkId = chunkId + 1
try: try:
if len(audio_clips) > 1: if len(audio_clips) > 1:
cbn.convert(samplerate=44100, n_channels=2) cbn.convert(samplerate=44100, n_channels=2)
cbn.build(audio_clips, filename, "concatenate") cbn.build(audio_clips, filename, "concatenate")
else: else:
os.rename(audio_clips[0], filename) os.rename(audio_clips[0], filename)
except ( except (
sox.core.SoxError, sox.core.SoxError,
FileNotFoundError, FileNotFoundError,
): # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339 ): # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339
for clip in audio_clips: for clip in audio_clips:
i = audio_clips.index(clip) # get the index of the clip i = audio_clips.index(clip) # get the index of the clip
audio_clips = ( audio_clips = (
audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1 :] audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1 :]
) # replace the clip with an AudioFileClip ) # replace the clip with an AudioFileClip
audio_concat = concatenate_audioclips(audio_clips) audio_concat = concatenate_audioclips(audio_clips)
audio_composite = CompositeAudioClip([audio_concat]) audio_composite = CompositeAudioClip([audio_concat])
audio_composite.write_audiofile(filename, 44100, 2, 2000, None) audio_composite.write_audiofile(filename, 44100, 2, 2000, None)
@staticmethod @staticmethod
def randomvoice(): def randomvoice():
ok_or_good = random.randrange(1, 10) ok_or_good = random.randrange(1, 10)
if ok_or_good == 1: # 1/10 chance of ok voice if ok_or_good == 1: # 1/10 chance of ok voice
return random.choice(voices) return random.choice(voices)
return random.choice(human) # 9/10 chance of good voice return random.choice(human) # 9/10 chance of good voice

@ -10,12 +10,12 @@ CHOICE_DIR = {"tiktok": TikTok, "gtts": GTTS, 'polly': POLLY}
class TTS: class TTS:
def __new__(cls): def __new__(cls):
load_dotenv() load_dotenv()
CHOICE = getenv("TTsChoice").casefold() CHOICE = getenv("TTsChoice").casefold()
valid_keys = [key.lower() for key in CHOICE_DIR.keys()] valid_keys = [key.lower() for key in CHOICE_DIR.keys()]
if CHOICE not in valid_keys: if CHOICE not in valid_keys:
raise ValueError( raise ValueError(
f"{CHOICE} is not valid. Please use one of these {valid_keys} options" f"{CHOICE} is not valid. Please use one of these {valid_keys} options"
) )
return CHOICE_DIR.get(CHOICE)() return CHOICE_DIR.get(CHOICE)()

@ -11,85 +11,85 @@ TEXT_WHITELIST = set("abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234
def textify(text): def textify(text):
return "".join(filter(TEXT_WHITELIST.__contains__, text)) return "".join(filter(TEXT_WHITELIST.__contains__, text))
def get_subreddit_threads(): def get_subreddit_threads():
""" """
Returns a list of threads from the AskReddit subreddit. Returns a list of threads from the AskReddit subreddit.
""" """
global submission global submission
print_substep("Logging into Reddit.") print_substep("Logging into Reddit.")
content = {} content = {}
if str(getenv("REDDIT_2FA")).casefold() == "yes": if str(getenv("REDDIT_2FA")).casefold() == "yes":
print("\nEnter your two-factor authentication code from your authenticator app.\n") print("\nEnter your two-factor authentication code from your authenticator app.\n")
code = input("> ") code = input("> ")
print() print()
pw = getenv("REDDIT_PASSWORD") pw = getenv("REDDIT_PASSWORD")
passkey = f"{pw}:{code}" passkey = f"{pw}:{code}"
else: else:
passkey = getenv("REDDIT_PASSWORD") passkey = getenv("REDDIT_PASSWORD")
reddit = praw.Reddit( reddit = praw.Reddit(
client_id=getenv("REDDIT_CLIENT_ID"), client_id=getenv("REDDIT_CLIENT_ID"),
client_secret=getenv("REDDIT_CLIENT_SECRET"), client_secret=getenv("REDDIT_CLIENT_SECRET"),
user_agent="Accessing Reddit threads", user_agent="Accessing Reddit threads",
username=getenv("REDDIT_USERNAME"), username=getenv("REDDIT_USERNAME"),
passkey=passkey, passkey=passkey,
check_for_async=False, check_for_async=False,
) )
""" """
Ask user for subreddit input Ask user for subreddit input
""" """
print_step("Getting subreddit threads...") print_step("Getting subreddit threads...")
if not getenv( if not getenv(
"SUBREDDIT" "SUBREDDIT"
): # note to self. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython") ): # note to self. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython")
subreddit = reddit.subreddit( subreddit = reddit.subreddit(
input("What subreddit would you like to pull from? ") input("What subreddit would you like to pull from? ")
) # if the env isnt set, ask user ) # if the env isnt set, ask user
else: else:
print_substep(f"Using subreddit: r/{getenv('SUBREDDIT')} from environment variable config") print_substep(f"Using subreddit: r/{getenv('SUBREDDIT')} from environment variable config")
subreddit = reddit.subreddit( subreddit = reddit.subreddit(
getenv("SUBREDDIT") getenv("SUBREDDIT")
) # Allows you to specify in .env. Done for automation purposes. ) # Allows you to specify in .env. Done for automation purposes.
if getenv("POST_ID"): if getenv("POST_ID"):
submission = reddit.submission(id=getenv("POST_ID")) submission = reddit.submission(id=getenv("POST_ID"))
else: else:
threads = subreddit.hot(limit=25) threads = subreddit.hot(limit=25)
submission = get_subreddit_undone(threads, subreddit) submission = get_subreddit_undone(threads, subreddit)
submission = check_done(submission) # double checking submission = check_done(submission) # double checking
if submission is None: if submission is None:
return get_subreddit_threads() # submission already done. rerun return get_subreddit_threads() # submission already done. rerun
upvotes = submission.score upvotes = submission.score
ratio = submission.upvote_ratio * 100 ratio = submission.upvote_ratio * 100
num_comments = submission.num_comments num_comments = submission.num_comments
print_substep(f"Video will be: {submission.title} :thumbsup:", style="bold green") print_substep(f"Video will be: {submission.title} :thumbsup:", style="bold green")
print_substep(f"Thread has {upvotes} upvotes", style="bold blue") print_substep(f"Thread has {upvotes} upvotes", style="bold blue")
print_substep(f"Thread has a upvote ratio of {ratio}%", style="bold blue") print_substep(f"Thread has a upvote ratio of {ratio}%", style="bold blue")
print_substep(f"Thread has {num_comments} comments", style="bold blue") print_substep(f"Thread has {num_comments} comments", style="bold blue")
environ["VIDEO_TITLE"] = str(textify(submission.title)) # todo use global instend of env vars environ["VIDEO_TITLE"] = str(textify(submission.title)) # todo use global instend of env vars
environ["VIDEO_ID"] = str(textify(submission.id)) environ["VIDEO_ID"] = str(textify(submission.id))
content["thread_url"] = f"https://reddit.com{submission.permalink}" content["thread_url"] = f"https://reddit.com{submission.permalink}"
content["thread_title"] = submission.title content["thread_title"] = submission.title
# content["thread_content"] = submission.content # content["thread_content"] = submission.content
content["comments"] = [] content["comments"] = []
for top_level_comment in submission.comments: for top_level_comment in submission.comments:
if isinstance(top_level_comment, MoreComments): if isinstance(top_level_comment, MoreComments):
continue continue
if top_level_comment.body in ["[removed]", "[deleted]"]: if top_level_comment.body in ["[removed]", "[deleted]"]:
continue # # see https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/78 continue # # see https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/78
if not top_level_comment.stickied: if not top_level_comment.stickied:
if len(top_level_comment.body) <= int(environ["MAX_COMMENT_LENGTH"]): if len(top_level_comment.body) <= int(environ["MAX_COMMENT_LENGTH"]):
content["comments"].append( content["comments"].append(
{ {
"comment_body": top_level_comment.body, "comment_body": top_level_comment.body,
"comment_url": top_level_comment.permalink, "comment_url": top_level_comment.permalink,
"comment_id": top_level_comment.id, "comment_id": top_level_comment.id,
} }
) )
print_substep("Received subreddit threads Successfully.", style="bold green") print_substep("Received subreddit threads Successfully.", style="bold green")
return content return content

@ -15,52 +15,52 @@ console = Console()
def handle_input( def handle_input(
message: str = "", message: str = "",
check_type=False, check_type=False,
match: str = "", match: str = "",
err_message: str = "", err_message: str = "",
nmin=None, nmin=None,
nmax=None, nmax=None,
oob_error="", oob_error="",
): ):
match = re.compile(match + "$") match = re.compile(match + "$")
while True: while True:
user_input = input(message + "\n> ").strip() user_input = input(message + "\n> ").strip()
if re.match(match, user_input) is not None: if re.match(match, user_input) is not None:
if check_type is not False: if check_type is not False:
try: try:
user_input = check_type(user_input) user_input = check_type(user_input)
if nmin is not None and user_input < nmin: if nmin is not None and user_input < nmin:
console.log("[red]" + oob_error) # Input too low failstate console.log("[red]" + oob_error) # Input too low failstate
continue continue
if nmax is not None and user_input > nmax: if nmax is not None and user_input > nmax:
console.log("[red]" + oob_error) # Input too high console.log("[red]" + oob_error) # Input too high
continue continue
break # Successful type conversion and number in bounds break # Successful type conversion and number in bounds
except ValueError: except ValueError:
console.log("[red]" + err_message) # Type conversion failed console.log("[red]" + err_message) # Type conversion failed
continue continue
if ( if (
nmin is not None and len(user_input) < nmin nmin is not None and len(user_input) < nmin
): # Check if string is long enough ): # Check if string is long enough
console.log("[red]" + oob_error) console.log("[red]" + oob_error)
continue continue
if ( if (
nmax is not None and len(user_input) > nmax nmax is not None and len(user_input) > nmax
): # Check if string is not too long ): # Check if string is not too long
console.log("[red]" + oob_error) console.log("[red]" + oob_error)
continue continue
break break
console.log("[red]" + err_message) console.log("[red]" + err_message)
return user_input return user_input
if os.path.isfile(".setup-done-before"): if os.path.isfile(".setup-done-before"):
console.log( console.log(
"[red]Setup was already completed! Please make sure you have to run this script again. If that is such, delete the file .setup-done-before" "[red]Setup was already completed! Please make sure you have to run this script again. If that is such, delete the file .setup-done-before"
) )
exit() exit()
# These lines ensure the user: # These lines ensure the user:
# - knows they are in setup mode # - knows they are in setup mode
@ -68,26 +68,26 @@ if os.path.isfile(".setup-done-before"):
print_step("Setup Assistant") print_step("Setup Assistant")
print_markdown( print_markdown(
"### You're in the setup wizard. Ensure you're supposed to be here, then type yes to continue. If you're not sure, type no to quit." "### You're in the setup wizard. Ensure you're supposed to be here, then type yes to continue. If you're not sure, type no to quit."
) )
# This Input is used to ensure the user is sure they want to continue. # This Input is used to ensure the user is sure they want to continue.
if input("Are you sure you want to continue? > ").strip().casefold() != "yes": if input("Are you sure you want to continue? > ").strip().casefold() != "yes":
console.print("[red]Exiting...") console.print("[red]Exiting...")
exit() exit()
# This code is inaccessible if the prior check fails, and thus an else statement is unnecessary # This code is inaccessible if the prior check fails, and thus an else statement is unnecessary
# Again, let them know they are about to erase all other setup data. # Again, let them know they are about to erase all other setup data.
console.print( console.print(
"[bold red] This will overwrite your current settings. Are you sure you want to continue? [bold green]yes/no" "[bold red] This will overwrite your current settings. Are you sure you want to continue? [bold green]yes/no"
) )
if input("Are you sure you want to continue? > ").strip().casefold() != "yes": if input("Are you sure you want to continue? > ").strip().casefold() != "yes":
console.print("[red]Abort mission! Exiting...") console.print("[red]Abort mission! Exiting...")
exit() exit()
# This is once again inaccessible if the prior checks fail # This is once again inaccessible if the prior checks fail
# Once they confirm, move on with the script. # Once they confirm, move on with the script.
console.print("[bold green]Alright! Let's get started!") console.print("[bold green]Alright! Let's get started!")
@ -103,18 +103,18 @@ console.log("[bold green]Opacity (range of 0-1, decimals are OK)")
console.log("[bold green]Subreddit (without r/ or /r/)") console.log("[bold green]Subreddit (without r/ or /r/)")
console.log("[bold green]Theme (light or dark)") console.log("[bold green]Theme (light or dark)")
console.print( console.print(
"[green]If you don't have these, please follow the instructions in the README.md file to set them up." "[green]If you don't have these, please follow the instructions in the README.md file to set them up."
) )
console.print( console.print(
"[green]If you do have these, type yes to continue. If you dont, go ahead and grab those quickly and come back." "[green]If you do have these, type yes to continue. If you dont, go ahead and grab those quickly and come back."
) )
print() print()
if input("Are you sure you have the credentials? > ").strip().casefold() != "yes": if input("Are you sure you have the credentials? > ").strip().casefold() != "yes":
console.print("[red]I don't understand that.") console.print("[red]I don't understand that.")
console.print("[red]Exiting...") console.print("[red]Exiting...")
exit() exit()
console.print("[bold green]Alright! Let's get started!") console.print("[bold green]Alright! Let's get started!")
@ -123,69 +123,69 @@ console.print("[bold green]Alright! Let's get started!")
console.log("Enter your credentials now.") console.log("Enter your credentials now.")
client_id = handle_input( client_id = handle_input(
"Client ID > ", "Client ID > ",
False, False,
"[-a-zA-Z0-9._~+/]+=*", "[-a-zA-Z0-9._~+/]+=*",
"That is somehow not a correct ID, try again.", "That is somehow not a correct ID, try again.",
12, 12,
30, 30,
"The ID should be over 12 and under 30 characters, double check your input.", "The ID should be over 12 and under 30 characters, double check your input.",
) )
client_sec = handle_input( client_sec = handle_input(
"Client Secret > ", "Client Secret > ",
False, False,
"[-a-zA-Z0-9._~+/]+=*", "[-a-zA-Z0-9._~+/]+=*",
"That is somehow not a correct secret, try again.", "That is somehow not a correct secret, try again.",
20, 20,
40, 40,
"The secret should be over 20 and under 40 characters, double check your input.", "The secret should be over 20 and under 40 characters, double check your input.",
) )
user = handle_input( user = handle_input(
"Username > ", "Username > ",
False, False,
r"[_0-9a-zA-Z]+", r"[_0-9a-zA-Z]+",
"That is not a valid user", "That is not a valid user",
3, 3,
20, 20,
"A username HAS to be between 3 and 20 characters", "A username HAS to be between 3 and 20 characters",
) )
passw = handle_input("Password > ", False, ".*", "", 8, None, "Password too short") passw = handle_input("Password > ", False, ".*", "", 8, None, "Password too short")
twofactor = handle_input( twofactor = handle_input(
"2fa Enabled? (yes/no) > ", "2fa Enabled? (yes/no) > ",
False, False,
r"(yes)|(no)", r"(yes)|(no)",
"You need to input either yes or no", "You need to input either yes or no",
) )
opacity = handle_input( opacity = handle_input(
"Opacity? (range of 0-1) > ", "Opacity? (range of 0-1) > ",
float, float,
".*", ".*",
"You need to input a number between 0 and 1", "You need to input a number between 0 and 1",
0, 0,
1, 1,
"Your number is not between 0 and 1", "Your number is not between 0 and 1",
) )
subreddit = handle_input( subreddit = handle_input(
"Subreddit (without r/) > ", "Subreddit (without r/) > ",
False, False,
r"[_0-9a-zA-Z]+", r"[_0-9a-zA-Z]+",
"This subreddit cannot exist, make sure you typed it in correctly and removed the r/ (or /r/).", "This subreddit cannot exist, make sure you typed it in correctly and removed the r/ (or /r/).",
3, 3,
20, 20,
"A subreddit name HAS to be between 3 and 20 characters", "A subreddit name HAS to be between 3 and 20 characters",
) )
theme = handle_input( theme = handle_input(
"Theme? (light or dark) > ", "Theme? (light or dark) > ",
False, False,
r"(light)|(dark)", r"(light)|(dark)",
"You need to input 'light' or 'dark'", "You need to input 'light' or 'dark'",
) )
loader = Loader("Attempting to save your credentials...", "Done!").start() loader = Loader("Attempting to save your credentials...", "Done!").start()
# you can also put a while loop here, e.g. while VideoIsBeingMade == True: ... # you can also put a while loop here, e.g. while VideoIsBeingMade == True: ...
console.log("Writing to the .env file...") console.log("Writing to the .env file...")
with open(".env", "w") as f: with open(".env", "w") as f:
f.write( f.write(
f"""REDDIT_CLIENT_ID="{client_id}" f"""REDDIT_CLIENT_ID="{client_id}"
REDDIT_CLIENT_SECRET="{client_sec}" REDDIT_CLIENT_SECRET="{client_sec}"
REDDIT_USERNAME="{user}" REDDIT_USERNAME="{user}"
REDDIT_PASSWORD="{passw}" REDDIT_PASSWORD="{passw}"
@ -194,12 +194,12 @@ THEME="{theme}"
SUBREDDIT="{subreddit}" SUBREDDIT="{subreddit}"
OPACITY={opacity} OPACITY={opacity}
""" """
) )
with open(".setup-done-before", "w") as f: with open(".setup-done-before", "w") as f:
f.write( f.write(
"This file blocks the setup assistant from running again. Delete this file to run setup again." "This file blocks the setup assistant from running again. Delete this file to run setup again."
) )
loader.stop() loader.stop()

@ -3,22 +3,22 @@ from os.path import exists
def cleanup() -> int: def cleanup() -> int:
if exists("./assets/temp"): if exists("./assets/temp"):
count = 0 count = 0
files = [ files = [
f for f in os.listdir(".") if f.endswith(".mp4") and "temp" in f.lower() f for f in os.listdir(".") if f.endswith(".mp4") and "temp" in f.lower()
] ]
count += len(files) count += len(files)
for f in files: for f in files:
os.remove(f) os.remove(f)
try: try:
for file in os.listdir("./assets/temp/mp4"): for file in os.listdir("./assets/temp/mp4"):
count += 1 count += 1
os.remove("./assets/temp/mp4/" + file) os.remove("./assets/temp/mp4/" + file)
except FileNotFoundError: except FileNotFoundError:
pass pass
for file in os.listdir("./assets/temp/mp3"): for file in os.listdir("./assets/temp/mp3"):
count += 1 count += 1
os.remove("./assets/temp/mp3/" + file) os.remove("./assets/temp/mp3/" + file)
return count return count
return 0 return 0

@ -3,36 +3,36 @@
from dotenv import dotenv_values from dotenv import dotenv_values
DEFAULTS = { DEFAULTS = {
"SUBREDDIT": "AskReddit", "SUBREDDIT": "AskReddit",
"ALLOW_NSFW": "False", "ALLOW_NSFW": "False",
"POST_ID": "", "POST_ID": "",
"THEME": "DARK", "THEME": "DARK",
"REDDIT_2FA": "no", "REDDIT_2FA": "no",
"TIMES_TO_RUN": "", "TIMES_TO_RUN": "",
"MAX_COMMENT_LENGTH": "500", "MAX_COMMENT_LENGTH": "500",
"OPACITY": "1", "OPACITY": "1",
"VOICE": "en_us_001", "VOICE": "en_us_001",
"STORYMODE": "False", "STORYMODE": "False",
} }
class Config: class Config:
def __init__(self): def __init__(self):
self.raw = dotenv_values("../.env") self.raw = dotenv_values("../.env")
self.load_attrs() self.load_attrs()
def __getattr__(self, attr): # code completion for attributes fix. def __getattr__(self, attr): # code completion for attributes fix.
return getattr(self, attr) return getattr(self, attr)
def load_attrs(self): def load_attrs(self):
for key, value in self.raw.items(): for key, value in self.raw.items():
self.add_attr(key, value) self.add_attr(key, value)
def add_attr(self, key, value): def add_attr(self, key, value):
if value is None or value == "": if value is None or value == "":
setattr(self, key, DEFAULTS[key]) setattr(self, key, DEFAULTS[key])
else: else:
setattr(self, key, str(value)) setattr(self, key, str(value))
config = Config() config = Config()

@ -9,19 +9,19 @@ console = Console()
def print_markdown(text): def print_markdown(text):
"""Prints a rich info message. Support Markdown syntax.""" """Prints a rich info message. Support Markdown syntax."""
md = Padding(Markdown(text), 2) md = Padding(Markdown(text), 2)
console.print(md) console.print(md)
def print_step(text): def print_step(text):
"""Prints a rich info message.""" """Prints a rich info message."""
panel = Panel(Text(text, justify="left")) panel = Panel(Text(text, justify="left"))
console.print(panel) console.print(panel)
def print_substep(text, style=""): def print_substep(text, style=""):
"""Prints a rich info message without the panelling.""" """Prints a rich info message without the panelling."""
console.print(text, style=style) console.print(text, style=style)

@ -9,43 +9,43 @@ from time import sleep
class Loader: class Loader:
def __init__(self, desc="Loading...", end="Done!", timeout=0.1): def __init__(self, desc="Loading...", end="Done!", timeout=0.1):
""" """
A loader-like context manager A loader-like context manager
Args: Args:
desc (str, optional): The loader's description. Defaults to "Loading...". desc (str, optional): The loader's description. Defaults to "Loading...".
end (str, optional): Final print. Defaults to "Done!". end (str, optional): Final print. Defaults to "Done!".
timeout (float, optional): Sleep time between prints. Defaults to 0.1. timeout (float, optional): Sleep time between prints. Defaults to 0.1.
""" """
self.desc = desc self.desc = desc
self.end = end self.end = end
self.timeout = timeout self.timeout = timeout
self._thread = Thread(target=self._animate, daemon=True) self._thread = Thread(target=self._animate, daemon=True)
self.steps = ["", "", "", "", "", "", "", ""] self.steps = ["", "", "", "", "", "", "", ""]
self.done = False self.done = False
def start(self): def start(self):
self._thread.start() self._thread.start()
return self return self
def _animate(self): def _animate(self):
for c in cycle(self.steps): for c in cycle(self.steps):
if self.done: if self.done:
break break
print(f"\r{self.desc} {c}", flush=True, end="") print(f"\r{self.desc} {c}", flush=True, end="")
sleep(self.timeout) sleep(self.timeout)
def __enter__(self): def __enter__(self):
self.start() self.start()
def stop(self): def stop(self):
self.done = True self.done = True
cols = get_terminal_size((80, 20)).columns cols = get_terminal_size((80, 20)).columns
print("\r" + " " * cols, end="", flush=True) print("\r" + " " * cols, end="", flush=True)
print(f"\r{self.end}", flush=True) print(f"\r{self.end}", flush=True)
def __exit__(self, exc_type, exc_value, tb): def __exit__(self, exc_type, exc_value, tb):
# handle exceptions with those variables ^ # handle exceptions with those variables ^
self.stop() self.stop()

@ -5,28 +5,28 @@ from utils.console import print_substep
def get_subreddit_undone(submissions: List, subreddit): def get_subreddit_undone(submissions: List, subreddit):
""" """
recursively checks if the top submission in the list was already done. recursively checks if the top submission in the list was already done.
""" """
with open("./video_creation/data/videos.json", "r") as done_vids_raw: with open("./video_creation/data/videos.json", "r") as done_vids_raw:
done_videos = json.load(done_vids_raw) done_videos = json.load(done_vids_raw)
for submission in submissions: for submission in submissions:
if already_done(done_videos, submission): if already_done(done_videos, submission):
continue continue
if submission.over_18: if submission.over_18:
if getenv("ALLOW_NSFW").casefold() == "false": if getenv("ALLOW_NSFW").casefold() == "false":
print_substep("NSFW Post Detected. Skipping...") print_substep("NSFW Post Detected. Skipping...")
continue continue
return submission return submission
print('all submissions have been done going by top submission order') print('all submissions have been done going by top submission order')
return get_subreddit_undone( return get_subreddit_undone(
subreddit.top(time_filter="hour"), subreddit subreddit.top(time_filter="hour"), subreddit
) # all of the videos in hot have already been done ) # all of the videos in hot have already been done
def already_done(done_videos: list, submission): def already_done(done_videos: list, submission):
for video in done_videos: for video in done_videos:
if video["id"] == str(submission): if video["id"] == str(submission):
return True return True
return False return False

@ -5,19 +5,19 @@ from utils.console import print_step
def check_done( def check_done(
redditobj, redditobj,
): # don't set this to be run anyplace that isn't subreddit.py bc of inspect stack ): # don't set this to be run anyplace that isn't subreddit.py bc of inspect stack
"""params: """params:
reddit_object: The Reddit Object you received in askreddit.py""" reddit_object: The Reddit Object you received in askreddit.py"""
with open("./video_creation/data/videos.json", "r") as done_vids_raw: with open("./video_creation/data/videos.json", "r") as done_vids_raw:
done_videos = json.load(done_vids_raw) done_videos = json.load(done_vids_raw)
for video in done_videos: for video in done_videos:
if video["id"] == str(redditobj): if video["id"] == str(redditobj):
if getenv("POST_ID"): if getenv("POST_ID"):
print_step( print_step(
"You already have done this video but since it was declared specifically in the .env file the program will continue" "You already have done this video but since it was declared specifically in the .env file the program will continue"
) )
return redditobj return redditobj
print_step("Getting new post as the current one has already been done") print_step("Getting new post as the current one has already been done")
return None return None
return redditobj return redditobj

@ -2,21 +2,21 @@ import re
def sanitize_text(text): def sanitize_text(text):
""" """
Sanitizes the text for tts. Sanitizes the text for tts.
What gets removed: What gets removed:
- following characters`^_~@!&;#:-%“”‘"%*/{}[]()\|<>?=+` - following characters`^_~@!&;#:-%“”‘"%*/{}[]()\|<>?=+`
- any http or https links - any http or https links
""" """
# remove any urls from the text # remove any urls from the text
regex_urls = r"((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*" regex_urls = r"((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*"
result = re.sub(regex_urls, " ", text) result = re.sub(regex_urls, " ", text)
# note: not removing apostrophes # note: not removing apostrophes
regex_expr = r"\s['|]|['|]\s|[\^_~@!&;#:\-%“”‘\"%\*/{}\[\]\(\)\\|<>=+]" regex_expr = r"\s['|]|['|]\s|[\^_~@!&;#:\-%“”‘\"%\*/{}\[\]\(\)\\|<>=+]"
result = re.sub(regex_expr, " ", result) result = re.sub(regex_expr, " ", result)
# remove extra whitespace # remove extra whitespace
return " ".join(result.split()) return " ".join(result.split())

@ -9,55 +9,55 @@ from utils.console import print_step, print_substep
def get_start_and_end_times(video_length, length_of_clip): def get_start_and_end_times(video_length, length_of_clip):
random_time = randrange(180, int(length_of_clip) - int(video_length)) random_time = randrange(180, int(length_of_clip) - int(video_length))
return random_time, random_time + video_length return random_time, random_time + video_length
def download_background(): def download_background():
"""Downloads the backgrounds/s video from YouTube.""" """Downloads the backgrounds/s video from YouTube."""
Path("./assets/backgrounds/").mkdir(parents=True, exist_ok=True) Path("./assets/backgrounds/").mkdir(parents=True, exist_ok=True)
background_options = [ # uri , filename , credit background_options = [ # uri , filename , credit
("https://www.youtube.com/watch?v=n_Dv4JMiwK8", "parkour.mp4", "bbswitzer"), ("https://www.youtube.com/watch?v=n_Dv4JMiwK8", "parkour.mp4", "bbswitzer"),
# ( # (
# "https://www.youtube.com/watch?v=2X9QGY__0II", # "https://www.youtube.com/watch?v=2X9QGY__0II",
# "rocket_league.mp4", # "rocket_league.mp4",
# "Orbital Gameplay", # "Orbital Gameplay",
# ), # ),
] ]
# note: make sure the file name doesn't include an - in it # note: make sure the file name doesn't include an - in it
if not len(listdir("./assets/backgrounds")) >= len( if not len(listdir("./assets/backgrounds")) >= len(
background_options background_options
): # if there are any background videos not installed ): # if there are any background videos not installed
print_step( print_step(
"We need to download the backgrounds videos. they are fairly large but it's only done once. 😎" "We need to download the backgrounds videos. they are fairly large but it's only done once. 😎"
) )
print_substep("Downloading the backgrounds videos... please be patient 🙏 ") print_substep("Downloading the backgrounds videos... please be patient 🙏 ")
for uri, filename, credit in background_options: for uri, filename, credit in background_options:
if Path(f"assets/backgrounds/{credit}-{filename}").is_file(): if Path(f"assets/backgrounds/{credit}-{filename}").is_file():
continue # adds check to see if file exists before downloading continue # adds check to see if file exists before downloading
print_substep(f"Downloading {filename} from {uri}") print_substep(f"Downloading {filename} from {uri}")
YouTube(uri).streams.filter(res="1080p").first().download( YouTube(uri).streams.filter(res="1080p").first().download(
"assets/backgrounds", filename=f"{credit}-{filename}" "assets/backgrounds", filename=f"{credit}-{filename}"
) )
print_substep( print_substep(
"Background videos downloaded successfully! 🎉", style="bold green" "Background videos downloaded successfully! 🎉", style="bold green"
) )
def chop_background_video(video_length): def chop_background_video(video_length):
print_step("Finding a spot in the backgrounds video to chop...✂️") print_step("Finding a spot in the backgrounds video to chop...✂️")
choice = random.choice(listdir("assets/backgrounds")) choice = random.choice(listdir("assets/backgrounds"))
environ["background_credit"] = choice.split("-")[0] environ["background_credit"] = choice.split("-")[0]
background = VideoFileClip(f"assets/backgrounds/{choice}") background = VideoFileClip(f"assets/backgrounds/{choice}")
start_time, end_time = get_start_and_end_times(video_length, background.duration) start_time, end_time = get_start_and_end_times(video_length, background.duration)
ffmpeg_extract_subclip( ffmpeg_extract_subclip(
f"assets/backgrounds/{choice}", f"assets/backgrounds/{choice}",
start_time, start_time,
end_time, end_time,
targetname="assets/temp/background.mp4", targetname="assets/temp/background.mp4",
) )
print_substep("Background video chopped successfully!", style="bold green") print_substep("Background video chopped successfully!", style="bold green")
return True return True

@ -1,8 +1,8 @@
[ [
{ {
"name": "USER", "name": "USER",
"value": "eyJwcmVmcyI6eyJ0b3BDb250ZW50RGlzbWlzc2FsVGltZSI6MCwiZ2xvYmFsVGhlbWUiOiJSRURESVQiLCJuaWdodG1vZGUiOnRydWUsImNvbGxhcHNlZFRyYXlTZWN0aW9ucyI6eyJmYXZvcml0ZXMiOmZhbHNlLCJtdWx0aXMiOmZhbHNlLCJtb2RlcmF0aW5nIjpmYWxzZSwic3Vic2NyaXB0aW9ucyI6ZmFsc2UsInByb2ZpbGVzIjpmYWxzZX0sInRvcENvbnRlbnRUaW1lc0Rpc21pc3NlZCI6MH19", "value": "eyJwcmVmcyI6eyJ0b3BDb250ZW50RGlzbWlzc2FsVGltZSI6MCwiZ2xvYmFsVGhlbWUiOiJSRURESVQiLCJuaWdodG1vZGUiOnRydWUsImNvbGxhcHNlZFRyYXlTZWN0aW9ucyI6eyJmYXZvcml0ZXMiOmZhbHNlLCJtdWx0aXMiOmZhbHNlLCJtb2RlcmF0aW5nIjpmYWxzZSwic3Vic2NyaXB0aW9ucyI6ZmFsc2UsInByb2ZpbGVzIjpmYWxzZX0sInRvcENvbnRlbnRUaW1lc0Rpc21pc3NlZCI6MH19",
"domain": ".reddit.com", "domain": ".reddit.com",
"path": "/" "path": "/"
} }
] ]

@ -1,14 +1,14 @@
[ [
{ {
"name": "USER", "name": "USER",
"value": "eyJwcmVmcyI6eyJ0b3BDb250ZW50RGlzbWlzc2FsVGltZSI6MCwiZ2xvYmFsVGhlbWUiOiJSRURESVQiLCJuaWdodG1vZGUiOnRydWUsImNvbGxhcHNlZFRyYXlTZWN0aW9ucyI6eyJmYXZvcml0ZXMiOmZhbHNlLCJtdWx0aXMiOmZhbHNlLCJtb2RlcmF0aW5nIjpmYWxzZSwic3Vic2NyaXB0aW9ucyI6ZmFsc2UsInByb2ZpbGVzIjpmYWxzZX0sInRvcENvbnRlbnRUaW1lc0Rpc21pc3NlZCI6MH19", "value": "eyJwcmVmcyI6eyJ0b3BDb250ZW50RGlzbWlzc2FsVGltZSI6MCwiZ2xvYmFsVGhlbWUiOiJSRURESVQiLCJuaWdodG1vZGUiOnRydWUsImNvbGxhcHNlZFRyYXlTZWN0aW9ucyI6eyJmYXZvcml0ZXMiOmZhbHNlLCJtdWx0aXMiOmZhbHNlLCJtb2RlcmF0aW5nIjpmYWxzZSwic3Vic2NyaXB0aW9ucyI6ZmFsc2UsInByb2ZpbGVzIjpmYWxzZX0sInRvcENvbnRlbnRUaW1lc0Rpc21pc3NlZCI6MH19",
"domain": ".reddit.com", "domain": ".reddit.com",
"path": "/" "path": "/"
}, },
{ {
"name": "eu_cookie", "name": "eu_cookie",
"value": "{%22opted%22:true%2C%22nonessential%22:false}", "value": "{%22opted%22:true%2C%22nonessential%22:false}",
"domain": ".reddit.com", "domain": ".reddit.com",
"path": "/" "path": "/"
} }
] ]

@ -1,8 +1,8 @@
[ [
{ {
"name": "eu_cookie", "name": "eu_cookie",
"value": "{%22opted%22:true%2C%22nonessential%22:false}", "value": "{%22opted%22:true%2C%22nonessential%22:false}",
"domain": ".reddit.com", "domain": ".reddit.com",
"path": "/" "path": "/"
} }
] ]

@ -5,13 +5,13 @@ import time
from os.path import exists from os.path import exists
from moviepy.editor import ( from moviepy.editor import (
VideoFileClip, VideoFileClip,
AudioFileClip, AudioFileClip,
ImageClip, ImageClip,
concatenate_videoclips, concatenate_videoclips,
concatenate_audioclips, concatenate_audioclips,
CompositeAudioClip, CompositeAudioClip,
CompositeVideoClip, CompositeVideoClip,
) )
from moviepy.video.io import ffmpeg_tools from moviepy.video.io import ffmpeg_tools
from rich.console import Console from rich.console import Console
@ -26,125 +26,125 @@ W, H = 1080, 1920
def make_final_video(number_of_clips, length): def make_final_video(number_of_clips, length):
print_step("Creating the final video 🎥") print_step("Creating the final video 🎥")
VideoFileClip.reW = lambda clip: clip.resize(width=W) VideoFileClip.reW = lambda clip: clip.resize(width=W)
VideoFileClip.reH = lambda clip: clip.resize(width=H) VideoFileClip.reH = lambda clip: clip.resize(width=H)
opacity = os.getenv("OPACITY") opacity = os.getenv("OPACITY")
background_clip = ( background_clip = (
VideoFileClip("assets/temp/background.mp4") VideoFileClip("assets/temp/background.mp4")
.without_audio() .without_audio()
.resize(height=H) .resize(height=H)
.crop(x1=1166.6, y1=0, x2=2246.6, y2=1920) .crop(x1=1166.6, y1=0, x2=2246.6, y2=1920)
) )
# Gather all audio clips # Gather all audio clips
audio_clips = [] audio_clips = []
for i in range(0, number_of_clips): for i in range(0, number_of_clips):
audio_clips.append(AudioFileClip(f"assets/temp/mp3/{i}.mp3")) audio_clips.append(AudioFileClip(f"assets/temp/mp3/{i}.mp3"))
audio_clips.insert(0, AudioFileClip("assets/temp/mp3/title.mp3")) audio_clips.insert(0, AudioFileClip("assets/temp/mp3/title.mp3"))
audio_concat = concatenate_audioclips(audio_clips) audio_concat = concatenate_audioclips(audio_clips)
audio_composite = CompositeAudioClip([audio_concat]) audio_composite = CompositeAudioClip([audio_concat])
# Get sum of all clip lengths # Get sum of all clip lengths
total_length = sum([clip.duration for clip in audio_clips]) total_length = sum([clip.duration for clip in audio_clips])
# round total_length to an integer # round total_length to an integer
int_total_length = round(total_length) int_total_length = round(total_length)
# Output Length # Output Length
console.log(f"[bold green] Video Will Be: {int_total_length} Seconds Long") console.log(f"[bold green] Video Will Be: {int_total_length} Seconds Long")
# add title to video # add title to video
image_clips = [] image_clips = []
# Gather all images # Gather all images
if opacity is None or float(opacity) >= 1: # opacity not set or is set to one OR MORE if opacity is None or float(opacity) >= 1: # opacity not set or is set to one OR MORE
image_clips.insert( image_clips.insert(
0, 0,
ImageClip("assets/temp/png/title.png") ImageClip("assets/temp/png/title.png")
.set_duration(audio_clips[0].duration) .set_duration(audio_clips[0].duration)
.set_position("center") .set_position("center")
.resize(width=W - 100) .resize(width=W - 100)
.set_opacity(float(opacity)), .set_opacity(float(opacity)),
) )
else: else:
image_clips.insert( image_clips.insert(
0, 0,
ImageClip("assets/temp/png/title.png") ImageClip("assets/temp/png/title.png")
.set_duration(audio_clips[0].duration) .set_duration(audio_clips[0].duration)
.set_position("center") .set_position("center")
.resize(width=W - 100), .resize(width=W - 100),
) )
for i in range(0, number_of_clips): for i in range(0, number_of_clips):
if opacity is None or float(opacity) >= 1: # opacity not set or is set to one OR MORE if opacity is None or float(opacity) >= 1: # opacity not set or is set to one OR MORE
image_clips.append( image_clips.append(
ImageClip(f"assets/temp/png/comment_{i}.png") ImageClip(f"assets/temp/png/comment_{i}.png")
.set_duration(audio_clips[i + 1].duration) .set_duration(audio_clips[i + 1].duration)
.set_position("center") .set_position("center")
.resize(width=W - 100), .resize(width=W - 100),
) )
else: else:
image_clips.append( image_clips.append(
ImageClip(f"assets/temp/png/comment_{i}.png") ImageClip(f"assets/temp/png/comment_{i}.png")
.set_duration(audio_clips[i + 1].duration) .set_duration(audio_clips[i + 1].duration)
.set_position("center") .set_position("center")
.resize(width=W - 100) .resize(width=W - 100)
.set_opacity(float(opacity)), .set_opacity(float(opacity)),
) )
# if os.path.exists("assets/mp3/posttext.mp3"): # if os.path.exists("assets/mp3/posttext.mp3"):
# image_clips.insert( # image_clips.insert(
# 0, # 0,
# ImageClip("assets/png/title.png") # ImageClip("assets/png/title.png")
# .set_duration(audio_clips[0].duration + audio_clips[1].duration) # .set_duration(audio_clips[0].duration + audio_clips[1].duration)
# .set_position("center") # .set_position("center")
# .resize(width=W - 100) # .resize(width=W - 100)
# .set_opacity(float(opacity)), # .set_opacity(float(opacity)),
# ) # )
# else: # else:
image_concat = concatenate_videoclips(image_clips).set_position(("center", "center")) image_concat = concatenate_videoclips(image_clips).set_position(("center", "center"))
image_concat.audio = audio_composite image_concat.audio = audio_composite
final = CompositeVideoClip([background_clip, image_concat]) final = CompositeVideoClip([background_clip, image_concat])
def get_video_title() -> str: def get_video_title() -> str:
title = os.getenv("VIDEO_TITLE") or "final_video" title = os.getenv("VIDEO_TITLE") or "final_video"
if len(title) <= 35: if len(title) <= 35:
return title return title
else: else:
return title[0:30] + "..." return title[0:30] + "..."
filename = f"{get_video_title()}.mp4" filename = f"{get_video_title()}.mp4"
def save_data(): def save_data():
with open("./video_creation/data/videos.json", "r+") as raw_vids: with open("./video_creation/data/videos.json", "r+") as raw_vids:
done_vids = json.load(raw_vids) done_vids = json.load(raw_vids)
if str(subreddit.submission.id) in [video["id"] for video in done_vids]: if str(subreddit.submission.id) in [video["id"] for video in done_vids]:
return # video already done but was specified to continue anyway in the .env file return # video already done but was specified to continue anyway in the .env file
payload = { payload = {
"id": str(os.getenv("VIDEO_ID")), "id": str(os.getenv("VIDEO_ID")),
"time": str(int(time.time())), "time": str(int(time.time())),
"background_credit": str(os.getenv("background_credit")), "background_credit": str(os.getenv("background_credit")),
"reddit_title": str(os.getenv("VIDEO_TITLE")), "reddit_title": str(os.getenv("VIDEO_TITLE")),
"filename": filename, "filename": filename,
} }
done_vids.append(payload) done_vids.append(payload)
raw_vids.seek(0) raw_vids.seek(0)
json.dump(done_vids, raw_vids, ensure_ascii=False, indent=4) json.dump(done_vids, raw_vids, ensure_ascii=False, indent=4)
save_data() save_data()
if not exists("./results"): if not exists("./results"):
print_substep("the results folder didn't exist so I made it") print_substep("the results folder didn't exist so I made it")
os.mkdir("./results") os.mkdir("./results")
final.write_videofile("assets/temp/temp.mp4", fps=30, audio_codec="aac", audio_bitrate="192k") final.write_videofile("assets/temp/temp.mp4", fps=30, audio_codec="aac", audio_bitrate="192k")
ffmpeg_tools.ffmpeg_extract_subclip( ffmpeg_tools.ffmpeg_extract_subclip(
"assets/temp/temp.mp4", 0, length, targetname=f"results/{filename}" "assets/temp/temp.mp4", 0, length, targetname=f"results/{filename}"
) )
# os.remove("assets/temp/temp.mp4") # os.remove("assets/temp/temp.mp4")
print_step("Removing temporary files 🗑") print_step("Removing temporary files 🗑")
cleanups = cleanup() cleanups = cleanup()
print_substep(f"Removed {cleanups} temporary files 🗑") print_substep(f"Removed {cleanups} temporary files 🗑")
print_substep("See result in the results folder!") print_substep("See result in the results folder!")
print_step( print_step(
f"Reddit title: {os.getenv('VIDEO_TITLE')} \n Background Credit: {os.getenv('background_credit')}" f"Reddit title: {os.getenv('VIDEO_TITLE')} \n Background Credit: {os.getenv('background_credit')}"
) )

@ -16,62 +16,62 @@ storymode = False
def download_screenshots_of_reddit_posts(reddit_object, screenshot_num): def download_screenshots_of_reddit_posts(reddit_object, screenshot_num):
"""Downloads screenshots of reddit posts as they are seen on the web. """Downloads screenshots of reddit posts as they are seen on the web.
Args: Args:
reddit_object: The Reddit Object you received in askreddit.py reddit_object: The Reddit Object you received in askreddit.py
screenshot_num: The number of screenshots you want to download. screenshot_num: The number of screenshots you want to download.
""" """
print_step("Downloading screenshots of reddit posts...") print_step("Downloading screenshots of reddit posts...")
# ! Make sure the reddit screenshots folder exists # ! Make sure the reddit screenshots folder exists
Path("assets/temp/png").mkdir(parents=True, exist_ok=True) Path("assets/temp/png").mkdir(parents=True, exist_ok=True)
with sync_playwright() as p: with sync_playwright() as p:
print_substep("Launching Headless Browser...") print_substep("Launching Headless Browser...")
browser = p.chromium.launch() browser = p.chromium.launch()
context = browser.new_context() context = browser.new_context()
if getenv("THEME").upper() == "DARK": if getenv("THEME").upper() == "DARK":
cookie_file = open("./video_creation/data/cookie-dark-mode.json") cookie_file = open("./video_creation/data/cookie-dark-mode.json")
else: else:
cookie_file = open("./video_creation/data/cookie-light-mode.json") cookie_file = open("./video_creation/data/cookie-light-mode.json")
cookies = json.load(cookie_file) cookies = json.load(cookie_file)
context.add_cookies(cookies) # load preference cookies context.add_cookies(cookies) # load preference cookies
# Get the thread screenshot # Get the thread screenshot
page = context.new_page() page = context.new_page()
page.goto(reddit_object["thread_url"]) page.goto(reddit_object["thread_url"])
page.set_viewport_size(ViewportSize(width=1920, height=1080)) page.set_viewport_size(ViewportSize(width=1920, height=1080))
if page.locator('[data-testid="content-gate"]').is_visible(): if page.locator('[data-testid="content-gate"]').is_visible():
# This means the post is NSFW and requires to click the proceed button. # This means the post is NSFW and requires to click the proceed button.
print_substep("Post is NSFW. You are spicy...") print_substep("Post is NSFW. You are spicy...")
page.locator('[data-testid="content-gate"] button').click() page.locator('[data-testid="content-gate"] button').click()
page.locator( page.locator(
'[data-click-id="text"] button' '[data-click-id="text"] button'
).click() # Remove "Click to see nsfw" Button in Screenshot ).click() # Remove "Click to see nsfw" Button in Screenshot
page.locator('[data-test-id="post-content"]').screenshot( page.locator('[data-test-id="post-content"]').screenshot(
path="assets/temp/png/title.png" path="assets/temp/png/title.png"
) )
if storymode: if storymode:
page.locator('[data-click-id="text"]').screenshot( page.locator('[data-click-id="text"]').screenshot(
path="assets/temp/png/story_content.png" path="assets/temp/png/story_content.png"
) )
else: else:
for idx, comment in track( for idx, comment in track(
enumerate(reddit_object["comments"]), "Downloading screenshots..." enumerate(reddit_object["comments"]), "Downloading screenshots..."
): ):
# Stop if we have reached the screenshot_num # Stop if we have reached the screenshot_num
if idx >= screenshot_num: if idx >= screenshot_num:
break break
if page.locator('[data-testid="content-gate"]').is_visible(): if page.locator('[data-testid="content-gate"]').is_visible():
page.locator('[data-testid="content-gate"] button').click() page.locator('[data-testid="content-gate"] button').click()
page.goto(f'https://reddit.com{comment["comment_url"]}') page.goto(f'https://reddit.com{comment["comment_url"]}')
page.locator(f"#t1_{comment['comment_id']}").screenshot( page.locator(f"#t1_{comment['comment_id']}").screenshot(
path=f"assets/temp/png/comment_{idx}.png" path=f"assets/temp/png/comment_{idx}.png"
) )
print_substep("Screenshots downloaded Successfully.", style="bold green") print_substep("Screenshots downloaded Successfully.", style="bold green")

@ -20,60 +20,60 @@ VIDEO_LENGTH: int = 40 # secs
def save_text_to_mp3(reddit_obj): def save_text_to_mp3(reddit_obj):
"""Saves Text to MP3 files. """Saves Text to MP3 files.
Args: Args:
reddit_obj : The reddit object you received from the reddit API in the askreddit.py file. reddit_obj : The reddit object you received from the reddit API in the askreddit.py file.
""" """
print_step("Saving Text to MP3 files...") print_step("Saving Text to MP3 files...")
length = 0 length = 0
# Create a folder for the mp3 files. # Create a folder for the mp3 files.
Path("assets/temp/mp3").mkdir(parents=True, exist_ok=True) Path("assets/temp/mp3").mkdir(parents=True, exist_ok=True)
TextToSpeech = TTS() TextToSpeech = TTS()
TextToSpeech.tts( TextToSpeech.tts(
sanitize_text(reddit_obj["thread_title"]), sanitize_text(reddit_obj["thread_title"]),
filename="assets/temp/mp3/title.mp3", filename="assets/temp/mp3/title.mp3",
random_speaker=False, random_speaker=False,
) )
try: try:
length += MP3("assets/temp/mp3/title.mp3").info.length length += MP3("assets/temp/mp3/title.mp3").info.length
except HeaderNotFoundError: # note to self AudioFileClip except HeaderNotFoundError: # note to self AudioFileClip
length += sox.file_info.duration("assets/temp/mp3/title.mp3") length += sox.file_info.duration("assets/temp/mp3/title.mp3")
if getenv("STORYMODE").casefold() == "true": if getenv("STORYMODE").casefold() == "true":
TextToSpeech.tts( TextToSpeech.tts(
sanitize_text(reddit_obj["thread_content"]), sanitize_text(reddit_obj["thread_content"]),
filename="assets/temp/mp3/story_content.mp3", filename="assets/temp/mp3/story_content.mp3",
random_speaker=False, random_speaker=False,
) )
# 'story_content' # 'story_content'
com = 0 com = 0
for comment in track((reddit_obj["comments"]), "Saving..."): for comment in track((reddit_obj["comments"]), "Saving..."):
# ! Stop creating mp3 files if the length is greater than VIDEO_LENGTH seconds. This can be longer # ! Stop creating mp3 files if the length is greater than VIDEO_LENGTH seconds. This can be longer
# but this is just a good_voices starting point # but this is just a good_voices starting point
if length > VIDEO_LENGTH: if length > VIDEO_LENGTH:
break break
TextToSpeech.tts( TextToSpeech.tts(
sanitize_text(comment["comment_body"]), sanitize_text(comment["comment_body"]),
filename=f"assets/temp/mp3/{com}.mp3", filename=f"assets/temp/mp3/{com}.mp3",
random_speaker=False, random_speaker=False,
) )
try: try:
length += MP3(f"assets/temp/mp3/{com}.mp3").info.length length += MP3(f"assets/temp/mp3/{com}.mp3").info.length
com += 1 com += 1
except (HeaderNotFoundError, MutagenError, Exception): except (HeaderNotFoundError, MutagenError, Exception):
try: try:
length += sox.file_info.duration(f"assets/temp/mp3/{com}.mp3") length += sox.file_info.duration(f"assets/temp/mp3/{com}.mp3")
com += 1 com += 1
except (OSError, IOError): except (OSError, IOError):
print( print(
"would have removed" "would have removed"
f"assets/temp/mp3/{com}.mp3" f"assets/temp/mp3/{com}.mp3"
f"assets/temp/png/comment_{com}.png" f"assets/temp/png/comment_{com}.png"
) )
# remove(f"assets/temp/mp3/{com}.mp3") # remove(f"assets/temp/mp3/{com}.mp3")
# remove(f"assets/temp/png/comment_{com}.png")# todo might cause odd un-syncing # remove(f"assets/temp/png/comment_{com}.png")# todo might cause odd un-syncing
print_substep("Saved Text to MP3 files Successfully.", style="bold green") 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. # ! Return the index, so we know how many screenshots of comments we need to make.
return length, com return length, com

Loading…
Cancel
Save