parent
ad1ce2958a
commit
127a7f245a
@ -1,39 +0,0 @@
|
|||||||
# This can be found in the email that reddit sent you when you created the app
|
|
||||||
REDDIT_CLIENT_ID=""
|
|
||||||
REDDIT_CLIENT_SECRET=""
|
|
||||||
|
|
||||||
REDDIT_USERNAME=""
|
|
||||||
REDDIT_PASSWORD=""
|
|
||||||
|
|
||||||
# If no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: "no"
|
|
||||||
RANDOM_THREAD=""
|
|
||||||
|
|
||||||
# Filters the comments by range of length (min and max characters)
|
|
||||||
# Min has to be less or equal to max
|
|
||||||
# DO NOT INSERT ANY SPACES BETWEEN THE COMMA AND THE VALUES
|
|
||||||
COMMENT_LENGTH_RANGE = "min,max"
|
|
||||||
|
|
||||||
# The absolute path of the folder where you want to save the final video
|
|
||||||
# If empty or wrong, the path will be 'results/'
|
|
||||||
FINAL_VIDEO_PATH=""
|
|
||||||
# Valid options are "yes" and "no" for the variable below
|
|
||||||
REDDIT_2FA=""
|
|
||||||
SUBREDDIT="AskReddit"
|
|
||||||
# True or False
|
|
||||||
ALLOW_NSFW="False"
|
|
||||||
# Used if you want to use a specific post. example of one is urdtfx
|
|
||||||
POST_ID=""
|
|
||||||
#set to either LIGHT or DARK
|
|
||||||
THEME="LIGHT"
|
|
||||||
# used if you want to run multiple times. set to an int e.g. 4 or 29 and leave blank for once
|
|
||||||
TIMES_TO_RUN=""
|
|
||||||
MAX_COMMENT_LENGTH="500"
|
|
||||||
# Range is 0 -> 1 recommended around 0.8-0.9
|
|
||||||
OPACITY="1"
|
|
||||||
|
|
||||||
# see TTSwrapper.py for all valid options
|
|
||||||
VOICE="Matthew" # e.g. en_us_002
|
|
||||||
TTsChoice="polly" # todo add docs
|
|
||||||
|
|
||||||
# IN-PROGRESS - not yet implemented
|
|
||||||
STORYMODE="False"
|
|
@ -1,11 +0,0 @@
|
|||||||
# To get started with Dependabot version updates, you'll need to specify which
|
|
||||||
# package ecosystems to update and where the package manifests are located.
|
|
||||||
# Please see the documentation for all configuration options:
|
|
||||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "pip" # See documentation for possible values
|
|
||||||
directory: "/" # Location of package manifests
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
@ -1,18 +0,0 @@
|
|||||||
name: Docker Image CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "master" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "master" ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Build the Docker image
|
|
||||||
run: docker build . --file Dockerfile --tag my-image-name:$(date +%s)
|
|
@ -1,90 +0,0 @@
|
|||||||
# Reddit Video Maker Bot 🎥
|
|
||||||
|
|
||||||
All done WITHOUT video editing or asset compiling. Just pure ✨programming magic✨.
|
|
||||||
|
|
||||||
Created by Lewis Menelaws & [TMRRW](https://tmrrwinc.ca)
|
|
||||||
|
|
||||||
<a target="_blank" href="https://tmrrwinc.ca">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/6053155/170528535-e274dc0b-7972-4b27-af22-637f8c370133.png">
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/6053155/170528582-cb6671e7-5a2f-4bd4-a048-0e6cfa54f0f7.png">
|
|
||||||
<img src="https://user-images.githubusercontent.com/6053155/170528582-cb6671e7-5a2f-4bd4-a048-0e6cfa54f0f7.png" width="350">
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## Video Explainer
|
|
||||||
|
|
||||||
[
|
|
||||||
](https://www.youtube.com/watch?v=3gjcY_00U1w)
|
|
||||||
|
|
||||||
## Motivation 🤔
|
|
||||||
|
|
||||||
These videos on TikTok, YouTube and Instagram get MILLIONS of views across all platforms and require very little effort.
|
|
||||||
The only original thing being done is the editing and gathering of all materials...
|
|
||||||
|
|
||||||
... but what if we can automate that process? 🤔
|
|
||||||
|
|
||||||
## Disclaimers 🚨
|
|
||||||
|
|
||||||
- **At the moment**, this repository won't attempt to upload this content through this bot. It will give you a file that
|
|
||||||
you will then have to upload manually. This is for the sake of avoiding any sort of community guideline issues.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Python 3.6+
|
|
||||||
- Playwright (this should install automatically in installation)
|
|
||||||
- Sox
|
|
||||||
|
|
||||||
## Installation 👩💻
|
|
||||||
|
|
||||||
1. Clone this repository
|
|
||||||
2. 2a **Automatic Install**: Run `python3 main.py` and type 'yes' to activate the setup assistant.
|
|
||||||
|
|
||||||
2b **Manual Install**: Rename `.env.template` to `.env` and replace all values with the appropriate fields. To get Reddit keys (**required**), visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps) TL;DR set up an app that is a "script". Copy your keys into the `.env` file, along with whether your account uses two-factor authentication.
|
|
||||||
|
|
||||||
3. Install [SoX](https://sourceforge.net/projects/sox/files/sox/)
|
|
||||||
|
|
||||||
4. Run `pip3 install -r requirements.txt`
|
|
||||||
|
|
||||||
5. Run `playwright install` and `playwright install-deps`.
|
|
||||||
|
|
||||||
6. Run `python3 main.py` (unless you chose automatic install, then the installer will automatically run main.py)
|
|
||||||
required\*\*), visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps) TL;DR set up an app that is a "script".
|
|
||||||
Copy your keys into the `.env` file, along with whether your account uses two-factor authentication.
|
|
||||||
7. Enjoy 😎
|
|
||||||
|
|
||||||
## Video
|
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/66544866/173453972-6526e4e6-c6ef-41c5-ab40-5d275e724e7c.mp4
|
|
||||||
|
|
||||||
## Contributing & Ways to improve 📈
|
|
||||||
|
|
||||||
In its current state, this bot does exactly what it needs to do. However, lots of improvements can 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!
|
|
||||||
|
|
||||||
- [ ] Creating better documentation and adding a command line interface.
|
|
||||||
- [x] Allowing users to choose a reddit thread instead of being randomized.
|
|
||||||
- [x] Allowing users to choose a background that is picked instead of the Minecraft one.
|
|
||||||
- [x] Allowing users to choose between any subreddit.
|
|
||||||
- [x] Allowing users to change voice.
|
|
||||||
- [x] Checks if a video has already been created
|
|
||||||
- [x] Light and Dark modes
|
|
||||||
- [x] NSFW post filter
|
|
||||||
|
|
||||||
Please read our [contributing guidelines](CONTRIBUTING.md) for more detailed information.
|
|
||||||
|
|
||||||
## Developers and maintainers.
|
|
||||||
|
|
||||||
Elebumm (Lewis#6305) - https://github.com/elebumm (Founder)
|
|
||||||
|
|
||||||
Jason (JasonLovesDoggo#1904) - https://github.com/JasonLovesDoggo
|
|
||||||
|
|
||||||
CallumIO (c.#6837) - https://github.com/CallumIO
|
|
||||||
|
|
||||||
HarryDaDev (hrvyy#9677) - https://github.com/ImmaHarry
|
|
||||||
|
|
||||||
LukaHietala (Pix.#0001) - https://github.com/LukaHietala
|
|
||||||
|
|
||||||
Freebiell (Freebie#6429) - https://github.com/FreebieII
|
|
@ -1,108 +0,0 @@
|
|||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import sox
|
|
||||||
from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip
|
|
||||||
from moviepy.audio.io.AudioFileClip import AudioFileClip
|
|
||||||
from requests.exceptions import JSONDecodeError
|
|
||||||
|
|
||||||
voices = [
|
|
||||||
"Brian",
|
|
||||||
"Emma",
|
|
||||||
"Russell",
|
|
||||||
"Joey",
|
|
||||||
"Matthew",
|
|
||||||
"Joanna",
|
|
||||||
"Kimberly",
|
|
||||||
"Amy",
|
|
||||||
"Geraint",
|
|
||||||
"Nicole",
|
|
||||||
"Justin",
|
|
||||||
"Ivy",
|
|
||||||
"Kendra",
|
|
||||||
"Salli",
|
|
||||||
"Raveena",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# valid voices https://lazypy.ro/tts/
|
|
||||||
|
|
||||||
|
|
||||||
class POLLY:
|
|
||||||
def __init__(self):
|
|
||||||
self.url = "https://streamlabs.com/polly/speak"
|
|
||||||
|
|
||||||
def tts(
|
|
||||||
self,
|
|
||||||
req_text: str = "Amazon Text To Speech",
|
|
||||||
filename: str = "title.mp3",
|
|
||||||
random_speaker=False,
|
|
||||||
censor=False,
|
|
||||||
):
|
|
||||||
if random_speaker:
|
|
||||||
voice = self.randomvoice()
|
|
||||||
else:
|
|
||||||
if not os.getenv("VOICE"):
|
|
||||||
return ValueError(
|
|
||||||
"Please set the environment variable VOICE to a valid voice. options are: {}".format(
|
|
||||||
voices
|
|
||||||
)
|
|
||||||
)
|
|
||||||
voice = str(os.getenv("VOICE")).capitalize()
|
|
||||||
body = {"voice": voice, "text": req_text, "service": "polly"}
|
|
||||||
response = requests.post(self.url, data=body)
|
|
||||||
try:
|
|
||||||
voice_data = requests.get(response.json()["speak_url"])
|
|
||||||
with open(filename, "wb") as f:
|
|
||||||
f.write(voice_data.content)
|
|
||||||
except (KeyError, JSONDecodeError):
|
|
||||||
if response.json()["error"] == "Text length is too long!":
|
|
||||||
chunks = [
|
|
||||||
m.group().strip() for m in re.finditer(r" *((.{0,499})(\.|.$))", req_text)
|
|
||||||
]
|
|
||||||
|
|
||||||
audio_clips = []
|
|
||||||
cbn = sox.Combiner()
|
|
||||||
|
|
||||||
chunkId = 0
|
|
||||||
for chunk in chunks:
|
|
||||||
body = {"voice": voice, "text": chunk, "service": "polly"}
|
|
||||||
resp = requests.post(self.url, data=body)
|
|
||||||
voice_data = requests.get(resp.json()["speak_url"])
|
|
||||||
with open(filename.replace(".mp3", f"-{chunkId}.mp3"), "wb") as out:
|
|
||||||
out.write(voice_data.content)
|
|
||||||
|
|
||||||
audio_clips.append(filename.replace(".mp3", f"-{chunkId}.mp3"))
|
|
||||||
|
|
||||||
chunkId = chunkId + 1
|
|
||||||
try:
|
|
||||||
if len(audio_clips) > 1:
|
|
||||||
cbn.convert(samplerate=44100, n_channels=2)
|
|
||||||
cbn.build(audio_clips, filename, "concatenate")
|
|
||||||
else:
|
|
||||||
os.rename(audio_clips[0], filename)
|
|
||||||
except (
|
|
||||||
sox.core.SoxError,
|
|
||||||
FileNotFoundError,
|
|
||||||
): # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339
|
|
||||||
for clip in audio_clips:
|
|
||||||
i = audio_clips.index(clip) # get the index of the clip
|
|
||||||
audio_clips = (
|
|
||||||
audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1 :]
|
|
||||||
) # replace the clip with an AudioFileClip
|
|
||||||
audio_concat = concatenate_audioclips(audio_clips)
|
|
||||||
audio_composite = CompositeAudioClip([audio_concat])
|
|
||||||
audio_composite.write_audiofile(filename, 44100, 2, 2000, None)
|
|
||||||
|
|
||||||
def make_readable(self, text):
|
|
||||||
"""
|
|
||||||
Amazon Polly fails to read some symbols properly such as '& (and)'.
|
|
||||||
So we normalize input text before passing it to the service
|
|
||||||
"""
|
|
||||||
text = text.replace("&", "and")
|
|
||||||
return text
|
|
||||||
|
|
||||||
def randomvoice(self):
|
|
||||||
return random.choice(voices)
|
|
@ -1,139 +0,0 @@
|
|||||||
import base64
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import sox
|
|
||||||
from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip
|
|
||||||
from moviepy.audio.io.AudioFileClip import AudioFileClip
|
|
||||||
from requests.adapters import HTTPAdapter, Retry
|
|
||||||
|
|
||||||
# from profanity_filter import ProfanityFilter
|
|
||||||
# pf = ProfanityFilter()
|
|
||||||
# Code by @JasonLovesDoggo
|
|
||||||
# https://twitter.com/scanlime/status/1512598559769702406
|
|
||||||
|
|
||||||
nonhuman = [ # DISNEY VOICES
|
|
||||||
"en_us_ghostface", # Ghost Face
|
|
||||||
"en_us_chewbacca", # Chewbacca
|
|
||||||
"en_us_c3po", # C3PO
|
|
||||||
"en_us_stitch", # Stitch
|
|
||||||
"en_us_stormtrooper", # Stormtrooper
|
|
||||||
"en_us_rocket", # Rocket
|
|
||||||
# ENGLISH VOICES
|
|
||||||
]
|
|
||||||
human = [
|
|
||||||
"en_au_001", # English AU - Female
|
|
||||||
"en_au_002", # English AU - Male
|
|
||||||
"en_uk_001", # English UK - Male 1
|
|
||||||
"en_uk_003", # English UK - Male 2
|
|
||||||
"en_us_001", # English US - Female (Int. 1)
|
|
||||||
"en_us_002", # English US - Female (Int. 2)
|
|
||||||
"en_us_006", # English US - Male 1
|
|
||||||
"en_us_007", # English US - Male 2
|
|
||||||
"en_us_009", # English US - Male 3
|
|
||||||
"en_us_010",
|
|
||||||
]
|
|
||||||
voices = nonhuman + human
|
|
||||||
|
|
||||||
noneng = [
|
|
||||||
"fr_001", # French - Male 1
|
|
||||||
"fr_002", # French - Male 2
|
|
||||||
"de_001", # German - Female
|
|
||||||
"de_002", # German - Male
|
|
||||||
"es_002", # Spanish - Male
|
|
||||||
# AMERICA VOICES
|
|
||||||
"es_mx_002", # Spanish MX - Male
|
|
||||||
"br_001", # Portuguese BR - Female 1
|
|
||||||
"br_003", # Portuguese BR - Female 2
|
|
||||||
"br_004", # Portuguese BR - Female 3
|
|
||||||
"br_005", # Portuguese BR - Male
|
|
||||||
# ASIA VOICES
|
|
||||||
"id_001", # Indonesian - Female
|
|
||||||
"jp_001", # Japanese - Female 1
|
|
||||||
"jp_003", # Japanese - Female 2
|
|
||||||
"jp_005", # Japanese - Female 3
|
|
||||||
"jp_006", # Japanese - Male
|
|
||||||
"kr_002", # Korean - Male 1
|
|
||||||
"kr_003", # Korean - Female
|
|
||||||
"kr_004", # Korean - Male 2
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# good_voices = {'good': ['en_us_002', 'en_us_006'],
|
|
||||||
# 'ok': ['en_au_002', 'en_uk_001']} # less en_us_stormtrooper more less en_us_rocket en_us_ghostface
|
|
||||||
|
|
||||||
|
|
||||||
class TikTok: # TikTok Text-to-Speech Wrapper
|
|
||||||
def __init__(self):
|
|
||||||
self.URI_BASE = "https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker="
|
|
||||||
|
|
||||||
def tts(
|
|
||||||
self,
|
|
||||||
req_text: str = "TikTok Text To Speech",
|
|
||||||
filename: str = "title.mp3",
|
|
||||||
random_speaker: bool = False,
|
|
||||||
censor=False,
|
|
||||||
):
|
|
||||||
req_text = req_text.replace("+", "plus").replace(" ", "+").replace("&", "and")
|
|
||||||
if censor:
|
|
||||||
# req_text = pf.censor(req_text)
|
|
||||||
pass
|
|
||||||
voice = (
|
|
||||||
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)]
|
|
||||||
|
|
||||||
audio_clips = []
|
|
||||||
cbn = sox.Combiner()
|
|
||||||
# cbn.set_input_format(file_type=["mp3" for _ in chunks])
|
|
||||||
|
|
||||||
chunkId = 0
|
|
||||||
for chunk in chunks:
|
|
||||||
try:
|
|
||||||
r = requests.post(f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0")
|
|
||||||
except requests.exceptions.SSLError:
|
|
||||||
# https://stackoverflow.com/a/47475019/18516611
|
|
||||||
session = requests.Session()
|
|
||||||
retry = Retry(connect=3, backoff_factor=0.5)
|
|
||||||
adapter = HTTPAdapter(max_retries=retry)
|
|
||||||
session.mount("http://", adapter)
|
|
||||||
session.mount("https://", adapter)
|
|
||||||
r = session.post(f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0")
|
|
||||||
print(r.text)
|
|
||||||
vstr = [r.json()["data"]["v_str"]][0]
|
|
||||||
b64d = base64.b64decode(vstr)
|
|
||||||
|
|
||||||
with open(filename.replace(".mp3", f"-{chunkId}.mp3"), "wb") as out:
|
|
||||||
out.write(b64d)
|
|
||||||
|
|
||||||
audio_clips.append(filename.replace(".mp3", f"-{chunkId}.mp3"))
|
|
||||||
|
|
||||||
chunkId = chunkId + 1
|
|
||||||
try:
|
|
||||||
if len(audio_clips) > 1:
|
|
||||||
cbn.convert(samplerate=44100, n_channels=2)
|
|
||||||
cbn.build(audio_clips, filename, "concatenate")
|
|
||||||
else:
|
|
||||||
os.rename(audio_clips[0], filename)
|
|
||||||
except (
|
|
||||||
sox.core.SoxError,
|
|
||||||
FileNotFoundError,
|
|
||||||
): # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339
|
|
||||||
for clip in audio_clips:
|
|
||||||
i = audio_clips.index(clip) # get the index of the clip
|
|
||||||
audio_clips = (
|
|
||||||
audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1 :]
|
|
||||||
) # replace the clip with an AudioFileClip
|
|
||||||
audio_concat = concatenate_audioclips(audio_clips)
|
|
||||||
audio_composite = CompositeAudioClip([audio_concat])
|
|
||||||
audio_composite.write_audiofile(filename, 44100, 2, 2000, None)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def randomvoice():
|
|
||||||
ok_or_good = random.randrange(1, 10)
|
|
||||||
if ok_or_good == 1: # 1/10 chance of ok voice
|
|
||||||
return random.choice(voices)
|
|
||||||
return random.choice(human) # 9/10 chance of good voice
|
|
@ -1,21 +0,0 @@
|
|||||||
from os import getenv
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
from TTS.GTTS import GTTS
|
|
||||||
from TTS.POLLY import POLLY
|
|
||||||
from TTS.TikTok import TikTok
|
|
||||||
|
|
||||||
CHOICE_DIR = {"tiktok": TikTok, "gtts": GTTS, 'polly': POLLY}
|
|
||||||
|
|
||||||
|
|
||||||
class TTS:
|
|
||||||
def __new__(cls):
|
|
||||||
load_dotenv()
|
|
||||||
CHOICE = getenv("TTsChoice").casefold()
|
|
||||||
valid_keys = [key.lower() for key in CHOICE_DIR.keys()]
|
|
||||||
if CHOICE not in valid_keys:
|
|
||||||
raise ValueError(
|
|
||||||
f"{CHOICE} is not valid. Please use one of these {valid_keys} options"
|
|
||||||
)
|
|
||||||
return CHOICE_DIR.get(CHOICE)()
|
|
@ -1,73 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
from subprocess import Popen
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from os import getenv, name
|
|
||||||
from reddit.subreddit import get_subreddit_threads
|
|
||||||
from utils.cleanup import cleanup
|
|
||||||
from utils.console import print_markdown, print_step
|
|
||||||
from video_creation.background import download_background, chop_background_video
|
|
||||||
from video_creation.final_video import make_final_video
|
|
||||||
from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts
|
|
||||||
from video_creation.voices import save_text_to_mp3
|
|
||||||
|
|
||||||
print(
|
|
||||||
"""
|
|
||||||
██████╗ ███████╗██████╗ ██████╗ ██╗████████╗ ██╗ ██╗██╗██████╗ ███████╗ ██████╗ ███╗ ███╗ █████╗ ██╗ ██╗███████╗██████╗
|
|
||||||
██╔══██╗██╔════╝██╔══██╗██╔══██╗██║╚══██╔══╝ ██║ ██║██║██╔══██╗██╔════╝██╔═══██╗ ████╗ ████║██╔══██╗██║ ██╔╝██╔════╝██╔══██╗
|
|
||||||
██████╔╝█████╗ ██║ ██║██║ ██║██║ ██║ ██║ ██║██║██║ ██║█████╗ ██║ ██║ ██╔████╔██║███████║█████╔╝ █████╗ ██████╔╝
|
|
||||||
██╔══██╗██╔══╝ ██║ ██║██║ ██║██║ ██║ ╚██╗ ██╔╝██║██║ ██║██╔══╝ ██║ ██║ ██║╚██╔╝██║██╔══██║██╔═██╗ ██╔══╝ ██╔══██╗
|
|
||||||
██║ ██║███████╗██████╔╝██████╔╝██║ ██║ ╚████╔╝ ██║██████╔╝███████╗╚██████╔╝ ██║ ╚═╝ ██║██║ ██║██║ ██╗███████╗██║ ██║
|
|
||||||
╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
load_dotenv()
|
|
||||||
# Modified by JasonLovesDoggo
|
|
||||||
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. You can find solutions to many common problems in the [Documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/)"
|
|
||||||
)
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
client_id = getenv("REDDIT_CLIENT_ID")
|
|
||||||
client_secret = getenv("REDDIT_CLIENT_SECRET")
|
|
||||||
username = getenv("REDDIT_USERNAME")
|
|
||||||
password = getenv("REDDIT_PASSWORD")
|
|
||||||
reddit2fa = getenv("REDDIT_2FA")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
cleanup()
|
|
||||||
|
|
||||||
def get_obj():
|
|
||||||
reddit_obj = get_subreddit_threads()
|
|
||||||
return reddit_obj
|
|
||||||
|
|
||||||
reddit_object = get_obj()
|
|
||||||
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)
|
|
||||||
make_final_video(number_of_comments, length)
|
|
||||||
|
|
||||||
|
|
||||||
def run_many(times):
|
|
||||||
for x in range(times):
|
|
||||||
x = x + 1
|
|
||||||
print_step(
|
|
||||||
f'on the {x}{("st" if x == 1 else ("nd" if x == 2 else ("rd" if x == 3 else "th")))} iteration of {times}'
|
|
||||||
) # correct 1st 2nd 3rd 4th 5th....
|
|
||||||
main()
|
|
||||||
Popen("cls" if name == "nt" else "clear", shell=True).wait()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
if getenv("TIMES_TO_RUN") and isinstance(int(getenv("TIMES_TO_RUN")), int):
|
|
||||||
run_many(int(getenv("TIMES_TO_RUN")))
|
|
||||||
else:
|
|
||||||
main()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print_markdown("## Clearing temp files")
|
|
||||||
cleanup()
|
|
||||||
exit()
|
|
@ -1,95 +0,0 @@
|
|||||||
from os import getenv, environ
|
|
||||||
|
|
||||||
import praw
|
|
||||||
|
|
||||||
from utils.console import print_step, print_substep
|
|
||||||
from utils.subreddit import get_subreddit_undone
|
|
||||||
from utils.videos import check_done
|
|
||||||
from praw.models import MoreComments
|
|
||||||
|
|
||||||
TEXT_WHITELIST = set("abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890")
|
|
||||||
|
|
||||||
|
|
||||||
def textify(text):
|
|
||||||
return "".join(filter(TEXT_WHITELIST.__contains__, text))
|
|
||||||
|
|
||||||
|
|
||||||
def get_subreddit_threads():
|
|
||||||
"""
|
|
||||||
Returns a list of threads from the AskReddit subreddit.
|
|
||||||
"""
|
|
||||||
global submission
|
|
||||||
print_substep("Logging into Reddit.")
|
|
||||||
|
|
||||||
content = {}
|
|
||||||
if str(getenv("REDDIT_2FA")).casefold() == "yes":
|
|
||||||
print("\nEnter your two-factor authentication code from your authenticator app.\n")
|
|
||||||
code = input("> ")
|
|
||||||
print()
|
|
||||||
pw = getenv("REDDIT_PASSWORD")
|
|
||||||
passkey = f"{pw}:{code}"
|
|
||||||
else:
|
|
||||||
passkey = getenv("REDDIT_PASSWORD")
|
|
||||||
reddit = praw.Reddit(
|
|
||||||
client_id=getenv("REDDIT_CLIENT_ID"),
|
|
||||||
client_secret=getenv("REDDIT_CLIENT_SECRET"),
|
|
||||||
user_agent="Accessing Reddit threads",
|
|
||||||
username=getenv("REDDIT_USERNAME"),
|
|
||||||
passkey=passkey,
|
|
||||||
check_for_async=False,
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
Ask user for subreddit input
|
|
||||||
"""
|
|
||||||
print_step("Getting subreddit threads...")
|
|
||||||
if not getenv(
|
|
||||||
"SUBREDDIT"
|
|
||||||
): # note to self. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython")
|
|
||||||
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/{getenv('SUBREDDIT')} from environment variable config")
|
|
||||||
subreddit = reddit.subreddit(
|
|
||||||
getenv("SUBREDDIT")
|
|
||||||
) # Allows you to specify in .env. Done for automation purposes.
|
|
||||||
|
|
||||||
if getenv("POST_ID"):
|
|
||||||
submission = reddit.submission(id=getenv("POST_ID"))
|
|
||||||
else:
|
|
||||||
threads = subreddit.hot(limit=25)
|
|
||||||
submission = get_subreddit_undone(threads, subreddit)
|
|
||||||
submission = check_done(submission) # double checking
|
|
||||||
if submission is None:
|
|
||||||
return get_subreddit_threads() # submission already done. rerun
|
|
||||||
upvotes = submission.score
|
|
||||||
ratio = submission.upvote_ratio * 100
|
|
||||||
num_comments = submission.num_comments
|
|
||||||
|
|
||||||
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 a upvote ratio of {ratio}%", 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_ID"] = str(textify(submission.id))
|
|
||||||
|
|
||||||
content["thread_url"] = f"https://reddit.com{submission.permalink}"
|
|
||||||
content["thread_title"] = submission.title
|
|
||||||
# content["thread_content"] = submission.content
|
|
||||||
content["comments"] = []
|
|
||||||
for top_level_comment in submission.comments:
|
|
||||||
if isinstance(top_level_comment, MoreComments):
|
|
||||||
continue
|
|
||||||
if top_level_comment.body in ["[removed]", "[deleted]"]:
|
|
||||||
continue # # see https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/78
|
|
||||||
if not top_level_comment.stickied:
|
|
||||||
if len(top_level_comment.body) <= int(environ["MAX_COMMENT_LENGTH"]):
|
|
||||||
content["comments"].append(
|
|
||||||
{
|
|
||||||
"comment_body": top_level_comment.body,
|
|
||||||
"comment_url": top_level_comment.permalink,
|
|
||||||
"comment_id": top_level_comment.id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
print_substep("Received subreddit threads Successfully.", style="bold green")
|
|
||||||
return content
|
|
@ -1,24 +0,0 @@
|
|||||||
import os
|
|
||||||
from os.path import exists
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup() -> int:
|
|
||||||
if exists("./assets/temp"):
|
|
||||||
count = 0
|
|
||||||
files = [
|
|
||||||
f for f in os.listdir(".") if f.endswith(".mp4") and "temp" in f.lower()
|
|
||||||
]
|
|
||||||
count += len(files)
|
|
||||||
for f in files:
|
|
||||||
os.remove(f)
|
|
||||||
try:
|
|
||||||
for file in os.listdir("./assets/temp/mp4"):
|
|
||||||
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)
|
|
||||||
return count
|
|
||||||
return 0
|
|
@ -1,51 +0,0 @@
|
|||||||
# Okay, have to admit. This code is from StackOverflow. It's so efficient, that it's probably the best way to do it.
|
|
||||||
# Although, it is edited to use less threads.
|
|
||||||
|
|
||||||
|
|
||||||
from itertools import cycle
|
|
||||||
from shutil import get_terminal_size
|
|
||||||
from threading import Thread
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
|
|
||||||
class Loader:
|
|
||||||
def __init__(self, desc="Loading...", end="Done!", timeout=0.1):
|
|
||||||
"""
|
|
||||||
A loader-like context manager
|
|
||||||
|
|
||||||
Args:
|
|
||||||
desc (str, optional): The loader's description. Defaults to "Loading...".
|
|
||||||
end (str, optional): Final print. Defaults to "Done!".
|
|
||||||
timeout (float, optional): Sleep time between prints. Defaults to 0.1.
|
|
||||||
"""
|
|
||||||
self.desc = desc
|
|
||||||
self.end = end
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
self._thread = Thread(target=self._animate, daemon=True)
|
|
||||||
self.steps = ["⢿", "⣻", "⣽", "⣾", "⣷", "⣯", "⣟", "⡿"]
|
|
||||||
self.done = False
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self._thread.start()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _animate(self):
|
|
||||||
for c in cycle(self.steps):
|
|
||||||
if self.done:
|
|
||||||
break
|
|
||||||
print(f"\r{self.desc} {c}", flush=True, end="")
|
|
||||||
sleep(self.timeout)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.done = True
|
|
||||||
cols = get_terminal_size((80, 20)).columns
|
|
||||||
print("\r" + " " * cols, end="", flush=True)
|
|
||||||
print(f"\r{self.end}", flush=True)
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
|
||||||
# handle exceptions with those variables ^
|
|
||||||
self.stop()
|
|
@ -1,32 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
import json
|
|
||||||
from os import getenv
|
|
||||||
from utils.console import print_substep
|
|
||||||
|
|
||||||
|
|
||||||
def get_subreddit_undone(submissions: List, subreddit):
|
|
||||||
"""
|
|
||||||
recursively checks if the top submission in the list was already done.
|
|
||||||
"""
|
|
||||||
with open("./video_creation/data/videos.json", "r") as done_vids_raw:
|
|
||||||
done_videos = json.load(done_vids_raw)
|
|
||||||
for submission in submissions:
|
|
||||||
if already_done(done_videos, submission):
|
|
||||||
continue
|
|
||||||
if submission.over_18:
|
|
||||||
if getenv("ALLOW_NSFW").casefold() == "false":
|
|
||||||
print_substep("NSFW Post Detected. Skipping...")
|
|
||||||
continue
|
|
||||||
return submission
|
|
||||||
print('all submissions have been done going by top submission order')
|
|
||||||
return get_subreddit_undone(
|
|
||||||
subreddit.top(time_filter="hour"), subreddit
|
|
||||||
) # all of the videos in hot have already been done
|
|
||||||
|
|
||||||
|
|
||||||
def already_done(done_videos: list, submission):
|
|
||||||
|
|
||||||
for video in done_videos:
|
|
||||||
if video["id"] == str(submission):
|
|
||||||
return True
|
|
||||||
return False
|
|
Loading…
Reference in new issue