diff --git a/README.md b/README.md index 5b2f8dd..10627f9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Youtube Studio -Unofficial Async YouTube Studio API. Set of features limited or not provided by official YouTube API +Unofficial Async YouTube Studio API. Set of features limited or not provided by official YouTube API! > This is the Python version of [this project](https://github.com/adasq/youtube-studio). All thanks going to [@adasq](https://github.com/adasq) :) @@ -15,33 +15,19 @@ You can install with PIP. - Async - Uploading Video (**NOT LIMITED** - official API's videos.insert charges you 1600 quota units) - Deleting Video +- Edit Video ## Examples **Note:** You need cookies. Use an cookie manager([EditThisCookie](https://chrome.google.com/webstore/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg?hl=tr)) for needed cookies. -### Upload Video +> You need SESSION_TOKEN for (upload/edit/delete) video. [How to get Session Token?](https://github.com/adasq/youtube-studio#preparing-authentication) -> You need SESSION_TOKEN for upload video. [How to get Session Token?](https://github.com/adasq/youtube-studio#preparing-authentication) +## TO-DO -```py -from ytstudio import Studio -import asyncio -import os - -def progress(uploaded, total): - print(f"{uploaded}/{total}", end="\r") - pass - -async def main(): - yt = Studio({'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN_INFO': '', 'SID': '', '__Secure-3PSID': '', 'HSID': '', 'SSID': '', 'APISID': '', 'SAPISID': '', '__Secure-3PAPISID': '', 'YSC': '', 'SIDCC': ''}) - await yt.login() - sonuc = await yt.uploadVideo(os.path.join(os.getcwd(), "deneme.mp4"), title="Hello World!", description="Uploaded by github.com/yusufusta/ytstudio", progress=progress) - print(sonuc['videoId']) # Print Video ID - -loop = asyncio.get_event_loop() -loop.run_until_complete(main()) -``` +- [ ] Better Documentation +- [ ] Better Tests +- [ ] More Functions ## Author diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..6b5b7c6 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,1047 @@ + + + + + + +ytstudio API documentation + + + + + + + + + + + +
+
+
+

Package ytstudio

+
+
+
+ +Expand source code + +
from hashlib import sha1
+import time
+import aiohttp
+import asyncio
+import aiofiles
+from pyquery import PyQuery as pq
+import js2py
+import js2py.pyjs
+import random
+import os
+import json
+from .templates import Templates
+import typing
+import pathlib
+import base64
+
+
+class Studio:
+    YT_STUDIO_URL = "https://studio.youtube.com"
+    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"
+    TRANSFERRED_BYTES = 0
+    CHUNK_SIZE = 64*1024
+
+    def __init__(self, cookies: dict = {'SESSION_TOKEN': '', 'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN_INFO': '', 'SID': '', '__Secure-3PSID': '.', 'HSID': '',
+                 'SSID': '', 'APISID': '', 'SAPISID': '', '__Secure-3PAPISID': '', 'YSC': '', 'SIDCC': ''}):
+        self.SAPISIDHASH = self.generateSAPISIDHASH(cookies['SAPISID'])
+        self.cookies = cookies
+        self.Cookie = " ".join(
+            [f"{c}={cookies[c]};" if not c == "SESSION_TOKEN" else "" for c in cookies.keys()])
+        self.HEADERS = {
+            'Authorization': f'SAPISIDHASH {self.SAPISIDHASH}',
+            'Content-Type': 'application/json',
+            'Cookie': self.Cookie,
+            'X-Origin': self.YT_STUDIO_URL,
+            'User-Agent': self.USER_AGENT
+        }
+        self.session = aiohttp.ClientSession(headers=self.HEADERS)
+        self.loop = asyncio.get_event_loop()
+        self.config = {}
+        self.js = js2py.EvalJs()
+        self.js.execute("var window = {ytcfg: {}};")
+
+    def __del__(self):
+        self.loop.create_task(self.session.close())
+
+    def generateSAPISIDHASH(self, SAPISID) -> str:
+        hash = f"{round(time.time())} {SAPISID} {self.YT_STUDIO_URL}"
+        sifrelenmis = sha1(hash.encode('utf-8')).hexdigest()
+        return f"{round(time.time())}_{sifrelenmis}"
+
+    async def getMainPage(self) -> str:
+        page = await self.session.get(self.YT_STUDIO_URL)
+        return await page.text("utf-8")
+
+    async def login(self) -> bool:
+        """
+        Login to your youtube account
+        """
+        page = await self.getMainPage()
+        _ = pq(page)
+        script = _("script")
+        if len(script) < 1:
+            raise Exception("Didn't find script. Can you check your cookies?")
+        script = script[0].text
+        self.js.execute(
+            f"{script} window.ytcfg = ytcfg;")
+
+        INNERTUBE_API_KEY = self.js.window.ytcfg.data_.INNERTUBE_API_KEY
+        CHANNEL_ID = self.js.window.ytcfg.data_.CHANNEL_ID
+        # DELEGATED_SESSION_ID = js.window.ytcfg.data_.DELEGATED_SESSION_ID Looking Google removed this key
+
+        if INNERTUBE_API_KEY == None or CHANNEL_ID == None:
+            raise Exception(
+                "Didn't find INNERTUBE_API_KEY or CHANNEL_ID. Can you check your cookies?")
+        self.config = {'INNERTUBE_API_KEY': INNERTUBE_API_KEY,
+                       'CHANNEL_ID': CHANNEL_ID, 'data_': self.js.window.ytcfg.data_}
+        self.templates = Templates({
+            'channelId': CHANNEL_ID,
+            'sessionToken': self.cookies['SESSION_TOKEN']
+        })
+
+        return True
+
+    def generateHash(self) -> str:
+        harfler = list(
+            '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
+        keys = ['' for i in range(0, 36)]
+        b = 0
+        c = ""
+        e = 0
+
+        while e < 36:
+            if 8 == e or 13 == e or 18 == e or 23 == e:
+                keys[e] = "-"
+            else:
+                if 14 == e:
+                    keys[e] = "4"
+                elif 2 >= b:
+                    b = round(33554432 + 16777216 * random.uniform(0, 0.9))
+                c = b & 15
+                b = b >> 4
+                keys[e] = harfler[c & 3 | 8 if 19 == e else c]
+            e += 1
+
+        return "".join(keys)
+
+    async def fileSender(self, file_name):
+        async with aiofiles.open(file_name, 'rb') as f:
+            chunk = await f.read(self.CHUNK_SIZE)
+            while chunk:
+                if self.progress != None:
+                    self.TRANSFERRED_BYTES += len(chunk)
+                    self.progress(self.TRANSFERRED_BYTES,
+                                  os.path.getsize(file_name))
+
+                self.TRANSFERRED_BYTES += len(chunk)
+                yield chunk
+                chunk = await f.read(self.CHUNK_SIZE)
+                if not chunk:
+                    break
+
+    async def uploadFileToYoutube(self, upload_url, file_path):
+        self.TRANSFERRED_BYTES = 0
+
+        uploaded = await self.session.post(upload_url,  headers={
+            "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'",
+            "x-goog-upload-command": "upload, finalize",
+            "x-goog-upload-file-name": f"file-{round(time.time())}",
+            "x-goog-upload-offset": "0",
+            "Referer": self.YT_STUDIO_URL,
+        }, data=self.fileSender(file_path), timeout=None)
+        _ = await uploaded.text("utf-8")
+        _ = json.loads(_)
+        return _['scottyResourceId']
+
+    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):
+        """
+        Uploads a video to youtube.
+        """
+        self.progress = progress
+        frontEndUID = f"innertube_studio:{self.generateHash()}:0"
+
+        uploadRequest = await self.session.post("https://upload.youtube.com/upload/studio",
+                                                headers={
+                                                    "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'",
+                                                    "x-goog-upload-command": "start",
+                                                    "x-goog-upload-file-name": f"file-{round(time.time())}",
+                                                    "x-goog-upload-protocol": "resumable",
+                                                    "Referer": self.YT_STUDIO_URL,
+                                                },
+                                                json={'frontendUploadId': frontEndUID})
+
+        uploadUrl = uploadRequest.headers.get("x-goog-upload-url")
+        scottyResourceId = await self.uploadFileToYoutube(uploadUrl, file_name)
+
+        _data = self.templates.UPLOAD_VIDEO
+        _data["resourceId"]["scottyResourceId"]["id"] = scottyResourceId
+        _data["frontendUploadId"] = frontEndUID
+        _data["initialMetadata"] = {
+            "title": {
+                "newTitle": title
+            },
+            "description": {
+                "newDescription": description,
+                "shouldSegment": True
+            },
+            "privacy": {
+                "newPrivacy": privacy
+            },
+            "draftState": {
+                "isDraft": draft
+            }
+        }
+        upload = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/upload/createvideo?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=_data
+        )
+
+        return await upload.json()
+
+    async def deleteVideo(self, video_id):
+        """
+        Delete video from your channel
+        """
+        self.templates.setVideoId(video_id)
+        delete = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/video/delete?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=self.templates.DELETE_VIDEO
+        )
+        return await delete.json()
+
+    async def listVideos(self):
+        """
+        Returns a list of videos in your channel
+        """
+        list = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/creator/list_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=self.templates.LIST_VIDEOS
+        )
+        return await list.json()
+
+    async def getVideo(self, video_id):
+        """
+        Get video data.
+        """
+        self.templates.setVideoId(video_id)
+        video = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/creator/get_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=self.templates.GET_VIDEO
+        )
+        return await video.json()
+
+    async def createPlaylist(self, title, privacy="PUBLIC") -> dict:
+        """
+        Create a new playlist.
+        """
+        _data = self.templates.CREATE_PLAYLIST
+        _data["title"] = title
+        _data["privacyStatus"] = privacy
+
+        create = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/playlist/create?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=_data
+        )
+        return await create.json()
+
+    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] = []):
+        """
+        Edit video metadata.
+        """
+        self.templates.setVideoId(video_id)
+        _data = self.templates.METADATA_UPDATE
+        if title != "":
+            _title = self.templates.METADATA_UPDATE_TITLE
+            _title["title"]["newTitle"] = title
+            _data.update(_title)
+
+        if description != "":
+            _description = self.templates.METADATA_UPDATE_DESCRIPTION
+            _description["description"]["newDescription"] = description
+            _data.update(_description)
+
+        if privacy != "":
+            _privacy = self.templates.METADATA_UPDATE_PRIVACY
+            _privacy["privacy"]["newPrivacy"] = privacy
+            _data.update(_privacy)
+
+        if thumb != "":
+            _thumb = self.templates.METADATA_UPDATE_THUMB
+            image = open(thumb, 'rb')
+            image_64_encode = base64.b64encode(image.read()).decode('utf-8')
+
+            _thumb["videoStill"]["image"][
+                "dataUri"] = f"data:image/png;base64,{image_64_encode}"
+            _data.update(_thumb)
+
+        if len(tags) > 0:
+            _tags = self.templates.METADATA_UPDATE_TAGS
+            _tags["tags"]["newTags"] = tags
+            _data.update(_tags)
+
+        if category != -1:
+            _category = self.templates.METADATA_UPDATE_CATEGORY
+            _category["category"]["newCategoryId"] = category
+            _data.update(_category)
+
+        if len(playlist) > 0:
+            _playlist = self.templates.METADATA_UPDATE_PLAYLIST
+            _playlist["addToPlaylist"]["addToPlaylistIds"] = playlist
+            if len(removeFromPlaylist) > 0:
+                _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist
+            _data.update(_playlist)
+
+        if len(removeFromPlaylist) > 0:
+            _playlist = self.templates.METADATA_UPDATE_PLAYLIST
+            _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist
+            _data.update(_playlist)
+
+        _monetization = self.templates.METADATA_UPDATE_MONETIZATION
+        _monetization["monetizationSettings"]["newMonetization"] = monetization
+        _data.update(_monetization)
+
+        update = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/video_manager/metadata_update?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=_data
+        )
+        return await update.json()
+
+
+
+

