youtube_up
Installation
pip install youtube-up
Installing certificates
On your first run you may get an error which says Was not able to load https://youtube.com. Have you installed the certificate at {cert_path} ?
.
If this happens you should follow the instructions at https://docs.mitmproxy.org/stable/concepts-certificates/#installing-the-mitmproxy-ca-certificate-manually
to install the certificate at the given path.
Examples
Upload a video
from youtube_up import AllowCommentsEnum, Metadata, PrivacyEnum, YTUploaderSession
uploader = YTUploaderSession.from_cookies_txt("cookies/cookies.txt")
metadata = Metadata(
title="Video title",
description="Video description",
privacy=PrivacyEnum.PUBLIC,
made_for_kids=False,
allow_comments_mode=AllowCommentsEnum.HOLD_ALL,
)
uploader.upload("video.webm", metadata)
Note that for Enum-type parameters we can either pass the Enum itself (as shown above), or the Enum value, or the Enum key, as a string. For example, instead of writing
allow_comments_mode=AllowCommentsEnum.HOLD_ALL
we could instead write allow_comments_mode="HOLD_ALL"
or allow_comments_mode="APPROVED_COMMENTS"
Upload multiple videos
from youtube_up import Metadata, YTUploaderSession
uploader = YTUploaderSession.from_cookies_txt("cookies/cookies.txt")
metadata_1 = Metadata(
title="Video 1",
)
metadata_2 = Metadata(
title="Video 2",
)
uploader.upload("video1.webm", metadata_1)
uploader.upload("video2.webm", metadata_2)
Upload to a new or existing playlist
from youtube_up import Metadata, YTUploaderSession, Playlist
uploader = YTUploaderSession.from_cookies_txt("cookies/cookies.txt")
metadata = Metadata(
title="Video 1",
playlists=[
Playlist(
"Songs by me",
description="Playlist that will only be created if "
"no playlist exists with the title 'Songs by me'",
create_if_title_doesnt_exist=True,
create_if_title_exists=False,
),
Playlist(
"test playlist",
description="Playlist that video will be added to "
"only if it exists already. This description does "
"nothing.",
create_if_title_doesnt_exist=False,
create_if_title_exists=False,
),
Playlist(
"Album",
description="Playlist will be created even if there"
" is already a playlist with the name 'Album'"
create_if_title_doesnt_exist=True,
create_if_title_exists=True,
),
],
)
uploader.upload("video.webm", metadata)
CLI
youtube-up comes with a CLI app for uploading videos. For example, if we wanted to
create a public video with the title "Video title", we would execute the following command:
youtube-up video video.webm --title="Video title" --cookies_file="cookies/cookies.txt" --privacy="PUBLIC"
The app can also take a JSON file as input. For example, the following JSON file would upload one video to a new or existing playlist called "Music" and one video which is set to premiere on December 25th, 2023 at 5 PM (local time).
[
{
"file": "song.webm",
"metadata": {
"title": "New song",
"privacy": "PUBLIC",
"playlists": [
{
"title": "Music"
}
]
}
},
{
"file": "premiere.webm",
"metadata": {
"title": "Special Announcement",
"scheduled_upload": "2023-12-25T17:00:00",
"premiere_countdown_duration": "ONE_MIN",
"premiere_theme": "BRIGHT"
}
}
]
If we wanted the video to premiere at 5 PM GMT, would could have written "2023-12-25T17:00:00+00:00" instead. We then run
youtube-up json metadata.json --cookies_file="cookies/cookies.txt"
to upload these videos.
1# ruff: noqa 2""" 3# Installation 4 5`pip install youtube-up` 6 7## Installing certificates 8 9On your first run you may get an error which says `Was not able to load https://youtube.com. Have you installed the certificate at {cert_path} ?`. 10If this happens you should follow the instructions at https://docs.mitmproxy.org/stable/concepts-certificates/#installing-the-mitmproxy-ca-certificate-manually 11to install the certificate at the given path. 12 13# Examples 14 15## Upload a video 16```python 17from youtube_up import AllowCommentsEnum, Metadata, PrivacyEnum, YTUploaderSession 18 19uploader = YTUploaderSession.from_cookies_txt("cookies/cookies.txt") 20metadata = Metadata( 21 title="Video title", 22 description="Video description", 23 privacy=PrivacyEnum.PUBLIC, 24 made_for_kids=False, 25 allow_comments_mode=AllowCommentsEnum.HOLD_ALL, 26) 27uploader.upload("video.webm", metadata) 28``` 29Note that for Enum-type parameters we can either pass the Enum itself (as shown above), 30or the Enum value, or the Enum key, as a string. For example, instead of writing 31 32`allow_comments_mode=AllowCommentsEnum.HOLD_ALL` 33 34we could instead write `allow_comments_mode="HOLD_ALL"` 35or `allow_comments_mode="APPROVED_COMMENTS"` 36 37## Upload multiple videos 38```python 39from youtube_up import Metadata, YTUploaderSession 40 41uploader = YTUploaderSession.from_cookies_txt("cookies/cookies.txt") 42metadata_1 = Metadata( 43 title="Video 1", 44) 45metadata_2 = Metadata( 46 title="Video 2", 47) 48uploader.upload("video1.webm", metadata_1) 49uploader.upload("video2.webm", metadata_2) 50``` 51 52## Upload to a new or existing playlist 53```python 54from youtube_up import Metadata, YTUploaderSession, Playlist 55 56uploader = YTUploaderSession.from_cookies_txt("cookies/cookies.txt") 57metadata = Metadata( 58 title="Video 1", 59 playlists=[ 60 Playlist( 61 "Songs by me", 62 description="Playlist that will only be created if " 63 "no playlist exists with the title 'Songs by me'", 64 create_if_title_doesnt_exist=True, 65 create_if_title_exists=False, 66 ), 67 Playlist( 68 "test playlist", 69 description="Playlist that video will be added to " 70 "only if it exists already. This description does " 71 "nothing.", 72 create_if_title_doesnt_exist=False, 73 create_if_title_exists=False, 74 ), 75 Playlist( 76 "Album", 77 description="Playlist will be created even if there" 78 " is already a playlist with the name 'Album'" 79 create_if_title_doesnt_exist=True, 80 create_if_title_exists=True, 81 ), 82 ], 83) 84uploader.upload("video.webm", metadata) 85``` 86 87## CLI 88youtube-up comes with a CLI app for uploading videos. For example, if we wanted to 89create a public video with the title "Video title", we would execute the following command: 90`youtube-up video video.webm --title="Video title" --cookies_file="cookies/cookies.txt" --privacy="PUBLIC"` 91 92The app can also take a JSON file as input. For example, the following JSON file would upload 93one video to a new or existing playlist called "Music" and one video which is set to premiere 94on December 25th, 2023 at 5 PM (local time). 95 96```json 97[ 98 { 99 "file": "song.webm", 100 "metadata": { 101 "title": "New song", 102 "privacy": "PUBLIC", 103 "playlists": [ 104 { 105 "title": "Music" 106 } 107 ] 108 } 109 }, 110 { 111 "file": "premiere.webm", 112 "metadata": { 113 "title": "Special Announcement", 114 "scheduled_upload": "2023-12-25T17:00:00", 115 "premiere_countdown_duration": "ONE_MIN", 116 "premiere_theme": "BRIGHT" 117 } 118 } 119] 120``` 121 122If we wanted the video to premiere at 5 PM GMT, would could have written "2023-12-25T17:00:00+00:00" 123instead. We then run 124 125`youtube-up json metadata.json --cookies_file="cookies/cookies.txt"` 126 127to upload these videos. 128""" 129 130from .metadata import * 131from .metadata import __all__ as m_all 132from .uploader import * 133from .uploader import __all__ as u_all 134 135__all__ = u_all + m_all
53class YTUploaderSession: 54 """ 55 Class for uploading YouTube videos to a single channel 56 """ 57 58 _delegated_session_id_regex = re.compile(r'"DELEGATED_SESSION_ID":"([^"]*)"') 59 _innertube_api_key_regex = re.compile(r'"INNERTUBE_API_KEY":"([^"]*)"') 60 _session_index_regex = re.compile(r'"SESSION_INDEX":"([^"]*)"') 61 _channel_id_regex = re.compile(r"https://studio.youtube.com/channel/([^/]*)/*") 62 _progress_steps = { 63 "start": 0, 64 "get_session_data": 10, 65 "get_upload_url": 20, 66 "upload_video": 70, 67 "get_session_token": 80, 68 "create_video": 90, 69 "upload_thumbnail": 95, 70 "finish": 100, 71 } 72 _cookie_whitelist = { 73 "LOGIN_INFO", 74 "__Secure-1PSID", 75 "__Secure-3PSID", 76 "__Secure-1PAPISID", 77 "__Secure-3PAPISID", 78 "__Secure-1PSIDTS", 79 "__Secure-3PSIDTS", 80 "SAPISID", 81 } 82 83 _session_token: str 84 _cookies: FileCookieJar 85 _session: requests.Session 86 87 def __init__( 88 self, 89 cookie_jar: FileCookieJar, 90 webdriver_path: Optional[str] = None, 91 selenium_timeout: float = 60, 92 ): 93 """Create YTUploaderSession from generic FileCookieJar 94 95 Args: 96 cookie_jar (FileCookieJar): FileCookieJar. Must have save(), load(), 97 and set_cookie(http.cookiejar.Cookie) methods 98 webdriver_path (str, optional): Optional path to geckodriver or chromedriver 99 executable 100 selenium_timeout (float, optional): Timeout to wait for grst request. 101 Defaults to 60 seconds 102 """ 103 self._session_token = "" 104 self._webdriver_path = webdriver_path 105 self._selenium_timeout = selenium_timeout 106 107 # load cookies and init session 108 self._cookies = cookie_jar 109 self._session = requests.Session() 110 self._reload_cookies() 111 hash = self._generateSAPISIDHASH(self._session.cookies["SAPISID"]) 112 self._session.headers = { 113 "Authorization": f"SAPISIDHASH {hash}", 114 "x-origin": "https://studio.youtube.com", 115 "user-agent": ( 116 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) " 117 "Gecko/20100101 Firefox/119.0" 118 ), 119 } 120 121 def _reload_cookies(self): 122 self._cookies.load(ignore_discard=True, ignore_expires=True) 123 self._session.cookies.clear() 124 for cookie in self._cookies: 125 if cookie.name == "SESSION_TOKEN": 126 self._session_token = cookie.value 127 elif cookie.name in self._cookie_whitelist: 128 self._session.cookies.set_cookie(copy.copy(cookie)) 129 130 @classmethod 131 def from_cookies_txt( 132 cls, 133 cookies_txt_path: str, 134 webdriver_path: Optional[str] = None, 135 selenium_timeout: float = 60, 136 ): 137 """Create YTUploaderSession from cookies.txt file 138 139 Args: 140 cookies_txt_path (str): Path to Netscape cookies format file 141 webdriver_path (str, optional): Optional path to geckodriver or chromedriver 142 executable 143 selenium_timeout (float, optional): Timeout to wait for grst request. 144 Defaults to 60 seconds 145 """ 146 cj = MozillaCookieJar(cookies_txt_path) 147 return cls(cj, webdriver_path, selenium_timeout) 148 149 def upload( 150 self, 151 file_path: str, 152 metadata: Metadata, 153 progress_callback: Callable[[str, float], None] = lambda step, percent: None, 154 ) -> str: 155 """Upload a video 156 157 Args: 158 file_path (str): Path to video file 159 metadata (Metadata): Metadata of video to set when uploaded 160 progress_callback (Callable[[str, float], None], optional): Optional 161 progress callback. Callback receives what step uploader is on and what 162 the total percentage of the upload progress is (defined by 163 YTUploaderSession._progress_steps). 164 165 Returns: 166 str: ID of video uploaded 167 """ 168 try: 169 metadata.validate() 170 except ValueError as ex: 171 raise YTUploaderException(f"Validation error: {ex}") from ex 172 progress_callback("start", self._progress_steps["start"]) 173 data = self._get_session_data() 174 progress_callback("get_session_data", self._progress_steps["get_session_data"]) 175 url = self._get_video_upload_url(data) 176 progress_callback("get_upload_url", self._progress_steps["get_upload_url"]) 177 scotty_resource_id = self._upload_file( 178 url, file_path, progress_callback, "get_upload_url", "upload_video" 179 ) 180 progress_callback("upload_video", self._progress_steps["upload_video"]) 181 encrypted_video_id = self._create_video(scotty_resource_id, metadata, data) 182 if encrypted_video_id is None: 183 # could be bad session token, try to get new one 184 self._get_session_token() 185 progress_callback( 186 "get_session_token", self._progress_steps["get_session_token"] 187 ) 188 encrypted_video_id = self._create_video(scotty_resource_id, metadata, data) 189 if encrypted_video_id is None: 190 raise YTUploaderException("Could not create video") 191 data.encrypted_video_id = encrypted_video_id 192 progress_callback("create_video", self._progress_steps["create_video"]) 193 194 # set thumbnail 195 if metadata.thumbnail is not None: 196 url = self._get_upload_url_thumbnail(data) 197 data.thumbnail_scotty_id = self._upload_file( 198 url, 199 metadata.thumbnail, 200 progress_callback, 201 "create_video", 202 "upload_thumbnail", 203 ) 204 data.thumbnail_format = self._get_thumbnail_format(metadata.thumbnail) 205 206 # playlists 207 if metadata.playlists: 208 playlists = self._get_creator_playlists(data) 209 if metadata.playlist_ids is None: 210 metadata.playlist_ids = [] 211 for playlist in metadata.playlists: 212 exists = playlist.title in playlists 213 if (playlist.create_if_title_exists and exists) or ( 214 playlist.create_if_title_doesnt_exist and not exists 215 ): 216 playlist_id = self._create_playlist(playlist, data) 217 metadata.playlist_ids.append(playlist_id) 218 elif exists: 219 metadata.playlist_ids.append(playlists[playlist.title]) 220 # captions 221 if metadata.captions_files: 222 for caption_file in metadata.captions_files: 223 if caption_file.language is None: 224 caption_file.language = metadata.audio_language 225 self._update_captions(caption_file, data) 226 227 self._update_metadata(metadata, data) 228 # save cookies 229 for cookie in self._session.cookies: 230 self._cookies.set_cookie(cookie) 231 self._cookies.save() 232 progress_callback("finish", self._progress_steps["finish"]) 233 return data.encrypted_video_id 234 235 def has_valid_cookies(self) -> bool: 236 """Check if cookies are valid 237 238 Returns: 239 bool: True if we are able to log in to YouTube with the given cookies 240 """ 241 r = self._session.get("https://youtube.com/upload") 242 243 return "studio.youtube.com/channel" in r.url 244 245 def _get_thumbnail_format(self, filename: str) -> ThumbnailFormatEnum: 246 ext = filename.split(".")[-1] 247 if ext in ("jpg", "jpeg", "jfif", "pjpeg", "pjp"): 248 return ThumbnailFormatEnum.JPG 249 if ext in ("png",): 250 return ThumbnailFormatEnum.PNG 251 raise YTUploaderException( 252 f"Unknown format for thumbnail with extension '{ext}'. " 253 "Only JPEG and PNG allowed" 254 ) 255 256 def _get_session_token(self): 257 try: 258 # try firefox 259 options = webdriver.FirefoxOptions() 260 options.add_argument("--headless") 261 if self._webdriver_path: 262 service = FirefoxService(self._webdriver_path) 263 driver = webdriver.Firefox(options=options, service=service) 264 else: 265 driver = webdriver.Firefox(options=options) 266 except Exception: 267 try: 268 # try chrome 269 options = webdriver.ChromeOptions() 270 options.add_argument("--headless=new") 271 if self._webdriver_path: 272 service = ChromeService(self._webdriver_path) 273 driver = webdriver.Chrome(options=options, service=service) 274 else: 275 driver = webdriver.Chrome(options=options) 276 except Exception as ex: 277 raise YTUploaderException( 278 "Could not launch Firefox or Chrome. Make sure geckodriver or " 279 "chromedriver is installed" 280 ) from ex 281 282 driver.set_page_load_timeout(self._selenium_timeout) 283 284 try: 285 driver.get("https://youtube.com") 286 except Exception as ex: 287 cert_path = os.path.join( 288 driver.backend.storage.home_dir, "mitmproxy-ca-cert.cer" 289 ) 290 raise YTUploaderException( 291 "Was not able to load https://youtube.com. Have you installed the cert" 292 f"ificate at {cert_path} ? See https://docs.mitmproxy.org/stable/conce" 293 "pts-certificates/#installing-the-mitmproxy-ca-certificate-manually" 294 ) from ex 295 296 self._reload_cookies() 297 for cookie in self._cookies: 298 if cookie.name in self._cookie_whitelist: 299 driver.add_cookie(cookie.__dict__) 300 301 driver.get("https://youtube.com/upload") 302 303 if "studio.youtube.com/channel" not in driver.current_url: 304 driver.quit() 305 raise YTUploaderException( 306 "Could not log in to YouTube account. Try getting new cookies" 307 ) 308 309 r = driver.wait_for_request( 310 "studio.youtube.com/youtubei/v1/ars/grst", timeout=self._selenium_timeout 311 ) 312 response = r.response 313 r_json = json.loads( 314 decode(response.body, response.headers.get("Content-Encoding")) 315 ) 316 self._session_token = r_json["sessionToken"] 317 self._cookies.set_cookie( 318 Cookie( 319 None, 320 "SESSION_TOKEN", 321 self._session_token, 322 None, 323 False, 324 "", 325 False, 326 False, 327 "", 328 False, 329 False, 330 None, 331 False, 332 None, 333 None, 334 {}, 335 ) 336 ) 337 self._cookies.save() 338 driver.quit() 339 340 @staticmethod 341 def _generateUUID() -> str: 342 return str(uuid.uuid4()).upper() 343 344 @staticmethod 345 def _generateSAPISIDHASH(SAPISID) -> str: 346 timestamp = math.floor(time.time()) 347 msg = f"{timestamp} {SAPISID} {'https://studio.youtube.com'}" 348 hash = sha1(msg.encode("utf-8")).hexdigest() 349 return f"{timestamp}_{hash}" 350 351 def _get_session_data(self) -> YTUploaderVideoData: 352 r = self._session.get("https://youtube.com/upload") 353 354 if "studio.youtube.com/channel" not in r.url: 355 raise YTUploaderException( 356 "Could not log in to YouTube account. Try getting new cookies" 357 ) 358 359 channel_id = self._channel_id_regex.match(r.url).group(1) # type: ignore[union-attr] 360 innertube_api_key = self._innertube_api_key_regex.search(r.text).group(1) # type: ignore[union-attr] 361 m = self._delegated_session_id_regex.search(r.text) 362 delegated_session_id = m and m.group(1) 363 authuser = self._session_index_regex.search(r.text).group(1) # type: ignore[union-attr] 364 self._session.headers["X-Goog-AuthUser"] = authuser 365 return YTUploaderVideoData( 366 authuser=authuser, 367 channel_id=channel_id, 368 innertube_api_key=innertube_api_key, 369 delegated_session_id=delegated_session_id, 370 ) 371 372 def _get_upload_url(self, api_url: str, authuser: str, data: dict) -> str: 373 params = {"authuser": authuser} 374 headers = { 375 "x-goog-upload-command": "start", 376 "x-goog-upload-protocol": "resumable", 377 } 378 r = self._session.post( 379 api_url, 380 headers=headers, 381 params=params, 382 json=data, 383 ) 384 r.raise_for_status() 385 upload_url = r.headers["x-goog-upload-url"] 386 return upload_url 387 388 def _get_video_upload_url(self, data: YTUploaderVideoData) -> str: 389 data.front_end_upload_id = f"innertube_studio:{self._generateUUID()}:0" 390 return self._get_upload_url( 391 "https://upload.youtube.com/upload/studio", 392 data.authuser, 393 {"frontendUploadId": data.front_end_upload_id}, 394 ) 395 396 def _get_upload_url_thumbnail(self, data: YTUploaderVideoData) -> str: 397 return self._get_upload_url( 398 "https://upload.youtube.com/upload/studiothumbnail", data.authuser, {} 399 ) 400 401 def _get_creator_playlists(self, data: YTUploaderVideoData) -> dict[str, str]: 402 playlists = {} 403 page_token = "" 404 while True: 405 params = {"key": data.innertube_api_key, "alt": "json"} 406 json = APIRequestListPlaylists.from_session_data( 407 data.channel_id, 408 self._session_token, 409 data.delegated_session_id, 410 page_token, 411 ).to_dict() 412 r = self._session.post( 413 "https://studio.youtube.com/youtubei/v1/creator/list_creator_playlists", 414 params=params, 415 json=json, 416 ) 417 r.raise_for_status() 418 json = r.json() 419 playlists.update( 420 { 421 playlist["title"]: playlist["playlistId"] 422 for playlist in json.get("playlists", []) 423 } 424 ) 425 if json.get("nextPageToken"): 426 page_token = json["nextPageToken"] 427 else: 428 break 429 return playlists 430 431 def _get_claimed_videos(self, data: YTUploaderVideoData) -> list[dict]: 432 videos = [] 433 page_token = "" 434 while True: 435 params = {"alt": "json"} 436 json = APIRequestListVideos.list_claimed( 437 data.channel_id, 438 data.delegated_session_id, 439 page_token, 440 ).to_dict() 441 r = self._session.post( 442 "https://studio.youtube.com/youtubei/v1/creator/list_creator_videos", 443 params=params, 444 json=json, 445 ) 446 r.raise_for_status() 447 json = r.json() 448 videos += json.get("videos", []) 449 if json.get("nextPageToken"): 450 page_token = json["nextPageToken"] 451 else: 452 break 453 return videos 454 455 def _get_claim_info(self, data: YTUploaderVideoData, video_id: str): 456 params = {"alt": "json"} 457 data = APIRequestListClaim.from_session_data( 458 data.channel_id, 459 data.delegated_session_id, 460 video_id, 461 ).to_dict() 462 r = self._session.post( 463 "https://studio.youtube.com/youtubei/v1/creator/list_creator_received_claims", 464 params=params, 465 json=data, 466 ) 467 r.raise_for_status() 468 json = r.json() 469 return list(zip(json["receivedClaims"], json["contentOwners"])) 470 471 def _dispute_claim( 472 self, 473 data: YTUploaderVideoData, 474 claim_id: str, 475 video_id: str, 476 justification: str, 477 legal_name: str, 478 ): 479 params = {"alt": "json"} 480 data = APIRequestDispute.from_session_data( 481 data.channel_id, 482 self._session_token, 483 data.delegated_session_id, 484 claim_id, 485 video_id, 486 justification, 487 legal_name, 488 ).to_dict() 489 r = self._session.post( 490 "https://studio.youtube.com/youtubei/v1/copyright/submit_claim_dispute", 491 params=params, 492 json=data, 493 ) 494 r.raise_for_status() 495 data = r.json() 496 497 def _create_playlist( 498 self, 499 playlist: Playlist, 500 data: YTUploaderVideoData, 501 ) -> str: 502 params = {"key": data.innertube_api_key, "alt": "json"} 503 data = APIRequestCreatePlaylist.from_session_data( 504 data.channel_id, self._session_token, data.delegated_session_id, playlist 505 ).to_dict() 506 r = self._session.post( 507 "https://studio.youtube.com/youtubei/v1/playlist/create", 508 params=params, 509 json=data, 510 ) 511 r.raise_for_status() 512 return r.json()["playlistId"] 513 514 def _update_captions( 515 self, 516 caption_file: CaptionsFile, 517 data: YTUploaderVideoData, 518 ): 519 params = {"key": data.innertube_api_key, "alt": "json"} 520 with open(caption_file.path, "rb") as f: 521 captions_b64 = "data:application/octet-stream;base64," + base64.b64encode( 522 f.read() 523 ).decode("utf-8") 524 timestamp = str(time.time_ns()) 525 assert caption_file.language is not None 526 assert data.encrypted_video_id is not None 527 data = APIRequestUpdateCaptions.from_session_data( 528 data.channel_id, 529 self._session_token, 530 data.delegated_session_id, 531 data.encrypted_video_id, 532 caption_file.path, 533 captions_b64, 534 caption_file.language, 535 timestamp, 536 ).to_dict() 537 r = self._session.post( 538 "https://studio.youtube.com/youtubei/v1/globalization/update_captions", 539 params=params, 540 json=data, 541 ) 542 r.raise_for_status() 543 544 def _upload_file( 545 self, 546 upload_url: str, 547 file_path: str, 548 progress_callback: Callable[[str, float], None], 549 prev_progress_step: str, 550 cur_progress_step: str, 551 ) -> str: 552 headers = { 553 "x-goog-upload-command": "upload, finalize", 554 "x-goog-upload-offset": "0", 555 } 556 557 with open(file_path, "rb") as f: 558 f.seek(0, os.SEEK_END) 559 size = f.tell() 560 f.seek(0) 561 bytes_sent = 0 562 563 def upload_callback(bytes: int): 564 nonlocal bytes_sent 565 bytes_sent += bytes 566 start_prog = self._progress_steps[prev_progress_step] 567 end_prog = self._progress_steps[cur_progress_step] 568 cur_prog = start_prog + (end_prog - start_prog) * (bytes_sent / size) 569 cur_prog = round(cur_prog, 1) 570 progress_callback(cur_progress_step, cur_prog) 571 572 wrapped_file = CallbackIOWrapper(upload_callback, f) 573 r = self._session.post(upload_url, headers=headers, data=wrapped_file) 574 575 r.raise_for_status() 576 return r.json()["scottyResourceId"] 577 578 def _create_video( 579 self, scotty_resource_id: str, metadata: Metadata, data: YTUploaderVideoData 580 ) -> Optional[str]: 581 if self._session_token == "": 582 return None 583 assert data.front_end_upload_id is not None 584 params = {"key": data.innertube_api_key, "alt": "json"} 585 data = APIRequestCreateVideo.from_session_data( 586 data.channel_id, 587 self._session_token, 588 data.delegated_session_id, 589 data.front_end_upload_id, 590 metadata, 591 scotty_resource_id, 592 ).to_dict() 593 r = self._session.post( 594 "https://studio.youtube.com/youtubei/v1/upload/createvideo", 595 params=params, 596 json=data, 597 ) 598 r.raise_for_status() 599 return r.json().get("videoId") 600 601 def _update_metadata(self, metadata: Metadata, data: YTUploaderVideoData): 602 assert data.encrypted_video_id is not None 603 params = {"key": data.innertube_api_key, "alt": "json"} 604 data = APIRequestUpdateMetadata.from_session_data( 605 data.channel_id, 606 self._session_token, 607 data.delegated_session_id, 608 data.encrypted_video_id, 609 metadata, 610 data.thumbnail_scotty_id, 611 data.thumbnail_format, 612 ).to_dict() 613 r = self._session.post( 614 "https://studio.youtube.com/youtubei/v1/video_manager/metadata_update", 615 params=params, 616 json=data, 617 ) 618 r.raise_for_status()
Class for uploading YouTube videos to a single channel
87 def __init__( 88 self, 89 cookie_jar: FileCookieJar, 90 webdriver_path: Optional[str] = None, 91 selenium_timeout: float = 60, 92 ): 93 """Create YTUploaderSession from generic FileCookieJar 94 95 Args: 96 cookie_jar (FileCookieJar): FileCookieJar. Must have save(), load(), 97 and set_cookie(http.cookiejar.Cookie) methods 98 webdriver_path (str, optional): Optional path to geckodriver or chromedriver 99 executable 100 selenium_timeout (float, optional): Timeout to wait for grst request. 101 Defaults to 60 seconds 102 """ 103 self._session_token = "" 104 self._webdriver_path = webdriver_path 105 self._selenium_timeout = selenium_timeout 106 107 # load cookies and init session 108 self._cookies = cookie_jar 109 self._session = requests.Session() 110 self._reload_cookies() 111 hash = self._generateSAPISIDHASH(self._session.cookies["SAPISID"]) 112 self._session.headers = { 113 "Authorization": f"SAPISIDHASH {hash}", 114 "x-origin": "https://studio.youtube.com", 115 "user-agent": ( 116 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) " 117 "Gecko/20100101 Firefox/119.0" 118 ), 119 }
Create YTUploaderSession from generic FileCookieJar
Arguments:
- cookie_jar (FileCookieJar): FileCookieJar. Must have save(), load(), and set_cookie(http.cookiejar.Cookie) methods
- webdriver_path (str, optional): Optional path to geckodriver or chromedriver executable
- selenium_timeout (float, optional): Timeout to wait for grst request. Defaults to 60 seconds
149 def upload( 150 self, 151 file_path: str, 152 metadata: Metadata, 153 progress_callback: Callable[[str, float], None] = lambda step, percent: None, 154 ) -> str: 155 """Upload a video 156 157 Args: 158 file_path (str): Path to video file 159 metadata (Metadata): Metadata of video to set when uploaded 160 progress_callback (Callable[[str, float], None], optional): Optional 161 progress callback. Callback receives what step uploader is on and what 162 the total percentage of the upload progress is (defined by 163 YTUploaderSession._progress_steps). 164 165 Returns: 166 str: ID of video uploaded 167 """ 168 try: 169 metadata.validate() 170 except ValueError as ex: 171 raise YTUploaderException(f"Validation error: {ex}") from ex 172 progress_callback("start", self._progress_steps["start"]) 173 data = self._get_session_data() 174 progress_callback("get_session_data", self._progress_steps["get_session_data"]) 175 url = self._get_video_upload_url(data) 176 progress_callback("get_upload_url", self._progress_steps["get_upload_url"]) 177 scotty_resource_id = self._upload_file( 178 url, file_path, progress_callback, "get_upload_url", "upload_video" 179 ) 180 progress_callback("upload_video", self._progress_steps["upload_video"]) 181 encrypted_video_id = self._create_video(scotty_resource_id, metadata, data) 182 if encrypted_video_id is None: 183 # could be bad session token, try to get new one 184 self._get_session_token() 185 progress_callback( 186 "get_session_token", self._progress_steps["get_session_token"] 187 ) 188 encrypted_video_id = self._create_video(scotty_resource_id, metadata, data) 189 if encrypted_video_id is None: 190 raise YTUploaderException("Could not create video") 191 data.encrypted_video_id = encrypted_video_id 192 progress_callback("create_video", self._progress_steps["create_video"]) 193 194 # set thumbnail 195 if metadata.thumbnail is not None: 196 url = self._get_upload_url_thumbnail(data) 197 data.thumbnail_scotty_id = self._upload_file( 198 url, 199 metadata.thumbnail, 200 progress_callback, 201 "create_video", 202 "upload_thumbnail", 203 ) 204 data.thumbnail_format = self._get_thumbnail_format(metadata.thumbnail) 205 206 # playlists 207 if metadata.playlists: 208 playlists = self._get_creator_playlists(data) 209 if metadata.playlist_ids is None: 210 metadata.playlist_ids = [] 211 for playlist in metadata.playlists: 212 exists = playlist.title in playlists 213 if (playlist.create_if_title_exists and exists) or ( 214 playlist.create_if_title_doesnt_exist and not exists 215 ): 216 playlist_id = self._create_playlist(playlist, data) 217 metadata.playlist_ids.append(playlist_id) 218 elif exists: 219 metadata.playlist_ids.append(playlists[playlist.title]) 220 # captions 221 if metadata.captions_files: 222 for caption_file in metadata.captions_files: 223 if caption_file.language is None: 224 caption_file.language = metadata.audio_language 225 self._update_captions(caption_file, data) 226 227 self._update_metadata(metadata, data) 228 # save cookies 229 for cookie in self._session.cookies: 230 self._cookies.set_cookie(cookie) 231 self._cookies.save() 232 progress_callback("finish", self._progress_steps["finish"]) 233 return data.encrypted_video_id
Upload a video
Arguments:
- file_path (str): Path to video file
- metadata (Metadata): Metadata of video to set when uploaded
- progress_callback (Callable[[str, float], None], optional): Optional progress callback. Callback receives what step uploader is on and what the total percentage of the upload progress is (defined by YTUploaderSession._progress_steps).
Returns:
str: ID of video uploaded
YouTube uploader exception
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- args
383@dataclass_json 384@dataclass 385class Metadata: 386 """Metadata of video to upload""" 387 388 title: str 389 """Title. Max length 100. Cannot contain < or > characters""" 390 391 description: str = "" 392 """Description. Max length 5000. Cannot contain < or > characters""" 393 394 privacy: PrivacyEnum = PrivacyEnum.PRIVATE 395 """Privacy. Possible values: PUBLIC, UNLISTED, PRIVATE""" 396 397 made_for_kids: bool = False 398 """Made for kids. If true comments will be disabled""" 399 400 tags: tuple[str, ...] = () 401 """List of tags""" 402 403 # optional metadata for update_metadata 404 scheduled_upload: Optional[datetime.datetime] = field( 405 default=None, 406 metadata=config( 407 decoder=lambda x: ( 408 datetime.datetime.fromisoformat(x) if x is not None else None 409 ), 410 encoder=lambda x: datetime.datetime.isoformat(x) if x is not None else None, 411 mm_field=mm_field.DateTime("iso", allow_none=True), 412 ), 413 ) 414 """ 415 Date to make upload public. If set, video will be set to private until the date, 416 unless video is a premiere in which case it will be set to public. Video will not 417 be a premiere unless both premiere_countdown_duration and premiere_theme are set 418 """ 419 420 premiere_countdown_duration: Optional[PremiereDurationEnum] = None 421 """ 422 Duration of premiere countdown in seconds. 423 Possible values: 60, 120, 180, 240, 300 424 """ 425 426 premiere_theme: Optional[PremiereThemeEnum] = None 427 """ 428 Theme of premiere countdown. 429 Possible values: CLASSIC, ALTERNATIVE, AMBIENT, BRIGHT, CALM, CINEMATIC, 430 CONTEMPORARY, DRAMATIC, FUNKY, GENTLE, HAPPY, INSPIRATIONAL, KIDS, SCI_FI, SPORTS 431 """ 432 433 playlist_ids: Optional[list[str]] = None 434 """List of existing playlist IDs to add video to""" 435 436 playlists: Optional[list[Playlist]] = None 437 """List of playlists to create and/or add video to""" 438 439 thumbnail: Optional[str] = None 440 """Path to thumbnail file to upload""" 441 442 publish_to_feed: Optional[bool] = None 443 """Whether to notify subscribers""" 444 445 category: Optional[CategoryEnum] = None 446 """ 447 Category. Category-specific metadata is not supported yet. 448 Possible values: FILM_ANIMATION, AUTOS_VEHICLES, MUSIC, PETS_ANIMALS, SPORTS, 449 TRAVEL_EVENTS, GAMING, PEOPLE_BLOGS, COMEDY, ENTERTAINMENT, NEWS_POLITICS, 450 HOWTO_STYLE, EDUCATION, SCIENCE_TECH, NONPROFITS_ACTIVISM 451 """ 452 453 auto_chapter: Optional[bool] = None 454 """Whether to use automatic video chapters""" 455 456 auto_places: Optional[bool] = None 457 """Whether to use automatic places""" 458 459 auto_concepts: Optional[bool] = None 460 """Whether to use automatic concepts""" 461 462 has_product_placement: Optional[bool] = None 463 """Whether video has product placement""" 464 465 show_product_placement_overlay: Optional[bool] = None 466 """Whether to show product placement overlay""" 467 468 recorded_date: Optional[datetime.date] = field( 469 default=None, 470 metadata=config( 471 decoder=lambda x: datetime.date.fromisoformat(x) if x is not None else None, 472 encoder=lambda x: datetime.date.isoformat(x) if x is not None else None, 473 mm_field=mm_field.Date("iso", allow_none=True), 474 ), 475 ) 476 """Day, month, and year that video was recorded""" 477 478 restricted_to_over_18: Optional[bool] = None 479 """Whether video is age restricted""" 480 481 audio_language: Optional[LanguageEnum] = None 482 """Language of audio""" 483 484 captions_files: Optional[list[CaptionsFile]] = None 485 """Path to captions files (.srt)""" 486 487 license: Optional[LicenseEnum] = None 488 """License. Possible values: STANDARD, CREATIVE_COMMONS""" 489 490 allow_comments: Optional[bool] = None 491 """Whether to allow comments""" 492 493 allow_comments_mode: Optional[AllowCommentsEnum] = None 494 """Comment filtering mode. Possible values: ALL_COMMENTS, HOLD_INAPPROPRIATE, 495 HOLD_INAPPROPRIATE_STRICT, HOLD_ALL""" 496 497 can_view_ratings: Optional[bool] = None 498 """Whether video likes/dislikes can be seen""" 499 500 comments_sort_order: Optional[CommentsSortOrderEnum] = None 501 """Default comment sort order. Possible values: LATEST, TOP""" 502 503 allow_embedding: Optional[bool] = None 504 """Whether to allow embedding on 3rd party sites""" 505 506 def validate(self): 507 """Raises error if metadata is invalid""" 508 if ( 509 self.premiere_countdown_duration is not None 510 or self.premiere_theme is not None 511 ): 512 if None in ( 513 self.premiere_countdown_duration, 514 self.premiere_theme, 515 self.scheduled_upload, 516 ): 517 raise ValueError( 518 "If trying to upload a premiere, premiere_countdown_duration, " 519 "premiere_theme, and scheduled_upload must be set" 520 ) 521 522 if self.captions_files is not None: 523 for caption_file in self.captions_files: 524 if caption_file.language is None and self.audio_language is None: 525 raise ValueError( 526 "Must either specify captions file language or audio_language" 527 ) 528 529 if self.restricted_to_over_18 and self.made_for_kids: 530 raise ValueError( 531 "Video cannot be made for kids and also restricted to over 18" 532 ) 533 534 if len(self.title) > 100: 535 raise ValueError("Title must be at most 100 characters long") 536 537 if len(self.description) > 5000: 538 raise ValueError("Description must be at most 5000 characters long") 539 540 to_check = [self.title, self.description] + list(self.tags) 541 if self.playlists: 542 for p in self.playlists: 543 to_check += [p.title, p.description] 544 545 if any(c in s for c in "<>" for s in to_check): 546 raise ValueError( 547 "Titles, descriptions, and tags cannot contain angled brackets" 548 ) 549 550 errors = self.schema().validate(self.to_dict()) 551 if errors: 552 raise ValueError(f"{errors}")
Metadata of video to upload
Date to make upload public. If set, video will be set to private until the date, unless video is a premiere in which case it will be set to public. Video will not be a premiere unless both premiere_countdown_duration and premiere_theme are set
Theme of premiere countdown. Possible values: CLASSIC, ALTERNATIVE, AMBIENT, BRIGHT, CALM, CINEMATIC, CONTEMPORARY, DRAMATIC, FUNKY, GENTLE, HAPPY, INSPIRATIONAL, KIDS, SCI_FI, SPORTS
Category. Category-specific metadata is not supported yet. Possible values: FILM_ANIMATION, AUTOS_VEHICLES, MUSIC, PETS_ANIMALS, SPORTS, TRAVEL_EVENTS, GAMING, PEOPLE_BLOGS, COMEDY, ENTERTAINMENT, NEWS_POLITICS, HOWTO_STYLE, EDUCATION, SCIENCE_TECH, NONPROFITS_ACTIVISM
Comment filtering mode. Possible values: ALL_COMMENTS, HOLD_INAPPROPRIATE, HOLD_INAPPROPRIATE_STRICT, HOLD_ALL
506 def validate(self): 507 """Raises error if metadata is invalid""" 508 if ( 509 self.premiere_countdown_duration is not None 510 or self.premiere_theme is not None 511 ): 512 if None in ( 513 self.premiere_countdown_duration, 514 self.premiere_theme, 515 self.scheduled_upload, 516 ): 517 raise ValueError( 518 "If trying to upload a premiere, premiere_countdown_duration, " 519 "premiere_theme, and scheduled_upload must be set" 520 ) 521 522 if self.captions_files is not None: 523 for caption_file in self.captions_files: 524 if caption_file.language is None and self.audio_language is None: 525 raise ValueError( 526 "Must either specify captions file language or audio_language" 527 ) 528 529 if self.restricted_to_over_18 and self.made_for_kids: 530 raise ValueError( 531 "Video cannot be made for kids and also restricted to over 18" 532 ) 533 534 if len(self.title) > 100: 535 raise ValueError("Title must be at most 100 characters long") 536 537 if len(self.description) > 5000: 538 raise ValueError("Description must be at most 5000 characters long") 539 540 to_check = [self.title, self.description] + list(self.tags) 541 if self.playlists: 542 for p in self.playlists: 543 to_check += [p.title, p.description] 544 545 if any(c in s for c in "<>" for s in to_check): 546 raise ValueError( 547 "Titles, descriptions, and tags cannot contain angled brackets" 548 ) 549 550 errors = self.schema().validate(self.to_dict()) 551 if errors: 552 raise ValueError(f"{errors}")
Raises error if metadata is invalid
27 def to_json(self, 28 *, 29 skipkeys: bool = False, 30 ensure_ascii: bool = True, 31 check_circular: bool = True, 32 allow_nan: bool = True, 33 indent: Optional[Union[int, str]] = None, 34 separators: Optional[Tuple[str, str]] = None, 35 default: Optional[Callable] = None, 36 sort_keys: bool = False, 37 **kw) -> str: 38 return json.dumps(self.to_dict(encode_json=False), 39 cls=_ExtendedEncoder, 40 skipkeys=skipkeys, 41 ensure_ascii=ensure_ascii, 42 check_circular=check_circular, 43 allow_nan=allow_nan, 44 indent=indent, 45 separators=separators, 46 default=default, 47 sort_keys=sort_keys, 48 **kw)
50 @classmethod 51 def from_json(cls: Type[A], 52 s: JsonData, 53 *, 54 parse_float=None, 55 parse_int=None, 56 parse_constant=None, 57 infer_missing=False, 58 **kw) -> A: 59 kvs = json.loads(s, 60 parse_float=parse_float, 61 parse_int=parse_int, 62 parse_constant=parse_constant, 63 **kw) 64 return cls.from_dict(kvs, infer_missing=infer_missing)
76 @classmethod 77 def schema(cls: Type[A], 78 *, 79 infer_missing: bool = False, 80 only=None, 81 exclude=(), 82 many: bool = False, 83 context=None, 84 load_only=(), 85 dump_only=(), 86 partial: bool = False, 87 unknown=None) -> "SchemaType[A]": 88 Schema = build_schema(cls, DataClassJsonMixin, infer_missing, partial) 89 90 if unknown is None: 91 undefined_parameter_action = _undefined_parameter_action_safe(cls) 92 if undefined_parameter_action is not None: 93 # We can just make use of the same-named mm keywords 94 unknown = undefined_parameter_action.name.lower() 95 96 return Schema(only=only, 97 exclude=exclude, 98 many=many, 99 context=context, 100 load_only=load_only, 101 dump_only=dump_only, 102 partial=partial, 103 unknown=unknown)
349@dataclass_json 350@dataclass 351class Playlist: 352 """Metadata of playlist to create and/or add video to""" 353 354 title: str 355 """Title. Max length 150""" 356 357 description: str = "" 358 """Description. Max length 5000""" 359 360 privacy: PrivacyEnum = PrivacyEnum.PUBLIC 361 """Privacy. Possible values: PUBLIC, UNLISTED, PRIVATE""" 362 363 create_if_title_exists: bool = False 364 """Whether to create playlist if a playlist with the same 365 title already exists on the channel""" 366 367 create_if_title_doesnt_exist: bool = True 368 """Whether to create playlist if there is no playlist with the same title"""
Metadata of playlist to create and/or add video to
Whether to create playlist if a playlist with the same title already exists on the channel
Whether to create playlist if there is no playlist with the same title
27 def to_json(self, 28 *, 29 skipkeys: bool = False, 30 ensure_ascii: bool = True, 31 check_circular: bool = True, 32 allow_nan: bool = True, 33 indent: Optional[Union[int, str]] = None, 34 separators: Optional[Tuple[str, str]] = None, 35 default: Optional[Callable] = None, 36 sort_keys: bool = False, 37 **kw) -> str: 38 return json.dumps(self.to_dict(encode_json=False), 39 cls=_ExtendedEncoder, 40 skipkeys=skipkeys, 41 ensure_ascii=ensure_ascii, 42 check_circular=check_circular, 43 allow_nan=allow_nan, 44 indent=indent, 45 separators=separators, 46 default=default, 47 sort_keys=sort_keys, 48 **kw)
50 @classmethod 51 def from_json(cls: Type[A], 52 s: JsonData, 53 *, 54 parse_float=None, 55 parse_int=None, 56 parse_constant=None, 57 infer_missing=False, 58 **kw) -> A: 59 kvs = json.loads(s, 60 parse_float=parse_float, 61 parse_int=parse_int, 62 parse_constant=parse_constant, 63 **kw) 64 return cls.from_dict(kvs, infer_missing=infer_missing)
76 @classmethod 77 def schema(cls: Type[A], 78 *, 79 infer_missing: bool = False, 80 only=None, 81 exclude=(), 82 many: bool = False, 83 context=None, 84 load_only=(), 85 dump_only=(), 86 partial: bool = False, 87 unknown=None) -> "SchemaType[A]": 88 Schema = build_schema(cls, DataClassJsonMixin, infer_missing, partial) 89 90 if unknown is None: 91 undefined_parameter_action = _undefined_parameter_action_safe(cls) 92 if undefined_parameter_action is not None: 93 # We can just make use of the same-named mm keywords 94 unknown = undefined_parameter_action.name.lower() 95 96 return Schema(only=only, 97 exclude=exclude, 98 many=many, 99 context=context, 100 load_only=load_only, 101 dump_only=dump_only, 102 partial=partial, 103 unknown=unknown)
371@dataclass_json 372@dataclass 373class CaptionsFile: 374 """Subtitles file""" 375 376 path: str 377 """Path to .srt file""" 378 379 language: Optional[LanguageEnum] = None 380 """Language of captions. If None, language will default to audio language"""
Subtitles file
27 def to_json(self, 28 *, 29 skipkeys: bool = False, 30 ensure_ascii: bool = True, 31 check_circular: bool = True, 32 allow_nan: bool = True, 33 indent: Optional[Union[int, str]] = None, 34 separators: Optional[Tuple[str, str]] = None, 35 default: Optional[Callable] = None, 36 sort_keys: bool = False, 37 **kw) -> str: 38 return json.dumps(self.to_dict(encode_json=False), 39 cls=_ExtendedEncoder, 40 skipkeys=skipkeys, 41 ensure_ascii=ensure_ascii, 42 check_circular=check_circular, 43 allow_nan=allow_nan, 44 indent=indent, 45 separators=separators, 46 default=default, 47 sort_keys=sort_keys, 48 **kw)
50 @classmethod 51 def from_json(cls: Type[A], 52 s: JsonData, 53 *, 54 parse_float=None, 55 parse_int=None, 56 parse_constant=None, 57 infer_missing=False, 58 **kw) -> A: 59 kvs = json.loads(s, 60 parse_float=parse_float, 61 parse_int=parse_int, 62 parse_constant=parse_constant, 63 **kw) 64 return cls.from_dict(kvs, infer_missing=infer_missing)
76 @classmethod 77 def schema(cls: Type[A], 78 *, 79 infer_missing: bool = False, 80 only=None, 81 exclude=(), 82 many: bool = False, 83 context=None, 84 load_only=(), 85 dump_only=(), 86 partial: bool = False, 87 unknown=None) -> "SchemaType[A]": 88 Schema = build_schema(cls, DataClassJsonMixin, infer_missing, partial) 89 90 if unknown is None: 91 undefined_parameter_action = _undefined_parameter_action_safe(cls) 92 if undefined_parameter_action is not None: 93 # We can just make use of the same-named mm keywords 94 unknown = undefined_parameter_action.name.lower() 95 96 return Schema(only=only, 97 exclude=exclude, 98 many=many, 99 context=context, 100 load_only=load_only, 101 dump_only=dump_only, 102 partial=partial, 103 unknown=unknown)
302class PremiereThemeEnum(str, Enum): 303 CLASSIC = "VIDEO_PREMIERE_INTRO_THEME_DEFAULT" 304 ALTERNATIVE = "VIDEO_PREMIERE_INTRO_THEME_ALTERNATIVE" 305 AMBIENT = "VIDEO_PREMIERE_INTRO_THEME_AMBIENT" 306 BRIGHT = "VIDEO_PREMIERE_INTRO_THEME_BRIGHT" 307 CALM = "VIDEO_PREMIERE_INTRO_THEME_CALM" 308 CINEMATIC = "VIDEO_PREMIERE_INTRO_THEME_CINEMATIC" 309 CONTEMPORARY = "VIDEO_PREMIERE_INTRO_THEME_CONTEMPORARY" 310 DRAMATIC = "VIDEO_PREMIERE_INTRO_THEME_DRAMATIC" 311 FUNKY = "VIDEO_PREMIERE_INTRO_THEME_FUNKY" 312 GENTLE = "VIDEO_PREMIERE_INTRO_THEME_GENTLE" 313 HAPPY = "VIDEO_PREMIERE_INTRO_THEME_HAPPY" 314 INSPIRATIONAL = "VIDEO_PREMIERE_INTRO_THEME_INSPIRATIONAL" 315 KIDS = "VIDEO_PREMIERE_INTRO_THEME_KIDS" 316 SCI_FI = "VIDEO_PREMIERE_INTRO_THEME_SCI_FI" 317 SPORTS = "VIDEO_PREMIERE_INTRO_THEME_SPORTS"
An enumeration.
Inherited Members
- enum.Enum
- name
- value
- builtins.str
- encode
- replace
- split
- rsplit
- join
- capitalize
- casefold
- title
- center
- count
- expandtabs
- find
- partition
- index
- ljust
- lower
- lstrip
- rfind
- rindex
- rjust
- rstrip
- rpartition
- splitlines
- strip
- swapcase
- translate
- upper
- startswith
- endswith
- removeprefix
- removesuffix
- isascii
- islower
- isupper
- istitle
- isspace
- isdecimal
- isdigit
- isnumeric
- isalpha
- isalnum
- isidentifier
- isprintable
- zfill
- format
- format_map
- maketrans
294class PremiereDurationEnum(str, Enum): 295 ONE_MIN = "60" 296 TWO_MIN = "120" 297 THREE_MIN = "180" 298 FOUR_MIN = "240" 299 FIVE_MIN = "300"
An enumeration.
Inherited Members
- enum.Enum
- name
- value
- builtins.str
- encode
- replace
- split
- rsplit
- join
- capitalize
- casefold
- title
- center
- count
- expandtabs
- find
- partition
- index
- ljust
- lower
- lstrip
- rfind
- rindex
- rjust
- rstrip
- rpartition
- splitlines
- strip
- swapcase
- translate
- upper
- startswith
- endswith
- removeprefix
- removesuffix
- isascii
- islower
- isupper
- istitle
- isspace
- isdecimal
- isdigit
- isnumeric
- isalpha
- isalnum
- isidentifier
- isprintable
- zfill
- format
- format_map
- maketrans
289class ThumbnailFormatEnum(str, Enum): 290 PNG = "CUSTOM_THUMBNAIL_IMAGE_FORMAT_PNG" 291 JPG = "CUSTOM_THUMBNAIL_IMAGE_FORMAT_JPEG"
An enumeration.
Inherited Members
- enum.Enum
- name
- value
- builtins.str
- encode
- replace
- split
- rsplit
- join
- capitalize
- casefold
- title
- center
- count
- expandtabs
- find
- partition
- index
- ljust
- lower
- lstrip
- rfind
- rindex
- rjust
- rstrip
- rpartition
- splitlines
- strip
- swapcase
- translate
- upper
- startswith
- endswith
- removeprefix
- removesuffix
- isascii
- islower
- isupper
- istitle
- isspace
- isdecimal
- isdigit
- isnumeric
- isalpha
- isalnum
- isidentifier
- isprintable
- zfill
- format
- format_map
- maketrans
284class CommentsSortOrderEnum(str, Enum): 285 LATEST = "MDE_COMMENT_SORT_ORDER_LATEST" 286 TOP = "MDE_COMMENT_SORT_ORDER_TOP"
An enumeration.
Inherited Members
- enum.Enum
- name
- value
- builtins.str
- encode
- replace
- split
- rsplit
- join
- capitalize
- casefold
- title
- center
- count
- expandtabs
- find
- partition
- index
- ljust
- lower
- lstrip
- rfind
- rindex
- rjust
- rstrip
- rpartition
- splitlines
- strip
- swapcase
- translate
- upper
- startswith
- endswith
- removeprefix
- removesuffix
- isascii
- islower
- isupper
- istitle
- isspace
- isdecimal
- isdigit
- isnumeric
- isalpha
- isalnum
- isidentifier
- isprintable
- zfill
- format
- format_map
- maketrans
277class AllowCommentsEnum(str, Enum): 278 ALL_COMMENTS = "ALL_COMMENTS" 279 HOLD_INAPPROPRIATE = "AUTOMATED_COMMENTS" 280 HOLD_INAPPROPRIATE_STRICT = "AUTO_MODERATED_COMMENTS_HOLD_MORE" 281 HOLD_ALL = "APPROVED_COMMENTS"
An enumeration.
Inherited Members
- enum.Enum
- name
- value
- builtins.str
- encode
- replace
- split
- rsplit
- join
- capitalize
- casefold
- title
- center
- count
- expandtabs
- find
- partition
- index
- ljust
- lower
- lstrip
- rfind
- rindex
- rjust
- rstrip
- rpartition
- splitlines
- strip
- swapcase
- translate
- upper
- startswith
- endswith
- removeprefix
- removesuffix
- isascii
- islower
- isupper
- istitle
- isspace
- isdecimal
- isdigit
- isnumeric
- isalpha
- isalnum
- isidentifier
- isprintable
- zfill
- format
- format_map
- maketrans
272class LicenseEnum(str, Enum): 273 STANDARD = "standard" 274 CREATIVE_COMMONS = "creative_commons"
An enumeration.
Inherited Members
- enum.Enum
- name
- value
- builtins.str
- encode
- replace
- split
- rsplit
- join
- capitalize
- casefold
- title
- center
- count
- expandtabs
- find
- partition
- index
- ljust
- lower
- lstrip
- rfind
- rindex
- rjust
- rstrip
- rpartition
- splitlines
- strip
- swapcase
- translate
- upper
- startswith
- endswith
- removeprefix
- removesuffix
- isascii
- islower
- isupper
- istitle
- isspace
- isdecimal
- isdigit
- isnumeric
- isalpha
- isalnum
- isidentifier
- isprintable
- zfill
- format
- format_map
- maketrans
35class LanguageEnum(str, Enum): 36 NOT_APPLICABLE = "zxx" 37 ABKHAZIAN = "ab" 38 AFAR = "aa" 39 AFRIKAANS = "af" 40 AKAN = "ak" 41 AKKADIAN = "akk" 42 ALBANIAN = "sq" 43 AMERICAN_SIGN_LANGUAGE = "ase" 44 AMHARIC = "am" 45 ARABIC = "ar" 46 ARAMAIC = "arc" 47 ARMENIAN = "hy" 48 ASSAMESE = "as" 49 AYMARA = "ay" 50 AZERBAIJANI = "az" 51 BAMBARA = "bm" 52 BANGLA = "bn" 53 BANGLA_INDIA = "bn-IN" 54 BASHKIR = "ba" 55 BASQUE = "eu" 56 BELARUSIAN = "be" 57 BHOJPURI = "bho" 58 BISLAMA = "bi" 59 BODO = "brx" 60 BOSNIAN = "bs" 61 BRETON = "br" 62 BULGARIAN = "bg" 63 BURMESE = "my" 64 CANTONESE = "yue" 65 CANTONESE_HONG_KONG = "yue-HK" 66 CATALAN = "ca" 67 CHEROKEE = "chr" 68 CHINESE = "zh" 69 CHINESE_CHINA = "zh-CN" 70 CHINESE_HONG_KONG = "zh-HK" 71 CHINESE_SIMPLIFIED = "zh-Hans" 72 CHINESE_SINGAPORE = "zh-SG" 73 CHINESE_TAIWAN = "zh-TW" 74 CHINESE_TRADITIONAL = "zh-Hant" 75 CHOCTAW = "cho" 76 COPTIC = "cop" 77 CORSICAN = "co" 78 CREE = "cr" 79 CROATIAN = "hr" 80 CZECH = "cs" 81 DANISH = "da" 82 DOGRI = "doi" 83 DUTCH = "nl" 84 DUTCH_BELGIUM = "nl-BE" 85 DUTCH_NETHERLANDS = "nl-NL" 86 DZONGKHA = "dz" 87 ENGLISH = "en" 88 ENGLISH_AUSTRALIA = "en-AU" 89 ENGLISH_CANADA = "en-CA" 90 ENGLISH_INDIA = "en-IN" 91 ENGLISH_IRELAND = "en-IE" 92 ENGLISH_UNITED_KINGDOM = "en-GB" 93 ENGLISH_UNITED_STATES = "en-US" 94 ESPERANTO = "eo" 95 ESTONIAN = "et" 96 EWE = "ee" 97 FAROESE = "fo" 98 FIJIAN = "fj" 99 FILIPINO = "fil" 100 FINNISH = "fi" 101 FRENCH = "fr" 102 FRENCH_BELGIUM = "fr-BE" 103 FRENCH_CANADA = "fr-CA" 104 FRENCH_FRANCE = "fr-FR" 105 FRENCH_SWITZERLAND = "fr-CH" 106 FULA = "ff" 107 GALICIAN = "gl" 108 GANDA = "lg" 109 GEORGIAN = "ka" 110 GERMAN = "de" 111 GERMAN_AUSTRIA = "de-AT" 112 GERMAN_GERMANY = "de-DE" 113 GERMAN_SWITZERLAND = "de-CH" 114 GREEK = "el" 115 GUARANI = "gn" 116 GUJARATI = "gu" 117 GUSII = "guz" 118 HAITIAN_CREOLE = "ht" 119 HAKKA_CHINESE = "hak" 120 HAKKA_CHINESE_TAIWAN = "hak-TW" 121 HARYANVI = "bgc" 122 HAUSA = "ha" 123 HAWAIIAN = "haw" 124 HEBREW = "he" 125 HINDI = "hi" 126 HINDI_LATIN = "hi-Latn" 127 HIRI_MOTU = "ho" 128 HUNGARIAN = "hu" 129 ICELANDIC = "is" 130 IGBO = "ig" 131 INDONESIAN = "id" 132 INTERLINGUA = "ia" 133 INTERLINGUE = "ie" 134 INUKTITUT = "iu" 135 INUPIAQ = "ik" 136 IRISH = "ga" 137 ITALIAN = "it" 138 JAPANESE = "ja" 139 JAVANESE = "jv" 140 KALAALLISUT = "kl" 141 KALENJIN = "kln" 142 KAMBA = "kam" 143 KANNADA = "kn" 144 KASHMIRI = "ks" 145 KAZAKH = "kk" 146 KHMER = "km" 147 KIKUYU = "ki" 148 KINYARWANDA = "rw" 149 KLINGON = "tlh" 150 KONKANI = "kok" 151 KOREAN = "ko" 152 KURDISH = "ku" 153 KYRGYZ = "ky" 154 LADINO = "lad" 155 LAO = "lo" 156 LATIN = "la" 157 LATVIAN = "lv" 158 LINGALA = "ln" 159 LITHUANIAN = "lt" 160 LUBA_KATANGA = "lu" 161 LUO = "luo" 162 LUXEMBOURGISH = "lb" 163 LUYIA = "luy" 164 MACEDONIAN = "mk" 165 MAITHILI = "mai" 166 MALAGASY = "mg" 167 MALAY = "ms" 168 MALAY_SINGAPORE = "ms-SG" 169 MALAYALAM = "ml" 170 MALTESE = "mt" 171 MANIPURI = "mni" 172 MAORI = "mi" 173 MARATHI = "mr" 174 MASAI = "mas" 175 MERU = "mer" 176 MIN_NAN_CHINESE = "nan" 177 MIN_NAN_CHINESE_TAIWAN = "nan-TW" 178 MIXE = "mco" 179 MIZO = "lus" 180 MONGOLIAN = "mn" 181 MONGOLIAN_MONGOLIAN = "mn-M" 182 NAURU = "na" 183 NAVAJO = "nv" 184 NEPALI = "ne" 185 NIGERIAN_PIDGIN = "pcm" 186 NORTH_NDEBELE = "nd" 187 NORTHERN_SOTHO = "nso" 188 NORWEGIAN = "no" 189 OCCITAN = "oc" 190 ODIA = "or" 191 OROMO = "om" 192 PAPIAMENTO = "pap" 193 PASHTO = "ps" 194 PERSIAN = "fa" 195 PERSIAN_AFGHANISTAN = "fa-AF" 196 PERSIAN_IRAN = "fa-IR" 197 POLISH = "pl" 198 PORTUGUESE = "pt" 199 PORTUGUESE_BRAZIL = "pt-BR" 200 PORTUGUESE_PORTUGAL = "pt-PT" 201 PUNJABI = "pa" 202 QUECHUA = "qu" 203 ROMANIAN = "ro" 204 ROMANIAN_MOLDOVA = "ro-MD" 205 ROMANSH = "rm" 206 RUNDI = "rn" 207 RUSSIAN = "ru" 208 RUSSIAN_LATIN = "ru-Latn" 209 SAMOAN = "sm" 210 SANGO = "sg" 211 SANSKRIT = "sa" 212 SANTALI = "sat" 213 SARDINIAN = "sc" 214 SCOTTISH_GAELIC = "gd" 215 SERBIAN = "sr" 216 SERBIAN_CYRILLIC = "hr-Cyrl" 217 SERBIAN_LATIN = "sr-Latn" 218 SERBO_CROATIAN = "sh" 219 SHERDUKPEN = "sdp" 220 SHONA = "sn" 221 SICILIAN = "scn" 222 SINDHI = "sd" 223 SINHALA = "si" 224 SLOVAK = "sk" 225 SLOVENIAN = "sl" 226 SOMALI = "so" 227 SOUTH_NDEBELE = "nr" 228 SOUTHERN_SOTHO = "st" 229 SPANISH = "es" 230 SPANISH_LATIN_AMERICA = "es-419" 231 SPANISH_MEXICO = "es-MX" 232 SPANISH_SPAIN = "es-ES" 233 SPANISH_UNITED_STATES = "es-US" 234 SUNDANESE = "su" 235 SWAHILI = "sw" 236 SWATI = "ss" 237 SWEDISH = "sv" 238 TAGALOG = "tl" 239 TAJIK = "tg" 240 TAMIL = "ta" 241 TATAR = "tt" 242 TELUGU = "te" 243 THAI = "th" 244 TIBETAN = "bo" 245 TIGRINYA = "ti" 246 TOK_PISIN = "tpi" 247 TOKI_PONA = "tok" 248 TONGAN = "to" 249 TSONGA = "ts" 250 TSWANA = "tn" 251 TURKISH = "tr" 252 TURKMEN = "tk" 253 TWI = "tw" 254 UKRAINIAN = "uk" 255 URDU = "ur" 256 UYGHUR = "ug" 257 UZBEK = "uz" 258 VENDA = "ve" 259 VIETNAMESE = "vi" 260 VOLAPÜK = "vo" 261 VÕRO = "vro" 262 WELSH = "cy" 263 WESTERN_FRISIAN = "fy" 264 WOLAYTTA = "wal" 265 WOLOF = "wo" 266 XHOSA = "xh" 267 YIDDISH = "yi" 268 YORUBA = "yo" 269 ZULU = "zu"
An enumeration.
Inherited Members
- enum.Enum
- name
- value
- builtins.str
- encode
- replace
- split
- rsplit
- join
- capitalize
- casefold
- title
- center
- count
- expandtabs
- find
- partition
- index
- ljust
- lower
- lstrip
- rfind
- rindex
- rjust
- rstrip
- rpartition
- splitlines
- strip
- swapcase
- translate
- upper
- startswith
- endswith
- removeprefix
- removesuffix
- isascii
- islower
- isupper
- istitle
- isspace
- isdecimal
- isdigit
- isnumeric
- isalpha
- isalnum
- isidentifier
- isprintable
- zfill
- format
- format_map
- maketrans
17class CategoryEnum(int, Enum): 18 FILM_ANIMATION = 1 19 AUTOS_VEHICLES = 2 20 MUSIC = 10 21 PETS_ANIMALS = 15 22 SPORTS = 17 23 TRAVEL_EVENTS = 19 24 GAMING = 20 25 PEOPLE_BLOGS = 22 26 COMEDY = 23 27 ENTERTAINMENT = 24 28 NEWS_POLITICS = 25 29 HOWTO_STYLE = 26 30 EDUCATION = 27 31 SCIENCE_TECH = 28 32 NONPROFITS_ACTIVISM = 29
An enumeration.
Inherited Members
- enum.Enum
- name
- value
- builtins.int
- conjugate
- bit_length
- bit_count
- to_bytes
- from_bytes
- as_integer_ratio
- real
- imag
- numerator
- denominator
11class PrivacyEnum(str, Enum): 12 PRIVATE = "PRIVATE" 13 UNLISTED = "UNLISTED" 14 PUBLIC = "PUBLIC"
An enumeration.
Inherited Members
- enum.Enum
- name
- value
- builtins.str
- encode
- replace
- split
- rsplit
- join
- capitalize
- casefold
- title
- center
- count
- expandtabs
- find
- partition
- index
- ljust
- lower
- lstrip
- rfind
- rindex
- rjust
- rstrip
- rpartition
- splitlines
- strip
- swapcase
- translate
- upper
- startswith
- endswith
- removeprefix
- removesuffix
- isascii
- islower
- isupper
- istitle
- isspace
- isdecimal
- isdigit
- isnumeric
- isalpha
- isalnum
- isidentifier
- isprintable
- zfill
- format
- format_map
- maketrans