class Youtube: # ------------------------------------------------------------- Init ------------------------------------------------------------- # def __init__(self, cookies_folder_path: str, extensions_folder_path: str, email: Optional[str] = None, password: Optional[str] = None, host: Optional[str] = None, port: Optional[int] = None, screen_size: Optional[Tuple[int, int]] = None, full_screen: bool = False, disable_images: bool = False, login_prompt_callback: Optional[Callable[[str], None]] = None): self.browser = Firefox(cookies_folder_path, extensions_folder_path, host=host, port=port, screen_size=screen_size, full_screen=full_screen, disable_images=disable_images) self.channel_id = None try: self.__internal_channel_id = cookies_folder_path.strip('/').split( '/')[-1] except: self.__internal_channel_id = cookies_folder_path try: if self.browser.login_via_cookies( YT_URL, LOGIN_INFO_COOKIE_NAME) or self.login( email=email, password=password, login_prompt_callback=login_prompt_callback): time.sleep(0.5) self.browser.get(YT_URL) time.sleep(0.5) self.browser.save_cookies() time.sleep(0.5) self.channel_id = self.get_current_channel_id() except Exception as e: print(e) self.quit() raise # ------------------------------------------------------ Public properties ------------------------------------------------------- # @property def is_logged_in(self) -> bool: return self.browser.has_cookie(LOGIN_INFO_COOKIE_NAME) # -------------------------------------------------------- Public methods -------------------------------------------------------- # def login( self, email: Optional[str], password: Optional[str], login_prompt_callback: Optional[Callable[[str], None]] = None) -> bool: org_url = self.browser.driver.current_url self.browser.get(YT_URL) time.sleep(0.5) logged_in = self.is_logged_in if not logged_in and email is not None and password is not None: logged_in = self.__login_auto(email, password) if not logged_in: print('Could not log you in automatically.') logged_in = self.__login_manual( login_prompt_callback=login_prompt_callback) if logged_in: time.sleep(0.5) self.browser.get(YT_URL) time.sleep(0.5) self.browser.save_cookies() time.sleep(0.5) self.browser.get(org_url) return logged_in def upload( self, video_path: str, title: str, description: str, tags: List[str], _timeout: Optional[int] = 60 * 3, # 3 min extra_sleep_after_upload: Optional[int] = None, extra_sleep_before_publish: Optional[int] = None ) -> (bool, Optional[str]): if _timeout is not None: try: with timeout.timeout(_timeout): return self.__upload( video_path, title, description, tags, extra_sleep_after_upload=extra_sleep_after_upload, extra_sleep_before_publish=extra_sleep_before_publish) except Exception as e: print('Upload', e) # self.browser.get(YT_URL) return False, None else: return self.__upload( video_path, title, description, tags, extra_sleep_after_upload=extra_sleep_after_upload, extra_sleep_before_publish=extra_sleep_before_publish) def get_current_channel_id(self) -> Optional[str]: self.browser.get(YT_URL) try: return json.loads( strings.between(self.browser.driver.page_source, 'var ytInitialGuideData = ', '};') + '}')['responseContext']['serviceTrackingParams'][2]['params'][ 0]['value'] except Exception as e: print('get_current_channel_id', e) return None def load_video(self, video_id: str): self.browser.get(self.__video_url(video_id)) def comment_on_video(self, video_id: str, comment: str, pinned: bool = False, _timeout: Optional[int] = 15) -> (bool, bool): if _timeout is not None: try: with timeout.timeout(_timeout): return self.__comment_on_video(video_id, comment, pinned=pinned) except Exception as e: print('Comment', e) # self.browser.get(YT_URL) return False, False else: return self.__comment_on_video(video_id, comment, pinned=pinned) def get_channel_video_ids( self, channel_id: Optional[str] = None, ignored_titles: Optional[List[str]] = None) -> List[str]: video_ids = [] ignored_titles = ignored_titles or [] channel_id = channel_id or self.channel_id try: self.browser.get(self.__channel_videos_url(channel_id)) last_page_source = self.browser.driver.page_source while True: self.browser.scroll(1500) i = 0 max_i = 100 sleep_time = 0.1 should_break = True while i < max_i: i += 1 time.sleep(sleep_time) if len(last_page_source) != len( self.browser.driver.page_source): last_page_source = self.browser.driver.page_source should_break = False break if should_break: break soup = bs(self.browser.driver.page_source, 'lxml') elems = soup.find_all( 'a', { 'id': 'video-title', 'class': 'yt-simple-endpoint style-scope ytd-grid-video-renderer' }) for elem in elems: if 'title' in elem.attrs: should_continue = False title = elem['title'].strip().lower() for ignored_title in ignored_titles: if ignored_title.strip().lower() == title: should_continue = True break if should_continue: continue if 'href' in elem.attrs and '/watch?v=' in elem['href']: vid_id = strings.between(elem['href'], '?v=', '&') if vid_id is not None and vid_id not in video_ids: video_ids.append(vid_id) except Exception as e: print(e) return video_ids def check_analytics(self) -> bool: self.browser.get(YT_STUDIO_URL) try: self.browser.get( self.browser.find( By.XPATH, "//a[@id='menu-item-3']").get_attribute('href')) return True except Exception as e: print(e) return False def quit(self): self.browser.driver.quit() # ------------------------------------------------------- Private methods -------------------------------------------------------- # def __upload( self, video_path: str, title: str, description: str, tags: List[str], extra_sleep_after_upload: Optional[int] = None, extra_sleep_before_publish: Optional[int] = None ) -> (bool, Optional[str]): self.browser.get(YT_URL) time.sleep(1.5) try: self.browser.get(YT_UPLOAD_URL) time.sleep(1.5) self.browser.save_cookies() self.browser.find(By.XPATH, "//input[@type='file']").send_keys(video_path) print('Upload: uploaded video') if extra_sleep_after_upload is not None and extra_sleep_after_upload > 0: time.sleep(extra_sleep_after_upload) title_field = self.browser.find_by('div', id_='textbox') time.sleep(0.5) title_field.send_keys(Keys.BACK_SPACE) time.sleep(0.5) title_field.send_keys(title[:MAX_TITLE_CHAR_LEN]) print('Upload: added title') description_container = self.browser.find( By.XPATH, "/html/body/ytcp-uploads-dialog/paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-details/div/ytcp-uploads-basics/ytcp-mention-textbox[2]" ) description_field = self.browser.find( By.ID, "textbox", element=description_container) description_field.click() time.sleep(0.5) description_field.clear() time.sleep(0.5) description_field.send_keys(description[:MAX_DESCRIPTION_CHAR_LEN]) print('Upload: added description') self.browser.find( By.XPATH, "/html/body/ytcp-uploads-dialog/paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-details/div/div/ytcp-button/div" ).click() print("Upload: clicked more options") tags_container = self.browser.find( By.XPATH, "/html/body/ytcp-uploads-dialog/paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-details/div/ytcp-uploads-advanced/ytcp-form-input-container/div[1]/div[2]/ytcp-free-text-chip-bar/ytcp-chip-bar/div" ) tags_field = self.browser.find(By.ID, "text-input", tags_container) tags_field.send_keys(','.join(tags) + ',') print("Upload: added tags") kids_section = self.browser.find(By.NAME, "NOT_MADE_FOR_KIDS") self.browser.find(By.ID, "radioLabel", kids_section).click() print("Upload: did set NOT_MADE_FOR_KIDS") self.browser.find(By.ID, 'next-button').click() print('Upload: clicked first next') self.browser.find(By.ID, 'next-button').click() print('Upload: clicked second next') public_main_button = self.browser.find(By.NAME, "PUBLIC") self.browser.find(By.ID, 'radioLabel', public_main_button).click() print('Upload: set to public') try: video_url_container = self.browser.find( By.XPATH, "//span[@class='video-url-fadeable style-scope ytcp-video-info']", timeout=2.5) video_url_element = self.browser.find( By.XPATH, "//a[@class='style-scope ytcp-video-info']", element=video_url_container, timeout=2.5) video_id = video_url_element.get_attribute('href').split( '/')[-1] except Exception as e: print(e) video_id = None i = 0 if extra_sleep_before_publish is not None and extra_sleep_before_publish > 0: time.sleep(extra_sleep_before_publish) while True: try: progress_label = self.browser.find_by( 'span', { 'class': 'progress-label style-scope ytcp-video-upload-progress' }, timeout=0.1 ) or self.browser.find_by('span', { 'class': 'progress-label-old style-scope ytcp-video-upload-progress' }, timeout=0.1) progress_label_text = progress_label.text.lower() if 'finished' in progress_label_text or 'process' in progress_label_text: done_button = self.browser.find(By.ID, 'done-button') if done_button.get_attribute( 'aria-disabled') == 'false': done_button.click() print('Upload: published') time.sleep(3) self.browser.get(YT_URL) return True, video_id except Exception as e: print(e) i += 1 if i >= 20: done_button = self.browser.find(By.ID, 'done-button') if done_button.get_attribute( 'aria-disabled') == 'false': done_button.click() print('Upload: published') time.sleep(3) self.browser.get(YT_URL) return True, video_id raise time.sleep(1) except Exception as e: print(e) self.browser.get(YT_URL) return False, None # returns (commented_successfully, pinned_comment_successfully) def __comment_on_video(self, video_id: str, comment: str, pinned: bool = False) -> (bool, bool): self.load_video(video_id) time.sleep(1) self.browser.scroll(150) time.sleep(1) self.browser.scroll(100) time.sleep(1) self.browser.scroll(100) try: header = self.browser.find_by('div', id_='masthead-container', class_='style-scope ytd-app') comment_field = self.browser.find(By.XPATH, "//div[@id='placeholder-area']", timeout=5) self.browser.scroll_to_element(comment_field, header_element=header) time.sleep(0.5) print('comment: clicking comment_field') comment_field.click() print('comment: sending keys') self.browser.find(By.XPATH, "//div[@id='contenteditable-root']", timeout=0.5).send_keys(comment) print('comment: clicking post_comment') self.browser.find( By.XPATH, "//ytd-button-renderer[@id='submit-button' and @class='style-scope ytd-commentbox style-primary size-default']", timeout=0.5).click() if not pinned: return True, False try: dropdown_menu_xpath = "//yt-sort-filter-sub-menu-renderer[@class='style-scope ytd-comments-header-renderer']" dropdown_menu = self.browser.find(By.XPATH, dropdown_menu_xpath) self.browser.scroll_to_element(dropdown_menu, header_element=header) time.sleep(0.5) dropdown_trigger_xpath = "//paper-button[@id='label' and @class='dropdown-trigger style-scope yt-dropdown-menu']" print('comment: clicking dropdown_trigger (open)') self.browser.find(By.XPATH, dropdown_trigger_xpath, element=dropdown_menu, timeout=2.5).click() try: dropdown_menu = self.browser.find(By.XPATH, dropdown_menu_xpath) dropdown_elements = [ elem for elem in self.browser.find_all( By.XPATH, "//a", element=dropdown_menu, timeout=2.5) if 'yt-dropdown-menu' in elem.get_attribute('class') ] last_dropdown_element = dropdown_elements[-1] if last_dropdown_element.get_attribute( 'aria-selected') == 'false': time.sleep(0.25) print('comment: clicking last_dropdown_element') last_dropdown_element.click() else: print( 'comment: clicking dropdown_trigger (close) (did not click last_dropdown_element (did not find it))' ) self.browser.find(By.XPATH, dropdown_trigger_xpath, element=dropdown_menu, timeout=2.5).click() except Exception as e: print(e) print( 'comment: clicking dropdown_trigger (close) (did not click last_dropdown_element (error occured))' ) self.browser.find(By.XPATH, dropdown_trigger_xpath, element=dropdown_menu, timeout=2.5).click() except Exception as e: print(e) # self.browser.scroll(100) time.sleep(2.5) for comment_thread in self.browser.find_all( By.XPATH, "//ytd-comment-thread-renderer[@class='style-scope ytd-item-section-renderer']" ): pinned_element = self.browser.find( By.XPATH, "//yt-icon[@class='style-scope ytd-pinned-comment-badge-renderer']", element=comment_thread, timeout=0.5) pinned = pinned_element is not None and pinned_element.is_displayed( ) if pinned: continue try: # button_3_dots button_3_dots = self.browser.find( By.XPATH, "//yt-icon-button[@id='button' and @class='dropdown-trigger style-scope ytd-menu-renderer']", element=comment_thread, timeout=2.5) self.browser.scroll_to_element(button_3_dots, header_element=header) time.sleep(0.5) print('comment: clicking button_3_dots') button_3_dots.click() popup_renderer_3_dots = self.browser.find( By.XPATH, "//ytd-menu-popup-renderer[@class='style-scope ytd-popup-container']", timeout=2) # dropdown menu item (first) print( 'comment: clicking button_3_dots-dropdown_menu_item (to pin it)' ) self.browser.find( By.XPATH, "//a[@class='yt-simple-endpoint style-scope ytd-menu-navigation-item-renderer']", element=popup_renderer_3_dots, timeout=2.5).click() confirm_button_container = self.browser.find( By.XPATH, "//yt-button-renderer[@id='confirm-button' and @class='style-scope yt-confirm-dialog-renderer style-primary size-default']", timeout=5) # confirm button print('comment: clicking confirm_button') self.browser.find( By.XPATH, "//a[@class='yt-simple-endpoint style-scope yt-button-renderer']", element=confirm_button_container, timeout=2.5).click() time.sleep(2) return True, True except Exception as e: print(e) return True, False # could not find new comment print('no_new_comments') return True, False except Exception as e: print(e) return False, False def __login_auto(self, email: str, password: str) -> bool: self.browser.get(YT_LOGIN_URL) time.sleep(0.5) try: with timeout.timeout(30): email_field = self.browser.find_by('input', { 'id': 'identifierId', 'type': 'email' }) email_field.click() self.browser.send_keys_delay_random(email_field, email) time.sleep(1) self.browser.find_by('div', { 'jscontroller': 'VXdfxd', 'role': 'button' }).click() time.sleep(1) from selenium.webdriver.common.action_chains import ActionChains action = ActionChains(self.browser.driver) pass_container = self.browser.find_by('div', { 'id': 'password', 'jscontroller': 'pxq3x' }) pass_field = self.browser.find_by('input', {'type': 'password'}, in_element=pass_container) action.move_to_element(pass_field).perform() pass_field.click() self.browser.send_keys_delay_random(pass_field, password) time.sleep(1) self.browser.find_by('div', { 'jscontroller': 'VXdfxd', 'role': 'button' }).click() time.sleep(1) except Exception as e: print('__login_auto', e) self.browser.get(YT_URL) time.sleep(0.5) return self.is_logged_in def __login_manual( self, login_prompt_callback: Optional[Callable[[str], None]] = None) -> bool: self.browser.get(YT_LOGIN_URL) time.sleep(0.5) try: if login_prompt_callback is not None: login_prompt_callback(self.__internal_channel_id + ' needs manual input to log in.') input('Log in then press return') except Exception as e: print('__login_manual', e) self.browser.get(YT_URL) time.sleep(0.5) return self.is_logged_in def __video_url(self, video_id: str) -> str: return YT_URL + '/watch?v=' + video_id def __channel_videos_url(self, channel_id: str) -> str: return YT_URL + '/channel/' + channel_id + '/videos?view=0&sort=da&flow=grid' # ---------------------------------------------------------------------------------------------------------------------------------------- #
class YouTubeUploader: """A class for uploading videos on YouTube via Selenium using metadata dict""" def __init__(self, username, cookies_path="") -> None: # Optional cookies_path to override username # for debugging purposes if cookies_path == "": cookies_path = YouTubeLogin.get_cookie_path_from_username(username) self.username = username self.browser = Firefox(full_screen=False, cookies_folder_path=cookies_path, default_find_func_timeout=10) self.logger = logging.getLogger() def __validate_inputs(self): if Constant.VIDEO_TITLE not in self.metadata_dict: self.logger.warning("The video title was not found in metadata") 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)) for key in (Constant.VIDEO_DESCRIPTION, Constant.PLAYLIST, Constant.TAGS): if key not in self.metadata_dict: self.metadata_dict[key] = "" title = self.metadata_dict[Constant.VIDEO_TITLE] if len(title) > Constant.MAX_TITLE_LENGTH: self.logger.warning("Truncating title to {} characters".format( Constant.MAX_TITLE_LENGTH)) self.metadata_dict[ Constant.VIDEO_TITLE] = title[:Constant.MAX_TITLE_LENGTH] description = self.metadata_dict[Constant.VIDEO_DESCRIPTION] if len(description) > Constant.MAX_DESCRIPTION_LENGTH: self.logger.warning( "Truncating description to {} characters".format( Constant.MAX_DESCRIPTION_LENGTH)) self.metadata_dict[ Constant. VIDEO_DESCRIPTION] = description[:Constant. MAX_DESCRIPTION_LENGTH] tags = self.metadata_dict[Constant.TAGS] if len(tags) > Constant.MAX_TAGS_LENGTH: self.logger.warning("Truncating tags to {} characters".format( Constant.MAX_TAGS_LENGTH)) self.metadata_dict[Constant.TAGS] = tags[:Constant.MAX_TAGS_LENGTH] def upload(self, video_path, metadata) -> (bool, Optional[str]): try: self.video_path = video_path self.metadata_dict = metadata self.__validate_inputs() self.__login() return self.__upload() except Exception as e: self.__quit() raise def __login(self): self.browser.get(Constant.YOUTUBE_URL) time.sleep(Constant.USER_WAITING_TIME) if self.browser.find(By.XPATH, Constant.USER_AVATAR_XPATH, timeout=5) is not None: # already logged in return if self.browser.has_cookies_for_current_website(): self.browser.load_cookies() time.sleep(Constant.USER_WAITING_TIME) self.browser.refresh() else: raise Exception("Could not find cookies at path {}".format( self.browser.cookies_folder_path)) def __find_playlist_checkbox_no_search(self, name): for element in self.browser.find_all(By.XPATH, Constant.PLAYLIST_LABEL): name_element = element.find_element_by_xpath( ".//span/span[@class='label label-text style-scope ytcp-checkbox-group']" ) if name_element.text == name: return element.find_element_by_xpath(".//ytcp-checkbox-lit") return None def __find_playlist_checkbox(self, name): try: checkbox = self.__find_playlist_checkbox_no_search(name) if not checkbox: # sometimes a newly created playlist will not show up in the list of playlists # we can search for it to update the list search = self.browser.find(By.XPATH, Constant.PLAYLIST_SEARCH) # search behaves weird with opening brackets / parentheses, # possibly other characters as well # need to investigate this further: # Uncaught SyntaxError: unterminated character class # Uncaught SyntaxError: unterminated parenthetical phrases = re.split(r"[\[(]", name) phrases.sort(key=lambda p: len(p)) search.click() time.sleep(Constant.USER_WAITING_TIME) search.clear() time.sleep(Constant.USER_WAITING_TIME) search.send_keys(phrases[-1]) checkbox = self.__find_playlist_checkbox_no_search(name) # clear search so we can create new playlist self.browser.find( By.XPATH, Constant.PLAYLIST_SEARCH_CLEAR_BUTTON).click() return checkbox except Exception as e: logging.error(e) return None 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)) time.sleep(Constant.USER_WAITING_TIME) if (title_field := self.browser.find( By.ID, Constant.TEXTBOX, timeout=30)) is None: self.logger.error('Could not find title field') return False, None title_field.click() time.sleep(Constant.USER_WAITING_TIME) title_field.clear() time.sleep(Constant.USER_WAITING_TIME) if sys.platform == 'darwin': title_field.send_keys(Keys.COMMAND + 'a') else: title_field.send_keys(Keys.CONTROL + 'a') time.sleep(Constant.USER_WAITING_TIME) title_field.send_keys(self.metadata_dict[Constant.VIDEO_TITLE]) self.logger.debug('The video title was set to \"{}\"'.format( self.metadata_dict[Constant.VIDEO_TITLE])) video_description = self.metadata_dict[Constant.VIDEO_DESCRIPTION] tags = self.metadata_dict[Constant.TAGS] playlist = self.metadata_dict[Constant.PLAYLIST] if video_description: description_container = self.browser.find( By.XPATH, Constant.DESCRIPTION_CONTAINER) description_field = self.browser.find( By.ID, Constant.TEXTBOX, element=description_container) description_field.click() time.sleep(Constant.USER_WAITING_TIME) description_field.clear() time.sleep(Constant.USER_WAITING_TIME) description_field.send_keys(video_description) self.logger.debug('The video description was set to \"{}\"'.format( video_description)) if playlist: self.browser.find(By.XPATH, Constant.PLAYLIST_CONTAINER).click() time.sleep(Constant.USER_WAITING_TIME) checkbox = self.__find_playlist_checkbox(playlist) if checkbox is None: self.logger.info( "Could not find playlist checkbox, attempting to create new playlist" ) playlist_new_button = self.browser.find( By.XPATH, Constant.PLAYLIST_NEW_BUTTON) self.browser.move_to_element(playlist_new_button) time.sleep(Constant.USER_WAITING_TIME) playlist_new_button.click() time.sleep(Constant.USER_WAITING_TIME) playlist_title = self.browser.find(By.XPATH, Constant.PLAYLIST_NEW_TITLE) if playlist_title is None: logging.error("Could not find playlist title field") return False, None playlist_title.click() time.sleep(Constant.USER_WAITING_TIME) playlist_title.send_keys(playlist) time.sleep(Constant.USER_WAITING_TIME) # Set playlist visibility self.browser.find( By.XPATH, Constant.PLAYLIST_VISIBILITY_DROPDOWN).click() time.sleep(Constant.USER_WAITING_TIME) playlist_visibility = self.browser.find( By.XPATH, '//*[@test-id="{}"]'.format( self.metadata_dict['visibility'])) if playlist_visibility is None: logging.error( "Could not find playlist visibility option {}".format( self.metadata_dict['visibility'])) return False, None playlist_visibility.click() time.sleep(Constant.USER_WAITING_TIME) self.browser.find(By.XPATH, Constant.PLAYLIST_CREATE_BUTTON).click() time.sleep(Constant.USER_WAITING_TIME) checkbox = self.__find_playlist_checkbox(playlist) if checkbox is None: logging.error("Could not find playlist: {}".format(playlist)) return False, None else: checkbox.click() time.sleep(Constant.USER_WAITING_TIME) self.browser.find(By.XPATH, Constant.PLAYLIST_DONE_BUTTON).click() time.sleep(Constant.USER_WAITING_TIME) # hide tooltips which can obscure buttons tooltips = self.browser.find_all(By.XPATH, Constant.TOOLTIP) if tooltips is not None: for element in tooltips: self.browser.execute_script_on_element( "arguments[0].style.display = 'none'", element) if tags: self.browser.find(By.XPATH, Constant.MORE_OPTIONS_CONTAINER).click() time.sleep(Constant.USER_WAITING_TIME) self.browser.find(By.XPATH, Constant.TAGS_TEXT_INPUT).send_keys(tags) time.sleep(Constant.USER_WAITING_TIME) time.sleep(Constant.USER_WAITING_TIME) kids_section = self.browser.find(By.NAME, Constant.NOT_MADE_FOR_KIDS_LABEL) self.browser.scroll_to_element(kids_section) time.sleep(Constant.USER_WAITING_TIME) self.browser.find(By.ID, Constant.RADIO_LABEL, kids_section).click() self.logger.debug('Selected \"{}\"'.format( Constant.NOT_MADE_FOR_KIDS_LABEL)) self.browser.find(By.ID, Constant.NEXT_BUTTON).click() self.logger.debug('Clicked {}'.format(Constant.NEXT_BUTTON)) self.browser.find(By.ID, Constant.NEXT_BUTTON).click() self.logger.debug('Clicked another {}'.format(Constant.NEXT_BUTTON)) visibility_button = self.browser.find(By.NAME, self.metadata_dict['visibility']) self.browser.find(By.ID, Constant.RADIO_LABEL, visibility_button).click() self.logger.debug('Made the video {}'.format( self.metadata_dict['visibility'])) 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)) # wait for youtube to save the video info while self.browser.find(By.XPATH, Constant.VIDEO_PUBLISHED_DIALOG) is None: time.sleep(1) return True, video_id
class Pinterest: def __init__(self, cookies_folder_path: str, extensions_folder_path: str, host: Optional[str] = None, port: Optional[int] = None, headless: bool = False): self.browser = Firefox(cookies_folder_path, extensions_folder_path, host=host, port=port, headless=headless) try: self.browser.get(PT_URL) time.sleep(1.5) if self.browser.has_cookies_for_current_website(): self.browser.load_cookies() time.sleep(1.5) self.browser.refresh() time.sleep(0.5) else: input('Log in then press enter') self.browser.get(PT_URL) time.sleep(1.5) self.browser.save_cookies() except: traceback.print_exc() self.browser.driver.quit() raise def follow(self, user_name: str) -> bool: try: self.browser.get(UrlCreator.user_url(user_name)) rand.sleep(0.5, 1) follow_container = self.browser.find( By.XPATH, "//div[contains(@data-test-id, 'user-follow-button')]", timeout=2.5) if follow_container: follow_button = self.browser.find( By.XPATH, "//div[contains(@class, 'tBJ dyH iFc yTZ erh tg7 mWe')]", follow_container) if follow_button is not None and follow_button.text == "Follow": follow_button.click() rand.sleep(0.5, 1) else: return False follow_buttons_updated = self.browser.find_all( By.XPATH, "//div[contains(@class, 'tBJ dyH iFc yTZ pBj tg7 mWe')]") for elem in follow_buttons_updated: if elem.text == "Following": print('user.text', elem.text) return True elif follow_container is None: user = self.browser.find( By.XPATH, "//div[contains(@class, 'tBJ dyH iFc yTZ erh tg7 mWe')]") if user.text == "Follow": user.click() rand.sleep(1, 1.5) user_updated = self.browser.find( By.XPATH, "//div[contains(@class, 'tBJ dyH iFc yTZ erh tg7 mWe')]") return user_updated.text == "Following" except: traceback.print_exc() return False def unfollow(self, user_name: str) -> bool: try: self.browser.get(UrlCreator.user_url(user_name)) rand.sleep(1, 2) user_container = self.browser.find( By.XPATH, "//div[contains(@data-test-id, 'user-unfollow-button')]") if user_container is not None: user_element = self.browser.find( By.XPATH, "//button[contains(@class, 'RCK Hsu USg Vxj aZc Zr3 hA- GmH adn a_A gpV hNT iyn BG7 NTm KhY')]", user_container) user_element.click() rand.sleep(0.5, 1) return self.browser.find_by( 'button', attributes={ 'class': 'RCK Hsu USg Vxj aZc Zr3 hA- GmH adn Il7 Jrn hNT iyn BG7 NTm KhY', 'type': 'button' }) is not None else: user = self.browser.find( By.XPATH, "//div[contains(@class, 'tBJ dyH iFc yTZ pBj tg7 mWe')]") if user.text == "Following": user.click() rand.sleep(1, 1.5) user_updated = self.browser.find( By.XPATH, "//div[contains(@class, 'tBJ dyH iFc yTZ erh tg7 mWe')]") return user_updated.text == "Follow" except: traceback.print_exc() return False def repin(self, pin_id: str, board_name: str, needs_repin_id: bool = False) -> Tuple[bool, Optional[str]]: try: self.browser.get(UrlCreator.pin_url(pin_id)) rand.sleep(0.7, 1.2) board_dropdown = self.browser.find( By.XPATH, "//div[contains(@class, 'tBJ dyH iFc _yT pBj DrD IZT swG z-6')]", timeout=3) if board_dropdown is not None: board_dropdown.click() rand.sleep(0.1, 0.5) else: if self.__create_and_save_to_board( board_name) and needs_repin_id: return True, self.__get_link_to_repinned_post() elif self.__create_and_save_to_board( board_name) and needs_repin_id is False: return True, None return False, None boards = self.browser.find_all( By.XPATH, "//div[contains(@class, 'tBJ dyH iFc yTZ pBj DrD IZT mWe z-6')]", timeout=5) for board in boards: if board.text == board_name: board.click() rand.sleep(0.2, 0.5) break else: self.browser.find( By.XPATH, "//div[contains(@class, 'rDA wzk zI7 iyn Hsu')]" ).click() # create board button text_tag = self.browser.find( By.XPATH, "//input[contains(@id, 'boardEditName')]") text_tag.send_keys(board_name) rand.sleep(0.5, 1) self.browser.find( By.XPATH, "//button[contains(@class, 'RCK Hsu USg Vxj aZc Zr3 hA- GmH adn Il7 Jrn hNT iyn BG7 NTm KhY')]" ).click() # create_button if needs_repin_id: return True, self.__get_link_to_repinned_post() else: return self.browser.find( By.XPATH, "//div[contains(@class, 'Eqh Shl s7I zI7 iyn Hsu')]" ) is not None, None rand.sleep(0.5, 1) if needs_repin_id: return self.browser.find( By.XPATH, "//div[contains(@class, 'Eqh Shl s7I zI7 iyn Hsu')]" ) is not None, self.__get_link_to_repinned_post() else: return self.browser.find( By.XPATH, "//div[contains(@class, 'Eqh Shl s7I zI7 iyn Hsu')]" ) is not None, None except: traceback.print_exc() return False, None def __get_link_to_repinned_post(self) -> Optional[str]: try: saved_to_button = self.browser.find( By.XPATH, "//div[contains(@class, 'Shl ujU zI7 iyn Hsu')]", timeout=3) full_link = self.browser.find( By.CSS_SELECTOR, 'a', saved_to_button).get_attribute('href') self.browser.get(full_link) print(full_link) rand.sleep(2.5, 3) latest_image_box = self.browser.find( By.XPATH, "//div[contains(@class, 'Yl- MIw Hb7')]", timeout=5) pin_id = self.browser.find( By.XPATH, "//div[contains(@data-test-id, 'pin')]", latest_image_box).get_attribute('data-test-pin-id') rand.sleep(0.1, 0.3) return pin_id except: traceback.print_exc() return None def __create_and_save_to_board(self, board_name: str) -> bool: try: print('I am in the __create_and_save_to_board func') self.browser.find( By.XPATH, "//div[contains(@class, 'tBJ dyH iFc MF7 erh DrD IZT mWe')]" ).click() # save button self.browser.find( By.XPATH, "//div[contains(@class, 'Umk fte zI7 iyn Hsu')]" ).click() # create_board_button text_tag = self.browser.find( By.XPATH, "//input[contains(@id, 'boardEditName')]") text_tag.send_keys(board_name) rand.sleep(0.5, 0.8) self.browser.find( By.XPATH, "//button[contains(@class, 'RCK Hsu USg Vxj aZc Zr3 hA- GmH adn Il7 Jrn hNT iyn BG7 NTm KhY')]" ).click() # create_button rand.sleep(1, 1.5) return self.browser.find( By.XPATH, "//button[contains(@class, 'RCK Hsu USg Vxj aZc Zr3 hA- GmH adn Il7 Jrn hNT iyn BG7 NTm KhY')]", timeout=3) is None except: traceback.print_exc() return False def get_board_followers( self, user_name: str, board_name: str, ignored_users: List[str], number_of_users_to_follow, full_board_url: str = None ) -> Optional[Tuple[List[str], List[str]]]: try: if full_board_url is not None: self.browser.get(full_board_url) else: self.browser.get(UrlCreator.board_url(user_name, board_name)) rand.sleep(1, 1.5) followers_container = self.browser.find( By.XPATH, "//div[contains(@class, 'rLK iyn eEj FTD L4E DI9 BG7')]") if followers_container is not None: followers_container.click() rand.sleep(1, 1.5) saved_users = 0 final_users = [] while number_of_users_to_follow >= saved_users: try: users_list = self.browser.find_all( By.XPATH, "//div[contains(@class, 'Module User hasText thumb medium')]" ) users_length_before = len(final_users) for user_container in users_list: try: print(user_container.text) print('im in the for') user_url = self.browser.find( By.CSS_SELECTOR, 'a', user_container).get_attribute('href') user_name = user_url.split('.com/')[1].split( '/')[0] if user_name in ignored_users: continue ignored_users.append(user_name) final_users.append(user_name) saved_users += 1 print(saved_users, ':', user_name) if saved_users == number_of_users_to_follow: return (final_users, ignored_users) except: traceback.print_exc() except: traceback.print_exc() users_length_after = len(final_users) see_more_button = self.browser.find( By.XPATH, "//div[contains(@class, 'tBJ dyH iFc yTZ pBj tg7 mWe')]", timeout=1.5) if see_more_button is None or users_length_before == users_length_after: return (final_users, ignored_users) see_more_button.click() rand.sleep(1, 1.5) except: traceback.print_exc() return None def search_pinterest_boards(self, search_term: str, number_of_boards_to_get: int = 35 ) -> Optional[List[Tuple[str, str]]]: try: self.browser.get(UrlCreator.search_board_url(search_term)) rand.sleep(1, 1.5) if self.browser.find(By.XPATH, "//div[contains(@class, 'noResults')]"): return None board_names_container = self.browser.find_all( By.XPATH, "//div[contains(@class, 'Yl- MIw Hb7')]") number_of_saved_boards = 0 board_urls = [] while True: before_scroll = self.browser.current_page_offset_y() for board_name_element in board_names_container: try: full_board_url = self.browser.find( By.CSS_SELECTOR, 'a', board_name_element).get_attribute('href') board_info = full_board_url.split('.com/')[1] user_name = board_info.split('/')[0] board_name = board_info.split('/')[1] if (user_name, board_name) in board_urls: continue board_urls.append((user_name, board_name)) number_of_saved_boards += 1 if number_of_boards_to_get == number_of_saved_boards: return board_urls except: traceback.print_exc() self.browser.scroll(1000) rand.sleep(0.5, 1.5) after_scroll = self.browser.current_page_offset_y() if after_scroll == before_scroll: return board_urls except: traceback.print_exc() return None def get_pins_from_home_feed(self) -> Optional[List[str]]: try: self.browser.get(UrlCreator.home_feed_url()) rand.sleep(1, 1.5) home_pins = [] home_pin_containers = self.browser.find_all( By.XPATH, "//div[contains(@class, 'Yl- MIw Hb7')]") for pin in home_pin_containers: try: full_url = self.browser.find(By.CSS_SELECTOR, 'a', pin).get_attribute('href') if 'pinterest.com' not in full_url: continue if 'pin/' in full_url: pin_id = full_url.split('pin/')[1] home_pins.append(pin_id) except: traceback.print_exc() return home_pins except: traceback.print_exc() return None def post_pin(self, image_path: str, board_name: str, title: Optional[str] = None, description: Optional[str] = None, url: Optional[str] = None) -> Optional[str]: try: self.browser.get(UrlCreator.pin_builder_url()) rand.sleep(1, 1.5) image_box = self.browser.find_by('div', class_='DUt XiG zI7 iyn Hsu') if not image_box: raise 'did not find image_box' image_input = self.browser.find(By.CSS_SELECTOR, 'input', image_box) if not image_input: raise 'did not find image_input' image_input.send_keys(image_path) rand.sleep(1, 1.5) select_board_button = self.browser.find_by( 'button', attributes={'data-test-id': 'board-dropdown-select-button'}) select_board_button.click() rand.sleep(1, 1.5) board_search_field = self.browser.find_by('input', id_='pickerSearchField') self.browser.send_keys_delay_random(board_search_field, board_name) rand.sleep(1, 1.5) boards = self.browser.find_all_by( 'div', class_='tBJ dyH iFc yTZ pBj DrD IZT mWe z-6', timeout=2.5) exists_board = False if boards and len(boards) > 0: for board in boards: if board.text == board_name: exists_board = True board.click() rand.sleep(0.1, 0.5) break if not exists_board: dropdown_boards = self.browser.find_by( 'div', class_='DUt qJc sLG zI7 iyn Hsu') create_board = self.browser.find_by( 'div', class_='rDA wzk zI7 iyn Hsu', in_element=dropdown_boards) if create_board is not None: create_board.click() rand.sleep(0.1, 0.5) board_name_textfield = self.browser.find_by( 'input', id_='boardEditName', timeout=2) if board_name_textfield.get_attribute('value') != board_name: while len(board_name_textfield.get_attribute('value')) > 0: board_name_textfield.send_keys(Keys.BACK_SPACE) board_name_textfield = self.browser.find_by( 'input', id_='boardEditName', timeout=2) self.browser.send_keys_delay_random( board_name_textfield, board_name) rand.sleep(0.5, 1) create_board_button = self.browser.find_by( 'button', class_= 'RCK Hsu USg adn CCY czT F10 xD4 fZz hUC Il7 Jrn hNT BG7 NTm KhY', timeout=2.5) or self.browser.find_by( 'button', {'type': 'submit'}, timeout=2.5) create_board_button.click() rand.sleep(0.5, 1) just_created_board_save_button = self.browser.find_by( 'div', class_='tBJ dyH iFc yTZ erh DrD IZT mWe') just_created_board_save_button.click() rand.sleep(0.5, 1.5) if title: title_box = self.browser.find_by( 'div', class_='CDp xcv L4E zI7 iyn Hsu') if title_box: title_textfield = self.browser.find( By.CSS_SELECTOR, "textarea", title_box) if title_textfield: self.browser.send_keys_delay_random( title_textfield, title) rand.sleep(0.5, 1.5) if description: about_box = self.browser.find_by( 'div', class_='Jea Tte ujU xcv L4E zI7 iyn Hsu', timeout=2) if about_box: about_textfield = self.browser.find_by( 'div', class_='notranslate public-DraftEditor-content', in_element=about_box, timeout=2) if about_textfield: self.browser.send_keys_delay_random( about_textfield, description) rand.sleep(0.5, 1.5) if url: url_box = self.browser.find_by( 'div', {'data-test-id': 'pin-draft-link'}) if url_box: url_textfield = self.browser.find(By.CSS_SELECTOR, "textarea", url_box) if url_textfield: self.browser.send_keys_delay_random(url_textfield, url) rand.sleep(0.5, 1.5) save_button = self.browser.find_by( 'button', {'data-test-id': 'board-dropdown-save-button'}) save_button.click() rand.sleep(0.1, 0.5) see_it_now = self.browser.find_by('div', {'data-test-id': 'seeItNow'}) full_pin_url = self.browser.find(By.CSS_SELECTOR, 'a', see_it_now).get_attribute('href') return full_pin_url.split('pin/')[1] except: traceback.print_exc() return None
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() 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: field.send_keys(Keys.COMMAND + '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] if video_description: # description_container = self.browser.find(By.XPATH, Constant.DESCRIPTION_CONTAINER) description_field = self.browser.find_all(By.ID, Constant.TEXTBOX)[1] # print(len(description_field)) # for each_t in description_field: # print(each_t) # description_field = self.browser.find(By.ID, Constant.TEXTBOX, element=description_container) 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 {}'.format(Constant.NEXT_BUTTON)) 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)) 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()
class YouTubeUploader: """A class for uploading videos on YouTube via Selenium using metadata JSON file to extract its title, description etc""" def __init__(self, headless: bool, cookies_path: str, channel: str) -> None: self.channel = channel cookies_path = str(Path(cookies_path)) if cookies_path else str( Path.cwd()) assert os.path.isdir( cookies_path), f"Directory '{cookies_path}' does not exist!" self.browser = Firefox(cookies_path, cookies_path, headless=headless) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.browser.driver.quit() def login(self, username: Optional[str], password: Optional[str]) -> bool: self.browser.get(Constant.YOUTUBE_URL) if self.browser.has_cookies_for_current_website(): print("Loading cookies") self.browser.load_cookies() YOUTUBE_STUDIO_URL = f"https://studio.youtube.com/channel/{self.channel}" self.browser.get(YOUTUBE_STUDIO_URL) if self.browser.driver.current_url == YOUTUBE_STUDIO_URL: print("Logged in!") return True if None in {username, password} or "" in { username.strip(), password.strip() }: print("Username or password not provided!") return False # [G] fill in the username (email) print(f"Sending keys: email: '{username}@fel.cvut.cz'") email_field = self.browser.find(By.ID, "identifierId") email_field.click() email_field.clear() email_field.send_keys(f"{username}@fel.cvut.cz") # [G] click 'next' button print("Click: next") self.browser.find(By.ID, "identifierNext").click() if self.browser.find(By.XPATH, Constant.G_LOGIN_FAILED, timeout=2) is not None: print("Invalid username!") return False # [SSO] fill in the username print(f"Sending keys: SSO username: '******'") sso_username_field = self.browser.find(By.ID, "username") sso_username_field.click() sso_username_field.clear() sso_username_field.send_keys(username) # [SSO] fill in the password print(f"Sending keys: SSO password: '******'") sso_password_field = self.browser.find(By.ID, "password") sso_password_field.click() sso_password_field.clear() sso_password_field.send_keys(password) # [SSO] click 'SSO login' button print("Click: SSO login") self.browser.find(By.NAME, "_eventId_proceed").click() if self.browser.find(By.CLASS_NAME, "error-message", timeout=2) is not None: print("Invalid username or password!") return False print("Waiting for Google login...") if self.browser.find(By.ID, "upload-icon", timeout=20) is None: print("Login timeout!") return False if self.browser.driver.current_url != YOUTUBE_STUDIO_URL: print("Login failed!") return False # save cookies print("Saving cookies") self.browser.save_cookies() print("Logged in!") return True def upload(self, video: Video) -> (bool, Optional[str]): self.browser.get(f"https://studio.youtube.com/channel/{self.channel}") print("Click: upload") self.browser.find(By.ID, "upload-icon").click() print(f"Attaching video: '{video.filename}'") self.browser.find(By.XPATH, Constant.INPUT_FILE_VIDEO).send_keys( os.path.abspath(video.filename)) if video.description: print(f"Sending keys: video description: '{video.description}'") description_field = self.browser.find( By.XPATH, Constant.DESCRIPTION_CONTAINER) description_field.click() description_field.clear() description_field.send_keys(video.description) if video.playlist: print("Click: playlist dropdown") self.browser.find(By.CLASS_NAME, "dropdown").click() # TODO: playlist ID print("Selecting the playlist") playlists = self.browser.find_all( By.CSS_SELECTOR, "label.style-scope.ytcp-checkbox-group.ytcp-checkbox-label") for playlist in playlists: playlist_name = self.browser.find(By.CSS_SELECTOR, "span.label.label-text", playlist).text if video.playlist == playlist_name: print(f"Click: playlist checkbox: '{playlist_name}'") self.browser.find(By.CSS_SELECTOR, "ytcp-checkbox-lit.ytcp-checkbox-group", playlist).click() break else: # create a new playlist print("Playlist not found in the list, creating a new one") self.browser.find(By.CLASS_NAME, "new-playlist-button").click() create_playlist_form = self.browser.find( By.ID, "create-playlist-form") print(f"Sending keys: playlist name: '{video.playlist}'") textarea = self.browser.find(By.TAG_NAME, "textarea", element=create_playlist_form) textarea.click() textarea.clear() textarea.send_keys(video.playlist) print("Click: visibility dropdown") self.browser.find(By.CLASS_NAME, "visibility", element=create_playlist_form).click() print(f"Click: visibility: '{video.privacy.upper()}'") self.browser.find( By.CLASS_NAME, f'paper-item[test-id="{video.privacy.upper()}"]', element=create_playlist_form).click() print("Click: create playlist") self.browser.find(By.CLASS_NAME, "create-playlist-button").click() print("Click: done") time.sleep(Constant.USER_WAITING_TIME) self.browser.find(By.CLASS_NAME, "done-button").click() print("Click: not made for kids") kids_section = self.browser.find(By.NAME, Constant.NOT_MADE_FOR_KIDS_LABEL) self.browser.find(By.ID, Constant.RADIO_LABEL, kids_section).click() print(f"Sending keys: video title: '{video.title}'") title_field = self.browser.find(By.ID, Constant.TEXTBOX, timeout=30) title_field.click() title_field.clear() title_field.send_keys(Keys.CONTROL + 'a') title_field.send_keys(video.title) for _ in range(3): print("Click: next") self.browser.find(By.ID, Constant.NEXT_BUTTON).click() if video.privacy: print(f"Click: visibility: '{video.privacy.upper()}'") privacy_button = self.browser.find(By.NAME, video.privacy.upper()) self.browser.find(By.ID, Constant.RADIO_LABEL, privacy_button).click() time.sleep(Constant.USER_WAITING_TIME) video_id = self.__get_video_id() print(f"Video link: https://youtu.be/{video_id}") container = self.browser.find( By.CSS_SELECTOR, ".left-button-area.style-scope.ytcp-uploads-dialog") while True: texts = [ a.text for a in self.browser.find_all( By.CLASS_NAME, "progress-label", container) ] print(f'\r{texts[-1]}\033[K', end="") if any(substring in element for substring in {"complete", "dokončeno"} for element in texts): print() time.sleep(Constant.USER_WAITING_TIME) break else: time.sleep(0.5) print("Click: done") 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 print(f"Upload ERROR: {error_message}") return False, None done_button.click() self.browser.get(Constant.YOUTUBE_URL) 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: print(f"ERROR: could not find video_id") return video_id