├── .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 |
--------------------------------------------------------------------------------