├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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
--------------------------------------------------------------------------------