├── .deepsource.toml ├── .github ├── ISSUE_TEMPLATE │ ├── basic_issue_template.md │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build-docs.yml │ ├── lint.yml │ └── pypi-publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── Usage.md ├── YoutubeUploader.md ├── index.md └── pillargg.png ├── mkdocs.yml ├── requirements.txt ├── setup.py └── youtube_upload ├── __init__.py ├── client.py └── oauth_template.py /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = "3.x.x" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/basic_issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Basic Issue 3 | about: Template for basic issues 4 | title: '' 5 | labels: potential 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Problem 11 | Short summary of the issue we need to address. 12 | 13 | ### Goals 14 | 15 | - [ ] High-level completion criteria #1 16 | - [ ] High-level completion criteria #2 17 | 18 | ### Context 19 | 20 | Add any additional context & links here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. When recording streamer '...' 16 | 2. During attempted '....' 17 | 3. The error is '.....' 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Desktop (please complete the following information):** 26 | - OS: [e.g. Windows 10] 27 | - Version [e.g. 2004] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: potential 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Description:** 11 | A clear and concise description detailing what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | 19 | **How to get started:** 20 | A clear point in the right direction to help someone who would solve this issue. 21 | 22 | **Acceptance Criteria:** 23 | A clear and concise description of what the expected output is. If the Acceptance criteria if complete, this feature request can be considered completed. 24 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Docs 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | generate: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.7 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.7 16 | - name: Install Dependencies 17 | run: | 18 | pip install -r requirements.txt 19 | - name: Commit and Push changes 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | run: | 23 | cat README.md > docs/index.md 24 | git config user.name "Github Action" 25 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 26 | git remote set-url origin https://$GITHUB_ACTOR:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git 27 | if [ -n "$(git status --porcelain)" ]; then 28 | echo "there are changes"; 29 | git checkout master 30 | git add -A 31 | git commit -m "[bot] Update index.md to match README.md" 32 | git push origin ${{ github.head_ref }} 33 | fi 34 | mkdocs gh-deploy -m "[bot] Updated documentation" 35 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | generate: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Python 3.7 11 | uses: actions/setup-python@v2 12 | with: 13 | python-version: 3.7 14 | - name: Install Dependencies 15 | run: | 16 | pip install --no-cache-dir autopep8 17 | - name: Run AutoPEP8 18 | run: | 19 | autopep8 -r --in-place --aggressive --aggressive . 20 | - name: Commit and Push changes 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | run: | 24 | git config user.name "Github Action" 25 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 26 | git fetch 27 | git checkout ${{ github.head_ref }} 28 | if [ -n "$(git status --porcelain)" ]; then 29 | echo "there are changes"; 30 | git add -A 31 | git commit -m "[bot] Lint files" 32 | git push "https://$GITHUB_ACTOR:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git" ${{ github.head_ref }} 33 | else 34 | echo "no changes"; 35 | fi 36 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .vscode/ 131 | client_secrets.json 132 | oauth.json 133 | test.* -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/mirrors-autopep8 5 | rev: v1.5.4 6 | hooks: 7 | - id: autopep8 8 | args: ['-r', '--in-place', 'youtube_upload'] 9 | 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at chandler@pillar.gg. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2021 Pillar 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Docs](https://github.com/pillargg/youtube-upload/workflows/Build%20Docs/badge.svg?branch=master) ![Upload Python Package](https://github.com/pillargg/youtube-upload/workflows/Upload%20Python%20Package/badge.svg) 2 | 3 | # PillarGG YouTube Uploader 4 | Upload Youtube Videos and more via Python. 5 | 6 | # Getting Started 7 | 8 | ![](https://thumbs.gfycat.com/ConventionalSecondhandGemsbuck-size_restricted.gif) 9 | 10 | `pip install pillar-youtube-upload` 11 | 12 | You can read more [below](https://github.com/pillargg/youtube-upload#usage) and on the [docs](https://pillargg.github.io/youtube-upload/YoutubeUploader/). 13 | 14 | This project uses the [Youtube Data API](https://developers.google.com/youtube/v3/docs/videos/insert). 15 | 16 | All implementations of youtube upload needs some form of [authentication](https://developers.google.com/youtube/v3/guides/authentication). 17 | 18 | ## Client Side youtube upload 19 | To upload youtube videos as a client, you need to follow [this guide](https://developers.google.com/youtube/v3/guides/auth/client-side-web-apps). 20 | 21 | 22 | ## Server Side youtube upload 23 | To upload youtube videos as a server, you need to follow [this guide](https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps). 24 | 25 | 26 | ## Getting a youtube api key 27 | 1. First, you need to go to [this website](https://console.developers.google.com/apis/library). 28 | 2. If prompted, select a project, or create a new one. 29 | 3. Use the Library page to find and enable the YouTube Data API v3. 30 | 4. Go to the [credentials page](https://console.developers.google.com/apis/credentials). 31 | 5. Click `Create Credentials > OAuth client ID`. 32 | 6. Select the Web application app type. 33 | 7. Fill in the form and click create. For testing redirect URIs that refer to the local machine with `http://localhost:8080`. 34 | 8. Download the client_secret.json file from the API Console and securely store the file in a location that only your application can access. By default, the application gets this file from the directory your script is being ran in. The path can also be changed when the class `YoutubeUploader` is being initialized. 35 | 8. Get your `client_id` and `client_secret`. 36 | 37 | We recommend that you [design your app's auth endpoints](https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#protectauthcode) so that your application does not expose authorization codes to other resources on the page. 38 | 39 | The key must have the scope 'https://www.googleapis.com/auth/youtube.upload'. 40 | 41 | 42 | ## Usage Info 43 | 44 | The variables `client_id` and `client_secret` are how Google identify your application. These are specified in the initialization parameters or in the `client_secrets.json` file. 45 | The variables `access_token` and `refresh_token` are how Google's APIs differentiate between different YouTube channels. These are specified in the authenticate method. 46 | 47 | This module uploads a given video to a youtube channel, as such there are 2 things this module needs. 48 | - A video 49 | - Video Options 50 | - Title 51 | - Description 52 | - Tags 53 | - Category 54 | - Status 55 | - DeclaredMadeForKids 56 | 57 | - A channel to upload to(In the form of a authentication file) 58 | 59 | - The authentication file should contain the following: 60 | - access_token: token_here 61 | - refresh_token: token_here 62 | - scope: scope_here 63 | - token_type: Bearer 64 | - expires_in: 3599 65 | 66 | This file should be called client_secrets.json and exist in the directory this script is. 67 | 68 | # Usage 69 | 70 | ### 0. Importing the package 71 | ```python 72 | # youtube upload api 73 | from youtube_upload.client import YoutubeUploader 74 | ``` 75 | 76 | ### 1. Instantiating an uploader 77 | ```python 78 | uploader = YoutubeUploader(client_id,client_secret) 79 | ``` 80 | *or* 81 | ```python 82 | uploader = YoutubeUploader() 83 | ``` 84 | If a client_secrets.json is present in the current working directory (downloaded from the [Credentials Control Panel](https://console.developers.google.com/apis/credentials) in the Google Cloud Console when you create the application). 85 | *or* 86 | ```python 87 | uploader = YoutubeUploader(secrets_file_path=secrets_file_path_here) 88 | ``` 89 | You can specify the path to the file with the [`secrets_file_path`](https://pillargg.github.io/youtube-upload/YoutubeUploader/#youtube_upload.client.YoutubeUploader) parameter. 90 | 91 | ### 2. Authentication 92 | 93 | If you run [`authenticate`](https://pillargg.github.io/youtube-upload/YoutubeUploader/#youtube_upload.client.YoutubeUploader.authenticate) with no parameters and no `oauth.json`, it opens a web page locally that you can use to sign into the YouTube Channel you want to upload the video to. 94 | 95 | 96 | ```python 97 | uploader.authenticate() 98 | ``` 99 | *or* 100 | ```python 101 | uploader.authenticate(access_token=access_token_here, refresh_token=refresh_token_here) 102 | ``` 103 | *OR* 104 | ```python 105 | uploader.authenticate(oauth_path='oauth.json') 106 | ``` 107 | 108 | ### 3. Uploading the video 109 | ```python 110 | 111 | # Video options 112 | options = { 113 | "title" : "Example title", # The video title 114 | "description" : "Example description", # The video description 115 | "tags" : ["tag1", "tag2", "tag3"], 116 | "categoryId" : "22", 117 | "privacyStatus" : "private", # Video privacy. Can either be "public", "private", or "unlisted" 118 | "kids" : False, # Specifies if the Video if for kids or not. Defaults to False. 119 | "thumbnailLink" : "https://cdn.havecamerawilltravel.com/photographer/files/2020/01/youtube-logo-new-1068x510.jpg" # Optional. Specifies video thumbnail. 120 | } 121 | 122 | # upload video 123 | uploader.upload(file_path, options) 124 | ``` 125 | Parameter `tags` should be list of strings only. The parameter `categoryId` refers to YouTube internal categories, more information can be found [here](https://stackoverflow.com/questions/17698040/youtube-api-v3-where-can-i-find-a-list-of-each-videocategoryid). 126 | ### 4. Closing the uploader 127 | ```python 128 | uploader.close() 129 | ``` 130 | 131 | This method deletes the OAuth file. If you do not want this behavior, skip calling this function. 132 | -------------------------------------------------------------------------------- /docs/Usage.md: -------------------------------------------------------------------------------- 1 | 2 | ## Usage Info 3 | 4 | The variables `client_id` and `client_secret` are how Google identify your application. These are specified in the initialization parameters or in the `client_secrets.json` file. 5 | The variables `access_token` and `refresh_token` are how Google's APIs differentiate between different YouTube channels. These are specified in the authenticate method. 6 | 7 | This module uploads a given video to a youtube channel, as such there are 2 things this module needs. 8 | - A video 9 | - Video Options 10 | - Title 11 | - Description 12 | - Tags 13 | - Category 14 | - Status 15 | - DeclaredMadeForKids 16 | 17 | - A channel to upload to(In the form of a authentication file) 18 | 19 | - The authentication file should contain the following: 20 | - access_token: token_here 21 | - refresh_token: token_here 22 | - scope: scope_here 23 | - token_type: Bearer 24 | - expires_in: 3599 25 | 26 | This file should be called client_secrets.json and exist in the directory this script is. 27 | 28 | # Quick Start 29 | 30 | ## 0. Importing the package 31 | ```python 32 | # youtube upload api 33 | from youtube_upload.client import YoutubeUploader 34 | ``` 35 | 36 | ## 1. Instantiating an uploader 37 | ```python 38 | uploader = YoutubeUploader(client_id,client_secret) 39 | ``` 40 | *or* 41 | ```python 42 | uploader = YoutubeUploader() 43 | ``` 44 | If a client_secrets.json is present in the current working directory (downloaded from the [Credentials Control Panel](https://console.developers.google.com/apis/credentials) in the Google Cloud Console when you create the application). 45 | *or* 46 | ```python 47 | uploader = YoutubeUploader(secrets_file_path=secrets_file_path_here) 48 | ``` 49 | You can specify the path to the file with the [`secrets_file_path`](https://pillargg.github.io/youtube-upload/YoutubeUploader/#youtube_upload.client.YoutubeUploader) parameter. 50 | 51 | ## 2. Authentication 52 | 53 | If you run [`authenticate`](https://pillargg.github.io/youtube-upload/YoutubeUploader/#youtube_upload.client.YoutubeUploader.authenticate) with no parameters and no `oauth.json`, it opens a web page locally that you can use to sign into the YouTube Channel you want to upload the video to. 54 | 55 | 56 | ```python 57 | uploader.authenticate() 58 | ``` 59 | *or* 60 | ```python 61 | uploader.authenticate(access_token=access_token_here, refresh_token=refresh_token_here) 62 | ``` 63 | *OR* 64 | ```python 65 | uploader.authenticate(oauth_path='oauth.json') 66 | ``` 67 | 68 | ## 3. Uploading the video 69 | ```python 70 | 71 | # Video options 72 | options = { 73 | "title" : "Example title", # The video title 74 | "description" : "Example description", # The video description 75 | "tags" : ["tag1", "tag2", "tag3"], 76 | "categoryId" : "22", 77 | "privacyStatus" : "private", # Video privacy. Can either be "public", "private", or "unlisted" 78 | "kids" : False # Specifies if the Video if for kids or not. Defaults to False. 79 | "thumbnailLink" : "https://cdn.havecamerawilltravel.com/photographer/files/2020/01/youtube-logo-new-1068x510.jpg" # Optional. Specifies video thumbnail. 80 | } 81 | 82 | # upload video 83 | uploader.upload(file_path, options) 84 | ``` 85 | Parameter `tags` should be list of strings only. The parameter `categoryId` refers to YouTube internal categories, more information can be found [here](https://stackoverflow.com/questions/17698040/youtube-api-v3-where-can-i-find-a-list-of-each-videocategoryid). 86 | ## 4. Closing the uploader 87 | ```python 88 | uploader.close() 89 | ``` 90 | 91 | This method deletes the OAuth JSON file. Omit this function if you do not desire that behavior. 92 | -------------------------------------------------------------------------------- /docs/YoutubeUploader.md: -------------------------------------------------------------------------------- 1 | ::: youtube_upload.client.YoutubeUploader 2 | handler: python 3 | selection: 4 | members: 5 | - __init__ 6 | - authenticate 7 | - upload 8 | - upload_stream 9 | - close 10 | rendering: 11 | show_root_heading: false 12 | show_source: false -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ![Build Docs](https://github.com/pillargg/youtube-upload/workflows/Build%20Docs/badge.svg?branch=master) ![Upload Python Package](https://github.com/pillargg/youtube-upload/workflows/Upload%20Python%20Package/badge.svg) 2 | 3 | # PillarGG YouTube Uploader 4 | Upload Youtube Videos and more via Python. 5 | 6 | # Getting Started 7 | 8 | ![](https://thumbs.gfycat.com/ConventionalSecondhandGemsbuck-size_restricted.gif) 9 | 10 | `pip install pillar-youtube-upload` 11 | 12 | You can read more [below](https://github.com/pillargg/youtube-upload#usage) and on the [docs](https://pillargg.github.io/youtube-upload/YoutubeUploader/). 13 | 14 | This project uses the [Youtube Data API](https://developers.google.com/youtube/v3/docs/videos/insert). 15 | 16 | All implementations of youtube upload needs some form of [authentication](https://developers.google.com/youtube/v3/guides/authentication). 17 | 18 | ## Client Side youtube upload 19 | To upload youtube videos as a client, you need to follow [this guide](https://developers.google.com/youtube/v3/guides/auth/client-side-web-apps). 20 | 21 | 22 | ## Server Side youtube upload 23 | To upload youtube videos as a server, you need to follow [this guide](https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps). 24 | 25 | 26 | ## Getting a youtube api key 27 | 1. First, you need to go to [this website](https://console.developers.google.com/apis/library). 28 | 2. If prompted, select a project, or create a new one. 29 | 3. Use the Library page to find and enable the YouTube Data API v3. 30 | 4. Go to the [credentials page](https://console.developers.google.com/apis/credentials). 31 | 5. Click `Create Credentials > OAuth client ID`. 32 | 6. Select the Web application app type. 33 | 7. Fill in the form and click create. For testing redirect URIs that refer to the local machine with `http://localhost:8080`. 34 | 8. Download the client_secret.json file from the API Console and securely store the file in a location that only your application can access. By default, the application gets this file from the directory your script is being ran in. The path can also be changed when the class `YoutubeUploader` is being initialized. 35 | 8. Get your `client_id` and `client_secret`. 36 | 37 | We recommend that you [design your app's auth endpoints](https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#protectauthcode) so that your application does not expose authorization codes to other resources on the page. 38 | 39 | The key must have the scope 'https://www.googleapis.com/auth/youtube.upload'. 40 | 41 | 42 | ## Usage Info 43 | 44 | The variables `client_id` and `client_secret` are how Google identify your application. These are specified in the initialization parameters or in the `client_secrets.json` file. 45 | The variables `access_token` and `refresh_token` are how Google's APIs differentiate between different YouTube channels. These are specified in the authenticate method. 46 | 47 | This module uploads a given video to a youtube channel, as such there are 2 things this module needs. 48 | - A video 49 | - Video Options 50 | - Title 51 | - Description 52 | - Tags 53 | - Category 54 | - Status 55 | - DeclaredMadeForKids 56 | 57 | - A channel to upload to(In the form of a authentication file) 58 | 59 | - The authentication file should contain the following: 60 | - access_token: token_here 61 | - refresh_token: token_here 62 | - scope: scope_here 63 | - token_type: Bearer 64 | - expires_in: 3599 65 | 66 | This file should be called client_secrets.json and exist in the directory this script is. 67 | 68 | # Usage 69 | 70 | ### 0. Importing the package 71 | ```python 72 | # youtube upload api 73 | from youtube_upload.client import YoutubeUploader 74 | ``` 75 | 76 | ### 1. Instantiating an uploader 77 | ```python 78 | uploader = YoutubeUploader(client_id,client_secret) 79 | ``` 80 | *or* 81 | ```python 82 | uploader = YoutubeUploader() 83 | ``` 84 | If a client_secrets.json is present in the current working directory (downloaded from the [Credentials Control Panel](https://console.developers.google.com/apis/credentials) in the Google Cloud Console when you create the application). 85 | *or* 86 | ```python 87 | uploader = YoutubeUploader(secrets_file_path=secrets_file_path_here) 88 | ``` 89 | You can specify the path to the file with the [`secrets_file_path`](https://pillargg.github.io/youtube-upload/YoutubeUploader/#youtube_upload.client.YoutubeUploader) parameter. 90 | 91 | ### 2. Authentication 92 | 93 | If you run [`authenticate`](https://pillargg.github.io/youtube-upload/YoutubeUploader/#youtube_upload.client.YoutubeUploader.authenticate) with no parameters and no `oauth.json`, it opens a web page locally that you can use to sign into the YouTube Channel you want to upload the video to. 94 | 95 | 96 | ```python 97 | uploader.authenticate() 98 | ``` 99 | *or* 100 | ```python 101 | uploader.authenticate(access_token=access_token_here, refresh_token=refresh_token_here) 102 | ``` 103 | *OR* 104 | ```python 105 | uploader.authenticate(oauth_path='oauth.json') 106 | ``` 107 | 108 | ### 3. Uploading the video 109 | ```python 110 | 111 | # Video options 112 | options = { 113 | "title" : "Example title", # The video title 114 | "description" : "Example description", # The video description 115 | "tags" : ["tag1", "tag2", "tag3"], 116 | "categoryId" : "22", 117 | "privacyStatus" : "private", # Video privacy. Can either be "public", "private", or "unlisted" 118 | "kids" : False, # Specifies if the Video if for kids or not. Defaults to False. 119 | "thumbnailLink" : "https://cdn.havecamerawilltravel.com/photographer/files/2020/01/youtube-logo-new-1068x510.jpg" # Optional. Specifies video thumbnail. 120 | } 121 | 122 | # upload video 123 | uploader.upload(file_path, options) 124 | ``` 125 | Parameter `tags` should be list of strings only. The parameter `categoryId` refers to YouTube internal categories, more information can be found [here](https://stackoverflow.com/questions/17698040/youtube-api-v3-where-can-i-find-a-list-of-each-videocategoryid). 126 | ### 4. Closing the uploader 127 | ```python 128 | uploader.close() 129 | ``` 130 | -------------------------------------------------------------------------------- /docs/pillargg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pillargg/youtube-upload/46f8c733dba40ab750776de53c163fcd6db6b9a7/docs/pillargg.png -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Pillar Youtube Uploader 2 | 3 | theme: 4 | name: "material" 5 | palette: 6 | scheme: slate 7 | primary: black 8 | accent: deep purple 9 | logo: pillargg.png 10 | favicon: pillargg.png 11 | icon: 12 | repo: fontawesome/brands/github 13 | 14 | repo_url: https://github.com/pillargg/youtube-upload 15 | repo_name: pillargg/youtube-upload 16 | 17 | plugins: 18 | - search 19 | - mkdocstrings: 20 | default_handler: python 21 | handlers: 22 | python: 23 | rendering: 24 | show_sources: false 25 | watch: 26 | - youtube_upload -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.9.3 2 | cachetools==4.2.0 3 | certifi==2020.12.5 4 | chardet==4.0.0 5 | click==7.1.2 6 | future==0.18.2 7 | google-api-core==1.24.1 8 | google-api-python-client==1.12.8 9 | google-auth==1.24.0 10 | google-auth-httplib2==0.0.4 11 | googleapis-common-protos==1.52.0 12 | httplib2==0.19.0 13 | idna==2.10 14 | importlib-metadata==3.3.0 15 | Jinja2==2.11.2 16 | joblib==1.0.0 17 | livereload==2.6.3 18 | lunr==0.5.8 19 | Markdown==3.3.3 20 | MarkupSafe==1.1.1 21 | mkdocs==1.1.2 22 | mkdocs-material==6.2.3 23 | mkdocs-material-extensions==1.0.1 24 | mkdocstrings==0.13.6 25 | nltk==3.5 26 | oauth2client==4.1.3 27 | protobuf==3.14.0 28 | pyasn1==0.4.8 29 | pyasn1-modules==0.2.8 30 | Pygments==2.7.3 31 | pymdown-extensions==8.1 32 | pytkdocs==0.9.0 33 | pytz==2020.5 34 | PyYAML==5.3.1 35 | regex==2020.11.13 36 | requests==2.25.1 37 | rsa==4.6 38 | six==1.15.0 39 | soupsieve==2.1 40 | tornado==6.1 41 | tqdm==4.55.1 42 | typing-extensions==3.7.4.3 43 | uritemplate==3.0.1 44 | urllib3==1.26.2 45 | zipp==3.4.0 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('README.md') as readme: 4 | long_desc = readme.read() 5 | 6 | setuptools.setup( 7 | name='pillar-youtube-upload', 8 | version='0.3.0', 9 | author="PillarGG", 10 | author_email='opensource@pillar.gg', 11 | description='Upload YouTube videos from Python and more.', 12 | long_description=long_desc, 13 | long_description_content_type="text/markdown", 14 | url='https://github.com/pillargg/youtube-upload', 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | python_requires='>=3.6', 22 | install_requires=[ 23 | 'httplib2>=0.18.1', 24 | 'google-auth>=1.22.1', 25 | 'google-api-core>=1.23.0', 26 | 'google-api-python-client>=1.12.5', 27 | 'oauth2client>=4.1.3' 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /youtube_upload/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import httplib2 3 | import http.client 4 | 5 | # Explicitly tell the underlying HTTP transport library not to retry, since 6 | # we are handling retry logic ourselves. 7 | httplib2.RETRIES = 1 8 | 9 | MAX_RETRIES = 10 10 | 11 | RETRYABLE_EXCEPTIONS = ( 12 | httplib2.HttpLib2Error, 13 | IOError, 14 | http.client.NotConnected, 15 | http.client.IncompleteRead, 16 | http.client.ImproperConnectionState, 17 | http.client.CannotSendRequest, 18 | http.client.CannotSendHeader, 19 | http.client.ResponseNotReady, 20 | http.client.BadStatusLine) 21 | 22 | RETRYABLE_STATUS_CODES = [500, 502, 503, 504] 23 | 24 | CLIENT_SECRETS_FILE = 'client_secrets.json' 25 | OAUTH_FILE = 'oauth.json' 26 | 27 | YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload" 28 | YOUTUBE_API_SERVICE_NAME = "youtube" 29 | YOUTUBE_API_VERSION = "v3" 30 | 31 | MISSING_CLIENT_SECRETS_MESSAGE = """ 32 | WARNING: Please configure OAuth 2.0 33 | 34 | To make this sample run you will need to populate the client_secrets.json file 35 | found at: 36 | 37 | %s 38 | 39 | with information from the Developers Console 40 | https://console.developers.google.com/ 41 | 42 | For more information about the client_secrets.json file format, please visit: 43 | https://developers.google.com/api-client-library/python/guide/aaa_client_secrets 44 | """ % os.path.abspath(os.path.join(os.path.dirname(__file__), 45 | CLIENT_SECRETS_FILE)) 46 | 47 | VALID_PRIVACY_STATUSES = ("public", "private", "unlisted") 48 | -------------------------------------------------------------------------------- /youtube_upload/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | import time 5 | import uuid 6 | 7 | import httplib2 8 | from googleapiclient.discovery import build 9 | from googleapiclient.errors import HttpError 10 | from googleapiclient.http import MediaFileUpload, MediaIoBaseUpload 11 | from oauth2client.client import flow_from_clientsecrets 12 | from oauth2client.file import Storage 13 | from oauth2client.tools import run_flow 14 | 15 | from youtube_upload import CLIENT_SECRETS_FILE 16 | from youtube_upload import MAX_RETRIES 17 | from youtube_upload import MISSING_CLIENT_SECRETS_MESSAGE 18 | from youtube_upload import OAUTH_FILE 19 | from youtube_upload import RETRYABLE_EXCEPTIONS 20 | from youtube_upload import RETRYABLE_STATUS_CODES 21 | from youtube_upload import VALID_PRIVACY_STATUSES 22 | from youtube_upload import YOUTUBE_API_SERVICE_NAME 23 | from youtube_upload import YOUTUBE_API_VERSION 24 | from youtube_upload import YOUTUBE_UPLOAD_SCOPE 25 | 26 | from youtube_upload.oauth_template import oauth_template 27 | 28 | 29 | class YoutubeUploader(): 30 | ''' 31 | The YouTube Uploader service. 32 | 33 | When using in a multithreaded environment, please create a new instance of the `YoutubeUploader` class per thread. 34 | ''' 35 | 36 | def __init__( 37 | self, 38 | client_id=None, 39 | client_secret=None, 40 | secrets_file_path=os.path.join( 41 | '.', 42 | CLIENT_SECRETS_FILE)): 43 | ''' 44 | Initialization Function for the class. 45 | 46 | The variables `client_id` and `client_secret` can be passed in when the class is initialized, this will have the function generate the `client_secrets.json` file. 47 | 48 | If you do not pass the variables in, the class will look for a `client_secrets.json` file in the same direction in which the script is being initialized. You can instead 49 | pass in a directory to where the `client_secrets.json` is with the parameter `secrets_file_path` here is an example `client_secrets.json` file: 50 | ```json 51 | { 52 | "web": { 53 | "client_id": "", 54 | "client_secret": "", 55 | "redirect_uris": [], 56 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 57 | "token_uri": "https://accounts.google.com/o/oauth2/token" 58 | } 59 | } 60 | ``` 61 | ''' 62 | 63 | self.client_secrets = {} 64 | if client_id is None or client_secret is None: 65 | self.secrets_file = secrets_file_path 66 | with open(secrets_file_path) as f: 67 | self.client_secrets = json.loads(f.read()) 68 | else: 69 | self.client_secrets = { 70 | 'web': { 71 | 'redirect_uris': [], 72 | 'auth_uri': 'https://accounts.google.com/o/oauth2/auth', 73 | 'token_uri': 'https://accounts.google.com/o/oauth2/token', 74 | 'client_id': client_id, 75 | 'client_secret': client_secret 76 | } 77 | } 78 | with open(secrets_file_path, 'w') as f: 79 | f.write(json.dumps(self.client_secrets)) 80 | self.secrets_file = secrets_file_path 81 | 82 | self.youtube = None 83 | self.options = None 84 | self.flow = None 85 | self.credentials = None 86 | self.oauth_path = OAUTH_FILE 87 | 88 | self.max_retry = MAX_RETRIES 89 | 90 | def __del__(self): 91 | self.close() 92 | 93 | # This is if you have another OAuth file to use 94 | def authenticate( 95 | self, 96 | oauth_path=OAUTH_FILE, 97 | access_token=None, 98 | refresh_token=None): 99 | ''' 100 | This method authenticates the user with Google's servers. If you give no path, the method will look for the `oauth.json` file in the current working directory. 101 | 102 | If a path is given, and the file does not exist at that path, the file will be created at that path. If the file does exist at the given path, it will be used. 103 | 104 | If in the case that you do not have an OAuth JSON file, you can specify an access and refresh token via the `access_token` and `refresh_token` parameters. 105 | 106 | ''' 107 | 108 | self.oauth_path = oauth_path 109 | # If access_token and refresh_token is provided, create new oauth.json 110 | if access_token is not None and refresh_token is not None: 111 | if '.json' not in oauth_path and os.path.isdir(oauth_path): 112 | # https://stackoverflow.com/questions/2961509/python-how-to-create-a-unique-file-name 113 | self.oauth_path = os.path.join( 114 | oauth_path, str(uuid.uuid4()) + '.json') 115 | else: 116 | self.oauth_path = str(uuid.uuid4()) + '.json' 117 | subs = { 118 | 'access_token': access_token, 119 | 'refresh_token': refresh_token, 120 | 'client_id': self.client_secrets['web'].get('client_id'), 121 | 'client_secret': self.client_secrets['web'].get('client_secret')} 122 | oauth_json_str = oauth_template.substitute(subs) 123 | with open(self.oauth_path, 'w') as f: 124 | f.write(oauth_json_str) 125 | 126 | self.flow = flow_from_clientsecrets( 127 | self.secrets_file, 128 | scope=YOUTUBE_UPLOAD_SCOPE, 129 | message=MISSING_CLIENT_SECRETS_MESSAGE) 130 | storage = Storage(self.oauth_path) 131 | self.credentials = storage.get() 132 | 133 | # Catches the edge case where no credentials were provided 134 | if self.credentials is None or self.credentials.invalid: 135 | # this wil only run if there is no callback function 136 | self.credentials = run_flow(self.flow, storage) 137 | 138 | self.youtube = build( 139 | YOUTUBE_API_SERVICE_NAME, 140 | YOUTUBE_API_VERSION, 141 | http=self.credentials.authorize( 142 | httplib2.Http())) 143 | 144 | def upload(self, file_path, options=None, chunksize=(-1)): 145 | ''' 146 | This uploads the file to YouTube. The only required argument is the `file_path`, which is the path to the video to be uploaded. 147 | 148 | The `options` parameter is a dictionary of options. The items are pretty self explanatory, here is an example options dictionary: 149 | ```Python 150 | # Video options 151 | options = { 152 | title : "Example title", 153 | description : "Example description", 154 | tags : ["tag1", "tag2", "tag3"], 155 | categoryId : "22", 156 | privacyStatus : "private", 157 | kids : False 158 | thumbnailLink : "https://cdn.havecamerawilltravel.com/photographer/files/2020/01/youtube-logo-new-1068x510.jpg" 159 | } 160 | ``` 161 | 162 | The parameter, `chunk_size` is the max size of the HTTP request to send the video. This parameter is in bytes, and if set to `-1`, which is the default, it 163 | will send the video in one large request. Set this to a different value if you are having issues with the upload failing. 164 | 165 | Will return the response from YouTube, as well as the response of the thumbnail upload as a tuple. 166 | 167 | ```Python 168 | response, thumbnail_response = client.upload(file_path, options) 169 | ``` 170 | 171 | ''' 172 | if options is None: 173 | options = {} 174 | body = { 175 | 'snippet': { 176 | 'title': options.get('title', 'Test Title'), 177 | 'description': options.get('description', 'Test Description'), 178 | 'tags': options.get('tags'), 179 | 'categoryId': options.get('category', '22') 180 | }, 181 | 'status': { 182 | 'privacyStatus': options.get('privacyStatus', VALID_PRIVACY_STATUSES[0]), 183 | 'selfDeclaredMadeForKids': options.get('kids', False) 184 | } 185 | } 186 | 187 | insert_request = self.youtube.videos().insert( 188 | part=",".join( 189 | list( 190 | body.keys())), body=body, media_body=MediaFileUpload( 191 | file_path, chunksize=chunksize, resumable=True)) 192 | 193 | return self._resumable_upload( 194 | insert_request, bool( 195 | options.get('thumbnailLink')), options) 196 | 197 | def upload_stream(self, file_object, options=None, chunksize=(-1)): 198 | ''' 199 | Uploads the file to YouTube from the specified file-like object. 200 | 201 | We are using this to stream files from S3 to YouTube! 202 | 203 | ```python 204 | import s3fs 205 | from youtube_upload.client import YouTubeUploader 206 | fs = s3fs.S3FileSystem(anon=True) 207 | 208 | video = fs.open('s3://bucket/video.mp4') 209 | 210 | client = YouTubeUploader() 211 | 212 | client.authenticate() 213 | 214 | client.upload_stream(video) 215 | 216 | client.close() 217 | video.close() 218 | 219 | ``` 220 | 221 | The `options` parameter is a dictionary of options. The items are pretty self explanatory, here is an example options dictionary: 222 | ```Python 223 | # Video options 224 | options = { 225 | title : "Example title", 226 | description : "Example description", 227 | tags : ["tag1", "tag2", "tag3"], 228 | categoryId : "22", 229 | privacyStatus : "private", 230 | kids : False 231 | thumbnailLink : "https://cdn.havecamerawilltravel.com/photographer/files/2020/01/youtube-logo-new-1068x510.jpg" 232 | } 233 | ``` 234 | 235 | The parameter, `chunk_size` is the max size of the HTTP request to send the video. This parameter is in bytes, and if set to `-1`, which is the default, it 236 | will send the video in one large request. Set this to a different value if you are having issues with the upload failing. 237 | 238 | Will return the response from YouTube, as well as the response of the thumbnail upload as a tuple. 239 | 240 | ```Python 241 | response, thumbnail_response = client.upload(file_path, options) 242 | ``` 243 | ''' 244 | if options is None: 245 | options = {} 246 | body = { 247 | 'snippet': { 248 | 'title': options.get('title', 'Test Title'), 249 | 'description': options.get('description', 'Test Description'), 250 | 'tags': options.get('tags'), 251 | 'categoryId': options.get('category', '22') 252 | }, 253 | 'status': { 254 | 'privacyStatus': options.get('privacyStatus', VALID_PRIVACY_STATUSES[0]), 255 | 'selfDeclaredMadeForKids': options.get('kids', False) 256 | } 257 | } 258 | 259 | media = MediaIoBaseUpload( 260 | file_object, 261 | "application/octet-stream", 262 | chunksize=chunksize, 263 | resumable=True) 264 | 265 | insert_request = self.youtube.videos().insert( 266 | part=",".join( 267 | list( 268 | body.keys())), body=body, media_body=media) 269 | 270 | return self._resumable_upload( 271 | insert_request, bool( 272 | options.get('thumbnailLink')), options) 273 | 274 | def _resumable_upload(self, insert_request, uploadThumbnail, options): 275 | response = None 276 | thumbnail_response = None 277 | error = None 278 | retry = 0 279 | 280 | while response is None: 281 | try: 282 | _, response = insert_request.next_chunk() 283 | #skipcq: PYL-R1723 284 | if 'id' in response: 285 | video_id = response.get('id') 286 | if uploadThumbnail: 287 | request = self.youtube.thumbnails().set( 288 | videoId=video_id, media_body=MediaFileUpload( 289 | options.get('thumbnailLink'))) 290 | thumbnail_response = request.execute() 291 | break 292 | 293 | else: 294 | # skipcq: PYL-E1120 295 | raise Exception(f'Unexpected response: {response}') 296 | except HttpError as e: 297 | if e.resp.status in RETRYABLE_STATUS_CODES: 298 | error = "A retryable HTTP error %d occurred:\n%s" % ( 299 | e.resp.status, e.content) 300 | else: 301 | raise e 302 | except RETRYABLE_EXCEPTIONS as e: 303 | error = "A retryable error occurred: %s" % e 304 | 305 | if error is not None: 306 | print(error) 307 | retry += 1 308 | if retry > self.max_retry: 309 | #skipcq: PYL-E1120 310 | raise Exception('Exceeded max retries. ' + error) 311 | 312 | print("Sleeping 5 seconds and then retrying...") 313 | time.sleep(5) 314 | 315 | return response, thumbnail_response 316 | 317 | def close(self): 318 | ''' 319 | Tears down and closes the class cleanly. 320 | ''' 321 | if os.path.exists(self.oauth_path): 322 | os.remove(self.oauth_path) 323 | -------------------------------------------------------------------------------- /youtube_upload/oauth_template.py: -------------------------------------------------------------------------------- 1 | from string import Template 2 | 3 | oauth_template = Template( 4 | """ 5 | { 6 | "access_token":"${access_token}", 7 | "client_id":"${client_id}", 8 | "client_secret":"${client_secret}", 9 | "refresh_token":"${refresh_token}", 10 | "token_expiry":"2020-10-27T18:03:48Z", 11 | "token_uri":"https://accounts.google.com/o/oauth2/token", 12 | "user_agent":null, 13 | "revoke_uri":"https://oauth2.googleapis.com/revoke", 14 | "id_token":null, 15 | "id_token_jwt":null, 16 | "token_response":{ 17 | "access_token":"${refresh_token}", 18 | "expires_in": 3599, 19 | "scope":"https://www.googleapis.com/auth/youtube.upload", 20 | "token_type":"Bearer" 21 | }, 22 | "scopes":[ 23 | "https://www.googleapis.com/auth/youtube.upload" 24 | ], 25 | "token_info_uri":"https://oauth2.googleapis.com/tokeninfo", 26 | "invalid":false, 27 | "_class":"OAuth2Credentials", 28 | "_module":"oauth2client.client" 29 | } 30 | """ 31 | 32 | ) 33 | --------------------------------------------------------------------------------