diff --git a/README.md b/README.md index 10dc539..98d9083 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,8 @@ An example JSON file would be: { "title": "Best Of James Harden | 2019-20 NBA Season", "description": "Check out the best of James Harden's 2019-20 season so far!", - "tags": ["James", "Harden", "NBA"] + "tags": ["James", "Harden", "NBA"], + "schedule":"10 Jan 2022" } ``` diff --git a/youtube_uploader_selenium/Constant.py b/youtube_uploader_selenium/Constant.py index 921e680..4b6c6e7 100644 --- a/youtube_uploader_selenium/Constant.py +++ b/youtube_uploader_selenium/Constant.py @@ -1,34 +1,39 @@ -class Constant: - """A class for storing constants for YoutubeUploader class""" - YOUTUBE_URL = 'https://www.youtube.com' - YOUTUBE_STUDIO_URL = 'https://studio.youtube.com' - YOUTUBE_UPLOAD_URL = 'https://www.youtube.com/upload' - USER_WAITING_TIME = 1 - VIDEO_TITLE = 'title' - VIDEO_DESCRIPTION = 'description' - VIDEO_TAGS = 'tags' - DESCRIPTION_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/' \ - 'ytcp-uploads-details/div/ytcp-uploads-basics/ytcp-mention-textbox[2]' - TEXTBOX = 'textbox' - TEXT_INPUT = 'text-input' - RADIO_LABEL = 'radioLabel' - STATUS_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[2]/' \ - 'div/div[1]/ytcp-video-upload-progress/span' - NOT_MADE_FOR_KIDS_LABEL = 'NOT_MADE_FOR_KIDS' - - # Thanks to romka777 - MORE_BUTTON = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-video-metadata-editor/div/div/ytcp-button/div' - TAGS_INPUT_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-video-metadata-editor/div/ytcp-video-metadata-editor-advanced/div[2]/ytcp-form-input-container/div[1]/div[2]/ytcp-free-text-chip-bar/ytcp-chip-bar/div' - - TAGS_INPUT = 'text-input' - NEXT_BUTTON = 'next-button' - PUBLIC_BUTTON = 'PUBLIC' - VIDEO_URL_CONTAINER = "//span[@class='video-url-fadeable style-scope ytcp-video-info']" - VIDEO_URL_ELEMENT = "//a[@class='style-scope ytcp-video-info']" - HREF = 'href' - UPLOADED = 'Uploading' - ERROR_CONTAINER = '//*[@id="error-message"]' - VIDEO_NOT_FOUND_ERROR = 'Could not find video_id' - DONE_BUTTON = 'done-button' - INPUT_FILE_VIDEO = "//input[@type='file']" - INPUT_FILE_THUMBNAIL = "//input[@id='file-loader']" +class Constant: + """A class for storing constants for YoutubeUploader class""" + YOUTUBE_URL = 'https://www.youtube.com' + YOUTUBE_STUDIO_URL = 'https://studio.youtube.com' + YOUTUBE_UPLOAD_URL = 'https://www.youtube.com/upload' + USER_WAITING_TIME = 1 + VIDEO_TITLE = 'title' + VIDEO_DESCRIPTION = 'description' + VIDEO_TAGS = 'tags' + VIDEO_SCHEDULE = 'schedule' + DESCRIPTION_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/' \ + 'ytcp-uploads-details/div/ytcp-uploads-basics/ytcp-mention-textbox[2]' + TEXTBOX = 'textbox' + TEXT_INPUT = 'text-input' + RADIO_LABEL = 'radioLabel' + STATUS_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[2]/' \ + 'div/div[1]/ytcp-video-upload-progress/span' + #NOT_MADE_FOR_KIDS_LABEL = 'NOT_MADE_FOR_KIDS' + NOT_MADE_FOR_KIDS_LABEL = 'VIDEO_MADE_FOR_KIDS_NOT_MFK' + + # Thanks to romka777 + MORE_BUTTON = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-video-metadata-editor/div/div/ytcp-button/div' + TAGS_INPUT_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-video-metadata-editor/div/ytcp-video-metadata-editor-advanced/div[2]/ytcp-form-input-container/div[1]/div[2]/ytcp-free-text-chip-bar/ytcp-chip-bar/div' + + TAGS_INPUT = 'text-input' + NEXT_BUTTON = 'next-button' + PUBLIC_BUTTON = 'PUBLIC' + SCHEDULE_BUTTON = 'SCHEDULE' + DATEPICKER_BUTTON = 'datepicker-trigger' + DATEPICKER_FIELD = '//input[contains(@class,"tp-yt-paper-input")]' + VIDEO_URL_CONTAINER = "//span[@class='video-url-fadeable style-scope ytcp-video-info']" + VIDEO_URL_ELEMENT = "//a[@class='style-scope ytcp-video-info']" + HREF = 'href' + UPLOADED = 'Uploading' + ERROR_CONTAINER = '//*[@id="error-message"]' + VIDEO_NOT_FOUND_ERROR = 'Could not find video_id' + DONE_BUTTON = 'done-button' + INPUT_FILE_VIDEO = "//input[@type='file']" + INPUT_FILE_THUMBNAIL = "//input[@id='file-loader']" \ No newline at end of file diff --git a/youtube_uploader_selenium/__init__.py b/youtube_uploader_selenium/__init__.py index 7d1c321..e0da861 100644 --- a/youtube_uploader_selenium/__init__.py +++ b/youtube_uploader_selenium/__init__.py @@ -1,199 +1,217 @@ -"""This module implements uploading videos on YouTube via Selenium using metadata JSON file - to extract its title, description etc.""" - -from typing import DefaultDict, Optional -from selenium_firefox.firefox import Firefox, By, Keys -from collections import defaultdict -import json -import time -from .Constant import * -from pathlib import Path -import logging -import platform - -logging.basicConfig() - - -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) -> None: - self.video_path = video_path - self.thumbnail_path = thumbnail_path - self.metadata_dict = load_metadata(metadata_json_path) - current_working_dir = str(Path.cwd()) - self.browser = Firefox(current_working_dir, current_working_dir) - self.logger = logging.getLogger(__name__) - self.logger.setLevel(logging.DEBUG) - self.__validate_inputs() - - self.is_mac = False - if not any(os_name in platform.platform() for os_name in ["Windows", "Linux"]): - self.is_mac = True - - 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(Constant.USER_WAITING_TIME) - - if self.browser.has_cookies_for_current_website(): - self.browser.load_cookies() - time.sleep(Constant.USER_WAITING_TIME) - self.browser.refresh() - else: - self.logger.info('Please sign in and then press enter') - input() - self.browser.get(Constant.YOUTUBE_URL) - time.sleep(Constant.USER_WAITING_TIME) - self.browser.save_cookies() - - def __write_in_field(self, field, string, select_all=False): - field.click() - time.sleep(Constant.USER_WAITING_TIME) - if select_all: - if self.is_mac: - field.send_keys(Keys.COMMAND + 'a') - else: - field.send_keys(Keys.CONTROL + 'a') - time.sleep(Constant.USER_WAITING_TIME) - field.send_keys(string) - - def __upload(self) -> (bool, Optional[str]): - self.browser.get(Constant.YOUTUBE_URL) - time.sleep(Constant.USER_WAITING_TIME) - self.browser.get(Constant.YOUTUBE_UPLOAD_URL) - time.sleep(Constant.USER_WAITING_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)) - - 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 = self.browser.find(By.ID, Constant.TEXTBOX, 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: - description_field = self.browser.find_all(By.ID, Constant.TEXTBOX)[1] - 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) - self.browser.find(By.ID, Constant.RADIO_LABEL, kids_section).click() - self.logger.debug('Selected \"{}\"'.format( - Constant.NOT_MADE_FOR_KIDS_LABEL)) - - # Advanced options - self.browser.find(By.XPATH, Constant.MORE_BUTTON).click() - self.logger.debug('Clicked MORE OPTIONS') - - tags_container = self.browser.find(By.XPATH, - Constant.TAGS_INPUT_CONTAINER) - tags_field = self.browser.find( - By.ID, Constant.TAGS_INPUT, element=tags_container) - self.__write_in_field(tags_field, ','.join( - self.metadata_dict[Constant.VIDEO_TAGS])) - self.logger.debug( - 'The tags were set to \"{}\"'.format(self.metadata_dict[Constant.VIDEO_TAGS])) - - - self.browser.find(By.ID, Constant.NEXT_BUTTON).click() - self.logger.debug('Clicked {} one'.format(Constant.NEXT_BUTTON)) - - # Thanks to romka777 - 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)) - 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() - - status_container = self.browser.find(By.XPATH, - Constant.STATUS_CONTAINER) - while True: - in_process = status_container.text.find(Constant.UPLOADED) != -1 - if in_process: - time.sleep(Constant.USER_WAITING_TIME) - else: - break - - 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(Constant.USER_WAITING_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() +"""This module implements uploading videos on YouTube via Selenium using metadata JSON file + to extract its title, description etc.""" + +from typing import DefaultDict, Optional +from selenium_firefox.firefox import Firefox, By, Keys +from collections import defaultdict +import json +import time +from .Constant import * +from pathlib import Path +import logging +import platform + +logging.basicConfig() + + +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) -> None: + self.video_path = video_path + self.thumbnail_path = thumbnail_path + self.metadata_dict = load_metadata(metadata_json_path) + current_working_dir = str(Path.cwd()) + self.browser = Firefox(current_working_dir, current_working_dir) + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.DEBUG) + self.__validate_inputs() + + self.is_mac = False + if not any(os_name in platform.platform() for os_name in ["Windows", "Linux"]): + self.is_mac = True + + 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(Constant.USER_WAITING_TIME) + + if self.browser.has_cookies_for_current_website(): + self.browser.load_cookies() + time.sleep(Constant.USER_WAITING_TIME) + self.browser.refresh() + else: + self.logger.info('Please sign in and then press enter') + input() + self.browser.get(Constant.YOUTUBE_URL) + time.sleep(Constant.USER_WAITING_TIME) + self.browser.save_cookies() + + def __write_in_field(self, field, string, select_all=False): + field.click() + time.sleep(Constant.USER_WAITING_TIME) + if select_all: + if self.is_mac: + field.send_keys(Keys.COMMAND + 'a') + else: + field.send_keys(Keys.CONTROL + 'a') + time.sleep(Constant.USER_WAITING_TIME) + field.send_keys(string) + + def __upload(self) -> (bool, Optional[str]): + self.browser.get(Constant.YOUTUBE_URL) + time.sleep(Constant.USER_WAITING_TIME) + self.browser.get(Constant.YOUTUBE_UPLOAD_URL) + time.sleep(Constant.USER_WAITING_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)) + + 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 = self.browser.find(By.ID, Constant.TEXTBOX, 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: + description_field = self.browser.find_all(By.ID, Constant.TEXTBOX)[1] + 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) + self.browser.find(By.ID, Constant.RADIO_LABEL, kids_section).click() + self.logger.debug('Selected \"{}\"'.format( + Constant.NOT_MADE_FOR_KIDS_LABEL)) + + # Advanced options + self.browser.find(By.XPATH, Constant.MORE_BUTTON).click() + self.logger.debug('Clicked MORE OPTIONS') + + tags_container = self.browser.find(By.XPATH, + Constant.TAGS_INPUT_CONTAINER) + + #not working for me + #tags_field = self.browser.find(By.ID, Constant.TAGS_INPUT, element=tags_container) + #self.__write_in_field(tags_field, ','.join(self.metadata_dict[Constant.VIDEO_TAGS])) + #self.logger.debug('The tags were set to \"{}\"'.format(self.metadata_dict[Constant.VIDEO_TAGS])) + #self.logger.info('Manually set the tags to '+",".join(self.metadata_dict[Constant.VIDEO_TAGS])) + + + + self.browser.find(By.ID, Constant.NEXT_BUTTON).click() + self.logger.debug('Clicked {} one'.format(Constant.NEXT_BUTTON)) + + # Thanks to romka777 + 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_string = self.metadata_dict[Constant.VIDEO_SCHEDULE] + + if schedule_string: + + schedule_main_button = self.browser.find(By.NAME, Constant.SCHEDULE_BUTTON) + self.browser.find(By.ID, Constant.RADIO_LABEL,schedule_main_button).click() + + calendar_button = self.browser.find(By.ID,Constant.DATEPICKER_BUTTON).click() + + calendar_field = self.browser.find(By.XPATH,Constant.DATEPICKER_FIELD) + self.__write_in_field(calendar_field,schedule_string,select_all=True) + calendar_field.send_keys(Keys.RETURN) + + 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.SCHEDULE_BUTTON)) + + video_id = self.__get_video_id() + + status_container = self.browser.find(By.XPATH, + Constant.STATUS_CONTAINER) + while True: + in_process = status_container.text.find(Constant.UPLOADED) != -1 + if in_process: + time.sleep(Constant.USER_WAITING_TIME) + else: + break + + 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.browser.execute_script("arguments[0].click();", done_button) + self.logger.debug( + "Published the video with video_id = {}".format(video_id)) + time.sleep(Constant.USER_WAITING_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() \ No newline at end of file