├── .gitignore ├── README.md ├── deprecated ├── tiktokvoice.py ├── uploadVideo.py └── videoMaker.py ├── fileDetails.py ├── generateClips.py ├── instagram_upload ├── testUpload.py └── testUploadInsta.py ├── requirements.txt ├── run.py ├── scrapeLinks.py ├── scrapeLinksHelpers.py ├── speech_synthesis.py ├── static ├── audio │ └── swoosh_transition.mp3 ├── fonts │ ├── ARLRDBD.TTF │ └── GILBI___.TTF ├── hashtags.txt └── profanity_replacement.py ├── stringHelpers.py ├── textOverlay.py ├── textToSpeech.py ├── tiktok_upload ├── auth.py ├── browsers.py ├── cli.py ├── config.toml ├── proxy_auth_extension │ ├── __init__.py │ ├── background.js │ ├── manifest.json │ └── proxy_auth_extension.py ├── upload.py ├── upload_vid.py └── utils.py ├── topKWeeklyPostsScraper.py └── youtube_upload ├── constant.py ├── helpers.py └── upload.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | .idea 3 | *.mp4 4 | *.env 5 | *static 6 | 7 | *RedditPosts 8 | *Test 9 | *Custom 10 | 11 | *__pycache__ 12 | *node_modules 13 | *accountCredentials 14 | *cookies 15 | 16 | yt-dlp* 17 | 18 | ffmpeg* 19 | geckodriver* 20 | edgedriver* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RedditReels (readme and requirements is outdated, will update sometime soon) 2 | 3 | RedditReels is an automated video creator/uploader for YouTube Shorts, Instagram Reels, and Tiktok, specializing in short-form video content, particularly Reddit-style narration videos layered over video gameplay. 4 | 5 | This script is designed to run independently every week. 6 | 7 | # Setup/Installation 8 | 9 | 1. Within project directory, run the following command: ```$ pip install -r requirements.txt``` 10 | 11 | 2. Download the following dependencies if not on machine: [Microsoft EdgeDriver](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/?form=MA13LH) | [FFmpeg](https://ffmpeg.org/download.html) | [ImageMagick](https://imagemagick.org/script/download.php) 12 | 13 | 3. Setup [Microsoft Azure](https://azure.microsoft.com/en-us/products/ai-services/text-to-speech) account and Speech Services. 14 | - If tiktok voices or pytt3x are sufficient, uncomment corresponding imports and code blocks in the ./TextToSpeech.py file. 15 | - OpenAI voices are also available if an account already exists 16 | 17 | 4. Follow instructions within provided example .env to fill in necesary environment variables 18 | 19 | # Usage/Instructions 20 | 21 | 1. Enter directory containing RedditReels 22 | 23 | 2. Run the following command: ```$ python run.py``` 24 | 25 | 3. Wait until script finishes. Created videos are contained within RedditPosts/{current_day} folder 26 | 27 | # How It Works 28 | 29 | 1. **Subreddit Selection and Weekly Scraping:** 30 | - The script selects specific subreddits for content sourcing. 31 | - The top 15 popular posts from these subreddits are scraped on a weekly basis using Selenium and Beautiful Soup, posting the results into a .txt file within folders managed by the script. 32 | 33 | 2. **Video Content Creation:** 34 | - Transforms the extracted text content into short-form video clips suitable for Tiktok, Reels, and Shorts. 35 | - Transforms posted results from .txt file contents into .wav format, using the chosen TTS service (Azure, pytt3x, tiktok API, openAI) 36 | - The script randomly selects a segment from tailored video gameplay, matching the length of the TTS .wav. Text captioning from the generated .wav is then overlayed using the Python moviePy library using openAI Whisper API to transcribe timestamps upon captions. 37 | 38 | 3. **YouTube Upload and Management:** 39 | - more requirements needed, will update later 40 | - Uploads the generated videos to YouTube channels managed by the user. 41 | - Handles video metadata, descriptions, and scheduling. 42 | -------------------------------------------------------------------------------- /deprecated/tiktokvoice.py: -------------------------------------------------------------------------------- 1 | # author: GiorDior aka Giorgio 2 | # date: 12.06.2023 3 | # topic: TikTok-Voice-TTS 4 | # version: 1.0 5 | # credits: https://github.com/oscie57/tiktok-voice 6 | 7 | import threading, requests, base64 8 | from playsound import playsound 9 | import re 10 | 11 | VOICES = [ 12 | # ENGLISH VOICES 13 | 'en_us_001', # English US - Female (Int. 1) 14 | 'en_us_002', # English US - Female (Int. 2) 15 | 'en_us_006', # English US - Male 1 16 | 'en_us_007', # English US - Male 2 17 | 'en_us_009', # English US - Male 3 18 | 'en_us_010', # English US - Male 4 19 | ] 20 | 21 | ENDPOINTS = ['https://tiktok-tts.weilnet.workers.dev/api/generation', "https://tiktoktts.com/api/tiktok-tts"] 22 | current_endpoint = 0 23 | # in one conversion, the text can have a maximum length of 300 characters 24 | TEXT_BYTE_LIMIT = 270 25 | 26 | # create a list by splitting a string, every element has n chars 27 | # def split_string(string: str, chunk_size: int) -> list[str]: 28 | # words = string.split() 29 | # result = [] 30 | # current_chunk = '' 31 | # for word in words: 32 | # if len(current_chunk) + len(word) + 1 <= chunk_size: # Check if adding the word exceeds the chunk size 33 | # current_chunk += ' ' + word 34 | # else: 35 | # if current_chunk: # Append the current chunk if not empty 36 | # result.append(current_chunk.strip()) 37 | # current_chunk = word 38 | # if current_chunk: # Append the last chunk if not empty 39 | # result.append(current_chunk.strip()) 40 | # return result 41 | 42 | def split_string(text, max_length=TEXT_BYTE_LIMIT): 43 | parts = [] 44 | current_part = "" 45 | 46 | # Split sentences using regular expression to handle "?", ".", and "!" 47 | sentences = re.split(r'(?<=[.!?])\s+', text) 48 | for sentence in sentences: 49 | # fits 50 | if len(current_part) + len(sentence) + 1 < max_length: 51 | current_part += sentence + ' ' 52 | # new sentence too long, split up into fragments by "," 53 | elif len(sentence) >= max_length: 54 | if current_part: 55 | parts.append(current_part.strip()) 56 | current_part = "" 57 | 58 | sentence_parts = sentence.split(',') 59 | current_fragment = "" 60 | for part in sentence_parts: 61 | if len(current_fragment) + len(part) + 1 <= max_length: 62 | current_fragment += part + ' ' 63 | else: 64 | parts.append(current_fragment.strip()) 65 | current_fragment = part + ' ' 66 | if current_fragment: 67 | parts.append(current_fragment.strip()) 68 | 69 | # new sentence fits within max length, but not with old sentences 70 | else: 71 | parts.append(current_part.strip()) 72 | current_part = sentence + ' ' 73 | 74 | if current_part: 75 | parts.append(current_part.strip()) 76 | return parts 77 | 78 | # checking if the website that provides the service is available 79 | def get_api_response() -> requests.Response: 80 | url = f'{ENDPOINTS[current_endpoint].split("/a")[0]}' 81 | response = requests.get(url) 82 | return response 83 | 84 | # saving the audio file 85 | def save_audio_file(base64_data: str, filename: str = "output.mp3") -> None: 86 | audio_bytes = base64.b64decode(base64_data) 87 | with open(filename, "wb") as file: 88 | file.write(audio_bytes) 89 | 90 | # def write_wav_file(output_path, audio_bytes, sample_width, channels, framerate): 91 | # with wave.open(output_path, 'wb') as wav_file: 92 | # wav_file.setnchannels(channels) 93 | # wav_file.setsampwidth(sample_width) 94 | # wav_file.setframerate(framerate) 95 | # wav_file.writeframes(audio_bytes) 96 | 97 | # send POST request to get the audio data 98 | def generate_audio(text: str, voice: str) -> bytes: 99 | url = f'{ENDPOINTS[current_endpoint]}' 100 | headers = {'Content-Type': 'application/json'} 101 | data = {'text': text, 'voice': voice} 102 | response = requests.post(url, headers=headers, json=data) 103 | return response.content 104 | 105 | # creates an text to speech audio file 106 | def tts(text: str, voice: str = "none", filename: str = "output.mp3", play_sound: bool = False) -> None: 107 | # checking if the website is available 108 | global current_endpoint 109 | 110 | # if get_api_response().status_code == 200: 111 | # print("Service available!") 112 | # else: 113 | if not get_api_response().status_code == 200: 114 | current_endpoint = (current_endpoint + 1) % 2 115 | # if get_api_response().status_code == 200: 116 | # print("Service available!") 117 | # else: 118 | if not get_api_response().status_code == 200: 119 | print(f"Service not available and probably temporarily rate limited, try again later...") 120 | return 121 | 122 | # checking if arguments are valid 123 | if voice == "none": 124 | print("No voice has been selected") 125 | return 126 | 127 | if not voice in VOICES: 128 | print("Voice does not exist") 129 | return 130 | 131 | if len(text) == 0: 132 | print("Insert a valid text") 133 | return 134 | 135 | # creating the audio file 136 | try: 137 | if len(text) < TEXT_BYTE_LIMIT: 138 | audio = generate_audio((text), voice) 139 | if current_endpoint == 0: 140 | audio_base64_data = str(audio).split('"')[5] 141 | else: 142 | audio_base64_data = str(audio).split('"')[3].split(",")[1] 143 | 144 | if audio_base64_data == "error": 145 | print("This voice is unavailable right now") 146 | return 147 | 148 | else: 149 | # Split longer text into smaller parts 150 | text_parts = split_string(text, TEXT_BYTE_LIMIT) 151 | audio_base64_data = [None] * len(text_parts) 152 | 153 | # Define a thread function to generate audio for each text part 154 | def generate_audio_thread(text_part, index): 155 | audio = generate_audio(text_part, voice) 156 | if current_endpoint == 0: 157 | base64_data = str(audio).split('"')[5] 158 | else: 159 | base64_data = str(audio).split('"')[3].split(",")[1] 160 | 161 | if audio_base64_data == "error": 162 | print("This voice is unavailable right now") 163 | return "error" 164 | 165 | audio_base64_data[index] = base64_data 166 | 167 | threads = [] 168 | for index, text_part in enumerate(text_parts): 169 | # Create and start a new thread for each text part 170 | thread = threading.Thread(target=generate_audio_thread, args=(text_part, index)) 171 | thread.start() 172 | threads.append(thread) 173 | 174 | # Wait for all threads to complete 175 | for thread in threads: 176 | thread.join() 177 | 178 | # Concatenate the base64 data in the correct order 179 | audio_base64_data = "".join(audio_base64_data) 180 | 181 | save_audio_file(audio_base64_data, filename) 182 | print(f"Audio file saved successfully as '{filename}'") 183 | if play_sound: 184 | playsound(filename) 185 | 186 | except Exception as e: 187 | print("Error occurred while generating audio:", str(e)) -------------------------------------------------------------------------------- /deprecated/uploadVideo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import http.client as httplib 4 | import httplib2 5 | import os 6 | import random 7 | import sys 8 | import time 9 | 10 | from googleapiclient.discovery import build 11 | from googleapiclient.errors import HttpError 12 | from googleapiclient.http import MediaFileUpload 13 | from oauth2client.client import flow_from_clientsecrets 14 | from oauth2client.file import Storage 15 | from oauth2client.tools import argparser, run_flow 16 | 17 | 18 | # Explicitly tell the underlying HTTP transport library not to retry, since 19 | # we are handling retry logic ourselves. 20 | httplib2.RETRIES = 1 21 | 22 | # Maximum number of times to retry before giving up. 23 | MAX_RETRIES = 10 24 | 25 | # Always retry when these exceptions are raised. 26 | RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError, httplib.NotConnected, 27 | httplib.IncompleteRead, httplib.ImproperConnectionState, 28 | httplib.CannotSendRequest, httplib.CannotSendHeader, 29 | httplib.ResponseNotReady, httplib.BadStatusLine) 30 | 31 | # Always retry when an apiclient.errors.HttpError with one of these status 32 | # codes is raised. 33 | RETRIABLE_STATUS_CODES = [500, 502, 503, 504] 34 | 35 | # The CLIENT_SECRETS_FILE variable specifies the name of a file that contains 36 | # the OAuth 2.0 information for this application, including its client_id and 37 | # client_secret. You can acquire an OAuth 2.0 client ID and client secret from 38 | # the Google API Console at 39 | # https://console.cloud.google.com/. 40 | # Please ensure that you have enabled the YouTube Data API for your project. 41 | # For more information about using OAuth2 to access the YouTube Data API, see: 42 | # https://developers.google.com/youtube/v3/guides/authentication 43 | # For more information about the client_secrets.json file format, see: 44 | # https://developers.google.com/api-client-library/python/guide/aaa_client_secrets 45 | 46 | script_dir = os.path.dirname(os.path.abspath(__file__)) 47 | 48 | accountCredentialsDir = "accountCredentials" 49 | # Find the client_secret dynamically in the directory 50 | prefix = "client_secret" 51 | client_secrets = [file for file in os.listdir(accountCredentialsDir) if file.startswith(prefix)] 52 | 53 | CLIENT_SECRETS_FILE = [os.path.join(script_dir, accountCredentialsDir, client_secrets[0])] 54 | 55 | # This OAuth 2.0 access scope allows an application to upload files to the 56 | # authenticated user's YouTube channel, but doesn't allow other types of access. 57 | YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload" 58 | YOUTUBE_API_SERVICE_NAME = "youtube" 59 | YOUTUBE_API_VERSION = "v3" 60 | 61 | # This variable defines a message to display if the CLIENT_SECRETS_FILE is 62 | # missing. 63 | MISSING_CLIENT_SECRETS_MESSAGE = """ 64 | WARNING: Please configure OAuth 2.0 65 | 66 | To make this sample run you will need to populate the client_secrets.json file 67 | found at: 68 | 69 | %s 70 | 71 | with information from the API Console 72 | https://console.cloud.google.com/ 73 | 74 | For more information about the client_secrets.json file format, please visit: 75 | https://developers.google.com/api-client-library/python/guide/aaa_client_secrets 76 | """ % os.path.abspath(os.path.join(os.path.dirname(__file__), 77 | CLIENT_SECRETS_FILE)) 78 | 79 | VALID_PRIVACY_STATUSES = ("public", "private", "unlisted") 80 | 81 | 82 | def get_authenticated_service(args, index): 83 | print(CLIENT_SECRETS_FILE[index]) 84 | flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE[index], 85 | scope=YOUTUBE_UPLOAD_SCOPE, 86 | message=MISSING_CLIENT_SECRETS_MESSAGE) 87 | 88 | storage = Storage("%s-oauth2.json" % sys.argv[0]) 89 | credentials = storage.get() 90 | 91 | if credentials is None or credentials.invalid: 92 | credentials = run_flow(flow, storage, args) 93 | 94 | return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, 95 | http=credentials.authorize(httplib2.Http())) 96 | 97 | def initialize_upload(youtube, options): 98 | tags = None 99 | if options.keywords: 100 | tags = options.keywords.split(",") 101 | 102 | body=dict( 103 | snippet=dict( 104 | title=options.title, 105 | description=options.description, 106 | tags=tags, 107 | categoryId=options.category 108 | ), 109 | status=dict( 110 | privacyStatus=options.privacyStatus 111 | ) 112 | ) 113 | 114 | # Call the API's videos.insert method to create and upload the video. 115 | insert_request = youtube.videos().insert( 116 | part=",".join(body.keys()), 117 | body=body, 118 | # The chunksize parameter specifies the size of each chunk of data, in 119 | # bytes, that will be uploaded at a time. Set a higher value for 120 | # reliable connections as fewer chunks lead to faster uploads. Set a lower 121 | # value for better recovery on less reliable connections. 122 | # 123 | # Setting "chunksize" equal to -1 in the code below means that the entire 124 | # file will be uploaded in a single HTTP request. (If the upload fails, 125 | # it will still be retried where it left off.) This is usually a best 126 | # practice, but if you're using Python older than 2.6 or if you're 127 | # running on App Engine, you should set the chunksize to something like 128 | # 1024 * 1024 (1 megabyte). 129 | media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True) 130 | ) 131 | 132 | resumable_upload(insert_request) 133 | 134 | # This method implements an exponential backoff strategy to resume a 135 | # failed upload. 136 | def resumable_upload(insert_request): 137 | response = None 138 | error = None 139 | retry = 0 140 | while response is None: 141 | try: 142 | print("Uploading file...") 143 | status, response = insert_request.next_chunk() 144 | if response is not None: 145 | if 'id' in response: 146 | print("Video id '%s' was successfully uploaded." % response['id']) 147 | else: 148 | exit("The upload failed with an unexpected response: %s" % response) 149 | except HttpError as e: 150 | if e.resp.status in RETRIABLE_STATUS_CODES: 151 | error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status, 152 | e.content) 153 | else: 154 | raise 155 | except RETRIABLE_EXCEPTIONS as e: 156 | error = "A retriable error occurred: %s" % e 157 | 158 | if error is not None: 159 | print(error) 160 | retry += 1 161 | if retry > MAX_RETRIES: 162 | exit("No longer attempting to retry.") 163 | 164 | max_sleep = 2 ** retry 165 | sleep_seconds = random.random() * max_sleep 166 | print("Sleeping %f seconds and then retrying..." % sleep_seconds) 167 | time.sleep(sleep_seconds) 168 | 169 | 170 | if __name__ == '__main__': 171 | argparser.add_argument("--file", required=True, help="Video file to upload") 172 | argparser.add_argument("--title", help="Video title", default="Test Title") 173 | argparser.add_argument("--description", help="Video description", 174 | default="Test Description") 175 | argparser.add_argument("--category", default="22", 176 | help="Numeric video category. " + 177 | "See https://developers.google.com/youtube/v3/docs/videoCategories/list") 178 | argparser.add_argument("--keywords", help="Video keywords, comma separated", 179 | default="") 180 | argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES, 181 | default=VALID_PRIVACY_STATUSES[0], help="Video privacy status.") 182 | args = argparser.parse_args() 183 | 184 | if not os.path.exists(args.file): 185 | exit("Please specify a valid file using the --file= parameter.") 186 | 187 | index = 0 188 | while index < len(CLIENT_SECRETS_FILE): 189 | youtube = get_authenticated_service(args, index) 190 | try: 191 | initialize_upload(youtube, args) 192 | break # Exit the loop if the upload is successful 193 | except HttpError as e: 194 | print("An HTTP error has occurred:", e) 195 | index += 1 196 | if index < len(CLIENT_SECRETS_FILE): 197 | print("Retrying with client secrets file at index: ", index) 198 | else: 199 | print("No client secrets file left\nExiting script") -------------------------------------------------------------------------------- /deprecated/videoMaker.py: -------------------------------------------------------------------------------- 1 | import random 2 | import os 3 | import time 4 | import subprocess 5 | from moviepy.editor import VideoFileClip, AudioFileClip, concatenate_videoclips 6 | from fileDetails import get_mp3_length 7 | 8 | # Get the current working directory of the script 9 | script_dir = os.path.dirname(os.path.abspath(__file__)) 10 | ffmpeg_exe_path = os.path.join(script_dir, "ffmpeg.exe") 11 | 12 | def splitTextForWrap(input_str: str, line_length: int): 13 | words = input_str.split(" ") 14 | line_count = 0 15 | split_input = "" 16 | line = "" 17 | for word in words: 18 | if (line_count + len(word) + 1) > line_length: 19 | paddingNeeded = line_length - line_count 20 | alternatePadding = True 21 | while (paddingNeeded > 0): 22 | if alternatePadding: 23 | line = "\u00A0" + line 24 | else: 25 | line = line + "\u00A0" 26 | alternatePadding = not alternatePadding 27 | paddingNeeded -= 1 28 | line += "\n" 29 | 30 | split_input += line 31 | line = word 32 | line_count = len(word) 33 | else: 34 | line += ("\u00A0" + word) 35 | line_count += len(word) + 1 36 | 37 | paddingNeeded = line_length - line_count 38 | alternatePadding = True 39 | while (paddingNeeded > 0): 40 | if alternatePadding: 41 | line = "\u00A0" + line 42 | else: 43 | line = line + "\u00A0" 44 | alternatePadding = not alternatePadding 45 | paddingNeeded -= 1 46 | split_input += line 47 | return split_input 48 | 49 | 50 | def randomVideoSegment(input_video_filepath, input_audio_filepath, output_video_filepath, duration): 51 | total_duration_seconds = 12 * 30 + 35 52 | # Generate a random start time within the valid range 53 | random_start_time_seconds = random.uniform(0, total_duration_seconds - duration) 54 | 55 | # Load the input video and audio 56 | video_clip = VideoFileClip(input_video_filepath) 57 | audio_clip = AudioFileClip(input_audio_filepath) 58 | 59 | # Trim the video to the 2-minute random segment 60 | random_segment = video_clip.subclip(random_start_time_seconds, random_start_time_seconds + duration) 61 | 62 | # Set the audio of the random segment to the input audio 63 | random_segment = random_segment.set_audio(audio_clip) 64 | 65 | # Write the final video to the output file 66 | random_segment.write_videofile(output_video_filepath, codec="libx264", threads=8, logger = None, preset='ultrafast') 67 | 68 | print(f"Snipped {duration} s length video starting at: {random_start_time_seconds}") 69 | 70 | 71 | 72 | def textOverlay(video_path, text_input, post_path, post): 73 | partNum = 0 74 | currentVidTime = 60 75 | 76 | start_time = 0 77 | video_segments = [[]] # To store paths of individual video segments 78 | durations = [] # Initialize an empty list to store the durations 79 | # Open the line_times file to fill line durations array 80 | with open(f"{video_path.split('.')[0]}_line_times.txt", "r") as file: 81 | for line in file: 82 | parts = line.split() 83 | duration_str = parts[-1] 84 | try: 85 | duration = float(duration_str) 86 | durations.append(duration) 87 | except ValueError: 88 | print(f"Skipped invalid line: {line.strip()}") 89 | 90 | duration_i = 0 91 | # Loop through the text_array and overlay text every 5 seconds 92 | for i, text in enumerate(text_input): 93 | # print(f"{duration_i}, {text}") 94 | # Remove leading and trailing whitespace and normal spaces from the line 95 | text = text.strip()#.replace(", ", " ") 96 | noWhitespaceText = text.replace(" ", "") 97 | 98 | if not noWhitespaceText or len(noWhitespaceText) == 0: 99 | continue 100 | 101 | wrappedText = splitTextForWrap(text, 20) 102 | print(start_time, " ", (start_time + durations[duration_i]), " ", durations[duration_i]) 103 | 104 | print(f"{duration_i},\n'{wrappedText}'") 105 | 106 | cmd = [ 107 | ffmpeg_exe_path, 108 | "-nostdin", # Disable interaction with standard input 109 | "-i", video_path, 110 | "-vf", f"drawtext=text='{wrappedText}':x=(w-text_w)/2:y=(h-text_h)/3:fontsize=70:fontcolor=white:fontfile=C\\:/Windows/fonts/arlrdbd.ttf:bordercolor=black:borderw=5", 111 | "-c:a", "copy", 112 | "-preset", "ultrafast", # Use a faster preset" 113 | "-threads", "4", 114 | "-ss", str(start_time), 115 | "-t", str(durations[duration_i]), 116 | "-y", 117 | f"temp/segment_{duration_i}.mp4" # Output path for this segment 118 | ] 119 | 120 | result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 121 | if result.returncode == 0: 122 | print("Command ran successfully") 123 | else: 124 | print("Command encountered an error") 125 | print(result.stdout) # Print the standard output for debugging 126 | print(result.stderr) # Print the standard error for debugging 127 | 128 | 129 | # if length is over 60 seconds, create a new part for the video 130 | if (start_time + durations[duration_i] > currentVidTime): 131 | currentVidTime += 60 132 | partNum += 1 133 | video_segments.append([]) 134 | 135 | # Append the path of the generated segment to the list 136 | video_segments[partNum].append(f"temp/segment_{duration_i}.mp4") 137 | 138 | # Update start time for the next segment 139 | start_time += durations[duration_i] 140 | duration_i += 1 141 | 142 | partNum = 1 143 | for part in video_segments: 144 | # Load the video clips, List to store video clips 145 | video_clips = [] 146 | # Iterate through the list of segment paths 147 | for path in part: 148 | if os.path.exists(path): 149 | clip = VideoFileClip(path) 150 | video_clips.append(clip) 151 | else: 152 | # Handle non-existent files (you can print a message or take other actions) 153 | print(f"File not found: {path}") 154 | # Concatenate the video clips sequentially; maybe change compose to chain 155 | final_video = concatenate_videoclips(video_clips, method="chain") 156 | # Write the concatenated video to a file 157 | output_video_path = f"{post_path}/(part{partNum})_{post}" 158 | print(f"Writing output video: {output_video_path}") 159 | final_video.write_videofile(output_video_path, codec="libx264", threads=8, logger = None, preset='ultrafast') 160 | print(f"Finished writing part {partNum}") 161 | # Close the video clips 162 | for clip in video_clips: 163 | clip.close() 164 | 165 | # Clean up individual video segments 166 | for segment in part: 167 | if os.path.exists(segment): 168 | os.remove(segment) 169 | 170 | partNum += 1 171 | 172 | print("Overlay complete.") 173 | 174 | 175 | if __name__ == "__main__": 176 | background_video_path = "SubwaySurfers/subwaySurfers.mp4" 177 | # today = date.today().strftime("%Y-%m-%d") 178 | today = "2023-12-29" 179 | 180 | folder_path = f"RedditPosts/{today}/Texts" 181 | for subreddit in os.listdir(folder_path): 182 | post_path = f"{folder_path}/{subreddit}" 183 | for post in os.listdir(post_path): 184 | if post.endswith(".mp3"): 185 | mp3_file_path = f"{post_path}/{post}" 186 | output_video_path = f"{post_path}/{post.split('.')[0]}.mp4" 187 | duration = get_mp3_length(mp3_file_path) 188 | randomVideoSegment(background_video_path, mp3_file_path, output_video_path, duration) 189 | 190 | for subreddit in os.listdir(folder_path): 191 | post_path = f"{folder_path}/{subreddit}" 192 | for post in os.listdir(post_path): 193 | if post.endswith(".mp4") and not post.endswith("F.mp4"): 194 | mp4_file_path = f"{post_path}/{post}" 195 | text_input = [] 196 | text_file_path = f"{post_path}/{post.split('.')[0]}.txt" 197 | # Open the file for reading 198 | with open(text_file_path, 'r', encoding='utf-8') as file: 199 | # Read each line and add it to the list 200 | for line in file: 201 | # Remove leading and trailing whitespace from the line 202 | line = line.strip() 203 | removed_spaces_line = line.replace(" ", "") # This removes spaces 204 | if removed_spaces_line: 205 | text_input.append(line) 206 | 207 | textOverlay(mp4_file_path, text_input, post_path, f"{post.split('.')[0]}F.mp4") 208 | print("Video Maker Completed") 209 | -------------------------------------------------------------------------------- /fileDetails.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | # import shutil 4 | import numpy as np 5 | import pydub 6 | import wave 7 | from pydub import AudioSegment 8 | from moviepy.editor import AudioFileClip, VideoFileClip 9 | 10 | # Construct the relative path to ffmpeg.exe 11 | script_dir = os.path.dirname(os.path.abspath(__file__)) 12 | ffmpeg_exe_path = os.path.join(script_dir, "ffmpeg.exe") 13 | pydub.AudioSegment.ffmpeg = ffmpeg_exe_path 14 | # pydub.AudioSegment.converter = ffmpeg_exe_path 15 | # pydub.utils.get_prober_name = lambda: ffmpeg_exe_path 16 | 17 | def get_wav_length(wav_file_path): 18 | try: 19 | with wave.open(wav_file_path, 'rb') as wav_file: 20 | # Get the duration in seconds 21 | duration_seconds = wav_file.getnframes() / float(wav_file.getframerate()) 22 | return duration_seconds 23 | except Exception as e: 24 | print(f"Error: {e}") 25 | return 0 26 | 27 | def get_mp3_length(mp3_file_path): 28 | try: 29 | audio_clip = AudioFileClip(mp3_file_path) 30 | duration_seconds = audio_clip.duration 31 | audio_clip.close() 32 | return duration_seconds 33 | except Exception as e: 34 | print(f"Error: {e}") 35 | return None 36 | 37 | def add_mp3_padding(file_path, padding_duration_seconds): 38 | audio = AudioSegment.from_file(file_path, format="mp3") 39 | padding_duration_ms = int(padding_duration_seconds * 1000) 40 | padding = AudioSegment.silent(duration=padding_duration_ms) 41 | extended_audio = audio + padding 42 | extended_audio.export(file_path, format="mp3") 43 | 44 | def calculate_db(input_file): 45 | print(input_file) 46 | audio_segment = AudioSegment.from_file(input_file) 47 | rms = audio_segment.rms 48 | if rms == 0: 49 | return float('-inf') # Avoid log(0) 50 | return 20 * math.log10(rms) 51 | 52 | def make_mp3_audio_louder(input_audio_path, output_audio_path, volume_factor): 53 | audio_clip = AudioSegment.from_file(input_audio_path) 54 | loud_audio = audio_clip + volume_factor 55 | loud_audio.export(output_audio_path, format="mp3") 56 | 57 | def convert_video_to_audio(input_video_path, output_audio_path): 58 | # Load the video clip 59 | video_clip = VideoFileClip(input_video_path) 60 | audio_clip = video_clip.audio 61 | audio_clip.write_audiofile(output_audio_path, codec='mp3') 62 | video_clip.close() 63 | audio_clip.close() 64 | 65 | def adjust_mp4_volume(file_path, target_dB): 66 | video_clip = VideoFileClip(file_path) 67 | mean_volume_dB = 20 * np.log10(np.sqrt(np.mean(video_clip.audio.to_soundarray()**2))) 68 | volume_factor = 10**((target_dB - mean_volume_dB) / 20) 69 | print(f"{volume_factor} {mean_volume_dB}") 70 | # new_video_clip = video_clip.volumex(volume_factor) 71 | new_video_clip = video_clip.multiply_volume(volume_factor).set_audio(video_clip.audio) 72 | 73 | print(f"Adjusting {file_path} to -14dB") 74 | temp_file_path = f"{file_path.split('.')[0]}_temp.mp4" 75 | # new_video_clip.write_videofile(temp_file_path, codec='libx264', audio_codec='aac', logger=None) 76 | new_video_clip.write_videofile(temp_file_path, codec='libx264', audio_codec='aac', logger=None, temp_audiofile="temp-audio.m4a", remove_temp=True) 77 | # shutil.move(temp_file_path, file_path) 78 | video_clip.close() 79 | new_video_clip.close() 80 | 81 | if __name__ == "__main__": 82 | calculate_db("RedditPosts/2024-01-05/Texts/creepyencounters/creepyencounters1/part1.mp4") 83 | # date = "Test" 84 | # directory_path = f'RedditPosts/{date}/Texts' # Replace with the path to your directory 85 | # all_files = [] 86 | # for subreddit in os.listdir(directory_path): 87 | # subredditFolder = f"{directory_path}/{subreddit}" 88 | # for post in os.listdir(subredditFolder): 89 | # if post.endswith(".mp3"): 90 | # postPath = f"{subredditFolder}/{post}" 91 | # print(calculate_db(postPath)) 92 | 93 | # input_video_path = 'audio/snowfall_volume_boosted.mp4' 94 | # output_audio_path = 'audio/snowfall_volume_boosted.mp3' 95 | 96 | # convert_video_to_audio(input_video_path, output_audio_path) 97 | 98 | # make_mp3_audio_louder("audio/snowfall.mp3", "audio/snowfall2x.mp3", 2.0) 99 | -------------------------------------------------------------------------------- /generateClips.py: -------------------------------------------------------------------------------- 1 | import os 2 | from moviepy.editor import VideoFileClip, ImageClip, TextClip, CompositeVideoClip 3 | 4 | def createTitleClip(wrappedText, start, duration): 5 | width_x = 1080 6 | height_y = 1920 7 | textbox_size_x = 900 8 | textbox_size_y = 600 9 | center_x = width_x / 2 - textbox_size_x / 2 10 | center_y = height_y / 2 - textbox_size_y / 2 11 | font = "ARLRDBD.TTF" 12 | new_textclip = TextClip( 13 | wrappedText, 14 | fontsize=50, 15 | color='black', 16 | bg_color='transparent', 17 | method='caption', 18 | font=f"static/fonts/{font}", 19 | size=(820, None), 20 | align='West', 21 | ).set_start(start).set_duration(duration).resize(width=820).set_position(('center', 'center')) 22 | 23 | text_width, text_height = new_textclip.size 24 | 25 | background_clip = TextClip( 26 | "", 27 | fontsize=50, 28 | color='white', 29 | bg_color='white', 30 | method='caption', 31 | font=f"static/fonts/{font}", 32 | size=(900, text_height + 20), 33 | align='West', 34 | ).set_start(start).set_duration(duration).set_position(('center', 'center')) 35 | 36 | banner_path = 'static/images/medalled_banner_resized.png' 37 | banner_clip = ImageClip(banner_path, duration=duration).resize(width=900) 38 | banner_clip = banner_clip.set_pos((center_x, height_y / 2 - (text_height / 2) - banner_clip.size[1] - 10)) 39 | 40 | comment_path = 'static/images/comments.png' 41 | comment_clip = ImageClip(comment_path, duration=duration).resize(width=900) 42 | comment_clip = comment_clip.set_pos((center_x, height_y / 2 + (text_height / 2) + 10)) 43 | 44 | return background_clip, new_textclip, banner_clip, comment_clip 45 | 46 | def createTextClip(wrappedText, start, duration, color='white'): 47 | width_x = 1080 48 | height_y = 1920 49 | textbox_size_x = 900 50 | textbox_size_y = 600 51 | center_x = width_x / 2 - textbox_size_x / 2 52 | center_y = height_y / 2 - textbox_size_y / 2 53 | 54 | font = 'GILBI___.TTF' 55 | new_textclip = TextClip( 56 | wrappedText, 57 | fontsize=105, 58 | color=color, 59 | bg_color='transparent', 60 | method='caption', 61 | # method='label', 62 | font=f'static/fonts/{font}', 63 | size=(textbox_size_x, None)#, textbox_size_y) 64 | ).set_start(start).set_duration(duration).resize(lambda t : min(1, 0.8 + 15 * t)).set_position(('center', 'center')) 65 | 66 | shadow_textclip = TextClip( 67 | wrappedText, 68 | fontsize=105, 69 | color='black', 70 | bg_color='transparent', 71 | stroke_width=20, 72 | stroke_color="black", 73 | method='caption', 74 | # method='label', 75 | font=f'static/fonts/{font}', 76 | size=(textbox_size_x + 20, None)#, textbox_size_y) 77 | ).set_start(start).set_duration(duration).resize(lambda t : min(1, 0.6 + 20 * t)).set_position(('center', 'center')) 78 | 79 | return new_textclip, shadow_textclip 80 | 81 | if __name__ == "__main__": 82 | # title page test 83 | # background_clip, new_textclip, banner_clip, comment_clip = createTitleClip("UPDATE: How can I (M40) explain to my kids (F12/15/17) that my infidelity is the cause of our divorce?\n(part 1)", 0, 5) 84 | # background_clip, new_textclip, banner_clip, comment_clip = createTitleClip("UPDATE: How can I (M40) explain to my kids that my infedelitys (p1)", 0, 5) 85 | # input_video_path = 'AITAH1.mp4' 86 | # video_clip = VideoFileClip(input_video_path) 87 | # segment_clip = video_clip.subclip(0, 5) 88 | # video_with_text = CompositeVideoClip([segment_clip] + [background_clip, new_textclip, banner_clip, comment_clip]) 89 | # video_with_text.write_videofile("test.mp4", codec="libx264", threads=8, preset='ultrafast', logger = None) 90 | 91 | # text scale test 92 | new_textclip, shadow_textclip = createTextClip("How can I (M40) explain to my kids", 0, 5) 93 | input_video_path = 'AITAH1.mp4' 94 | video_clip = VideoFileClip(input_video_path) 95 | segment_clip = video_clip.subclip(0, 5) 96 | video_with_text = CompositeVideoClip([segment_clip] + [shadow_textclip, new_textclip]) 97 | video_with_text.write_videofile("test.mp4", codec="libx264", threads=8, preset='ultrafast', logger = None) -------------------------------------------------------------------------------- /instagram_upload/testUpload.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import requests 4 | from fake_useragent import UserAgent 5 | 6 | from selenium import webdriver 7 | from selenium.webdriver.common.by import By 8 | from selenium.webdriver.support.ui import WebDriverWait 9 | from selenium.webdriver.chrome import service 10 | from selenium.webdriver.common.action_chains import ActionChains 11 | from selenium.webdriver.support import expected_conditions as EC 12 | from selenium.webdriver.common.keys import Keys 13 | from selenium.common.exceptions import NoSuchElementException 14 | from webdriver_manager.chrome import ChromeDriverManager as CM 15 | 16 | 17 | options = webdriver.ChromeOptions() 18 | options.add_argument("--profile-directory=Profile 1") 19 | options.add_argument("--user-data-dir=/Users/joshuakim/Library/Application Support/Google/Chrome") 20 | options.add_argument("disable-infobars") 21 | # options.add_argument('--disable-blink-features=AutomationControlled') 22 | options.add_argument('--start-fullscreen') 23 | ua = UserAgent().random 24 | options.add_argument('user-agent={}'.format(ua)) 25 | s=service.Service(r"/Users/joshuakim/Downloads/chromedriver-mac-arm64/chromedriver.exe") 26 | driver = webdriver.Chrome(options=options, service=s) 27 | # driver = webdriver.Chrome(options=options, executable_path=CM().install()) 28 | # driver.set_window_size(1680, 900) 29 | 30 | # driver.get('https://www.tiktok.com') 31 | # ActionChains(driver).key_down(Keys.CONTROL).send_keys( 32 | # '-').key_up(Keys.CONTROL).perform() 33 | # ActionChains(driver).key_down(Keys.CONTROL).send_keys( 34 | # '-').key_up(Keys.CONTROL).perform() 35 | # print('Waiting 30s for manual login...') 36 | # time.sleep(30) 37 | driver.get('https://www.tiktok.com/creator-center/upload?lang=en') 38 | # time.sleep(20) 39 | 40 | 41 | def check_exists_by_xpath(driver, xpath): 42 | try: 43 | driver.find_element_by_xpath(xpath) 44 | except NoSuchElementException: 45 | return False 46 | 47 | return True 48 | 49 | 50 | def upload(video_path): 51 | # while True: 52 | time.sleep(5) 53 | # upload = driver.find_element(By.XPATH, '//span[@class="css-1bgawvd"]') 54 | # ac = ActionChains(driver) 55 | # ac.move_to_element(upload).move_by_offset(500, 0).click().perform() 56 | 57 | 58 | 59 | driver.switch_to.frame(0) 60 | 61 | upload_input = driver.find_element(By.XPATH, '//input[@type="file"]') 62 | upload_input.send_keys(video_path) 63 | time.sleep(60) 64 | post_button = WebDriverWait(driver, 60).until( 65 | EC.presence_of_element_located((By.XPATH, '//button[@class="css-y1m958"]')) 66 | ) 67 | post_button.click() 68 | # post = css-y1m958 69 | # for div in all_divs: 70 | # try: 71 | # print(div.get_attribute("class")) 72 | # except: 73 | # print("not found") 74 | driver.close() 75 | # file_uploader = WebDriverWait(driver, 10).until( 76 | # # slot="full-post-link" 77 | # EC.presence_of_element_located((By.XPATH, "//span[@class='css-1bgawvd']")) 78 | # ) 79 | # ActionChains(driver).move_to_element(file_uploader).click().perform() 80 | # file_uploader.click() 81 | 82 | 83 | # ================================================================ 84 | # Here is the path of the video that you want to upload in tiktok. 85 | # Plese edit the path because this is different to everyone. 86 | upload(r"/Users/joshuakim/Desktop/vid.mp4") 87 | # ================================================================ -------------------------------------------------------------------------------- /instagram_upload/testUploadInsta.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import requests 4 | from fake_useragent import UserAgent 5 | 6 | from selenium import webdriver 7 | from selenium.webdriver.common.by import By 8 | from selenium.webdriver.support.ui import WebDriverWait 9 | from selenium.webdriver.chrome import service 10 | from selenium.webdriver.common.action_chains import ActionChains 11 | from selenium.webdriver.support import expected_conditions as EC 12 | from selenium.webdriver.common.keys import Keys 13 | from selenium.common.exceptions import NoSuchElementException 14 | from webdriver_manager.chrome import ChromeDriverManager as CM 15 | 16 | # insta: 17 | 18 | options = webdriver.ChromeOptions() 19 | options.add_argument("--profile-directory=Profile 1") 20 | options.add_argument("--user-data-dir=/Users/joshuakim/Library/Application Support/Google/Chrome") 21 | options.add_argument("disable-infobars") 22 | options.add_argument('--start-fullscreen') 23 | ua = UserAgent().random 24 | options.add_argument('user-agent={}'.format(ua)) 25 | s=service.Service(r"/Users/joshuakim/Downloads/chromedriver-mac-arm64/chromedriver.exe") 26 | driver = webdriver.Chrome(options=options, service=s) 27 | 28 | # time.sleep(30) 29 | driver.get('https://www.instagram.com/') 30 | # time.sleep(20) 31 | 32 | 33 | def check_exists_by_xpath(driver, xpath): 34 | try: 35 | driver.find_element_by_xpath(xpath) 36 | except NoSuchElementException: 37 | return False 38 | 39 | return True 40 | 41 | 42 | def upload(video_path): 43 | # while True: 44 | time.sleep(5) 45 | # upload = driver.find_element(By.XPATH, '//span[@class="css-1bgawvd"]') 46 | # ac = ActionChains(driver) 47 | # ac.move_to_element(upload).move_by_offset(500, 0).click().perform() 48 | 49 | new_post = driver.find_element(By.XPATH, '//*[name()="svg" and @aria-label="New post"]') 50 | new_post.click() 51 | 52 | # specify file 53 | upload_input = WebDriverWait(driver, 10).until( 54 | EC.presence_of_element_located((By.XPATH, '//input[@type="file"]')) 55 | ) 56 | upload_input.send_keys(video_path) 57 | 58 | # go to next page 59 | next_button = WebDriverWait(driver, 60).until( 60 | EC.presence_of_element_located((By.XPATH, '//div[@class="x1i10hfl xjqpnuy xa49m3k xqeqjp1 x2hbi6w xdl72j9 x2lah0s xe8uvvx xdj266r x11i5rnm xat24cr x1mh8g0r x2lwn1j xeuugli x1hl2dhg xggy1nq x1ja2u2z x1t137rt x1q0g3np x1lku1pv x1a2a7pz x6s0dn4 xjyslct x1ejq31n xd10rxx x1sy0etr x17r0tee x9f619 x1ypdohk x1f6kntn xwhw2v2 xl56j7k x17ydfre x2b8uid xlyipyv x87ps6o x14atkfc xcdnw81 x1i0vuye xjbqb8w xm3z3ea x1x8b98j x131883w x16mih1h x972fbf xcfux6l x1qhh985 xm0m39n xt0psk2 xt7dq6l xexx8yu x4uap5 x18d9i69 xkhd6sd x1n2onr6 x1n5bzlp x173jzuc x1yc6y37"]')) 61 | ) 62 | time.sleep(3) 63 | next_button.send_keys('\n') 64 | 65 | # go to next page 66 | next_button = WebDriverWait(driver, 60).until( 67 | EC.presence_of_element_located((By.XPATH, '//div[@class="x1i10hfl xjqpnuy xa49m3k xqeqjp1 x2hbi6w xdl72j9 x2lah0s xe8uvvx xdj266r x11i5rnm xat24cr x1mh8g0r x2lwn1j xeuugli x1hl2dhg xggy1nq x1ja2u2z x1t137rt x1q0g3np x1lku1pv x1a2a7pz x6s0dn4 xjyslct x1ejq31n xd10rxx x1sy0etr x17r0tee x9f619 x1ypdohk x1f6kntn xwhw2v2 xl56j7k x17ydfre x2b8uid xlyipyv x87ps6o x14atkfc xcdnw81 x1i0vuye xjbqb8w xm3z3ea x1x8b98j x131883w x16mih1h x972fbf xcfux6l x1qhh985 xm0m39n xt0psk2 xt7dq6l xexx8yu x4uap5 x18d9i69 xkhd6sd x1n2onr6 x1n5bzlp x173jzuc x1yc6y37"]')) 68 | ) 69 | time.sleep(2) 70 | next_button.send_keys('\n') 71 | 72 | # post reel 73 | next_button = WebDriverWait(driver, 60).until( 74 | EC.presence_of_element_located((By.XPATH, '//div[@class="x1i10hfl xjqpnuy xa49m3k xqeqjp1 x2hbi6w xdl72j9 x2lah0s xe8uvvx xdj266r x11i5rnm xat24cr x1mh8g0r x2lwn1j xeuugli x1hl2dhg xggy1nq x1ja2u2z x1t137rt x1q0g3np x1lku1pv x1a2a7pz x6s0dn4 xjyslct x1ejq31n xd10rxx x1sy0etr x17r0tee x9f619 x1ypdohk x1f6kntn xwhw2v2 xl56j7k x17ydfre x2b8uid xlyipyv x87ps6o x14atkfc xcdnw81 x1i0vuye xjbqb8w xm3z3ea x1x8b98j x131883w x16mih1h x972fbf xcfux6l x1qhh985 xm0m39n xt0psk2 xt7dq6l xexx8yu x4uap5 x18d9i69 xkhd6sd x1n2onr6 x1n5bzlp x173jzuc x1yc6y37"]')) 75 | ) 76 | time.sleep(1) 77 | next_button.send_keys('\n') 78 | # next_button.click() 79 | # post_button = WebDriverWait(driver, 60).until( 80 | # EC.presence_of_element_located((By.XPATH, '//button[@class="css-y1m958"]')) 81 | # ) 82 | # post_button.click() 83 | # post = css-y1m958 84 | 85 | 86 | # time.sleep(10) 87 | # file_uploader = WebDriverWait(driver, 10).until( 88 | # # slot="full-post-link" 89 | # EC.presence_of_element_located((By.XPATH, "//span[@class='css-1bgawvd']")) 90 | # ) 91 | # ActionChains(driver).move_to_element(file_uploader).click().perform() 92 | # file_uploader.click() 93 | 94 | 95 | # ================================================================ 96 | # Here is the path of the video that you want to upload to insta. 97 | # Plese edit the path because this is different to everyone. 98 | # upload(r"/Users/joshuakim/Desktop/newvid.mp4") 99 | driver.close() 100 | # ================================================================ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | azure-cognitiveservices-speech 2 | whisper_timestamped 3 | pydub 4 | wave 5 | moviepy 6 | pillow==9.5.0 7 | 8 | selenium_firefox==2.0.8 9 | selenium==4.0.0 10 | webdriver-manager 11 | bs4 12 | 13 | demoji 14 | requests 15 | python-dotenv 16 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | if __name__ == "__main__": 5 | # Run the Python files 6 | # get top weekly posts 7 | subprocess.run(["python", "./topKWeeklyPostsScraper.py"]) 8 | # get post content 9 | subprocess.run(["python", "./scrapeLinks.py"]) 10 | # convert post content to mp3 11 | subprocess.run(["python", "./textToSpeech.py"]) 12 | # create videos 13 | subprocess.run(["python", "./textOverlay.py"]) 14 | 15 | # upload videos to YouTube, Tiktok, and Instagram 16 | # subprocess.run(["python", "./youtube_upload/upload.py"]) 17 | # subprocess.run(["python", "./tiktok_uploader/upload_vid.py"]) -------------------------------------------------------------------------------- /scrapeLinks.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.common.by import By 3 | from selenium.common.exceptions import NoSuchElementException 4 | from selenium.webdriver.edge import service 5 | # from openai import OpenAI 6 | # from accountCredentials.openai_key import OPENAI_API_KEY 7 | from scrapeLinksHelpers import getAskRedditComments, remove_emojis 8 | from static.profanity_replacement import replaceProfanity 9 | from datetime import date 10 | import time 11 | import os 12 | import re 13 | 14 | from webdriver_manager.chrome import ChromeDriverManager 15 | 16 | from dotenv import load_dotenv 17 | load_dotenv() 18 | reddit_username = os.environ.get('REDDIT_USERNAME') 19 | reddit_password = os.environ.get('REDDIT_PASSWORD') 20 | 21 | driver = webdriver.Chrome(ChromeDriverManager().install()) 22 | entire_post = "" 23 | 24 | subreddits = { 25 | "relationships": 3, 26 | "relationship_advice": 3, 27 | "confessions": 3, 28 | "TrueOffMyChest": 5, 29 | "offmychest": 5, 30 | "tifu": 4, 31 | "legaladvice": 1, 32 | "AmItheAsshole": 8, 33 | "AITAH": 8, 34 | "askreddit": 3 35 | } 36 | 37 | def check_id(id_name): 38 | try: 39 | driver.find_element(By.ID, id_name) 40 | except NoSuchElementException: 41 | return False 42 | return True 43 | 44 | def check_class(class_name): 45 | try: 46 | driver.find_element(By.CLASS_NAME, class_name) 47 | except NoSuchElementException: 48 | return False 49 | return True 50 | 51 | 52 | def check_selector(selector): 53 | try: 54 | driver.find_element(By.CSS_SELECTOR, selector) 55 | except NoSuchElementException: 56 | return False 57 | return True 58 | 59 | from selenium.webdriver.support.ui import WebDriverWait 60 | from selenium.webdriver.common.by import By 61 | from selenium.webdriver.support import expected_conditions as EC 62 | 63 | def login(): 64 | driver.get("https://www.reddit.com/login/") 65 | username_field = WebDriverWait(driver, 10).until( 66 | EC.presence_of_element_located((By.ID, "login-username")) 67 | ) 68 | password_field = WebDriverWait(driver, 10).until( 69 | EC.presence_of_element_located((By.ID, "login-password")) 70 | ) 71 | username_field.send_keys(reddit_username) 72 | password_field.send_keys(reddit_password) 73 | # login_button = WebDriverWait(driver, 10).until( 74 | # EC.element_to_be_clickable((By.CLASS_NAME, "AnimatedForm__submitButton")) 75 | # ) 76 | # login_button.click() 77 | 78 | time.sleep(10) 79 | 80 | def getContentLoggedIn(url, download_path, subreddit, number, custom): 81 | global subreddits 82 | if not custom and subreddits[subreddit] <= 0 or "removed_by_reddit" in url: 83 | return False 84 | 85 | if not os.path.exists(download_path): 86 | os.makedirs(download_path) 87 | 88 | # create the file 89 | filename = subreddit + str(number) + ".txt" 90 | output_file = os.path.join(download_path, filename) 91 | try: 92 | driver.get(url) 93 | driver.execute_script("return document.readyState") 94 | time.sleep(1) 95 | 96 | div_post = "" 97 | contentClass = "_3xX726aBn29LDbsDtzr_6E" 98 | # div_post = driver.find_element(By.CLASS_NAME, contentClass) 99 | div_post = driver.find_element(By.XPATH, "//div[starts-with(@id, 't3')]") 100 | 101 | # title_element = driver.find_element(By.TAG_NAME, "title") 102 | title_element = driver.find_element(By.XPATH, "//h1[starts-with(@id, 'post-title-t3')]") 103 | 104 | # title = title_element.get_attribute("text").split(':')[0].strip() 105 | title = title_element.text.rsplit(':', 1)[0].strip() 106 | if not title.endswith(('.', '!', '?', ';', ':')): 107 | title += '.' 108 | 109 | if title == 'Reddit - Dive into anything.': 110 | title_element = driver.find_element(By.XPATH, '//*[@id="t3_198rdnr"]/div/div[3]/div[1]/div/h1') 111 | title = title_element.get_attribute("text").rsplit(':', 1)[0].strip() 112 | if not title.endswith(('.', '!', '?', ';', ':')): 113 | title += '.' 114 | print(f"Default title found, replacing with {title}") 115 | 116 | if "update" in title.lower(): 117 | print(f"Skipping post at url {url}: Update instead of new content") 118 | return False 119 | 120 | entire_post = title + "\n" 121 | 122 | if subreddit == "askreddit": 123 | # create a file and write the title to it 124 | with open(output_file, 'w', encoding='utf-8') as file: 125 | cleaned_post = replaceProfanity(entire_post) 126 | file.write(cleaned_post) 127 | if getAskRedditComments(output_file, url): 128 | subreddits[subreddit] -= 1 129 | return True 130 | return False 131 | 132 | # get all text into a variable 133 | p_elements = div_post.find_elements(By.TAG_NAME, "p") 134 | for p_element in p_elements: 135 | # Tokenize the input text into sentences 136 | entire_post += p_element.text + '\n' 137 | 138 | pattern = re.compile(r'edit:', re.IGNORECASE) 139 | match = pattern.search(entire_post) 140 | if match: 141 | entire_post = entire_post[:match.start()] 142 | pattern = re.compile(r'update:', re.IGNORECASE) 143 | match = pattern.search(entire_post) 144 | if match and (match.start() > (len(entire_post) / 4)): 145 | entire_post = entire_post[:match.start()] 146 | pattern = re.compile(r'update post:', re.IGNORECASE) 147 | match = pattern.search(entire_post) 148 | if match and (match.start() > (len(entire_post) / 4)): 149 | entire_post = entire_post[:match.start()] 150 | pattern = re.compile(r'edited to:', re.IGNORECASE) 151 | match = pattern.search(entire_post) 152 | if match and (match.start() > (len(entire_post) / 4)): 153 | entire_post = entire_post[:match.start()] 154 | 155 | entire_post = entire_post 156 | # entire_post = title + '.\n' + completion.choices[0].message.content 157 | entire_post = remove_emojis(entire_post) 158 | 159 | if len(entire_post) < 900 or len(entire_post) > 2100: 160 | print(f"Post at {url} is too short or long with {len(entire_post)} characters, skipping...") 161 | return False 162 | 163 | with open(output_file, 'w', encoding='utf-8') as file: 164 | cleaned_post = replaceProfanity(entire_post) 165 | file.write(cleaned_post) 166 | 167 | subreddits[subreddit] -= 1 168 | return True 169 | 170 | except Exception as e: 171 | print("An error occurred:", str(e)) 172 | return False 173 | 174 | if __name__ == "__main__": 175 | # Define the URL of the Reddit page you want to scrape 176 | today = date.today().strftime("%Y-%m-%d") 177 | # today = "2024-01-18" 178 | # today = "Custom" 179 | 180 | login() 181 | custom = True if today == "Custom" else False 182 | filePath = f"RedditPosts/{today}/links.txt" 183 | download_path = f"RedditPosts/{today}/Texts" 184 | file = open(filePath, 'r') 185 | links = file.readlines() 186 | subreddit = "TIFU" 187 | count = 1 188 | for link in links: 189 | if link.strip(): 190 | tryLink = "https://" + link 191 | path = download_path + '/' + subreddit 192 | if "reddit.com" in tryLink: 193 | if getContentLoggedIn(tryLink, path, subreddit, count, custom): 194 | count += 1 195 | else: 196 | subreddit = link.strip() 197 | count = 1 198 | 199 | # Close the browser 200 | driver.quit() -------------------------------------------------------------------------------- /scrapeLinksHelpers.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | import re 4 | import html 5 | import demoji 6 | from bs4 import BeautifulSoup 7 | from static.profanity_replacement import replaceProfanity 8 | 9 | MAX_COMMENTS = 8 10 | 11 | def remove_emojis(text): 12 | return demoji.replace(text, '') 13 | 14 | def getAskRedditComments(output_file, url): 15 | # Send a GET request to the URL 16 | response = requests.get(f"{url}.json?sort=top") 17 | while response.status_code != 200: 18 | print(f"Status code of {response.status_code}: Trying again...") 19 | time.sleep(1) 20 | response = requests.get(f"{url}.json") 21 | if response.status_code == 200: 22 | # Parse the HTML content using Beautiful Soup 23 | data = response.json() 24 | 25 | i = 0 26 | # top_comments = [] 27 | for thread in data[1]["data"]["children"]: 28 | try: 29 | thread_data = thread["data"] 30 | if "body" in thread_data: 31 | top_thread_body = html.unescape(thread_data["body"]) 32 | soup = BeautifulSoup(top_thread_body, 'html.parser') 33 | top_thread_body = soup.get_text() 34 | 35 | pattern = re.compile(r'edit:', re.IGNORECASE) 36 | match = pattern.search(top_thread_body) 37 | if match: 38 | top_thread_body = top_thread_body[:match.start()] 39 | pattern = re.compile(r'update:', re.IGNORECASE) 40 | match = pattern.search(top_thread_body) 41 | if match and (match.start() > (len(top_thread_body) / 4)): 42 | top_thread_body = top_thread_body[:match.start()] 43 | 44 | # top_comments.append(remove_emojis(top_thread_body)) 45 | if (top_thread_body == '[removed]' or top_thread_body == '[deleted]' or 'https' in top_thread_body): 46 | continue 47 | with open(output_file, 'a', encoding='utf-8') as file: 48 | file.write(replaceProfanity(str(remove_emojis(top_thread_body).replace("\n", " ")) + "\n\n")) 49 | except: 50 | print("Out of comments, moving on...") 51 | i += 1 52 | if (i >= MAX_COMMENTS): 53 | break 54 | return True 55 | else: 56 | print(f"Failed to fetch the URL. Status code: {response.status_code}") 57 | return False -------------------------------------------------------------------------------- /speech_synthesis.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import azure.cognitiveservices.speech as speechsdk 4 | 5 | load_dotenv() 6 | speech_key = os.environ.get('SPEECH_KEY') 7 | secondary_speech_key = os.environ.get('SPEECH_KEY_SECONDARY') 8 | speech_region = os.environ.get('SPEECH_REGION') 9 | 10 | speech_config = speechsdk.SpeechConfig(subscription=speech_key, region=speech_region) 11 | # speech_config = speechsdk.SpeechConfig(subscription=secondary_speech_key, region=speech_region) 12 | 13 | # The language of the voice that speaks. 14 | # speech_config.speech_synthesis_language = "en-US" 15 | # speech_config.speech_synthesis_voice_name='en-US-RyanMultilingualNeural' 16 | # 17 | # 18 | 19 | def synth_speech(text, output_file): 20 | ssml_text = f""" 21 | 22 | 23 | 24 | {text} 25 | 26 | 27 | """ 28 | 29 | speech_synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=None) 30 | result = speech_synthesizer.speak_ssml_async(ssml_text).get() 31 | stream = speechsdk.AudioDataStream(result) 32 | stream.save_to_wav_file(output_file) 33 | 34 | if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted: 35 | print("Speech synthesized") 36 | elif result.reason == speechsdk.ResultReason.Canceled: 37 | cancellation_details = result.cancellation_details 38 | print("Speech synthesis canceled: {}".format(cancellation_details.reason)) 39 | if cancellation_details.reason == speechsdk.CancellationReason.Error: 40 | if cancellation_details.error_details: 41 | print("Error details: {}".format(cancellation_details.error_details)) 42 | return False 43 | return True -------------------------------------------------------------------------------- /static/audio/swoosh_transition.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvinniev34/RedditReels/a1d9ffa625a22abbffb44505a5ca10cb11cea253/static/audio/swoosh_transition.mp3 -------------------------------------------------------------------------------- /static/fonts/ARLRDBD.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvinniev34/RedditReels/a1d9ffa625a22abbffb44505a5ca10cb11cea253/static/fonts/ARLRDBD.TTF -------------------------------------------------------------------------------- /static/fonts/GILBI___.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvinniev34/RedditReels/a1d9ffa625a22abbffb44505a5ca10cb11cea253/static/fonts/GILBI___.TTF -------------------------------------------------------------------------------- /static/hashtags.txt: -------------------------------------------------------------------------------- 1 | #fyp #reddit #redditstories #minecraft #shorts -------------------------------------------------------------------------------- /static/profanity_replacement.py: -------------------------------------------------------------------------------- 1 | profanity = { 2 | "fuck": "frick", 3 | "damn": "darn", 4 | "bitch": "dog", 5 | "shitty": "lousy", 6 | "whore": "tramp", 7 | "bastard": "scoundrel", 8 | "cock": "rod", 9 | # "ass": "butt", 10 | 11 | # "sex":"shenanigans" 12 | } 13 | 14 | def replaceProfanity(text): 15 | replaced_text = text 16 | for key, value in profanity.items(): 17 | replaced_text = replaced_text.replace(key, value) 18 | return replaced_text 19 | -------------------------------------------------------------------------------- /stringHelpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def replace_abbreviations(sentence): 4 | pattern_aita1 = r'\bada\b' 5 | pattern_aita2 = r'\bida\b' 6 | pattern_aita3 = r'\baida\b' 7 | pattern_aita4 = r'\bada\b' 8 | pattern_tifu1 = r'\btyphoo\b' 9 | pattern_tifu2 = r'\bTIF(?:\s*,*\s*)you\b' 10 | 11 | modified_sentence = re.sub(pattern_aita1, 'AITA', sentence, flags=re.IGNORECASE) 12 | modified_sentence = re.sub(pattern_aita2, 'AITA', modified_sentence, flags=re.IGNORECASE) 13 | modified_sentence = re.sub(pattern_aita3, 'AITA', modified_sentence, flags=re.IGNORECASE) 14 | modified_sentence = re.sub(pattern_aita4, 'AITA', modified_sentence, flags=re.IGNORECASE) 15 | modified_sentence = re.sub(pattern_tifu1, 'TIFU', modified_sentence, flags=re.IGNORECASE) 16 | modified_sentence = re.sub(pattern_tifu2, 'TIFU', modified_sentence, flags=re.IGNORECASE) 17 | 18 | return modified_sentence 19 | 20 | def title_to_print(video_title): 21 | first_5_words = video_title[:-1].split()[:5] 22 | words_until_10_chars = "" 23 | for word in first_5_words: 24 | if len(words_until_10_chars) > 15: 25 | break 26 | else: 27 | words_until_10_chars += word + "_" 28 | return words_until_10_chars[:-1].replace(':', '').replace('&', '').replace('"', '').replace('/', '') 29 | 30 | def splitTextForWrap(input_str: str, line_length: int): 31 | words = input_str.split(" ") 32 | line_count = 0 33 | split_input = "" 34 | line = "" 35 | i = 0 36 | for word in words: 37 | # long word case 38 | if (line_count == 0 and len(word) >= line_length): 39 | split_input += (word + ("\n" if i < (len(words) - 1) else "")) 40 | elif (line_count + len(word) + 1) > line_length: 41 | paddingNeeded = line_length - line_count 42 | alternatePadding = True 43 | while (paddingNeeded > 0): 44 | if alternatePadding: 45 | line = "\u00A0" + line 46 | else: 47 | line = line + "\u00A0" 48 | alternatePadding = not alternatePadding 49 | paddingNeeded -= 1 50 | line += "\n" 51 | 52 | split_input += line 53 | line = word 54 | line_count = len(word) 55 | else: 56 | line += ("\u00A0" + word) 57 | line_count += len(word) + 1 58 | i += 1 59 | 60 | paddingNeeded = line_length - line_count 61 | alternatePadding = True 62 | while (line_count != 0 and paddingNeeded > 0): 63 | if alternatePadding: 64 | line = "\u00A0" + line 65 | else: 66 | line = line + "\u00A0" 67 | alternatePadding = not alternatePadding 68 | paddingNeeded -= 1 69 | split_input += line 70 | return split_input 71 | -------------------------------------------------------------------------------- /textOverlay.py: -------------------------------------------------------------------------------- 1 | import random 2 | import os 3 | import random 4 | from datetime import date 5 | import whisper_timestamped as whisper 6 | from moviepy.editor import VideoFileClip, AudioFileClip, CompositeVideoClip, concatenate_videoclips 7 | from fileDetails import get_wav_length 8 | from generateClips import createTitleClip, createTextClip 9 | from stringHelpers import replace_abbreviations, title_to_print#, splitTextForWrap 10 | 11 | model = whisper.load_model("base") 12 | 13 | MAX_SHORTS_TIME = 60 14 | MAX_REEL_TIME = 90 15 | 16 | INSTAGRAM_REELS_QUEUE = [] 17 | TIKTOK_QUEUE = [] 18 | YOUTUBE_SHORTS_QUEUE = [] 19 | 20 | def randomVideoSegment(output_video_filepath, duration, background="zachchoi"): 21 | total_duration_seconds = 30 * 60 22 | background_video_path = "static/backgroundVideos/minecraft/minecraft_parkour.mp4" 23 | if background == "minecraft": 24 | minecraft_times = [4*60+17, 4*60+58, 9*60+50, 7*60+4, 5*60+59, 6*60+53, 51*60+54] 25 | rand_background = random.randint(1, 7) 26 | total_duration_seconds = minecraft_times[rand_background - 1] 27 | background_video_path = f"static/backgroundVideos/minecraft/minecraft{rand_background}.mp4" 28 | elif background == "bayashi": 29 | background_video_path = "static/backgroundVideos/bayashicompilation.mp4" 30 | total_duration_seconds = 63 * 60 31 | elif background == "zachchoi": 32 | zachchoi_times = [60*60, 57*60+10] 33 | rand_background = random.randint(1, 2) 34 | total_duration_seconds = zachchoi_times[rand_background - 1] 35 | background_video_path = f"static/backgroundVideos/zachchoi/zachchoi{rand_background}.mp4" 36 | 37 | # dark_theme_subreddits = ["nosleep", "letsnotmeet", "glitch_in_the_matrix", "creepyencounters"] 38 | # if any(text.lower() in output_video_filepath.lower() for text in dark_theme_subreddits): 39 | # background_video_path = "static/backgroundVideos/nighttime_minecraft_parkour.mp4" 40 | # total_duration_seconds = 33 * 60 + 33 # nightime minecraft parkour 41 | 42 | random_start_time_seconds = random.uniform(0, total_duration_seconds - duration) 43 | video_clip = VideoFileClip(background_video_path) 44 | random_segment = video_clip.subclip(random_start_time_seconds, random_start_time_seconds + duration) 45 | random_segment.write_videofile(output_video_filepath, codec="libx264", threads=8, preset='ultrafast', logger=None) 46 | print(f"Snipped {duration} s length video starting at: {random_start_time_seconds} for {output_video_path.split('/')[-1]}") 47 | 48 | 49 | def overlayText(wav_file_path, wav_title_file_path, video_path, post_path, postName): 50 | global INSTAGRAM_REELS_QUEUE 51 | global YOUTUBE_SHORTS_QUEUE 52 | global TIKTOK_QUEUE 53 | text_colors = ['white', 'cyan', 'yellow', 'magenta'] 54 | askreddit_comment_times = [] 55 | if (postName.startswith("askreddit")): 56 | with open(f"{video_path.split('.')[0]}/comment_times.txt", 'r', encoding='utf-8') as comment_times: 57 | askreddit_comment_times = [float(value) for value in comment_times.read().split(',')] 58 | partNum = 0 59 | wav_duration = get_wav_length(wav_file_path) 60 | 61 | video_title_path = f"{mp4_file_path.split('.')[0]}/videoTitle.txt" 62 | video_title = "Errors Reading From Title" 63 | with open(video_title_path, 'r', encoding='utf-8') as file: 64 | video_title = file.read().strip() 65 | title_duration = get_wav_length(wav_title_file_path) 66 | 67 | # if more than a 6 parter, create long form content intead 68 | long_form = False 69 | if (wav_duration + title_duration) >= 180: 70 | long_form = True 71 | insta_reel = False 72 | if (wav_duration + title_duration) < 90: 73 | insta_reel = True 74 | # only create multiple parts if proceeding clips are of valulable length 75 | multipleParts = title_duration + wav_duration > MAX_SHORTS_TIME 76 | 77 | multipleShorts = title_duration + wav_duration > MAX_SHORTS_TIME + 10 78 | b_clip, title_clip, banner_clip, comment_clip = createTitleClip(video_title + (" (p1)" if multipleShorts and not long_form else ""), 0, title_duration) 79 | 80 | audio = whisper.load_audio(wav_file_path) 81 | result = whisper.transcribe(model, audio, 'en') 82 | 83 | start_time = 0 84 | currentVidTime = 0 85 | currentMaxVidTime = 60 86 | 87 | video_segments = [[[], []]] # To store paths of individual video segments 88 | video_segments[partNum][1].append(0) 89 | video_segments[partNum][0].append(b_clip) 90 | video_segments[partNum][0].append(title_clip) 91 | video_segments[partNum][0].append(banner_clip) 92 | video_segments[partNum][0].append(comment_clip) 93 | 94 | reels_video_segments = [[], []] 95 | reels_video_segments[1].append(0) 96 | 97 | tiktok_video_segments = [[], []] 98 | tiktok_video_segments[1].append(0) 99 | 100 | video_clip = VideoFileClip(video_path) 101 | 102 | print(f"\nOverlaying Text on {postName}...") 103 | first_segment = True 104 | for segment in result['segments']: 105 | abbreviationFixedText = replace_abbreviations(segment['text']) 106 | if first_segment: 107 | first_segment = False 108 | 109 | # split segment into multiple if phrase is longer than 30 characters 110 | splitSegments = [] 111 | if segment['end'] > start_time + 30: 112 | continue 113 | if len(abbreviationFixedText) <= 30: 114 | splitSegments.append([abbreviationFixedText, segment['end']]) 115 | else: 116 | # if True: 117 | currentText = "" 118 | prevEnd = start_time 119 | for word in segment['words']: 120 | if (len(word['text']) + len(currentText) + 1) < 15: 121 | currentText += (word['text'] + " ") 122 | else: 123 | splitSegments.append([replace_abbreviations(currentText), prevEnd]) 124 | currentText = (word['text'] + " ") 125 | prevEnd = word['end'] 126 | splitSegments.append([replace_abbreviations(currentText), prevEnd]) 127 | if (prevEnd == -1): 128 | print("Invalid word, too many characters") 129 | return 130 | 131 | # create text overlay for each segment 132 | for split in splitSegments: 133 | text = split[0].strip() 134 | endTime = split[1] 135 | # wrappedText = splitTextForWrap(text.strip(), 15) 136 | wrappedText = text 137 | if len(wrappedText) == 0: 138 | continue 139 | 140 | duration = endTime - start_time 141 | # print(f"{start_time} {start_time + duration} {duration}\n'{wrappedText}'") 142 | 143 | # if length is over 60 seconds, create a new part for the video 144 | if ((endTime + (title_duration + 1) * (partNum + 1)) > currentMaxVidTime and not long_form): 145 | video_segments[partNum][1].append(start_time) 146 | currentMaxVidTime += 60 147 | partNum += 1 148 | video_segments.append([[], []]) 149 | video_segments[partNum][1].append(start_time) 150 | b_clip, title_clip, banner_clip, comment_clip = createTitleClip(video_title + f" (p{partNum + 1})", 0, title_duration) 151 | video_segments[partNum][0].append(b_clip) 152 | video_segments[partNum][0].append(title_clip) 153 | video_segments[partNum][0].append(banner_clip) 154 | video_segments[partNum][0].append(comment_clip) 155 | 156 | currentVidTime = 0 157 | 158 | color = 'white' 159 | if postName.startswith("askreddit"): 160 | colorI = 0 161 | for time in askreddit_comment_times: 162 | if time <= start_time + duration: 163 | colorI += 1 164 | else: 165 | break 166 | color = text_colors[colorI % 4] 167 | 168 | new_textclip, shadow_textclip = createTextClip(wrappedText, currentVidTime, duration, color) 169 | video_segments[partNum][0].append(shadow_textclip) 170 | video_segments[partNum][0].append(new_textclip) 171 | 172 | if insta_reel and multipleParts: 173 | reels_new_textclip, reels_shadow_textclip = createTextClip(wrappedText, start_time, duration, color) 174 | reels_video_segments[0].append(reels_shadow_textclip) 175 | reels_video_segments[0].append(reels_new_textclip) 176 | 177 | if not insta_reel and not long_form: 178 | tiktok_new_textclip, tiktok_shadow_textclip = createTextClip(wrappedText, start_time, duration, color) 179 | tiktok_video_segments[0].append(tiktok_shadow_textclip) 180 | tiktok_video_segments[0].append(tiktok_new_textclip) 181 | 182 | # Update start time for the next segment 183 | start_time = endTime 184 | currentVidTime += duration 185 | 186 | video_segments[partNum][1].append(start_time) 187 | reels_video_segments[1].append(start_time) 188 | tiktok_video_segments[1].append(start_time) 189 | 190 | audio_clip = AudioFileClip(wav_file_path) 191 | 192 | # subclip to remove audio artifact, unusure why AudioFileclip makes, maybe a bug? 193 | title_audio_clip = AudioFileClip(wav_title_file_path) 194 | print_title = title_to_print(video_title) 195 | 196 | partNum = 1 197 | for part in video_segments: 198 | start_time = part[1][0] 199 | end_time = part[1][1] 200 | # last clip is too short to make 201 | if end_time - start_time < 10: 202 | continue 203 | 204 | snipped_title_video = video_clip.subclip(0, title_duration) if partNum == 1 else video_clip.subclip(start_time, start_time + title_duration) 205 | snipped_title_audio_clip = title_audio_clip.subclip(0, -0.15)#.audio_fadeout(snipped_title_video.duration - title_last_word_time) 206 | 207 | snipped_video = video_clip.subclip(start_time + title_duration, end_time + title_duration) 208 | snipped_audio = audio_clip.subclip(start_time, end_time) 209 | 210 | title_video_with_text = snipped_title_video.set_audio(snipped_title_audio_clip) 211 | title_video_with_text = CompositeVideoClip([title_video_with_text] + part[0][:4]) 212 | 213 | video_with_text = CompositeVideoClip([snipped_video] + part[0][4:]) 214 | video_with_text = video_with_text.set_audio(snipped_audio) 215 | 216 | final_video_clip = concatenate_videoclips([title_video_with_text, video_with_text]) 217 | 218 | # # add dark theme to some subreddits and if it's not long form content 219 | # dark_theme_subreddits = ["nosleep", "creepyencounters", "letsnotmeet", "glitch_in_the_matrix"] 220 | # if any(text.lower() in postName.lower() for text in dark_theme_subreddits) and not long_form: 221 | # # add snowfall background music for horror stories 222 | # print(f"adding snowfall background music to {postName}") 223 | # random_start_time_seconds = random.uniform(0, (15 * 60 + 28) - final_video_clip.duration) 224 | # snowfall_audio = AudioFileClip("static/audio/snowfall_volume_boosted.mp3").subclip(random_start_time_seconds, random_start_time_seconds + final_video_clip.duration) 225 | # final_audio = CompositeAudioClip([final_video_clip.audio, snowfall_audio]) 226 | # final_video_clip.audio = final_audio 227 | 228 | if not os.path.exists(f"{post_path}/{postName}"): 229 | os.makedirs(f"{post_path}/{postName}") 230 | video_num = f"_p{partNum}" if multipleShorts and not long_form else "" 231 | output_video_path = f"{post_path}/{postName}/{print_title}{video_num}.mp4" 232 | print(f"Writing output video: {output_video_path}") 233 | final_video_clip.write_videofile(output_video_path, codec="libx264", threads=8, preset='ultrafast') 234 | YOUTUBE_SHORTS_QUEUE.append(output_video_path) 235 | partNum += 1 236 | 237 | # write seperate if fits within instagram reels 238 | if insta_reel and multipleParts: 239 | end_time = reels_video_segments[1][1] 240 | b_clip, title_clip, banner_clip, comment_clip = createTitleClip(video_title, 0, title_duration) 241 | snipped_title_video = video_clip.subclip(0, title_duration) 242 | snipped_title_audio_clip = title_audio_clip.subclip(0, -0.15) 243 | snipped_video = video_clip.subclip(title_duration, end_time + title_duration) 244 | snipped_audio = audio_clip.subclip(0, end_time) 245 | title_video_with_text = snipped_title_video.set_audio(snipped_title_audio_clip) 246 | title_video_with_text = CompositeVideoClip([title_video_with_text] + [b_clip, title_clip, banner_clip, comment_clip]) 247 | video_with_text = CompositeVideoClip([snipped_video] + reels_video_segments[0]) 248 | video_with_text = video_with_text.set_audio(snipped_audio) 249 | final_video_clip = concatenate_videoclips([title_video_with_text, video_with_text]) 250 | output_video_path = f"{post_path}/{postName}/{print_title}_reel.mp4" 251 | final_video_clip.write_videofile(output_video_path, codec="libx264", threads=8, preset='ultrafast') 252 | INSTAGRAM_REELS_QUEUE.append(output_video_path) 253 | print(f"Finished writing reel: {output_video_path}") 254 | elif insta_reel: 255 | output_video_path = f"{post_path}/{postName}/{print_title}.mp4" 256 | print(f"Using short {output_video_path} as Instagram Reel") 257 | INSTAGRAM_REELS_QUEUE.append(output_video_path) 258 | 259 | # write seperate tiktok 260 | if not insta_reel and not long_form: 261 | end_time = tiktok_video_segments[1][1] 262 | b_clip, title_clip, banner_clip, comment_clip = createTitleClip(video_title, 0, title_duration) 263 | snipped_title_video = video_clip.subclip(0, title_duration) 264 | snipped_title_audio_clip = title_audio_clip.subclip(0, -0.15) 265 | snipped_video = video_clip.subclip(title_duration, end_time + title_duration) 266 | snipped_audio = audio_clip.subclip(0, end_time) 267 | title_video_with_text = snipped_title_video.set_audio(snipped_title_audio_clip) 268 | title_video_with_text = CompositeVideoClip([title_video_with_text] + [b_clip, title_clip, banner_clip, comment_clip]) 269 | video_with_text = CompositeVideoClip([snipped_video] + tiktok_video_segments[0]) 270 | video_with_text = video_with_text.set_audio(snipped_audio) 271 | final_video_clip = concatenate_videoclips([title_video_with_text, video_with_text]) 272 | output_video_path = f"{post_path}/{postName}/{print_title}_tiktok.mp4" 273 | final_video_clip.write_videofile(output_video_path, codec="libx264", threads=8, preset='ultrafast') 274 | TIKTOK_QUEUE.append(output_video_path) 275 | print(f"Finished writing tiktok: {output_video_path}") 276 | elif insta_reel and multipleParts: 277 | output_video_path = f"{post_path}/{postName}/{print_title}_reel.mp4" 278 | print(f"Using reel {output_video_path} as Tiktok") 279 | TIKTOK_QUEUE.append(output_video_path) 280 | else: 281 | output_video_path = f"{post_path}/{postName}/{print_title}.mp4" 282 | print(f"Using short {output_video_path} as Tiktok") 283 | TIKTOK_QUEUE.append(output_video_path) 284 | 285 | print("Overlay complete.") 286 | 287 | if __name__ == "__main__": 288 | today = date.today().strftime("%Y-%m-%d") 289 | # today = "2024-02-08" 290 | # today = "Custom" 291 | # today = "Test" 292 | folder_path = f"RedditPosts/{today}/Texts" 293 | for subreddit in os.listdir(folder_path): 294 | post_path = f"{folder_path}/{subreddit}" 295 | for post in os.listdir(post_path): 296 | if post.endswith(".wav") and not post.endswith("title.wav"): 297 | wav_file_path = f"{post_path}/{post}" 298 | wav_title_file_path = f"{post_path}/{post.split('.')[0]}_title.wav" 299 | output_video_path = f"{post_path}/{post.split('.')[0]}.mp4" 300 | duration = get_wav_length(wav_file_path) 301 | title_duration = get_wav_length(wav_title_file_path) 302 | background = "minecraft" if subreddit == "askreddit" else "zachchoi" 303 | randomVideoSegment(output_video_path, duration + title_duration, background) 304 | 305 | for subreddit in os.listdir(folder_path): 306 | post_path = f"{folder_path}/{subreddit}" 307 | for post in os.listdir(post_path): 308 | if post.endswith(".mp4"):# and post.startswith("askreddit"): 309 | wav_file_path = f"{post_path}/{post.split('.')[0]}.wav" 310 | wav_title_file_path = f"{post_path}/{post.split('.')[0]}_title.wav" 311 | mp4_file_path = f"{post_path}/{post}" 312 | # if post != "AITAH2.mp4": 313 | # continue 314 | overlayText(wav_file_path, wav_title_file_path, mp4_file_path, post_path, f"{post.split('.')[0]}") 315 | 316 | upload_queue_folder_path = f"RedditPosts/{today}/uploadQueue" 317 | if not os.path.exists(upload_queue_folder_path): 318 | os.makedirs(upload_queue_folder_path) 319 | 320 | with open(f"{upload_queue_folder_path}/tiktok_queue.txt", 'w', encoding='utf-8') as tiktok_upload_queue: 321 | tiktok_upload_queue.write('\n'.join(TIKTOK_QUEUE)) 322 | with open(f"{upload_queue_folder_path}/instagram_queue.txt", 'w', encoding='utf-8') as insta_upload_queue: 323 | insta_upload_queue.write('\n'.join(INSTAGRAM_REELS_QUEUE)) 324 | with open(f"{upload_queue_folder_path}/youtube_queue.txt", 'w', encoding='utf-8') as youtube_upload_queue: 325 | youtube_upload_queue.write('\n'.join(YOUTUBE_SHORTS_QUEUE)) 326 | 327 | print("Video Maker Completed") 328 | -------------------------------------------------------------------------------- /textToSpeech.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from datetime import date 4 | from pydub import utils, AudioSegment, effects 5 | from pydub.utils import mediainfo 6 | from fileDetails import get_wav_length 7 | # import pyttsx3 8 | # from deprecated.tiktokvoice import tts 9 | # from openai import OpenAI 10 | # from accountCredentials.openai_key import OPENAI_API_KEY 11 | from speech_synthesis import synth_speech 12 | 13 | # client = OpenAI(api_key=OPENAI_API_KEY) 14 | 15 | # Construct the relative path to ffmpeg.exe if ffmpeg not in System PATH. Uncomment if needed 16 | # script_dir = os.path.dirname(os.path.abspath(__file__)) 17 | # ffmpeg_exe_path = os.path.join(script_dir, "ffmpeg.exe") 18 | # AudioSegment.converter = ffmpeg_exe_path 19 | # def get_prob_path(): 20 | # return ffmpeg_exe_path 21 | # utils.get_prober_name = get_prob_path 22 | 23 | MAX_PART_TIME = 59 24 | 25 | def speedup_audio(filename, subreddit_path): 26 | path = os.path.join(subreddit_path, f"{filename.split('.')[0]}.wav") 27 | media_info = mediainfo(path) 28 | original_bitrate = media_info.get('bit_rate') 29 | if original_bitrate is None: 30 | original_bitrate = "352k" 31 | audio = AudioSegment.from_file(path) 32 | 33 | # audio = speedup(audio, 1.2) # use either 1.25 or 1.3 34 | 35 | # Calculate the dB adjustment 36 | print("Audio Level (dBFS):", audio.dBFS) 37 | effects.normalize(audio) 38 | dB_adjustment = min(-14 - audio.dBFS, -1 * audio.max_dBFS) 39 | audio = audio + dB_adjustment 40 | effects.normalize(audio) 41 | 42 | print("Audio Level (dBFS):", audio.dBFS) 43 | audio.export(path, format="wav", bitrate=original_bitrate) # export to wav 44 | 45 | def convert(filename, folder_path): 46 | text_file_path = os.path.join(folder_path, filename) 47 | output_file = os.path.join(folder_path, f"{filename.split('.')[0]}.wav") 48 | output_title_file = os.path.join(folder_path, f"{filename.split('.')[0]}_title.wav") 49 | 50 | # Initialize pyttsx3 51 | # engine = pyttsx3.init("sapi5") 52 | # voices = engine.getProperty("voices")[1] 53 | # rate = engine.getProperty('rate') 54 | # engine.setProperty('rate', rate) 55 | # engine.setProperty('voice', voices) 56 | max_video_time = MAX_PART_TIME 57 | try: 58 | with open(text_file_path, 'r', encoding='utf-8') as file: 59 | title = file.readline().replace("&", "and").strip() 60 | 61 | output_directory = f"{folder_path}/{filename.split('.')[0]}" 62 | if not os.path.exists(output_directory): 63 | os.makedirs(output_directory) 64 | with open(f"{output_directory}/videoTitle.txt", 'w', encoding='utf-8') as title_file: 65 | title_file.write(title) 66 | 67 | if filename.startswith("askreddit"): 68 | swoosh_transition = AudioSegment.from_file("static/audio/swoosh_transition.mp3") 69 | swoosh_transition_length = len(swoosh_transition) / 1000 70 | 71 | title_synthesized = synth_speech(title, output_title_file) 72 | while not title_synthesized: 73 | print("Title not synthesized, trying again...") 74 | time.sleep(3) 75 | title_synthesized = synth_speech(title, output_title_file) 76 | 77 | segment_files = [] 78 | total_time = [] 79 | title_time = get_wav_length(output_title_file) 80 | cur_time = 0 81 | num_comments = 0 82 | for line in file: 83 | if not line.isspace() and cur_time + title_time < max_video_time: 84 | segment_file = f"{output_file.split('.')[0]}_seg{num_comments}.wav" 85 | segment_synthesized = synth_speech(line.strip().replace("&", "and"), segment_file) 86 | while not segment_synthesized: 87 | print("Segment not synthesized, trying again...") 88 | time.sleep(3) 89 | segment_synthesized = synth_speech(line.strip().replace("&", "and"), segment_file) 90 | 91 | new_segment_time = get_wav_length(segment_file) 92 | if cur_time + new_segment_time + title_time >= max_video_time and cur_time != 0: 93 | os.remove(segment_file) 94 | break 95 | elif cur_time + new_segment_time + title_time >= max_video_time and cur_time == 0: 96 | total_time.append((cur_time + (swoosh_transition_length + new_segment_time))) 97 | segment_files.append(segment_file) 98 | break 99 | cur_time += (swoosh_transition_length + new_segment_time) 100 | total_time.append(cur_time) 101 | segment_files.append(segment_file) 102 | num_comments += 1 103 | 104 | # concatenate audio into single audio 105 | askreddit_segments = [AudioSegment.from_wav(segment_file) for segment_file in segment_files] 106 | askreddit_audio = AudioSegment.empty() 107 | first = True 108 | for segment in askreddit_segments: 109 | askreddit_audio += segment if first else (swoosh_transition + segment) 110 | first = False 111 | askreddit_audio.export(output_file, format="wav") 112 | with open(f"{output_directory}/comment_times.txt", 'w', encoding='utf-8') as comment_time_file: 113 | comment_time_file.write(','.join(map(str, total_time))) 114 | 115 | # cleanup individual comment files 116 | for segment_file in segment_files: 117 | try: 118 | os.remove(segment_file) 119 | except OSError as e: 120 | print(f"Error deleting file {segment_file}") 121 | else: 122 | lines = file.read().replace("&", "and") 123 | if len(lines) > 4000: 124 | print(f"Exiting file tts, {filename} too large") 125 | return False 126 | 127 | # tiktok TTS 128 | # tts(lines, "en_us_010", output_file, play_sound=False) 129 | 130 | # Microsoft Azure tts 131 | title_synthesized = synth_speech(title, output_title_file) 132 | while not title_synthesized: 133 | print("Title not synthesized, trying again...") 134 | time.sleep(3) 135 | title_synthesized = synth_speech(title, output_title_file) 136 | 137 | body_synthesized = synth_speech(lines, output_file) 138 | while not body_synthesized: 139 | print("Body not synthesized, trying again...") 140 | time.sleep(3) 141 | body_synthesized = synth_speech(lines, output_file) 142 | 143 | # openai tts 144 | # response = client.audio.speech.create( 145 | # model="tts-1", 146 | # voice="onyx", 147 | # input=lines 148 | # ) 149 | # response.stream_to_file(output_file) 150 | 151 | # pyttsx3 tts 152 | # engine.save_to_file(title, output_title_file) 153 | # engine.runAndWait() 154 | # engine.save_to_file(lines, output_file) 155 | # engine.runAndWait() 156 | 157 | print(f"wav creation successful. Saved as {output_file}") 158 | 159 | except FileNotFoundError: 160 | print(f"Error: File not found error") 161 | except Exception as e: 162 | print(f"An error occurred: {e}") 163 | 164 | if __name__ == "__main__": 165 | today = date.today().strftime("%Y-%m-%d") 166 | # today = "2024-01-06" 167 | # today = "Custom" 168 | # today = "Test" 169 | processed = ["askreddit"] 170 | 171 | folder_path = f"RedditPosts/{today}/Texts" 172 | # Iterate through all files in the folder 173 | for subreddit in os.listdir(folder_path): 174 | subreddit_path = f"{folder_path}/{subreddit}" 175 | print(f"Currently processing {subreddit}") 176 | for filename in os.listdir(subreddit_path): 177 | if filename.split('.')[-1] == "txt":# and subreddit in processed: 178 | convert(filename, subreddit_path) 179 | print(f"Processed {filename}") 180 | 181 | # for subreddit in os.listdir(folder_path): 182 | # subreddit_path = f"{folder_path}/{subreddit}" 183 | # for filename in os.listdir(subreddit_path): 184 | # wav_file_path = f"{subreddit_path}/{filename}" 185 | # if filename.split('.')[-1] == "wav":# and subreddit in processed: 186 | # # speedup_audio(filename, subreddit_path) 187 | # print(f"Increased Volume of {wav_file_path}") 188 | 189 | 190 | -------------------------------------------------------------------------------- /tiktok_upload/auth.py: -------------------------------------------------------------------------------- 1 | """Handles authentication for TikTokUploader""" 2 | from http import cookiejar 3 | from time import time, sleep 4 | 5 | from selenium.webdriver.common.by import By 6 | 7 | from selenium.webdriver.support.ui import WebDriverWait 8 | from selenium.webdriver.support import expected_conditions as EC 9 | 10 | from tiktok_uploader import config, logger 11 | from tiktok_uploader.browsers import get_browser 12 | from tiktok_uploader.utils import green 13 | 14 | class AuthBackend: 15 | """ 16 | Handles authentication for TikTokUploader 17 | """ 18 | username: str 19 | password: str 20 | cookies: list 21 | 22 | def __init__(self, username: str = '', password: str = '', 23 | cookies_list: list = None, cookies=None, cookies_str=None, sessionid: str = None): 24 | """ 25 | Creates the authentication backend 26 | 27 | Keyword arguments: 28 | - username -> the accounts's username or email 29 | - password -> the account's password 30 | 31 | - cookies -> a list of cookie dictionaries of cookies which is Selenium-compatible 32 | """ 33 | if (username and not password) or (password and not username): 34 | raise InsufficientAuth() 35 | 36 | self.cookies = self.get_cookies(path=cookies) if cookies else [] 37 | self.cookies += self.get_cookies(cookies_str=cookies_str) if cookies_str else [] 38 | self.cookies += cookies_list if cookies_list else [] 39 | self.cookies += [{'name': 'sessionid', 'value': sessionid}] if sessionid else [] 40 | 41 | if not (self.cookies or (username and password)): 42 | raise InsufficientAuth() 43 | 44 | self.username = username 45 | self.password = password 46 | 47 | if cookies: 48 | logger.debug(green("Authenticating browser with cookies")) 49 | elif username and password: 50 | logger.debug(green("Authenticating browser with username and password")) 51 | elif sessionid: 52 | logger.debug(green("Authenticating browser with sessionid")) 53 | elif cookies_list: 54 | logger.debug(green("Authenticating browser with cookies_list")) 55 | 56 | 57 | def authenticate_agent(self, driver): 58 | """ 59 | Authenticates the agent using the browser backend 60 | """ 61 | # tries to use cookies 62 | if not self.cookies and self.username and self.password: 63 | self.cookies = login(driver, username=self.username, password=self.password) 64 | 65 | logger.debug(green("Authenticating browser with cookies")) 66 | 67 | driver.get(config['paths']['main']) 68 | 69 | WebDriverWait(driver, config['explicit_wait']).until(EC.title_contains("TikTok")) 70 | 71 | for cookie in self.cookies: 72 | try: 73 | driver.add_cookie(cookie) 74 | except Exception as _: 75 | logger.error('Failed to add cookie %s', cookie) 76 | 77 | return driver 78 | 79 | 80 | def get_cookies(self, path: str = None, cookies_str: str = None) -> dict: 81 | """ 82 | Gets cookies from the passed file using the netscape standard 83 | """ 84 | if path: 85 | with open(path, "r", encoding="utf-8") as file: 86 | lines = file.read().split("\n") 87 | else: 88 | lines = cookies_str.split("\n") 89 | 90 | return_cookies = [] 91 | for line in lines: 92 | split = line.split('\t') 93 | if len(split) < 6: 94 | continue 95 | 96 | split = [x.strip() for x in split] 97 | 98 | try: 99 | split[4] = int(split[4]) 100 | except ValueError: 101 | split[4] = None 102 | 103 | return_cookies.append({ 104 | 'name': split[5], 105 | 'value': split[6], 106 | 'domain': split[0], 107 | 'path': split[2], 108 | }) 109 | 110 | if split[4]: 111 | return_cookies[-1]['expiry'] = split[4] 112 | return return_cookies 113 | 114 | 115 | def login_accounts(driver=None, accounts=[(None, None)], *args, **kwargs) -> list: 116 | """ 117 | Authenticates the accounts using the browser backend and saves the required credentials 118 | 119 | Keyword arguments: 120 | - driver -> the webdriver to use 121 | - accounts -> a list of tuples of the form (username, password) 122 | """ 123 | driver = driver or get_browser(headless=False, *args, **kwargs) 124 | 125 | cookies = {} 126 | for account in accounts: 127 | username, password = get_username_and_password(account) 128 | 129 | cookies[username] = login(driver, username, password) 130 | 131 | return cookies 132 | 133 | 134 | def login(driver, username: str, password: str): 135 | """ 136 | Logs in the user using the email and password 137 | """ 138 | assert username and password, "Username and password are required" 139 | 140 | # checks if the browser is on TikTok 141 | if not config['paths']['main'] in driver.current_url: 142 | driver.get(config['paths']['main']) 143 | 144 | # checks if the user is already logged in 145 | if driver.get_cookie(config['selectors']['login']['cookie_of_interest']): 146 | # clears the existing cookies 147 | driver.delete_all_cookies() 148 | 149 | # goes to the login site 150 | driver.get(config['paths']['login']) 151 | 152 | # selects and fills the login and the password 153 | username_field = WebDriverWait(driver, config['explicit_wait']).until( 154 | EC.presence_of_element_located((By.XPATH, config['selectors']['login']['username_field'])) 155 | ) 156 | username_field.clear() 157 | username_field.send_keys(username) 158 | 159 | password_field = driver.find_element(By.XPATH, config['selectors']['login']['password_field']) 160 | password_field.clear() 161 | password_field.send_keys(password) 162 | 163 | # submits the form 164 | submit = driver.find_element(By.XPATH, config['selectors']['login']['login_button']) 165 | submit.click() 166 | 167 | print(f'Complete the captcha for {username}') 168 | 169 | # Wait until the session id cookie is set 170 | start_time = time() 171 | while not driver.get_cookie(config['selectors']['login']['cookie_of_interest']): 172 | sleep(0.5) 173 | if time() - start_time > config['explicit_wait']: 174 | raise InsufficientAuth() # TODO: Make this something more real 175 | 176 | # wait until the url changes 177 | WebDriverWait(driver, config['explicit_wait']).until(EC.url_changes(config['paths']['login'])) 178 | 179 | return driver.get_cookies() 180 | 181 | 182 | def get_username_and_password(login_info: tuple or dict): 183 | """ 184 | Parses the input into a username and password 185 | """ 186 | if not isinstance(login_info, dict): 187 | return login_info[0], login_info[1] 188 | 189 | # checks if they used email or username 190 | if 'email' in login_info: 191 | return login_info['email'], login_info['password'] 192 | elif 'username' in login_info: 193 | return login_info['username'], login_info['password'] 194 | 195 | raise InsufficientAuth() 196 | 197 | 198 | def save_cookies(path, cookies: list): 199 | """ 200 | Saves the cookies to a netscape file 201 | """ 202 | # saves the cookies to a file 203 | cookie_jar = cookiejar.MozillaCookieJar(path) 204 | cookie_jar.load() 205 | 206 | for cookie in cookies: 207 | cookie_jar.set_cookie(cookie) 208 | 209 | cookie_jar.save() 210 | 211 | 212 | class InsufficientAuth(Exception): 213 | """ 214 | Insufficient authentication: 215 | 216 | > TikTok uses cookies to keep track of the user's authentication or session. 217 | 218 | Either: 219 | - Use a cookies file passed as the `cookies` argument 220 | - easily obtained using https://github.com/kairi003/Get-cookies.txt-LOCALLY 221 | - Use a cookies list passed as the `cookies_list` argument 222 | - can be obtained from your browser's developer tools under storage -> cookies 223 | - only the `sessionid` cookie is required 224 | """ 225 | 226 | def __init__(self, message=None): 227 | super().__init__(message or self.__doc__) 228 | -------------------------------------------------------------------------------- /tiktok_upload/browsers.py: -------------------------------------------------------------------------------- 1 | """Gets the browser's given the user's input""" 2 | from selenium.webdriver.chrome.options import Options as ChromeOptions 3 | from selenium.webdriver.firefox.options import Options as FirefoxOptions 4 | from selenium.webdriver.safari.options import Options as SafariOptions 5 | from selenium.webdriver.edge.options import Options as EdgeOptions 6 | from selenium.webdriver.chrome import service 7 | 8 | # Webdriver managers 9 | from selenium.webdriver.chrome.service import Service as ChromeService 10 | from webdriver_manager.chrome import ChromeDriverManager 11 | from selenium.webdriver.firefox.service import Service as FirefoxService 12 | from webdriver_manager.firefox import GeckoDriverManager 13 | from webdriver_manager.microsoft import EdgeChromiumDriverManager 14 | from selenium.webdriver.edge.service import Service as EdgeService 15 | 16 | from selenium import webdriver 17 | 18 | from tiktok_uploader import config 19 | from tiktok_uploader.proxy_auth_extension.proxy_auth_extension import generate_proxy_auth_extension 20 | 21 | 22 | def get_browser(name: str = 'chrome', options=None, *args, **kwargs) -> webdriver: 23 | """ 24 | Gets a browser based on the name with the ability to pass in additional arguments 25 | """ 26 | 27 | # get the web driver for the browser 28 | driver_to_use = get_driver(name=name, *args, **kwargs) 29 | 30 | # gets the options for the browser 31 | 32 | options = options or get_default_options(name=name, *args, **kwargs) 33 | 34 | # combines them together into a completed driver 35 | s = service.Service(r"/Users/joshuakim/Downloads/chromedriver-mac-arm64/chromedriver.exe") 36 | 37 | if s: 38 | driver = driver_to_use(service=s, options=options) 39 | else: 40 | driver = driver_to_use(options=options) 41 | 42 | driver.implicitly_wait(config['implicit_wait']) 43 | 44 | return driver 45 | 46 | 47 | def get_driver(name: str = 'chrome', *args, **kwargs) -> webdriver: 48 | """ 49 | Gets the web driver function for the browser 50 | """ 51 | if _clean_name(name) in drivers: 52 | return drivers[name] 53 | 54 | raise UnsupportedBrowserException() 55 | 56 | 57 | # def get_service(name: str = 'chrome'): 58 | # """ 59 | # Gets a service to install the browser driver per webdriver-manager docs 60 | 61 | # https://pypi.org/project/webdriver-manager/ 62 | # """ 63 | # if _clean_name(name) in services: 64 | # return services[name]() 65 | 66 | # return None # Safari doesn't need a service 67 | 68 | 69 | def get_default_options(name: str, *args, **kwargs): 70 | """ 71 | Gets the default options for each browser to help remain undetected 72 | """ 73 | name = _clean_name(name) 74 | 75 | if name in defaults: 76 | return defaults[name](*args, **kwargs) 77 | 78 | raise UnsupportedBrowserException() 79 | 80 | 81 | def chrome_defaults(*args, headless: bool = False, proxy: dict = None, **kwargs) -> ChromeOptions: 82 | """ 83 | Creates Chrome with Options 84 | """ 85 | 86 | options = ChromeOptions() 87 | 88 | ## regular 89 | options.add_argument('--disable-blink-features=AutomationControlled') 90 | options.add_argument('--profile-directory=Default') 91 | 92 | ## experimental 93 | options.add_experimental_option('excludeSwitches', ['enable-automation']) 94 | options.add_experimental_option('useAutomationExtension', False) 95 | 96 | ## add english language to avoid languages translation error 97 | options.add_argument("--lang=en") 98 | 99 | # headless 100 | if headless: 101 | options.add_argument('--headless=new') 102 | if proxy: 103 | if 'user' in proxy.keys() and 'pass' in proxy.keys(): 104 | # This can fail if you are executing the function more than once in the same time 105 | extension_file = 'temp_proxy_auth_extension.zip' 106 | generate_proxy_auth_extension(proxy['host'], proxy['port'], proxy['user'], proxy['pass'], extension_file) 107 | options.add_extension(extension_file) 108 | else: 109 | options.add_argument(f'--proxy-server={proxy["host"]}:{proxy["port"]}') 110 | 111 | return options 112 | 113 | 114 | def firefox_defaults(*args, headless: bool = False, proxy: dict = None, **kwargs) -> FirefoxOptions: 115 | """ 116 | Creates Firefox with default options 117 | """ 118 | 119 | options = FirefoxOptions() 120 | 121 | # default options 122 | 123 | if headless: 124 | options.add_argument('--headless') 125 | if proxy: 126 | raise NotImplementedError('Proxy support is not implemented for this browser') 127 | return options 128 | 129 | 130 | def safari_defaults(*args, headless: bool = False, proxy: dict = None, **kwargs) -> SafariOptions: 131 | """ 132 | Creates Safari with default options 133 | """ 134 | options = SafariOptions() 135 | 136 | # default options 137 | 138 | if headless: 139 | options.add_argument('--headless') 140 | if proxy: 141 | raise NotImplementedError('Proxy support is not implemented for this browser') 142 | return options 143 | 144 | 145 | def edge_defaults(*args, headless: bool = False, proxy: dict = None, **kwargs) -> EdgeOptions: 146 | """ 147 | Creates Edge with default options 148 | """ 149 | options = EdgeOptions() 150 | 151 | # default options 152 | 153 | if headless: 154 | options.add_argument('--headless') 155 | if proxy: 156 | raise NotImplementedError('Proxy support is not implemented for this browser') 157 | return options 158 | 159 | # Misc 160 | class UnsupportedBrowserException(Exception): 161 | """ 162 | Browser is not supported by the library 163 | 164 | Supported browsers are: 165 | - Chrome 166 | - Firefox 167 | - Safari 168 | - Edge 169 | """ 170 | 171 | def __init__(self, message=None): 172 | super().__init__(message or self.__doc__) 173 | 174 | 175 | def _clean_name(name: str) -> str: 176 | """ 177 | Cleans the name of the browser to make it easier to use 178 | """ 179 | return name.strip().lower() 180 | 181 | 182 | drivers = { 183 | 'chrome': webdriver.Chrome, 184 | 'firefox': webdriver.Firefox, 185 | 'safari': webdriver.Safari, 186 | 'edge': webdriver.ChromiumEdge, 187 | } 188 | 189 | defaults = { 190 | 'chrome': chrome_defaults, 191 | 'firefox': firefox_defaults, 192 | 'safari': safari_defaults, 193 | 'edge': edge_defaults, 194 | } 195 | 196 | 197 | services = { 198 | 'chrome': lambda : ChromeService(ChromeDriverManager().install()), 199 | 'firefox': lambda : FirefoxService(GeckoDriverManager().install()), 200 | 'edge': lambda : EdgeService(EdgeChromiumDriverManager().install()), 201 | } 202 | -------------------------------------------------------------------------------- /tiktok_upload/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | CLI is a controller for the command line use of this library 3 | """ 4 | 5 | from argparse import ArgumentParser 6 | from os.path import exists, join 7 | import datetime 8 | import json 9 | 10 | from tiktok_uploader.upload import upload_video 11 | from tiktok_uploader.auth import login_accounts, save_cookies 12 | 13 | def main(): 14 | """ 15 | Passes arguments into the program 16 | """ 17 | args = get_uploader_args() 18 | 19 | args = validate_uploader_args(args=args) 20 | 21 | # parse args 22 | schedule = parse_schedule(args.schedule) 23 | proxy = parse_proxy(args.proxy) 24 | 25 | # runs the program using the arguments provided 26 | result = upload_video( 27 | filename=args.video, 28 | description=args.description, 29 | schedule=schedule, 30 | username=args.username, 31 | password=args.password, 32 | cookies=args.cookies, 33 | proxy=proxy, 34 | sessionid=args.sessionid, 35 | headless=not args.attach, 36 | ) 37 | 38 | print('-------------------------') 39 | if result: 40 | print('Error while uploading video') 41 | else: 42 | print('Video uploaded successfully') 43 | print('-------------------------') 44 | 45 | 46 | def get_uploader_args(): 47 | """ 48 | Generates a parser which is used to get all of the video's information 49 | """ 50 | parser = ArgumentParser( 51 | description='TikTok uploader is a video uploader which can upload a' + 52 | 'video from your computer to the TikTok using selenium automation' 53 | ) 54 | 55 | # primary arguments 56 | parser.add_argument('-v', '--video', help='Video file', required=True) 57 | parser.add_argument('-d', '--description', help='Description', default='') 58 | 59 | # secondary arguments 60 | parser.add_argument('-t', '--schedule', help='Schedule UTC time in %Y-%m-%d %H:%M format ', default=None) 61 | parser.add_argument('--proxy', help='Proxy user:pass@host:port or host:port format', default=None) 62 | 63 | # authentication arguments 64 | parser.add_argument('-c', '--cookies', help='The cookies you want to use') 65 | parser.add_argument('-s', '--sessionid', help='The session id you want to use') 66 | 67 | parser.add_argument('-u', '--username', help='Your TikTok email / username') 68 | parser.add_argument('-p', '--password', help='Your TikTok password') 69 | 70 | # selenium arguments 71 | parser.add_argument('--attach', '-a', action='store_true', default=False, 72 | help='Runs the program in headless mode (no browser window)') 73 | 74 | return parser.parse_args() 75 | 76 | 77 | def validate_uploader_args(args: dict): 78 | """ 79 | Preforms validation on each input given 80 | """ 81 | 82 | # Makes sure the video file exists 83 | if not exists(args.video): 84 | raise FileNotFoundError(f'Could not find the video file at {args["video"]}') 85 | 86 | # User can not pass in both cookies and username / password 87 | if args.cookies and (args.username or args.password): 88 | raise ValueError('You can not pass in both cookies and username / password') 89 | 90 | return args 91 | 92 | 93 | def auth(): 94 | """ 95 | Authenticates the user 96 | """ 97 | args = get_auth_args() 98 | args = validate_auth_args(args=args) 99 | 100 | # runs the program using the arguments provided 101 | if args.input: 102 | login_info = get_login_info(path=args.input, header=args.header) 103 | else: 104 | login_info = [(args.username, args.password)] 105 | 106 | username_and_cookies = login_accounts(accounts=login_info) 107 | 108 | for username, cookies in username_and_cookies.items(): 109 | save_cookies(path=join(args.output, username + '.txt'), cookies=cookies) 110 | 111 | 112 | def get_auth_args(): 113 | """ 114 | Generates a parser which is used to get all of the authentication information 115 | """ 116 | parser = ArgumentParser( 117 | description='TikTok Auth is a program which can log you into multiple accounts sequentially' 118 | ) 119 | 120 | # authentication arguments 121 | parser.add_argument('-o', '--output', default='tmp', 122 | help='The output folder to save the cookies to') 123 | parser.add_argument('-i', '--input', help='A csv file with username and password') 124 | # parser.add_argument('-h', '--header', default=True, 125 | # help='The header of the csv file which contains the username and password') 126 | parser.add_argument('-u', '--username', help='Your TikTok email / username') 127 | parser.add_argument('-p', '--password', help='Your TikTok password') 128 | 129 | return parser.parse_args() 130 | 131 | def validate_auth_args(args): 132 | """ 133 | Preforms validation on each input given 134 | """ 135 | # username and password or input files are mutually exclusive 136 | if (args['username'] and args['password']) and args['input']: 137 | raise ValueError('You can not pass in both username / password and input file') 138 | 139 | return args 140 | 141 | 142 | def get_login_info(path: str, header=True) -> list: 143 | """ 144 | Parses the input file into a list of usernames and passwords 145 | """ 146 | with open(path, 'r', encoding='utf-8') as file: 147 | file = file.readlines() 148 | if header: 149 | file = file[1:] 150 | return [line.split(',')[:2] for line in file] 151 | 152 | 153 | def parse_schedule(schedule_raw): 154 | if schedule_raw: 155 | schedule = datetime.datetime.strptime(schedule_raw, '%Y-%m-%d %H:%M') 156 | else: 157 | schedule = None 158 | return schedule 159 | 160 | 161 | def parse_proxy(proxy_raw): 162 | proxy = {} 163 | if proxy_raw: 164 | if '@' in proxy_raw: 165 | proxy['user'] = proxy_raw.split('@')[0].split(':')[0] 166 | proxy['pass'] = proxy_raw.split('@')[0].split(':')[1] 167 | proxy['host'] = proxy_raw.split('@')[1].split(':')[0] 168 | proxy['port'] = proxy_raw.split('@')[1].split(':')[1] 169 | else: 170 | proxy['host'] = proxy_raw.split(':')[0] 171 | proxy['port'] = proxy_raw.split(':')[1] 172 | return proxy 173 | -------------------------------------------------------------------------------- /tiktok_upload/config.toml: -------------------------------------------------------------------------------- 1 | # TikTok Uploader Default Configuation File 2 | 3 | headless = true 4 | quit_on_end = true 5 | 6 | # Messing around with inputs 7 | valid_path_names = ["path", "filename", "video", "video_path"] 8 | valid_descriptions = ["description", "desc", "caption"] 9 | 10 | # Selenium Webdriver Waits 11 | implicit_wait = 5 # seconds 12 | explicit_wait = 60 # seconds 13 | 14 | supported_file_types = ["mp4", "mov", "avi", "wmv", "flv", "webm", "mkv", "m4v", "3gp", "3g2", "gif"] 15 | 16 | max_description_length = 150 # characters 17 | 18 | [paths] 19 | main = "https://www.tiktok.com/" 20 | login = "https://www.tiktok.com/login/phone-or-email/email" 21 | upload = "https://www.tiktok.com/creator-center/upload?lang=en" 22 | 23 | [disguising] 24 | user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3' 25 | 26 | [selectors] # Selenium XPATH selectors 27 | 28 | [selectors.login] 29 | username_field = "//input[@name=\"username\"]" 30 | password_field = "//input[@type=\"password\"]" 31 | login_button = "//button[@type=\"submit\"]" 32 | 33 | alert_user_if_failed = true # sends an alert and waits instead of failing 34 | 35 | cookie_of_interest = "sessionid" # cookie to check if login was successful 36 | 37 | [selectors.upload] 38 | iframe = "//iframe" 39 | 40 | split_window = "//button[./div[text()='Not now']]" 41 | upload_video = "//input[@type='file']" 42 | upload_finished = "//div[contains(@class, 'btn-cancel')]" 43 | upload_confirmation = "//video" 44 | process_confirmation = "//img[@draggable='false']" 45 | 46 | description = "//div[@contenteditable='true']" 47 | 48 | visibility = "//div[@class='tiktok-select-selector']" 49 | options = ["Public", "Friends", "Private"] 50 | 51 | hashtags = "//div[@class='mentionSuggestions']//*[contains(text(), '{}')]" 52 | mentions = "//div[contains(concat(' ', normalize-space(@class), ' '), 'user-id') and .='{}']/.." 53 | 54 | mention_box = "//input[contains(concat(' ', normalize-space(@class), ' '), 'search-friends')]" 55 | 56 | comment = "//label[.='Comment']/following-sibling::div/input" 57 | duet = "//label[.='Duet']/following-sibling::div/input" 58 | stitch = "//label[.='Stitch']/following-sibling::div/input" 59 | 60 | post = "//div[contains(@class, 'btn-post')]" 61 | post_confirmation = "//div[contains(@class, 'tiktok-modal__modal-button')]" 62 | 63 | [selectors.schedule] 64 | switch = "//*[@id='tux-3']" 65 | 66 | date_picker = "//div[contains(@class, 'date-picker-input')]" 67 | calendar = "//div[contains(@class, 'calendar-wrapper')]" 68 | calendar_month = "//span[contains(@class, 'month-title')]" 69 | calendar_valid_days = "//div[@class='jsx-4172176419 days-wrapper']//span[contains(@class, 'day') and contains(@class, 'valid')]" 70 | calendar_arrows = "//span[contains(@class, 'arrow')]" # first last, second next 71 | 72 | time_picker = "//div[contains(@class, 'time-picker-input')]" 73 | time_picker_text = "//div[contains(@class, 'time-picker-input')]/*[1]" 74 | time_picker_container = "//div[@class='tiktok-timepicker-time-picker-container']" 75 | timepicker_hours = "//span[contains(@class, 'tiktok-timepicker-left')]" 76 | timepicker_minutes = "//span[contains(@class, 'tiktok-timepicker-right')]" 77 | 78 | [selectors.upload.cookies_banner] 79 | banner = "tiktok-cookie-banner" 80 | button = "div.button-wrapper" 81 | -------------------------------------------------------------------------------- /tiktok_upload/proxy_auth_extension/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvinniev34/RedditReels/a1d9ffa625a22abbffb44505a5ca10cb11cea253/tiktok_upload/proxy_auth_extension/__init__.py -------------------------------------------------------------------------------- /tiktok_upload/proxy_auth_extension/background.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | mode: "fixed_servers", 3 | rules: { 4 | singleProxy: { 5 | scheme: "http", 6 | host: "{{ proxy_host }}", 7 | port: parseInt("{{ proxy_port }}") 8 | }, 9 | bypassList: ["localhost"] 10 | } 11 | }; 12 | 13 | chrome.proxy.settings.set({value: config, scope: "regular"}, function() {}); 14 | 15 | function callbackFn(details) { 16 | return { 17 | authCredentials: { 18 | username: "{{ proxy_user }}", 19 | password: "{{ proxy_pass }}" 20 | } 21 | }; 22 | } 23 | 24 | chrome.webRequest.onAuthRequired.addListener( 25 | callbackFn, 26 | {urls: [""]}, 27 | ['blocking'] 28 | ); 29 | -------------------------------------------------------------------------------- /tiktok_upload/proxy_auth_extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "manifest_version": 2, 4 | "name": "Chrome Proxy", 5 | "permissions": [ 6 | "proxy", 7 | "tabs", 8 | "unlimitedStorage", 9 | "storage", 10 | "", 11 | "webRequest", 12 | "webRequestBlocking" 13 | ], 14 | "background": { 15 | "scripts": ["background.js"] 16 | }, 17 | "minimum_chrome_version":"22.0.0" 18 | } -------------------------------------------------------------------------------- /tiktok_upload/proxy_auth_extension/proxy_auth_extension.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | import os 3 | import json 4 | from selenium.webdriver.common.by import By 5 | 6 | 7 | def replace_variables_in_js(js_content: str, variables_dict: dict): 8 | for variable, value in variables_dict.items(): 9 | js_content = js_content.replace('{{ ' + variable + ' }}', value) 10 | return js_content 11 | 12 | 13 | def generate_proxy_auth_extension( 14 | proxy_host: str, proxy_port: str, proxy_user: str, proxy_pass: str, 15 | extension_file: str): 16 | """Generate a Chrome extension that modify proxy settings based on desired host, port, username and password. 17 | 18 | If you are using --headless in chromedriver, you must use --headless=new to support extensions in headless mode. 19 | """ 20 | current_dir = os.path.dirname(os.path.abspath(__file__)) 21 | manifest_json_path = os.path.join(current_dir, 'manifest.json') 22 | background_js_path = os.path.join(current_dir, 'background.js') 23 | with open(manifest_json_path, 'r', encoding='utf-8') as f: 24 | manifest_json = f.read() 25 | with open(background_js_path, 'r', encoding='utf-8') as f: 26 | background_js = f.read() 27 | 28 | variables_dict = { 29 | 'proxy_host': proxy_host, 30 | 'proxy_port': proxy_port, 31 | 'proxy_user': proxy_user, 32 | 'proxy_pass': proxy_pass 33 | } 34 | background_js = replace_variables_in_js(background_js, variables_dict) 35 | 36 | with zipfile.ZipFile(extension_file, 'w') as zp: 37 | zp.writestr('manifest.json', manifest_json) 38 | zp.writestr('background.js', background_js) 39 | 40 | 41 | def get_my_ip(driver): 42 | origin_tab = driver.current_window_handle 43 | driver.execute_script("window.open('', '_blank');") 44 | driver.switch_to.window(driver.window_handles[-1]) 45 | 46 | driver.get('https://api.ipify.org/?format=json') 47 | 48 | ip_row = driver.find_element(By.XPATH, '//body').text 49 | ip = json.loads(ip_row)['ip'] 50 | 51 | driver.close() 52 | driver.switch_to.window(origin_tab) 53 | 54 | return ip 55 | 56 | 57 | def proxy_is_working(driver, host: str): 58 | ip = get_my_ip(driver) 59 | 60 | if ip == host: 61 | return True 62 | else: 63 | return False 64 | -------------------------------------------------------------------------------- /tiktok_upload/upload.py: -------------------------------------------------------------------------------- 1 | """ 2 | `tiktok_uploader` module for uploading videos to TikTok 3 | 4 | Key Functions 5 | ------------- 6 | upload_video : Uploads a single TikTok video 7 | upload_videos : Uploads multiple TikTok videos 8 | """ 9 | from os.path import abspath, exists 10 | from typing import List 11 | import time 12 | import pytz 13 | import datetime 14 | 15 | from selenium.webdriver.common.by import By 16 | 17 | from selenium.webdriver.common.action_chains import ActionChains 18 | from selenium.webdriver.support.ui import WebDriverWait 19 | from selenium.webdriver.support import expected_conditions as EC 20 | from selenium.webdriver.common.keys import Keys 21 | from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException 22 | 23 | from tiktok_uploader.browsers import get_browser 24 | from tiktok_uploader.auth import AuthBackend 25 | from tiktok_uploader import config, logger 26 | from tiktok_uploader.utils import bold, green, red 27 | from tiktok_uploader.proxy_auth_extension.proxy_auth_extension import proxy_is_working 28 | 29 | 30 | def upload_video(filename=None, description='', cookies='', schedule: datetime.datetime = None, username='', 31 | password='', sessionid=None, cookies_list=None, cookies_str=None, proxy=None, *args, **kwargs): 32 | """ 33 | Uploads a single TikTok video. 34 | 35 | Consider using `upload_videos` if using multiple videos 36 | 37 | Parameters 38 | ---------- 39 | filename : str 40 | The path to the video to upload 41 | description : str 42 | The description to set for the video 43 | schedule: datetime.datetime 44 | The datetime to schedule the video, must be naive or aware with UTC timezone, if naive it will be aware with UTC timezone 45 | cookies : str 46 | The cookies to use for uploading 47 | sessionid: str 48 | The `sessionid` is the only required cookie for uploading, 49 | but it is recommended to use all cookies to avoid detection 50 | """ 51 | auth = AuthBackend(username=username, password=password, cookies=cookies, 52 | cookies_list=cookies_list, cookies_str=cookies_str, sessionid=sessionid) 53 | 54 | return upload_videos( 55 | videos=[ { 'path': filename, 'description': description, 'schedule': schedule } ], 56 | auth=auth, 57 | proxy=proxy, 58 | *args, **kwargs 59 | ) 60 | 61 | 62 | def upload_videos(videos: list = None, auth: AuthBackend = None, proxy: dict = None, browser='chrome', 63 | browser_agent=None, on_complete=None, headless=False, num_retries : int = 1, *args, **kwargs): 64 | """ 65 | Uploads multiple videos to TikTok 66 | 67 | Parameters 68 | ---------- 69 | videos : list 70 | A list of dictionaries containing the video's ('path') and description ('description') 71 | proxy: dict 72 | A dictionary containing the proxy user, pass, host and port 73 | browser : str 74 | The browser to use for uploading 75 | browser_agent : selenium.webdriver 76 | A selenium webdriver object to use for uploading 77 | on_complete : function 78 | A function to call when the upload is complete 79 | headless : bool 80 | Whether or not the browser should be run in headless mode 81 | num_retries : int 82 | The number of retries to attempt if the upload fails 83 | options : SeleniumOptions 84 | The options to pass into the browser -> custom privacy settings, etc. 85 | *args : 86 | Additional arguments to pass into the upload function 87 | **kwargs : 88 | Additional keyword arguments to pass into the upload function 89 | 90 | Returns 91 | ------- 92 | failed : list 93 | A list of videos which failed to upload 94 | """ 95 | videos = _convert_videos_dict(videos) 96 | 97 | if videos and len(videos) > 1: 98 | logger.debug("Uploading %d videos", len(videos)) 99 | 100 | if not browser_agent: # user-specified browser agent 101 | logger.debug('Create a %s browser instance %s', browser, 102 | 'in headless mode' if headless else '') 103 | driver = get_browser(name=browser, headless=headless, proxy=proxy, *args, **kwargs) 104 | else: 105 | logger.debug('Using user-defined browser agent') 106 | driver = browser_agent 107 | if proxy: 108 | if proxy_is_working(driver, proxy['host']): 109 | logger.debug(green('Proxy is working')) 110 | else: 111 | logger.error('Proxy is not working') 112 | driver.quit() 113 | raise Exception('Proxy is not working') 114 | driver = auth.authenticate_agent(driver) 115 | 116 | failed = [] 117 | # uploads each video 118 | for video in videos: 119 | try: 120 | path = abspath(video.get('path')) 121 | description = video.get('description', '') 122 | schedule = video.get('schedule', None) 123 | 124 | logger.debug('Posting %s%s', bold(video.get('path')), 125 | f'\n{" " * 15}with description: {bold(description)}' if description else '') 126 | 127 | # Video must be of supported type 128 | if not _check_valid_path(path): 129 | print(f'{path} is invalid, skipping') 130 | failed.append(video) 131 | continue 132 | 133 | # Video must have a valid datetime for tiktok's scheduler 134 | if schedule: 135 | timezone = pytz.UTC 136 | if schedule.tzinfo is None: 137 | schedule = schedule.astimezone(timezone) 138 | elif int(schedule.utcoffset().total_seconds()) == 0: # Equivalent to UTC 139 | schedule = timezone.localize(schedule) 140 | else: 141 | print(f'{schedule} is invalid, the schedule datetime must be naive or aware with UTC timezone, skipping') 142 | failed.append(video) 143 | continue 144 | 145 | valid_tiktok_minute_multiple = 5 146 | schedule = _get_valid_schedule_minute(schedule, valid_tiktok_minute_multiple) 147 | if not _check_valid_schedule(schedule): 148 | print(f'{schedule} is invalid, the schedule datetime must be as least 20 minutes in the future, and a maximum of 10 days, skipping') 149 | failed.append(video) 150 | continue 151 | 152 | complete_upload_form(driver, path, description, schedule, 153 | num_retries=num_retries, headless=headless, 154 | *args, **kwargs) 155 | except Exception as exception: 156 | logger.error('Failed to upload %s', path) 157 | logger.error(exception) 158 | failed.append(video) 159 | 160 | if on_complete is callable: # calls the user-specified on-complete function 161 | on_complete(video) 162 | 163 | if config['quit_on_end']: 164 | driver.quit() 165 | 166 | return failed 167 | 168 | 169 | def complete_upload_form(driver, path: str, description: str, schedule: datetime.datetime, headless=False, *args, **kwargs) -> None: 170 | """ 171 | Actually uploads each video 172 | 173 | Parameters 174 | ---------- 175 | driver : selenium.webdriver 176 | The selenium webdriver to use for uploading 177 | path : str 178 | The path to the video to upload 179 | """ 180 | _go_to_upload(driver) 181 | # _remove_cookies_window(driver) 182 | time.sleep(5) 183 | _set_video(driver, path=path, **kwargs) 184 | time.sleep(5) 185 | _remove_split_window(driver) 186 | time.sleep(5) 187 | _set_interactivity(driver, **kwargs) 188 | time.sleep(5) 189 | _set_description(driver, description) 190 | time.sleep(5) 191 | if schedule: 192 | _set_schedule_video(driver, schedule) 193 | time.sleep(5) 194 | _post_video(driver) 195 | 196 | 197 | def _go_to_upload(driver) -> None: 198 | """ 199 | Navigates to the upload page, switches to the iframe and waits for it to load 200 | 201 | Parameters 202 | ---------- 203 | driver : selenium.webdriver 204 | """ 205 | logger.debug(green('Navigating to upload page')) 206 | 207 | # if the upload page is not open, navigate to it 208 | if driver.current_url != config['paths']['upload']: 209 | driver.get(config['paths']['upload']) 210 | # otherwise, refresh the page and accept the reload alert 211 | else: 212 | _refresh_with_alert(driver) 213 | 214 | # changes to the iframe 215 | _change_to_upload_iframe(driver) 216 | 217 | # waits for the iframe to load 218 | root_selector = EC.presence_of_element_located((By.ID, 'root')) 219 | WebDriverWait(driver, config['explicit_wait']).until(root_selector) 220 | 221 | # Return to default webpage 222 | driver.switch_to.default_content() 223 | 224 | def _change_to_upload_iframe(driver) -> None: 225 | """ 226 | Switch to the iframe of the upload page 227 | 228 | Parameters 229 | ---------- 230 | driver : selenium.webdriver 231 | """ 232 | iframe_selector = EC.presence_of_element_located( 233 | (By.XPATH, config['selectors']['upload']['iframe']) 234 | ) 235 | iframe = WebDriverWait(driver, config['explicit_wait']).until(iframe_selector) 236 | driver.switch_to.frame(iframe) 237 | 238 | def _set_description(driver, description: str) -> None: 239 | """ 240 | Sets the description of the video 241 | 242 | Parameters 243 | ---------- 244 | driver : selenium.webdriver 245 | description : str 246 | The description to set 247 | """ 248 | if description is None: 249 | # if no description is provided, filename 250 | return 251 | 252 | logger.debug(green('Setting description')) 253 | 254 | # Remove any characters outside the BMP range (emojis, etc) & Fix accents 255 | description = description.encode('utf-8', 'ignore').decode('utf-8') 256 | 257 | saved_description = description # save the description in case it fails 258 | desc = WebDriverWait(driver, 10).until( 259 | EC.presence_of_element_located((By.XPATH, config['selectors']['upload']['description'])) 260 | ) 261 | # desc populates with filename before clearing 262 | WebDriverWait(driver, config['explicit_wait']).until(lambda driver: desc.text != '') 263 | 264 | _clear(desc) 265 | 266 | try: 267 | while description: 268 | nearest_mention = description.find('@') 269 | nearest_hash = description.find('#') 270 | 271 | if nearest_mention == 0 or nearest_hash == 0: 272 | desc.send_keys('@' if nearest_mention == 0 else '#') 273 | 274 | name = description[1:].split(' ')[0] 275 | if nearest_mention == 0: # @ case 276 | mention_xpath = config['selectors']['upload']['mention_box'] 277 | condition = EC.presence_of_element_located((By.XPATH, mention_xpath)) 278 | mention_box = WebDriverWait(driver, config['explicit_wait']).until(condition) 279 | mention_box.send_keys(name) 280 | else: 281 | desc.send_keys(name) 282 | 283 | time.sleep(config['implicit_wait']) 284 | 285 | if nearest_mention == 0: # @ case 286 | time.sleep(2) 287 | mention_xpath = config['selectors']['upload']['mentions'].format('@' + name) 288 | condition = EC.presence_of_element_located((By.XPATH, mention_xpath)) 289 | else: 290 | time.sleep(2) 291 | hashtag_xpath = config['selectors']['upload']['hashtags'].format(name) 292 | condition = EC.presence_of_element_located((By.XPATH, hashtag_xpath)) 293 | 294 | # if the element never appears (timeout exception) remove the tag and continue 295 | try: 296 | elem = WebDriverWait(driver, config['implicit_wait']).until(condition) 297 | except: 298 | desc.send_keys(Keys.BACKSPACE * (len(name) + 1)) 299 | description = description[len(name) + 2:] 300 | continue 301 | 302 | ActionChains(driver).move_to_element(elem).click(elem).perform() 303 | 304 | description = description[len(name) + 2:] 305 | else: 306 | min_index = _get_splice_index(nearest_mention, nearest_hash, description) 307 | 308 | desc.send_keys(description[:min_index]) 309 | description = description[min_index:] 310 | except Exception as exception: 311 | print('Failed to set description: ', exception) 312 | _clear(desc) 313 | desc.send_keys(saved_description) # if fail, use saved description 314 | 315 | 316 | def _clear(element) -> None: 317 | """ 318 | Clears the text of the element (an issue with the TikTok website when automating) 319 | 320 | Parameters 321 | ---------- 322 | element 323 | The text box to clear 324 | """ 325 | element.send_keys(2 * len(element.text) * Keys.BACKSPACE) 326 | 327 | 328 | def _set_video(driver, path: str = '', num_retries: int = 3, **kwargs) -> None: 329 | """ 330 | Sets the video to upload 331 | 332 | Parameters 333 | ---------- 334 | driver : selenium.webdriver 335 | path : str 336 | The path to the video to upload 337 | num_retries : number of retries (can occasionally fail) 338 | """ 339 | # uploads the element 340 | logger.debug(green('Uploading video file')) 341 | 342 | for _ in range(num_retries): 343 | try: 344 | _change_to_upload_iframe(driver) 345 | upload_box = driver.find_element( 346 | By.XPATH, config['selectors']['upload']['upload_video'] 347 | ) 348 | upload_box.send_keys(path) 349 | # waits for the upload progress bar to disappear 350 | upload_finished = EC.presence_of_element_located( 351 | (By.XPATH, config['selectors']['upload']['upload_finished']) 352 | ) 353 | 354 | WebDriverWait(driver, config['explicit_wait']).until(upload_finished) 355 | 356 | # waits for the video to upload 357 | upload_confirmation = EC.presence_of_element_located( 358 | (By.XPATH, config['selectors']['upload']['upload_confirmation']) 359 | ) 360 | 361 | # An exception throw here means the video failed to upload an a retry is needed 362 | WebDriverWait(driver, config['explicit_wait']).until(upload_confirmation) 363 | 364 | # wait until a non-draggable image is found 365 | process_confirmation = EC.presence_of_element_located( 366 | (By.XPATH, config['selectors']['upload']['process_confirmation']) 367 | ) 368 | WebDriverWait(driver, config['explicit_wait']).until(process_confirmation) 369 | return 370 | except Exception as exception: 371 | print(exception) 372 | 373 | raise FailedToUpload() 374 | 375 | def _remove_cookies_window(driver) -> None: 376 | """ 377 | Removes the cookies window if it is open 378 | 379 | Parameters 380 | ---------- 381 | driver : selenium.webdriver 382 | """ 383 | 384 | logger.debug(green(f'Removing cookies window')) 385 | cookies_banner = WebDriverWait(driver, config['implicit_wait']).until( 386 | EC.presence_of_element_located((By.TAG_NAME, config['selectors']['upload']['cookies_banner']['banner']))) 387 | 388 | item = WebDriverWait(driver, config['implicit_wait']).until( 389 | EC.visibility_of(cookies_banner.shadow_root.find_element(By.CSS_SELECTOR, config['selectors']['upload']['cookies_banner']['button']))) 390 | 391 | # Wait that the Decline all button is clickable 392 | decline_button = WebDriverWait(driver, config['implicit_wait']).until( 393 | EC.element_to_be_clickable(item.find_elements(By.TAG_NAME, 'button')[0])) 394 | 395 | decline_button.click() 396 | 397 | def _remove_split_window(driver) -> None: 398 | """ 399 | Remove the split window if it is open 400 | 401 | Parameters 402 | ---------- 403 | driver : selenium.webdriver 404 | """ 405 | logger.debug(green(f'Removing split window')) 406 | window_xpath = config['selectors']['upload']['split_window'] 407 | 408 | try: 409 | condition = EC.presence_of_element_located((By.XPATH, window_xpath)) 410 | window = WebDriverWait(driver, config['implicit_wait']).until(condition) 411 | window.click() 412 | 413 | except TimeoutException: 414 | logger.debug(red(f"Split window not found or operation timed out")) 415 | 416 | 417 | def _set_interactivity(driver, comment=True, stitch=True, duet=True, *args, **kwargs) -> None: 418 | """ 419 | Sets the interactivity settings of the video 420 | 421 | Parameters 422 | ---------- 423 | driver : selenium.webdriver 424 | comment : bool 425 | Whether or not to allow comments 426 | stitch : bool 427 | Whether or not to allow stitching 428 | duet : bool 429 | Whether or not to allow duets 430 | """ 431 | try: 432 | logger.debug(green('Setting interactivity settings')) 433 | 434 | comment_box = driver.find_element(By.XPATH, config['selectors']['upload']['comment']) 435 | stitch_box = driver.find_element(By.XPATH, config['selectors']['upload']['stitch']) 436 | duet_box = driver.find_element(By.XPATH, config['selectors']['upload']['duet']) 437 | 438 | # xor the current state with the desired state 439 | if comment ^ comment_box.is_selected(): 440 | comment_box.click() 441 | 442 | if stitch ^ stitch_box.is_selected(): 443 | stitch_box.click() 444 | 445 | if duet ^ duet_box.is_selected(): 446 | duet_box.click() 447 | 448 | except Exception as _: 449 | logger.error('Failed to set interactivity settings') 450 | 451 | 452 | def _set_schedule_video(driver, schedule: datetime.datetime) -> None: 453 | """ 454 | Sets the schedule of the video 455 | 456 | Parameters 457 | ---------- 458 | driver : selenium.webdriver 459 | schedule : datetime.datetime 460 | The datetime to set 461 | """ 462 | 463 | logger.debug(green('Setting schedule')) 464 | 465 | driver_timezone = __get_driver_timezone(driver) 466 | schedule = schedule.astimezone(driver_timezone) 467 | 468 | month = schedule.month 469 | day = schedule.day 470 | hour = schedule.hour 471 | minute = schedule.minute 472 | 473 | try: 474 | time.sleep(5) 475 | switch = driver.find_element(By.XPATH, config['selectors']['schedule']['switch']) 476 | switch.click() 477 | time.sleep(5) 478 | __date_picker(driver, month, day) 479 | time.sleep(5) 480 | __time_picker(driver, hour, minute) 481 | except Exception as e: 482 | msg = f'Failed to set schedule: {e}' 483 | print(msg) 484 | logger.error(msg) 485 | raise FailedToUpload() 486 | 487 | 488 | 489 | def __date_picker(driver, month: int, day: int) -> None: 490 | logger.debug(green('Picking date')) 491 | 492 | condition = EC.presence_of_element_located( 493 | (By.XPATH, config['selectors']['schedule']['date_picker']) 494 | ) 495 | date_picker = WebDriverWait(driver, config['implicit_wait']).until(condition) 496 | date_picker.click() 497 | 498 | condition = EC.presence_of_element_located( 499 | (By.XPATH, config['selectors']['schedule']['calendar']) 500 | ) 501 | calendar = WebDriverWait(driver, config['implicit_wait']).until(condition) 502 | 503 | calendar_month = driver.find_element(By.XPATH, config['selectors']['schedule']['calendar_month']).text 504 | n_calendar_month = datetime.datetime.strptime(calendar_month, '%B').month 505 | if n_calendar_month != month: # Max can be a month before or after 506 | if n_calendar_month < month: 507 | arrow = driver.find_elements(By.XPATH, config['selectors']['schedule']['calendar_arrows'])[-1] 508 | else: 509 | arrow = driver.find_elements(By.XPATH, config['selectors']['schedule']['calendar_arrows'])[0] 510 | arrow.click() 511 | valid_days = driver.find_elements(By.XPATH, config['selectors']['schedule']['calendar_valid_days']) 512 | 513 | day_to_click = None 514 | for day_option in valid_days: 515 | if int(day_option.text) == day: 516 | day_to_click = day_option 517 | break 518 | if day_to_click: 519 | day_to_click.click() 520 | else: 521 | raise Exception('Day not found in calendar') 522 | 523 | __verify_date_picked_is_correct(driver, month, day) 524 | 525 | 526 | def __verify_date_picked_is_correct(driver, month: int, day: int): 527 | date_selected = driver.find_element(By.XPATH, config['selectors']['schedule']['date_picker']).text 528 | date_selected_month = int(date_selected.split('-')[1]) 529 | date_selected_day = int(date_selected.split('-')[2]) 530 | 531 | if date_selected_month == month and date_selected_day == day: 532 | logger.debug(green('Date picked correctly')) 533 | else: 534 | msg = f'Something went wrong with the date picker, expected {month}-{day} but got {date_selected_month}-{date_selected_day}' 535 | logger.error(msg) 536 | raise Exception(msg) 537 | 538 | 539 | def __time_picker(driver, hour: int, minute: int) -> None: 540 | logger.debug(green('Picking time')) 541 | 542 | condition = EC.presence_of_element_located( 543 | (By.XPATH, config['selectors']['schedule']['time_picker']) 544 | ) 545 | time_picker = WebDriverWait(driver, config['implicit_wait']).until(condition) 546 | time_picker.click() 547 | time.sleep(5) 548 | 549 | condition = EC.presence_of_element_located( 550 | (By.XPATH, config['selectors']['schedule']['time_picker_container']) 551 | ) 552 | time_picker_container = WebDriverWait(driver, config['implicit_wait']).until(condition) 553 | 554 | # 00 = 0, 01 = 1, 02 = 2, 03 = 3, 04 = 4, 05 = 5, 06 = 6, 07 = 7, 08 = 8, 09 = 9, 10 = 10, 11 = 11, 12 = 12, 555 | # 13 = 13, 14 = 14, 15 = 15, 16 = 16, 17 = 17, 18 = 18, 19 = 19, 20 = 20, 21 = 21, 22 = 22, 23 = 23 556 | hour_options = driver.find_elements(By.XPATH, config['selectors']['schedule']['timepicker_hours']) 557 | # 00 == 0, 05 == 1, 10 == 2, 15 == 3, 20 == 4, 25 == 5, 30 == 6, 35 == 7, 40 == 8, 45 == 9, 50 == 10, 55 == 11 558 | minute_options = driver.find_elements(By.XPATH, config['selectors']['schedule']['timepicker_minutes']) 559 | 560 | hour_to_click = hour_options[hour] 561 | minute_option_correct_index = int(minute / 5) 562 | minute_to_click = minute_options[minute_option_correct_index] 563 | 564 | driver.execute_script("arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});", hour_to_click) 565 | hour_to_click.click() 566 | driver.execute_script("arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});", minute_to_click) 567 | minute_to_click.click() 568 | 569 | # click somewhere else to close the time picker 570 | time_picker.click() 571 | 572 | time.sleep(.5) # wait for the DOM change 573 | __verify_time_picked_is_correct(driver, hour, minute) 574 | 575 | 576 | def __verify_time_picked_is_correct(driver, hour: int, minute: int): 577 | time_selected = driver.find_element(By.XPATH, config['selectors']['schedule']['time_picker_text']).text 578 | time_selected_hour = int(time_selected.split(':')[0]) 579 | time_selected_minute = int(time_selected.split(':')[1]) 580 | 581 | if time_selected_hour == hour and time_selected_minute == minute: 582 | logger.debug(green('Time picked correctly')) 583 | else: 584 | msg = f'Something went wrong with the time picker, ' \ 585 | f'expected {hour:02d}:{minute:02d} ' \ 586 | f'but got {time_selected_hour:02d}:{time_selected_minute:02d}' 587 | logger.error(msg) 588 | raise Exception(msg) 589 | 590 | 591 | def _post_video(driver) -> None: 592 | """ 593 | Posts the video by clicking the post button 594 | 595 | Parameters 596 | ---------- 597 | driver : selenium.webdriver 598 | """ 599 | logger.debug(green('Clicking the post button')) 600 | 601 | try: 602 | post = WebDriverWait(driver, config['implicit_wait']).until(EC.element_to_be_clickable((By.XPATH, config['selectors']['upload']['post']))) 603 | post.click() 604 | except ElementClickInterceptedException: 605 | logger.debug(green("Trying to click on the button again")) 606 | driver.execute_script('document.querySelector(".btn-post > button").click()') 607 | 608 | # waits for the video to upload 609 | post_confirmation = EC.presence_of_element_located( 610 | (By.XPATH, config['selectors']['upload']['post_confirmation']) 611 | ) 612 | WebDriverWait(driver, config['explicit_wait']).until(post_confirmation) 613 | 614 | logger.debug(green('Video posted successfully')) 615 | 616 | 617 | # HELPERS 618 | 619 | def _check_valid_path(path: str) -> bool: 620 | """ 621 | Returns whether or not the filetype is supported by TikTok 622 | """ 623 | return exists(path) and path.split('.')[-1] in config['supported_file_types'] 624 | 625 | 626 | def _get_valid_schedule_minute(schedule, valid_multiple) -> datetime.datetime: 627 | """ 628 | Returns a datetime.datetime with valid minute for TikTok 629 | """ 630 | if _is_valid_schedule_minute(schedule.minute, valid_multiple): 631 | return schedule 632 | else: 633 | return _set_valid_schedule_minute(schedule, valid_multiple) 634 | 635 | 636 | def _is_valid_schedule_minute(minute, valid_multiple) -> bool: 637 | if minute % valid_multiple != 0: 638 | return False 639 | else: 640 | return True 641 | 642 | 643 | def _set_valid_schedule_minute(schedule, valid_multiple) -> datetime.datetime: 644 | minute = schedule.minute 645 | 646 | remainder = minute % valid_multiple 647 | integers_to_valid_multiple = 5 - remainder 648 | schedule += datetime.timedelta(minutes=integers_to_valid_multiple) 649 | 650 | return schedule 651 | 652 | 653 | def _check_valid_schedule(schedule: datetime.datetime) -> bool: 654 | """ 655 | Returns if the schedule is supported by TikTok 656 | """ 657 | valid_tiktok_minute_multiple = 5 658 | margin_to_complete_upload_form = 5 659 | 660 | datetime_utc_now = pytz.UTC.localize(datetime.datetime.utcnow()) 661 | min_datetime_tiktok_valid = datetime_utc_now + datetime.timedelta(minutes=15) 662 | min_datetime_tiktok_valid += datetime.timedelta(minutes=margin_to_complete_upload_form) 663 | max_datetime_tiktok_valid = datetime_utc_now + datetime.timedelta(days=10) 664 | if schedule < min_datetime_tiktok_valid \ 665 | or schedule > max_datetime_tiktok_valid: 666 | return False 667 | elif not _is_valid_schedule_minute(schedule.minute, valid_tiktok_minute_multiple): 668 | return False 669 | else: 670 | return True 671 | 672 | 673 | def _get_splice_index(nearest_mention: int, nearest_hashtag: int, description: str) -> int: 674 | """ 675 | Returns the index to splice the description at 676 | 677 | Parameters 678 | ---------- 679 | nearest_mention : int 680 | The index of the nearest mention 681 | nearest_hashtag : int 682 | The index of the nearest hashtag 683 | 684 | Returns 685 | ------- 686 | int 687 | The index to splice the description at 688 | """ 689 | if nearest_mention == -1 and nearest_hashtag == -1: 690 | return len(description) 691 | elif nearest_hashtag == -1: 692 | return nearest_mention 693 | elif nearest_mention == -1: 694 | return nearest_hashtag 695 | else: 696 | return min(nearest_mention, nearest_hashtag) 697 | 698 | def _convert_videos_dict(videos_list_of_dictionaries) -> List: 699 | """ 700 | Takes in a videos dictionary and converts it. 701 | 702 | This allows the user to use the wrong stuff and thing to just work 703 | """ 704 | if not videos_list_of_dictionaries: 705 | raise RuntimeError("No videos to upload") 706 | 707 | valid_path = config['valid_path_names'] 708 | valid_description = config['valid_descriptions'] 709 | 710 | correct_path = valid_path[0] 711 | correct_description = valid_description[0] 712 | 713 | def intersection(lst1, lst2): 714 | """ return the intersection of two lists """ 715 | return list(set(lst1) & set(lst2)) 716 | 717 | return_list = [] 718 | for elem in videos_list_of_dictionaries: 719 | # preprocess the dictionary 720 | elem = {k.strip().lower(): v for k, v in elem.items()} 721 | 722 | keys = elem.keys() 723 | path_intersection = intersection(valid_path, keys) 724 | description_intersection = intersection(valid_description, keys) 725 | 726 | if path_intersection: 727 | # we have a path 728 | path = elem[path_intersection.pop()] 729 | 730 | if not _check_valid_path(path): 731 | raise RuntimeError("Invalid path: " + path) 732 | 733 | elem[correct_path] = path 734 | else: 735 | # iterates over the elem and find a key which is a path with a valid extension 736 | for _, value in elem.items(): 737 | if _check_valid_path(value): 738 | elem[correct_path] = value 739 | break 740 | else: 741 | # no valid path found 742 | raise RuntimeError("Path not found in dictionary: " + str(elem)) 743 | 744 | if description_intersection: 745 | # we have a description 746 | elem[correct_description] = elem[description_intersection.pop()] 747 | else: 748 | # iterates over the elem and finds a description which is not a valid path 749 | for _, value in elem.items(): 750 | if not _check_valid_path(value): 751 | elem[correct_description] = value 752 | break 753 | else: 754 | elem[correct_description] = '' # null description is fine 755 | 756 | return_list.append(elem) 757 | 758 | return return_list 759 | 760 | def __get_driver_timezone(driver) -> pytz.timezone: 761 | """ 762 | Returns the timezone of the driver 763 | """ 764 | timezone_str = driver.execute_script("return Intl.DateTimeFormat().resolvedOptions().timeZone") 765 | return pytz.timezone(timezone_str) 766 | 767 | def _refresh_with_alert(driver) -> None: 768 | try: 769 | # attempt to refresh the page 770 | driver.refresh() 771 | 772 | # wait for the alert to appear 773 | WebDriverWait(driver, config['explicit_wait']).until(EC.alert_is_present()) 774 | 775 | # accept the alert 776 | driver.switch_to.alert.accept() 777 | except: 778 | # if no alert appears, the page is fine 779 | pass 780 | 781 | class DescriptionTooLong(Exception): 782 | """ 783 | A video description longer than the maximum allowed by TikTok's website (not app) uploader 784 | """ 785 | 786 | def __init__(self, message=None): 787 | super().__init__(message or self.__doc__) 788 | 789 | 790 | class FailedToUpload(Exception): 791 | """ 792 | A video failed to upload 793 | """ 794 | 795 | def __init__(self, message=None): 796 | super().__init__(message or self.__doc__) 797 | -------------------------------------------------------------------------------- /tiktok_upload/upload_vid.py: -------------------------------------------------------------------------------- 1 | from tiktok_uploader.upload import upload_video 2 | import argparse 3 | from datetime import datetime, date, timedelta 4 | import re 5 | from typing import Optional 6 | import time 7 | 8 | FILENAME = "/Users/joshuakim/Downloads/TIFU_thinking_I_p1.mp4" 9 | 10 | current_time = datetime.now() 11 | month = current_time.month 12 | day = current_time.day 13 | year = current_time.year 14 | hour = current_time.hour 15 | minutes = current_time.minute 16 | SCHEDULE_DATE = f"{month}/{day}/{year}, {hour}:{minutes}" 17 | 18 | dates = [datetime.datetime(year, day, 12, 00, 00), datetime.datetime(year, day, 17, 30, 00), datetime.datetime(year, day, 00, 00, 00) + timedelta(days=1)] 19 | curr = 0 20 | days_since = 0 21 | 22 | def getNextSchedule(): 23 | global SCHEDULE_DATE 24 | date = SCHEDULE_DATE.split(',')[0].strip().split("/") 25 | month, day, year = map(int, date) 26 | time = SCHEDULE_DATE.split(',')[1].strip().split(":") 27 | hour, minutes = map(int, time) 28 | if (hour < 12): 29 | SCHEDULE_DATE = f"{month}/{day}/{year}, 12:00" 30 | return datetime.datetime(year, month, day, 12, 00, 00) 31 | elif (hour < 17 or (hour <= 17 and minutes < 30)): 32 | SCHEDULE_DATE = f"{month}/{day}/{year}, 17:30" 33 | return datetime.datetime(year, month, day, 17, 30, 00) 34 | else: 35 | next_day = datetime(year=year, month=month, day=day) + timedelta(days=1) 36 | year = next_day.year 37 | month = next_day.month 38 | day = next_day.day 39 | SCHEDULE_DATE = f"{month}/{day}/{year}, 00:00" 40 | return datetime.datetime(year, month, day, 00, 00, 00) 41 | 42 | def remove_ending(string): 43 | pattern_with_part = r"_p\d+\.mp4" 44 | pattern_long_form = r"\.mp4" 45 | modified_string = re.sub(pattern_with_part, '', string) 46 | modified_string = re.sub(pattern_long_form, '', modified_string) 47 | return modified_string 48 | 49 | def get_max_title(title): 50 | valid_title = "" 51 | title_words = title.split() 52 | for word in title_words: 53 | if len(valid_title) + len(word) + 1 <= 100: 54 | valid_title += (word + " ") 55 | else: 56 | break 57 | return valid_title.strip() 58 | 59 | if __name__ == "__main__": 60 | today = date.today().strftime("%Y-%m-%d") 61 | today = "2024-01-12" 62 | 63 | TIKTOK_UPLOADS = [] 64 | with open(f"../RedditPosts/{today}/uploadQueue/tiktok_queue.txt", "r", encoding="utf-8") as file: 65 | file_contents = file.read() 66 | TIKTOK_UPLOADS = file_contents.split('\n') 67 | TIKTOK_UPLOADS = [upload for upload in TIKTOK_UPLOADS if upload] 68 | 69 | used_uploads = [] 70 | max_uploads = 21 71 | for upload in TIKTOK_UPLOADS: 72 | subreddit = upload.split("/")[3] 73 | video_num = upload.split("/")[4] 74 | title = "redditstory" 75 | with open(f"../RedditPosts/{today}/Texts/{subreddit}/{video_num}/videoTitle.txt", "r", encoding="utf-8") as file: 76 | title = file.readline().strip() 77 | title = get_max_title(title) + f"{title}\n\n#shorts #redditstories #{subreddit} #cooking" 78 | schedule = getNextSchedule() 79 | 80 | upload_video(upload, 81 | description="title", 82 | cookies="cookies/tiktokcookies.txt", schedule=schedule) 83 | 84 | used_uploads.append(upload) 85 | max_uploads -= 1 86 | if max_uploads <= 0: 87 | break 88 | 89 | time.sleep(5) 90 | 91 | remaining_tiktok_uploads = [upload for upload in TIKTOK_UPLOADS if upload not in used_uploads] 92 | with open(f"../RedditPosts/{today}/uploadQueue/tiktok_queue.txt", "w", encoding="utf-8") as file: 93 | file.writelines('\n'.join(remaining_tiktok_uploads)) 94 | -------------------------------------------------------------------------------- /tiktok_upload/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for TikTok Uploader 3 | """ 4 | 5 | HEADER = '\033[95m' 6 | OKBLUE = '\033[94m' 7 | OKCYAN = '\033[96m' 8 | OKGREEN = '\033[92m' 9 | WARNING = '\033[93m' 10 | FAIL = '\033[91m' 11 | ENDC = '\033[0m' 12 | BOLD = '\033[1m' 13 | UNDERLINE = '\033[4m' 14 | 15 | def bold(to_bold: str) -> str: 16 | """ 17 | Returns the input bolded 18 | """ 19 | return BOLD + to_bold + ENDC 20 | 21 | def green(to_green: str) -> str: 22 | """ 23 | Returns the input green 24 | """ 25 | return OKGREEN + to_green + ENDC 26 | 27 | def red(to_red: str) -> str: 28 | """ 29 | Returns the input red 30 | """ 31 | return FAIL + to_red + ENDC 32 | -------------------------------------------------------------------------------- /topKWeeklyPostsScraper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | import time 4 | from bs4 import BeautifulSoup 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.support import expected_conditions as EC 7 | from selenium.webdriver.support.ui import WebDriverWait 8 | from selenium.webdriver.edge import service 9 | 10 | from selenium import webdriver 11 | from selenium.webdriver.edge.options import Options as EdgeOptions 12 | from datetime import date 13 | 14 | from webdriver_manager.chrome import ChromeDriverManager 15 | 16 | from dotenv import load_dotenv 17 | load_dotenv() 18 | reddit_username = os.environ.get('REDDIT_USERNAME') 19 | reddit_password = os.environ.get('REDDIT_PASSWORD') 20 | 21 | driver = webdriver.Chrome(ChromeDriverManager().install()) 22 | 23 | # Function to scroll the page by a specified amount (in pixels) 24 | def scroll_page(by_pixels): 25 | driver.execute_script(f"window.scrollBy(0, {by_pixels});") 26 | 27 | def login(): 28 | driver.get("https://www.reddit.com/login/") 29 | # time.sleep(3000) 30 | username_field = WebDriverWait(driver, 10).until( 31 | EC.presence_of_element_located((By.ID, "login-username")) 32 | ) 33 | password_field = WebDriverWait(driver, 10).until( 34 | EC.presence_of_element_located((By.ID, "login-password")) 35 | ) 36 | username_field.send_keys(reddit_username) 37 | password_field.send_keys(reddit_password) 38 | # time.sleep(30000) 39 | # login_button = WebDriverWait(driver, 10).until( 40 | # # EC.element_to_be_clickable((By.CLASS_NAME, "AnimatedForm__submitButton")) 41 | # EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'Log In')]")) 42 | # ) 43 | # print(login_button) 44 | # login_button.click() 45 | 46 | time.sleep(10) 47 | 48 | def scrape(url, download_path, subreddit): 49 | # Create the download directory if it doesn't exist 50 | if not os.path.exists(download_path): 51 | os.makedirs(download_path) 52 | 53 | output_file = os.path.join(download_path, "links.txt") 54 | with open(output_file, 'a') as file: 55 | file.write(f"{subreddit[0]}\n\n") 56 | 57 | try: 58 | # Send an HTTP GET request to the URL using Selenium 59 | driver.get(url) 60 | # Wait for the page to load (adjust the wait time as needed) 61 | scroll_page("document.body.scrollHeight") 62 | time.sleep(3) 63 | 64 | wait = WebDriverWait(driver, 5) 65 | wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'a[slot="full-post-link"]'))) 66 | # wait.until(EC.presence_of_element_located((By.CLASS_NAME, "SQnoC3ObvgnGjWt90zD9Z"))) 67 | # Get the page source (HTML content) using Selenium 68 | page_source = driver.page_source 69 | 70 | # Parse the HTML content of the page using BeautifulSoup 71 | soup = BeautifulSoup(page_source, "html.parser") 72 | 73 | # Find all
elements with the specified class 74 | link_elements = soup.find_all("a", {"slot": "full-post-link"}) 75 | # link_elements = soup.find_all("a", class_="SQnoC3ObvgnGjWt90zD9Z") 76 | 77 | # Iterate through the div elements and filter based on your criteria 78 | for i in range(min(len(link_elements), 15)): 79 | link_element = link_elements[i] 80 | print(f"reddit.com{link_element['href']}") 81 | 82 | with open(output_file, 'a') as file: 83 | file.write(f"reddit.com{link_element.get('href')}\n") 84 | with open(output_file, 'a') as file: 85 | file.write("\n") 86 | except: 87 | print(f"No posts today on {subreddit[0]}") 88 | finally: 89 | print(f"Finished running {subreddit[0]}") 90 | 91 | if __name__ == "__main__": 92 | today = date.today().strftime("%Y-%m-%d") 93 | # today = "Custom" 94 | current_date = datetime.datetime.now() 95 | 96 | login() 97 | 98 | long_form_subreddits = ["nosleep"] 99 | # considered = [["entitledparents", 1, 6], ["Glitch_in_the_Matrix", 1, 6], ["creepyencounters", 1, 6], ["LetsNotMeet", 1, 6], ["confession", 2, 6],] 100 | subreddits = [ 101 | ["relationship_advice", 2, 6], ["relationships", 1, 6], 102 | ["confessions", 2, 6], 103 | ["TrueOffMyChest", 1, 6], ["offmychest", 3, 6], 104 | ["tifu", 1, 6], ["legaladvice", 1, 6], 105 | ["AmItheAsshole", 3, 6], ["AITAH", 4, 6], 106 | # ["askreddit", 4, 6] 107 | ] 108 | 109 | for subreddit in subreddits: 110 | # if current_date.weekday() == subreddit[2]: 111 | if True: 112 | url = f"https://www.reddit.com/r/{subreddit[0]}/top/?t=week" 113 | download_path = f"RedditPosts/{today}" 114 | scrape(url, download_path, subreddit) 115 | 116 | # Close the browser 117 | driver.quit() -------------------------------------------------------------------------------- /youtube_upload/constant.py: -------------------------------------------------------------------------------- 1 | class Constant: 2 | """A class for storing constants for YoutubeUploader class""" 3 | YOUTUBE_URL = 'https://www.youtube.com' 4 | YOUTUBE_STUDIO_URL = 'https://studio.youtube.com' 5 | YOUTUBE_UPLOAD_URL = 'https://www.youtube.com/upload' 6 | USER_WAITING_TIME = 1 7 | VIDEO_TITLE = 'title' 8 | VIDEO_DESCRIPTION = 'description' 9 | VIDEO_EDIT = 'edit' 10 | VIDEO_TAGS = 'tags' 11 | TEXTBOX_ID = 'textbox' 12 | TEXT_INPUT = 'text-input' 13 | RADIO_LABEL = 'radioLabel' 14 | UPLOADING_STATUS_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[2]/div/div[1]/ytcp-video-upload-progress[@uploading=""]' 15 | NOT_MADE_FOR_KIDS_LABEL = 'VIDEO_MADE_FOR_KIDS_NOT_MFK' 16 | 17 | UPLOAD_DIALOG = '//ytcp-uploads-dialog' 18 | ADVANCED_BUTTON_ID = 'toggle-button' 19 | TAGS_CONTAINER_ID = 'tags-container' 20 | 21 | TAGS_INPUT = 'text-input' 22 | NEXT_BUTTON = 'next-button' 23 | PUBLIC_BUTTON = 'PUBLIC' 24 | VIDEO_URL_CONTAINER = "//span[@class='video-url-fadeable style-scope ytcp-video-info']" 25 | VIDEO_URL_ELEMENT = "//a[@class='style-scope ytcp-video-info']" 26 | HREF = 'href' 27 | ERROR_CONTAINER = '//*[@id="error-message"]' 28 | VIDEO_NOT_FOUND_ERROR = 'Could not find video_id' 29 | DONE_BUTTON = 'done-button' 30 | INPUT_FILE_VIDEO = "//input[@type='file']" 31 | INPUT_FILE_THUMBNAIL = "//input[@id='file-loader']" 32 | 33 | # Playlist 34 | VIDEO_PLAYLIST = 'playlist_title' 35 | PL_DROPDOWN_CLASS = 'ytcp-video-metadata-playlists' 36 | PL_SEARCH_INPUT_ID = 'search-input' 37 | PL_ITEMS_CONTAINER_ID = 'items' 38 | PL_ITEM_CONTAINER = '//span[text()="{}"]' 39 | PL_NEW_BUTTON_CLASS = 'new-playlist-button' 40 | PL_CREATE_PLAYLIST_CONTAINER_ID = 'create-playlist-form' 41 | PL_CREATE_BUTTON_CLASS = 'create-playlist-button' 42 | PL_DONE_BUTTON_CLASS = 'done-button' 43 | 44 | # Publish to Subscriptions Feed Deselect 45 | SHOW_MORE_BUTTON = '//*[@id="toggle-button"]' 46 | PUBLISH_TO_SUBSCRIPTIONS_TOGGLE = '//*[@id="notify-subscribers"]' 47 | # also can use id = 'toggle-button' and 'notify-subscribers' as they are unique 48 | 49 | #Schedule 50 | VIDEO_SCHEDULE = 'schedule' 51 | SCHEDULE_CONTAINER_ID = 'second-container-expand-button' 52 | SCHEDULE_DATE_ID = 'datepicker-trigger' 53 | SCHEDULE_DATE_TEXTBOX = '/html/body/ytcp-date-picker/tp-yt-paper-dialog/div/form/tp-yt-paper-input/tp-yt-paper-input-container/div[2]/div/iron-input/input' 54 | # SCHEDULE_TIME = "/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-review/div[2]/div[1]/ytcp-video-visibility-select/div[3]/ytcp-visibility-scheduler/div[1]/ytcp-datetime-picker/div/div[2]/form/ytcp-form-input-container/div[1]/div/tp-yt-paper-input/tp-yt-paper-input-container/div[2]/div/iron-input/input" 55 | SCHEDULE_TIME = "/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-review/div[2]/div[1]/ytcp-video-visibility-select/div[3]/div[2]/ytcp-visibility-scheduler/div[1]/ytcp-datetime-picker/div/div[2]/form/ytcp-form-input-container/div[1]/div/tp-yt-paper-input/tp-yt-paper-input-container/div[2]/div/iron-input/input" -------------------------------------------------------------------------------- /youtube_upload/helpers.py: -------------------------------------------------------------------------------- 1 | """This module implements uploading videos on YouTube via Selenium using metadata JSON file 2 | to extract its title, description etc.""" 3 | 4 | from typing import DefaultDict, Optional, Tuple 5 | from selenium_firefox.firefox import Firefox 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.common.keys import Keys 8 | from collections import defaultdict 9 | from datetime import datetime 10 | import json 11 | import time 12 | from constant import * 13 | from pathlib import Path 14 | import logging 15 | import platform 16 | 17 | logging.basicConfig() 18 | 19 | 20 | def load_metadata(metadata_json_path: Optional[str] = None) -> DefaultDict[str, str]: 21 | if metadata_json_path is None: 22 | return defaultdict(str) 23 | # return json.dumps(metadata_json_path) 24 | return defaultdict(str, metadata_json_path) 25 | # with open(metadata_json_path, encoding='utf-8') as metadata_json_file: 26 | # return defaultdict(str, json.load(metadata_json_file)) 27 | 28 | 29 | class YouTubeUploader: 30 | """A class for uploading videos on YouTube via Selenium using metadata JSON file 31 | to extract its title, description etc""" 32 | 33 | def __init__(self, video_path: str, metadata_json_path: Optional[str] = None, 34 | thumbnail_path: Optional[str] = None, 35 | profile_path: Optional[str] = str(Path.cwd().parent) + "/profile") -> None: # 36 | 37 | self.video_path = video_path 38 | self.thumbnail_path = thumbnail_path 39 | self.metadata_dict = load_metadata(metadata_json_path) 40 | self.browser = Firefox(profile_path=profile_path, pickle_cookies=True, full_screen=False) 41 | self.logger = logging.getLogger(__name__) 42 | self.logger.setLevel(logging.DEBUG) 43 | self.__validate_inputs() 44 | 45 | self.is_mac = False 46 | if not any(os_name in platform.platform() for os_name in ["Windows", "Linux"]): 47 | self.is_mac = True 48 | 49 | self.logger.debug("Use profile path: {}".format(self.browser.source_profile_path)) 50 | 51 | def __validate_inputs(self): 52 | if not self.metadata_dict[Constant.VIDEO_TITLE]: 53 | self.logger.warning( 54 | "The video title was not found in a metadata file") 55 | self.metadata_dict[Constant.VIDEO_TITLE] = Path( 56 | self.video_path).stem 57 | self.logger.warning("The video title was set to {}".format( 58 | Path(self.video_path).stem)) 59 | if not self.metadata_dict[Constant.VIDEO_DESCRIPTION]: 60 | self.logger.warning( 61 | "The video description was not found in a metadata file") 62 | 63 | def upload(self): 64 | try: 65 | # self.__login() 66 | return self.__upload() 67 | except Exception as e: 68 | print(e) 69 | self.__quit() 70 | raise 71 | 72 | def __login(self): 73 | self.browser.get(Constant.YOUTUBE_URL) 74 | time.sleep(Constant.USER_WAITING_TIME) 75 | 76 | if self.browser.has_cookies_for_current_website(): 77 | self.browser.load_cookies() 78 | self.logger.debug("Loaded cookies from {}".format(self.browser.cookies_folder_path)) 79 | time.sleep(Constant.USER_WAITING_TIME) 80 | self.browser.refresh() 81 | else: 82 | self.logger.info('Please sign in and then press enter') 83 | input() 84 | self.browser.get(Constant.YOUTUBE_URL) 85 | time.sleep(Constant.USER_WAITING_TIME) 86 | self.browser.save_cookies() 87 | self.logger.debug("Saved cookies to {}".format(self.browser.cookies_folder_path)) 88 | 89 | def __clear_field(self, field): 90 | field.click() 91 | time.sleep(Constant.USER_WAITING_TIME) 92 | if self.is_mac: 93 | field.send_keys(Keys.COMMAND + 'a') 94 | else: 95 | field.send_keys(Keys.CONTROL + 'a') 96 | time.sleep(Constant.USER_WAITING_TIME) 97 | field.send_keys(Keys.BACKSPACE) 98 | 99 | def __write_in_field(self, field, string, select_all=False): 100 | if select_all: 101 | self.__clear_field(field) 102 | else: 103 | field.click() 104 | time.sleep(Constant.USER_WAITING_TIME) 105 | 106 | field.send_keys(string) 107 | 108 | def __upload(self) -> Tuple[bool, Optional[str]]: 109 | edit_mode = self.metadata_dict[Constant.VIDEO_EDIT] 110 | if edit_mode: 111 | self.browser.get(edit_mode) 112 | time.sleep(Constant.USER_WAITING_TIME) 113 | else: 114 | self.browser.get(Constant.YOUTUBE_URL) 115 | time.sleep(Constant.USER_WAITING_TIME) 116 | self.browser.get(Constant.YOUTUBE_UPLOAD_URL) 117 | time.sleep(Constant.USER_WAITING_TIME) 118 | absolute_video_path = str(Path.cwd().parent / self.video_path) 119 | self.browser.find(By.XPATH, Constant.INPUT_FILE_VIDEO).send_keys( 120 | absolute_video_path) 121 | self.logger.debug('Attached video {}'.format(self.video_path)) 122 | 123 | # Find status container 124 | uploading_status_container = None 125 | while uploading_status_container is None: 126 | time.sleep(Constant.USER_WAITING_TIME) 127 | uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER) 128 | 129 | try: 130 | if self.thumbnail_path is not None: 131 | absolute_thumbnail_path = str(Path.cwd().parent / self.thumbnail_path) 132 | self.browser.find(By.XPATH, Constant.INPUT_FILE_THUMBNAIL).send_keys( 133 | absolute_thumbnail_path) 134 | change_display = "document.getElementById('file-loader').style = 'display: block! important'" 135 | self.browser.driver.execute_script(change_display) 136 | self.logger.debug( 137 | 'Attached thumbnail {}'.format(self.thumbnail_path)) 138 | 139 | title_field, description_field = self.browser.find_all(By.ID, Constant.TEXTBOX_ID, timeout=15) 140 | 141 | self.__write_in_field( 142 | title_field, self.metadata_dict[Constant.VIDEO_TITLE], select_all=True) 143 | self.logger.debug('The video title was set to \"{}\"'.format( 144 | self.metadata_dict[Constant.VIDEO_TITLE])) 145 | 146 | video_description = self.metadata_dict[Constant.VIDEO_DESCRIPTION] 147 | video_description = video_description.replace("\n", Keys.ENTER); 148 | if video_description: 149 | self.__write_in_field(description_field, video_description, select_all=True) 150 | self.logger.debug('Description filled.') 151 | 152 | kids_section = self.browser.find(By.NAME, Constant.NOT_MADE_FOR_KIDS_LABEL) 153 | kids_section.location_once_scrolled_into_view 154 | time.sleep(Constant.USER_WAITING_TIME) 155 | 156 | self.browser.find(By.ID, Constant.RADIO_LABEL, kids_section).click() 157 | self.logger.debug('Selected \"{}\"'.format(Constant.NOT_MADE_FOR_KIDS_LABEL)) 158 | 159 | # Playlist 160 | playlist = self.metadata_dict[Constant.VIDEO_PLAYLIST] 161 | if playlist: 162 | self.browser.find(By.CLASS_NAME, Constant.PL_DROPDOWN_CLASS).click() 163 | time.sleep(Constant.USER_WAITING_TIME) 164 | search_field = self.browser.find(By.ID, Constant.PL_SEARCH_INPUT_ID) 165 | self.__write_in_field(search_field, playlist) 166 | time.sleep(Constant.USER_WAITING_TIME * 2) 167 | playlist_items_container = self.browser.find(By.ID, Constant.PL_ITEMS_CONTAINER_ID) 168 | # Try to find playlist 169 | self.logger.debug('Playlist xpath: "{}".'.format(Constant.PL_ITEM_CONTAINER.format(playlist))) 170 | playlist_item = self.browser.find(By.XPATH, Constant.PL_ITEM_CONTAINER.format(playlist), playlist_items_container) 171 | if playlist_item: 172 | self.logger.debug('Playlist found.') 173 | playlist_item.click() 174 | time.sleep(Constant.USER_WAITING_TIME) 175 | else: 176 | self.logger.debug('Playlist not found. Creating') 177 | self.__clear_field(search_field) 178 | time.sleep(Constant.USER_WAITING_TIME) 179 | 180 | new_playlist_button = self.browser.find(By.CLASS_NAME, Constant.PL_NEW_BUTTON_CLASS) 181 | new_playlist_button.click() 182 | 183 | create_playlist_container = self.browser.find(By.ID, Constant.PL_CREATE_PLAYLIST_CONTAINER_ID) 184 | playlist_title_textbox = self.browser.find(By.XPATH, "//textarea", create_playlist_container) 185 | self.__write_in_field(playlist_title_textbox, playlist) 186 | 187 | time.sleep(Constant.USER_WAITING_TIME) 188 | create_playlist_button = self.browser.find(By.CLASS_NAME, Constant.PL_CREATE_BUTTON_CLASS) 189 | create_playlist_button.click() 190 | time.sleep(Constant.USER_WAITING_TIME) 191 | 192 | done_button = self.browser.find(By.CLASS_NAME, Constant.PL_DONE_BUTTON_CLASS) 193 | done_button.click() 194 | 195 | # Advanced options 196 | self.browser.find(By.ID, Constant.ADVANCED_BUTTON_ID).click() 197 | self.logger.debug('Clicked MORE OPTIONS') 198 | time.sleep(Constant.USER_WAITING_TIME) 199 | 200 | # Tags 201 | tags = self.metadata_dict[Constant.VIDEO_TAGS] 202 | if tags: 203 | tags_container = self.browser.find(By.ID, Constant.TAGS_CONTAINER_ID) 204 | tags_field = self.browser.find(By.ID, Constant.TAGS_INPUT, tags_container) 205 | self.__write_in_field(tags_field, ','.join(tags)) 206 | self.logger.debug('The tags were set to \"{}\"'.format(tags)) 207 | 208 | # Toggle Publish to Subscriptions Feed 209 | self.browser.find(By.XPATH, Constant.SHOW_MORE_BUTTON).click() 210 | self.browser.find(By.XPATH, Constant.PUBLISH_TO_SUBSCRIPTIONS_TOGGLE).click() 211 | 212 | # Navigate to Publish Page 213 | self.browser.find(By.ID, Constant.NEXT_BUTTON).click() 214 | self.logger.debug('Clicked {} one'.format(Constant.NEXT_BUTTON)) 215 | 216 | self.browser.find(By.ID, Constant.NEXT_BUTTON).click() 217 | self.logger.debug('Clicked {} two'.format(Constant.NEXT_BUTTON)) 218 | 219 | self.browser.find(By.ID, Constant.NEXT_BUTTON).click() 220 | self.logger.debug('Clicked {} three'.format(Constant.NEXT_BUTTON)) 221 | 222 | # Schedule 223 | schedule = self.metadata_dict[Constant.VIDEO_SCHEDULE] 224 | if schedule: 225 | upload_time_object = datetime.strptime(schedule, "%m/%d/%Y, %H:%M") 226 | self.browser.find(By.ID, Constant.SCHEDULE_CONTAINER_ID).click() 227 | self.browser.find(By.ID, Constant.SCHEDULE_DATE_ID).click() 228 | self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).clear() 229 | self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).send_keys( 230 | datetime.strftime(upload_time_object, "%b %e, %Y")) 231 | self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).send_keys(Keys.ENTER) 232 | self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).click() 233 | self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).clear() 234 | self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).send_keys( 235 | datetime.strftime(upload_time_object, "%H:%M")) 236 | self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).send_keys(Keys.ENTER) 237 | self.logger.debug(f"Scheduled the video for {schedule}") 238 | else: 239 | public_main_button = self.browser.find(By.NAME, Constant.PUBLIC_BUTTON) 240 | self.browser.find(By.ID, Constant.RADIO_LABEL, public_main_button).click() 241 | self.logger.debug('Made the video {}'.format(Constant.PUBLIC_BUTTON)) 242 | 243 | video_id = self.__get_video_id() 244 | 245 | # Check status container and upload progress 246 | uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER) 247 | while uploading_status_container is not None: 248 | uploading_progress = uploading_status_container.get_attribute('value') 249 | self.logger.debug('Upload video progress: {}%'.format(uploading_progress)) 250 | time.sleep(Constant.USER_WAITING_TIME * 5) 251 | uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER) 252 | 253 | self.logger.debug('Upload container gone.') 254 | 255 | done_button = self.browser.find(By.ID, Constant.DONE_BUTTON) 256 | 257 | # Catch such error as 258 | # "File is a duplicate of a video you have already uploaded" 259 | if done_button.get_attribute('aria-disabled') == 'true': 260 | error_message = self.browser.find(By.XPATH, Constant.ERROR_CONTAINER).text 261 | self.logger.error(error_message) 262 | return False, None 263 | 264 | done_button.click() 265 | self.logger.debug( 266 | "Published the video with video_id = {}".format(video_id)) 267 | time.sleep(Constant.USER_WAITING_TIME) 268 | self.browser.get(Constant.YOUTUBE_URL) 269 | self.__quit() 270 | return True, video_id 271 | except: 272 | print("Error occured, video upload limit may be reached") 273 | return False, None 274 | 275 | def __get_video_id(self) -> Optional[str]: 276 | video_id = None 277 | try: 278 | video_url_container = self.browser.find( 279 | By.XPATH, Constant.VIDEO_URL_CONTAINER) 280 | video_url_element = self.browser.find(By.XPATH, Constant.VIDEO_URL_ELEMENT, element=video_url_container) 281 | video_id = video_url_element.get_attribute( 282 | Constant.HREF).split('/')[-1] 283 | except: 284 | self.logger.warning(Constant.VIDEO_NOT_FOUND_ERROR) 285 | pass 286 | return video_id 287 | 288 | def __quit(self): 289 | self.browser.driver.quit() -------------------------------------------------------------------------------- /youtube_upload/upload.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from datetime import datetime, date, timedelta 3 | import re 4 | from helpers import YouTubeUploader 5 | from typing import Optional 6 | import time 7 | 8 | from firefox_profile import FIREFOX_PROFILE 9 | 10 | current_time = datetime.now() 11 | month = current_time.month 12 | day = current_time.day 13 | year = current_time.year 14 | hour = current_time.hour 15 | minutes = current_time.minute 16 | SCHEDULE_DATE = f"{month}/{day}/{year}, {hour}:{minutes}" 17 | 18 | # check if string contains "_p{number}.mp4" or ".mp4" and remove 19 | def remove_ending(string): 20 | pattern_with_part = r"_p\d+\.mp4" 21 | pattern_long_form = r"\.mp4" 22 | modified_string = re.sub(pattern_with_part, '', string) 23 | modified_string = re.sub(pattern_long_form, '', modified_string) 24 | return modified_string 25 | 26 | def contains_pattern(string): 27 | pattern = re.compile(r"_p\d+\.mp4") 28 | return bool(pattern.search(string)) 29 | 30 | def get_max_title(title): 31 | valid_title = "" 32 | title_words = title.split() 33 | for word in title_words: 34 | if len(valid_title) + len(word) + 1 <= 100: 35 | valid_title += (word + " ") 36 | else: 37 | break 38 | return valid_title.strip() 39 | 40 | def getNextSchedule(): 41 | global SCHEDULE_DATE 42 | date = SCHEDULE_DATE.split(',')[0].strip().split("/") 43 | month, day, year = map(int, date) 44 | time = SCHEDULE_DATE.split(',')[1].strip().split(":") 45 | hour, minutes = map(int, time) 46 | 47 | if (hour < 12): 48 | SCHEDULE_DATE = f"{month}/{day}/{year}, 12:00" 49 | elif (hour < 17 or (hour <= 17 and minutes < 30)): 50 | SCHEDULE_DATE = f"{month}/{day}/{year}, 17:30" 51 | else: 52 | next_day = datetime(year=year, month=month, day=day) + timedelta(days=1) 53 | year = next_day.year 54 | month = next_day.month 55 | day = next_day.day 56 | SCHEDULE_DATE = f"{month}/{day}/{year}, 00:00" 57 | return SCHEDULE_DATE 58 | 59 | def getNextDaySchedule(): 60 | global SCHEDULE_DATE 61 | date = SCHEDULE_DATE.split(',')[0].strip().split("/") 62 | month, day, year = map(int, date) 63 | time = SCHEDULE_DATE.split(',')[1].strip().split(":") 64 | hour, minutes = map(int, time) 65 | 66 | next_day = datetime(year=year, month=month, day=day) + timedelta(days=1) 67 | year = next_day.year 68 | month = next_day.month 69 | day = next_day.day 70 | SCHEDULE_DATE = f"{month}/{day}/{year}, 00:00" 71 | return SCHEDULE_DATE 72 | 73 | def main(video_path: str, 74 | metadata_path: Optional[str] = None, 75 | thumbnail_path: Optional[str] = None, 76 | profile_path: Optional[str] = None): 77 | uploader = YouTubeUploader(video_path, metadata_path, thumbnail_path, profile_path) 78 | was_video_uploaded, video_id = uploader.upload() 79 | return was_video_uploaded 80 | 81 | 82 | if __name__ == "__main__": 83 | today = date.today().strftime("%Y-%m-%d") 84 | today = "2024-01-12" 85 | 86 | YOUTUBE_UPLOADS = [] 87 | with open(f"../RedditPosts/{today}/uploadQueue/youtube_queue.txt", "r", encoding="utf-8") as file: 88 | file_contents = file.read() 89 | YOUTUBE_UPLOADS = file_contents.split('\n') 90 | YOUTUBE_UPLOADS = [upload for upload in YOUTUBE_UPLOADS if upload] 91 | 92 | used_uploads = [] 93 | max_uploads = 20 94 | for upload in YOUTUBE_UPLOADS: 95 | # skip multiple part videos, might remove might keep 96 | if contains_pattern(upload): 97 | continue 98 | 99 | subreddit = upload.split("/")[3] 100 | video_num = upload.split("/")[4] 101 | title = "redditstory" 102 | with open(f"../RedditPosts/{today}/Texts/{subreddit}/{video_num}/videoTitle.txt", "r", encoding="utf-8") as file: 103 | title = file.readline().strip() 104 | json = { 105 | "title": get_max_title(title), 106 | "description": f"{title}\n\n#shorts #redditstories #{subreddit} #cooking", 107 | "tags": [], 108 | # "schedule": f"{getNextSchedule()}" 109 | "schedule": f"{getNextDaySchedule()}" 110 | } 111 | 112 | if not main(upload, json, profile_path=FIREFOX_PROFILE): 113 | break 114 | 115 | used_uploads.append(upload) 116 | max_uploads -= 1 117 | if max_uploads <= 0: 118 | break 119 | 120 | time.sleep(2) 121 | 122 | # remaining_youtube_uploads = [upload for upload in YOUTUBE_UPLOADS if upload not in used_uploads] 123 | # with open(f"../RedditPosts/{today}/uploadQueue/youtube_queue.txt", "w", encoding="utf-8") as file: 124 | # file.writelines('\n'.join(remaining_youtube_uploads)) 125 | --------------------------------------------------------------------------------