",
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 |
--------------------------------------------------------------------------------