Package ytstudio
23 | 27 | Expand source code 28 |
29 |from hashlib import sha1
30 | import time
31 | import aiohttp
32 | import asyncio
33 | import aiofiles
34 | from pyquery import PyQuery as pq
35 | import js2py
36 | import js2py.pyjs
37 | import random
38 | import os
39 | import json
40 | from .templates import Templates
41 | import typing
42 | import pathlib
43 | import base64
44 | import datetime
45 |
46 |
47 | class Studio:
48 | YT_STUDIO_URL = "https://studio.youtube.com"
49 | USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36"
50 | TRANSFERRED_BYTES = 0
51 | CHUNK_SIZE = 64*1024
52 |
53 | def __init__(self, cookies: dict = {'SESSION_TOKEN': '', 'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN_INFO': '', 'SID': '', '__Secure-3PSID': '.', 'HSID': '',
54 | 'SSID': '', 'APISID': '', 'SAPISID': '', '__Secure-3PAPISID': '', 'YSC': '', 'SIDCC': ''}):
55 | self.SAPISIDHASH = self.generateSAPISIDHASH(cookies['SAPISID'])
56 | self.cookies = cookies
57 | self.Cookie = " ".join(
58 | [f"{c}={cookies[c]};" if not c in ["SESSION_TOKEN", "BOTGUARD_RESPONSE"] else "" for c in cookies.keys()])
59 | self.HEADERS = {
60 | 'Authorization': f'SAPISIDHASH {self.SAPISIDHASH}',
61 | 'Content-Type': 'application/json',
62 | 'Cookie': self.Cookie,
63 | 'X-Origin': self.YT_STUDIO_URL,
64 | 'User-Agent': self.USER_AGENT
65 | }
66 | self.session = aiohttp.ClientSession(headers=self.HEADERS)
67 | self.loop = asyncio.get_event_loop()
68 | self.config = {}
69 | self.js = js2py.EvalJs()
70 | self.js.execute("var window = {ytcfg: {}};")
71 |
72 | def __del__(self):
73 | asyncio.run(self.session.close())
74 |
75 | def generateSAPISIDHASH(self, SAPISID) -> str:
76 | hash = f"{round(time.time())} {SAPISID} {self.YT_STUDIO_URL}"
77 | sifrelenmis = sha1(hash.encode('utf-8')).hexdigest()
78 | return f"{round(time.time())}_{sifrelenmis}"
79 |
80 | async def getMainPage(self) -> str:
81 | page = await self.session.get(self.YT_STUDIO_URL)
82 | return await page.text("utf-8")
83 |
84 | async def login(self) -> bool:
85 | """
86 | Login to your youtube account
87 | """
88 | page = await self.getMainPage()
89 | _ = pq(page)
90 | script = _("script")
91 | if len(script) < 1:
92 | raise Exception("Didn't find script. Can you check your cookies?")
93 | script = script[0].text
94 | self.js.execute(
95 | f"{script} window.ytcfg = ytcfg;")
96 |
97 | INNERTUBE_API_KEY = self.js.window.ytcfg.data_.INNERTUBE_API_KEY
98 | CHANNEL_ID = self.js.window.ytcfg.data_.CHANNEL_ID
99 | DELEGATED_SESSION_ID = self.js.window.ytcfg.data_.DELEGATED_SESSION_ID
100 |
101 | if INNERTUBE_API_KEY == None or CHANNEL_ID == None:
102 | raise Exception(
103 | "Didn't find INNERTUBE_API_KEY or CHANNEL_ID. Can you check your cookies?")
104 | self.config = {'INNERTUBE_API_KEY': INNERTUBE_API_KEY,
105 | 'CHANNEL_ID': CHANNEL_ID, 'data_': self.js.window.ytcfg.data_}
106 | self.templates = Templates({
107 | 'channelId': CHANNEL_ID,
108 | 'sessionToken': self.cookies['SESSION_TOKEN'],
109 | 'botguardResponse': self.cookies['BOTGUARD_RESPONSE'] if 'BOTGUARD_RESPONSE' in self.cookies else '',
110 | 'delegatedSessionId': DELEGATED_SESSION_ID
111 | })
112 |
113 | return True
114 |
115 | def generateHash(self) -> str:
116 | harfler = list(
117 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
118 | keys = ['' for i in range(0, 36)]
119 | b = 0
120 | c = ""
121 | e = 0
122 |
123 | while e < 36:
124 | if 8 == e or 13 == e or 18 == e or 23 == e:
125 | keys[e] = "-"
126 | else:
127 | if 14 == e:
128 | keys[e] = "4"
129 | elif 2 >= b:
130 | b = round(33554432 + 16777216 * random.uniform(0, 0.9))
131 | c = b & 15
132 | b = b >> 4
133 | keys[e] = harfler[c & 3 | 8 if 19 == e else c]
134 | e += 1
135 |
136 | return "".join(keys)
137 |
138 | async def fileSender(self, file_name):
139 | async with aiofiles.open(file_name, 'rb') as f:
140 | chunk = await f.read(self.CHUNK_SIZE)
141 | while chunk:
142 | if self.progress != None:
143 | self.TRANSFERRED_BYTES += len(chunk)
144 | self.progress(self.TRANSFERRED_BYTES,
145 | os.path.getsize(file_name))
146 |
147 | self.TRANSFERRED_BYTES += len(chunk)
148 | yield chunk
149 | chunk = await f.read(self.CHUNK_SIZE)
150 | if not chunk:
151 | break
152 |
153 | async def uploadFileToYoutube(self, upload_url, file_path):
154 | self.TRANSFERRED_BYTES = 0
155 |
156 | uploaded = await self.session.post(upload_url, headers={
157 | "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'",
158 | "x-goog-upload-command": "upload, finalize",
159 | "x-goog-upload-file-name": f"file-{round(time.time())}",
160 | "x-goog-upload-offset": "0",
161 | "Referer": self.YT_STUDIO_URL,
162 | }, data=self.fileSender(file_path), timeout=None)
163 | _ = await uploaded.text("utf-8")
164 | _ = json.loads(_)
165 | return _['scottyResourceId']
166 |
167 | async def uploadVideo(self, file_name, title=f"New Video {round(time.time())}", description='This video uploaded by github.com/yusufusta/ytstudio', privacy='PRIVATE', draft=False, progress=None, extra_fields={}):
168 | """
169 | Uploads a video to youtube.
170 | """
171 | self.progress = progress
172 | frontEndUID = f"innertube_studio:{self.generateHash()}:0"
173 |
174 | uploadRequest = await self.session.post("https://upload.youtube.com/upload/studio",
175 | headers={
176 | "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'",
177 | "x-goog-upload-command": "start",
178 | "x-goog-upload-file-name": f"file-{round(time.time())}",
179 | "x-goog-upload-protocol": "resumable",
180 | "Referer": self.YT_STUDIO_URL,
181 | },
182 | json={'frontendUploadId': frontEndUID})
183 |
184 | uploadUrl = uploadRequest.headers.get("x-goog-upload-url")
185 | scottyResourceId = await self.uploadFileToYoutube(uploadUrl, file_name)
186 |
187 | _data = self.templates.UPLOAD_VIDEO
188 | _data["resourceId"]["scottyResourceId"]["id"] = scottyResourceId
189 | _data["frontendUploadId"] = frontEndUID
190 | _data["initialMetadata"] = {
191 | "title": {
192 | "newTitle": title
193 | },
194 | "description": {
195 | "newDescription": description,
196 | "shouldSegment": True
197 | },
198 | "privacy": {
199 | "newPrivacy": privacy
200 | },
201 | "draftState": {
202 | "isDraft": draft
203 | },
204 | }
205 | _data["initialMetadata"].update(extra_fields)
206 |
207 | upload = await self.session.post(
208 | f"https://studio.youtube.com/youtubei/v1/upload/createvideo?alt=json&key={self.config['INNERTUBE_API_KEY']}",
209 | json=_data
210 | )
211 |
212 | return await upload.json()
213 |
214 | async def deleteVideo(self, video_id):
215 | """
216 | Delete video from your channel
217 | """
218 | self.templates.setVideoId(video_id)
219 | delete = await self.session.post(
220 | f"https://studio.youtube.com/youtubei/v1/video/delete?alt=json&key={self.config['INNERTUBE_API_KEY']}",
221 | json=self.templates.DELETE_VIDEO
222 | )
223 | return await delete.json()
224 |
225 | async def listVideos(self):
226 | """
227 | Returns a list of videos in your channel
228 | """
229 | list = await self.session.post(
230 | f"https://studio.youtube.com/youtubei/v1/creator/list_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}",
231 | json=self.templates.LIST_VIDEOS
232 | )
233 | return await list.json()
234 |
235 | async def getVideo(self, video_id):
236 | """
237 | Get video data.
238 | """
239 | self.templates.setVideoId(video_id)
240 | video = await self.session.post(
241 | f"https://studio.youtube.com/youtubei/v1/creator/get_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}",
242 | json=self.templates.GET_VIDEO
243 | )
244 | return await video.json()
245 |
246 | async def createPlaylist(self, title, privacy="PUBLIC") -> dict:
247 | """
248 | Create a new playlist.
249 | """
250 | _data = self.templates.CREATE_PLAYLIST
251 | _data["title"] = title
252 | _data["privacyStatus"] = privacy
253 |
254 | create = await self.session.post(
255 | f"https://studio.youtube.com/youtubei/v1/playlist/create?alt=json&key={self.config['INNERTUBE_API_KEY']}",
256 | json=_data
257 | )
258 | return await create.json()
259 |
260 | async def editVideo(self, video_id, title: str = "", description: str = "", privacy: str = "", thumb: typing.Union[str, pathlib.Path, os.PathLike] = "", tags: typing.List[str] = [], category: int = -1, monetization: bool = True, playlist: typing.List[str] = [], removeFromPlaylist: typing.List[str] = []):
261 | """
262 | Edit video metadata.
263 | """
264 | self.templates.setVideoId(video_id)
265 | _data = self.templates.METADATA_UPDATE
266 | if title != "":
267 | _title = self.templates.METADATA_UPDATE_TITLE
268 | _title["title"]["newTitle"] = title
269 | _data.update(_title)
270 |
271 | if description != "":
272 | _description = self.templates.METADATA_UPDATE_DESCRIPTION
273 | _description["description"]["newDescription"] = description
274 | _data.update(_description)
275 |
276 | if privacy != "":
277 | _privacy = self.templates.METADATA_UPDATE_PRIVACY
278 | _privacy["privacy"]["newPrivacy"] = privacy
279 | _data.update(_privacy)
280 |
281 | if thumb != "":
282 | _thumb = self.templates.METADATA_UPDATE_THUMB
283 | image = open(thumb, 'rb')
284 | image_64_encode = base64.b64encode(image.read()).decode('utf-8')
285 |
286 | _thumb["videoStill"]["image"][
287 | "dataUri"] = f"data:image/png;base64,{image_64_encode}"
288 | _data.update(_thumb)
289 |
290 | if len(tags) > 0:
291 | _tags = self.templates.METADATA_UPDATE_TAGS
292 | _tags["tags"]["newTags"] = tags
293 | _data.update(_tags)
294 |
295 | if category != -1:
296 | _category = self.templates.METADATA_UPDATE_CATEGORY
297 | _category["category"]["newCategoryId"] = category
298 | _data.update(_category)
299 |
300 | if len(playlist) > 0:
301 | _playlist = self.templates.METADATA_UPDATE_PLAYLIST
302 | _playlist["addToPlaylist"]["addToPlaylistIds"] = playlist
303 | if len(removeFromPlaylist) > 0:
304 | _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist
305 | _data.update(_playlist)
306 |
307 | if len(removeFromPlaylist) > 0:
308 | _playlist = self.templates.METADATA_UPDATE_PLAYLIST
309 | _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist
310 | _data.update(_playlist)
311 |
312 | _monetization = self.templates.METADATA_UPDATE_MONETIZATION
313 | _monetization["monetizationSettings"]["newMonetization"] = monetization
314 | _data.update(_monetization)
315 |
316 | update = await self.session.post(
317 | f"https://studio.youtube.com/youtubei/v1/video_manager/metadata_update?alt=json&key={self.config['INNERTUBE_API_KEY']}",
318 | json=_data
319 | )
320 | return await update.json()
321 |
322 | async def scheduledUploadVideo(self, file_name, title="New Video", description='This video uploaded by github.com/yusufusta/ytstudio', now_privacy='PRIVATE', schedule_time: datetime.datetime | int = 0, scheduled_privacy="PUBLIC", progress=None, extra_fields={}):
323 | """
324 | Scheduled uploads a video to youtube.
325 | """
326 | upload = await self.uploadVideo(file_name, title, description, now_privacy, draft=True, progress=progress, extra_fields=extra_fields)
327 | if not "videoId" in upload:
328 | return upload
329 |
330 | self.templates.setVideoId(upload["videoId"])
331 |
332 | _data = self.templates.METADATA_UPDATE
333 | _schedule = self.templates.METADATA_UPDATE_SCHEDULE
334 |
335 | if isinstance(schedule_time, datetime.datetime):
336 | schedule_time = int(schedule_time.timestamp())
337 | elif schedule_time == 0:
338 | schedule_time = int(datetime.datetime.now().timestamp()) + 60
339 |
340 | _schedule["scheduledPublishing"]["set"]["timeSec"] = schedule_time
341 | _schedule["scheduledPublishing"]["set"]["privacy"] = scheduled_privacy
342 | _schedule["privacyState"]["newPrivacy"] = now_privacy
343 |
344 | _data.update(self.templates.METADATA_UPDATE_SCHEDULE)
345 |
346 | update = await self.session.post(
347 | f"https://studio.youtube.com/youtubei/v1/video_manager/metadata_update?alt=json&key={self.config['INNERTUBE_API_KEY']}",
348 | json=_data
349 | )
350 | return upload, await update.json()
351 | Sub-modules
355 |-
356 |
ytstudio.templates
357 | - 358 | 359 | 360 |
Classes
368 |-
369 |
370 | class Studio 371 | (cookies: dict = {'SESSION_TOKEN': '', 'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN_INFO': '', 'SID': '', '__Secure-3PSID': '.', 'HSID': '', 'SSID': '', 'APISID': '', 'SAPISID': '', '__Secure-3PAPISID': '', 'YSC': '', 'SIDCC': ''}) 372 |
373 | -
374 |
375 | 376 |684 |
377 | Expand source code 378 |
379 |
683 |class Studio: 380 | YT_STUDIO_URL = "https://studio.youtube.com" 381 | USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36" 382 | TRANSFERRED_BYTES = 0 383 | CHUNK_SIZE = 64*1024 384 | 385 | def __init__(self, cookies: dict = {'SESSION_TOKEN': '', 'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN_INFO': '', 'SID': '', '__Secure-3PSID': '.', 'HSID': '', 386 | 'SSID': '', 'APISID': '', 'SAPISID': '', '__Secure-3PAPISID': '', 'YSC': '', 'SIDCC': ''}): 387 | self.SAPISIDHASH = self.generateSAPISIDHASH(cookies['SAPISID']) 388 | self.cookies = cookies 389 | self.Cookie = " ".join( 390 | [f"{c}={cookies[c]};" if not c in ["SESSION_TOKEN", "BOTGUARD_RESPONSE"] else "" for c in cookies.keys()]) 391 | self.HEADERS = { 392 | 'Authorization': f'SAPISIDHASH {self.SAPISIDHASH}', 393 | 'Content-Type': 'application/json', 394 | 'Cookie': self.Cookie, 395 | 'X-Origin': self.YT_STUDIO_URL, 396 | 'User-Agent': self.USER_AGENT 397 | } 398 | self.session = aiohttp.ClientSession(headers=self.HEADERS) 399 | self.loop = asyncio.get_event_loop() 400 | self.config = {} 401 | self.js = js2py.EvalJs() 402 | self.js.execute("var window = {ytcfg: {}};") 403 | 404 | def __del__(self): 405 | asyncio.run(self.session.close()) 406 | 407 | def generateSAPISIDHASH(self, SAPISID) -> str: 408 | hash = f"{round(time.time())} {SAPISID} {self.YT_STUDIO_URL}" 409 | sifrelenmis = sha1(hash.encode('utf-8')).hexdigest() 410 | return f"{round(time.time())}_{sifrelenmis}" 411 | 412 | async def getMainPage(self) -> str: 413 | page = await self.session.get(self.YT_STUDIO_URL) 414 | return await page.text("utf-8") 415 | 416 | async def login(self) -> bool: 417 | """ 418 | Login to your youtube account 419 | """ 420 | page = await self.getMainPage() 421 | _ = pq(page) 422 | script = _("script") 423 | if len(script) < 1: 424 | raise Exception("Didn't find script. Can you check your cookies?") 425 | script = script[0].text 426 | self.js.execute( 427 | f"{script} window.ytcfg = ytcfg;") 428 | 429 | INNERTUBE_API_KEY = self.js.window.ytcfg.data_.INNERTUBE_API_KEY 430 | CHANNEL_ID = self.js.window.ytcfg.data_.CHANNEL_ID 431 | DELEGATED_SESSION_ID = self.js.window.ytcfg.data_.DELEGATED_SESSION_ID 432 | 433 | if INNERTUBE_API_KEY == None or CHANNEL_ID == None: 434 | raise Exception( 435 | "Didn't find INNERTUBE_API_KEY or CHANNEL_ID. Can you check your cookies?") 436 | self.config = {'INNERTUBE_API_KEY': INNERTUBE_API_KEY, 437 | 'CHANNEL_ID': CHANNEL_ID, 'data_': self.js.window.ytcfg.data_} 438 | self.templates = Templates({ 439 | 'channelId': CHANNEL_ID, 440 | 'sessionToken': self.cookies['SESSION_TOKEN'], 441 | 'botguardResponse': self.cookies['BOTGUARD_RESPONSE'] if 'BOTGUARD_RESPONSE' in self.cookies else '', 442 | 'delegatedSessionId': DELEGATED_SESSION_ID 443 | }) 444 | 445 | return True 446 | 447 | def generateHash(self) -> str: 448 | harfler = list( 449 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') 450 | keys = ['' for i in range(0, 36)] 451 | b = 0 452 | c = "" 453 | e = 0 454 | 455 | while e < 36: 456 | if 8 == e or 13 == e or 18 == e or 23 == e: 457 | keys[e] = "-" 458 | else: 459 | if 14 == e: 460 | keys[e] = "4" 461 | elif 2 >= b: 462 | b = round(33554432 + 16777216 * random.uniform(0, 0.9)) 463 | c = b & 15 464 | b = b >> 4 465 | keys[e] = harfler[c & 3 | 8 if 19 == e else c] 466 | e += 1 467 | 468 | return "".join(keys) 469 | 470 | async def fileSender(self, file_name): 471 | async with aiofiles.open(file_name, 'rb') as f: 472 | chunk = await f.read(self.CHUNK_SIZE) 473 | while chunk: 474 | if self.progress != None: 475 | self.TRANSFERRED_BYTES += len(chunk) 476 | self.progress(self.TRANSFERRED_BYTES, 477 | os.path.getsize(file_name)) 478 | 479 | self.TRANSFERRED_BYTES += len(chunk) 480 | yield chunk 481 | chunk = await f.read(self.CHUNK_SIZE) 482 | if not chunk: 483 | break 484 | 485 | async def uploadFileToYoutube(self, upload_url, file_path): 486 | self.TRANSFERRED_BYTES = 0 487 | 488 | uploaded = await self.session.post(upload_url, headers={ 489 | "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'", 490 | "x-goog-upload-command": "upload, finalize", 491 | "x-goog-upload-file-name": f"file-{round(time.time())}", 492 | "x-goog-upload-offset": "0", 493 | "Referer": self.YT_STUDIO_URL, 494 | }, data=self.fileSender(file_path), timeout=None) 495 | _ = await uploaded.text("utf-8") 496 | _ = json.loads(_) 497 | return _['scottyResourceId'] 498 | 499 | async def uploadVideo(self, file_name, title=f"New Video {round(time.time())}", description='This video uploaded by github.com/yusufusta/ytstudio', privacy='PRIVATE', draft=False, progress=None, extra_fields={}): 500 | """ 501 | Uploads a video to youtube. 502 | """ 503 | self.progress = progress 504 | frontEndUID = f"innertube_studio:{self.generateHash()}:0" 505 | 506 | uploadRequest = await self.session.post("https://upload.youtube.com/upload/studio", 507 | headers={ 508 | "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'", 509 | "x-goog-upload-command": "start", 510 | "x-goog-upload-file-name": f"file-{round(time.time())}", 511 | "x-goog-upload-protocol": "resumable", 512 | "Referer": self.YT_STUDIO_URL, 513 | }, 514 | json={'frontendUploadId': frontEndUID}) 515 | 516 | uploadUrl = uploadRequest.headers.get("x-goog-upload-url") 517 | scottyResourceId = await self.uploadFileToYoutube(uploadUrl, file_name) 518 | 519 | _data = self.templates.UPLOAD_VIDEO 520 | _data["resourceId"]["scottyResourceId"]["id"] = scottyResourceId 521 | _data["frontendUploadId"] = frontEndUID 522 | _data["initialMetadata"] = { 523 | "title": { 524 | "newTitle": title 525 | }, 526 | "description": { 527 | "newDescription": description, 528 | "shouldSegment": True 529 | }, 530 | "privacy": { 531 | "newPrivacy": privacy 532 | }, 533 | "draftState": { 534 | "isDraft": draft 535 | }, 536 | } 537 | _data["initialMetadata"].update(extra_fields) 538 | 539 | upload = await self.session.post( 540 | f"https://studio.youtube.com/youtubei/v1/upload/createvideo?alt=json&key={self.config['INNERTUBE_API_KEY']}", 541 | json=_data 542 | ) 543 | 544 | return await upload.json() 545 | 546 | async def deleteVideo(self, video_id): 547 | """ 548 | Delete video from your channel 549 | """ 550 | self.templates.setVideoId(video_id) 551 | delete = await self.session.post( 552 | f"https://studio.youtube.com/youtubei/v1/video/delete?alt=json&key={self.config['INNERTUBE_API_KEY']}", 553 | json=self.templates.DELETE_VIDEO 554 | ) 555 | return await delete.json() 556 | 557 | async def listVideos(self): 558 | """ 559 | Returns a list of videos in your channel 560 | """ 561 | list = await self.session.post( 562 | f"https://studio.youtube.com/youtubei/v1/creator/list_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}", 563 | json=self.templates.LIST_VIDEOS 564 | ) 565 | return await list.json() 566 | 567 | async def getVideo(self, video_id): 568 | """ 569 | Get video data. 570 | """ 571 | self.templates.setVideoId(video_id) 572 | video = await self.session.post( 573 | f"https://studio.youtube.com/youtubei/v1/creator/get_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}", 574 | json=self.templates.GET_VIDEO 575 | ) 576 | return await video.json() 577 | 578 | async def createPlaylist(self, title, privacy="PUBLIC") -> dict: 579 | """ 580 | Create a new playlist. 581 | """ 582 | _data = self.templates.CREATE_PLAYLIST 583 | _data["title"] = title 584 | _data["privacyStatus"] = privacy 585 | 586 | create = await self.session.post( 587 | f"https://studio.youtube.com/youtubei/v1/playlist/create?alt=json&key={self.config['INNERTUBE_API_KEY']}", 588 | json=_data 589 | ) 590 | return await create.json() 591 | 592 | async def editVideo(self, video_id, title: str = "", description: str = "", privacy: str = "", thumb: typing.Union[str, pathlib.Path, os.PathLike] = "", tags: typing.List[str] = [], category: int = -1, monetization: bool = True, playlist: typing.List[str] = [], removeFromPlaylist: typing.List[str] = []): 593 | """ 594 | Edit video metadata. 595 | """ 596 | self.templates.setVideoId(video_id) 597 | _data = self.templates.METADATA_UPDATE 598 | if title != "": 599 | _title = self.templates.METADATA_UPDATE_TITLE 600 | _title["title"]["newTitle"] = title 601 | _data.update(_title) 602 | 603 | if description != "": 604 | _description = self.templates.METADATA_UPDATE_DESCRIPTION 605 | _description["description"]["newDescription"] = description 606 | _data.update(_description) 607 | 608 | if privacy != "": 609 | _privacy = self.templates.METADATA_UPDATE_PRIVACY 610 | _privacy["privacy"]["newPrivacy"] = privacy 611 | _data.update(_privacy) 612 | 613 | if thumb != "": 614 | _thumb = self.templates.METADATA_UPDATE_THUMB 615 | image = open(thumb, 'rb') 616 | image_64_encode = base64.b64encode(image.read()).decode('utf-8') 617 | 618 | _thumb["videoStill"]["image"][ 619 | "dataUri"] = f"data:image/png;base64,{image_64_encode}" 620 | _data.update(_thumb) 621 | 622 | if len(tags) > 0: 623 | _tags = self.templates.METADATA_UPDATE_TAGS 624 | _tags["tags"]["newTags"] = tags 625 | _data.update(_tags) 626 | 627 | if category != -1: 628 | _category = self.templates.METADATA_UPDATE_CATEGORY 629 | _category["category"]["newCategoryId"] = category 630 | _data.update(_category) 631 | 632 | if len(playlist) > 0: 633 | _playlist = self.templates.METADATA_UPDATE_PLAYLIST 634 | _playlist["addToPlaylist"]["addToPlaylistIds"] = playlist 635 | if len(removeFromPlaylist) > 0: 636 | _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist 637 | _data.update(_playlist) 638 | 639 | if len(removeFromPlaylist) > 0: 640 | _playlist = self.templates.METADATA_UPDATE_PLAYLIST 641 | _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist 642 | _data.update(_playlist) 643 | 644 | _monetization = self.templates.METADATA_UPDATE_MONETIZATION 645 | _monetization["monetizationSettings"]["newMonetization"] = monetization 646 | _data.update(_monetization) 647 | 648 | update = await self.session.post( 649 | f"https://studio.youtube.com/youtubei/v1/video_manager/metadata_update?alt=json&key={self.config['INNERTUBE_API_KEY']}", 650 | json=_data 651 | ) 652 | return await update.json() 653 | 654 | async def scheduledUploadVideo(self, file_name, title="New Video", description='This video uploaded by github.com/yusufusta/ytstudio', now_privacy='PRIVATE', schedule_time: datetime.datetime | int = 0, scheduled_privacy="PUBLIC", progress=None, extra_fields={}): 655 | """ 656 | Scheduled uploads a video to youtube. 657 | """ 658 | upload = await self.uploadVideo(file_name, title, description, now_privacy, draft=True, progress=progress, extra_fields=extra_fields) 659 | if not "videoId" in upload: 660 | return upload 661 | 662 | self.templates.setVideoId(upload["videoId"]) 663 | 664 | _data = self.templates.METADATA_UPDATE 665 | _schedule = self.templates.METADATA_UPDATE_SCHEDULE 666 | 667 | if isinstance(schedule_time, datetime.datetime): 668 | schedule_time = int(schedule_time.timestamp()) 669 | elif schedule_time == 0: 670 | schedule_time = int(datetime.datetime.now().timestamp()) + 60 671 | 672 | _schedule["scheduledPublishing"]["set"]["timeSec"] = schedule_time 673 | _schedule["scheduledPublishing"]["set"]["privacy"] = scheduled_privacy 674 | _schedule["privacyState"]["newPrivacy"] = now_privacy 675 | 676 | _data.update(self.templates.METADATA_UPDATE_SCHEDULE) 677 | 678 | update = await self.session.post( 679 | f"https://studio.youtube.com/youtubei/v1/video_manager/metadata_update?alt=json&key={self.config['INNERTUBE_API_KEY']}", 680 | json=_data 681 | ) 682 | return upload, await update.json()Class variables
685 |-
686 |
var CHUNK_SIZE
687 | - 688 | 689 | 690 |
var TRANSFERRED_BYTES
691 | - 692 | 693 | 694 |
var USER_AGENT
695 | - 696 | 697 | 698 |
var YT_STUDIO_URL
699 | - 700 | 701 | 702 |
Methods
704 |-
705 |
706 | async def createPlaylist(self, title, privacy='PUBLIC') ‑> dict 707 |
708 | -
709 | 710 |
Create a new playlist.
711 |728 |712 | Expand source code 713 |
714 |
727 |async def createPlaylist(self, title, privacy="PUBLIC") -> dict: 715 | """ 716 | Create a new playlist. 717 | """ 718 | _data = self.templates.CREATE_PLAYLIST 719 | _data["title"] = title 720 | _data["privacyStatus"] = privacy 721 | 722 | create = await self.session.post( 723 | f"https://studio.youtube.com/youtubei/v1/playlist/create?alt=json&key={self.config['INNERTUBE_API_KEY']}", 724 | json=_data 725 | ) 726 | return await create.json()
729 | 730 | async def deleteVideo(self, video_id) 731 |
732 | -
733 | 734 |
Delete video from your channel
735 |749 |736 | Expand source code 737 |
738 |
748 |async def deleteVideo(self, video_id): 739 | """ 740 | Delete video from your channel 741 | """ 742 | self.templates.setVideoId(video_id) 743 | delete = await self.session.post( 744 | f"https://studio.youtube.com/youtubei/v1/video/delete?alt=json&key={self.config['INNERTUBE_API_KEY']}", 745 | json=self.templates.DELETE_VIDEO 746 | ) 747 | return await delete.json()
750 | 751 | async def editVideo(self, video_id, title: str = '', description: str = '', privacy: str = '', thumb: Union[str, pathlib.Path, os.PathLike] = '', tags: List[str] = [], category: int = -1, monetization: bool = True, playlist: List[str] = [], removeFromPlaylist: List[str] = []) 752 |
753 | -
754 | 755 |
Edit video metadata.
756 |821 |757 | Expand source code 758 |
759 |
820 |async def editVideo(self, video_id, title: str = "", description: str = "", privacy: str = "", thumb: typing.Union[str, pathlib.Path, os.PathLike] = "", tags: typing.List[str] = [], category: int = -1, monetization: bool = True, playlist: typing.List[str] = [], removeFromPlaylist: typing.List[str] = []): 760 | """ 761 | Edit video metadata. 762 | """ 763 | self.templates.setVideoId(video_id) 764 | _data = self.templates.METADATA_UPDATE 765 | if title != "": 766 | _title = self.templates.METADATA_UPDATE_TITLE 767 | _title["title"]["newTitle"] = title 768 | _data.update(_title) 769 | 770 | if description != "": 771 | _description = self.templates.METADATA_UPDATE_DESCRIPTION 772 | _description["description"]["newDescription"] = description 773 | _data.update(_description) 774 | 775 | if privacy != "": 776 | _privacy = self.templates.METADATA_UPDATE_PRIVACY 777 | _privacy["privacy"]["newPrivacy"] = privacy 778 | _data.update(_privacy) 779 | 780 | if thumb != "": 781 | _thumb = self.templates.METADATA_UPDATE_THUMB 782 | image = open(thumb, 'rb') 783 | image_64_encode = base64.b64encode(image.read()).decode('utf-8') 784 | 785 | _thumb["videoStill"]["image"][ 786 | "dataUri"] = f"data:image/png;base64,{image_64_encode}" 787 | _data.update(_thumb) 788 | 789 | if len(tags) > 0: 790 | _tags = self.templates.METADATA_UPDATE_TAGS 791 | _tags["tags"]["newTags"] = tags 792 | _data.update(_tags) 793 | 794 | if category != -1: 795 | _category = self.templates.METADATA_UPDATE_CATEGORY 796 | _category["category"]["newCategoryId"] = category 797 | _data.update(_category) 798 | 799 | if len(playlist) > 0: 800 | _playlist = self.templates.METADATA_UPDATE_PLAYLIST 801 | _playlist["addToPlaylist"]["addToPlaylistIds"] = playlist 802 | if len(removeFromPlaylist) > 0: 803 | _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist 804 | _data.update(_playlist) 805 | 806 | if len(removeFromPlaylist) > 0: 807 | _playlist = self.templates.METADATA_UPDATE_PLAYLIST 808 | _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist 809 | _data.update(_playlist) 810 | 811 | _monetization = self.templates.METADATA_UPDATE_MONETIZATION 812 | _monetization["monetizationSettings"]["newMonetization"] = monetization 813 | _data.update(_monetization) 814 | 815 | update = await self.session.post( 816 | f"https://studio.youtube.com/youtubei/v1/video_manager/metadata_update?alt=json&key={self.config['INNERTUBE_API_KEY']}", 817 | json=_data 818 | ) 819 | return await update.json()
822 | 823 | async def fileSender(self, file_name) 824 |
825 | -
826 |
827 | 828 |846 |
829 | Expand source code 830 |
831 |
845 |async def fileSender(self, file_name): 832 | async with aiofiles.open(file_name, 'rb') as f: 833 | chunk = await f.read(self.CHUNK_SIZE) 834 | while chunk: 835 | if self.progress != None: 836 | self.TRANSFERRED_BYTES += len(chunk) 837 | self.progress(self.TRANSFERRED_BYTES, 838 | os.path.getsize(file_name)) 839 | 840 | self.TRANSFERRED_BYTES += len(chunk) 841 | yield chunk 842 | chunk = await f.read(self.CHUNK_SIZE) 843 | if not chunk: 844 | break
847 | 848 | def generateHash(self) ‑> str 849 |
850 | -
851 |
852 | 853 |879 |
854 | Expand source code 855 |
856 |
878 |def generateHash(self) -> str: 857 | harfler = list( 858 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') 859 | keys = ['' for i in range(0, 36)] 860 | b = 0 861 | c = "" 862 | e = 0 863 | 864 | while e < 36: 865 | if 8 == e or 13 == e or 18 == e or 23 == e: 866 | keys[e] = "-" 867 | else: 868 | if 14 == e: 869 | keys[e] = "4" 870 | elif 2 >= b: 871 | b = round(33554432 + 16777216 * random.uniform(0, 0.9)) 872 | c = b & 15 873 | b = b >> 4 874 | keys[e] = harfler[c & 3 | 8 if 19 == e else c] 875 | e += 1 876 | 877 | return "".join(keys)
880 | 881 | def generateSAPISIDHASH(self, SAPISID) ‑> str 882 |
883 | -
884 |
885 | 886 |894 |
887 | Expand source code 888 |
889 |
893 |def generateSAPISIDHASH(self, SAPISID) -> str: 890 | hash = f"{round(time.time())} {SAPISID} {self.YT_STUDIO_URL}" 891 | sifrelenmis = sha1(hash.encode('utf-8')).hexdigest() 892 | return f"{round(time.time())}_{sifrelenmis}"
895 | 896 | async def getMainPage(self) ‑> str 897 |
898 | -
899 |
900 | 901 |908 |
902 | Expand source code 903 |
904 |
907 |async def getMainPage(self) -> str: 905 | page = await self.session.get(self.YT_STUDIO_URL) 906 | return await page.text("utf-8")
909 | 910 | async def getVideo(self, video_id) 911 |
912 | -
913 | 914 |
Get video data.
915 |929 |916 | Expand source code 917 |
918 |
928 |async def getVideo(self, video_id): 919 | """ 920 | Get video data. 921 | """ 922 | self.templates.setVideoId(video_id) 923 | video = await self.session.post( 924 | f"https://studio.youtube.com/youtubei/v1/creator/get_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}", 925 | json=self.templates.GET_VIDEO 926 | ) 927 | return await video.json()
930 | 931 | async def listVideos(self) 932 |
933 | -
934 | 935 |
Returns a list of videos in your channel
936 |949 |937 | Expand source code 938 |
939 |
948 |async def listVideos(self): 940 | """ 941 | Returns a list of videos in your channel 942 | """ 943 | list = await self.session.post( 944 | f"https://studio.youtube.com/youtubei/v1/creator/list_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}", 945 | json=self.templates.LIST_VIDEOS 946 | ) 947 | return await list.json()
950 | 951 | async def login(self) ‑> bool 952 |
953 | -
954 | 955 |
Login to your youtube account
956 |990 |957 | Expand source code 958 |
959 |
989 |async def login(self) -> bool: 960 | """ 961 | Login to your youtube account 962 | """ 963 | page = await self.getMainPage() 964 | _ = pq(page) 965 | script = _("script") 966 | if len(script) < 1: 967 | raise Exception("Didn't find script. Can you check your cookies?") 968 | script = script[0].text 969 | self.js.execute( 970 | f"{script} window.ytcfg = ytcfg;") 971 | 972 | INNERTUBE_API_KEY = self.js.window.ytcfg.data_.INNERTUBE_API_KEY 973 | CHANNEL_ID = self.js.window.ytcfg.data_.CHANNEL_ID 974 | DELEGATED_SESSION_ID = self.js.window.ytcfg.data_.DELEGATED_SESSION_ID 975 | 976 | if INNERTUBE_API_KEY == None or CHANNEL_ID == None: 977 | raise Exception( 978 | "Didn't find INNERTUBE_API_KEY or CHANNEL_ID. Can you check your cookies?") 979 | self.config = {'INNERTUBE_API_KEY': INNERTUBE_API_KEY, 980 | 'CHANNEL_ID': CHANNEL_ID, 'data_': self.js.window.ytcfg.data_} 981 | self.templates = Templates({ 982 | 'channelId': CHANNEL_ID, 983 | 'sessionToken': self.cookies['SESSION_TOKEN'], 984 | 'botguardResponse': self.cookies['BOTGUARD_RESPONSE'] if 'BOTGUARD_RESPONSE' in self.cookies else '', 985 | 'delegatedSessionId': DELEGATED_SESSION_ID 986 | }) 987 | 988 | return True
991 | 992 | async def scheduledUploadVideo(self, file_name, title='New Video', description='This video uploaded by github.com/yusufusta/ytstudio', now_privacy='PRIVATE', schedule_time: datetime.datetime | int = 0, scheduled_privacy='PUBLIC', progress=None, extra_fields={}) 993 |
994 | -
995 | 996 |
Scheduled uploads a video to youtube.
997 |1030 |998 | Expand source code 999 |
1000 |
1029 |async def scheduledUploadVideo(self, file_name, title="New Video", description='This video uploaded by github.com/yusufusta/ytstudio', now_privacy='PRIVATE', schedule_time: datetime.datetime | int = 0, scheduled_privacy="PUBLIC", progress=None, extra_fields={}): 1001 | """ 1002 | Scheduled uploads a video to youtube. 1003 | """ 1004 | upload = await self.uploadVideo(file_name, title, description, now_privacy, draft=True, progress=progress, extra_fields=extra_fields) 1005 | if not "videoId" in upload: 1006 | return upload 1007 | 1008 | self.templates.setVideoId(upload["videoId"]) 1009 | 1010 | _data = self.templates.METADATA_UPDATE 1011 | _schedule = self.templates.METADATA_UPDATE_SCHEDULE 1012 | 1013 | if isinstance(schedule_time, datetime.datetime): 1014 | schedule_time = int(schedule_time.timestamp()) 1015 | elif schedule_time == 0: 1016 | schedule_time = int(datetime.datetime.now().timestamp()) + 60 1017 | 1018 | _schedule["scheduledPublishing"]["set"]["timeSec"] = schedule_time 1019 | _schedule["scheduledPublishing"]["set"]["privacy"] = scheduled_privacy 1020 | _schedule["privacyState"]["newPrivacy"] = now_privacy 1021 | 1022 | _data.update(self.templates.METADATA_UPDATE_SCHEDULE) 1023 | 1024 | update = await self.session.post( 1025 | f"https://studio.youtube.com/youtubei/v1/video_manager/metadata_update?alt=json&key={self.config['INNERTUBE_API_KEY']}", 1026 | json=_data 1027 | ) 1028 | return upload, await update.json()
1031 | 1032 | async def uploadFileToYoutube(self, upload_url, file_path) 1033 |
1034 | -
1035 |
1036 | 1037 |1054 |
1038 | Expand source code 1039 |
1040 |
1053 |async def uploadFileToYoutube(self, upload_url, file_path): 1041 | self.TRANSFERRED_BYTES = 0 1042 | 1043 | uploaded = await self.session.post(upload_url, headers={ 1044 | "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'", 1045 | "x-goog-upload-command": "upload, finalize", 1046 | "x-goog-upload-file-name": f"file-{round(time.time())}", 1047 | "x-goog-upload-offset": "0", 1048 | "Referer": self.YT_STUDIO_URL, 1049 | }, data=self.fileSender(file_path), timeout=None) 1050 | _ = await uploaded.text("utf-8") 1051 | _ = json.loads(_) 1052 | return _['scottyResourceId']
1055 | 1056 | async def uploadVideo(self, file_name, title='New Video 1675980145', description='This video uploaded by github.com/yusufusta/ytstudio', privacy='PRIVATE', draft=False, progress=None, extra_fields={}) 1057 |
1058 | -
1059 | 1060 |
Uploads a video to youtube.
1061 |1111 |1062 | Expand source code 1063 |
1064 |
1110 |async def uploadVideo(self, file_name, title=f"New Video {round(time.time())}", description='This video uploaded by github.com/yusufusta/ytstudio', privacy='PRIVATE', draft=False, progress=None, extra_fields={}): 1065 | """ 1066 | Uploads a video to youtube. 1067 | """ 1068 | self.progress = progress 1069 | frontEndUID = f"innertube_studio:{self.generateHash()}:0" 1070 | 1071 | uploadRequest = await self.session.post("https://upload.youtube.com/upload/studio", 1072 | headers={ 1073 | "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'", 1074 | "x-goog-upload-command": "start", 1075 | "x-goog-upload-file-name": f"file-{round(time.time())}", 1076 | "x-goog-upload-protocol": "resumable", 1077 | "Referer": self.YT_STUDIO_URL, 1078 | }, 1079 | json={'frontendUploadId': frontEndUID}) 1080 | 1081 | uploadUrl = uploadRequest.headers.get("x-goog-upload-url") 1082 | scottyResourceId = await self.uploadFileToYoutube(uploadUrl, file_name) 1083 | 1084 | _data = self.templates.UPLOAD_VIDEO 1085 | _data["resourceId"]["scottyResourceId"]["id"] = scottyResourceId 1086 | _data["frontendUploadId"] = frontEndUID 1087 | _data["initialMetadata"] = { 1088 | "title": { 1089 | "newTitle": title 1090 | }, 1091 | "description": { 1092 | "newDescription": description, 1093 | "shouldSegment": True 1094 | }, 1095 | "privacy": { 1096 | "newPrivacy": privacy 1097 | }, 1098 | "draftState": { 1099 | "isDraft": draft 1100 | }, 1101 | } 1102 | _data["initialMetadata"].update(extra_fields) 1103 | 1104 | upload = await self.session.post( 1105 | f"https://studio.youtube.com/youtubei/v1/upload/createvideo?alt=json&key={self.config['INNERTUBE_API_KEY']}", 1106 | json=_data 1107 | ) 1108 | 1109 | return await upload.json()
1112 |
1114 |