├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── upload.py └── youtube_uploader_selenium ├── Constant.py └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/**/usage.statistics.xml 8 | .idea/**/dictionaries 9 | .idea/**/shelf 10 | 11 | # Generated files 12 | .idea/**/contentModel.xml 13 | 14 | # Sensitive or high-churn files 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | .idea/**/dbnavigator.xml 22 | 23 | # Gradle 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # Gradle and Maven with auto-import 28 | # When using Gradle or Maven with auto-import, you should exclude module files, 29 | # since they will be recreated, and may cause churn. Uncomment if using 30 | # auto-import. 31 | # .idea/artifacts 32 | # .idea/compiler.xml 33 | # .idea/jarRepositories.xml 34 | # .idea/modules.xml 35 | # .idea/*.iml 36 | # .idea/modules 37 | # *.iml 38 | # *.ipr 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | # Mac 74 | /.DS_Store 75 | 76 | # Project temp data 77 | /youtube_uploader_selenium/__pycache__/ 78 | /cookies/ 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The Python Packaging Authority 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | Python script to upload videos on YouTube using Selenium 3 | that allows to upload more than 61 videos per day 4 | which is the maximum [[1]](https://github.com/tokland/youtube-upload/issues/268) for all other tools that use the [YouTube Data API v3](https://developers.google.com/youtube/v3). 5 | 6 | ###### 1: Since the projects that enable the YouTube Data API have a default quota allocation of `10,000` units per day [[2]](https://developers.google.com/youtube/v3/getting-started#calculating-quota-usage) and a video upload has a cost of approximately `1,600` units [[3]](https://developers.google.com/youtube/v3/getting-started#quota): `10,000 / 1,600 = 6.25`. 7 | 8 | Instead, this script is only restricted by a daily upload limit for a channel on YouTube: 9 | > 100 videos is the limit in the first 24 hours, then drops to 50 every 24 hours after that. [[4]](https://support.google.com/youtube/thread/1187675?hl=en) 10 | 11 | ## Package Installation 12 | ```bash 13 | pip3 install --upgrade youtube-uploader-selenium 14 | ``` 15 | 16 | ## Script Installation 17 | 18 | ```bash 19 | git clone https://github.com/linouk23/youtube_uploader_selenium 20 | cd youtube-uploader-selenium 21 | ``` 22 | 23 | ## Package Usage 24 | ```python 25 | from youtube_uploader_selenium import YouTubeUploader 26 | 27 | video_path = '123/rockets.flv' 28 | metadata_path = '123/rockets_metadata.json' 29 | 30 | uploader = YouTubeUploader(video_path, metadata_path, thumbnail_path) 31 | was_video_uploaded, video_id = uploader.upload() 32 | assert was_video_uploaded 33 | ``` 34 | 35 | ## Script Usage 36 | At a minimum, just specify a video: 37 | 38 | ```bash 39 | python3 upload.py --video rockets.flv 40 | ``` 41 | 42 | If it is the first time you've run the script, a browser window should popup and prompt you to provide YouTube credentials (and then simply press Enter after a successful login). 43 | A token will be created and stored in a file in the local directory for subsequent use. 44 | 45 | Video title, description and other metadata can specified via a JSON file using the `--meta` flag: 46 | ```bash 47 | python3 upload.py --video rockets.flv --meta metadata.json 48 | ``` 49 | 50 | An example JSON file would be: 51 | ```json 52 | { 53 | "title": "Best Of James Harden | 2019-20 NBA Season", 54 | "description": "Check out the best of James Harden's 2019-20 season so far!", 55 | "tags": ["James", "Harden", "NBA"], 56 | "schedule": "06/05/2013, 23:05" 57 | } 58 | ``` 59 | For your convenience, the format string for the schedule is `%m/%d/%Y, %H:%M` 60 | 61 | ## Dependencies 62 | * geckodriver 63 | * Firefox **[(Works with version 77)](https://ftp.mozilla.org/pub/firefox/releases/)** 64 | * selenium_firefox 65 | 66 | ## FAQ 67 | * [Selenium using Python - Geckodriver executable needs to be in PATH](https://stackoverflow.com/questions/40208051/selenium-using-python-geckodriver-executable-needs-to-be-in-path) 68 | * [SessionNotCreatedException: Message: Unable to find a matching set of capabilities](https://stackoverflow.com/questions/47782650/selenium-common-exceptions-sessionnotcreatedexception-message-unable-to-find-a) 69 | * Please make sure that Firefox browser is installed on your machine. 70 | 71 | ## Contributing 72 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 73 | 74 | ## Feedback 75 | If you find a bug / want a new feature to be added, please [open an issue](https://github.com/tokland/youtube-upload/issues). 76 | 77 | ## License 78 | [MIT](https://choosealicense.com/licenses/mit/) 79 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium_firefox 2 | selenium < 4 -------------------------------------------------------------------------------- /upload.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from youtube_uploader_selenium import YouTubeUploader 3 | from typing import Optional 4 | 5 | 6 | def main(video_path: str, 7 | metadata_path: Optional[str] = None, 8 | thumbnail_path: Optional[str] = None, 9 | profile_path: Optional[str] = None): 10 | uploader = YouTubeUploader(video_path, metadata_path, thumbnail_path, profile_path) 11 | was_video_uploaded, video_id = uploader.upload() 12 | assert was_video_uploaded 13 | 14 | 15 | if __name__ == "__main__": 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument("--video", 18 | help='Path to the video file', 19 | required=True) 20 | parser.add_argument("-t", 21 | "--thumbnail", 22 | help='Path to the thumbnail image',) 23 | parser.add_argument("--meta", help='Path to the JSON file with metadata') 24 | parser.add_argument("--profile", help='Path to the firefox profile') 25 | args = parser.parse_args() 26 | 27 | main(args.video, args.meta, args.thumbnail, profile_path=args.profile) 28 | -------------------------------------------------------------------------------- /youtube_uploader_selenium/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 | #Schedule 45 | VIDEO_SCHEDULE = 'schedule' 46 | SCHEDULE_CONTAINER_ID = 'schedule-radio-button' 47 | SCHEDULE_DATE_ID = 'datepicker-trigger' 48 | 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' 49 | 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" 50 | -------------------------------------------------------------------------------- /youtube_uploader_selenium/__init__.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 | with open(metadata_json_path, encoding='utf-8') as metadata_json_file: 24 | return defaultdict(str, json.load(metadata_json_file)) 25 | 26 | 27 | class YouTubeUploader: 28 | """A class for uploading videos on YouTube via Selenium using metadata JSON file 29 | to extract its title, description etc""" 30 | 31 | def __init__(self, video_path: str, metadata_json_path: Optional[str] = None, 32 | thumbnail_path: Optional[str] = None, 33 | profile_path: Optional[str] = str(Path.cwd()) + "/profile") -> None: 34 | self.video_path = video_path 35 | self.thumbnail_path = thumbnail_path 36 | self.metadata_dict = load_metadata(metadata_json_path) 37 | self.browser = Firefox(profile_path=profile_path, pickle_cookies=True, full_screen=False) 38 | self.logger = logging.getLogger(__name__) 39 | self.logger.setLevel(logging.DEBUG) 40 | self.__validate_inputs() 41 | 42 | self.is_mac = False 43 | if not any(os_name in platform.platform() for os_name in ["Windows", "Linux"]): 44 | self.is_mac = True 45 | 46 | self.logger.debug("Use profile path: {}".format(self.browser.source_profile_path)) 47 | 48 | def __validate_inputs(self): 49 | if not self.metadata_dict[Constant.VIDEO_TITLE]: 50 | self.logger.warning( 51 | "The video title was not found in a metadata file") 52 | self.metadata_dict[Constant.VIDEO_TITLE] = Path( 53 | self.video_path).stem 54 | self.logger.warning("The video title was set to {}".format( 55 | Path(self.video_path).stem)) 56 | if not self.metadata_dict[Constant.VIDEO_DESCRIPTION]: 57 | self.logger.warning( 58 | "The video description was not found in a metadata file") 59 | 60 | def upload(self): 61 | try: 62 | self.__login() 63 | return self.__upload() 64 | except Exception as e: 65 | print(e) 66 | self.__quit() 67 | raise 68 | 69 | def __login(self): 70 | self.browser.get(Constant.YOUTUBE_URL) 71 | time.sleep(Constant.USER_WAITING_TIME) 72 | 73 | if self.browser.has_cookies_for_current_website(): 74 | self.browser.load_cookies() 75 | self.logger.debug("Loaded cookies from {}".format(self.browser.cookies_folder_path)) 76 | time.sleep(Constant.USER_WAITING_TIME) 77 | self.browser.refresh() 78 | else: 79 | self.logger.info('Please sign in and then press enter') 80 | input() 81 | self.browser.get(Constant.YOUTUBE_URL) 82 | time.sleep(Constant.USER_WAITING_TIME) 83 | self.browser.save_cookies() 84 | self.logger.debug("Saved cookies to {}".format(self.browser.cookies_folder_path)) 85 | 86 | def __clear_field(self, field): 87 | field.click() 88 | time.sleep(Constant.USER_WAITING_TIME) 89 | if self.is_mac: 90 | field.send_keys(Keys.COMMAND + 'a') 91 | else: 92 | field.send_keys(Keys.CONTROL + 'a') 93 | time.sleep(Constant.USER_WAITING_TIME) 94 | field.send_keys(Keys.BACKSPACE) 95 | 96 | def __write_in_field(self, field, string, select_all=False): 97 | if select_all: 98 | self.__clear_field(field) 99 | else: 100 | field.click() 101 | time.sleep(Constant.USER_WAITING_TIME) 102 | 103 | field.send_keys(string) 104 | 105 | def __upload(self) -> Tuple[bool, Optional[str]]: 106 | edit_mode = self.metadata_dict[Constant.VIDEO_EDIT] 107 | if edit_mode: 108 | self.browser.get(edit_mode) 109 | time.sleep(Constant.USER_WAITING_TIME) 110 | else: 111 | self.browser.get(Constant.YOUTUBE_URL) 112 | time.sleep(Constant.USER_WAITING_TIME) 113 | self.browser.get(Constant.YOUTUBE_UPLOAD_URL) 114 | time.sleep(Constant.USER_WAITING_TIME) 115 | absolute_video_path = str(Path.cwd() / self.video_path) 116 | self.browser.find(By.XPATH, Constant.INPUT_FILE_VIDEO).send_keys( 117 | absolute_video_path) 118 | self.logger.debug('Attached video {}'.format(self.video_path)) 119 | 120 | # Find status container 121 | uploading_status_container = None 122 | while uploading_status_container is None: 123 | time.sleep(Constant.USER_WAITING_TIME) 124 | uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER) 125 | 126 | if self.thumbnail_path is not None: 127 | absolute_thumbnail_path = str(Path.cwd() / self.thumbnail_path) 128 | self.browser.find(By.XPATH, Constant.INPUT_FILE_THUMBNAIL).send_keys( 129 | absolute_thumbnail_path) 130 | change_display = "document.getElementById('file-loader').style = 'display: block! important'" 131 | self.browser.driver.execute_script(change_display) 132 | self.logger.debug( 133 | 'Attached thumbnail {}'.format(self.thumbnail_path)) 134 | 135 | title_field, description_field = self.browser.find_all(By.ID, Constant.TEXTBOX_ID, timeout=15) 136 | 137 | self.__write_in_field( 138 | title_field, self.metadata_dict[Constant.VIDEO_TITLE], select_all=True) 139 | self.logger.debug('The video title was set to \"{}\"'.format( 140 | self.metadata_dict[Constant.VIDEO_TITLE])) 141 | 142 | video_description = self.metadata_dict[Constant.VIDEO_DESCRIPTION] 143 | video_description = video_description.replace("\n", Keys.ENTER); 144 | if video_description: 145 | self.__write_in_field(description_field, video_description, select_all=True) 146 | self.logger.debug('Description filled.') 147 | 148 | kids_section = self.browser.find(By.NAME, Constant.NOT_MADE_FOR_KIDS_LABEL) 149 | kids_section.location_once_scrolled_into_view 150 | time.sleep(Constant.USER_WAITING_TIME) 151 | 152 | self.browser.find(By.ID, Constant.RADIO_LABEL, kids_section).click() 153 | self.logger.debug('Selected \"{}\"'.format(Constant.NOT_MADE_FOR_KIDS_LABEL)) 154 | 155 | # Playlist 156 | playlist = self.metadata_dict[Constant.VIDEO_PLAYLIST] 157 | if playlist: 158 | self.browser.find(By.CLASS_NAME, Constant.PL_DROPDOWN_CLASS).click() 159 | time.sleep(Constant.USER_WAITING_TIME) 160 | search_field = self.browser.find(By.ID, Constant.PL_SEARCH_INPUT_ID) 161 | self.__write_in_field(search_field, playlist) 162 | time.sleep(Constant.USER_WAITING_TIME * 2) 163 | playlist_items_container = self.browser.find(By.ID, Constant.PL_ITEMS_CONTAINER_ID) 164 | # Try to find playlist 165 | self.logger.debug('Playlist xpath: "{}".'.format(Constant.PL_ITEM_CONTAINER.format(playlist))) 166 | playlist_item = self.browser.find(By.XPATH, Constant.PL_ITEM_CONTAINER.format(playlist), playlist_items_container) 167 | if playlist_item: 168 | self.logger.debug('Playlist found.') 169 | playlist_item.click() 170 | time.sleep(Constant.USER_WAITING_TIME) 171 | else: 172 | self.logger.debug('Playlist not found. Creating') 173 | self.__clear_field(search_field) 174 | time.sleep(Constant.USER_WAITING_TIME) 175 | 176 | new_playlist_button = self.browser.find(By.CLASS_NAME, Constant.PL_NEW_BUTTON_CLASS) 177 | new_playlist_button.click() 178 | 179 | create_playlist_container = self.browser.find(By.ID, Constant.PL_CREATE_PLAYLIST_CONTAINER_ID) 180 | playlist_title_textbox = self.browser.find(By.XPATH, "//textarea", create_playlist_container) 181 | self.__write_in_field(playlist_title_textbox, playlist) 182 | 183 | time.sleep(Constant.USER_WAITING_TIME) 184 | create_playlist_button = self.browser.find(By.CLASS_NAME, Constant.PL_CREATE_BUTTON_CLASS) 185 | create_playlist_button.click() 186 | time.sleep(Constant.USER_WAITING_TIME) 187 | 188 | done_button = self.browser.find(By.CLASS_NAME, Constant.PL_DONE_BUTTON_CLASS) 189 | done_button.click() 190 | 191 | # Advanced options 192 | self.browser.find(By.ID, Constant.ADVANCED_BUTTON_ID).click() 193 | self.logger.debug('Clicked MORE OPTIONS') 194 | time.sleep(Constant.USER_WAITING_TIME) 195 | 196 | # Tags 197 | tags = self.metadata_dict[Constant.VIDEO_TAGS] 198 | if tags: 199 | tags_container = self.browser.find(By.ID, Constant.TAGS_CONTAINER_ID) 200 | tags_field = self.browser.find(By.ID, Constant.TAGS_INPUT, tags_container) 201 | self.__write_in_field(tags_field, ','.join(tags)) 202 | self.logger.debug('The tags were set to \"{}\"'.format(tags)) 203 | 204 | 205 | self.browser.find(By.ID, Constant.NEXT_BUTTON).click() 206 | self.logger.debug('Clicked {} one'.format(Constant.NEXT_BUTTON)) 207 | 208 | self.browser.find(By.ID, Constant.NEXT_BUTTON).click() 209 | self.logger.debug('Clicked {} two'.format(Constant.NEXT_BUTTON)) 210 | 211 | self.browser.find(By.ID, Constant.NEXT_BUTTON).click() 212 | self.logger.debug('Clicked {} three'.format(Constant.NEXT_BUTTON)) 213 | 214 | schedule = self.metadata_dict[Constant.VIDEO_SCHEDULE] 215 | if schedule: 216 | upload_time_object = datetime.strptime(schedule, "%m/%d/%Y, %H:%M") 217 | self.browser.find(By.ID, Constant.SCHEDULE_CONTAINER_ID).click() 218 | self.browser.find(By.ID, Constant.SCHEDULE_DATE_ID).click() 219 | self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).clear() 220 | self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).send_keys( 221 | datetime.strftime(upload_time_object, "%b %e, %Y")) 222 | self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).send_keys(Keys.ENTER) 223 | self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).click() 224 | self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).clear() 225 | self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).send_keys( 226 | datetime.strftime(upload_time_object, "%H:%M")) 227 | self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).send_keys(Keys.ENTER) 228 | self.logger.debug(f"Scheduled the video for {schedule}") 229 | else: 230 | public_main_button = self.browser.find(By.NAME, Constant.PUBLIC_BUTTON) 231 | self.browser.find(By.ID, Constant.RADIO_LABEL, public_main_button).click() 232 | self.logger.debug('Made the video {}'.format(Constant.PUBLIC_BUTTON)) 233 | 234 | video_id = self.__get_video_id() 235 | 236 | # Check status container and upload progress 237 | uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER) 238 | while uploading_status_container is not None: 239 | uploading_progress = uploading_status_container.get_attribute('value') 240 | self.logger.debug('Upload video progress: {}%'.format(uploading_progress)) 241 | time.sleep(Constant.USER_WAITING_TIME * 5) 242 | uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER) 243 | 244 | self.logger.debug('Upload container gone.') 245 | 246 | done_button = self.browser.find(By.ID, Constant.DONE_BUTTON) 247 | 248 | # Catch such error as 249 | # "File is a duplicate of a video you have already uploaded" 250 | if done_button.get_attribute('aria-disabled') == 'true': 251 | error_message = self.browser.find(By.XPATH, Constant.ERROR_CONTAINER).text 252 | self.logger.error(error_message) 253 | return False, None 254 | 255 | done_button.click() 256 | self.logger.debug( 257 | "Published the video with video_id = {}".format(video_id)) 258 | time.sleep(Constant.USER_WAITING_TIME) 259 | self.browser.get(Constant.YOUTUBE_URL) 260 | self.__quit() 261 | return True, video_id 262 | 263 | def __get_video_id(self) -> Optional[str]: 264 | video_id = None 265 | try: 266 | video_url_container = self.browser.find( 267 | By.XPATH, Constant.VIDEO_URL_CONTAINER) 268 | video_url_element = self.browser.find(By.XPATH, Constant.VIDEO_URL_ELEMENT, element=video_url_container) 269 | video_id = video_url_element.get_attribute( 270 | Constant.HREF).split('/')[-1] 271 | except: 272 | self.logger.warning(Constant.VIDEO_NOT_FOUND_ERROR) 273 | pass 274 | return video_id 275 | 276 | def __quit(self): 277 | self.browser.driver.quit() 278 | --------------------------------------------------------------------------------