Sub-modules

+
+
ytstudio.templates
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Studio +(cookies: dict = {'SESSION_TOKEN': '', 'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN_INFO': '', 'SID': '', '__Secure-3PSID': '.', 'HSID': '', 'SSID': '', 'APISID': '', 'SAPISID': '', '__Secure-3PAPISID': '', 'YSC': '', 'SIDCC': ''}) +
+
+
+
+ +Expand source code + +
class Studio:
+    YT_STUDIO_URL = "https://studio.youtube.com"
+    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"
+    TRANSFERRED_BYTES = 0
+    CHUNK_SIZE = 64*1024
+
+    def __init__(self, cookies: dict = {'SESSION_TOKEN': '', 'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN_INFO': '', 'SID': '', '__Secure-3PSID': '.', 'HSID': '',
+                 'SSID': '', 'APISID': '', 'SAPISID': '', '__Secure-3PAPISID': '', 'YSC': '', 'SIDCC': ''}):
+        self.SAPISIDHASH = self.generateSAPISIDHASH(cookies['SAPISID'])
+        self.cookies = cookies
+        self.Cookie = " ".join(
+            [f"{c}={cookies[c]};" if not c == "SESSION_TOKEN" else "" for c in cookies.keys()])
+        self.HEADERS = {
+            'Authorization': f'SAPISIDHASH {self.SAPISIDHASH}',
+            'Content-Type': 'application/json',
+            'Cookie': self.Cookie,
+            'X-Origin': self.YT_STUDIO_URL,
+            'User-Agent': self.USER_AGENT
+        }
+        self.session = aiohttp.ClientSession(headers=self.HEADERS)
+        self.loop = asyncio.get_event_loop()
+        self.config = {}
+        self.js = js2py.EvalJs()
+        self.js.execute("var window = {ytcfg: {}};")
+
+    def __del__(self):
+        self.loop.create_task(self.session.close())
+
+    def generateSAPISIDHASH(self, SAPISID) -> str:
+        hash = f"{round(time.time())} {SAPISID} {self.YT_STUDIO_URL}"
+        sifrelenmis = sha1(hash.encode('utf-8')).hexdigest()
+        return f"{round(time.time())}_{sifrelenmis}"
+
+    async def getMainPage(self) -> str:
+        page = await self.session.get(self.YT_STUDIO_URL)
+        return await page.text("utf-8")
+
+    async def login(self) -> bool:
+        """
+        Login to your youtube account
+        """
+        page = await self.getMainPage()
+        _ = pq(page)
+        script = _("script")
+        if len(script) < 1:
+            raise Exception("Didn't find script. Can you check your cookies?")
+        script = script[0].text
+        self.js.execute(
+            f"{script} window.ytcfg = ytcfg;")
+
+        INNERTUBE_API_KEY = self.js.window.ytcfg.data_.INNERTUBE_API_KEY
+        CHANNEL_ID = self.js.window.ytcfg.data_.CHANNEL_ID
+        # DELEGATED_SESSION_ID = js.window.ytcfg.data_.DELEGATED_SESSION_ID Looking Google removed this key
+
+        if INNERTUBE_API_KEY == None or CHANNEL_ID == None:
+            raise Exception(
+                "Didn't find INNERTUBE_API_KEY or CHANNEL_ID. Can you check your cookies?")
+        self.config = {'INNERTUBE_API_KEY': INNERTUBE_API_KEY,
+                       'CHANNEL_ID': CHANNEL_ID, 'data_': self.js.window.ytcfg.data_}
+        self.templates = Templates({
+            'channelId': CHANNEL_ID,
+            'sessionToken': self.cookies['SESSION_TOKEN']
+        })
+
+        return True
+
+    def generateHash(self) -> str:
+        harfler = list(
+            '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
+        keys = ['' for i in range(0, 36)]
+        b = 0
+        c = ""
+        e = 0
+
+        while e < 36:
+            if 8 == e or 13 == e or 18 == e or 23 == e:
+                keys[e] = "-"
+            else:
+                if 14 == e:
+                    keys[e] = "4"
+                elif 2 >= b:
+                    b = round(33554432 + 16777216 * random.uniform(0, 0.9))
+                c = b & 15
+                b = b >> 4
+                keys[e] = harfler[c & 3 | 8 if 19 == e else c]
+            e += 1
+
+        return "".join(keys)
+
+    async def fileSender(self, file_name):
+        async with aiofiles.open(file_name, 'rb') as f:
+            chunk = await f.read(self.CHUNK_SIZE)
+            while chunk:
+                if self.progress != None:
+                    self.TRANSFERRED_BYTES += len(chunk)
+                    self.progress(self.TRANSFERRED_BYTES,
+                                  os.path.getsize(file_name))
+
+                self.TRANSFERRED_BYTES += len(chunk)
+                yield chunk
+                chunk = await f.read(self.CHUNK_SIZE)
+                if not chunk:
+                    break
+
+    async def uploadFileToYoutube(self, upload_url, file_path):
+        self.TRANSFERRED_BYTES = 0
+
+        uploaded = await self.session.post(upload_url,  headers={
+            "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'",
+            "x-goog-upload-command": "upload, finalize",
+            "x-goog-upload-file-name": f"file-{round(time.time())}",
+            "x-goog-upload-offset": "0",
+            "Referer": self.YT_STUDIO_URL,
+        }, data=self.fileSender(file_path), timeout=None)
+        _ = await uploaded.text("utf-8")
+        _ = json.loads(_)
+        return _['scottyResourceId']
+
+    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):
+        """
+        Uploads a video to youtube.
+        """
+        self.progress = progress
+        frontEndUID = f"innertube_studio:{self.generateHash()}:0"
+
+        uploadRequest = await self.session.post("https://upload.youtube.com/upload/studio",
+                                                headers={
+                                                    "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'",
+                                                    "x-goog-upload-command": "start",
+                                                    "x-goog-upload-file-name": f"file-{round(time.time())}",
+                                                    "x-goog-upload-protocol": "resumable",
+                                                    "Referer": self.YT_STUDIO_URL,
+                                                },
+                                                json={'frontendUploadId': frontEndUID})
+
+        uploadUrl = uploadRequest.headers.get("x-goog-upload-url")
+        scottyResourceId = await self.uploadFileToYoutube(uploadUrl, file_name)
+
+        _data = self.templates.UPLOAD_VIDEO
+        _data["resourceId"]["scottyResourceId"]["id"] = scottyResourceId
+        _data["frontendUploadId"] = frontEndUID
+        _data["initialMetadata"] = {
+            "title": {
+                "newTitle": title
+            },
+            "description": {
+                "newDescription": description,
+                "shouldSegment": True
+            },
+            "privacy": {
+                "newPrivacy": privacy
+            },
+            "draftState": {
+                "isDraft": draft
+            }
+        }
+        upload = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/upload/createvideo?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=_data
+        )
+
+        return await upload.json()
+
+    async def deleteVideo(self, video_id):
+        """
+        Delete video from your channel
+        """
+        self.templates.setVideoId(video_id)
+        delete = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/video/delete?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=self.templates.DELETE_VIDEO
+        )
+        return await delete.json()
+
+    async def listVideos(self):
+        """
+        Returns a list of videos in your channel
+        """
+        list = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/creator/list_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=self.templates.LIST_VIDEOS
+        )
+        return await list.json()
+
+    async def getVideo(self, video_id):
+        """
+        Get video data.
+        """
+        self.templates.setVideoId(video_id)
+        video = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/creator/get_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=self.templates.GET_VIDEO
+        )
+        return await video.json()
+
+    async def createPlaylist(self, title, privacy="PUBLIC") -> dict:
+        """
+        Create a new playlist.
+        """
+        _data = self.templates.CREATE_PLAYLIST
+        _data["title"] = title
+        _data["privacyStatus"] = privacy
+
+        create = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/playlist/create?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=_data
+        )
+        return await create.json()
+
+    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] = []):
+        """
+        Edit video metadata.
+        """
+        self.templates.setVideoId(video_id)
+        _data = self.templates.METADATA_UPDATE
+        if title != "":
+            _title = self.templates.METADATA_UPDATE_TITLE
+            _title["title"]["newTitle"] = title
+            _data.update(_title)
+
+        if description != "":
+            _description = self.templates.METADATA_UPDATE_DESCRIPTION
+            _description["description"]["newDescription"] = description
+            _data.update(_description)
+
+        if privacy != "":
+            _privacy = self.templates.METADATA_UPDATE_PRIVACY
+            _privacy["privacy"]["newPrivacy"] = privacy
+            _data.update(_privacy)
+
+        if thumb != "":
+            _thumb = self.templates.METADATA_UPDATE_THUMB
+            image = open(thumb, 'rb')
+            image_64_encode = base64.b64encode(image.read()).decode('utf-8')
+
+            _thumb["videoStill"]["image"][
+                "dataUri"] = f"data:image/png;base64,{image_64_encode}"
+            _data.update(_thumb)
+
+        if len(tags) > 0:
+            _tags = self.templates.METADATA_UPDATE_TAGS
+            _tags["tags"]["newTags"] = tags
+            _data.update(_tags)
+
+        if category != -1:
+            _category = self.templates.METADATA_UPDATE_CATEGORY
+            _category["category"]["newCategoryId"] = category
+            _data.update(_category)
+
+        if len(playlist) > 0:
+            _playlist = self.templates.METADATA_UPDATE_PLAYLIST
+            _playlist["addToPlaylist"]["addToPlaylistIds"] = playlist
+            if len(removeFromPlaylist) > 0:
+                _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist
+            _data.update(_playlist)
+
+        if len(removeFromPlaylist) > 0:
+            _playlist = self.templates.METADATA_UPDATE_PLAYLIST
+            _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist
+            _data.update(_playlist)
+
+        _monetization = self.templates.METADATA_UPDATE_MONETIZATION
+        _monetization["monetizationSettings"]["newMonetization"] = monetization
+        _data.update(_monetization)
+
+        update = await self.session.post(
+            f"https://studio.youtube.com/youtubei/v1/video_manager/metadata_update?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+            json=_data
+        )
+        return await update.json()
+
+

