Python script to upload videos on YouTube using Selenium that allows to upload more than 61 videos per day which is the maximum [1] for all other tools that use the YouTube Data API v3.
10,000
units per day [2] and a video upload has a cost of approximately 1,600
units [3]: 10,000 / 1,600 = 6.25
.
1: Since the projects that enable the YouTube Data API have a default quota allocation of Instead, this script is only restricted by a daily upload limit for a channel on YouTube:
100 videos is the limit in the first 24 hours, then drops to 50 every 24 hours after that. [4]
Package Installation
1 |
pip3 install --upgrade youtube-uploader-selenium |
Script Installation
1 2 |
git clone https://github.com/linouk23/youtube_uploader_selenium <span class="pl-c1">cd</span> youtube-uploader-selenium |
Package Usage
1 2 3 4 5 6 7 8 |
<span class="pl-k">from</span> <span class="pl-s1">youtube_uploader_selenium</span> <span class="pl-k">import</span> <span class="pl-v">YouTubeUploader</span> <span class="pl-s1">video_path</span> <span class="pl-c1">=</span> <span class="pl-s">'123/rockets.flv'</span> <span class="pl-s1">metadata_path</span> <span class="pl-c1">=</span> <span class="pl-s">'123/rockets_metadata.json'</span> <span class="pl-s1">uploader</span> <span class="pl-c1">=</span> <span class="pl-v">YouTubeUploader</span>(<span class="pl-s1">video_path</span>, <span class="pl-s1">metadata_path</span>, <span class="pl-s1">thumbnail_path</span>) <span class="pl-s1">was_video_uploaded</span>, <span class="pl-s1">video_id</span> <span class="pl-c1">=</span> <span class="pl-s1">uploader</span>.<span class="pl-en">upload</span>() <span class="pl-k">assert</span> <span class="pl-s1">was_video_uploaded</span> |
Script Usage
At a minimum, just specify a video:
1 |
python3 upload.py --video rockets.flv |
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). A token will be created and stored in a file in the local directory for subsequent use.
Video title, description and other metadata can specified via a JSON file using the --meta
flag:
1 |
python3 upload.py --video rockets.flv --meta metadata.json |
An example JSON file would be:
1 2 3 4 5 |
{ <span class="pl-ent">"title"</span>: <span class="pl-s"><span class="pl-pds">"</span>Best Of James Harden | 2019-20 NBA Season<span class="pl-pds">"</span></span>, <span class="pl-ent">"description"</span>: <span class="pl-s"><span class="pl-pds">"</span>Check out the best of James Harden's 2019-20 season so far!<span class="pl-pds">"</span></span>, <span class="pl-ent">"tags"</span>: [<span class="pl-s"><span class="pl-pds">"</span>James<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>Harden<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>NBA<span class="pl-pds">"</span></span>] } |
Dependencies
- geckodriver
- Firefox (Works with version 77)
- selenium_firefox
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import argparse from youtube_uploader_selenium import YouTubeUploader from typing import Optional def main(video_path: str, metadata_path: Optional[str] = None, thumbnail_path: Optional[str] = None): uploader = YouTubeUploader(video_path, metadata_path, thumbnail_path) was_video_uploaded, video_id = uploader.upload() assert was_video_uploaded if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--video", help='Path to the video file', required=True) parser.add_argument("-t", "--thumbnail", help='Path to the thumbnail image',) parser.add_argument("--meta", help='Path to the JSON file with metadata') args = parser.parse_args() main(args.video, args.meta, args.thumbnail) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Constant: """A class for storing constants for YoutubeUploader class""" YOUTUBE_URL = 'https://www.youtube.com' YOUTUBE_STUDIO_URL = 'https://studio.youtube.com' YOUTUBE_UPLOAD_URL = 'https://www.youtube.com/upload' USER_WAITING_TIME = 1 VIDEO_TITLE = 'title' VIDEO_DESCRIPTION = 'description' VIDEO_TAGS = 'tags' DESCRIPTION_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/' \ 'ytcp-uploads-details/div/ytcp-uploads-basics/ytcp-mention-textbox[2]' TEXTBOX = 'textbox' TEXT_INPUT = 'text-input' RADIO_LABEL = 'radioLabel' STATUS_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[2]/' \ 'div/div[1]/ytcp-video-upload-progress/span' NOT_MADE_FOR_KIDS_LABEL = 'VIDEO_MADE_FOR_KIDS_NOT_MFK' # Thanks to romka777 MORE_BUTTON = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-video-metadata-editor/div/div/ytcp-button/div' TAGS_INPUT_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-video-metadata-editor/div/ytcp-video-metadata-editor-advanced/div[3]/ytcp-form-input-container/div[1]/div[2]/ytcp-free-text-chip-bar/ytcp-chip-bar/div' TAGS_INPUT = 'text-input' NEXT_BUTTON = 'next-button' PUBLIC_BUTTON = 'PUBLIC' VIDEO_URL_CONTAINER = "//span[@class='video-url-fadeable style-scope ytcp-video-info']" VIDEO_URL_ELEMENT = "//a[@class='style-scope ytcp-video-info']" HREF = 'href' UPLOADED = 'Uploading' ERROR_CONTAINER = '//*[@id="error-message"]' VIDEO_NOT_FOUND_ERROR = 'Could not find video_id' DONE_BUTTON = 'done-button' INPUT_FILE_VIDEO = "//input[@type='file']" INPUT_FILE_THUMBNAIL = "//input[@id='file-loader']" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
"""This module implements uploading videos on YouTube via Selenium using metadata JSON file to extract its title, description etc.""" from typing import DefaultDict, Optional from selenium_firefox.firefox import Firefox, By, Keys from collections import defaultdict import json import time from .Constant import * from pathlib import Path import logging import platform logging.basicConfig() def load_metadata(metadata_json_path: Optional[str] = None) -> DefaultDict[str, str]: if metadata_json_path is None: return defaultdict(str) with open(metadata_json_path, encoding='utf-8') as metadata_json_file: return defaultdict(str, json.load(metadata_json_file)) class YouTubeUploader: """A class for uploading videos on YouTube via Selenium using metadata JSON file to extract its title, description etc""" def __init__(self, video_path: str, metadata_json_path: Optional[str] = None, thumbnail_path: Optional[str] = None) -> None: self.video_path = video_path self.thumbnail_path = thumbnail_path self.metadata_dict = load_metadata(metadata_json_path) current_working_dir = str(Path.cwd()) self.browser = Firefox(current_working_dir, current_working_dir) self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.DEBUG) self.__validate_inputs() self.is_mac = False if not any(os_name in platform.platform() for os_name in ["Windows", "Linux"]): self.is_mac = True def __validate_inputs(self): if not self.metadata_dict[Constant.VIDEO_TITLE]: self.logger.warning( "The video title was not found in a metadata file") self.metadata_dict[Constant.VIDEO_TITLE] = Path( self.video_path).stem self.logger.warning("The video title was set to {}".format( Path(self.video_path).stem)) if not self.metadata_dict[Constant.VIDEO_DESCRIPTION]: self.logger.warning( "The video description was not found in a metadata file") def upload(self): try: self.__login() return self.__upload() except Exception as e: print(e) self.__quit() raise def __login(self): self.browser.get(Constant.YOUTUBE_URL) time.sleep(Constant.USER_WAITING_TIME) if self.browser.has_cookies_for_current_website(): self.browser.load_cookies() time.sleep(Constant.USER_WAITING_TIME) self.browser.refresh() else: self.logger.info('Please sign in and then press enter') input() self.browser.get(Constant.YOUTUBE_URL) time.sleep(Constant.USER_WAITING_TIME) self.browser.save_cookies() def __write_in_field(self, field, string, select_all=False): field.click() time.sleep(Constant.USER_WAITING_TIME) if select_all: if self.is_mac: field.send_keys(Keys.COMMAND + 'a') else: field.send_keys(Keys.CONTROL + 'a') time.sleep(Constant.USER_WAITING_TIME) field.send_keys(string) def __upload(self) -> (bool, Optional[str]): self.browser.get(Constant.YOUTUBE_URL) time.sleep(Constant.USER_WAITING_TIME) self.browser.get(Constant.YOUTUBE_UPLOAD_URL) time.sleep(Constant.USER_WAITING_TIME) absolute_video_path = str(Path.cwd() / self.video_path) self.browser.find(By.XPATH, Constant.INPUT_FILE_VIDEO).send_keys( absolute_video_path) self.logger.debug('Attached video {}'.format(self.video_path)) if self.thumbnail_path is not None: absolute_thumbnail_path = str(Path.cwd() / self.thumbnail_path) self.browser.find(By.XPATH, Constant.INPUT_FILE_THUMBNAIL).send_keys( absolute_thumbnail_path) change_display = "document.getElementById('file-loader').style = 'display: block! important'" self.browser.driver.execute_script(change_display) self.logger.debug( 'Attached thumbnail {}'.format(self.thumbnail_path)) title_field = self.browser.find(By.ID, Constant.TEXTBOX, timeout=15) self.__write_in_field( title_field, self.metadata_dict[Constant.VIDEO_TITLE], select_all=True) self.logger.debug('The video title was set to \"{}\"'.format( self.metadata_dict[Constant.VIDEO_TITLE])) video_description = self.metadata_dict[Constant.VIDEO_DESCRIPTION] video_description = video_description.replace("\n", Keys.ENTER); if video_description: description_field = self.browser.find_all(By.ID, Constant.TEXTBOX)[1] self.__write_in_field(description_field, video_description, select_all=True) self.logger.debug('Description filled.') kids_section = self.browser.find( By.NAME, Constant.NOT_MADE_FOR_KIDS_LABEL) self.browser.find(By.ID, Constant.RADIO_LABEL, kids_section).click() self.logger.debug('Selected \"{}\"'.format( Constant.NOT_MADE_FOR_KIDS_LABEL)) # Advanced options self.browser.find(By.XPATH, Constant.MORE_BUTTON).click() self.logger.debug('Clicked MORE OPTIONS') tags_container = self.browser.find(By.XPATH, Constant.TAGS_INPUT_CONTAINER) tags_field = self.browser.find( By.ID, Constant.TAGS_INPUT, element=tags_container) self.__write_in_field(tags_field, ','.join( self.metadata_dict[Constant.VIDEO_TAGS])) self.logger.debug( 'The tags were set to \"{}\"'.format(self.metadata_dict[Constant.VIDEO_TAGS])) self.browser.find(By.ID, Constant.NEXT_BUTTON).click() self.logger.debug('Clicked {} one'.format(Constant.NEXT_BUTTON)) # Thanks to romka777 self.browser.find(By.ID, Constant.NEXT_BUTTON).click() self.logger.debug('Clicked {} two'.format(Constant.NEXT_BUTTON)) self.browser.find(By.ID, Constant.NEXT_BUTTON).click() self.logger.debug('Clicked {} three'.format(Constant.NEXT_BUTTON)) public_main_button = self.browser.find(By.NAME, Constant.PUBLIC_BUTTON) self.browser.find(By.ID, Constant.RADIO_LABEL, public_main_button).click() self.logger.debug('Made the video {}'.format(Constant.PUBLIC_BUTTON)) video_id = self.__get_video_id() status_container = self.browser.find(By.XPATH, Constant.STATUS_CONTAINER) while True: in_process = status_container.text.find(Constant.UPLOADED) != -1 if in_process: time.sleep(Constant.USER_WAITING_TIME) else: break done_button = self.browser.find(By.ID, Constant.DONE_BUTTON) # Catch such error as # "File is a duplicate of a video you have already uploaded" if done_button.get_attribute('aria-disabled') == 'true': error_message = self.browser.find(By.XPATH, Constant.ERROR_CONTAINER).text self.logger.error(error_message) return False, None done_button.click() self.logger.debug( "Published the video with video_id = {}".format(video_id)) time.sleep(Constant.USER_WAITING_TIME) self.browser.get(Constant.YOUTUBE_URL) self.__quit() return True, video_id def __get_video_id(self) -> Optional[str]: video_id = None try: video_url_container = self.browser.find( By.XPATH, Constant.VIDEO_URL_CONTAINER) video_url_element = self.browser.find(By.XPATH, Constant.VIDEO_URL_ELEMENT, element=video_url_container) video_id = video_url_element.get_attribute( Constant.HREF).split('/')[-1] except: self.logger.warning(Constant.VIDEO_NOT_FOUND_ERROR) pass return video_id def __quit(self): self.browser.driver.quit() |
https://github.com/linouk23/youtube_uploader_selenium