├── .DS_Store ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── cookies.py └── uploader.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolgaouz/youtube-auto-upload/df3fa700c1c8fc4cdaac37a66838680007930dc0/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .ipynb.../ 3 | .ipynb_checkpoints/Untitled-checkpoint.ipynb 4 | Untitled.ipynb 5 | __pycache__/cookies.cpython-37.pyc 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "venv/bin/python3.7" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WARNING 2 | 3 | - This script is not tested and should be used on your own risk! 4 | 5 | # Youtube Video Uploader 6 | 7 | A script to bypass Youtube Data API's 10.000/day query limit (every video query costs like 1.6k so that's 6 videos per day). 8 | 9 | This script will do the following: 10 | 11 | - Use a slightly changed version of the library in the link here: https://github.com/n8henrie/pycookiecheat, 12 | to get YouTube cookies from user's default Chrome browser. 13 | 14 | - Operate with Selenium Webdriver to automate the video uploading procedure to YouTube. 15 | 16 | # How To 17 | 18 | 1. Create a json file which would hold all the parameters of a YouTube video. 19 | 20 | 2. Create an Uploader object. 21 | 22 | ``` 23 | from uploader import Uploader 24 | 25 | file_path = /path/to/{filename}.json 26 | 27 | uploader = Uploader() 28 | 29 | ``` 30 | 31 | 3. Initiate the upload via passing the json file path to upload_video function 32 | ``` 33 | uploader.upload_video(file_path) 34 | ``` 35 | -------------------------------------------------------------------------------- /cookies.py: -------------------------------------------------------------------------------- 1 | """pycookiecheat.py :: Retrieve and decrypt cookies from Chrome. 2 | 3 | See relevant post at http://n8h.me/HufI1w 4 | 5 | Use your browser's cookies to make grabbing data from login-protected sites 6 | easier. Intended for use with Python Requests http://python-requests.org 7 | 8 | Accepts a URL from which it tries to extract a domain. If you want to force the 9 | domain, just send it the domain you'd like to use instead. 10 | 11 | Adapted from my code at http://n8h.me/HufI1w 12 | """ 13 | 14 | import pathlib 15 | import sqlite3 16 | import sys 17 | import urllib.error 18 | import urllib.parse 19 | from typing import Iterator, Union 20 | 21 | import keyring 22 | from cryptography.hazmat.backends import default_backend 23 | from cryptography.hazmat.primitives.ciphers import Cipher 24 | from cryptography.hazmat.primitives.ciphers.algorithms import AES 25 | from cryptography.hazmat.primitives.ciphers.modes import CBC 26 | from cryptography.hazmat.primitives.hashes import SHA1 27 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 28 | 29 | 30 | def clean(decrypted: bytes) -> str: 31 | r"""Strip padding from decrypted value. 32 | 33 | Remove number indicated by padding 34 | e.g. if last is '\x0e' then ord('\x0e') == 14, so take off 14. 35 | 36 | Args: 37 | decrypted: decrypted value 38 | Returns: 39 | Decrypted stripped of junk padding 40 | 41 | """ 42 | last = decrypted[-1] 43 | if isinstance(last, int): 44 | return decrypted[:-last].decode("utf8") 45 | return decrypted[: -ord(last)].decode("utf8") 46 | 47 | 48 | def chrome_decrypt( 49 | encrypted_value: bytes, key: bytes, init_vector: bytes 50 | ) -> str: 51 | """Decrypt Chrome/Chromium's encrypted cookies. 52 | 53 | Args: 54 | encrypted_value: Encrypted cookie from Chrome/Chromium's cookie file 55 | key: Key to decrypt encrypted_value 56 | init_vector: Initialization vector for decrypting encrypted_value 57 | Returns: 58 | Decrypted value of encrypted_value 59 | 60 | """ 61 | # Encrypted cookies should be prefixed with 'v10' or 'v11' according to the 62 | # Chromium code. Strip it off. 63 | encrypted_value = encrypted_value[3:] 64 | 65 | cipher = Cipher( 66 | algorithm=AES(key), mode=CBC(init_vector), backend=default_backend() 67 | ) 68 | decryptor = cipher.decryptor() 69 | decrypted = decryptor.update(encrypted_value) + decryptor.finalize() 70 | 71 | return clean(decrypted) 72 | 73 | 74 | def get_osx_config(browser: str) -> dict: 75 | """Get settings for getting Chrome/Chromium cookies on OSX. 76 | 77 | Args: 78 | browser: Either "Chrome" or "Chromium" 79 | Returns: 80 | Config dictionary for Chrome/Chromium cookie decryption 81 | 82 | """ 83 | # Verify supported browser, fail early otherwise 84 | if browser.lower() == "chrome": 85 | cookie_file = ( 86 | "~/Library/Application Support/Google/Chrome/Default/" "Cookies" 87 | ) 88 | elif browser.lower() == "chromium": 89 | cookie_file = "~/Library/Application Support/Chromium/Default/Cookies" 90 | else: 91 | raise ValueError("Browser must be either Chrome or Chromium.") 92 | 93 | config = { 94 | "my_pass": keyring.get_password( 95 | "{} Safe Storage".format(browser), browser 96 | ), 97 | "iterations": 1003, 98 | "cookie_file": cookie_file, 99 | } 100 | return config 101 | 102 | 103 | def get_linux_config(browser: str) -> dict: 104 | """Get the settings for Chrome/Chromium cookies on Linux. 105 | 106 | Args: 107 | browser: Either "Chrome" or "Chromium" 108 | Returns: 109 | Config dictionary for Chrome/Chromium cookie decryption 110 | 111 | """ 112 | # Verify supported browser, fail early otherwise 113 | if browser.lower() == "chrome": 114 | cookie_file = "~/.config/google-chrome/Default/Cookies" 115 | elif browser.lower() == "chromium": 116 | cookie_file = "~/.config/chromium/Default/Cookies" 117 | else: 118 | raise ValueError("Browser must be either Chrome or Chromium.") 119 | 120 | # Set the default linux password 121 | config = { 122 | "my_pass": "peanuts", 123 | "iterations": 1, 124 | "cookie_file": cookie_file, 125 | } 126 | 127 | # Try to get pass from Gnome / libsecret if it seems available 128 | # https://github.com/n8henrie/pycookiecheat/issues/12 129 | pass_found = False 130 | try: 131 | import gi 132 | 133 | gi.require_version("Secret", "1") 134 | from gi.repository import Secret 135 | except ImportError: 136 | pass 137 | else: 138 | flags = Secret.ServiceFlags.LOAD_COLLECTIONS 139 | service = Secret.Service.get_sync(flags) 140 | 141 | gnome_keyring = service.get_collections() 142 | unlocked_keyrings = service.unlock_sync(gnome_keyring).unlocked 143 | 144 | keyring_name = "{} Safe Storage".format(browser.capitalize()) 145 | 146 | for unlocked_keyring in unlocked_keyrings: 147 | for item in unlocked_keyring.get_items(): 148 | if item.get_label() == keyring_name: 149 | item.load_secret_sync() 150 | config["my_pass"] = item.get_secret().get_text() 151 | pass_found = True 152 | break 153 | else: 154 | # Inner loop didn't `break`, keep looking 155 | continue 156 | 157 | # Inner loop did `break`, so `break` outer loop 158 | break 159 | 160 | # Try to get pass from keyring, which should support KDE / KWallet 161 | # if dbus-python is installed. 162 | if not pass_found: 163 | try: 164 | my_pass = keyring.get_password( 165 | "{} Keys".format(browser), "{} Safe Storage".format(browser) 166 | ) 167 | except RuntimeError: 168 | pass 169 | else: 170 | if my_pass: 171 | config["my_pass"] = my_pass 172 | 173 | return config 174 | 175 | 176 | def chrome_cookies( 177 | url: str, 178 | cookie_file: str = None, 179 | browser: str = "Chrome", 180 | curl_cookie_file: str = None, 181 | password: Union[bytes, str] = None, 182 | ) -> dict: 183 | """Retrieve cookies from Chrome/Chromium on OSX or Linux. 184 | 185 | Args: 186 | url: Domain from which to retrieve cookies, starting with http(s) 187 | cookie_file: Path to alternate file to search for cookies 188 | browser: Name of the browser's cookies to read ('Chrome' or 'Chromium') 189 | curl_cookie_file: Path to save the cookie file to be used with cURL 190 | password: Optional system password 191 | Returns: 192 | Dictionary of cookie values for URL 193 | 194 | """ 195 | # If running Chrome on OSX 196 | if sys.platform == "darwin": 197 | config = get_osx_config(browser) 198 | elif sys.platform.startswith("linux"): 199 | config = get_linux_config(browser) 200 | else: 201 | raise OSError("This script only works on OSX or Linux.") 202 | 203 | config.update( 204 | {"init_vector": b" " * 16, "length": 16, "salt": b"saltysalt"} 205 | ) 206 | 207 | if cookie_file: 208 | cookie_file = str(pathlib.Path(cookie_file).expanduser()) 209 | else: 210 | cookie_file = str(pathlib.Path(config["cookie_file"]).expanduser()) 211 | 212 | if isinstance(password, bytes): 213 | config["my_pass"] = password 214 | elif isinstance(password, str): 215 | config["my_pass"] = password.encode("utf8") 216 | elif isinstance(config["my_pass"], str): 217 | config["my_pass"] = config["my_pass"].encode("utf8") 218 | 219 | kdf = PBKDF2HMAC( 220 | algorithm=SHA1(), 221 | backend=default_backend(), 222 | iterations=config["iterations"], 223 | length=config["length"], 224 | salt=config["salt"], 225 | ) 226 | enc_key = kdf.derive(config["my_pass"]) 227 | 228 | parsed_url = urllib.parse.urlparse(url) 229 | if parsed_url.scheme: 230 | domain = parsed_url.netloc 231 | else: 232 | raise urllib.error.URLError("You must include a scheme with your URL.") 233 | 234 | try: 235 | conn = sqlite3.connect(cookie_file) 236 | except sqlite3.OperationalError: 237 | print("Unable to connect to cookie_file at: {}\n".format(cookie_file)) 238 | raise 239 | 240 | # Check whether the column name is `secure` or `is_secure` 241 | secure_column_name = "is_secure" 242 | for ( 243 | sl_no, 244 | column_name, 245 | data_type, 246 | is_null, 247 | default_val, 248 | pk, 249 | ) in conn.execute("PRAGMA table_info(cookies)"): 250 | if column_name == "secure": 251 | secure_column_name = "secure" 252 | break 253 | 254 | sql = ( 255 | "select host_key, path, " 256 | + secure_column_name 257 | + ", expires_utc, name, value, encrypted_value " 258 | "from cookies where host_key like ?" 259 | ) 260 | 261 | # here is some change, return a list instead of a dictionary 262 | cookies = [] 263 | curl_cookies = [] 264 | 265 | for host_key in generate_host_keys(domain): 266 | for ( 267 | hk, 268 | path, 269 | is_secure, 270 | expires_utc, 271 | cookie_key, 272 | val, 273 | enc_val, 274 | ) in conn.execute(sql, (host_key,)): 275 | # if there is a not encrypted value or if the encrypted value 276 | # doesn't start with the 'v1[01]' prefix, return v 277 | if val or (enc_val[:3] not in (b"v10", b"v11")): 278 | pass 279 | else: 280 | val = chrome_decrypt( 281 | enc_val, key=enc_key, init_vector=config["init_vector"] 282 | ) 283 | # create a temporary dictionary for each cookie 284 | tmp = {} 285 | # edit in the way that we can pass it directly to selenium webdriver 286 | tmp['name'] = cookie_key 287 | tmp['value'] = val 288 | tmp['domain'] = host_key 289 | tmp['secure'] = bool(is_secure) 290 | tmp['expiry'] = expires_utc 291 | tmp['path'] = path 292 | # append it to the list 293 | cookies.append(tmp) 294 | if curl_cookie_file: 295 | # http://www.cookiecentral.com/faq/#3.5 296 | curl_cookies.append( 297 | "\t".join( 298 | [ 299 | hk, 300 | "TRUE", 301 | path, 302 | "TRUE" if is_secure else "FALSE", 303 | str(expires_utc), 304 | cookie_key, 305 | val, 306 | ] 307 | ) 308 | ) 309 | 310 | conn.rollback() 311 | 312 | # Save the file to destination 313 | if curl_cookie_file: 314 | with open(curl_cookie_file, "w") as text_file: 315 | text_file.write("\n".join(curl_cookies) + "\n") 316 | 317 | return cookies 318 | 319 | 320 | def generate_host_keys(hostname: str) -> Iterator[str]: 321 | """Yield Chrome/Chromium keys for `hostname`, from least to most specific. 322 | 323 | Given a hostname like foo.example.com, this yields the key sequence: 324 | 325 | example.com 326 | .example.com 327 | foo.example.com 328 | .foo.example.com 329 | 330 | """ 331 | labels = hostname.split(".") 332 | for i in range(2, len(labels) + 1): 333 | domain = ".".join(labels[-i:]) 334 | yield domain 335 | yield "." + domain 336 | -------------------------------------------------------------------------------- /uploader.py: -------------------------------------------------------------------------------- 1 | import json 2 | from selenium.webdriver.support.ui import WebDriverWait 3 | from selenium.webdriver.support import expected_conditions as EC 4 | from selenium.webdriver.common.by import By 5 | from selenium.common.exceptions import TimeoutException 6 | from selenium.webdriver.common.keys import Keys 7 | from selenium.webdriver.chrome.options import Options 8 | from selenium import webdriver 9 | from cookies import chrome_cookies 10 | 11 | class Uploader(): 12 | 13 | """ 14 | 15 | Uploader object which would handle all the upload functions 16 | as well as browser automation via Selenium 17 | 18 | 19 | """ 20 | def init_video(self,filepath): 21 | 22 | """ 23 | 24 | A function to read below parameters from json file: 25 | Title *Required 26 | Description, 27 | Tags, 28 | Category, 29 | Age Restrictions 30 | 31 | Which are held in a json file. Initiate an object with passing the 32 | file path as the parameter. 33 | 34 | """ 35 | 36 | # If only the filename is given, add the extension 37 | if not ('.json' in filepath): 38 | filepath+='.json' 39 | with open(filepath,'w') as fl: 40 | data = json.load(fl) 41 | return data 42 | 43 | def __init__(self,visible=False): 44 | # Initialize the browser object 45 | chrome_options = Options() 46 | chrome_options.add_argument("--disable-gpu") 47 | if visible: 48 | chrome_options.add_argument("--headless") 49 | chrome_options.add_argument("--window-size=1280,800") 50 | self.browser = webdriver.Chrome(chrome_options=chrome_options) 51 | 52 | def get_cookies(self): 53 | 54 | """ 55 | 56 | Gets cookies from user's default chrome browser, 57 | User has to be logged in to YouTube in default Chrome Browser 58 | for this function to return cookies. 59 | 60 | """ 61 | self.cookies = chrome_cookies('https://www.youtube.com') 62 | return cookies 63 | 64 | def inject_cookies(self): 65 | 66 | """ 67 | 68 | Inject cookies to the browser object 69 | 70 | """ 71 | # Go to youtube.com so that we can inject the cookies 72 | self.browser.get('https://www.youtube.com') 73 | 74 | # inject cookies to the Chrome Browser that is used by Selenium 75 | for c in cookies: 76 | # set expiry date to infinity or something 77 | c['expiry'] = 33333333333 78 | self.browser.add_cookie(c) 79 | 80 | def upload_video(self,filepath): 81 | 82 | """ 83 | 84 | Gets video data, then uploads it by automating clicks and actions on Selenium 85 | WebDriver object. 86 | 87 | """ 88 | # read video data 89 | data = self.init_video(filepath) 90 | # if title is not in data, return immediately 91 | if 'title' not in data.keys(): 92 | raise Exception('Field Required (title and video_path are required)') 93 | # navigate to the upload page 94 | browser.get('https://studio.youtube.com/') 95 | # click on 'Create' button 96 | browser.find_element_by_id('create-icon').click() 97 | # Click on 'Upload Video' 98 | browser.find_element_by_css_selector('paper-listbox > paper-item').click() 99 | # Get Input Element 100 | browser.find_element_by_css_selector('input[name="Filedata"]').send_keys(data['video_path']) 101 | # Title 102 | browser.find_element_by_id('textbox').send_keys(data.title) 103 | # Desc 104 | browser.find_element_by_class_name('description-textarea').find_element_by_css_selector('div#textbox').send_keys(data.desc) 105 | 106 | ## TODO: Add Other functionalities here! 107 | ## TODO: Add info about video upload here! 108 | 109 | # Next 110 | browser.find_element_by_id('next-button').click() 111 | browser.find_element_by_id('next-button').click() 112 | 113 | visibilities = ['PUBLIC','UNLISTED','PRIVATE'] 114 | browser.find_element_by_css_selector('paper-radio-button[name="'+visibilities[0]+'"]').click() 115 | 116 | # Done 117 | browser.find_element_by_id('done-button').click() 118 | --------------------------------------------------------------------------------