Class variables

+
+
var CHUNK_SIZE
+
+
+
+
var TRANSFERRED_BYTES
+
+
+
+
var USER_AGENT
+
+
+
+
var YT_STUDIO_URL
+
+
+
+
+

Methods

+
+
+async def createPlaylist(self, title, privacy='PUBLIC') ‑> dict +
+
+

Create a new playlist.

+
+ +Expand source code + +
async def createPlaylist(self, title, privacy="PUBLIC") -> dict:
+    """
+    Create a new playlist.
+    """
+    _data = self.templates.CREATE_PLAYLIST
+    _data["title"] = title
+    _data["privacyStatus"] = privacy
+
+    create = await self.session.post(
+        f"https://studio.youtube.com/youtubei/v1/playlist/create?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+        json=_data
+    )
+    return await create.json()
+
+
+
+async def deleteVideo(self, video_id) +
+
+

Delete video from your channel

+
+ +Expand source code + +
async def deleteVideo(self, video_id):
+    """
+    Delete video from your channel
+    """
+    self.templates.setVideoId(video_id)
+    delete = await self.session.post(
+        f"https://studio.youtube.com/youtubei/v1/video/delete?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+        json=self.templates.DELETE_VIDEO
+    )
+    return await delete.json()
+
+
+
+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] = []) +
+
+

Edit video metadata.

+
+ +Expand source code + +
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] = []):
+    """
+    Edit video metadata.
+    """
+    self.templates.setVideoId(video_id)
+    _data = self.templates.METADATA_UPDATE
+    if title != "":
+        _title = self.templates.METADATA_UPDATE_TITLE
+        _title["title"]["newTitle"] = title
+        _data.update(_title)
+
+    if description != "":
+        _description = self.templates.METADATA_UPDATE_DESCRIPTION
+        _description["description"]["newDescription"] = description
+        _data.update(_description)
+
+    if privacy != "":
+        _privacy = self.templates.METADATA_UPDATE_PRIVACY
+        _privacy["privacy"]["newPrivacy"] = privacy
+        _data.update(_privacy)
+
+    if thumb != "":
+        _thumb = self.templates.METADATA_UPDATE_THUMB
+        image = open(thumb, 'rb')
+        image_64_encode = base64.b64encode(image.read()).decode('utf-8')
+
+        _thumb["videoStill"]["image"][
+            "dataUri"] = f"data:image/png;base64,{image_64_encode}"
+        _data.update(_thumb)
+
+    if len(tags) > 0:
+        _tags = self.templates.METADATA_UPDATE_TAGS
+        _tags["tags"]["newTags"] = tags
+        _data.update(_tags)
+
+    if category != -1:
+        _category = self.templates.METADATA_UPDATE_CATEGORY
+        _category["category"]["newCategoryId"] = category
+        _data.update(_category)
+
+    if len(playlist) > 0:
+        _playlist = self.templates.METADATA_UPDATE_PLAYLIST
+        _playlist["addToPlaylist"]["addToPlaylistIds"] = playlist
+        if len(removeFromPlaylist) > 0:
+            _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist
+        _data.update(_playlist)
+
+    if len(removeFromPlaylist) > 0:
+        _playlist = self.templates.METADATA_UPDATE_PLAYLIST
+        _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist
+        _data.update(_playlist)
+
+    _monetization = self.templates.METADATA_UPDATE_MONETIZATION
+    _monetization["monetizationSettings"]["newMonetization"] = monetization
+    _data.update(_monetization)
+
+    update = await self.session.post(
+        f"https://studio.youtube.com/youtubei/v1/video_manager/metadata_update?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+        json=_data
+    )
+    return await update.json()
+
+
+
+async def fileSender(self, file_name) +
+
+
+
+ +Expand source code + +
async def fileSender(self, file_name):
+    async with aiofiles.open(file_name, 'rb') as f:
+        chunk = await f.read(self.CHUNK_SIZE)
+        while chunk:
+            if self.progress != None:
+                self.TRANSFERRED_BYTES += len(chunk)
+                self.progress(self.TRANSFERRED_BYTES,
+                              os.path.getsize(file_name))
+
+            self.TRANSFERRED_BYTES += len(chunk)
+            yield chunk
+            chunk = await f.read(self.CHUNK_SIZE)
+            if not chunk:
+                break
+
+
+
+def generateHash(self) ‑> str +
+
+
+
+ +Expand source code + +
def generateHash(self) -> str:
+    harfler = list(
+        '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
+    keys = ['' for i in range(0, 36)]
+    b = 0
+    c = ""
+    e = 0
+
+    while e < 36:
+        if 8 == e or 13 == e or 18 == e or 23 == e:
+            keys[e] = "-"
+        else:
+            if 14 == e:
+                keys[e] = "4"
+            elif 2 >= b:
+                b = round(33554432 + 16777216 * random.uniform(0, 0.9))
+            c = b & 15
+            b = b >> 4
+            keys[e] = harfler[c & 3 | 8 if 19 == e else c]
+        e += 1
+
+    return "".join(keys)
+
+
+
+def generateSAPISIDHASH(self, SAPISID) ‑> str +
+
+
+
+ +Expand source code + +
def generateSAPISIDHASH(self, SAPISID) -> str:
+    hash = f"{round(time.time())} {SAPISID} {self.YT_STUDIO_URL}"
+    sifrelenmis = sha1(hash.encode('utf-8')).hexdigest()
+    return f"{round(time.time())}_{sifrelenmis}"
+
+
+
+async def getMainPage(self) ‑> str +
+
+
+
+ +Expand source code + +
async def getMainPage(self) -> str:
+    page = await self.session.get(self.YT_STUDIO_URL)
+    return await page.text("utf-8")
+
+
+
+async def getVideo(self, video_id) +
+
+

Get video data.

+
+ +Expand source code + +
async def getVideo(self, video_id):
+    """
+    Get video data.
+    """
+    self.templates.setVideoId(video_id)
+    video = await self.session.post(
+        f"https://studio.youtube.com/youtubei/v1/creator/get_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+        json=self.templates.GET_VIDEO
+    )
+    return await video.json()
+
+
+
+async def listVideos(self) +
+
+

Returns a list of videos in your channel

+
+ +Expand source code + +
async def listVideos(self):
+    """
+    Returns a list of videos in your channel
+    """
+    list = await self.session.post(
+        f"https://studio.youtube.com/youtubei/v1/creator/list_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+        json=self.templates.LIST_VIDEOS
+    )
+    return await list.json()
+
+
+
+async def login(self) ‑> bool +
+
+

Login to your youtube account

+
+ +Expand source code + +
async def login(self) -> bool:
+    """
+    Login to your youtube account
+    """
+    page = await self.getMainPage()
+    _ = pq(page)
+    script = _("script")
+    if len(script) < 1:
+        raise Exception("Didn't find script. Can you check your cookies?")
+    script = script[0].text
+    self.js.execute(
+        f"{script} window.ytcfg = ytcfg;")
+
+    INNERTUBE_API_KEY = self.js.window.ytcfg.data_.INNERTUBE_API_KEY
+    CHANNEL_ID = self.js.window.ytcfg.data_.CHANNEL_ID
+    # DELEGATED_SESSION_ID = js.window.ytcfg.data_.DELEGATED_SESSION_ID Looking Google removed this key
+
+    if INNERTUBE_API_KEY == None or CHANNEL_ID == None:
+        raise Exception(
+            "Didn't find INNERTUBE_API_KEY or CHANNEL_ID. Can you check your cookies?")
+    self.config = {'INNERTUBE_API_KEY': INNERTUBE_API_KEY,
+                   'CHANNEL_ID': CHANNEL_ID, 'data_': self.js.window.ytcfg.data_}
+    self.templates = Templates({
+        'channelId': CHANNEL_ID,
+        'sessionToken': self.cookies['SESSION_TOKEN']
+    })
+
+    return True
+
+
+
+async def uploadFileToYoutube(self, upload_url, file_path) +
+
+
+
+ +Expand source code + +
async def uploadFileToYoutube(self, upload_url, file_path):
+    self.TRANSFERRED_BYTES = 0
+
+    uploaded = await self.session.post(upload_url,  headers={
+        "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'",
+        "x-goog-upload-command": "upload, finalize",
+        "x-goog-upload-file-name": f"file-{round(time.time())}",
+        "x-goog-upload-offset": "0",
+        "Referer": self.YT_STUDIO_URL,
+    }, data=self.fileSender(file_path), timeout=None)
+    _ = await uploaded.text("utf-8")
+    _ = json.loads(_)
+    return _['scottyResourceId']
+
+
+
+async def uploadVideo(self, file_name, title='New Video 1637196488', description='This video uploaded by github.com/yusufusta/ytstudio', privacy='PRIVATE', draft=False, progress=None) +
+
+

Uploads a video to youtube.

+
+ +Expand source code + +
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):
+    """
+    Uploads a video to youtube.
+    """
+    self.progress = progress
+    frontEndUID = f"innertube_studio:{self.generateHash()}:0"
+
+    uploadRequest = await self.session.post("https://upload.youtube.com/upload/studio",
+                                            headers={
+                                                "Content-Type": "application/x-www-form-urlencoded;charset=utf-8'",
+                                                "x-goog-upload-command": "start",
+                                                "x-goog-upload-file-name": f"file-{round(time.time())}",
+                                                "x-goog-upload-protocol": "resumable",
+                                                "Referer": self.YT_STUDIO_URL,
+                                            },
+                                            json={'frontendUploadId': frontEndUID})
+
+    uploadUrl = uploadRequest.headers.get("x-goog-upload-url")
+    scottyResourceId = await self.uploadFileToYoutube(uploadUrl, file_name)
+
+    _data = self.templates.UPLOAD_VIDEO
+    _data["resourceId"]["scottyResourceId"]["id"] = scottyResourceId
+    _data["frontendUploadId"] = frontEndUID
+    _data["initialMetadata"] = {
+        "title": {
+            "newTitle": title
+        },
+        "description": {
+            "newDescription": description,
+            "shouldSegment": True
+        },
+        "privacy": {
+            "newPrivacy": privacy
+        },
+        "draftState": {
+            "isDraft": draft
+        }
+    }
+    upload = await self.session.post(
+        f"https://studio.youtube.com/youtubei/v1/upload/createvideo?alt=json&key={self.config['INNERTUBE_API_KEY']}",
+        json=_data
+    )
+
+    return await upload.json()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/templates.html b/docs/templates.html new file mode 100644 index 0000000..96d99d0 --- /dev/null +++ b/docs/templates.html @@ -0,0 +1,1343 @@ + + + + + + +ytstudio.templates API documentation + + + + + + + + + + + +
+
+
+

