"""This module implements uploading videos on YouTube via Selenium using metadata JSON file to extract its title, description etc.""" from typing import DefaultDict, Optional, Tuple from selenium_firefox.firefox import Firefox from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from collections import defaultdict from datetime import datetime import json import time from .Constant import * from pathlib import Path import logging import platform logging.basicConfig() import random #used to generate a random wait time to perform the actions on the browser def random_time(): rand : int = random.randint(1, 3) print("Random time: ", rand) return rand def load_metadata(metadata_json_path: Optional[str] = None) -> DefaultDict[str, str]: if metadata_json_path is None: return defaultdict(str) with open(metadata_json_path, encoding='utf-8') as metadata_json_file: return defaultdict(str, json.load(metadata_json_file)) class YouTubeUploader: """A class for uploading videos on YouTube via Selenium using metadata JSON file to extract its title, description etc""" def __init__(self, video_path: str, metadata_json_path: Optional[str] = None, thumbnail_path: Optional[str] = None, profile_path: Optional[str] = str(Path.cwd()) + "/profile", headless : bool = True) -> None: self.video_path = video_path self.thumbnail_path = thumbnail_path self.metadata_dict = load_metadata(metadata_json_path) self.browser = Firefox(profile_path=profile_path, pickle_cookies=True, full_screen=False, headless=headless) self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.DEBUG) self.__validate_inputs() print("headless for youtube =" + str(headless)) self.is_mac = False if not any(os_name in platform.platform() for os_name in ["Windows", "Linux"]): self.is_mac = True self.logger.debug("Use profile path: {}".format(self.browser.source_profile_path)) def __validate_inputs(self): if not self.metadata_dict[Constant.VIDEO_TITLE]: self.logger.warning( "The video title was not found in a metadata file") self.metadata_dict[Constant.VIDEO_TITLE] = Path( self.video_path).stem self.logger.warning("The video title was set to {}".format( Path(self.video_path).stem)) if not self.metadata_dict[Constant.VIDEO_DESCRIPTION]: self.logger.warning( "The video description was not found in a metadata file") def upload(self): try: self.login() return self.__upload() except Exception as e: print(e) self.__quit() raise def login(self): self.browser.get(Constant.YOUTUBE_URL) time.sleep(random_time()) if self.browser.has_cookies_for_current_website(): self.browser.load_cookies() self.logger.debug("Loaded cookies from {}".format(self.browser.cookies_folder_path)) time.sleep(random_time()) self.browser.refresh() else: self.logger.info('Please sign in and then press enter') input() self.browser.get(Constant.YOUTUBE_URL) time.sleep(random_time()) self.browser.save_cookies() self.logger.debug("Saved cookies to {}".format(self.browser.cookies_folder_path)) def __clear_field(self, field): field.click() time.sleep(random_time()) if self.is_mac: field.send_keys(Keys.COMMAND + 'a') else: field.send_keys(Keys.CONTROL + 'a') time.sleep(random_time()) field.send_keys(Keys.BACKSPACE) def __write_in_field(self, field, string, select_all=False): if select_all: self.__clear_field(field) else: field.click() time.sleep(random_time()) field.send_keys(string) def __upload(self) -> Tuple[bool, Optional[str]]: self.browser.get(Constant.YOUTUBE_URL) time.sleep(random_time()) self.browser.get(Constant.YOUTUBE_UPLOAD_URL) time.sleep(random_time()) absolute_video_path = str(Path.cwd() / self.video_path) self.browser.find(By.XPATH, Constant.INPUT_FILE_VIDEO).send_keys( absolute_video_path) self.logger.debug('Attached video {}'.format(self.video_path)) # Find status container uploading_status_container = None while uploading_status_container is None: time.sleep(0.5) #bug where slept too long, missed finding the element got soft locked uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER) if self.thumbnail_path is not None: absolute_thumbnail_path = str(Path.cwd() / self.thumbnail_path) self.browser.find(By.XPATH, Constant.INPUT_FILE_THUMBNAIL).send_keys( absolute_thumbnail_path) change_display = "document.getElementById('file-loader').style = 'display: block! important'" self.browser.driver.execute_script(change_display) self.logger.debug( 'Attached thumbnail {}'.format(self.thumbnail_path)) title_field, description_field = self.browser.find_all(By.ID, Constant.TEXTBOX_ID, timeout=15) self.__write_in_field( title_field, self.metadata_dict[Constant.VIDEO_TITLE], select_all=True) self.logger.debug('The video title was set to \"{}\"'.format( self.metadata_dict[Constant.VIDEO_TITLE])) video_description = self.metadata_dict[Constant.VIDEO_DESCRIPTION] video_description = video_description.replace("\n", Keys.ENTER) if video_description: self.__write_in_field(description_field, video_description, select_all=True) self.logger.debug('Description filled.') kids_section = self.browser.find(By.NAME, Constant.NOT_MADE_FOR_KIDS_LABEL) kids_section.location_once_scrolled_into_view time.sleep(random_time()) self.browser.find(By.ID, Constant.RADIO_LABEL, kids_section).click() self.logger.debug('Selected \"{}\"'.format(Constant.NOT_MADE_FOR_KIDS_LABEL)) # Playlist playlist = self.metadata_dict[Constant.VIDEO_PLAYLIST] if playlist: self.browser.find(By.CLASS_NAME, Constant.PL_DROPDOWN_CLASS).click() time.sleep(random_time()) search_field = self.browser.find(By.ID, Constant.PL_SEARCH_INPUT_ID) self.__write_in_field(search_field, playlist) time.sleep(random_time() * 2) playlist_items_container = self.browser.find(By.ID, Constant.PL_ITEMS_CONTAINER_ID) # Try to find playlist self.logger.debug('Playlist xpath: "{}".'.format(Constant.PL_ITEM_CONTAINER.format(playlist))) playlist_item = self.browser.find(By.XPATH, Constant.PL_ITEM_CONTAINER.format(playlist), playlist_items_container) if playlist_item: self.logger.debug('Playlist found.') playlist_item.click() time.sleep(random_time()) else: self.logger.debug('Playlist not found. Creating') self.__clear_field(search_field) time.sleep(random_time()) new_playlist_button = self.browser.find(By.CLASS_NAME, Constant.PL_NEW_BUTTON_CLASS) new_playlist_button.click() create_playlist_container = self.browser.find(By.ID, Constant.PL_CREATE_PLAYLIST_CONTAINER_ID) playlist_title_textbox = self.browser.find(By.XPATH, "//textarea", create_playlist_container) self.__write_in_field(playlist_title_textbox, playlist) time.sleep(random_time()) create_playlist_button = self.browser.find(By.CLASS_NAME, Constant.PL_CREATE_BUTTON_CLASS) create_playlist_button.click() time.sleep(random_time()) done_button = self.browser.find(By.CLASS_NAME, Constant.PL_DONE_BUTTON_CLASS) done_button.click() # Advanced options self.browser.find(By.ID, Constant.ADVANCED_BUTTON_ID).click() self.logger.debug('Clicked MORE OPTIONS') time.sleep(random_time()) #click not ai (added myself by inspecting element and right clicking copy by xpath) not_ai = self.browser.find(By.XPATH, "/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-ve/ytcp-video-metadata-editor/div/ytcp-video-metadata-editor-advanced/div[2]/ytkp-altered-content-select/div[2]/tp-yt-paper-radio-group/tp-yt-paper-radio-button[2]/div[1]/div[1]") not_ai.click() time.sleep(random_time()) # Tags tags = self.metadata_dict[Constant.VIDEO_TAGS] if tags: tags_container = self.browser.find(By.ID, Constant.TAGS_CONTAINER_ID) tags_field = self.browser.find(By.ID, Constant.TAGS_INPUT, tags_container) self.__write_in_field(tags_field, ','.join(tags)) self.logger.debug('The tags were set to \"{}\"'.format(tags)) self.browser.find(By.ID, Constant.NEXT_BUTTON).click() self.logger.debug('Clicked {} one'.format(Constant.NEXT_BUTTON)) self.browser.find(By.ID, Constant.NEXT_BUTTON).click() self.logger.debug('Clicked {} two'.format(Constant.NEXT_BUTTON)) self.browser.find(By.ID, Constant.NEXT_BUTTON).click() self.logger.debug('Clicked {} three'.format(Constant.NEXT_BUTTON)) schedule = self.metadata_dict[Constant.VIDEO_SCHEDULE] #Schedule if schedule: upload_time_object = datetime.strptime(schedule, "%m/%d/%Y, %H:%M") self.browser.find(By.ID, Constant.SCHEDULE_CONTAINER_ID).click() #click the schedule dropdown time.sleep(1) #make sure there is time for the date picker to load schedule2 = self.browser.find(By.ID, Constant.SCHEDULE_DATE_ID) #find the date picker time.sleep(1) #wait for schedule2 to load schedule2.click() #click to open calendar time.sleep(1) #wait for the date picker to load self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).clear() #clear the date text box self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).send_keys( datetime.strftime(upload_time_object, "%b %e, %Y")) self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).send_keys(Keys.ENTER) time.sleep(1) #wait for the date picker to load self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).click() self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).clear() time.sleep(1) #wait for the date picker to load self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).send_keys( datetime.strftime(upload_time_object, "%H:%M")) self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).send_keys(Keys.ENTER) self.logger.debug(f"Scheduled the video for {schedule}") else: public_main_button = self.browser.find(By.NAME, Constant.PUBLIC_BUTTON) self.browser.find(By.ID, Constant.RADIO_LABEL, public_main_button).click() self.logger.debug('Made the video {}'.format(Constant.PUBLIC_BUTTON)) video_id = self.__get_video_id() # Check status container and upload progress uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER) while uploading_status_container is not None: uploading_progress = uploading_status_container.get_attribute('value') self.logger.debug('Upload video progress: {}%'.format(uploading_progress)) time.sleep(random_time() * 5) uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER) self.logger.debug('Upload container gone.') done_button = self.browser.find(By.ID, Constant.DONE_BUTTON) # Catch such error as # "File is a duplicate of a video you have already uploaded" if done_button.get_attribute('aria-disabled') == 'true': error_message = self.browser.find(By.XPATH, Constant.ERROR_CONTAINER).text self.logger.error(error_message) return False, None done_button.click() self.logger.debug( "Published the video with video_id = {}".format(video_id)) time.sleep(random_time()) self.browser.get(Constant.YOUTUBE_URL) self.__quit() return True, video_id def __get_video_id(self) -> Optional[str]: video_id = None try: video_url_container = self.browser.find( By.XPATH, Constant.VIDEO_URL_CONTAINER) video_url_element = self.browser.find(By.XPATH, Constant.VIDEO_URL_ELEMENT, element=video_url_container) video_id = video_url_element.get_attribute( Constant.HREF).split('/')[-1] except: self.logger.warning(Constant.VIDEO_NOT_FOUND_ERROR) pass return video_id def __quit(self): self.browser.driver.quit()