├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── main.py ├── poetry.lock ├── pyproject.toml ├── src ├── login.py └── upload.py └── uploads └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | geckodriver.log 3 | login-cookies.json 4 | __pycache__/ 5 | uploads/**/* 6 | !uploads/.gitkeep 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Christian C., Moritz M., Luca S. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Logo 3 |
4 |
5 | MIT License 6 | Code style: black 7 |
8 | Platform: YouTube 9 | Uses Docker 10 | Automation supporting Firefox and Chrome 11 |
12 | Firefox supported 13 | Chrome supported 14 |
15 |
16 | An automated, headless YouTube Uploader 17 |
18 |
19 | Authors: 20 | Christian C., 21 | Moritz M., 22 | Luca S. 23 | 24 |
25 | Related Projects: 26 | YouTube Watcher, 27 | Twitch Compilation Creator, 28 | Neural Networks 29 | 30 |

31 | 32 | 33 |
34 | 35 | ## About 36 | 37 | This project aims to automate the upload process for YouTube Videos. Since videos can only be publicly uploaded through the [YouTube Data API](https://developers.google.com/youtube/v3) by using a [Google Workspaces Account](https://workspace.google.com/) (not free!), we decided to create a headless uploader using [Selenium](https://www.selenium.dev/) and [Docker](https://www.docker.com/). This approach also bypasses API restrictions (e.g. Rate Limits/Endcards can't be set through the API). 38 | 39 | *Note: Because the upload process is often updated by Google, the code might not work when you try it! Often, there are only minor changes that have to be made. If you find yourself in this situation, please open an [Issue](https://github.com/ContentAutomation/YouTubeUploader/issues) or provide a quick fix in form of a [Pull Request](https://github.com/ContentAutomation/YouTubeUploader/pulls) to make sure that the codebase stays up to date!* 40 | 41 | **This project is for educational purposes only. Automating video uploads to YouTube with automation software might be against [YouTube's Terms of Service](https://www.youtube.com/static?template=terms). Even though our tests went smoothly, one might encounter problems when using the YouTube Uploader extensively.** 42 | 43 | ## Setup 44 | 45 | ### Dockerized Browser 46 | To run the uploader in a headless mode, it needs to connect to a docker container. To test the uploader locally without using docker, this section can be skipped. Otherwise, the docker container can be started by executing the following steps: 47 | 1. Install [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) 48 | 49 | *Note: On Windows and Mac, docker-compose is already installed when installing docker.* 50 | 51 | 2. Clone/Download this repository 52 | 3. Navigate to the root of the repository 53 | 4. Run ```docker-compose up``` to start the docker container (append ```-d``` if you want to run it in a detached mode) 54 | 55 | *Note: Selenium can now connect to the browser via port 4444. In Python the connection can be established with the following command.* 56 | 57 | ```python 58 | driver = webdriver.Remote( 59 | command_executor="http://127.0.0.1:4444/wd/hub", 60 | desired_capabilities=DesiredCapabilities.FIREFOX, 61 | ) 62 | ``` 63 | 64 | *See `main.py` for more information.* 65 | 66 | 6. Continue with the [YouTube Uploader Setup](#setup-uploader) 67 | 68 | ### YouTube Uploader 69 | 70 | 71 | This project requires [Poetry](https://python-poetry.org/) to install the required dependencies. 72 | Check out [this link](https://python-poetry.org/docs/) to install Poetry on your operating system. 73 | 74 | Make sure you have installed [Python](https://www.python.org/downloads/) 3.8! Otherwise Step 3 will let you know that you have no compatible Python version installed. 75 | 76 | 1. Clone/Download this repository 77 | 2. Navigate to the root of the repository 78 | 3. Run ```poetry install``` to create a virtual environment with Poetry 79 | 4. In a browser of your choice, login into the YouTube account that you want to use the uploader with 80 | 5. Use a cookie extraction tool to extract the YouTube cookies into a JSON file (for example [EditThisCookie](https://chrome.google.com/webstore/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg) [Chrome] or [Cookie Quick Manager](https://addons.mozilla.org/en-US/firefox/addon/cookie-quick-manager/) [Firefox]) 81 | 82 | *Note: This is required so that the uploader is automatically logged in into the YouTube account using the cookies. Performing a Google login through automated software is extremely hard due to Google's bot detection/Login safety features* 83 | 84 | 7. Either run the dockerized Browser with `docker-compose up`, install [geckodriver](https://github.com/mozilla/geckodriver/releases) for a local Firefox or [ChromeDriver](https://chromedriver.chromium.org/downloads) for Chromium. Ensure that geckodriver/ChromeDriver are in a location in your `$PATH`. 85 | 8. Run ```poetry run python main.py``` to run the program. Alternatively you can run ```poetry shell``` followed by ```python main.py```. By default this connects to the dockerized Firefox Browser (headless). To automate a different Browser (not-headless) use the `--browser [chrome/firefox]` command line option. 86 | 87 | *Note: When using Docker, the video that should be uploaded needs to be in the repository's ```uploads``` folder. This is because ```REPOSITORY_ROOT/uploads/``` is mounted to ```/uploads/``` in the Docker container. Therefore, the ```video_path``` argument has to be passed in the following format: ```/uploads/VIDEO_FILE_NAME.xxx```* 88 | 89 | ## Run Parameters 90 | You can also get these definitions by running ```main.py --help``` 91 | 92 | ``` 93 | usage: main.py [-h] [-B {docker,chrome,firefox}] -l LOGIN_COOKIES [--thumbnail-path THUMBNAIL] -t TITLE -d DESCRIPTION [-g GAME] [-k KIDS] [-ut UPLOAD_TIME] video_path 94 | 95 | positional arguments: 96 | video_path Path to the video file. When using docker, this path has to be inside the container (default mount is /uploads/). 97 | 98 | optional arguments: 99 | -h, --help show this help message and exit 100 | -B {docker,chrome,firefox}, --browser {docker,chrome,firefox} 101 | Select the driver/browser to use for executing the script (default: docker). 102 | -l LOGIN_COOKIES, --login-cookies-path LOGIN_COOKIES 103 | A json file that contains the cookies required to sign into YouTube in the target browser. 104 | --thumbnail-path THUMBNAIL, -T THUMBNAIL 105 | Path to the thumbnail file (default: None). 106 | -t TITLE, --title TITLE 107 | This argument declares the title of the uploaded video. 108 | -d DESCRIPTION, --description DESCRIPTION 109 | This argument declares the description of the uploaded video. 110 | -g GAME, --game GAME This argument declares the game of the uploaded video (default: None). 111 | -k KIDS, --kids KIDS Whether the video is made for kids or not. (default: False) 112 | -ut UPLOAD_TIME, --upload_time UPLOAD_TIME 113 | This argument declares the scheduled upload time (UTC) of the uploaded video. (Example: 2021-04-04T20:00:00) 114 | ``` 115 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | firefox: 4 | # Use the official standalone selenium image for Firefox 5 | image: selenium/standalone-firefox 6 | # Mounting /dev/shm is a workaround for browser crashes inside docker. See https://github.com/SeleniumHQ/docker-selenium#quick-start 7 | volumes: 8 | - /dev/shm:/dev/shm 9 | - ./uploads/:/uploads/:ro 10 | # Selenium port we connect to from python 11 | ports: 12 | - "4444:4444" -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | from argparse import ArgumentError 4 | from datetime import datetime 5 | 6 | from selenium import webdriver 7 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 8 | from selenium.webdriver.remote.file_detector import LocalFileDetector 9 | 10 | from src.login import confirm_logged_in, login_using_cookie_file 11 | from src.upload import upload_file 12 | 13 | 14 | def main(): 15 | logging.getLogger().setLevel(logging.INFO) 16 | 17 | # Setup Selenium web driver 18 | parser = get_arg_parser() 19 | args = parser.parse_args() 20 | 21 | if args.browser == "docker": 22 | driver = webdriver.Remote( 23 | command_executor="http://127.0.0.1:4444/wd/hub", 24 | desired_capabilities=DesiredCapabilities.FIREFOX, 25 | ) 26 | elif args.browser == "firefox": 27 | firefox_profile = webdriver.FirefoxProfile() 28 | firefox_profile.set_preference("intl.accept_languages", "en-us") 29 | firefox_profile.update_preferences() 30 | driver = webdriver.Firefox(firefox_profile) 31 | elif args.browser == "chrome": 32 | driver = webdriver.Chrome() 33 | else: 34 | raise ArgumentError(message="Unknown driver.") 35 | 36 | driver.set_window_size(1920, 1080) 37 | login_using_cookie_file(driver, cookie_file=args.login_cookies) 38 | driver.get("https://www.youtube.com") 39 | 40 | assert "YouTube" in driver.title 41 | 42 | try: 43 | confirm_logged_in(driver) 44 | driver.get("https://studio.youtube.com") 45 | assert "Channel dashboard" in driver.title 46 | driver.file_detector = LocalFileDetector() 47 | upload_file( 48 | driver, 49 | video_path=args.video_path, 50 | title=args.title, 51 | thumbnail_path=args.thumbnail, 52 | description=args.description, 53 | game=args.game, 54 | kids=args.kids, 55 | upload_time=args.upload_time, 56 | ) 57 | except: 58 | driver.close() 59 | raise 60 | 61 | 62 | def get_arg_parser() -> argparse.ArgumentParser: 63 | parser = argparse.ArgumentParser() 64 | today = datetime.now() 65 | parser.add_argument( 66 | "-B", 67 | "--browser", 68 | choices=["docker", "chrome", "firefox"], 69 | default="docker", 70 | type=str, 71 | help="Select the driver/browser to use for executing the script (default: docker).", 72 | ) 73 | parser.add_argument( 74 | "-l", 75 | "--login-cookies-path", 76 | dest="login_cookies", 77 | type=str, 78 | help="A json file that contains the cookies required to sign into YouTube in the target browser.", 79 | required=True, 80 | ) 81 | parser.add_argument( 82 | "video_path", 83 | help="Path to the video file. When using docker, this path has to be inside the container " 84 | "(default mount is /uploads/).", 85 | ) 86 | parser.add_argument( 87 | "--thumbnail-path", 88 | "-T", 89 | help="Path to the thumbnail file (default: None).", 90 | dest="thumbnail", 91 | type=str, 92 | default=None, 93 | required=False, 94 | ) 95 | parser.add_argument( 96 | "-t", 97 | "--title", 98 | help="This argument declares the title of the uploaded video.", 99 | type=str, 100 | required=True, 101 | ) 102 | parser.add_argument( 103 | "-d", 104 | "--description", 105 | help="This argument declares the description of the uploaded video.", 106 | type=str, 107 | required=True, 108 | ) 109 | parser.add_argument( 110 | "-g", 111 | "--game", 112 | help="This argument declares the game of the uploaded video (default: None).", 113 | default=None, 114 | required=False, 115 | ) 116 | parser.add_argument( 117 | "-k", 118 | "--kids", 119 | help="Whether the video is made for kids or not. (default: False)", 120 | required=False, 121 | type=bool, 122 | default=False, 123 | ) 124 | parser.add_argument( 125 | "-ut", 126 | "--upload_time", 127 | help="This argument declares the scheduled upload time (UTC) of the uploaded video. " 128 | "(Example: 2021-04-04T20:00:00)", 129 | required=False, 130 | type=datetime.fromisoformat, 131 | default=datetime(today.year, today.month, today.day, 20, 15), 132 | ) 133 | return parser 134 | 135 | 136 | if __name__ == "__main__": 137 | main() 138 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "black" 11 | version = "20.8b1" 12 | description = "The uncompromising code formatter." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=3.6" 16 | 17 | [package.dependencies] 18 | regex = ">=2020.1.8" 19 | mypy-extensions = ">=0.4.3" 20 | typed-ast = ">=1.4.0" 21 | toml = ">=0.10.1" 22 | typing-extensions = ">=3.7.4" 23 | pathspec = ">=0.6,<1" 24 | click = ">=7.1.2" 25 | appdirs = "*" 26 | 27 | [package.extras] 28 | colorama = ["colorama (>=0.4.3)"] 29 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 30 | 31 | [[package]] 32 | name = "cfgv" 33 | version = "3.2.0" 34 | description = "Validate configuration and produce human readable error messages." 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=3.6.1" 38 | 39 | [[package]] 40 | name = "click" 41 | version = "7.1.2" 42 | description = "Composable command line interface toolkit" 43 | category = "dev" 44 | optional = false 45 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 46 | 47 | [[package]] 48 | name = "distlib" 49 | version = "0.3.1" 50 | description = "Distribution utilities" 51 | category = "dev" 52 | optional = false 53 | python-versions = "*" 54 | 55 | [[package]] 56 | name = "filelock" 57 | version = "3.0.12" 58 | description = "A platform independent file lock." 59 | category = "dev" 60 | optional = false 61 | python-versions = "*" 62 | 63 | [[package]] 64 | name = "identify" 65 | version = "2.2.2" 66 | description = "File identification library for Python" 67 | category = "dev" 68 | optional = false 69 | python-versions = ">=3.6.1" 70 | 71 | [package.extras] 72 | license = ["editdistance-s"] 73 | 74 | [[package]] 75 | name = "mypy-extensions" 76 | version = "0.4.3" 77 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 78 | category = "dev" 79 | optional = false 80 | python-versions = "*" 81 | 82 | [[package]] 83 | name = "nodeenv" 84 | version = "1.5.0" 85 | description = "Node.js virtual environment builder" 86 | category = "dev" 87 | optional = false 88 | python-versions = "*" 89 | 90 | [[package]] 91 | name = "pathspec" 92 | version = "0.8.1" 93 | description = "Utility library for gitignore style pattern matching of file paths." 94 | category = "dev" 95 | optional = false 96 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 97 | 98 | [[package]] 99 | name = "pre-commit" 100 | version = "2.11.1" 101 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 102 | category = "dev" 103 | optional = false 104 | python-versions = ">=3.6.1" 105 | 106 | [package.dependencies] 107 | virtualenv = ">=20.0.8" 108 | pyyaml = ">=5.1" 109 | cfgv = ">=2.0.0" 110 | nodeenv = ">=0.11.1" 111 | identify = ">=1.0.0" 112 | toml = "*" 113 | 114 | [[package]] 115 | name = "pyyaml" 116 | version = "5.4.1" 117 | description = "YAML parser and emitter for Python" 118 | category = "dev" 119 | optional = false 120 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 121 | 122 | [[package]] 123 | name = "regex" 124 | version = "2021.3.17" 125 | description = "Alternative regular expression module, to replace re." 126 | category = "dev" 127 | optional = false 128 | python-versions = "*" 129 | 130 | [[package]] 131 | name = "selenium" 132 | version = "3.141.0" 133 | description = "Python bindings for Selenium" 134 | category = "main" 135 | optional = false 136 | python-versions = "*" 137 | 138 | [package.dependencies] 139 | urllib3 = "*" 140 | 141 | [[package]] 142 | name = "six" 143 | version = "1.15.0" 144 | description = "Python 2 and 3 compatibility utilities" 145 | category = "dev" 146 | optional = false 147 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 148 | 149 | [[package]] 150 | name = "toml" 151 | version = "0.10.2" 152 | description = "Python Library for Tom's Obvious, Minimal Language" 153 | category = "dev" 154 | optional = false 155 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 156 | 157 | [[package]] 158 | name = "typed-ast" 159 | version = "1.4.2" 160 | description = "a fork of Python 2 and 3 ast modules with type comment support" 161 | category = "dev" 162 | optional = false 163 | python-versions = "*" 164 | 165 | [[package]] 166 | name = "typing-extensions" 167 | version = "3.7.4.3" 168 | description = "Backported and Experimental Type Hints for Python 3.5+" 169 | category = "dev" 170 | optional = false 171 | python-versions = "*" 172 | 173 | [[package]] 174 | name = "urllib3" 175 | version = "1.26.4" 176 | description = "HTTP library with thread-safe connection pooling, file post, and more." 177 | category = "main" 178 | optional = false 179 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 180 | 181 | [package.extras] 182 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 183 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 184 | brotli = ["brotlipy (>=0.6.0)"] 185 | 186 | [[package]] 187 | name = "virtualenv" 188 | version = "20.4.3" 189 | description = "Virtual Python Environment builder" 190 | category = "dev" 191 | optional = false 192 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 193 | 194 | [package.dependencies] 195 | distlib = ">=0.3.1,<1" 196 | filelock = ">=3.0.0,<4" 197 | six = ">=1.9.0,<2" 198 | appdirs = ">=1.4.3,<2" 199 | 200 | [package.extras] 201 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] 202 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] 203 | 204 | [metadata] 205 | lock-version = "1.1" 206 | python-versions = "^3.8" 207 | content-hash = "335acd0b12484d5cd0394b6c8af2b2f0a3f3015064b884630db967928dd0773e" 208 | 209 | [metadata.files] 210 | appdirs = [ 211 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 212 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 213 | ] 214 | black = [ 215 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 216 | ] 217 | cfgv = [ 218 | {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, 219 | {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"}, 220 | ] 221 | click = [ 222 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 223 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 224 | ] 225 | distlib = [ 226 | {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, 227 | {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, 228 | ] 229 | filelock = [ 230 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 231 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 232 | ] 233 | identify = [ 234 | {file = "identify-2.2.2-py2.py3-none-any.whl", hash = "sha256:c7c0f590526008911ccc5ceee6ed7b085cbc92f7b6591d0ee5913a130ad64034"}, 235 | {file = "identify-2.2.2.tar.gz", hash = "sha256:43cb1965e84cdd247e875dec6d13332ef5be355ddc16776396d98089b9053d87"}, 236 | ] 237 | mypy-extensions = [ 238 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 239 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 240 | ] 241 | nodeenv = [ 242 | {file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"}, 243 | {file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"}, 244 | ] 245 | pathspec = [ 246 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 247 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 248 | ] 249 | pre-commit = [ 250 | {file = "pre_commit-2.11.1-py2.py3-none-any.whl", hash = "sha256:94c82f1bf5899d56edb1d926732f4e75a7df29a0c8c092559c77420c9d62428b"}, 251 | {file = "pre_commit-2.11.1.tar.gz", hash = "sha256:de55c5c72ce80d79106e48beb1b54104d16495ce7f95b0c7b13d4784193a00af"}, 252 | ] 253 | pyyaml = [ 254 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 255 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 256 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 257 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 258 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 259 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 260 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 261 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 262 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 263 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 264 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 265 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 266 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 267 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 268 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 269 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 270 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 271 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 272 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 273 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 274 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 275 | ] 276 | regex = [ 277 | {file = "regex-2021.3.17-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b97ec5d299c10d96617cc851b2e0f81ba5d9d6248413cd374ef7f3a8871ee4a6"}, 278 | {file = "regex-2021.3.17-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cb4ee827857a5ad9b8ae34d3c8cc51151cb4a3fe082c12ec20ec73e63cc7c6f0"}, 279 | {file = "regex-2021.3.17-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:633497504e2a485a70a3268d4fc403fe3063a50a50eed1039083e9471ad0101c"}, 280 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a59a2ee329b3de764b21495d78c92ab00b4ea79acef0f7ae8c1067f773570afa"}, 281 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f85d6f41e34f6a2d1607e312820971872944f1661a73d33e1e82d35ea3305e14"}, 282 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4651f839dbde0816798e698626af6a2469eee6d9964824bb5386091255a1694f"}, 283 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:39c44532d0e4f1639a89e52355b949573e1e2c5116106a395642cbbae0ff9bcd"}, 284 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3d9a7e215e02bd7646a91fb8bcba30bc55fd42a719d6b35cf80e5bae31d9134e"}, 285 | {file = "regex-2021.3.17-cp36-cp36m-win32.whl", hash = "sha256:159fac1a4731409c830d32913f13f68346d6b8e39650ed5d704a9ce2f9ef9cb3"}, 286 | {file = "regex-2021.3.17-cp36-cp36m-win_amd64.whl", hash = "sha256:13f50969028e81765ed2a1c5fcfdc246c245cf8d47986d5172e82ab1a0c42ee5"}, 287 | {file = "regex-2021.3.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9d8d286c53fe0cbc6d20bf3d583cabcd1499d89034524e3b94c93a5ab85ca90"}, 288 | {file = "regex-2021.3.17-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:201e2619a77b21a7780580ab7b5ce43835e242d3e20fef50f66a8df0542e437f"}, 289 | {file = "regex-2021.3.17-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d47d359545b0ccad29d572ecd52c9da945de7cd6cf9c0cfcb0269f76d3555689"}, 290 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ea2f41445852c660ba7c3ebf7d70b3779b20d9ca8ba54485a17740db49f46932"}, 291 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:486a5f8e11e1f5bbfcad87f7c7745eb14796642323e7e1829a331f87a713daaa"}, 292 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:18e25e0afe1cf0f62781a150c1454b2113785401ba285c745acf10c8ca8917df"}, 293 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:a2ee026f4156789df8644d23ef423e6194fad0bc53575534101bb1de5d67e8ce"}, 294 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:4c0788010a93ace8a174d73e7c6c9d3e6e3b7ad99a453c8ee8c975ddd9965643"}, 295 | {file = "regex-2021.3.17-cp37-cp37m-win32.whl", hash = "sha256:575a832e09d237ae5fedb825a7a5bc6a116090dd57d6417d4f3b75121c73e3be"}, 296 | {file = "regex-2021.3.17-cp37-cp37m-win_amd64.whl", hash = "sha256:8e65e3e4c6feadf6770e2ad89ad3deb524bcb03d8dc679f381d0568c024e0deb"}, 297 | {file = "regex-2021.3.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a0df9a0ad2aad49ea3c7f65edd2ffb3d5c59589b85992a6006354f6fb109bb18"}, 298 | {file = "regex-2021.3.17-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b98bc9db003f1079caf07b610377ed1ac2e2c11acc2bea4892e28cc5b509d8d5"}, 299 | {file = "regex-2021.3.17-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:808404898e9a765e4058bf3d7607d0629000e0a14a6782ccbb089296b76fa8fe"}, 300 | {file = "regex-2021.3.17-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:5770a51180d85ea468234bc7987f5597803a4c3d7463e7323322fe4a1b181578"}, 301 | {file = "regex-2021.3.17-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:976a54d44fd043d958a69b18705a910a8376196c6b6ee5f2596ffc11bff4420d"}, 302 | {file = "regex-2021.3.17-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:63f3ca8451e5ff7133ffbec9eda641aeab2001be1a01878990f6c87e3c44b9d5"}, 303 | {file = "regex-2021.3.17-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bcd945175c29a672f13fce13a11893556cd440e37c1b643d6eeab1988c8b209c"}, 304 | {file = "regex-2021.3.17-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:3d9356add82cff75413bec360c1eca3e58db4a9f5dafa1f19650958a81e3249d"}, 305 | {file = "regex-2021.3.17-cp38-cp38-win32.whl", hash = "sha256:f5d0c921c99297354cecc5a416ee4280bd3f20fd81b9fb671ca6be71499c3fdf"}, 306 | {file = "regex-2021.3.17-cp38-cp38-win_amd64.whl", hash = "sha256:14de88eda0976020528efc92d0a1f8830e2fb0de2ae6005a6fc4e062553031fa"}, 307 | {file = "regex-2021.3.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4c2e364491406b7888c2ad4428245fc56c327e34a5dfe58fd40df272b3c3dab3"}, 308 | {file = "regex-2021.3.17-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8bd4f91f3fb1c9b1380d6894bd5b4a519409135bec14c0c80151e58394a4e88a"}, 309 | {file = "regex-2021.3.17-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:882f53afe31ef0425b405a3f601c0009b44206ea7f55ee1c606aad3cc213a52c"}, 310 | {file = "regex-2021.3.17-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:07ef35301b4484bce843831e7039a84e19d8d33b3f8b2f9aab86c376813d0139"}, 311 | {file = "regex-2021.3.17-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:360a01b5fa2ad35b3113ae0c07fb544ad180603fa3b1f074f52d98c1096fa15e"}, 312 | {file = "regex-2021.3.17-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:709f65bb2fa9825f09892617d01246002097f8f9b6dde8d1bb4083cf554701ba"}, 313 | {file = "regex-2021.3.17-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:c66221e947d7207457f8b6f42b12f613b09efa9669f65a587a2a71f6a0e4d106"}, 314 | {file = "regex-2021.3.17-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c782da0e45aff131f0bed6e66fbcfa589ff2862fc719b83a88640daa01a5aff7"}, 315 | {file = "regex-2021.3.17-cp39-cp39-win32.whl", hash = "sha256:dc9963aacb7da5177e40874585d7407c0f93fb9d7518ec58b86e562f633f36cd"}, 316 | {file = "regex-2021.3.17-cp39-cp39-win_amd64.whl", hash = "sha256:a0d04128e005142260de3733591ddf476e4902c0c23c1af237d9acf3c96e1b38"}, 317 | {file = "regex-2021.3.17.tar.gz", hash = "sha256:4b8a1fb724904139149a43e172850f35aa6ea97fb0545244dc0b805e0154ed68"}, 318 | ] 319 | selenium = [ 320 | {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"}, 321 | {file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"}, 322 | ] 323 | six = [ 324 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 325 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 326 | ] 327 | toml = [ 328 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 329 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 330 | ] 331 | typed-ast = [ 332 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, 333 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, 334 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, 335 | {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, 336 | {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, 337 | {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, 338 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, 339 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, 340 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, 341 | {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, 342 | {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, 343 | {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, 344 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, 345 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, 346 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, 347 | {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, 348 | {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, 349 | {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, 350 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, 351 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, 352 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, 353 | {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, 354 | {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, 355 | {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, 356 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, 357 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, 358 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, 359 | {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, 360 | {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, 361 | {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, 362 | ] 363 | typing-extensions = [ 364 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 365 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 366 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 367 | ] 368 | urllib3 = [ 369 | {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, 370 | {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, 371 | ] 372 | virtualenv = [ 373 | {file = "virtualenv-20.4.3-py2.py3-none-any.whl", hash = "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c"}, 374 | {file = "virtualenv-20.4.3.tar.gz", hash = "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107"}, 375 | ] 376 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "youtubeuploader" 3 | version = "1.0" 4 | description = "An automated, headless YouTube Uploader" 5 | authors = ["Christian C., Moritz M., Luca S."] 6 | 7 | [tool.poetry.dependencies] 8 | python = "~3.8" 9 | selenium = "^3.141.0" 10 | 11 | [tool.poetry.dev-dependencies] 12 | black = "20.8b1" 13 | pre-commit = "^2.11.1" 14 | 15 | [build-system] 16 | requires = ["poetry-core>=1.0.0a5"] 17 | build-backend = "poetry.core.masonry.api" 18 | 19 | [tool.black] 20 | line-length = 120 21 | target-version = ['py38'] 22 | include = '\.pyi?$' 23 | exclude = ''' 24 | /( 25 | \.eggs 26 | | \.git 27 | | \.hg 28 | | \.mypy_cache 29 | | \.tox 30 | | \.venv 31 | | _build 32 | | buck-out 33 | | build 34 | | dist 35 | )/ 36 | ''' 37 | -------------------------------------------------------------------------------- /src/login.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Dict, List 3 | 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.remote.webdriver import WebDriver 6 | from selenium.webdriver.support import expected_conditions as EC 7 | from selenium.webdriver.support.ui import WebDriverWait 8 | 9 | """ Login module """ 10 | 11 | 12 | def domain_to_url(domain: str) -> str: 13 | """ Converts a (partial) domain to valid URL """ 14 | if domain.startswith("."): 15 | domain = "www" + domain 16 | return "http://" + domain 17 | 18 | 19 | def login_using_cookie_file(driver: WebDriver, cookie_file: str): 20 | """Restore auth cookies from a file. Does not guarantee that the user is logged in afterwards. 21 | Visits the domains specified in the cookies to set them, the previous page is not restored.""" 22 | domain_cookies: Dict[str, List[object]] = {} 23 | with open(cookie_file) as file: 24 | cookies: List = json.load(file) 25 | # Sort cookies by domain, because we need to visit to domain to add cookies 26 | for cookie in cookies: 27 | try: 28 | domain_cookies[cookie["domain"]].append(cookie) 29 | except KeyError: 30 | domain_cookies[cookie["domain"]] = [cookie] 31 | 32 | for domain, cookies in domain_cookies.items(): 33 | driver.get(domain_to_url(domain + "/robots.txt")) 34 | for cookie in cookies: 35 | cookie.pop("sameSite", None) # Attribute should be available in Selenium >4 36 | cookie.pop("storeId", None) # Firefox container attribute 37 | try: 38 | driver.add_cookie(cookie) 39 | except: 40 | print(f"Couldn't set cookie {cookie['name']} for {domain}") 41 | 42 | 43 | def confirm_logged_in(driver: WebDriver) -> bool: 44 | """ Confirm that the user is logged in. The browser needs to be navigated to a YouTube page. """ 45 | try: 46 | WebDriverWait(driver, 5).until(EC.element_to_be_clickable((By.ID, "avatar-btn"))) 47 | return True 48 | except TimeoutError: 49 | return False 50 | -------------------------------------------------------------------------------- /src/upload.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from datetime import datetime 4 | from time import sleep 5 | 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.common.keys import Keys 8 | from selenium.webdriver.remote.webdriver import WebDriver 9 | from selenium.webdriver.remote.webelement import WebElement 10 | from selenium.webdriver.support import expected_conditions as EC 11 | from selenium.webdriver.support.ui import WebDriverWait 12 | from selenium.common.exceptions import NoSuchElementException, ElementNotInteractableException 13 | 14 | 15 | def upload_file( 16 | driver: WebDriver, 17 | video_path: str, 18 | title: str, 19 | description: str, 20 | game: str, 21 | kids: bool, 22 | upload_time: datetime, 23 | thumbnail_path: str = None, 24 | ): 25 | WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.CSS_SELECTOR, "ytcp-button#create-icon"))).click() 26 | WebDriverWait(driver, 20).until( 27 | EC.element_to_be_clickable((By.XPATH, '//tp-yt-paper-item[@test-id="upload-beta"]')) 28 | ).click() 29 | video_input = driver.find_element_by_xpath('//input[@type="file"]') 30 | video_input.send_keys(video_path) 31 | 32 | _set_basic_settings(driver, title, description, thumbnail_path) 33 | _set_advanced_settings(driver, game, kids) 34 | # Go to visibility settings 35 | for i in range(3): 36 | WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.ID, "next-button"))).click() 37 | 38 | _set_time(driver, upload_time) 39 | _wait_for_processing(driver) 40 | # Go back to endcard settings 41 | driver.find_element_by_css_selector("#step-badge-1").click() 42 | _set_endcard(driver) 43 | 44 | for _ in range(2): 45 | # Sometimes, the button is clickable but clicking it raises an error, so we add a "safety-sleep" here 46 | sleep(5) 47 | WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.ID, "next-button"))).click() 48 | 49 | sleep(5) 50 | WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.ID, "done-button"))).click() 51 | 52 | # Wait for the dialog to disappear 53 | sleep(5) 54 | logging.info("Upload is complete") 55 | 56 | 57 | def _wait_for_processing(driver): 58 | # Wait for processing to complete 59 | progress_label: WebElement = driver.find_element_by_css_selector("span.progress-label") 60 | pattern = re.compile(r"(finished processing)|(processing hd.*)|(check.*)") 61 | current_progress = progress_label.get_attribute("textContent") 62 | last_progress = None 63 | while not pattern.match(current_progress.lower()): 64 | if last_progress != current_progress: 65 | logging.info(f'Current progress: {current_progress}') 66 | last_progress = current_progress 67 | sleep(5) 68 | current_progress = progress_label.get_attribute("textContent") 69 | 70 | 71 | def _set_basic_settings(driver: WebDriver, title: str, description: str, thumbnail_path: str = None): 72 | title_input: WebElement = WebDriverWait(driver, 20).until( 73 | EC.element_to_be_clickable( 74 | ( 75 | By.XPATH, 76 | '//ytcp-mention-textbox[@label="Title"]//div[@id="textbox"]', 77 | 78 | ) 79 | ) 80 | ) 81 | 82 | # Input meta data (title, description, etc ... ) 83 | description_input: WebElement = driver.find_element_by_xpath( 84 | '//ytcp-mention-textbox[@label="Description"]//div[@id="textbox"]' 85 | ) 86 | thumbnail_input: WebElement = driver.find_element_by_css_selector( 87 | "input#file-loader" 88 | ) 89 | 90 | title_input.clear() 91 | title_input.send_keys(title) 92 | description_input.send_keys(description) 93 | if thumbnail_path: 94 | thumbnail_input.send_keys(thumbnail_path) 95 | 96 | 97 | def _set_advanced_settings(driver: WebDriver, game_title: str, made_for_kids: bool): 98 | # Open advanced options 99 | driver.find_element_by_css_selector("#toggle-button").click() 100 | if game_title: 101 | game_title_input: WebElement = driver.find_element_by_css_selector( 102 | ".ytcp-form-gaming > " 103 | "ytcp-dropdown-trigger:nth-child(1) > " 104 | ":nth-child(2) > div:nth-child(3) > input:nth-child(3)" 105 | ) 106 | game_title_input.send_keys(game_title) 107 | 108 | # Select first item in game drop down 109 | WebDriverWait(driver, 20).until( 110 | EC.element_to_be_clickable( 111 | ( 112 | By.CSS_SELECTOR, 113 | "#text-item-2", # The first item is an empty item 114 | ) 115 | ) 116 | ).click() 117 | 118 | WebDriverWait(driver, 20).until(EC.element_to_be_clickable( 119 | (By.NAME, "VIDEO_MADE_FOR_KIDS_MFK" if made_for_kids else "VIDEO_MADE_FOR_KIDS_NOT_MFK") 120 | )).click() 121 | 122 | 123 | def _set_endcard(driver: WebDriver): 124 | # Add endscreen 125 | driver.find_element_by_css_selector("#endscreens-button").click() 126 | sleep(5) 127 | 128 | for i in range(1, 11): 129 | try: 130 | # Select endcard type from last video or first suggestion if no prev. video 131 | driver.find_element_by_css_selector("div.card:nth-child(1)").click() 132 | break 133 | except (NoSuchElementException, ElementNotInteractableException): 134 | logging.warning(f"Couldn't find endcard button. Retry in 5s! ({i}/10)") 135 | sleep(5) 136 | 137 | WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.ID, "save-button"))).click() 138 | 139 | 140 | def _set_time(driver: WebDriver, upload_time: datetime): 141 | # Start time scheduling 142 | WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.NAME, "SCHEDULE"))).click() 143 | 144 | # Open date_picker 145 | driver.find_element_by_css_selector("#datepicker-trigger > ytcp-dropdown-trigger:nth-child(1)").click() 146 | 147 | date_input: WebElement = driver.find_element_by_css_selector("input.tp-yt-paper-input") 148 | date_input.clear() 149 | # Transform date into required format: Mar 19, 2021 150 | date_input.send_keys(upload_time.strftime("%b %d, %Y")) 151 | date_input.send_keys(Keys.RETURN) 152 | 153 | # Open time_picker 154 | driver.find_element_by_css_selector( 155 | "#time-of-day-trigger > ytcp-dropdown-trigger:nth-child(1) > div:nth-child(2)" 156 | ).click() 157 | 158 | time_list = driver.find_elements_by_css_selector("tp-yt-paper-item.tp-yt-paper-item") 159 | # Transform time into required format: 8:15 PM 160 | time_str = upload_time.strftime("%I:%M %p").strip("0") 161 | time = [time for time in time_list[2:] if time.text == time_str][0] 162 | time.click() 163 | -------------------------------------------------------------------------------- /uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContentAutomation/YouTubeUploader/74255c90435ba412f0003e0dfb33b39aa85394c6/uploads/.gitkeep --------------------------------------------------------------------------------