Module ytstudio.templates

+
+
+
+ +Expand source code + +
class Templates:
+    channelId = ""
+    videoId = ""
+    sessionToken = ""
+
+    CLIENT = {
+        "clientName": 62,
+        "clientVersion": "1.20201130.03.00",
+        "hl": "en-GB",
+        "gl": "PL",
+        "experimentsToken": "",
+        "utcOffsetMinutes": 60
+    }
+
+    def __init__(self, config) -> None:
+        self.config = config
+        self.channelId = self.config["channelId"]
+        self.sessionToken = self.config["sessionToken"]
+        self._()
+
+    def setVideoId(self, videoId):
+        self.videoId = videoId
+        self._()
+
+    def _(self):
+        self.DELETE_VIDEO = {
+            "videoId": self.videoId,
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": [],
+                    "sessionInfo": {
+                        "token": self.sessionToken
+                    }
+                },
+                "user": {
+                    "delegationContext": {
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        },
+                        "externalChannelId": self.channelId
+                    },
+                    "serializedDelegationContext": ""
+                },
+                "clientScreenNonce": ""
+            },
+            "delegationContext": {
+                "roleType": {
+                    "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                },
+                "externalChannelId": self.channelId
+            }
+        }
+
+        self.UPLOAD_VIDEO = {
+            "channelId": self.channelId,
+            "resourceId": {
+                "scottyResourceId": {
+                    "id": ""
+                }
+            },
+            "frontendUploadId": "",
+            "initialMetadata": {
+                "title": {
+                    "newTitle": ""
+                },
+                "description": {
+                    "newDescription": "",
+                    "shouldSegment": True
+                },
+                "privacy": {
+                    "newPrivacy": ""
+                },
+                "draftState": {
+                    "isDraft": ""
+                }
+            },
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": [],
+                    "sessionInfo": {
+                        "token": self.sessionToken
+                    }
+                },
+                "user": {
+                    "delegationContext": {
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        },
+                        "externalChannelId": self.channelId
+                    },
+                    "serializedDelegationContext": "",
+                    "onBehalfOfUser": ""
+                },
+                "clientScreenNonce": ""
+            },
+            "delegationContext": {
+                "roleType": {
+                    "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                },
+                "externalChannelId": self.channelId
+            }
+        }
+
+        self.METADATA_UPDATE = {
+            "encryptedVideoId": self.videoId,
+            "videoReadMask": {
+                "channelId": True,
+                "videoId": True,
+                "lengthSeconds": True,
+                "premiere": {
+                    "all": True
+                },
+                "status": True,
+                "thumbnailDetails": {
+                    "all": True
+                },
+                "title": True,
+                "draftStatus": True,
+                "downloadUrl": True,
+                "watchUrl": True,
+                "permissions": {
+                    "all": True
+                },
+                "timeCreatedSeconds": True,
+                "timePublishedSeconds": True,
+                "origin": True,
+                "livestream": {
+                    "all": True
+                },
+                "privacy": True,
+                "contentOwnershipModelSettings": {
+                    "all": True
+                },
+                "features": {
+                    "all": True
+                },
+                "responseStatus": {
+                    "all": True
+                },
+                "statusDetails": {
+                    "all": True
+                },
+                "description": True,
+                "metrics": {
+                    "all": True
+                },
+                "publicLivestream": {
+                    "all": True
+                },
+                "publicPremiere": {
+                    "all": True
+                },
+                "titleFormattedString": {
+                    "all": True
+                },
+                "descriptionFormattedString": {
+                    "all": True
+                },
+                "audienceRestriction": {
+                    "all": True
+                },
+                "monetization": {
+                    "all": True
+                },
+                "selfCertification": {
+                    "all": True
+                },
+                "allRestrictions": {
+                    "all": True
+                },
+                "inlineEditProcessingStatus": True,
+                "videoPrechecks": {
+                    "all": True
+                },
+                "videoResolutions": {
+                    "all": True
+                },
+                "scheduledPublishingDetails": {
+                    "all": True
+                },
+                "visibility": {
+                    "all": True
+                },
+                "privateShare": {
+                    "all": True
+                },
+                "sponsorsOnly": {
+                    "all": True
+                },
+                "unlistedExpired": True,
+                "videoTrailers": {
+                    "all": True
+                }
+            },
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": [],
+                    "sessionInfo": {
+                        "token": self.sessionToken
+                    }
+                },
+                "user": {
+                    "delegationContext": {
+                        "externalChannelId": self.channelId,
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        }
+                    },
+                    "serializedDelegationContext": ""
+                },
+                "clientScreenNonce": ""
+            },
+            "delegationContext": {
+                "externalChannelId": self.channelId,
+                "roleType": {
+                    "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                }
+            }
+        }
+
+        self.METADATA_UPDATE_MONETIZATION = {
+            "monetizationSettings": {
+                "newMonetizeWithAds": True
+            }
+        }
+
+        self.LIST_VIDEOS = {
+            "filter": {
+                "and": {
+                    "operands": [
+                        {
+                            "channelIdIs": {
+                                "value": self.channelId
+                            }
+                        }, {
+                            "videoOriginIs": {
+                                "value": "VIDEO_ORIGIN_UPLOAD"
+                            }
+                        }
+                    ]
+                }
+            },
+            "order": "VIDEO_ORDER_DISPLAY_TIME_DESC",
+            "pageSize": 30,
+            "mask": {
+                "channelId": True,
+                "videoId": True,
+                "lengthSeconds": True,
+                "premiere": {
+                    "all": True
+                },
+                "status": True,
+                "thumbnailDetails": {
+                    "all": True
+                },
+                "title": True,
+                "draftStatus": True,
+                "downloadUrl": True,
+                "watchUrl": True,
+                "permissions": {
+                    "all": True
+                },
+                "timeCreatedSeconds": True,
+                "timePublishedSeconds": True,
+                "origin": True,
+                "livestream": {
+                    "all": True
+                },
+                "privacy": True,
+                "contentOwnershipModelSettings": {
+                    "all": True
+                },
+                "features": {
+                    "all": True
+                },
+                "responseStatus": {
+                    "all": True
+                },
+                "statusDetails": {
+                    "all": True
+                },
+                "description": True,
+                "metrics": {
+                    "all": True
+                },
+                "publicLivestream": {
+                    "all": True
+                },
+                "publicPremiere": {
+                    "all": True
+                },
+                "titleFormattedString": {
+                    "all": True
+                },
+                "descriptionFormattedString": {
+                    "all": True
+                },
+                "audienceRestriction": {
+                    "all": True
+                },
+                "monetization": {
+                    "all": True
+                },
+                "selfCertification": {
+                    "all": True
+                },
+                "allRestrictions": {
+                    "all": True
+                },
+                "inlineEditProcessingStatus": True,
+                "videoPrechecks": {
+                    "all": True
+                },
+                "videoResolutions": {
+                    "all": True
+                },
+                "scheduledPublishingDetails": {
+                    "all": True
+                },
+                "visibility": {
+                    "all": True
+                },
+                "privateShare": {
+                    "all": True
+                },
+                "sponsorsOnly": {
+                    "all": True
+                },
+                "unlistedExpired": True,
+                "videoTrailers": {
+                    "all": True
+                }
+            },
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": []
+                },
+                "user": {
+                    "delegationContext": {
+                        "externalChannelId": self.channelId,
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        }
+                    },
+                    "serializedDelegationContext": ""
+                },
+                "clientScreenNonce": ""
+            }
+        }
+
+        self.GET_VIDEO = {
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": []
+                },
+                "user": {
+                    "delegationContext": {
+                        "externalChannelId": self.channelId,
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        }
+                    },
+                    "serializedDelegationContext": ""
+                },
+                "clientScreenNonce": ""
+            },
+            "failOnError": True,
+            "videoIds": [self.videoId],
+            "mask": {
+                "downloadUrl": True,
+                "origin": True,
+                "premiere": {
+                    "all": True
+                },
+                "privacy": True,
+                "videoId": True,
+                "status": True,
+                "permissions": {
+                    "all": True
+                },
+                "draftStatus": True,
+                "statusDetails": {
+                    "all": True
+                },
+                "inlineEditProcessingStatus": True,
+                "selfCertification": {
+                    "all": True
+                },
+                "monetization": {
+                    "all": True
+                },
+                "allRestrictions": {
+                    "all": True
+                },
+                "videoPrechecks": {
+                    "all": True
+                },
+                "audienceRestriction": {
+                    "all": True
+                },
+                "responseStatus": {
+                    "all": True
+                },
+                "features": {
+                    "all": True
+                },
+                "videoAdvertiserSpecificAgeGates": {
+                    "all": True
+                },
+                "claimDetails": {
+                    "all": True
+                },
+                "commentsDisabledInternally": True,
+                "livestream": {
+                    "all": True
+                },
+                "music": {
+                    "all": True
+                },
+                "ownedClaimDetails": {
+                    "all": True
+                },
+                "timePublishedSeconds": True,
+                "uncaptionedReason": True,
+                "remix": {
+                    "all": True
+                },
+                "contentOwnershipModelSettings": {
+                    "all": True
+                },
+                "channelId": True,
+                "mfkSettings": {
+                    "all": True
+                },
+                "thumbnailEditorState": {
+                    "all": True
+                },
+                "thumbnailDetails": {
+                    "all": True
+                },
+                "scheduledPublishingDetails": {
+                    "all": True
+                },
+                "visibility": {
+                    "all": True
+                },
+                "privateShare": {
+                    "all": True
+                },
+                "sponsorsOnly": {
+                    "all": True
+                },
+                "unlistedExpired": True,
+                "videoTrailers": {
+                    "all": True
+                },
+                "allowComments": True,
+                "allowEmbed": True,
+                "allowRatings": True,
+                "ageRestriction": True,
+                "audioLanguage": {
+                    "all": True
+                },
+                "category": True,
+                "commentFilter": True,
+                "crowdsourcingEnabled": True,
+                "dateRecorded": {
+                    "all": True
+                },
+                "defaultCommentSortOrder": True,
+                "description": True,
+                "descriptionFormattedString": {
+                    "all": True
+                },
+                "gameTitle": {
+                    "all": True
+                },
+                "license": True,
+                "liveChat": {
+                    "all": True
+                },
+                "location": {
+                    "all": True
+                },
+                "metadataLanguage": {
+                    "all": True
+                },
+                "paidProductPlacement": True,
+                "publishing": {
+                    "all": True
+                },
+                "tags": {
+                    "all": True
+                },
+                "title": True,
+                "titleFormattedString": {
+                    "all": True
+                },
+                "viewCountIsHidden": True,
+                "autoChapterSettings": {
+                    "all": True
+                },
+                "videoStreamUrl": True,
+                "videoDurationMs": True,
+                "videoEditorProject": {
+                    "videoDimensions": {
+                        "all": True
+                    }
+                },
+                "originalFilename": True,
+                "videoResolutions": {
+                    "all": True
+                }
+            },
+            "criticalRead": False
+        }
+
+        self.CREATE_PLAYLIST = {
+            "title": "",
+            "privacyStatus": "",
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": [],
+                    "sessionInfo": {
+                        "token": self.sessionToken
+                    }
+                },
+                "user": {
+                    "delegationContext": {
+                        "externalChannelId": self.channelId,
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        }
+                    },
+                    "serializedDelegationContext": ""
+                },
+                "clientScreenNonce": ""
+            },
+            "delegationContext": {
+                "externalChannelId": self.channelId,
+                "roleType": {
+                    "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                }
+            }
+        }
+
+        self.METADATA_UPDATE_PLAYLIST = {
+            "addToPlaylist": {
+                "addToPlaylistIds": [],
+                "deleteFromPlaylistIds": []
+            }
+        }
+
+        self.METADATA_UPDATE_TITLE = {
+            "title": {
+                "newTitle": "",
+                "shouldSegment": True
+            }
+        }
+
+        self.METADATA_UPDATE_DESCRIPTION = {
+            "description": {
+                "newDescription": "",
+                "shouldSegment": True
+            }
+        }
+
+        self.METADATA_UPDATE_TAGS = {
+            "tags": {
+                "newTags": [],
+                "shouldSegment": True
+            }
+        }
+
+        self.METADATA_UPDATE_CATEGORY = {
+            "category": {
+                "newCategoryId": 0
+            }
+        }
+
+        self.METADATA_UPDATE_COMMENTS = {
+            "commentOptions": {
+                "newAllowComments": True,
+                "newAllowCommentsMode": "ALL_COMMENTS",
+                "newCanViewRatings": True,
+                "newDefaultSortOrder": "MDE_COMMENT_SORT_ORDER_TOP"
+            }
+        }
+
+        self.METADATA_UPDATE_PRIVACY = {
+            "privacyState": {"newPrivacy": "PUBLIC"}
+        }
+
+        self.METADATA_UPDATE_THUMB = {
+            "videoStill": {"operation": "UPLOAD_CUSTOM_THUMBNAIL", "image": {
+                "dataUri": ""
+            }}
+        }
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Templates +(config) +
+
+
+
+ +Expand source code + +
class Templates:
+    channelId = ""
+    videoId = ""
+    sessionToken = ""
+
+    CLIENT = {
+        "clientName": 62,
+        "clientVersion": "1.20201130.03.00",
+        "hl": "en-GB",
+        "gl": "PL",
+        "experimentsToken": "",
+        "utcOffsetMinutes": 60
+    }
+
+    def __init__(self, config) -> None:
+        self.config = config
+        self.channelId = self.config["channelId"]
+        self.sessionToken = self.config["sessionToken"]
+        self._()
+
+    def setVideoId(self, videoId):
+        self.videoId = videoId
+        self._()
+
+    def _(self):
+        self.DELETE_VIDEO = {
+            "videoId": self.videoId,
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": [],
+                    "sessionInfo": {
+                        "token": self.sessionToken
+                    }
+                },
+                "user": {
+                    "delegationContext": {
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        },
+                        "externalChannelId": self.channelId
+                    },
+                    "serializedDelegationContext": ""
+                },
+                "clientScreenNonce": ""
+            },
+            "delegationContext": {
+                "roleType": {
+                    "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                },
+                "externalChannelId": self.channelId
+            }
+        }
+
+        self.UPLOAD_VIDEO = {
+            "channelId": self.channelId,
+            "resourceId": {
+                "scottyResourceId": {
+                    "id": ""
+                }
+            },
+            "frontendUploadId": "",
+            "initialMetadata": {
+                "title": {
+                    "newTitle": ""
+                },
+                "description": {
+                    "newDescription": "",
+                    "shouldSegment": True
+                },
+                "privacy": {
+                    "newPrivacy": ""
+                },
+                "draftState": {
+                    "isDraft": ""
+                }
+            },
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": [],
+                    "sessionInfo": {
+                        "token": self.sessionToken
+                    }
+                },
+                "user": {
+                    "delegationContext": {
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        },
+                        "externalChannelId": self.channelId
+                    },
+                    "serializedDelegationContext": "",
+                    "onBehalfOfUser": ""
+                },
+                "clientScreenNonce": ""
+            },
+            "delegationContext": {
+                "roleType": {
+                    "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                },
+                "externalChannelId": self.channelId
+            }
+        }
+
+        self.METADATA_UPDATE = {
+            "encryptedVideoId": self.videoId,
+            "videoReadMask": {
+                "channelId": True,
+                "videoId": True,
+                "lengthSeconds": True,
+                "premiere": {
+                    "all": True
+                },
+                "status": True,
+                "thumbnailDetails": {
+                    "all": True
+                },
+                "title": True,
+                "draftStatus": True,
+                "downloadUrl": True,
+                "watchUrl": True,
+                "permissions": {
+                    "all": True
+                },
+                "timeCreatedSeconds": True,
+                "timePublishedSeconds": True,
+                "origin": True,
+                "livestream": {
+                    "all": True
+                },
+                "privacy": True,
+                "contentOwnershipModelSettings": {
+                    "all": True
+                },
+                "features": {
+                    "all": True
+                },
+                "responseStatus": {
+                    "all": True
+                },
+                "statusDetails": {
+                    "all": True
+                },
+                "description": True,
+                "metrics": {
+                    "all": True
+                },
+                "publicLivestream": {
+                    "all": True
+                },
+                "publicPremiere": {
+                    "all": True
+                },
+                "titleFormattedString": {
+                    "all": True
+                },
+                "descriptionFormattedString": {
+                    "all": True
+                },
+                "audienceRestriction": {
+                    "all": True
+                },
+                "monetization": {
+                    "all": True
+                },
+                "selfCertification": {
+                    "all": True
+                },
+                "allRestrictions": {
+                    "all": True
+                },
+                "inlineEditProcessingStatus": True,
+                "videoPrechecks": {
+                    "all": True
+                },
+                "videoResolutions": {
+                    "all": True
+                },
+                "scheduledPublishingDetails": {
+                    "all": True
+                },
+                "visibility": {
+                    "all": True
+                },
+                "privateShare": {
+                    "all": True
+                },
+                "sponsorsOnly": {
+                    "all": True
+                },
+                "unlistedExpired": True,
+                "videoTrailers": {
+                    "all": True
+                }
+            },
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": [],
+                    "sessionInfo": {
+                        "token": self.sessionToken
+                    }
+                },
+                "user": {
+                    "delegationContext": {
+                        "externalChannelId": self.channelId,
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        }
+                    },
+                    "serializedDelegationContext": ""
+                },
+                "clientScreenNonce": ""
+            },
+            "delegationContext": {
+                "externalChannelId": self.channelId,
+                "roleType": {
+                    "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                }
+            }
+        }
+
+        self.METADATA_UPDATE_MONETIZATION = {
+            "monetizationSettings": {
+                "newMonetizeWithAds": True
+            }
+        }
+
+        self.LIST_VIDEOS = {
+            "filter": {
+                "and": {
+                    "operands": [
+                        {
+                            "channelIdIs": {
+                                "value": self.channelId
+                            }
+                        }, {
+                            "videoOriginIs": {
+                                "value": "VIDEO_ORIGIN_UPLOAD"
+                            }
+                        }
+                    ]
+                }
+            },
+            "order": "VIDEO_ORDER_DISPLAY_TIME_DESC",
+            "pageSize": 30,
+            "mask": {
+                "channelId": True,
+                "videoId": True,
+                "lengthSeconds": True,
+                "premiere": {
+                    "all": True
+                },
+                "status": True,
+                "thumbnailDetails": {
+                    "all": True
+                },
+                "title": True,
+                "draftStatus": True,
+                "downloadUrl": True,
+                "watchUrl": True,
+                "permissions": {
+                    "all": True
+                },
+                "timeCreatedSeconds": True,
+                "timePublishedSeconds": True,
+                "origin": True,
+                "livestream": {
+                    "all": True
+                },
+                "privacy": True,
+                "contentOwnershipModelSettings": {
+                    "all": True
+                },
+                "features": {
+                    "all": True
+                },
+                "responseStatus": {
+                    "all": True
+                },
+                "statusDetails": {
+                    "all": True
+                },
+                "description": True,
+                "metrics": {
+                    "all": True
+                },
+                "publicLivestream": {
+                    "all": True
+                },
+                "publicPremiere": {
+                    "all": True
+                },
+                "titleFormattedString": {
+                    "all": True
+                },
+                "descriptionFormattedString": {
+                    "all": True
+                },
+                "audienceRestriction": {
+                    "all": True
+                },
+                "monetization": {
+                    "all": True
+                },
+                "selfCertification": {
+                    "all": True
+                },
+                "allRestrictions": {
+                    "all": True
+                },
+                "inlineEditProcessingStatus": True,
+                "videoPrechecks": {
+                    "all": True
+                },
+                "videoResolutions": {
+                    "all": True
+                },
+                "scheduledPublishingDetails": {
+                    "all": True
+                },
+                "visibility": {
+                    "all": True
+                },
+                "privateShare": {
+                    "all": True
+                },
+                "sponsorsOnly": {
+                    "all": True
+                },
+                "unlistedExpired": True,
+                "videoTrailers": {
+                    "all": True
+                }
+            },
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": []
+                },
+                "user": {
+                    "delegationContext": {
+                        "externalChannelId": self.channelId,
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        }
+                    },
+                    "serializedDelegationContext": ""
+                },
+                "clientScreenNonce": ""
+            }
+        }
+
+        self.GET_VIDEO = {
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": []
+                },
+                "user": {
+                    "delegationContext": {
+                        "externalChannelId": self.channelId,
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        }
+                    },
+                    "serializedDelegationContext": ""
+                },
+                "clientScreenNonce": ""
+            },
+            "failOnError": True,
+            "videoIds": [self.videoId],
+            "mask": {
+                "downloadUrl": True,
+                "origin": True,
+                "premiere": {
+                    "all": True
+                },
+                "privacy": True,
+                "videoId": True,
+                "status": True,
+                "permissions": {
+                    "all": True
+                },
+                "draftStatus": True,
+                "statusDetails": {
+                    "all": True
+                },
+                "inlineEditProcessingStatus": True,
+                "selfCertification": {
+                    "all": True
+                },
+                "monetization": {
+                    "all": True
+                },
+                "allRestrictions": {
+                    "all": True
+                },
+                "videoPrechecks": {
+                    "all": True
+                },
+                "audienceRestriction": {
+                    "all": True
+                },
+                "responseStatus": {
+                    "all": True
+                },
+                "features": {
+                    "all": True
+                },
+                "videoAdvertiserSpecificAgeGates": {
+                    "all": True
+                },
+                "claimDetails": {
+                    "all": True
+                },
+                "commentsDisabledInternally": True,
+                "livestream": {
+                    "all": True
+                },
+                "music": {
+                    "all": True
+                },
+                "ownedClaimDetails": {
+                    "all": True
+                },
+                "timePublishedSeconds": True,
+                "uncaptionedReason": True,
+                "remix": {
+                    "all": True
+                },
+                "contentOwnershipModelSettings": {
+                    "all": True
+                },
+                "channelId": True,
+                "mfkSettings": {
+                    "all": True
+                },
+                "thumbnailEditorState": {
+                    "all": True
+                },
+                "thumbnailDetails": {
+                    "all": True
+                },
+                "scheduledPublishingDetails": {
+                    "all": True
+                },
+                "visibility": {
+                    "all": True
+                },
+                "privateShare": {
+                    "all": True
+                },
+                "sponsorsOnly": {
+                    "all": True
+                },
+                "unlistedExpired": True,
+                "videoTrailers": {
+                    "all": True
+                },
+                "allowComments": True,
+                "allowEmbed": True,
+                "allowRatings": True,
+                "ageRestriction": True,
+                "audioLanguage": {
+                    "all": True
+                },
+                "category": True,
+                "commentFilter": True,
+                "crowdsourcingEnabled": True,
+                "dateRecorded": {
+                    "all": True
+                },
+                "defaultCommentSortOrder": True,
+                "description": True,
+                "descriptionFormattedString": {
+                    "all": True
+                },
+                "gameTitle": {
+                    "all": True
+                },
+                "license": True,
+                "liveChat": {
+                    "all": True
+                },
+                "location": {
+                    "all": True
+                },
+                "metadataLanguage": {
+                    "all": True
+                },
+                "paidProductPlacement": True,
+                "publishing": {
+                    "all": True
+                },
+                "tags": {
+                    "all": True
+                },
+                "title": True,
+                "titleFormattedString": {
+                    "all": True
+                },
+                "viewCountIsHidden": True,
+                "autoChapterSettings": {
+                    "all": True
+                },
+                "videoStreamUrl": True,
+                "videoDurationMs": True,
+                "videoEditorProject": {
+                    "videoDimensions": {
+                        "all": True
+                    }
+                },
+                "originalFilename": True,
+                "videoResolutions": {
+                    "all": True
+                }
+            },
+            "criticalRead": False
+        }
+
+        self.CREATE_PLAYLIST = {
+            "title": "",
+            "privacyStatus": "",
+            "context": {
+                "client": self.CLIENT,
+                "request": {
+                    "returnLogEntry": True,
+                    "internalExperimentFlags": [],
+                    "sessionInfo": {
+                        "token": self.sessionToken
+                    }
+                },
+                "user": {
+                    "delegationContext": {
+                        "externalChannelId": self.channelId,
+                        "roleType": {
+                            "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                        }
+                    },
+                    "serializedDelegationContext": ""
+                },
+                "clientScreenNonce": ""
+            },
+            "delegationContext": {
+                "externalChannelId": self.channelId,
+                "roleType": {
+                    "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER"
+                }
+            }
+        }
+
+        self.METADATA_UPDATE_PLAYLIST = {
+            "addToPlaylist": {
+                "addToPlaylistIds": [],
+                "deleteFromPlaylistIds": []
+            }
+        }
+
+        self.METADATA_UPDATE_TITLE = {
+            "title": {
+                "newTitle": "",
+                "shouldSegment": True
+            }
+        }
+
+        self.METADATA_UPDATE_DESCRIPTION = {
+            "description": {
+                "newDescription": "",
+                "shouldSegment": True
+            }
+        }
+
+        self.METADATA_UPDATE_TAGS = {
+            "tags": {
+                "newTags": [],
+                "shouldSegment": True
+            }
+        }
+
+        self.METADATA_UPDATE_CATEGORY = {
+            "category": {
+                "newCategoryId": 0
+            }
+        }
+
+        self.METADATA_UPDATE_COMMENTS = {
+            "commentOptions": {
+                "newAllowComments": True,
+                "newAllowCommentsMode": "ALL_COMMENTS",
+                "newCanViewRatings": True,
+                "newDefaultSortOrder": "MDE_COMMENT_SORT_ORDER_TOP"
+            }
+        }
+
+        self.METADATA_UPDATE_PRIVACY = {
+            "privacyState": {"newPrivacy": "PUBLIC"}
+        }
+
+        self.METADATA_UPDATE_THUMB = {
+            "videoStill": {"operation": "UPLOAD_CUSTOM_THUMBNAIL", "image": {
+                "dataUri": ""
+            }}
+        }
+
+

Class variables

+
+
var CLIENT
+
+
+
+
var channelId
+
+
+
+
var sessionToken
+
+
+
+
var videoId
+
+
+
+
+

Methods

+
+
+def setVideoId(self, videoId) +
+
+
+
+ +Expand source code + +
def setVideoId(self, videoId):
+    self.videoId = videoId
+    self._()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/examples/edit_video.py b/examples/edit_video.py new file mode 100644 index 0000000..f0e388a --- /dev/null +++ b/examples/edit_video.py @@ -0,0 +1,38 @@ +from ytstudio import Studio +import asyncio +import json +import os + +if os.path.exists("./login.json"): + LOGIN_FILE = json.loads(open("./login.json", "r")) +else: + exit("can't run example without login json") +yt = Studio(LOGIN_FILE) + + +async def edit_video(): + await yt.login() + sonuc = await yt.editVideo( + video_id="aaaaaaaa", + title="test", # new title + description="test", # new description + privacy="PUBLIC", # new privacy status (PUBLIC, PRIVATE, UNLISTER) + tags=["test", "test2"], # new tags + category=22, # new category + thumb="./test.png", # new thumbnail (png, jpg, jpeg, <2MB) + playlist=["aaaaa", "bbbbbb"], # new playlist + monetization=True, # new monetization status (True, False) + ) + print(f"successfully edited! videoId: {sonuc['videoId']}") + + +async def delete_video(): + await yt.login() + sonuc = await yt.deleteVideo( + video_id="aaaaaaaa", + ) + print(f"successfully deleted! videoId: {sonuc['videoId']}") + +loop = asyncio.get_event_loop() +loop.run_until_complete(edit_video()) +loop.run_until_complete(delete_video()) diff --git a/examples/get_videos.py b/examples/get_videos.py new file mode 100644 index 0000000..4268bae --- /dev/null +++ b/examples/get_videos.py @@ -0,0 +1,34 @@ +from ytstudio import Studio +import asyncio +import json +import os + + +async def get_video_list(): + if os.path.exists("./login.json"): + LOGIN_FILE = json.loads(open("./login.json", "r")) + else: + exit("can't run example without login json") + + yt = Studio(LOGIN_FILE) + + await yt.login() + sonuc = await yt.listVideos() + print(sonuc) + + +async def get_video(): + if os.path.exists("./login.json"): + LOGIN_FILE = json.loads(open("./login.json", "r")) + else: + exit("can't run example without login json") + + yt = Studio(LOGIN_FILE) + + await yt.login() + sonuc = await yt.getVideo("aaaaaaa") + print(sonuc) + +loop = asyncio.get_event_loop() +loop.run_until_complete(get_video()) +loop.run_until_complete(get_video_list()) diff --git a/examples/login.json b/examples/login.json new file mode 100644 index 0000000..908d93c --- /dev/null +++ b/examples/login.json @@ -0,0 +1,15 @@ +{ + "SESSION_TOKEN": "", + "VISITOR_INFO1_LIVE": "", + "PREF": "", + "LOGIN_INFO": "", + "SID": "", + "__Secure-3PSID": "", + "HSID": "", + "SSID": "", + "APISID": "", + "SAPISID": "", + "__Secure-3PAPISID": "", + "YSC": "", + "SIDCC": "" +} \ No newline at end of file diff --git a/examples/upload_video.py b/examples/upload_video.py index ac0dae4..e65565a 100644 --- a/examples/upload_video.py +++ b/examples/upload_video.py @@ -1,20 +1,26 @@ from ytstudio import Studio import asyncio -from pyquery import PyQuery as pq import os +import json def progress(yuklenen, toplam): - #print(f"{yuklenen}/{toplam}", end="\r") + print(f"{round(yuklenen / toplam) * 100}% upload", end="\r") pass +if os.path.exists("./login.json"): + LOGIN_FILE = json.loads(open("./login.json", "r")) +else: + exit("can't run example without login json") + +yt = Studio(LOGIN_FILE) + + async def main(): - yt = Studio({'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN_INFO': '', 'SID': '', '__Secure-3PSID': '', 'HSID': '', - 'SSID': '', 'APISID': '', 'SAPISID': '', '__Secure-3PAPISID': '', 'YSC': '', 'SIDCC': ''}, session_token="") await yt.login() - sonuc = await yt.uploadVideo(os.path.join(os.getcwd(), "deneme.mp4"), progress=progress) - print(sonuc['videoId']) + sonuc = await yt.uploadVideo(os.path.join(os.getcwd(), "test_video.mp4"), progress=progress) + print(f"successfully uploaded! videoId: {sonuc['videoId']}") loop = asyncio.get_event_loop() loop.run_until_complete(main()) diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..f3a7068 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest +pytest-asyncio +pdoc3 \ No newline at end of file diff --git a/run_dev.sh b/run_dev.sh new file mode 100755 index 0000000..54ed394 --- /dev/null +++ b/run_dev.sh @@ -0,0 +1,2 @@ +pdoc --html ytstudio +pytest diff --git a/setup.py b/setup.py index 42f2933..f75bbd9 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,12 @@ import setuptools -import os -required = ["js2py", "aiohttp", "pyquery", "aiofiles"] +required = open("requirements.txt", "r").readlines() long_description = open('README.md').read() setuptools.setup( name='ytstudio', - version='1.0.5', + version='1.5.0', description='Unofficial API for Youtube Studio.', long_description=long_description, author='Yusuf Usta', diff --git a/tests/test_upload_video.py b/tests/test_upload_video.py new file mode 100644 index 0000000..849446d --- /dev/null +++ b/tests/test_upload_video.py @@ -0,0 +1,18 @@ +import pytest +import ytstudio +import json +import os + +if os.path.exists("./login.json"): + LOGIN_FILE = json.loads(open("./login.json", "r")) +else: + exit("can't run test without login json") + +studio = ytstudio.Studio(LOGIN_FILE) + + +@pytest.mark.asyncio +async def test_upload_video(): + await studio.login() + assert 'videoId' in (await studio.uploadVideo(os.path.join( + os.getcwd(), "test.mp4"))) diff --git a/ytstudio/__init__.py b/ytstudio/__init__.py index ac689cf..d95d037 100644 --- a/ytstudio/__init__.py +++ b/ytstudio/__init__.py @@ -9,17 +9,24 @@ import random import os import json +from .templates import Templates +import typing +import pathlib +import base64 class Studio: YT_STUDIO_URL = "https://studio.youtube.com" 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" TRANSFERRED_BYTES = 0 - CHUNK_SIZE = 1024*2 + CHUNK_SIZE = 64*1024 - def __init__(self, cookies: dict = {'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN_INFO': '', 'HSID': '', 'SAPISID': '', 'YSC': '', 'SIDCC': ''}, session_token: str = ""): + def __init__(self, cookies: dict = {'SESSION_TOKEN': '', 'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN_INFO': '', 'SID': '', '__Secure-3PSID': '.', 'HSID': '', + 'SSID': '', 'APISID': '', 'SAPISID': '', '__Secure-3PAPISID': '', 'YSC': '', 'SIDCC': ''}): self.SAPISIDHASH = self.generateSAPISIDHASH(cookies['SAPISID']) - self.Cookie = " ".join([f"{c}={cookies[c]};" for c in cookies.keys()]) + self.cookies = cookies + self.Cookie = " ".join( + [f"{c}={cookies[c]};" if not c == "SESSION_TOKEN" else "" for c in cookies.keys()]) self.HEADERS = { 'Authorization': f'SAPISIDHASH {self.SAPISIDHASH}', 'Content-Type': 'application/json', @@ -31,8 +38,7 @@ def __init__(self, cookies: dict = {'VISITOR_INFO1_LIVE': '', 'PREF': '', 'LOGIN self.loop = asyncio.get_event_loop() self.config = {} self.js = js2py.EvalJs() - self.js.execute("var window = {};") - self.session_token = session_token + self.js.execute("var window = {ytcfg: {}};") def __del__(self): self.loop.create_task(self.session.close()) @@ -47,13 +53,17 @@ async def getMainPage(self) -> str: return await page.text("utf-8") async def login(self) -> bool: + """ + Login to your youtube account + """ page = await self.getMainPage() _ = pq(page) script = _("script") if len(script) < 1: raise Exception("Didn't find script. Can you check your cookies?") script = script[0].text - self.js.execute(f"{script} window.ytcfg = ytcfg;") + self.js.execute( + f"{script} window.ytcfg = ytcfg;") INNERTUBE_API_KEY = self.js.window.ytcfg.data_.INNERTUBE_API_KEY CHANNEL_ID = self.js.window.ytcfg.data_.CHANNEL_ID @@ -64,6 +74,11 @@ async def login(self) -> bool: "Didn't find INNERTUBE_API_KEY or CHANNEL_ID. Can you check your cookies?") self.config = {'INNERTUBE_API_KEY': INNERTUBE_API_KEY, 'CHANNEL_ID': CHANNEL_ID, 'data_': self.js.window.ytcfg.data_} + self.templates = Templates({ + 'channelId': CHANNEL_ID, + 'sessionToken': self.cookies['SESSION_TOKEN'] + }) + return True def generateHash(self) -> str: @@ -91,15 +106,18 @@ def generateHash(self) -> str: async def fileSender(self, file_name): async with aiofiles.open(file_name, 'rb') as f: - while True: - chunk = await f.read(self.CHUNK_SIZE) - if not chunk: - break + chunk = await f.read(self.CHUNK_SIZE) + while chunk: if self.progress != None: self.TRANSFERRED_BYTES += len(chunk) self.progress(self.TRANSFERRED_BYTES, os.path.getsize(file_name)) + + self.TRANSFERRED_BYTES += len(chunk) yield chunk + chunk = await f.read(self.CHUNK_SIZE) + if not chunk: + break async def uploadFileToYoutube(self, upload_url, file_path): self.TRANSFERRED_BYTES = 0 @@ -116,6 +134,9 @@ async def uploadFileToYoutube(self, upload_url, file_path): return _['scottyResourceId'] 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): + """ + Uploads a video to youtube. + """ self.progress = progress frontEndUID = f"innertube_studio:{self.generateHash()}:0" @@ -132,109 +153,135 @@ async def uploadVideo(self, file_name, title=f"New Video {round(time.time())}", uploadUrl = uploadRequest.headers.get("x-goog-upload-url") scottyResourceId = await self.uploadFileToYoutube(uploadUrl, file_name) + _data = self.templates.UPLOAD_VIDEO + _data["resourceId"]["scottyResourceId"]["id"] = scottyResourceId + _data["frontendUploadId"] = frontEndUID + _data["initialMetadata"] = { + "title": { + "newTitle": title + }, + "description": { + "newDescription": description, + "shouldSegment": True + }, + "privacy": { + "newPrivacy": privacy + }, + "draftState": { + "isDraft": draft + } + } upload = await self.session.post( f"https://studio.youtube.com/youtubei/v1/upload/createvideo?alt=json&key={self.config['INNERTUBE_API_KEY']}", - json={ - "channelId": self.config['CHANNEL_ID'], - "resourceId": { - "scottyResourceId": { - "id": scottyResourceId - } - }, - "frontendUploadId": frontEndUID, - "initialMetadata": { - "title": { - "newTitle": title - }, - "description": { - "newDescription": description, - "shouldSegment": True - }, - "privacy": { - "newPrivacy": privacy - }, - "draftState": { - "isDraft": draft - } - }, - "context": { - "client": { - "clientName": 62, - "clientVersion": "1.20201130.03.00", - "hl": "en-GB", - "gl": "PL", - "experimentsToken": "", - "utcOffsetMinutes": 60 - }, - "request": { - "returnLogEntry": True, - "internalExperimentFlags": [], - "sessionInfo": { - "token": self.session_token - } - }, - "user": { - "delegationContext": { - "roleType": { - "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" - }, - "externalChannelId": self.config['CHANNEL_ID'] - }, - "serializedDelegationContext": "", - "onBehalfOfUser": "", - }, - "clientScreenNonce": "" - }, - "delegationContext": { - "roleType": { - "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" - }, - "externalChannelId": self.config['CHANNEL_ID'] - } - } + json=_data ) return await upload.json() async def deleteVideo(self, video_id): + """ + Delete video from your channel + """ + self.templates.setVideoId(video_id) delete = await self.session.post( f"https://studio.youtube.com/youtubei/v1/video/delete?alt=json&key={self.config['INNERTUBE_API_KEY']}", - json={ - "videoId": video_id, - "context": { - "client": { - "clientName": 62, - "clientVersion": "1.20201130.03.00", - "hl": "en-GB", - "gl": "PL", - "experimentsToken": "", - "utcOffsetMinutes": 60 - }, - "request": { - "returnLogEntry": True, - "internalExperimentFlags": [], - "sessionInfo": { - "token": "" - } - }, - "user": { - "delegationContext": { - "roleType": { - "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" - }, - "externalChannelId": self.config['CHANNEL_ID'] - }, - "serializedDelegationContext": "" - }, - "clientScreenNonce": "" - }, - "delegationContext": { - "roleType": { - "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" - }, - "externalChannelId": self.config['CHANNEL_ID'] - } - } + json=self.templates.DELETE_VIDEO ) - return await delete.json() + + async def listVideos(self): + """ + Returns a list of videos in your channel + """ + list = await self.session.post( + f"https://studio.youtube.com/youtubei/v1/creator/list_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}", + json=self.templates.LIST_VIDEOS + ) + return await list.json() + + async def getVideo(self, video_id): + """ + Get video data. + """ + self.templates.setVideoId(video_id) + video = await self.session.post( + f"https://studio.youtube.com/youtubei/v1/creator/get_creator_videos?alt=json&key={self.config['INNERTUBE_API_KEY']}", + json=self.templates.GET_VIDEO + ) + return await video.json() + + async def createPlaylist(self, title, privacy="PUBLIC") -> dict: + """ + Create a new playlist. + """ + _data = self.templates.CREATE_PLAYLIST + _data["title"] = title + _data["privacyStatus"] = privacy + + create = await self.session.post( + f"https://studio.youtube.com/youtubei/v1/playlist/create?alt=json&key={self.config['INNERTUBE_API_KEY']}", + json=_data + ) + return await create.json() + + 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] = []): + """ + Edit video metadata. + """ + self.templates.setVideoId(video_id) + _data = self.templates.METADATA_UPDATE + if title != "": + _title = self.templates.METADATA_UPDATE_TITLE + _title["title"]["newTitle"] = title + _data.update(_title) + + if description != "": + _description = self.templates.METADATA_UPDATE_DESCRIPTION + _description["description"]["newDescription"] = description + _data.update(_description) + + if privacy != "": + _privacy = self.templates.METADATA_UPDATE_PRIVACY + _privacy["privacy"]["newPrivacy"] = privacy + _data.update(_privacy) + + if thumb != "": + _thumb = self.templates.METADATA_UPDATE_THUMB + image = open(thumb, 'rb') + image_64_encode = base64.b64encode(image.read()).decode('utf-8') + + _thumb["videoStill"]["image"][ + "dataUri"] = f"data:image/png;base64,{image_64_encode}" + _data.update(_thumb) + + if len(tags) > 0: + _tags = self.templates.METADATA_UPDATE_TAGS + _tags["tags"]["newTags"] = tags + _data.update(_tags) + + if category != -1: + _category = self.templates.METADATA_UPDATE_CATEGORY + _category["category"]["newCategoryId"] = category + _data.update(_category) + + if len(playlist) > 0: + _playlist = self.templates.METADATA_UPDATE_PLAYLIST + _playlist["addToPlaylist"]["addToPlaylistIds"] = playlist + if len(removeFromPlaylist) > 0: + _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist + _data.update(_playlist) + + if len(removeFromPlaylist) > 0: + _playlist = self.templates.METADATA_UPDATE_PLAYLIST + _playlist["addToPlaylist"]["deleteFromPlaylistIds"] = removeFromPlaylist + _data.update(_playlist) + + _monetization = self.templates.METADATA_UPDATE_MONETIZATION + _monetization["monetizationSettings"]["newMonetization"] = monetization + _data.update(_monetization) + + update = await self.session.post( + f"https://studio.youtube.com/youtubei/v1/video_manager/metadata_update?alt=json&key={self.config['INNERTUBE_API_KEY']}", + json=_data + ) + return await update.json() diff --git a/ytstudio/templates.py b/ytstudio/templates.py new file mode 100644 index 0000000..e42780c --- /dev/null +++ b/ytstudio/templates.py @@ -0,0 +1,610 @@ +class Templates: + channelId = "" + videoId = "" + sessionToken = "" + + CLIENT = { + "clientName": 62, + "clientVersion": "1.20201130.03.00", + "hl": "en-GB", + "gl": "PL", + "experimentsToken": "", + "utcOffsetMinutes": 60 + } + + def __init__(self, config) -> None: + self.config = config + self.channelId = self.config["channelId"] + self.sessionToken = self.config["sessionToken"] + self._() + + def setVideoId(self, videoId): + self.videoId = videoId + self._() + + def _(self): + self.DELETE_VIDEO = { + "videoId": self.videoId, + "context": { + "client": self.CLIENT, + "request": { + "returnLogEntry": True, + "internalExperimentFlags": [], + "sessionInfo": { + "token": self.sessionToken + } + }, + "user": { + "delegationContext": { + "roleType": { + "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" + }, + "externalChannelId": self.channelId + }, + "serializedDelegationContext": "" + }, + "clientScreenNonce": "" + }, + "delegationContext": { + "roleType": { + "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" + }, + "externalChannelId": self.channelId + } + } + + self.UPLOAD_VIDEO = { + "channelId": self.channelId, + "resourceId": { + "scottyResourceId": { + "id": "" + } + }, + "frontendUploadId": "", + "initialMetadata": { + "title": { + "newTitle": "" + }, + "description": { + "newDescription": "", + "shouldSegment": True + }, + "privacy": { + "newPrivacy": "" + }, + "draftState": { + "isDraft": "" + } + }, + "context": { + "client": self.CLIENT, + "request": { + "returnLogEntry": True, + "internalExperimentFlags": [], + "sessionInfo": { + "token": self.sessionToken + } + }, + "user": { + "delegationContext": { + "roleType": { + "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" + }, + "externalChannelId": self.channelId + }, + "serializedDelegationContext": "", + "onBehalfOfUser": "" + }, + "clientScreenNonce": "" + }, + "delegationContext": { + "roleType": { + "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" + }, + "externalChannelId": self.channelId + } + } + + self.METADATA_UPDATE = { + "encryptedVideoId": self.videoId, + "videoReadMask": { + "channelId": True, + "videoId": True, + "lengthSeconds": True, + "premiere": { + "all": True + }, + "status": True, + "thumbnailDetails": { + "all": True + }, + "title": True, + "draftStatus": True, + "downloadUrl": True, + "watchUrl": True, + "permissions": { + "all": True + }, + "timeCreatedSeconds": True, + "timePublishedSeconds": True, + "origin": True, + "livestream": { + "all": True + }, + "privacy": True, + "contentOwnershipModelSettings": { + "all": True + }, + "features": { + "all": True + }, + "responseStatus": { + "all": True + }, + "statusDetails": { + "all": True + }, + "description": True, + "metrics": { + "all": True + }, + "publicLivestream": { + "all": True + }, + "publicPremiere": { + "all": True + }, + "titleFormattedString": { + "all": True + }, + "descriptionFormattedString": { + "all": True + }, + "audienceRestriction": { + "all": True + }, + "monetization": { + "all": True + }, + "selfCertification": { + "all": True + }, + "allRestrictions": { + "all": True + }, + "inlineEditProcessingStatus": True, + "videoPrechecks": { + "all": True + }, + "videoResolutions": { + "all": True + }, + "scheduledPublishingDetails": { + "all": True + }, + "visibility": { + "all": True + }, + "privateShare": { + "all": True + }, + "sponsorsOnly": { + "all": True + }, + "unlistedExpired": True, + "videoTrailers": { + "all": True + } + }, + "context": { + "client": self.CLIENT, + "request": { + "returnLogEntry": True, + "internalExperimentFlags": [], + "sessionInfo": { + "token": self.sessionToken + } + }, + "user": { + "delegationContext": { + "externalChannelId": self.channelId, + "roleType": { + "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" + } + }, + "serializedDelegationContext": "" + }, + "clientScreenNonce": "" + }, + "delegationContext": { + "externalChannelId": self.channelId, + "roleType": { + "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" + } + } + } + + self.METADATA_UPDATE_MONETIZATION = { + "monetizationSettings": { + "newMonetizeWithAds": True + } + } + + self.LIST_VIDEOS = { + "filter": { + "and": { + "operands": [ + { + "channelIdIs": { + "value": self.channelId + } + }, { + "videoOriginIs": { + "value": "VIDEO_ORIGIN_UPLOAD" + } + } + ] + } + }, + "order": "VIDEO_ORDER_DISPLAY_TIME_DESC", + "pageSize": 30, + "mask": { + "channelId": True, + "videoId": True, + "lengthSeconds": True, + "premiere": { + "all": True + }, + "status": True, + "thumbnailDetails": { + "all": True + }, + "title": True, + "draftStatus": True, + "downloadUrl": True, + "watchUrl": True, + "permissions": { + "all": True + }, + "timeCreatedSeconds": True, + "timePublishedSeconds": True, + "origin": True, + "livestream": { + "all": True + }, + "privacy": True, + "contentOwnershipModelSettings": { + "all": True + }, + "features": { + "all": True + }, + "responseStatus": { + "all": True + }, + "statusDetails": { + "all": True + }, + "description": True, + "metrics": { + "all": True + }, + "publicLivestream": { + "all": True + }, + "publicPremiere": { + "all": True + }, + "titleFormattedString": { + "all": True + }, + "descriptionFormattedString": { + "all": True + }, + "audienceRestriction": { + "all": True + }, + "monetization": { + "all": True + }, + "selfCertification": { + "all": True + }, + "allRestrictions": { + "all": True + }, + "inlineEditProcessingStatus": True, + "videoPrechecks": { + "all": True + }, + "videoResolutions": { + "all": True + }, + "scheduledPublishingDetails": { + "all": True + }, + "visibility": { + "all": True + }, + "privateShare": { + "all": True + }, + "sponsorsOnly": { + "all": True + }, + "unlistedExpired": True, + "videoTrailers": { + "all": True + } + }, + "context": { + "client": self.CLIENT, + "request": { + "returnLogEntry": True, + "internalExperimentFlags": [] + }, + "user": { + "delegationContext": { + "externalChannelId": self.channelId, + "roleType": { + "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" + } + }, + "serializedDelegationContext": "" + }, + "clientScreenNonce": "" + } + } + + self.GET_VIDEO = { + "context": { + "client": self.CLIENT, + "request": { + "returnLogEntry": True, + "internalExperimentFlags": [] + }, + "user": { + "delegationContext": { + "externalChannelId": self.channelId, + "roleType": { + "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" + } + }, + "serializedDelegationContext": "" + }, + "clientScreenNonce": "" + }, + "failOnError": True, + "videoIds": [self.videoId], + "mask": { + "downloadUrl": True, + "origin": True, + "premiere": { + "all": True + }, + "privacy": True, + "videoId": True, + "status": True, + "permissions": { + "all": True + }, + "draftStatus": True, + "statusDetails": { + "all": True + }, + "inlineEditProcessingStatus": True, + "selfCertification": { + "all": True + }, + "monetization": { + "all": True + }, + "allRestrictions": { + "all": True + }, + "videoPrechecks": { + "all": True + }, + "audienceRestriction": { + "all": True + }, + "responseStatus": { + "all": True + }, + "features": { + "all": True + }, + "videoAdvertiserSpecificAgeGates": { + "all": True + }, + "claimDetails": { + "all": True + }, + "commentsDisabledInternally": True, + "livestream": { + "all": True + }, + "music": { + "all": True + }, + "ownedClaimDetails": { + "all": True + }, + "timePublishedSeconds": True, + "uncaptionedReason": True, + "remix": { + "all": True + }, + "contentOwnershipModelSettings": { + "all": True + }, + "channelId": True, + "mfkSettings": { + "all": True + }, + "thumbnailEditorState": { + "all": True + }, + "thumbnailDetails": { + "all": True + }, + "scheduledPublishingDetails": { + "all": True + }, + "visibility": { + "all": True + }, + "privateShare": { + "all": True + }, + "sponsorsOnly": { + "all": True + }, + "unlistedExpired": True, + "videoTrailers": { + "all": True + }, + "allowComments": True, + "allowEmbed": True, + "allowRatings": True, + "ageRestriction": True, + "audioLanguage": { + "all": True + }, + "category": True, + "commentFilter": True, + "crowdsourcingEnabled": True, + "dateRecorded": { + "all": True + }, + "defaultCommentSortOrder": True, + "description": True, + "descriptionFormattedString": { + "all": True + }, + "gameTitle": { + "all": True + }, + "license": True, + "liveChat": { + "all": True + }, + "location": { + "all": True + }, + "metadataLanguage": { + "all": True + }, + "paidProductPlacement": True, + "publishing": { + "all": True + }, + "tags": { + "all": True + }, + "title": True, + "titleFormattedString": { + "all": True + }, + "viewCountIsHidden": True, + "autoChapterSettings": { + "all": True + }, + "videoStreamUrl": True, + "videoDurationMs": True, + "videoEditorProject": { + "videoDimensions": { + "all": True + } + }, + "originalFilename": True, + "videoResolutions": { + "all": True + } + }, + "criticalRead": False + } + + self.CREATE_PLAYLIST = { + "title": "", + "privacyStatus": "", + "context": { + "client": self.CLIENT, + "request": { + "returnLogEntry": True, + "internalExperimentFlags": [], + "sessionInfo": { + "token": self.sessionToken + } + }, + "user": { + "delegationContext": { + "externalChannelId": self.channelId, + "roleType": { + "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" + } + }, + "serializedDelegationContext": "" + }, + "clientScreenNonce": "" + }, + "delegationContext": { + "externalChannelId": self.channelId, + "roleType": { + "channelRoleType": "CREATOR_CHANNEL_ROLE_TYPE_OWNER" + } + } + } + + self.METADATA_UPDATE_PLAYLIST = { + "addToPlaylist": { + "addToPlaylistIds": [], + "deleteFromPlaylistIds": [] + } + } + + self.METADATA_UPDATE_TITLE = { + "title": { + "newTitle": "", + "shouldSegment": True + } + } + + self.METADATA_UPDATE_DESCRIPTION = { + "description": { + "newDescription": "", + "shouldSegment": True + } + } + + self.METADATA_UPDATE_TAGS = { + "tags": { + "newTags": [], + "shouldSegment": True + } + } + + self.METADATA_UPDATE_CATEGORY = { + "category": { + "newCategoryId": 0 + } + } + + self.METADATA_UPDATE_COMMENTS = { + "commentOptions": { + "newAllowComments": True, + "newAllowCommentsMode": "ALL_COMMENTS", + "newCanViewRatings": True, + "newDefaultSortOrder": "MDE_COMMENT_SORT_ORDER_TOP" + } + } + + self.METADATA_UPDATE_PRIVACY = { + "privacyState": {"newPrivacy": "PUBLIC"} + } + + self.METADATA_UPDATE_THUMB = { + "videoStill": {"operation": "UPLOAD_CUSTOM_THUMBNAIL", "image": { + "dataUri": "" + }} + }