├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── python-package-integration-tests.yaml │ ├── python-package-unit-tests.yml │ └── pythonpublish.yml ├── .gitignore ├── .pylintrc ├── .readthedocs.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.rst ├── README.rst ├── docs ├── Makefile ├── categories.txt ├── make.bat └── source │ ├── LICENSE.rst │ ├── conf.py │ ├── examples │ ├── examples.rst │ ├── fetch_my_uploads.rst │ ├── fetch_youtube_categories.rst │ ├── update_video.rst │ ├── youtube_search.rst │ └── youtube_upload.rst │ ├── getting_started │ └── getting_started.rst │ ├── index.rst │ ├── install.rst │ └── ref │ ├── Channel.rst │ ├── Comment.rst │ ├── Video.rst │ └── ref.rst ├── examples ├── example_fetch_youtube_categories.py ├── example_my_uploads.py ├── example_update_video.py ├── example_youtube_search.py ├── example_youtube_upload.py └── test_vid.mp4 ├── resources ├── SCOPES ├── categories │ ├── categories.json │ ├── categories.pickle │ ├── parse_categories.py │ └── youtube_categories.txt ├── examples │ ├── comment_threads.json │ └── videos.json ├── languages │ ├── languages.json │ ├── parse_languages.py │ └── youtube_supported_languages.txt └── schema │ ├── channel.json │ ├── channel_sections.json │ ├── comment.json │ ├── playlist.json │ └── video.json ├── scratch_pad ├── example_fetch_youtube_categories.py ├── example_my_uploads.py ├── example_update_video.py ├── example_youtube_search.py └── example_youtube_upload.py ├── setup.py ├── simple_youtube_api ├── __init__.py ├── channel.py ├── comment.py ├── comment_thread.py ├── decorators.py ├── local_video.py ├── name_converter.py ├── video.py ├── youtube.py ├── youtube_api.py ├── youtube_constants.py └── youtube_video.py ├── tests ├── __init__.py ├── api_test │ ├── __init__.py │ ├── test_channel.py │ ├── test_youtube.py │ └── test_youtube_video.py ├── test_data │ ├── comment_test.json │ ├── comment_thread_test.json │ └── video.json └── unit_test │ ├── __init__.py │ ├── test_documentation.py │ ├── test_local_video.py │ └── test_youtube_api.py └── tools ├── autogenerate.py ├── comment.py └── video.py /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/python-package-integration-tests.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | # For running integration tests and uploading coverage. Separate file to minimize quota usage. 5 | name: Python package Integration Tests 6 | 7 | on: 8 | push: 9 | paths: 10 | - .github/workflows/** 11 | - simple_youtube_api/** 12 | - tests/** 13 | - setup.py 14 | branches: 'master' 15 | pull_request: 16 | branches: 'master' 17 | paths: 18 | - .github/workflows/** 19 | - simple_youtube_api/** 20 | - tests/** 21 | - setup.py 22 | 23 | jobs: 24 | pre-build: 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | python-version: [3.6, 3.7, 3.8] 29 | os: [windows-latest, ubuntu-latest, macos-latest] 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | - name: Set up Python ${{ matrix.python-version }} 34 | uses: actions/setup-python@v2 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | 38 | - name: Get pip cache dir 39 | id: pip-cache 40 | run: | 41 | echo "::set-output name=dir::$(pip cache dir)" 42 | 43 | - name: pip cache 44 | uses: actions/cache@v2 45 | id: cache 46 | with: 47 | path: ${{ steps.pip-cache.outputs.dir }} 48 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 49 | restore-keys: | 50 | ${{ runner.os }}-pip- 51 | 52 | - name: Install dependencies 53 | run: | 54 | python -m pip install --upgrade pip 55 | pip install wheel 56 | pip install pycodestyle 57 | pip install .[test] 58 | - name: Lint with pycodestyle 59 | run: | 60 | # stop the build if there are Python syntax errors or undefined names 61 | pycodestyle --max-line-length=127 --count --show-pep8 --show-source ./simple_youtube_api ./tests 62 | - name: Run unit tests with pytest 63 | run: | 64 | pytest tests/unit_test/ 65 | 66 | integ-test: 67 | runs-on: ubuntu-latest 68 | needs: [pre-build] 69 | strategy: 70 | matrix: 71 | python-version: [3.8] 72 | 73 | steps: 74 | - uses: actions/checkout@v2 75 | - name: Set up Python ${{ matrix.python-version }} 76 | uses: actions/setup-python@v2 77 | with: 78 | python-version: ${{ matrix.python-version }} 79 | - name: Install dependencies 80 | run: | 81 | python -m pip install --upgrade pip 82 | pip install pycodestyle 83 | pip install .[test] 84 | - name: Create Credential Files 85 | run: | 86 | mkdir credentials 87 | echo "${{ secrets.YOUTUBE_DEVELOPER_KEY }}" > "credentials/developer_key" 88 | echo "${{ secrets.CHANNEL_CREDENTIALS }}" > "credentials/credentials.storage" 89 | echo "${{ secrets.CLIENT_SECRET }}" > "credentials/client_secret.json" 90 | 91 | - name: Run all tests 92 | run: pytest tests/ --doctest-modules -v --cov simple_youtube_api --cov-report term-missing 93 | 94 | - name: Coveralls 95 | env: 96 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 97 | run: coveralls 98 | -------------------------------------------------------------------------------- /.github/workflows/python-package-unit-tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | # Only for running unit tests, doesn't use YouTube Quota so can be run more freely. 5 | name: Python package Unit Tests 6 | 7 | on: 8 | push: 9 | branches: '**' 10 | pull_request: 11 | branches: '**' 12 | 13 | jobs: 14 | lint: 15 | 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | python-version: [3.8] 20 | os: [windows-latest, ubuntu-latest, macos-latest] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install pycodestyle 33 | pip install pylint 34 | pip install .[test] 35 | 36 | - name: Lint with pycodestyle 37 | run: | 38 | # stop the build if there are Python syntax errors or undefined names 39 | pycodestyle --max-line-length=127 --count --show-pep8 --show-source ./simple_youtube_api ./tests 40 | 41 | - name: Lint with pylint 42 | run: | 43 | pylint --fail-under=9 simple_youtube_api tests 44 | 45 | build: 46 | 47 | needs: [lint] 48 | runs-on: ${{ matrix.os }} 49 | strategy: 50 | matrix: 51 | python-version: [3.6, 3.7, 3.8] 52 | os: [windows-latest, ubuntu-latest, macos-latest] 53 | 54 | steps: 55 | - uses: actions/checkout@v2 56 | - name: Set up Python ${{ matrix.python-version }} 57 | uses: actions/setup-python@v2 58 | with: 59 | python-version: ${{ matrix.python-version }} 60 | 61 | - name: Get pip cache dir 62 | id: pip-cache 63 | run: | 64 | echo "::set-output name=dir::$(pip cache dir)" 65 | 66 | - name: pip cache 67 | uses: actions/cache@v2 68 | id: cache 69 | with: 70 | path: ${{ steps.pip-cache.outputs.dir }} 71 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 72 | restore-keys: | 73 | ${{ runner.os }}-pip- 74 | 75 | - name: Install dependencies 76 | run: | 77 | python -m pip install --upgrade pip 78 | pip install pycodestyle 79 | pip install .[test] 80 | - name: Run unit tests with pytest 81 | run: | 82 | pytest tests/unit_test/ 83 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.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@v1 19 | with: 20 | python-version: '3.7' 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/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .vscode 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | .DS_Store 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | 109 | #youtube api files 110 | credentials.storage 111 | client_secret.json 112 | developer_key 113 | *.mp4 114 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Specify a score threshold to be exceeded before program exits with error. 9 | fail-under=10.0 10 | 11 | # Add files or directories to the blacklist. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the blacklist. The 16 | # regex matches against base names, not paths. 17 | ignore-patterns= 18 | 19 | # Python code to execute, usually for sys.path manipulation such as 20 | # pygtk.require(). 21 | #init-hook= 22 | 23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 24 | # number of processors available to use. 25 | jobs=1 26 | 27 | # Control the amount of potential inferred values when inferring a single 28 | # object. This can help the performance when dealing with large functions or 29 | # complex, nested conditions. 30 | limit-inference-results=100 31 | 32 | # List of plugins (as comma separated values of python module names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape, 142 | too-many-instance-attributes, # Naturally will have a lot 143 | too-few-public-methods, # Naturally some will not have a lot 144 | too-many-arguments, # Naturally some will have a lot 145 | too-many-locals, # Naturally some will have a lot 146 | fixme # Naturally will have some TODOs 147 | 148 | # Enable the message, report, category or checker with the given id(s). You can 149 | # either give multiple identifier separated by comma (,) or put this option 150 | # multiple time (only on the command line, not in the configuration file where 151 | # it should appear only once). See also the "--disable" option for examples. 152 | enable=c-extension-no-member 153 | 154 | 155 | [REPORTS] 156 | 157 | # Python expression which should return a score less than or equal to 10. You 158 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 159 | # which contain the number of messages in each category, as well as 'statement' 160 | # which is the total number of statements analyzed. This score is used by the 161 | # global evaluation report (RP0004). 162 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 163 | 164 | # Template used to display messages. This is a python new-style format string 165 | # used to format the message information. See doc for all details. 166 | #msg-template= 167 | 168 | # Set the output format. Available formats are text, parseable, colorized, json 169 | # and msvs (visual studio). You can also give a reporter class, e.g. 170 | # mypackage.mymodule.MyReporterClass. 171 | output-format=text 172 | 173 | # Tells whether to display a full report or only the messages. 174 | reports=no 175 | 176 | # Activate the evaluation score. 177 | score=yes 178 | 179 | 180 | [REFACTORING] 181 | 182 | # Maximum number of nested blocks for function / method body 183 | max-nested-blocks=5 184 | 185 | # Complete name of functions that never returns. When checking for 186 | # inconsistent-return-statements if a never returning function is called then 187 | # it will be considered as an explicit return statement and no message will be 188 | # printed. 189 | never-returning-functions=sys.exit 190 | 191 | 192 | [LOGGING] 193 | 194 | # The type of string formatting that logging methods do. `old` means using % 195 | # formatting, `new` is for `{}` formatting. 196 | logging-format-style=old 197 | 198 | # Logging modules to check that the string format arguments are in logging 199 | # function parameter format. 200 | logging-modules=logging 201 | 202 | 203 | [SPELLING] 204 | 205 | # Limits count of emitted suggestions for spelling mistakes. 206 | max-spelling-suggestions=4 207 | 208 | # Spelling dictionary name. Available dictionaries: none. To make it work, 209 | # install the python-enchant package. 210 | spelling-dict= 211 | 212 | # List of comma separated words that should not be checked. 213 | spelling-ignore-words= 214 | 215 | # A path to a file that contains the private dictionary; one word per line. 216 | spelling-private-dict-file= 217 | 218 | # Tells whether to store unknown words to the private dictionary (see the 219 | # --spelling-private-dict-file option) instead of raising a message. 220 | spelling-store-unknown-words=no 221 | 222 | 223 | [MISCELLANEOUS] 224 | 225 | # List of note tags to take in consideration, separated by a comma. 226 | notes=FIXME, 227 | XXX, 228 | TODO 229 | 230 | # Regular expression of note tags to take in consideration. 231 | #notes-rgx= 232 | 233 | 234 | [TYPECHECK] 235 | 236 | # List of decorators that produce context managers, such as 237 | # contextlib.contextmanager. Add to this list to register other decorators that 238 | # produce valid context managers. 239 | contextmanager-decorators=contextlib.contextmanager 240 | 241 | # List of members which are set dynamically and missed by pylint inference 242 | # system, and so shouldn't trigger E1101 when accessed. Python regular 243 | # expressions are accepted. 244 | generated-members= 245 | 246 | # Tells whether missing members accessed in mixin class should be ignored. A 247 | # mixin class is detected if its name ends with "mixin" (case insensitive). 248 | ignore-mixin-members=yes 249 | 250 | # Tells whether to warn about missing members when the owner of the attribute 251 | # is inferred to be None. 252 | ignore-none=yes 253 | 254 | # This flag controls whether pylint should warn about no-member and similar 255 | # checks whenever an opaque object is returned when inferring. The inference 256 | # can return multiple potential results while evaluating a Python object, but 257 | # some branches might not be evaluated, which results in partial inference. In 258 | # that case, it might be useful to still emit no-member and other checks for 259 | # the rest of the inferred objects. 260 | ignore-on-opaque-inference=yes 261 | 262 | # List of class names for which member attributes should not be checked (useful 263 | # for classes with dynamically set attributes). This supports the use of 264 | # qualified names. 265 | ignored-classes=optparse.Values,thread._local,_thread._local 266 | 267 | # List of module names for which member attributes should not be checked 268 | # (useful for modules/projects where namespaces are manipulated during runtime 269 | # and thus existing member attributes cannot be deduced by static analysis). It 270 | # supports qualified module names, as well as Unix pattern matching. 271 | ignored-modules= 272 | 273 | # Show a hint with possible names when a member name was not found. The aspect 274 | # of finding the hint is based on edit distance. 275 | missing-member-hint=yes 276 | 277 | # The minimum edit distance a name should have in order to be considered a 278 | # similar match for a missing member name. 279 | missing-member-hint-distance=1 280 | 281 | # The total number of similar names that should be taken in consideration when 282 | # showing a hint for a missing member. 283 | missing-member-max-choices=1 284 | 285 | # List of decorators that change the signature of a decorated function. 286 | signature-mutators= 287 | 288 | 289 | [VARIABLES] 290 | 291 | # List of additional names supposed to be defined in builtins. Remember that 292 | # you should avoid defining new builtins when possible. 293 | additional-builtins= 294 | 295 | # Tells whether unused global variables should be treated as a violation. 296 | allow-global-unused-variables=yes 297 | 298 | # List of strings which can identify a callback function by name. A callback 299 | # name must start or end with one of those strings. 300 | callbacks=cb_, 301 | _cb 302 | 303 | # A regular expression matching the name of dummy variables (i.e. expected to 304 | # not be used). 305 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 306 | 307 | # Argument names that match this expression will be ignored. Default to name 308 | # with leading underscore. 309 | ignored-argument-names=_.*|^ignored_|^unused_ 310 | 311 | # Tells whether we should check for unused import in __init__ files. 312 | init-import=no 313 | 314 | # List of qualified module names which can have objects that can redefine 315 | # builtins. 316 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 317 | 318 | 319 | [FORMAT] 320 | 321 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 322 | expected-line-ending-format= 323 | 324 | # Regexp for a line that is allowed to be longer than the limit. 325 | ignore-long-lines=^\s*(# )??$ 326 | 327 | # Number of spaces of indent required inside a hanging or continued line. 328 | indent-after-paren=4 329 | 330 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 331 | # tab). 332 | indent-string=' ' 333 | 334 | # Maximum number of characters on a single line. 335 | max-line-length=100 336 | 337 | # Maximum number of lines in a module. 338 | max-module-lines=1000 339 | 340 | # Allow the body of a class to be on the same line as the declaration if body 341 | # contains single statement. 342 | single-line-class-stmt=no 343 | 344 | # Allow the body of an if to be on the same line as the test if there is no 345 | # else. 346 | single-line-if-stmt=no 347 | 348 | 349 | [SIMILARITIES] 350 | 351 | # Ignore comments when computing similarities. 352 | ignore-comments=yes 353 | 354 | # Ignore docstrings when computing similarities. 355 | ignore-docstrings=yes 356 | 357 | # Ignore imports when computing similarities. 358 | ignore-imports=no 359 | 360 | # Minimum lines number of a similarity. 361 | min-similarity-lines=4 362 | 363 | 364 | [BASIC] 365 | 366 | # Naming style matching correct argument names. 367 | argument-naming-style=snake_case 368 | 369 | # Regular expression matching correct argument names. Overrides argument- 370 | # naming-style. 371 | #argument-rgx= 372 | 373 | # Naming style matching correct attribute names. 374 | attr-naming-style=snake_case 375 | 376 | # Regular expression matching correct attribute names. Overrides attr-naming- 377 | # style. 378 | attr-rgx=(([a-z_][a-z0-9_]{1,30})|(_[a-z0-9_]*)|(__[a-z][a-z0-9_]+__))$ 379 | 380 | # Bad variable names which should always be refused, separated by a comma. 381 | bad-names=foo, 382 | bar, 383 | baz, 384 | toto, 385 | tutu, 386 | tata 387 | 388 | # Bad variable names regexes, separated by a comma. If names match any regex, 389 | # they will always be refused 390 | bad-names-rgxs= 391 | 392 | # Naming style matching correct class attribute names. 393 | class-attribute-naming-style=any 394 | 395 | # Regular expression matching correct class attribute names. Overrides class- 396 | # attribute-naming-style. 397 | #class-attribute-rgx= 398 | 399 | # Naming style matching correct class names. 400 | class-naming-style=PascalCase 401 | 402 | # Regular expression matching correct class names. Overrides class-naming- 403 | # style. 404 | #class-rgx= 405 | 406 | # Naming style matching correct constant names. 407 | const-naming-style=UPPER_CASE 408 | 409 | # Regular expression matching correct constant names. Overrides const-naming- 410 | # style. 411 | #const-rgx= 412 | 413 | # Minimum line length for functions/classes that require docstrings, shorter 414 | # ones are exempt. 415 | docstring-min-length=-1 416 | 417 | # Naming style matching correct function names. 418 | function-naming-style=snake_case 419 | 420 | # Regular expression matching correct function names. Overrides function- 421 | # naming-style. 422 | #function-rgx= 423 | 424 | # Good variable names which should always be accepted, separated by a comma. 425 | good-names=i, 426 | j, 427 | k, 428 | ex, 429 | Run, 430 | _ 431 | 432 | # Good variable names regexes, separated by a comma. If names match any regex, 433 | # they will always be accepted 434 | good-names-rgxs= 435 | 436 | # Include a hint for the correct naming format with invalid-name. 437 | include-naming-hint=no 438 | 439 | # Naming style matching correct inline iteration names. 440 | inlinevar-naming-style=any 441 | 442 | # Regular expression matching correct inline iteration names. Overrides 443 | # inlinevar-naming-style. 444 | #inlinevar-rgx= 445 | 446 | # Naming style matching correct method names. 447 | method-naming-style=snake_case 448 | 449 | # Regular expression matching correct method names. Overrides method-naming- 450 | # style. 451 | #method-rgx= 452 | 453 | # Naming style matching correct module names. 454 | module-naming-style=snake_case 455 | 456 | # Regular expression matching correct module names. Overrides module-naming- 457 | # style. 458 | #module-rgx= 459 | 460 | # Colon-delimited sets of names that determine each other's naming style when 461 | # the name regexes allow several styles. 462 | name-group= 463 | 464 | # Regular expression which should only match function or class names that do 465 | # not require a docstring. 466 | no-docstring-rgx=^_ 467 | 468 | # List of decorators that produce properties, such as abc.abstractproperty. Add 469 | # to this list to register other decorators that produce valid properties. 470 | # These decorators are taken in consideration only for invalid-name. 471 | property-classes=abc.abstractproperty 472 | 473 | # Naming style matching correct variable names. 474 | variable-naming-style=snake_case 475 | 476 | # Regular expression matching correct variable names. Overrides variable- 477 | # naming-style. 478 | #variable-rgx= 479 | 480 | 481 | [STRING] 482 | 483 | # This flag controls whether inconsistent-quotes generates a warning when the 484 | # character used as a quote delimiter is used inconsistently within a module. 485 | check-quote-consistency=no 486 | 487 | # This flag controls whether the implicit-str-concat should generate a warning 488 | # on implicit string concatenation in sequences defined over several lines. 489 | check-str-concat-over-line-jumps=no 490 | 491 | 492 | [IMPORTS] 493 | 494 | # List of modules that can be imported at any level, not just the top level 495 | # one. 496 | allow-any-import-level= 497 | 498 | # Allow wildcard imports from modules that define __all__. 499 | allow-wildcard-with-all=no 500 | 501 | # Analyse import fallback blocks. This can be used to support both Python 2 and 502 | # 3 compatible code, which means that the block might have code that exists 503 | # only in one or another interpreter, leading to false positives when analysed. 504 | analyse-fallback-blocks=no 505 | 506 | # Deprecated modules which should not be used, separated by a comma. 507 | deprecated-modules=optparse,tkinter.tix 508 | 509 | # Create a graph of external dependencies in the given file (report RP0402 must 510 | # not be disabled). 511 | ext-import-graph= 512 | 513 | # Create a graph of every (i.e. internal and external) dependencies in the 514 | # given file (report RP0402 must not be disabled). 515 | import-graph= 516 | 517 | # Create a graph of internal dependencies in the given file (report RP0402 must 518 | # not be disabled). 519 | int-import-graph= 520 | 521 | # Force import order to recognize a module as part of the standard 522 | # compatibility libraries. 523 | known-standard-library= 524 | 525 | # Force import order to recognize a module as part of a third party library. 526 | known-third-party=enchant 527 | 528 | # Couples of modules and preferred modules, separated by a comma. 529 | preferred-modules= 530 | 531 | 532 | [CLASSES] 533 | 534 | # List of method names used to declare (i.e. assign) instance attributes. 535 | defining-attr-methods=__init__, 536 | __new__, 537 | setUp, 538 | __post_init__ 539 | 540 | # List of member names, which should be excluded from the protected access 541 | # warning. 542 | exclude-protected=_asdict, 543 | _fields, 544 | _replace, 545 | _source, 546 | _make 547 | 548 | # List of valid names for the first argument in a class method. 549 | valid-classmethod-first-arg=cls 550 | 551 | # List of valid names for the first argument in a metaclass class method. 552 | valid-metaclass-classmethod-first-arg=cls 553 | 554 | 555 | [DESIGN] 556 | 557 | # Maximum number of arguments for function / method. 558 | max-args=5 559 | 560 | # Maximum number of attributes for a class (see R0902). 561 | max-attributes=7 562 | 563 | # Maximum number of boolean expressions in an if statement (see R0916). 564 | max-bool-expr=5 565 | 566 | # Maximum number of branch for function / method body. 567 | max-branches=12 568 | 569 | # Maximum number of locals for function / method body. 570 | max-locals=15 571 | 572 | # Maximum number of parents for a class (see R0901). 573 | max-parents=7 574 | 575 | # Maximum number of public methods for a class (see R0904). 576 | max-public-methods=20 577 | 578 | # Maximum number of return / yield for function / method body. 579 | max-returns=6 580 | 581 | # Maximum number of statements in function / method body. 582 | max-statements=50 583 | 584 | # Minimum number of public methods for a class (see R0903). 585 | min-public-methods=2 586 | 587 | 588 | [EXCEPTIONS] 589 | 590 | # Exceptions that will emit a warning when being caught. Defaults to 591 | # "BaseException, Exception". 592 | overgeneral-exceptions=BaseException, 593 | Exception 594 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/source/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF and ePub 17 | formats: all 18 | 19 | # Optionally set the version of Python and requirements required to build your docs 20 | python: 21 | version: 3.7 22 | install: 23 | - method: pip 24 | path: . 25 | extra_requirements: 26 | - doc 27 | -------------------------------------------------------------------------------- /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 jonneka@gmail.com. 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 | # Simple YouTube API contribution guidelines 2 | 3 | ## Code standards 4 | - Use PEP8 Standard 5 | 6 | ## How to start contributing 7 | - Fork the repository from Github 8 | - Clone your fork: `git clone https://github.com/yourname/simple-youtube-api.git` 9 | - Add the main repository as a remote: `git remote add upstream https://github.com/jonnekaunisto/simple-youtube-api.git` 10 | - Go to https://cloud.google.com/ 11 | - Create a new Project 12 | - Search for "YouTube Data API v3" from the search bar 13 | - Click "Create Credentials" and choose "Web Server" 14 | - Select Public Data 15 | - Get your Public Data Developer Key and copy and paste it into a file in your project 16 | - Create another credential, but this time for User Data and download the file it gives you 17 | 18 | 19 | 20 | ## Standard workflow -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | LICENSE 2 | ======= 3 | MIT License 4 | 5 | Copyright (c) 2019-2020 Jonne Kaunisto 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Simple Youtube API 2 | ================== 3 | 4 | .. image:: https://badge.fury.io/py/simple-youtube-api.svg 5 | :target: https://badge.fury.io/py/simple-youtube-api 6 | :alt: Simple YouTube API page on the Python Package Index 7 | .. image:: https://github.com/jonnekaunisto/simple-youtube-api/workflows/Python%20package%20Integration%20Tests/badge.svg 8 | :target: https://github.com/jonnekaunisto/simple-youtube-api/actions?query=workflow%3A%22Python+package+Integration+Tests%22 9 | :alt: Build status on travis 10 | .. image:: https://coveralls.io/repos/github/jonnekaunisto/simple-youtube-api/badge.svg?branch=master 11 | :target: https://coveralls.io/github/jonnekaunisto/simple-youtube-api?branch=master 12 | :alt: Coverage on coveralls 13 | 14 | 15 | 16 | Simple Youtube API(full documentation_) is a Youtube API wrapper for python, making it easier to search and upload your videos. 17 | 18 | 19 | Examples 20 | -------- 21 | 22 | In this example we log in into a YouTube channel, set the appropriate variables for a video and upload the video to the YouTube channel that we logged into: 23 | 24 | .. code:: python 25 | 26 | from simple_youtube_api.Channel import Channel 27 | from simple_youtube_api.LocalVideo import LocalVideo 28 | 29 | # loggin into the channel 30 | channel = Channel() 31 | channel.login("client_secret.json", "credentials.storage") 32 | 33 | # setting up the video that is going to be uploaded 34 | video = LocalVideo(file_path="test_vid.mp4") 35 | 36 | # setting snippet 37 | video.set_title("My Title") 38 | video.set_description("This is a description") 39 | video.set_tags(["this", "tag"]) 40 | video.set_category("gaming") 41 | video.set_default_language("en-US") 42 | 43 | # setting status 44 | video.set_embeddable(True) 45 | video.set_license("creativeCommon") 46 | video.set_privacy_status("private") 47 | video.set_public_stats_viewable(True) 48 | 49 | # setting thumbnail 50 | video.set_thumbnail_path('test_thumb.png') 51 | 52 | # uploading video and printing the results 53 | video = channel.upload_video(video) 54 | print(video.id) 55 | print(video) 56 | 57 | # liking video 58 | video.like() 59 | 60 | 61 | 62 | Installation 63 | ------------ 64 | Simple YouTube API needs API keys from Google in order to be able to make queries to YouTube. 65 | 66 | **Installation by hand:** you can download the source files from PyPi or Github: 67 | 68 | .. code:: bash 69 | 70 | python setup.py install 71 | 72 | **Installation with pip:** make sure that you have ``pip`` installed, type this in a terminal: 73 | 74 | .. code:: bash 75 | 76 | pip install simple-youtube-api 77 | 78 | 79 | Generating YouTube API Keys 80 | --------------------------- 81 | 1. Log into https://console.cloud.google.com 82 | 2. Create a new Project 83 | 3. Search for "YouTube Data API V3" or go to https://console.cloud.google.com/apis/library/youtube.googleapis.com 84 | 4. Click Credentials 85 | 5. Click Create Credentials 86 | 87 | For user data: 88 | 89 | 1. Select OAuth Client ID 90 | 2. Select that you will call API from "Web Server" 91 | 3. Download or copy your API key from the Credentials tab 92 | 93 | For non-user data: 94 | 95 | 1. Select API Key 96 | 2. Paste the key into a file 97 | 98 | Running Tests 99 | ------------- 100 | Run the python command 101 | 102 | .. code:: bash 103 | 104 | python setup.py test 105 | 106 | References 107 | ---------- 108 | `YouTube API Documentation`_ 109 | 110 | `Python YouTube API Examples`_ 111 | 112 | 113 | Contribute 114 | ---------- 115 | 1. Fork the repository from Github 116 | 2. Clone your fork 117 | 118 | .. code:: bash 119 | 120 | git clone https://github.com/yourname/simple-youtube-api.git 121 | 122 | 3. Add the main repository as a remote 123 | 124 | .. code:: bash 125 | 126 | git remote add upstream https://github.com/jonnekaunisto/simple-youtube-api.git 127 | 128 | 4. Create a pull request and follow the guidelines 129 | 130 | 131 | Maintainers 132 | ----------- 133 | jonnekaunisto (owner) 134 | 135 | 136 | .. _`YouTube API Documentation`: https://developers.google.com/youtube/v3/docs/ 137 | .. _`Python YouTube API Examples`: https://github.com/youtube/api-samples/tree/master/python 138 | .. _documentation: https://simple-youtube-api.readthedocs.io/ 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/categories.txt: -------------------------------------------------------------------------------- 1 | 1 - Film & Animation 2 | 2 - Autos & Vehicles 3 | 10 - Music 4 | 15 - Pets & Animals 5 | 17 - Sports 6 | 18 - Short Movies 7 | 19 - Travel & Events 8 | 20 - Gaming 9 | 21 - Videoblogging 10 | 22 - People & Blogs 11 | 23 - Comedy 12 | 24 - Entertainment 13 | 25 - News & Politics 14 | 26 - Howto & Style 15 | 27 - Education 16 | 28 - Science & Technology 17 | 29 - Nonprofits & Activism 18 | 30 - Movies 19 | 31 - Anime/Animation 20 | 32 - Action/Adventure 21 | 33 - Classics 22 | 34 - Comedy 23 | 35 - Documentary 24 | 36 - Drama 25 | 37 - Family 26 | 38 - Foreign 27 | 39 - Horror 28 | 40 - Sci-Fi/Fantasy 29 | 41 - Thriller 30 | 42 - Shorts 31 | 43 - Shows 32 | 44 - Trailers 33 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/LICENSE.rst: -------------------------------------------------------------------------------- 1 | LICENSE 2 | ======= 3 | MIT License 4 | 5 | Copyright (c) 2019-2020 Jonne Kaunisto 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.abspath("../simple_youtube_api")) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "Simple YouTube API" 24 | copyright = "2019, Jonne Kaunisto" 25 | author = "Jonne Kaunisto" 26 | 27 | # The short X.Y version 28 | version = "0.2.2" 29 | # The full version, including alpha/beta/rc tags 30 | release = "0.2.2" 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | "sphinx.ext.autodoc", 44 | "sphinx.ext.todo", 45 | "sphinx.ext.viewcode", 46 | "sphinx.ext.autosummary", 47 | "sphinx_autodoc_typehints", 48 | ] 49 | 50 | autosummary_generate = True 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ["_templates"] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # 58 | # source_suffix = ['.rst', '.md'] 59 | source_suffix = ".rst" 60 | 61 | # The master toctree document. 62 | master_doc = "index" 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This pattern also affects html_static_path and html_extra_path. 74 | exclude_patterns = [] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = None 78 | 79 | 80 | # -- Options for HTML output ------------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | html_theme = "sphinx_rtd_theme" 86 | 87 | # Theme options are theme-specific and customize the look and feel of a theme 88 | # further. For a list of options available for each theme, see the 89 | # documentation. 90 | # 91 | # html_theme_options = {} 92 | 93 | # Add any paths that contain custom static files (such as style sheets) here, 94 | # relative to this directory. They are copied after the builtin static files, 95 | # so a file named "default.css" will overwrite the builtin "default.css". 96 | html_static_path = ["_static"] 97 | 98 | # Custom sidebar templates, must be a dictionary that maps document names 99 | # to template names. 100 | # 101 | # The default sidebars (for documents that don't match any pattern) are 102 | # defined by theme itself. Builtin themes are using these templates by 103 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 104 | # 'searchbox.html']``. 105 | # 106 | # html_sidebars = {} 107 | 108 | 109 | # -- Options for HTMLHelp output --------------------------------------------- 110 | 111 | # Output file base name for HTML help builder. 112 | htmlhelp_basename = "SimpleYouTubeAPIdoc" 113 | 114 | 115 | # -- Options for LaTeX output ------------------------------------------------ 116 | 117 | latex_elements = { 118 | # The paper size ('letterpaper' or 'a4paper'). 119 | # 120 | # 'papersize': 'letterpaper', 121 | # The font size ('10pt', '11pt' or '12pt'). 122 | # 123 | # 'pointsize': '10pt', 124 | # Additional stuff for the LaTeX preamble. 125 | # 126 | # 'preamble': '', 127 | # Latex figure (float) alignment 128 | # 129 | # 'figure_align': 'htbp', 130 | } 131 | 132 | # Grouping the document tree into LaTeX files. List of tuples 133 | # (source start file, target name, title, 134 | # author, documentclass [howto, manual, or own class]). 135 | latex_documents = [ 136 | ( 137 | master_doc, 138 | "SimpleYouTubeAPI.tex", 139 | "Simple YouTube API Documentation", 140 | "Jonne Kaunisto", 141 | "manual", 142 | ), 143 | ] 144 | 145 | 146 | # -- Options for manual page output ------------------------------------------ 147 | 148 | # One entry per manual page. List of tuples 149 | # (source start file, name, description, authors, manual section). 150 | man_pages = [ 151 | ( 152 | master_doc, 153 | "simpleyoutubeapi", 154 | "Simple YouTube API Documentation", 155 | [author], 156 | 1, 157 | ) 158 | ] 159 | 160 | 161 | # -- Options for Texinfo output ---------------------------------------------- 162 | 163 | # Grouping the document tree into Texinfo files. List of tuples 164 | # (source start file, target name, title, author, 165 | # dir menu entry, description, category) 166 | texinfo_documents = [ 167 | ( 168 | master_doc, 169 | "SimpleYouTubeAPI", 170 | "Simple YouTube API Documentation", 171 | author, 172 | "SimpleYouTubeAPI", 173 | "One line description of project.", 174 | "Miscellaneous", 175 | ), 176 | ] 177 | 178 | 179 | # -- Options for Epub output ------------------------------------------------- 180 | 181 | # Bibliographic Dublin Core info. 182 | epub_title = project 183 | 184 | # The unique identifier of the text. This can be a ISBN number 185 | # or the project homepage. 186 | # 187 | # epub_identifier = '' 188 | 189 | # A unique identification for the text. 190 | # 191 | # epub_uid = '' 192 | 193 | # A list of files that should not be packed into the epub file. 194 | epub_exclude_files = ["search.html"] 195 | 196 | 197 | # -- Extension configuration ------------------------------------------------- 198 | -------------------------------------------------------------------------------- /docs/source/examples/examples.rst: -------------------------------------------------------------------------------- 1 | .. _examples: 2 | 3 | Example Scripts 4 | =============== 5 | 6 | Here are a few example scripts to get you started. 7 | 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | youtube_upload 13 | youtube_search 14 | update_video 15 | fetch_my_uploads 16 | fetch_youtube_categories 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/source/examples/fetch_my_uploads.rst: -------------------------------------------------------------------------------- 1 | Fetch My Uploads 2 | ================= 3 | 4 | .. literalinclude:: ../../../examples/example_my_uploads.py -------------------------------------------------------------------------------- /docs/source/examples/fetch_youtube_categories.rst: -------------------------------------------------------------------------------- 1 | Fetch available categories in YouTube 2 | ===================================== 3 | 4 | .. literalinclude:: ../../../examples/example_fetch_youtube_categories.py -------------------------------------------------------------------------------- /docs/source/examples/update_video.rst: -------------------------------------------------------------------------------- 1 | Update a YouTube Video 2 | ====================== 3 | 4 | .. literalinclude:: ../../../examples/example_update_video.py -------------------------------------------------------------------------------- /docs/source/examples/youtube_search.rst: -------------------------------------------------------------------------------- 1 | YouTube Search 2 | ============== 3 | 4 | .. literalinclude:: ../../../examples/example_youtube_search.py -------------------------------------------------------------------------------- /docs/source/examples/youtube_upload.rst: -------------------------------------------------------------------------------- 1 | Upload a YouTube Video 2 | ====================== 3 | 4 | .. literalinclude:: ../../../examples/example_youtube_upload.py -------------------------------------------------------------------------------- /docs/source/getting_started/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started: 2 | 3 | 4 | Getting started with Simple YouTube API 5 | --------------------------------------- 6 | 7 | 8 | These pages explain everything you need to know to start using Simple YouTube API. 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Simple YouTube API's documentation! 2 | ============================================== 3 | 4 | Simple YouTube API is a YouTube API wrapper that supports uploading, fetching and updating videos and bunch of other basic YouTube features. 5 | 6 | 7 | Guide 8 | ^^^^^ 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | install 13 | getting_started/getting_started 14 | examples/examples 15 | ref/ref 16 | LICENSE 17 | 18 | 19 | Contribute ! 20 | -------------- 21 | Simple YouTube API is an open source Python YouTube API wrapper originally written by `Jonne Kaunisto`_. 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | 31 | .. _`Jonne Kaunisto`: https://github.com/jonnekaunisto -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Download and Installation 4 | ========================== 5 | 6 | 7 | Installation 8 | -------------- 9 | 10 | Simple YouTube API needs API keys from Google in order to be able to make queries to YouTube. 11 | 12 | **Installation by hand:** you can download the source files from PyPi or Github: 13 | 14 | .. code:: bash 15 | 16 | $ (sudo) python setup.py install 17 | 18 | **Installation with pip:** make sure that you have ``pip`` installed, type this in a terminal: 19 | 20 | .. code:: bash 21 | 22 | $ (sudo) pip install simple-youtube-api 23 | 24 | 25 | Generating YouTube API Keys 26 | --------------------------- 27 | 1. Log into https://console.cloud.google.com 28 | 2. Create a new Project 29 | 3. Search for "YouTube Data API V3" 30 | 4. Click Credentials 31 | 5. Click Create Credentials 32 | 6. Select that you will call API from "Web Server" 33 | 7. Select "Public Data" if you want to not get private data and "User Data" if you do 34 | 8. Download or copy your API key from the Credentials tab 35 | 36 | 37 | 38 | 39 | 40 | 41 | .. _`Numpy`: https://www.scipy.org/install.html 42 | .. _Decorator: https://pypi.python.org/pypi/decorator 43 | .. _tqdm: https://pypi.python.org/pypi/tqdm 44 | 45 | .. _ffmpeg: https://www.ffmpeg.org/download.html 46 | 47 | 48 | .. _imageMagick: https://www.imagemagick.org/script/index.php 49 | .. _Pygame: https://www.pygame.org/download.shtml 50 | .. _imageio: https://imageio.github.io/ 51 | 52 | .. _Pillow: https://pillow.readthedocs.org/en/latest/ 53 | .. _Scipy: https://www.scipy.org/ 54 | .. _`Scikit Image`: http://scikit-image.org/download.html 55 | 56 | .. _Github: https://github.com/Zulko/moviepy 57 | .. _PyPI: https://pypi.python.org/pypi/moviepy 58 | .. _`OpenCV 2.4.6`: https://sourceforge.net/projects/opencvlibrary/files/ 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/source/ref/Channel.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Channel 3 | ************ 4 | 5 | :class:`Channel` 6 | ========================== 7 | 8 | .. autoclass:: simple_youtube_api.Channel.Channel 9 | :members: 10 | :inherited-members: 11 | :show-inheritance: 12 | -------------------------------------------------------------------------------- /docs/source/ref/Comment.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Comment 3 | ************ 4 | 5 | :class:`CommentThread` 6 | ========================== 7 | 8 | .. autoclass:: simple_youtube_api.CommentThread.CommentThread 9 | :members: 10 | 11 | 12 | 13 | :class:`Comment` 14 | ========================== 15 | 16 | .. autoclass:: simple_youtube_api.Comment.Comment 17 | :members: 18 | -------------------------------------------------------------------------------- /docs/source/ref/Video.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Video 3 | ************ 4 | 5 | :class:`Video` 6 | ========================== 7 | 8 | .. autoclass:: simple_youtube_api.Video.Video 9 | :members: 10 | :inherited-members: 11 | :show-inheritance: 12 | 13 | :class:`LocalVideo` 14 | ========================== 15 | 16 | .. autoclass:: simple_youtube_api.LocalVideo.LocalVideo 17 | :members: 18 | :inherited-members: 19 | :show-inheritance: 20 | 21 | :class:`YouTubeVideo` 22 | ========================== 23 | 24 | .. autoclass:: simple_youtube_api.YouTubeVideo.YouTubeVideo 25 | :members: 26 | :inherited-members: 27 | :show-inheritance: 28 | -------------------------------------------------------------------------------- /docs/source/ref/ref.rst: -------------------------------------------------------------------------------- 1 | .. _reference_manual: 2 | 3 | 4 | Reference Manual 5 | ================ 6 | 7 | Reference manual for Simple YouTube API 8 | 9 | 10 | .. toctree:: 11 | :maxdepth: 3 12 | 13 | Video 14 | Channel 15 | Comment 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/example_fetch_youtube_categories.py: -------------------------------------------------------------------------------- 1 | from simple_youtube_api.YouTube import YouTube 2 | 3 | with open("developer_key", "r") as myfile: 4 | data = myfile.read().replace("\n", "") 5 | 6 | developer_key = data 7 | 8 | youtube = YouTube() 9 | youtube.login(developer_key) 10 | 11 | youtube.fetch_categories() 12 | -------------------------------------------------------------------------------- /examples/example_my_uploads.py: -------------------------------------------------------------------------------- 1 | from simple_youtube_api.Channel import Channel 2 | from simple_youtube_api.YouTubeVideo import YouTubeVideo 3 | 4 | 5 | channel = Channel() 6 | channel.login("client_secret.json", "credentials.storage") 7 | videos = channel.fetch_uploads() 8 | 9 | for video in videos: 10 | print(video) 11 | -------------------------------------------------------------------------------- /examples/example_update_video.py: -------------------------------------------------------------------------------- 1 | from simple_youtube_api.YouTubeVideo import YouTubeVideo 2 | from simple_youtube_api.Channel import Channel 3 | from simple_youtube_api.YouTube import YouTube 4 | 5 | with open("developer_key", "r") as myfile: 6 | data = myfile.read().replace("\n", "") 7 | 8 | developer_key = data 9 | youtube = YouTube() 10 | youtube.login(developer_key) 11 | 12 | video = youtube.search_by_video_id("aSw9blXxNJY") 13 | 14 | channel = Channel() 15 | channel.login("client_secret.json", "credentials.storage") 16 | 17 | 18 | video.update(channel, title="hehe") 19 | -------------------------------------------------------------------------------- /examples/example_youtube_search.py: -------------------------------------------------------------------------------- 1 | from simple_youtube_api.YouTube import YouTube 2 | 3 | with open("developer_key", "r") as myfile: 4 | data = myfile.read().replace("\n", "") 5 | 6 | developer_key = data 7 | 8 | # logging into youtube 9 | youtube = YouTube() 10 | youtube.login(developer_key) 11 | 12 | # searching videos with the term 13 | videos = youtube.search("Your Search Term") 14 | 15 | # printing results 16 | for video in videos: 17 | print(video) 18 | 19 | # search a specific video 20 | video = youtube.search_by_video_id("Ks-_Mh1QhMc") 21 | print(video) 22 | -------------------------------------------------------------------------------- /examples/example_youtube_upload.py: -------------------------------------------------------------------------------- 1 | from simple_youtube_api.Channel import Channel 2 | from simple_youtube_api.LocalVideo import LocalVideo 3 | 4 | # loggin into the channel 5 | channel = Channel() 6 | channel.login("client_secret.json", "credentials.storage") 7 | 8 | # setting up the video that is going to be uploaded 9 | video = LocalVideo(file_path="test_vid.mp4") 10 | 11 | # setting snippet 12 | video.set_title("My Title") 13 | video.set_description("This is a description") 14 | video.set_tags(["this", "tag"]) 15 | video.set_category("gaming") 16 | video.set_default_language("en-US") 17 | 18 | # setting status 19 | video.set_embeddable(True) 20 | video.set_license("creativeCommon") 21 | video.set_privacy_status("private") 22 | video.set_public_stats_viewable(True) 23 | 24 | # setting thumbnail 25 | #video.set_thumbnail_path("test_thumb.png") 26 | video.set_playlist("PLDjcYN-DQyqTeSzCg-54m4stTVyQaJrGi") 27 | 28 | # uploading video and printing the results 29 | video = channel.upload_video(video) 30 | print(video.id) 31 | print(video) 32 | 33 | # liking video 34 | video.like() 35 | -------------------------------------------------------------------------------- /examples/test_vid.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonnekaunisto/simple-youtube-api/704f136a4e9f56612caa14a9af128d88ed384202/examples/test_vid.mp4 -------------------------------------------------------------------------------- /resources/SCOPES: -------------------------------------------------------------------------------- 1 | https://www.googleapis.com/auth/youtube Manage your YouTube account 2 | https://www.googleapis.com/auth/youtube.force-ssl See, edit, and permanently delete your YouTube videos, ratings, comments and captions 3 | https://www.googleapis.com/auth/youtube.readonly View your YouTube account 4 | https://www.googleapis.com/auth/youtube.upload Manage your YouTube videos 5 | https://www.googleapis.com/auth/youtubepartner View and manage your assets and associated content on YouTube 6 | https://www.googleapis.com/auth/youtubepartner-channel-audit View private information of your YouTube channel relevant during the audit process with a YouTube partner 7 | -------------------------------------------------------------------------------- /resources/categories/categories.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#videoCategoryListResponse", 3 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/C8Xcwiv01jKatPMXR0g7W60SZLU\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#videoCategory", 7 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/Xy1mB4_yLrHy_BmKmPBggty2mZQ\"", 8 | "id": "1", 9 | "snippet": { 10 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 11 | "title": "Film & Animation", 12 | "assignable": true 13 | } 14 | }, 15 | { 16 | "kind": "youtube#videoCategory", 17 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/UZ1oLIIz2dxIhO45ZTFR3a3NyTA\"", 18 | "id": "2", 19 | "snippet": { 20 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 21 | "title": "Autos & Vehicles", 22 | "assignable": true 23 | } 24 | }, 25 | { 26 | "kind": "youtube#videoCategory", 27 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/nqRIq97-xe5XRZTxbknKFVe5Lmg\"", 28 | "id": "10", 29 | "snippet": { 30 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 31 | "title": "Music", 32 | "assignable": true 33 | } 34 | }, 35 | { 36 | "kind": "youtube#videoCategory", 37 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/HwXKamM1Q20q9BN-oBJavSGkfDI\"", 38 | "id": "15", 39 | "snippet": { 40 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 41 | "title": "Pets & Animals", 42 | "assignable": true 43 | } 44 | }, 45 | { 46 | "kind": "youtube#videoCategory", 47 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", 48 | "id": "17", 49 | "snippet": { 50 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 51 | "title": "Sports", 52 | "assignable": true 53 | } 54 | }, 55 | { 56 | "kind": "youtube#videoCategory", 57 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/FJwVpGCVZ1yiJrqZbpqe68Sy_OE\"", 58 | "id": "18", 59 | "snippet": { 60 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 61 | "title": "Short Movies", 62 | "assignable": false 63 | } 64 | }, 65 | { 66 | "kind": "youtube#videoCategory", 67 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/M-3iD9dwK7YJCafRf_DkLN8CouA\"", 68 | "id": "19", 69 | "snippet": { 70 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 71 | "title": "Travel & Events", 72 | "assignable": true 73 | } 74 | }, 75 | { 76 | "kind": "youtube#videoCategory", 77 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/WmA0qYEfjWsAoyJFSw2zinhn2wM\"", 78 | "id": "20", 79 | "snippet": { 80 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 81 | "title": "Gaming", 82 | "assignable": true 83 | } 84 | }, 85 | { 86 | "kind": "youtube#videoCategory", 87 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/EapFaGYG7K0StIXVf8aba249tdM\"", 88 | "id": "21", 89 | "snippet": { 90 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 91 | "title": "Videoblogging", 92 | "assignable": false 93 | } 94 | }, 95 | { 96 | "kind": "youtube#videoCategory", 97 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/xId8RX7vRN8rqkbYZbNIytUQDRo\"", 98 | "id": "22", 99 | "snippet": { 100 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 101 | "title": "People & Blogs", 102 | "assignable": true 103 | } 104 | }, 105 | { 106 | "kind": "youtube#videoCategory", 107 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/G9LHzQmx44rX2S5yaga_Aqtwz8M\"", 108 | "id": "23", 109 | "snippet": { 110 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 111 | "title": "Comedy", 112 | "assignable": true 113 | } 114 | }, 115 | { 116 | "kind": "youtube#videoCategory", 117 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/UVB9oxX2Bvqa_w_y3vXSLVK5E_s\"", 118 | "id": "24", 119 | "snippet": { 120 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 121 | "title": "Entertainment", 122 | "assignable": true 123 | } 124 | }, 125 | { 126 | "kind": "youtube#videoCategory", 127 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/QiLK0ZIrFoORdk_g2l_XR_ECjDc\"", 128 | "id": "25", 129 | "snippet": { 130 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 131 | "title": "News & Politics", 132 | "assignable": true 133 | } 134 | }, 135 | { 136 | "kind": "youtube#videoCategory", 137 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/r6Ck6Z0_L0rG37VJQR200SGNA_w\"", 138 | "id": "26", 139 | "snippet": { 140 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 141 | "title": "Howto & Style", 142 | "assignable": true 143 | } 144 | }, 145 | { 146 | "kind": "youtube#videoCategory", 147 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/EoYkczo9I3RCf96RveKTOgOPkUM\"", 148 | "id": "27", 149 | "snippet": { 150 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 151 | "title": "Education", 152 | "assignable": true 153 | } 154 | }, 155 | { 156 | "kind": "youtube#videoCategory", 157 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/w5HjcTD82G_XA3xBctS30zS-JpQ\"", 158 | "id": "28", 159 | "snippet": { 160 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 161 | "title": "Science & Technology", 162 | "assignable": true 163 | } 164 | }, 165 | { 166 | "kind": "youtube#videoCategory", 167 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/SalkJoBWq_smSEqiAx_qyri6Wa8\"", 168 | "id": "29", 169 | "snippet": { 170 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 171 | "title": "Nonprofits & Activism", 172 | "assignable": true 173 | } 174 | }, 175 | { 176 | "kind": "youtube#videoCategory", 177 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/lL7uWDr_071CHxifjYG1tJrp4Uo\"", 178 | "id": "30", 179 | "snippet": { 180 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 181 | "title": "Movies", 182 | "assignable": false 183 | } 184 | }, 185 | { 186 | "kind": "youtube#videoCategory", 187 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/WnuVfjO-PyFLO7NTRQIbrGE62nk\"", 188 | "id": "31", 189 | "snippet": { 190 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 191 | "title": "Anime/Animation", 192 | "assignable": false 193 | } 194 | }, 195 | { 196 | "kind": "youtube#videoCategory", 197 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/ctpH2hGA_UZ3volJT_FTlOg9M00\"", 198 | "id": "32", 199 | "snippet": { 200 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 201 | "title": "Action/Adventure", 202 | "assignable": false 203 | } 204 | }, 205 | { 206 | "kind": "youtube#videoCategory", 207 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/L0kR3-g1BAo5UD1PLVbQ7LkkDtQ\"", 208 | "id": "33", 209 | "snippet": { 210 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 211 | "title": "Classics", 212 | "assignable": false 213 | } 214 | }, 215 | { 216 | "kind": "youtube#videoCategory", 217 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/pUZOAC_s9sfiwar639qr_wAB-aI\"", 218 | "id": "34", 219 | "snippet": { 220 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 221 | "title": "Comedy", 222 | "assignable": false 223 | } 224 | }, 225 | { 226 | "kind": "youtube#videoCategory", 227 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/Xb5JLhtyNRN3AQq021Ds-OV50Jk\"", 228 | "id": "35", 229 | "snippet": { 230 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 231 | "title": "Documentary", 232 | "assignable": false 233 | } 234 | }, 235 | { 236 | "kind": "youtube#videoCategory", 237 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/u8WXzF4HIhtEi805__sqjuA4lEk\"", 238 | "id": "36", 239 | "snippet": { 240 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 241 | "title": "Drama", 242 | "assignable": false 243 | } 244 | }, 245 | { 246 | "kind": "youtube#videoCategory", 247 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/D04PP4Gr7wc4IV_O9G66Z4A8KWQ\"", 248 | "id": "37", 249 | "snippet": { 250 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 251 | "title": "Family", 252 | "assignable": false 253 | } 254 | }, 255 | { 256 | "kind": "youtube#videoCategory", 257 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/i5-_AceGXQCEEMWU0V8CcQm_vLQ\"", 258 | "id": "38", 259 | "snippet": { 260 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 261 | "title": "Foreign", 262 | "assignable": false 263 | } 264 | }, 265 | { 266 | "kind": "youtube#videoCategory", 267 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/rtlxd0zOixA9QHdIZB26-St5qgQ\"", 268 | "id": "39", 269 | "snippet": { 270 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 271 | "title": "Horror", 272 | "assignable": false 273 | } 274 | }, 275 | { 276 | "kind": "youtube#videoCategory", 277 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/N1TrDFLRppxZgBowCJfJCvh0Dpg\"", 278 | "id": "40", 279 | "snippet": { 280 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 281 | "title": "Sci-Fi/Fantasy", 282 | "assignable": false 283 | } 284 | }, 285 | { 286 | "kind": "youtube#videoCategory", 287 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/7UMGi6zRySqXopr_rv4sZq6Za2E\"", 288 | "id": "41", 289 | "snippet": { 290 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 291 | "title": "Thriller", 292 | "assignable": false 293 | } 294 | }, 295 | { 296 | "kind": "youtube#videoCategory", 297 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/RScXhi324h8usyIetreAVb-uKeM\"", 298 | "id": "42", 299 | "snippet": { 300 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 301 | "title": "Shorts", 302 | "assignable": false 303 | } 304 | }, 305 | { 306 | "kind": "youtube#videoCategory", 307 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/0n9MJVCDLpA8q7aiGVrFsuFsd0A\"", 308 | "id": "43", 309 | "snippet": { 310 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 311 | "title": "Shows", 312 | "assignable": false 313 | } 314 | }, 315 | { 316 | "kind": "youtube#videoCategory", 317 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/x5NxSf5fz8hn4loSN4rvhwzD_pY\"", 318 | "id": "44", 319 | "snippet": { 320 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 321 | "title": "Trailers", 322 | "assignable": false 323 | } 324 | } 325 | ] 326 | } 327 | -------------------------------------------------------------------------------- /resources/categories/categories.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonnekaunisto/simple-youtube-api/704f136a4e9f56612caa14a9af128d88ed384202/resources/categories/categories.pickle -------------------------------------------------------------------------------- /resources/categories/parse_categories.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | 4 | f = open("categories.json") 5 | 6 | data = json.load(f) 7 | 8 | categories = {} 9 | 10 | for item in data["items"]: 11 | if item["snippet"]["assignable"]: 12 | category_name = item["snippet"]["title"] 13 | category_id = item["id"] 14 | categories[category_name.lower()] = category_id 15 | 16 | 17 | with open("categories.pickle", "wb") as handle: 18 | pickle.dump(categories, handle, protocol=pickle.HIGHEST_PROTOCOL) 19 | 20 | 21 | with open("categories.pickle", "rb") as handle: 22 | b = pickle.load(handle) 23 | -------------------------------------------------------------------------------- /resources/categories/youtube_categories.txt: -------------------------------------------------------------------------------- 1 | {'film & animation': '1', 'autos & vehicles': '2', 'music': '10', 'pets & animals': '15', 'sports': '17', 'travel & events': '19', 'gaming': '20', 'people & blogs': '22', 'comedy': '23', 'entertainment': '24', 'news & politics': '25', 'howto & style': '26', 'education': '27', 'science & technology': '28', 'nonprofits & activism': '29'} -------------------------------------------------------------------------------- /resources/languages/languages.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#i18nLanguageListResponse", 3 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/qgFy24yvs-L_dNjr2d-Rd_Xcfw4\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#i18nLanguage", 7 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/GMrwiM1f-4KHxMka40cB3lysLgY\"", 8 | "id": "af", 9 | "snippet": { 10 | "hl": "af", 11 | "name": "Afrikaans" 12 | } 13 | }, 14 | { 15 | "kind": "youtube#i18nLanguage", 16 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/wOlCLE4kfCyCca9_ssuNDceE0yk\"", 17 | "id": "az", 18 | "snippet": { 19 | "hl": "az", 20 | "name": "Azerbaijani" 21 | } 22 | }, 23 | { 24 | "kind": "youtube#i18nLanguage", 25 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/1mc5QDsxoAICO6jscO-tFPgIu90\"", 26 | "id": "id", 27 | "snippet": { 28 | "hl": "id", 29 | "name": "Indonesian" 30 | } 31 | }, 32 | { 33 | "kind": "youtube#i18nLanguage", 34 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/wsT9bPntXoLehs2KLJYbqnGE0BU\"", 35 | "id": "ms", 36 | "snippet": { 37 | "hl": "ms", 38 | "name": "Malay" 39 | } 40 | }, 41 | { 42 | "kind": "youtube#i18nLanguage", 43 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/d4-apDn0ZDft0Yoco6Lj3wJYH-g\"", 44 | "id": "bs", 45 | "snippet": { 46 | "hl": "bs", 47 | "name": "Bosnian" 48 | } 49 | }, 50 | { 51 | "kind": "youtube#i18nLanguage", 52 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/hVswTFoMTSh7qztQXgsDjxgTETU\"", 53 | "id": "ca", 54 | "snippet": { 55 | "hl": "ca", 56 | "name": "Catalan" 57 | } 58 | }, 59 | { 60 | "kind": "youtube#i18nLanguage", 61 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/3Y90h664Ob7mJ0QMNKsBNUq4CU0\"", 62 | "id": "cs", 63 | "snippet": { 64 | "hl": "cs", 65 | "name": "Czech" 66 | } 67 | }, 68 | { 69 | "kind": "youtube#i18nLanguage", 70 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/ebVwqGAAMKQ8RnZHJ1v1zjSlLYc\"", 71 | "id": "da", 72 | "snippet": { 73 | "hl": "da", 74 | "name": "Danish" 75 | } 76 | }, 77 | { 78 | "kind": "youtube#i18nLanguage", 79 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/PGhdoQnaK-BWQU4ym2yZcuiqlvo\"", 80 | "id": "de", 81 | "snippet": { 82 | "hl": "de", 83 | "name": "German" 84 | } 85 | }, 86 | { 87 | "kind": "youtube#i18nLanguage", 88 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/jJeiNic4lStyfYgyad8EXXSH2JA\"", 89 | "id": "et", 90 | "snippet": { 91 | "hl": "et", 92 | "name": "Estonian" 93 | } 94 | }, 95 | { 96 | "kind": "youtube#i18nLanguage", 97 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/2vzKH692FQxYp_M1_rNk1yIFV8Y\"", 98 | "id": "en-GB", 99 | "snippet": { 100 | "hl": "en-GB", 101 | "name": "English (United Kingdom)" 102 | } 103 | }, 104 | { 105 | "kind": "youtube#i18nLanguage", 106 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/so_Y993qk4-_su7kjbFEx7nnUp4\"", 107 | "id": "en", 108 | "snippet": { 109 | "hl": "en", 110 | "name": "English" 111 | } 112 | }, 113 | { 114 | "kind": "youtube#i18nLanguage", 115 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/XWxnUXsW_IJhD5ZOwHqOQoxQwYY\"", 116 | "id": "es", 117 | "snippet": { 118 | "hl": "es", 119 | "name": "Spanish (Spain)" 120 | } 121 | }, 122 | { 123 | "kind": "youtube#i18nLanguage", 124 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/l-bosHudQSmdi5HROuUr_YdsSqE\"", 125 | "id": "es-419", 126 | "snippet": { 127 | "hl": "es-419", 128 | "name": "Spanish (Latin America)" 129 | } 130 | }, 131 | { 132 | "kind": "youtube#i18nLanguage", 133 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/SrBrtHLAGm7kadsQ4c5DgEs9WYo\"", 134 | "id": "es-US", 135 | "snippet": { 136 | "hl": "es-US", 137 | "name": "Spanish (United States)" 138 | } 139 | }, 140 | { 141 | "kind": "youtube#i18nLanguage", 142 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/kv8FTUm9fJESlhhsA0VHgaoW_9o\"", 143 | "id": "eu", 144 | "snippet": { 145 | "hl": "eu", 146 | "name": "Basque" 147 | } 148 | }, 149 | { 150 | "kind": "youtube#i18nLanguage", 151 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/NzJjZ2TVm87kGGnoltxAYXp8O0o\"", 152 | "id": "fil", 153 | "snippet": { 154 | "hl": "fil", 155 | "name": "Filipino" 156 | } 157 | }, 158 | { 159 | "kind": "youtube#i18nLanguage", 160 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/TNUHp9LsiTmgH8dlEf9JNhV4ohw\"", 161 | "id": "fr", 162 | "snippet": { 163 | "hl": "fr", 164 | "name": "French" 165 | } 166 | }, 167 | { 168 | "kind": "youtube#i18nLanguage", 169 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/YfJYO1tiIE29JfeN_UW0Tb5sJIY\"", 170 | "id": "fr-CA", 171 | "snippet": { 172 | "hl": "fr-CA", 173 | "name": "French (Canada)" 174 | } 175 | }, 176 | { 177 | "kind": "youtube#i18nLanguage", 178 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/P5dUevqZ6BlWyrDIDjZv60t4m7k\"", 179 | "id": "gl", 180 | "snippet": { 181 | "hl": "gl", 182 | "name": "Galician" 183 | } 184 | }, 185 | { 186 | "kind": "youtube#i18nLanguage", 187 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/vh1P5hcnWe2N_5cWm7RvNVyBFZI\"", 188 | "id": "hr", 189 | "snippet": { 190 | "hl": "hr", 191 | "name": "Croatian" 192 | } 193 | }, 194 | { 195 | "kind": "youtube#i18nLanguage", 196 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/1X1Ghj1zxxsI79xZqe3q1X5IH9M\"", 197 | "id": "zu", 198 | "snippet": { 199 | "hl": "zu", 200 | "name": "Zulu" 201 | } 202 | }, 203 | { 204 | "kind": "youtube#i18nLanguage", 205 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/SZDTKBO2oYN6xtKmjQA010KV2dk\"", 206 | "id": "is", 207 | "snippet": { 208 | "hl": "is", 209 | "name": "Icelandic" 210 | } 211 | }, 212 | { 213 | "kind": "youtube#i18nLanguage", 214 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/GkgF_P9qdb7fTnwkgbxcyn6m3Rs\"", 215 | "id": "it", 216 | "snippet": { 217 | "hl": "it", 218 | "name": "Italian" 219 | } 220 | }, 221 | { 222 | "kind": "youtube#i18nLanguage", 223 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/uNRd_FYvmSYFHYzF9aPsvAQsogQ\"", 224 | "id": "sw", 225 | "snippet": { 226 | "hl": "sw", 227 | "name": "Swahili" 228 | } 229 | }, 230 | { 231 | "kind": "youtube#i18nLanguage", 232 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/uPWPSnBy6yqRcKDBL1Qi4LnFoLg\"", 233 | "id": "lv", 234 | "snippet": { 235 | "hl": "lv", 236 | "name": "Latvian" 237 | } 238 | }, 239 | { 240 | "kind": "youtube#i18nLanguage", 241 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/DawyQ-FGzpJzc_8cNRqFbT07v_E\"", 242 | "id": "lt", 243 | "snippet": { 244 | "hl": "lt", 245 | "name": "Lithuanian" 246 | } 247 | }, 248 | { 249 | "kind": "youtube#i18nLanguage", 250 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/3V9J28NoQBu29aIOlAVChWF_Kns\"", 251 | "id": "hu", 252 | "snippet": { 253 | "hl": "hu", 254 | "name": "Hungarian" 255 | } 256 | }, 257 | { 258 | "kind": "youtube#i18nLanguage", 259 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/lJOztqYOgdQvvfmB0K_DdWuUeq8\"", 260 | "id": "nl", 261 | "snippet": { 262 | "hl": "nl", 263 | "name": "Dutch" 264 | } 265 | }, 266 | { 267 | "kind": "youtube#i18nLanguage", 268 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/t7VHxMiLEpiTvTnjo23cptlIWTQ\"", 269 | "id": "no", 270 | "snippet": { 271 | "hl": "no", 272 | "name": "Norwegian" 273 | } 274 | }, 275 | { 276 | "kind": "youtube#i18nLanguage", 277 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/C9uxQ_iPORF4ZWcc3WOcX1t70j0\"", 278 | "id": "uz", 279 | "snippet": { 280 | "hl": "uz", 281 | "name": "Uzbek" 282 | } 283 | }, 284 | { 285 | "kind": "youtube#i18nLanguage", 286 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/Nx2oULoIl34LYNPdcfpsu1H6lRU\"", 287 | "id": "pl", 288 | "snippet": { 289 | "hl": "pl", 290 | "name": "Polish" 291 | } 292 | }, 293 | { 294 | "kind": "youtube#i18nLanguage", 295 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/YdBgwFNd8IC3dSomi4FySFwlKJw\"", 296 | "id": "pt-PT", 297 | "snippet": { 298 | "hl": "pt-PT", 299 | "name": "Portuguese (Portugal)" 300 | } 301 | }, 302 | { 303 | "kind": "youtube#i18nLanguage", 304 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/qiKLbq_oWL4zljYGomyqgkH-6QA\"", 305 | "id": "pt", 306 | "snippet": { 307 | "hl": "pt", 308 | "name": "Portuguese (Brazil)" 309 | } 310 | }, 311 | { 312 | "kind": "youtube#i18nLanguage", 313 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/Pn58jAcxF7qX5ATMsX-ggAwuxOs\"", 314 | "id": "ro", 315 | "snippet": { 316 | "hl": "ro", 317 | "name": "Romanian" 318 | } 319 | }, 320 | { 321 | "kind": "youtube#i18nLanguage", 322 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/UplXAMXI0ufZC1c4ug1P9GLxSz4\"", 323 | "id": "sq", 324 | "snippet": { 325 | "hl": "sq", 326 | "name": "Albanian" 327 | } 328 | }, 329 | { 330 | "kind": "youtube#i18nLanguage", 331 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/Z3EiXrpvTORUdZkpL0tSXuGhwyU\"", 332 | "id": "sk", 333 | "snippet": { 334 | "hl": "sk", 335 | "name": "Slovak" 336 | } 337 | }, 338 | { 339 | "kind": "youtube#i18nLanguage", 340 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/OR2fBqiMEI-SgPkcbpUujYdt180\"", 341 | "id": "sl", 342 | "snippet": { 343 | "hl": "sl", 344 | "name": "Slovenian" 345 | } 346 | }, 347 | { 348 | "kind": "youtube#i18nLanguage", 349 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/K_iJM4hro7crlz-7aGI1Y_SwjNc\"", 350 | "id": "sr-Latn", 351 | "snippet": { 352 | "hl": "sr-Latn", 353 | "name": "Serbian (Latin)" 354 | } 355 | }, 356 | { 357 | "kind": "youtube#i18nLanguage", 358 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/c-zvGcAzJLR2YtImY-wpVGgQq8Q\"", 359 | "id": "fi", 360 | "snippet": { 361 | "hl": "fi", 362 | "name": "Finnish" 363 | } 364 | }, 365 | { 366 | "kind": "youtube#i18nLanguage", 367 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/zRiHhvImrdhnQR1mY6g7ze2abEM\"", 368 | "id": "sv", 369 | "snippet": { 370 | "hl": "sv", 371 | "name": "Swedish" 372 | } 373 | }, 374 | { 375 | "kind": "youtube#i18nLanguage", 376 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/SCTfVJMz9xZI2E44-7UJP5OMrbE\"", 377 | "id": "vi", 378 | "snippet": { 379 | "hl": "vi", 380 | "name": "Vietnamese" 381 | } 382 | }, 383 | { 384 | "kind": "youtube#i18nLanguage", 385 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/QhD4Q6EupU3gDCUaTu_GfQNZ0oc\"", 386 | "id": "tr", 387 | "snippet": { 388 | "hl": "tr", 389 | "name": "Turkish" 390 | } 391 | }, 392 | { 393 | "kind": "youtube#i18nLanguage", 394 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/KpMXRgwxyWN2een_d8ZmUKN49N4\"", 395 | "id": "be", 396 | "snippet": { 397 | "hl": "be", 398 | "name": "Belarusian" 399 | } 400 | }, 401 | { 402 | "kind": "youtube#i18nLanguage", 403 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/2h9ZelGleOUWIFGsIwOZPF0db7g\"", 404 | "id": "bg", 405 | "snippet": { 406 | "hl": "bg", 407 | "name": "Bulgarian" 408 | } 409 | }, 410 | { 411 | "kind": "youtube#i18nLanguage", 412 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/L7XQWQKAdB4RKyxcdT7_zF3Evqg\"", 413 | "id": "ky", 414 | "snippet": { 415 | "hl": "ky", 416 | "name": "Kyrgyz" 417 | } 418 | }, 419 | { 420 | "kind": "youtube#i18nLanguage", 421 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/oGC_uk6pN2Vvlm-Xpj2sOVqKKjI\"", 422 | "id": "kk", 423 | "snippet": { 424 | "hl": "kk", 425 | "name": "Kazakh" 426 | } 427 | }, 428 | { 429 | "kind": "youtube#i18nLanguage", 430 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/wYm6Y2DP6q9wzyLhrodUSCK5r4E\"", 431 | "id": "mk", 432 | "snippet": { 433 | "hl": "mk", 434 | "name": "Macedonian" 435 | } 436 | }, 437 | { 438 | "kind": "youtube#i18nLanguage", 439 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/Wjr61C4VmckZjh4xwUab8y4f0RU\"", 440 | "id": "mn", 441 | "snippet": { 442 | "hl": "mn", 443 | "name": "Mongolian" 444 | } 445 | }, 446 | { 447 | "kind": "youtube#i18nLanguage", 448 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/t2zC4SXLYVo5G2kfXk9-bePFRpk\"", 449 | "id": "ru", 450 | "snippet": { 451 | "hl": "ru", 452 | "name": "Russian" 453 | } 454 | }, 455 | { 456 | "kind": "youtube#i18nLanguage", 457 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/Ay3s66nL913XBNAEOd3CNd5PjtY\"", 458 | "id": "sr", 459 | "snippet": { 460 | "hl": "sr", 461 | "name": "Serbian" 462 | } 463 | }, 464 | { 465 | "kind": "youtube#i18nLanguage", 466 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/0brBZoxnBEUP-Du3i48gXWkA0S8\"", 467 | "id": "uk", 468 | "snippet": { 469 | "hl": "uk", 470 | "name": "Ukrainian" 471 | } 472 | }, 473 | { 474 | "kind": "youtube#i18nLanguage", 475 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/JfQhv6NxmYwz5mDVNBbo5Zm6U8I\"", 476 | "id": "el", 477 | "snippet": { 478 | "hl": "el", 479 | "name": "Greek" 480 | } 481 | }, 482 | { 483 | "kind": "youtube#i18nLanguage", 484 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/KylLrdp_i6vpR-ykX-_e3qbVclA\"", 485 | "id": "hy", 486 | "snippet": { 487 | "hl": "hy", 488 | "name": "Armenian" 489 | } 490 | }, 491 | { 492 | "kind": "youtube#i18nLanguage", 493 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/md5OauXuB2desazaJGSf4JO70_I\"", 494 | "id": "iw", 495 | "snippet": { 496 | "hl": "iw", 497 | "name": "Hebrew" 498 | } 499 | }, 500 | { 501 | "kind": "youtube#i18nLanguage", 502 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/Uz-9K5eVJ0MM2iz-ZCswjK4yqIs\"", 503 | "id": "ur", 504 | "snippet": { 505 | "hl": "ur", 506 | "name": "Urdu" 507 | } 508 | }, 509 | { 510 | "kind": "youtube#i18nLanguage", 511 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/mws5phUh7Qxy_uf06QWokL42X9g\"", 512 | "id": "ar", 513 | "snippet": { 514 | "hl": "ar", 515 | "name": "Arabic" 516 | } 517 | }, 518 | { 519 | "kind": "youtube#i18nLanguage", 520 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/GZemVMnoMUYY3wosO0nXf-_rk2o\"", 521 | "id": "fa", 522 | "snippet": { 523 | "hl": "fa", 524 | "name": "Persian" 525 | } 526 | }, 527 | { 528 | "kind": "youtube#i18nLanguage", 529 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/S3eqTLKBnwQX2WvwcHkMiltFRx8\"", 530 | "id": "ne", 531 | "snippet": { 532 | "hl": "ne", 533 | "name": "Nepali" 534 | } 535 | }, 536 | { 537 | "kind": "youtube#i18nLanguage", 538 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/CsWmhcPj0SiaL2VFA72Bp7ohDSE\"", 539 | "id": "mr", 540 | "snippet": { 541 | "hl": "mr", 542 | "name": "Marathi" 543 | } 544 | }, 545 | { 546 | "kind": "youtube#i18nLanguage", 547 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/JCyuVDfWcOgtaZwswnZyy1AzR5I\"", 548 | "id": "hi", 549 | "snippet": { 550 | "hl": "hi", 551 | "name": "Hindi" 552 | } 553 | }, 554 | { 555 | "kind": "youtube#i18nLanguage", 556 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/dhFhF7UIAmlZRrmvX_erFuBQCP4\"", 557 | "id": "bn", 558 | "snippet": { 559 | "hl": "bn", 560 | "name": "Bangla" 561 | } 562 | }, 563 | { 564 | "kind": "youtube#i18nLanguage", 565 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/qSMM8n3SiMBRIEwoCukCVsN_U1M\"", 566 | "id": "pa", 567 | "snippet": { 568 | "hl": "pa", 569 | "name": "Punjabi" 570 | } 571 | }, 572 | { 573 | "kind": "youtube#i18nLanguage", 574 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/CamKnOPN6X6DO5N_w2M62uu-BBo\"", 575 | "id": "gu", 576 | "snippet": { 577 | "hl": "gu", 578 | "name": "Gujarati" 579 | } 580 | }, 581 | { 582 | "kind": "youtube#i18nLanguage", 583 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/UpkJlfs19zVrDL4jxmko2TFfeiI\"", 584 | "id": "ta", 585 | "snippet": { 586 | "hl": "ta", 587 | "name": "Tamil" 588 | } 589 | }, 590 | { 591 | "kind": "youtube#i18nLanguage", 592 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/snSOiVsugZraJQmQooFEUgwrvGg\"", 593 | "id": "te", 594 | "snippet": { 595 | "hl": "te", 596 | "name": "Telugu" 597 | } 598 | }, 599 | { 600 | "kind": "youtube#i18nLanguage", 601 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/pTwsDdi2u1LezXypsaalDTmGsos\"", 602 | "id": "kn", 603 | "snippet": { 604 | "hl": "kn", 605 | "name": "Kannada" 606 | } 607 | }, 608 | { 609 | "kind": "youtube#i18nLanguage", 610 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/j9vpsCSOrQBCuqW95C1wNgF_OPY\"", 611 | "id": "ml", 612 | "snippet": { 613 | "hl": "ml", 614 | "name": "Malayalam" 615 | } 616 | }, 617 | { 618 | "kind": "youtube#i18nLanguage", 619 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/4q_QN7GcwgP5b_DbB3EEc5xAW8U\"", 620 | "id": "si", 621 | "snippet": { 622 | "hl": "si", 623 | "name": "Sinhala" 624 | } 625 | }, 626 | { 627 | "kind": "youtube#i18nLanguage", 628 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/_E_YSjVSJtbo4nl97gnQer9POqA\"", 629 | "id": "th", 630 | "snippet": { 631 | "hl": "th", 632 | "name": "Thai" 633 | } 634 | }, 635 | { 636 | "kind": "youtube#i18nLanguage", 637 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/2jrl7H3eP2Z63pm8-Ag-NrenlDY\"", 638 | "id": "lo", 639 | "snippet": { 640 | "hl": "lo", 641 | "name": "Lao" 642 | } 643 | }, 644 | { 645 | "kind": "youtube#i18nLanguage", 646 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/f-CmZu2q-Huf-NSgtSZ31BNLcss\"", 647 | "id": "my", 648 | "snippet": { 649 | "hl": "my", 650 | "name": "Myanmar (Burmese)" 651 | } 652 | }, 653 | { 654 | "kind": "youtube#i18nLanguage", 655 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/TCXP3ae7Akd-IrzsHK5lMTeFO64\"", 656 | "id": "ka", 657 | "snippet": { 658 | "hl": "ka", 659 | "name": "Georgian" 660 | } 661 | }, 662 | { 663 | "kind": "youtube#i18nLanguage", 664 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/0TSfvf4f7k04LUHD7rSbdwlcbrI\"", 665 | "id": "am", 666 | "snippet": { 667 | "hl": "am", 668 | "name": "Amharic" 669 | } 670 | }, 671 | { 672 | "kind": "youtube#i18nLanguage", 673 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/RUsS-Iy62HUcG2uy3VN0qZhMQy0\"", 674 | "id": "km", 675 | "snippet": { 676 | "hl": "km", 677 | "name": "Khmer" 678 | } 679 | }, 680 | { 681 | "kind": "youtube#i18nLanguage", 682 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/wD2SRT6G7gbFH07ePlumHAynSRo\"", 683 | "id": "zh-CN", 684 | "snippet": { 685 | "hl": "zh-CN", 686 | "name": "Chinese" 687 | } 688 | }, 689 | { 690 | "kind": "youtube#i18nLanguage", 691 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/6fRre896XUzNQjc89q329PaFKjE\"", 692 | "id": "zh-TW", 693 | "snippet": { 694 | "hl": "zh-TW", 695 | "name": "Chinese (Taiwan)" 696 | } 697 | }, 698 | { 699 | "kind": "youtube#i18nLanguage", 700 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/MbipoDEFiRRUlYr5UzjpCwXXRMc\"", 701 | "id": "zh-HK", 702 | "snippet": { 703 | "hl": "zh-HK", 704 | "name": "Chinese (Hong Kong)" 705 | } 706 | }, 707 | { 708 | "kind": "youtube#i18nLanguage", 709 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/XBmnabKsnR1WSWcZvNxC2NvrLYo\"", 710 | "id": "ja", 711 | "snippet": { 712 | "hl": "ja", 713 | "name": "Japanese" 714 | } 715 | }, 716 | { 717 | "kind": "youtube#i18nLanguage", 718 | "etag": "\"XpPGQXPnxQJhLgs6enD_n8JR4Qk/l06bH0pLscIVm87oyBnJ3aZR4Ts\"", 719 | "id": "ko", 720 | "snippet": { 721 | "hl": "ko", 722 | "name": "Korean" 723 | } 724 | } 725 | ] 726 | } 727 | -------------------------------------------------------------------------------- /resources/languages/parse_languages.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | f = open("languages.json") 4 | 5 | data = json.load(f) 6 | 7 | languages = [] 8 | 9 | for item in data["items"]: 10 | language = item["snippet"]["name"] 11 | languages.append(language) 12 | 13 | o = open("youtube_supported_languages.txt", "w") 14 | o.write(str(languages)) 15 | -------------------------------------------------------------------------------- /resources/languages/youtube_supported_languages.txt: -------------------------------------------------------------------------------- 1 | ['Afrikaans', 'Azerbaijani', 'Indonesian', 'Malay', 'Bosnian', 'Catalan', 'Czech', 'Danish', 'German', 'Estonian', 'English (United Kingdom)', 'English', 'Spanish (Spain)', 'Spanish (Latin America)', 'Spanish (United States)', 'Basque', 'Filipino', 'French', 'French (Canada)', 'Galician', 'Croatian', 'Zulu', 'Icelandic', 'Italian', 'Swahili', 'Latvian', 'Lithuanian', 'Hungarian', 'Dutch', 'Norwegian', 'Uzbek', 'Polish', 'Portuguese (Portugal)', 'Portuguese (Brazil)', 'Romanian', 'Albanian', 'Slovak', 'Slovenian', 'Serbian (Latin)', 'Finnish', 'Swedish', 'Vietnamese', 'Turkish', 'Belarusian', 'Bulgarian', 'Kyrgyz', 'Kazakh', 'Macedonian', 'Mongolian', 'Russian', 'Serbian', 'Ukrainian', 'Greek', 'Armenian', 'Hebrew', 'Urdu', 'Arabic', 'Persian', 'Nepali', 'Marathi', 'Hindi', 'Bangla', 'Punjabi', 'Gujarati', 'Tamil', 'Telugu', 'Kannada', 'Malayalam', 'Sinhala', 'Thai', 'Lao', 'Myanmar (Burmese)', 'Georgian', 'Amharic', 'Khmer', 'Chinese', 'Chinese (Taiwan)', 'Chinese (Hong Kong)', 'Japanese', 'Korean'] -------------------------------------------------------------------------------- /resources/schema/channel.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channel", 3 | "etag": "etag", 4 | "id": "string", 5 | "snippet": { 6 | "title": "string", 7 | "description": "string", 8 | "customUrl": "string", 9 | "publishedAt": "datetime", 10 | "thumbnails": { 11 | "(key)": { 12 | "url": "string", 13 | "width": "unsigned integer", 14 | "height": "unsigned integer" 15 | } 16 | }, 17 | "defaultLanguage": "string", 18 | "localized": { 19 | "title": "string", 20 | "description": "string" 21 | }, 22 | "country": "string" 23 | }, 24 | "contentDetails": { 25 | "relatedPlaylists": { 26 | "likes": "string", 27 | "favorites": "string", 28 | "uploads": "string", 29 | "watchHistory": "string", 30 | "watchLater": "string" 31 | } 32 | }, 33 | "statistics": { 34 | "viewCount": "unsigned long", 35 | "commentCount": "unsigned long", 36 | "subscriberCount": "unsigned long", // this value is rounded to three significant figures 37 | "hiddenSubscriberCount": "boolean", 38 | "videoCount": "unsigned long" 39 | }, 40 | "topicDetails": { 41 | "topicIds": [ 42 | "string" 43 | ], 44 | "topicCategories": [ 45 | "string" 46 | ] 47 | }, 48 | "status": { 49 | "privacyStatus": "string", 50 | "isLinked": "boolean", 51 | "longUploadsStatus": "string" 52 | }, 53 | "brandingSettings": { 54 | "channel": { 55 | "title": "string", 56 | "description": "string", 57 | "keywords": "string", 58 | "defaultTab": "string", 59 | "trackingAnalyticsAccountId": "string", 60 | "moderateComments": "boolean", 61 | "showRelatedChannels": "boolean", 62 | "showBrowseView": "boolean", 63 | "featuredChannelsTitle": "string", 64 | "featuredChannelsUrls": [ 65 | "string" 66 | ], 67 | "unsubscribedTrailer": "string", 68 | "profileColor": "string", 69 | "defaultLanguage": "string", 70 | "country": "string" 71 | }, 72 | "watch": { 73 | "textColor": "string", 74 | "backgroundColor": "string", 75 | "featuredPlaylistId": "string" 76 | }, 77 | "image": { 78 | "bannerImageUrl": "string", 79 | "bannerMobileImageUrl": "string", 80 | "watchIconImageUrl": "string", 81 | "trackingImageUrl": "string", 82 | "bannerTabletLowImageUrl": "string", 83 | "bannerTabletImageUrl": "string", 84 | "bannerTabletHdImageUrl": "string", 85 | "bannerTabletExtraHdImageUrl": "string", 86 | "bannerMobileLowImageUrl": "string", 87 | "bannerMobileMediumHdImageUrl": "string", 88 | "bannerMobileHdImageUrl": "string", 89 | "bannerMobileExtraHdImageUrl": "string", 90 | "bannerTvImageUrl": "string", 91 | "bannerTvLowImageUrl": "string", 92 | "bannerTvMediumImageUrl": "string", 93 | "bannerTvHighImageUrl": "string", 94 | "bannerExternalUrl": "string" 95 | }, 96 | "hints": [ 97 | { 98 | "property": "string", 99 | "value": "string" 100 | } 101 | ] 102 | }, 103 | "invideoPromotion": { 104 | "defaultTiming": { 105 | "type": "string", 106 | "offsetMs": "unsigned long", 107 | "durationMs": "unsigned long" 108 | }, 109 | "position": { 110 | "type": "string", 111 | "cornerPosition": "string" 112 | }, 113 | "items": [ 114 | { 115 | "id": { 116 | "type": "string", 117 | "videoId": "string", 118 | "websiteUrl": "string", 119 | "recentlyUploadedBy": "string" 120 | }, 121 | "timing": { 122 | "type": "string", 123 | "offsetMs": "unsigned long", 124 | "durationMs": "unsigned long" 125 | }, 126 | "customMessage": "string", 127 | "promotedByContentOwner": "boolean" 128 | } 129 | ], 130 | "useSmartTiming": "boolean" 131 | }, 132 | "auditDetails": { 133 | "overallGoodStanding": "boolean", 134 | "communityGuidelinesGoodStanding": "boolean", 135 | "copyrightStrikesGoodStanding": "boolean", 136 | "contentIdClaimsGoodStanding": "boolean" 137 | }, 138 | "contentOwnerDetails": { 139 | "contentOwner": "string", 140 | "timeLinked": "datetime" 141 | }, 142 | "localizations": { 143 | "(key)": { 144 | "title": "string", 145 | "description": "string" 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /resources/schema/channel_sections.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channelSection", 3 | "etag": "etag", 4 | "id": "string", 5 | "snippet": { 6 | "type": "string", 7 | "style": "string", 8 | "channelId": "string", 9 | "title": "string", 10 | "position": "unsigned integer", 11 | "defaultLanguage": "string", 12 | "localized": { 13 | "title": "string" 14 | } 15 | }, 16 | "contentDetails": { 17 | "playlists": [ 18 | "string" 19 | ], 20 | "channels": [ 21 | "string" 22 | ] 23 | }, 24 | "localizations": { 25 | "(key)": { 26 | "title": "string" 27 | } 28 | }, 29 | "targeting": { 30 | "languages": [ 31 | "string" 32 | ], 33 | "regions": [ 34 | "string" 35 | ], 36 | "countries": [ 37 | "string" 38 | ] 39 | } 40 | } -------------------------------------------------------------------------------- /resources/schema/comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#comment", 3 | "etag": "etag", 4 | "id": "string", 5 | "snippet": { 6 | "authorDisplayName": "string", 7 | "authorProfileImageUrl": "string", 8 | "authorChannelUrl": "string", 9 | "authorChannelId": { 10 | "value": "string" 11 | }, 12 | "channelId": "string", 13 | "videoId": "string", 14 | "textDisplay": "string", 15 | "textOriginal": "string", 16 | "parentId": "string", 17 | "canRate": "boolean", 18 | "viewerRating": "string", 19 | "likeCount": "unsigned integer", 20 | "moderationStatus": "string", 21 | "publishedAt": "datetime", 22 | "updatedAt": "datetime" 23 | } 24 | } -------------------------------------------------------------------------------- /resources/schema/playlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#playlist", 3 | "etag": "etag", 4 | "id": "string", 5 | "snippet": { 6 | "publishedAt": "datetime", 7 | "channelId": "string", 8 | "title": "string", 9 | "description": "string", 10 | "thumbnails": { 11 | "(key)": { 12 | "url": "string", 13 | "width": "unsigned integer", 14 | "height": "unsigned integer" 15 | } 16 | }, 17 | "channelTitle": "string", 18 | "tags": [ 19 | "string" 20 | ], 21 | "defaultLanguage": "string", 22 | "localized": { 23 | "title": "string", 24 | "description": "string" 25 | } 26 | }, 27 | "status": { 28 | "privacyStatus": "string" 29 | }, 30 | "contentDetails": { 31 | "itemCount": "unsigned integer" 32 | }, 33 | "player": { 34 | "embedHtml": "string" 35 | }, 36 | "localizations": { 37 | "(key)": { 38 | "title": "string", 39 | "description": "string" 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /resources/schema/video.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#video", 3 | "etag": "etag", 4 | "id": "string", 5 | "snippet": { 6 | "publishedAt": "datetime", 7 | "channelId": "string", 8 | "title": "string", 9 | "description": "string", 10 | "thumbnails": { 11 | "(key)": { 12 | "url": "string", 13 | "width": "unsigned integer", 14 | "height": "unsigned integer" 15 | } 16 | }, 17 | "channelTitle": "string", 18 | "tags": [ 19 | "string" 20 | ], 21 | "categoryId": "string", 22 | "liveBroadcastContent": "string", 23 | "defaultLanguage": "string", 24 | "localized": { 25 | "title": "string", 26 | "description": "string" 27 | }, 28 | "defaultAudioLanguage": "string" 29 | }, 30 | "contentDetails": { 31 | "duration": "string", 32 | "dimension": "string", 33 | "definition": "string", 34 | "caption": "string", 35 | "licensedContent": "boolean", 36 | "regionRestriction": { 37 | "allowed": [ 38 | "string" 39 | ], 40 | "blocked": [ 41 | "string" 42 | ] 43 | }, 44 | "contentRating": { 45 | "acbRating": "string", 46 | "agcomRating": "string", 47 | "anatelRating": "string", 48 | "bbfcRating": "string", 49 | "bfvcRating": "string", 50 | "bmukkRating": "string", 51 | "catvRating": "string", 52 | "catvfrRating": "string", 53 | "cbfcRating": "string", 54 | "cccRating": "string", 55 | "cceRating": "string", 56 | "chfilmRating": "string", 57 | "chvrsRating": "string", 58 | "cicfRating": "string", 59 | "cnaRating": "string", 60 | "cncRating": "string", 61 | "csaRating": "string", 62 | "cscfRating": "string", 63 | "czfilmRating": "string", 64 | "djctqRating": "string", 65 | "djctqRatingReasons": [ 66 | "string", 67 | "string" 68 | ], 69 | "ecbmctRating": "string", 70 | "eefilmRating": "string", 71 | "egfilmRating": "string", 72 | "eirinRating": "string", 73 | "fcbmRating": "string", 74 | "fcoRating": "string", 75 | "fmocRating": "string", 76 | "fpbRating": "string", 77 | "fpbRatingReasons": [ 78 | "string", 79 | "string" 80 | ], 81 | "fskRating": "string", 82 | "grfilmRating": "string", 83 | "icaaRating": "string", 84 | "ifcoRating": "string", 85 | "ilfilmRating": "string", 86 | "incaaRating": "string", 87 | "kfcbRating": "string", 88 | "kijkwijzerRating": "string", 89 | "kmrbRating": "string", 90 | "lsfRating": "string", 91 | "mccaaRating": "string", 92 | "mccypRating": "string", 93 | "mcstRating": "string", 94 | "mdaRating": "string", 95 | "medietilsynetRating": "string", 96 | "mekuRating": "string", 97 | "mibacRating": "string", 98 | "mocRating": "string", 99 | "moctwRating": "string", 100 | "mpaaRating": "string", 101 | "mpaatRating": "string", 102 | "mtrcbRating": "string", 103 | "nbcRating": "string", 104 | "nbcplRating": "string", 105 | "nfrcRating": "string", 106 | "nfvcbRating": "string", 107 | "nkclvRating": "string", 108 | "oflcRating": "string", 109 | "pefilmRating": "string", 110 | "rcnofRating": "string", 111 | "resorteviolenciaRating": "string", 112 | "rtcRating": "string", 113 | "rteRating": "string", 114 | "russiaRating": "string", 115 | "skfilmRating": "string", 116 | "smaisRating": "string", 117 | "smsaRating": "string", 118 | "tvpgRating": "string", 119 | "ytRating": "string" 120 | }, 121 | "projection": "string", 122 | "hasCustomThumbnail": "boolean" 123 | }, 124 | "status": { 125 | "uploadStatus": "string", 126 | "failureReason": "string", 127 | "rejectionReason": "string", 128 | "privacyStatus": "string", 129 | "publishAt": "datetime", 130 | "license": "string", 131 | "embeddable": "boolean", 132 | "publicStatsViewable": "boolean" 133 | }, 134 | "statistics": { 135 | "viewCount": "unsigned long", 136 | "likeCount": "unsigned long", 137 | "dislikeCount": "unsigned long", 138 | "favoriteCount": "unsigned long", 139 | "commentCount": "unsigned long" 140 | }, 141 | "player": { 142 | "embedHtml": "string", 143 | "embedHeight": "long", 144 | "embedWidth": "long" 145 | }, 146 | "topicDetails": { 147 | "topicIds": [ 148 | "string" 149 | ], 150 | "relevantTopicIds": [ 151 | "string" 152 | ], 153 | "topicCategories": [ 154 | "string" 155 | ] 156 | }, 157 | "recordingDetails": { 158 | "recordingDate": "datetime" 159 | }, 160 | "fileDetails": { 161 | "fileName": "string", 162 | "fileSize": "unsigned long", 163 | "fileType": "string", 164 | "container": "string", 165 | "videoStreams": [ 166 | { 167 | "widthPixels": "unsigned integer", 168 | "heightPixels": "unsigned integer", 169 | "frameRateFps": "double", 170 | "aspectRatio": "double", 171 | "codec": "string", 172 | "bitrateBps": "unsigned long", 173 | "rotation": "string", 174 | "vendor": "string" 175 | } 176 | ], 177 | "audioStreams": [ 178 | { 179 | "channelCount": "unsigned integer", 180 | "codec": "string", 181 | "bitrateBps": "unsigned long", 182 | "vendor": "string" 183 | } 184 | ], 185 | "durationMs": "unsigned long", 186 | "bitrateBps": "unsigned long", 187 | "creationTime": "string" 188 | }, 189 | "processingDetails": { 190 | "processingStatus": "string", 191 | "processingProgress": { 192 | "partsTotal": "unsigned long", 193 | "partsProcessed": "unsigned long", 194 | "timeLeftMs": "unsigned long" 195 | }, 196 | "processingFailureReason": "string", 197 | "fileDetailsAvailability": "string", 198 | "processingIssuesAvailability": "string", 199 | "tagSuggestionsAvailability": "string", 200 | "editorSuggestionsAvailability": "string", 201 | "thumbnailsAvailability": "string" 202 | }, 203 | "suggestions": { 204 | "processingErrors": [ 205 | "string" 206 | ], 207 | "processingWarnings": [ 208 | "string" 209 | ], 210 | "processingHints": [ 211 | "string" 212 | ], 213 | "tagSuggestions": [ 214 | { 215 | "tag": "string", 216 | "categoryRestricts": [ 217 | "string" 218 | ] 219 | } 220 | ], 221 | "editorSuggestions": [ 222 | "string" 223 | ] 224 | }, 225 | "liveStreamingDetails": { 226 | "actualStartTime": "datetime", 227 | "actualEndTime": "datetime", 228 | "scheduledStartTime": "datetime", 229 | "scheduledEndTime": "datetime", 230 | "concurrentViewers": "unsigned long", 231 | "activeLiveChatId": "string" 232 | }, 233 | "localizations": "dict" 234 | } -------------------------------------------------------------------------------- /scratch_pad/example_fetch_youtube_categories.py: -------------------------------------------------------------------------------- 1 | from simple_youtube_api.YouTube import YouTube 2 | 3 | with open("developer_key", "r") as myfile: 4 | data = myfile.read().replace("\n", "") 5 | 6 | developer_key = data 7 | 8 | youtube = YouTube() 9 | youtube.login(developer_key) 10 | 11 | youtube.fetch_categories() 12 | -------------------------------------------------------------------------------- /scratch_pad/example_my_uploads.py: -------------------------------------------------------------------------------- 1 | from simple_youtube_api.Channel import Channel 2 | from simple_youtube_api.YouTubeVideo import YouTubeVideo 3 | 4 | 5 | channel = Channel() 6 | channel.login("client_secret.json", "credentials.storage") 7 | videos = channel.fetch_uploads() 8 | 9 | for video in videos: 10 | print(video.title) 11 | -------------------------------------------------------------------------------- /scratch_pad/example_update_video.py: -------------------------------------------------------------------------------- 1 | from simple_youtube_api.YouTubeVideo import YouTubeVideo 2 | from simple_youtube_api.Channel import Channel 3 | from simple_youtube_api.YouTube import YouTube 4 | 5 | with open("developer_key", "r") as myfile: 6 | data = myfile.read().replace("\n", "") 7 | 8 | developer_key = data 9 | youtube = YouTube() 10 | youtube.login(developer_key) 11 | 12 | video = youtube.search_by_video_id("aSw9blXxNJY") 13 | 14 | channel = Channel() 15 | channel.login("client_secret.json", "credentials.storage") 16 | 17 | 18 | video.update(channel, title="hehe") 19 | -------------------------------------------------------------------------------- /scratch_pad/example_youtube_search.py: -------------------------------------------------------------------------------- 1 | from simple_youtube_api.YouTube import YouTube 2 | 3 | with open("developer_key", "r") as myfile: 4 | data = myfile.read().replace("\n", "") 5 | 6 | developer_key = data 7 | 8 | youtube = YouTube() 9 | youtube.login(developer_key) 10 | 11 | videos = youtube.search("Your Search Term") 12 | 13 | for video in videos: 14 | print(video.title) 15 | 16 | print() 17 | 18 | video = youtube.search_by_video_id("Ks-_Mh1QhMc") 19 | print(video.title) 20 | -------------------------------------------------------------------------------- /scratch_pad/example_youtube_upload.py: -------------------------------------------------------------------------------- 1 | from simple_youtube_api.Channel import Channel 2 | from simple_youtube_api.LocalVideo import LocalVideo 3 | 4 | channel = Channel() 5 | channel.login("client_secret.json", "credentials.storage") 6 | 7 | video = LocalVideo(file_path="test_vid.mp4") 8 | print(video.default_language) 9 | # snippet 10 | video.set_title("My Title") 11 | video.set_description("This is a description") 12 | video.set_tags(["this", "tag"]) 13 | video.set_category("gaming") 14 | # video.set_default_language("english") 15 | 16 | # status 17 | video.set_embeddable(True) 18 | video.set_license("creativeCommon") 19 | video.set_privacy_status("private") 20 | video.set_public_stats_viewable(True) 21 | 22 | 23 | video = channel.upload_video(video) 24 | print(video.id) 25 | print(video.title) 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from codecs import open 3 | 4 | from setuptools import find_packages, setup 5 | from setuptools.command.test import test as TestCommand 6 | 7 | MAJOR = 0 8 | MINOR = 2 9 | MICRO = 8 10 | VERSION = "%d.%d.%d" % (MAJOR, MINOR, MICRO) 11 | 12 | 13 | class PyTest(TestCommand): 14 | """Handle test execution from setup.""" 15 | 16 | user_options = [("pytest-args=", "a", "Arguments to pass into pytest")] 17 | 18 | def initialize_options(self): 19 | """Initialize the PyTest options.""" 20 | TestCommand.initialize_options(self) 21 | self.pytest_args = "" 22 | 23 | def finalize_options(self): 24 | """Finalize the PyTest options.""" 25 | TestCommand.finalize_options(self) 26 | self.test_args = [] 27 | self.test_suite = True 28 | 29 | def run_tests(self): 30 | """Run the PyTest testing suite.""" 31 | try: 32 | import pytest 33 | except ImportError: 34 | raise ImportError( 35 | "Running tests requires additional dependencies." 36 | "\nPlease run (pip install simple-youtube-api[test])" 37 | ) 38 | 39 | errno = pytest.main(self.pytest_args.split(" ")) 40 | sys.exit(errno) 41 | 42 | 43 | cmdclass = {"test": PyTest} # Define custom commands. 44 | 45 | 46 | if "build_docs" in sys.argv: 47 | try: 48 | from sphinx.setup_command import BuildDoc 49 | except ImportError: 50 | raise ImportError( 51 | "Running the documenation builds has additional" 52 | " dependencies. Please run (pip install pyser[docs])" 53 | ) 54 | cmdclass["build_docs"] = BuildDoc 55 | 56 | # Define the requirements for specific execution needs. 57 | requires = [ 58 | "google-api-python-client>=1.7.7", 59 | "google-auth>=1.6.2", 60 | "google-auth-httplib2>=0.0.3", 61 | "oauth2client>=4.1.3", 62 | "decorator>=4.4.0", 63 | "progressbar2>=3.42.0", 64 | "pyser>=0.1.4", 65 | ] 66 | 67 | test_reqs = [ 68 | "pytest-cov>=2.5.1", 69 | "pytest>=3.0.0", 70 | "coveralls>=1.1,<3.0", 71 | "docutils>=0.14", 72 | "rstcheck>=3.3.1", 73 | ] 74 | 75 | doc_reqs = [ 76 | "sphinx_rtd_theme>=0.1.10b0S", 77 | "Sphinx>=1.5.2", 78 | "sphinx-autodoc-typehints>=1.10.3" 79 | ] 80 | 81 | extra_reqs = {"doc": doc_reqs, "test": test_reqs} 82 | 83 | with open("README.rst", "r", "utf-8") as fh: 84 | long_description = fh.read() 85 | 86 | setup( 87 | name="simple-youtube-api", 88 | version=VERSION, 89 | author="Jonne Kaunisto", 90 | author_email="jonneka@gmail.com", 91 | description="A python YouTube API wrapper", 92 | long_description=long_description, 93 | url="https://github.com/jonnekaunisto/simple-youtube-api", 94 | license="MIT License", 95 | keywords="youtube", 96 | packages=find_packages(exclude="docs"), 97 | cmdclass=cmdclass, 98 | classifiers=[ 99 | "Development Status :: 2 - Pre-Alpha", 100 | "Intended Audience :: Developers", 101 | "License :: OSI Approved :: MIT License", 102 | "Natural Language :: English", 103 | "Operating System :: OS Independent", 104 | "Programming Language :: Python :: 3 :: Only", 105 | "Topic :: Utilities", 106 | ], 107 | install_requires=requires, 108 | tests_require=test_reqs, 109 | extras_require=extra_reqs, 110 | ) 111 | -------------------------------------------------------------------------------- /simple_youtube_api/__init__.py: -------------------------------------------------------------------------------- 1 | '''__init__ file for simple-youtube-api''' 2 | from simple_youtube_api.channel import Channel 3 | from simple_youtube_api.comment import Comment 4 | from simple_youtube_api.comment_thread import CommentThread 5 | from simple_youtube_api.local_video import LocalVideo 6 | from simple_youtube_api.video import Video 7 | from simple_youtube_api.youtube import YouTube 8 | from simple_youtube_api.youtube_video import YouTubeVideo 9 | -------------------------------------------------------------------------------- /simple_youtube_api/channel.py: -------------------------------------------------------------------------------- 1 | '''Log into YouTube Channel and query and update data''' 2 | 3 | 4 | import time 5 | import random 6 | import http.client 7 | import os 8 | import sys 9 | from typing import List 10 | 11 | import progressbar 12 | import httplib2 13 | 14 | from googleapiclient.discovery import build 15 | from googleapiclient.errors import HttpError 16 | from googleapiclient.http import MediaFileUpload 17 | 18 | from oauth2client.client import flow_from_clientsecrets 19 | from oauth2client.tools import run_flow 20 | from oauth2client.file import Storage 21 | 22 | from .local_video import LocalVideo 23 | from .youtube_video import YouTubeVideo 24 | 25 | from . import youtube_constants 26 | 27 | httplib2.RETRIES = 1 28 | # Maximum number of times to retry before giving up. 29 | MAX_RETRIES = 10 30 | 31 | # Always retry when these exceptions are raised. 32 | RETRIABLE_EXCEPTIONS = ( 33 | httplib2.HttpLib2Error, 34 | IOError, 35 | http.client.NotConnected, 36 | http.client.IncompleteRead, 37 | http.client.ImproperConnectionState, 38 | http.client.CannotSendRequest, 39 | http.client.CannotSendHeader, 40 | http.client.ResponseNotReady, 41 | http.client.BadStatusLine, 42 | ) 43 | 44 | 45 | # Always retry when an apiclient.errors.HttpError with one of these status 46 | # codes is raised. 47 | RETRIABLE_STATUS_CODES = [500, 502, 503, 504] 48 | 49 | 50 | API_SERVICE_NAME = "youtube" 51 | API_VERSION = "v3" 52 | 53 | 54 | class Channel(): 55 | """ 56 | Class for authorizing changes to channel 57 | 58 | channel 59 | login object to the channel 60 | """ 61 | 62 | def __init__(self): 63 | self.channel = None 64 | 65 | def login( 66 | self, 67 | client_secret_path: str, 68 | storage_path: str, 69 | scope=youtube_constants.SCOPES, 70 | auth_local_webserver=True, 71 | ): 72 | """Logs into the channel with credentials 73 | 74 | client_secret_path 75 | The path to the client_secret file, which should be obtained from 76 | Google cloud 77 | storage_path 78 | The path where the login is stored, or if logging in for the first 79 | the path where the login is saved into 80 | scope 81 | Sets the scope that the login will ask for 82 | auth_local_webserver 83 | Whether login process should use local auth webserver, set this to 84 | false if you are not doing this locally. 85 | """ 86 | 87 | storage = Storage(storage_path) 88 | credentials = storage.get() 89 | 90 | if credentials is None or credentials.invalid: 91 | saved_argv = [] 92 | if auth_local_webserver is False: 93 | saved_argv = sys.argv 94 | sys.argv = [sys.argv[0], "--noauth_local_webserver"] 95 | 96 | flow = flow_from_clientsecrets(client_secret_path, scope=scope) 97 | credentials = run_flow(flow, storage, http=httplib2.Http()) 98 | 99 | sys.argv = saved_argv 100 | 101 | self.channel = build( 102 | API_SERVICE_NAME, API_VERSION, credentials=credentials 103 | ) 104 | 105 | def get_login(self): 106 | """ Returns the login object 107 | """ 108 | return self.channel 109 | 110 | def fetch_uploads(self) -> List[YouTubeVideo]: 111 | """ Fetches uploaded videos from channel 112 | """ 113 | response = ( 114 | self.channel.channels() 115 | .list(mine=True, part="contentDetails") 116 | .execute() 117 | ) 118 | content_details = response["items"][0]["contentDetails"] 119 | uploads_playlist_id = content_details["relatedPlaylists"]["uploads"] 120 | 121 | playlistitems_list_request = self.channel.playlistItems().list( 122 | playlistId=uploads_playlist_id, part="snippet", maxResults=5 123 | ) 124 | 125 | videos = [] 126 | while playlistitems_list_request: 127 | playlistitems_list_response = playlistitems_list_request.execute() 128 | # Print information about each video. 129 | for playlist_item in playlistitems_list_response.get("items", []): 130 | video_title = playlist_item["snippet"]["title"] 131 | video_id = playlist_item["snippet"]["resourceId"]["videoId"] 132 | video_description = playlist_item["snippet"]["description"] 133 | 134 | video = YouTubeVideo(video_id, channel=self.channel) 135 | video.title = video_title 136 | video.description = video_description 137 | 138 | videos.append(video) 139 | 140 | playlistitems_list_request = self.channel.playlistItems().list_next( 141 | playlistitems_list_request, playlistitems_list_response 142 | ) 143 | 144 | return videos 145 | 146 | # TODO add more metadata to returned video 147 | def upload_video(self, video: LocalVideo): 148 | """ Uploads video to authorized channel 149 | """ 150 | if video.file_path is None: 151 | Exception("Must specify a file path") 152 | 153 | youtube_video = initialize_upload(self.channel, video) 154 | 155 | youtube_video.channel = self.get_login() 156 | 157 | if video.thumbnail_path is not None: 158 | self.set_video_thumbnail(youtube_video, video.thumbnail_path) 159 | 160 | if video.playlist_id is not None: 161 | self.add_video_to_playlist(video.playlist_id, youtube_video) 162 | 163 | return youtube_video 164 | 165 | # TODO: check that thumbnail path is valid 166 | def set_video_thumbnail(self, video, thumbnail_path): 167 | """ Sets thumbnail for video 168 | 169 | video 170 | YouTubeVideo object or the string id of the video 171 | thumbnail_path 172 | Path to the thumbnail 173 | """ 174 | if isinstance(video, str): 175 | video_id = video 176 | else: 177 | video_id = video.id 178 | 179 | response = self.channel.thumbnails().set( 180 | videoId=video_id, media_body=thumbnail_path 181 | ).execute() 182 | 183 | return response 184 | 185 | def add_video_to_playlist(self, playlist_id, video): 186 | """ Adds video to playlist 187 | 188 | playlist_id 189 | The id of the playlist to be added to 190 | video 191 | YouTubeVideo object or the string id of the video 192 | """ 193 | 194 | if isinstance(video, str): 195 | video_id = video 196 | else: 197 | video_id = video.id 198 | 199 | response = self.channel.playlistItems().insert( 200 | part="snippet", 201 | body={ 202 | "snippet": { 203 | "playlistId": playlist_id, 204 | "position": 0, 205 | "resourceId": { 206 | "kind": "youtube#video", 207 | "videoId": video_id 208 | } 209 | } 210 | } 211 | ).execute() 212 | 213 | return response 214 | 215 | 216 | def generate_upload_body(video): 217 | """ Generates upload body """ 218 | body = dict() 219 | 220 | snippet = dict() 221 | if video.title is not None: 222 | snippet.update({"title": video.title}) 223 | else: 224 | Exception("Title is required") 225 | if video.description is not None: 226 | snippet.update({"description": video.description}) 227 | if video.tags is not None: 228 | snippet.update({"tags": video.tags}) 229 | if video.category is not None: 230 | snippet.update({"categoryId": video.category}) 231 | else: 232 | Exception("Category is required") 233 | if video.default_language is not None: 234 | snippet.update({"defaultLanguage": video.default_language}) 235 | body.update({"snippet": snippet}) 236 | 237 | if video.status_set: 238 | status = dict() 239 | if video.embeddable is not None: 240 | status.update({"embeddable": video.embeddable}) 241 | if video.license is not None: 242 | status.update({"license": video.license}) 243 | if video.privacy_status is not None: 244 | status.update({"privacyStatus": video.privacy_status}) 245 | if video.public_stats_viewable is not None: 246 | status.update({"publicStatsViewable": video.public_stats_viewable}) 247 | if video.publish_at is not None: 248 | status.update({"publishAt": video.publish_at}) 249 | if video.self_declared_made_for_kids is not None: 250 | status.update({"selfDeclaredMadeForKids": video.self_declared_made_for_kids}) 251 | body.update({"status": status}) 252 | 253 | return body 254 | 255 | 256 | def calculate_chunk_size(video_path): 257 | """ Calculates the chuncsize for video """ 258 | video_size = os.path.getsize(video_path) 259 | print("Video size: " + str(video_size) + " bytes") 260 | 261 | if video_size > 1000000: 262 | chunk_size = 1000000 263 | else: 264 | chunk_size = -1 265 | 266 | return chunk_size 267 | 268 | 269 | def initialize_upload(channel, video): 270 | """ Initializes upload """ 271 | body = generate_upload_body(video) 272 | chunk_size = calculate_chunk_size(video.file_path) 273 | # Call the API's videos.insert method to create and upload the video. 274 | insert_request = channel.videos().insert( 275 | part=",".join(list(body.keys())), 276 | body=body, 277 | media_body=MediaFileUpload( 278 | video.file_path, chunksize=chunk_size, resumable=True 279 | ), 280 | ) 281 | 282 | return resumable_upload(insert_request) 283 | 284 | 285 | # This method implements an exponential backoff strategy to resume a 286 | # failed upload. 287 | # TODO: add more variables into video when returned 288 | def resumable_upload(request): 289 | """ Uploads video """ 290 | youtube_video = None 291 | response = None 292 | error = None 293 | retry = 0 294 | while response is None: 295 | try: 296 | widgets = [ 297 | "Upload: ", 298 | progressbar.Percentage(), 299 | " ", 300 | progressbar.Bar(marker=progressbar.RotatingMarker()), 301 | " ", 302 | progressbar.ETA(), 303 | " ", 304 | progressbar.FileTransferSpeed(), 305 | ] 306 | bar_object = progressbar.ProgressBar( 307 | widgets=widgets, max_value=1000 308 | ).start() 309 | 310 | response = None 311 | while response is None: 312 | status, response = request.next_chunk(num_retries=4) 313 | if status: 314 | bar_object.update(min(1000, 100 * 10 * status.progress() + 1)) 315 | bar_object.finish() 316 | if "id" in response: 317 | youtube_video = YouTubeVideo(response["id"]) 318 | else: 319 | raise Exception("The upload failed unexpectedly: " + response) 320 | except HttpError as http_error: 321 | if http_error.resp.status in RETRIABLE_STATUS_CODES: 322 | error = "A retriable HTTP error %d occurred:\n%s" % ( 323 | http_error.resp.status, 324 | http_error.content, 325 | ) 326 | else: 327 | raise 328 | except RETRIABLE_EXCEPTIONS as http_error: 329 | error = "A retriable error occurred: %s" % http_error 330 | 331 | if error is not None: 332 | print(error) 333 | retry += 1 334 | if retry > MAX_RETRIES: 335 | return youtube_video 336 | 337 | max_sleep = 2 ** retry 338 | sleep_seconds = random.random() * max_sleep 339 | print("Sleeping %f seconds and then retrying..." % sleep_seconds) 340 | time.sleep(sleep_seconds) 341 | return youtube_video 342 | -------------------------------------------------------------------------------- /simple_youtube_api/comment.py: -------------------------------------------------------------------------------- 1 | '''YouTube Comment Resource''' 2 | from pyser import SchemaJSON, DeserField 3 | from simple_youtube_api.name_converter import u_to_c 4 | 5 | 6 | class CommentSchema(SchemaJSON): 7 | """Schema for comment""" 8 | def __init__(self): 9 | self.etag = DeserField() 10 | self.id = DeserField() 11 | 12 | # snippet 13 | self.author_display_name = DeserField( 14 | name_conv=u_to_c, parent_keys=["snippet"] 15 | ) 16 | self.author_profile_image_url = DeserField( 17 | name_conv=u_to_c, parent_keys=["snippet"] 18 | ) 19 | self.author_channel_url = DeserField( 20 | name_conv=u_to_c, optional=True, parent_keys=["snippet"] 21 | ) 22 | self.author_channel_id = DeserField( 23 | name="value", 24 | optional=True, 25 | parent_keys=["snippet", "authorChannelId"], 26 | ) 27 | 28 | self.channel_id = DeserField( 29 | name_conv=u_to_c, optional=True, parent_keys=["snippet"] 30 | ) 31 | self.video_id = DeserField( 32 | name_conv=u_to_c, optional=True, parent_keys=["snippet"] 33 | ) 34 | self.text_display = DeserField( 35 | name_conv=u_to_c, parent_keys=["snippet"] 36 | ) 37 | self.text_original = DeserField( 38 | name_conv=u_to_c, parent_keys=["snippet"] 39 | ) 40 | self.parent_id = DeserField( 41 | name_conv=u_to_c, optional=True, parent_keys=["snippet"] 42 | ) 43 | self.can_rate = DeserField( 44 | name_conv=u_to_c, parent_keys=["snippet"] 45 | ) 46 | self.viewer_rating = DeserField( 47 | name_conv=u_to_c, parent_keys=["snippet"] 48 | ) 49 | self.like_count = DeserField( 50 | name_conv=u_to_c, parent_keys=["snippet"] 51 | ) 52 | self.moderation_status = DeserField( 53 | name_conv=u_to_c, optional=True, parent_keys=["snippet"] 54 | ) 55 | self.published_at = DeserField( 56 | name_conv=u_to_c, parent_keys=["snippet"] 57 | ) 58 | self.updated_at = DeserField( 59 | name_conv=u_to_c, parent_keys=["snippet"] 60 | ) 61 | 62 | 63 | class Comment(): 64 | """ 65 | Class for comment resource 66 | 67 | Attributes 68 | ----------- 69 | 70 | etag 71 | The Etag of this resource. 72 | id 73 | The ID that YouTube uses to uniquely identify the comment. 74 | 75 | author_display_name 76 | The display name of the user who posted the comment. 77 | 78 | author_profile_image_url 79 | The URL for the avatar of the user who posted the comment. 80 | 81 | author_channel_url 82 | The URL of the comment author's YouTube channel, if available. 83 | 84 | author_channel_id 85 | The ID of the comment author's YouTube channel, if available. 86 | 87 | channel_id 88 | The ID of the YouTube channel associated with the comment. 89 | 90 | video_id 91 | The ID of the video that the comment refers to. This property is only 92 | present if the comment was made on a video. 93 | text_display 94 | The comment's text. The text can be retrieved in either plain text or 95 | HTML. 96 | 97 | text_original 98 | The original, raw text of the comment as it was initially posted or 99 | last updated. The original text is only returned if it is accessible 100 | to the authenticated user, which is only guaranteed if the user is 101 | the comment's author. 102 | 103 | parent_id 104 | The unique ID of the parent comment. This property is only set if the 105 | comment was submitted as a reply to another comment. 106 | 107 | can_rate 108 | This setting indicates whether the current viewer can rate the comment. 109 | 110 | viewer_rating 111 | The rating the viewer has given to this comment. Note that this 112 | property does not currently identify dislike ratings, though this 113 | behavior is subject to change. 114 | 115 | like_counter 116 | The total number of likes (positive ratings) the comment has received. 117 | 118 | moderation_status 119 | The moderation status of the comment [heldForReview ,likelySpam, 120 | published, rejected] 121 | 122 | published_at 123 | The date and time when the comment was orignally published. 124 | 125 | updated_at 126 | The date and time when the comment was last updated. 127 | 128 | """ 129 | 130 | def __init__(self): 131 | self.author_display_name = None 132 | self.author_profile_image_url = None 133 | self.author_channel_url = None 134 | self.author_channel_id = None 135 | self.channel_id = None 136 | self.video_id = None 137 | self.text_display = None 138 | self.text_original = None 139 | self.parent_id = None 140 | self.can_rate = None 141 | self.viewer_rating = None 142 | self.like_count = None 143 | self.moderation_status = None 144 | self.published_at = None 145 | self.updated_at = None 146 | 147 | def __str__(self): 148 | return str(self.text_original) 149 | -------------------------------------------------------------------------------- /simple_youtube_api/comment_thread.py: -------------------------------------------------------------------------------- 1 | '''YouTube Comment Thread Resource''' 2 | from pyser import SchemaJSON, DeserField, DeserObjectField 3 | from simple_youtube_api.name_converter import u_to_c 4 | from simple_youtube_api.comment import Comment, CommentSchema 5 | 6 | 7 | class CommentThreadSchema(SchemaJSON): 8 | """ CommentThread Schema 9 | """ 10 | def __init__(self): 11 | self.etag = DeserField() 12 | self.id = DeserField() 13 | 14 | # snippet 15 | self.channel_id = DeserField( 16 | name_conv=u_to_c, optional=True, parent_keys=["snippet"] 17 | ) 18 | self.video_id = DeserField( 19 | name_conv=u_to_c, optional=True, parent_keys=["snippet"] 20 | ) 21 | self.top_level_comment = DeserObjectField( 22 | name_conv=u_to_c, kind=Comment, schema=CommentSchema, 23 | parent_keys=["snippet"] 24 | ) 25 | self.can_reply = DeserField( 26 | name_conv=u_to_c, parent_keys=["snippet"] 27 | ) 28 | self.total_reply_count = DeserField( 29 | name_conv=u_to_c, parent_keys=["snippet"] 30 | ) 31 | self.is_public = DeserField( 32 | name_conv=u_to_c, parent_keys=["snippet"] 33 | ) 34 | self.replies = DeserObjectField( 35 | name="comments", 36 | optional=True, 37 | kind=Comment, 38 | schema=CommentSchema, 39 | parent_keys=["replies"], 40 | repeated=True, 41 | ) 42 | 43 | 44 | class CommentThread(): 45 | """ 46 | Class for CommentThread resource which holds the top level comment and 47 | replies 48 | 49 | Attributes 50 | ----------- 51 | 52 | etag 53 | The Etag of this resource. 54 | 55 | id 56 | The ID that YouTube uses to uniquely identify the comment thread. 57 | 58 | channel_id 59 | The YouTube channel that is associated with the comments in the thread. 60 | 61 | video_id 62 | The ID of the video that the comments refer to, if any. If this 63 | property is not present or does not have a value, then the thread 64 | applies to the channel and not to a specific video. 65 | 66 | top_level_comment 67 | Has the comment object of the top level comment 68 | 69 | can_reply 70 | This setting indicates whether the current viewer can reply to the 71 | thread. 72 | 73 | total_reply_count 74 | The total number of replies that have been submitted in response to the 75 | top-level comment. 76 | 77 | is_public 78 | This setting indicates whether the thread, including all of its 79 | comments and comment replies, is visible to all YouTube users. 80 | 81 | replies 82 | The list of comment object replies 83 | """ 84 | 85 | def __init__(self): 86 | self.etag = None 87 | self.id = None 88 | 89 | # snippet 90 | self.channel_id = None 91 | self.video_id = None 92 | self.top_level_comment = None 93 | self.can_reply = None 94 | self.total_reply_count = None 95 | self.is_public = None 96 | self.replies = None 97 | 98 | def __str__(self): 99 | return str(self.top_level_comment.text_original) 100 | -------------------------------------------------------------------------------- /simple_youtube_api/decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | All the decorators used in Simple YouTube API go here 3 | """ 4 | import decorator 5 | 6 | 7 | @decorator.decorator 8 | def video_snippet_set(func, video, *a, **k): 9 | ''' Sets the snippet_set to true ''' 10 | video.snippet_set = True 11 | return func(video, *a, **k) 12 | 13 | 14 | @decorator.decorator 15 | def video_status_set(func, video, *a, **k): 16 | ''' Sets the status_set to true ''' 17 | video.status_set = True 18 | return func(video, *a, **k) 19 | 20 | 21 | @decorator.decorator 22 | def require_channel_auth(func, video, *a, **k): 23 | ''' Checks that channel auth exists ''' 24 | if video.channel is not None: 25 | return func(video, *a, **k) 26 | 27 | raise Exception( 28 | "Setting channel authentication is required before calling " 29 | + func.__name__ 30 | ) 31 | 32 | 33 | @decorator.decorator 34 | def require_youtube_auth(func, video, *a, **k): 35 | ''' Checks that youtube auth exists ''' 36 | if video.youtube is not None: 37 | return func(video, *a, **k) 38 | 39 | raise Exception( 40 | "Setting youtube authentication is required before calling " 41 | + func.__name__ 42 | ) 43 | 44 | 45 | @decorator.decorator 46 | def require_channel_or_youtube_auth(func, video, *a, **k): 47 | ''' Checks that youtube or channel auth exists ''' 48 | if video.youtube is not None or video.channel is not None: 49 | return func(video, *a, **k) 50 | 51 | raise Exception( 52 | "Setting youtube or channel authentication is required before calling " 53 | + func.__name__ 54 | ) 55 | -------------------------------------------------------------------------------- /simple_youtube_api/local_video.py: -------------------------------------------------------------------------------- 1 | '''LocalVideo for uploading to YouTube''' 2 | import os.path 3 | 4 | from simple_youtube_api.video import Video 5 | 6 | 7 | class LocalVideo(Video): 8 | """ 9 | Class for making a video that is uploaded to YouTube 10 | 11 | Attributes 12 | ----------- 13 | file_path: 14 | Specifies which file is going to be uploaded 15 | 16 | title: 17 | The video's title. The property value has a maximum length of 100 18 | characters and may contain all valid UTF-8 characters except < and >. 19 | 20 | description: 21 | The video's description. The property value has a maximum length of 22 | 5000 bytes and may contain all valid UTF-8 characters except < and >. 23 | 24 | tags: 25 | A list of keyword tags associated with the video. Tags may contain 26 | spaces. The property value has a maximum length of 500 characters. 27 | 28 | category: 29 | The YouTube video category associated with the video. 30 | 31 | publish_at: 32 | Specifies when the video will be published 33 | (the video has to be private for this) Has to be in 34 | (YYYY-MM-DDThh:mm:ss.sZ) format 35 | 36 | thumbnail_path: 37 | Specifies which file is going to be set as thumbnail. 38 | 39 | playlist_id 40 | Specifies which playlist the video gets added onto. 41 | """ 42 | 43 | def __init__( 44 | self, 45 | file_path, 46 | title="", 47 | description="", 48 | tags=None, 49 | category=1, 50 | default_language=None, 51 | ): 52 | Video.__init__(self) 53 | 54 | if tags is None: 55 | tags = [] 56 | 57 | self.set_file_path(file_path) 58 | self.set_title(title) 59 | self.set_description(description) 60 | self.set_tags(tags) 61 | self.set_category(category) 62 | self.publish_at = None 63 | self.thumbnail_path = None 64 | self.self_declared_made_for_kids = None 65 | self.playlist_id = None 66 | 67 | if default_language is not None: 68 | self.set_default_language(default_language) 69 | 70 | def set_file_path(self, file_path: str): 71 | """ Specifies which video file is going to be uploaded 72 | """ 73 | if file_path is not None and os.path.isfile(file_path): 74 | self.file_path = file_path 75 | else: 76 | raise Exception("Not a valid file path: " + str(file_path)) 77 | 78 | def set_thumbnail_path(self, thumbnail_path: str): 79 | """ Specifies which image file is going to be uploaded 80 | """ 81 | if thumbnail_path is not None and os.path.isfile(thumbnail_path): 82 | self.thumbnail_path = thumbnail_path 83 | else: 84 | raise Exception("Not a valid file path: " + str(thumbnail_path)) 85 | 86 | def set_made_for_kids(self, made_for_kids: bool): 87 | """ Specifies if video is made for kids 88 | """ 89 | if type(made_for_kids): 90 | self.self_declared_made_for_kids = made_for_kids 91 | else: 92 | raise Exception("Must be a type bool") 93 | 94 | def set_playlist(self, playlist_id: str): 95 | """ Specifies playlist that video will be put on 96 | """ 97 | if isinstance(playlist_id, str): 98 | self.playlist_id = playlist_id 99 | else: 100 | raise Exception("playlist_id must be a string") 101 | -------------------------------------------------------------------------------- /simple_youtube_api/name_converter.py: -------------------------------------------------------------------------------- 1 | '''Convert from YouTube name to simple-youtube-api name and vice versa''' 2 | import re 3 | 4 | 5 | def c_to_u(name): 6 | ''' Converts from camel case ''' 7 | return re.sub(r"(? MAX_YOUTUBE_TITLE_LENGTH: 65 | raise Exception("Title is too long: " + str(len(title))) 66 | 67 | self.title = title 68 | 69 | @video_snippet_set 70 | def set_description(self, description: str): 71 | """Sets description for video and returns an exception if description 72 | is invalid 73 | """ 74 | if not isinstance(description, str): 75 | raise Exception("Description must be a string") 76 | if len(description) > MAX_YOUTUBE_DESCRIPTION_LENGTH: 77 | raise Exception("Description is too long: " + str(len(description))) 78 | 79 | self.description = description 80 | 81 | @video_snippet_set 82 | def set_tags(self, tags: List[str]): 83 | """Sets tags to the video and returns an exception if tags are invalid 84 | """ 85 | if not isinstance(tags, list): 86 | raise Exception("Tags must be a list") 87 | if len("".join(tags)) > MAX_YOUTUBE_TAGS_LENGTH: 88 | raise Exception("Tags are too long: " + str(len("".join(tags)))) 89 | 90 | self.tags = tags 91 | 92 | @video_snippet_set 93 | def set_category(self, category: Union[int, str]): 94 | """ Sets category for video 95 | 96 | Parameters 97 | ---------- 98 | 99 | category 100 | Can either be the name of the category or the category id 101 | 102 | """ 103 | cat_type = type(category) 104 | 105 | if cat_type == int and category in YOUTUBE_CATEGORIES.values(): 106 | self.category = category 107 | 108 | elif cat_type == str and category.lower() in YOUTUBE_CATEGORIES.keys(): 109 | self.category = YOUTUBE_CATEGORIES[category] 110 | else: 111 | raise Exception("Not a valid category: " + str(category)) 112 | 113 | @video_snippet_set 114 | def set_default_language(self, language: str): 115 | """ Sets default language for video 116 | """ 117 | if not isinstance(language, str): 118 | raise Exception("Language must be a string") 119 | 120 | self.default_language = language 121 | 122 | @video_status_set 123 | def set_embeddable(self, embeddable: bool): 124 | """ Specifies if video is embeddable 125 | """ 126 | if not isinstance(embeddable, bool): 127 | raise Exception("Embeddable must be a boolean") 128 | 129 | self.embeddable = embeddable 130 | 131 | @video_status_set 132 | def set_license(self, video_license: str): 133 | """ Specifies license for video either 'youtube' or 'creativeCommon' 134 | """ 135 | if isinstance(video_license, str) and video_license in YOUTUBE_LICENCES: 136 | self.license = video_license 137 | else: 138 | raise Exception("Not a valid license: " + str(video_license)) 139 | 140 | @video_status_set 141 | def set_privacy_status(self, privacy_status: str): 142 | """ Set privacy status, either 'private', 'unlisted' or 'public 143 | """ 144 | if privacy_status not in VALID_PRIVACY_STATUS: 145 | raise Exception("Not valid privacy status: " + str(privacy_status)) 146 | 147 | self.privacy_status = privacy_status 148 | 149 | @video_status_set 150 | def set_public_stats_viewable(self, viewable: bool): 151 | """ Specifies if public stats are viewable 152 | """ 153 | if isinstance(viewable, bool): 154 | self.public_stats_viewable = viewable 155 | else: 156 | raise Exception("Not a valid status: " + str(viewable)) 157 | 158 | # TODO enforce (YYYY-MM-DDThh:mm:ss.sZ) format 159 | def set_publish_at(self, time: str): 160 | """ Sets time that video is going to be published at in 161 | (YYYY-MM-DDThh:mm:ss.sZ) format 162 | """ 163 | if isinstance(time, str): 164 | self.publish_at = time 165 | else: 166 | raise Exception("Not a valid publish time: " + str(time)) 167 | 168 | def __str__(self): 169 | form = "Title: {0}\nDescription: {1}\n Tags:{2}" 170 | return form.format(self.title, self.description, self.tags) 171 | -------------------------------------------------------------------------------- /simple_youtube_api/youtube.py: -------------------------------------------------------------------------------- 1 | '''Query public YouTube data''' 2 | 3 | from googleapiclient.discovery import build 4 | 5 | from simple_youtube_api import youtube_api 6 | from simple_youtube_api.youtube_video import YouTubeVideo 7 | 8 | # Always retry when an apiclient.errors.HttpError with one of these status 9 | # codes is raised. 10 | RETRIABLE_STATUS_CODES = [500, 502, 503, 504] 11 | API_SERVICE_NAME = "youtube" 12 | API_VERSION = "v3" 13 | 14 | 15 | MAX_YOUTUBE_TITLE_LENGTH = 100 16 | MAX_YOUTUBE_DESCRIPTION_LENGTH = 5000 17 | MAX_YOUTUBE_TAGS_LENGTH = 500 18 | VALID_PRIVACY_STATUSES = ("public", "private", "unlisted") 19 | 20 | 21 | class YouTube(): 22 | """ Query YouTube public data 23 | """ 24 | def __init__(self): 25 | self.youtube = None 26 | 27 | def login(self, developer_key): 28 | """Logs into YouTube with credentials 29 | """ 30 | self.youtube = build( 31 | API_SERVICE_NAME, API_VERSION, developerKey=developer_key 32 | ) 33 | 34 | def get_login(self): 35 | """Gets login object 36 | """ 37 | return self.youtube 38 | 39 | def search_raw(self, search_term, max_results=25): 40 | """Searches YouTube and returns a JSON response 41 | """ 42 | search_response = ( 43 | self.youtube.search() 44 | .list(q=search_term, part="snippet", maxResults=max_results) 45 | .execute() 46 | ) 47 | 48 | return search_response 49 | 50 | def search(self, search_term, max_results=25): 51 | """Searches YouTube and returns a list of video objects 52 | """ 53 | search_response = self.search_raw(search_term, max_results=max_results) 54 | 55 | videos = [] 56 | for search_result in search_response.get("items", []): 57 | if search_result["id"]["kind"] == "youtube#video": 58 | search_result["id"] = search_result["id"]["videoId"] 59 | 60 | video = YouTubeVideo(youtube=self.youtube) 61 | video = youtube_api.parse_youtube_video(video, search_result) 62 | videos.append(video) 63 | 64 | return videos 65 | 66 | def search_by_video_id_raw(self, video_id): 67 | """Returns a JSON of video 68 | """ 69 | search_response = ( 70 | self.youtube.videos().list(part="snippet", id=video_id).execute() 71 | ) 72 | 73 | return search_response 74 | 75 | def search_by_video_id(self, video_id): 76 | """Returns a video object 77 | """ 78 | video = YouTubeVideo(video_id, youtube=self.youtube) 79 | video.fetch() 80 | 81 | return video 82 | 83 | def fetch_categories(self): 84 | """Finds all the video categories on YouTube 85 | """ 86 | response = ( 87 | self.youtube.videoCategories() 88 | .list(part="snippet", regionCode="US") 89 | .execute() 90 | ) 91 | return youtube_api.parse_categories(response) 92 | -------------------------------------------------------------------------------- /simple_youtube_api/youtube_api.py: -------------------------------------------------------------------------------- 1 | ''' Helper functions for simple-youtube-api ''' 2 | 3 | 4 | # autogenerate code, do not edit 5 | # TODO: get rid of this 6 | def parse_youtube_video(video, data): 7 | ''' parses youtube video ''' 8 | video.etag = data["etag"] 9 | video.id = data["id"] 10 | 11 | # snippet 12 | snippet_data = data.get("snippet", False) 13 | if snippet_data: 14 | video.published_at = snippet_data.get("publishedAt", None) 15 | video.channel_id = snippet_data.get("channelId", None) 16 | video.title = snippet_data.get("title", None) 17 | video.description = snippet_data.get("description", None) 18 | video.channel_title = snippet_data.get("channelTitle", None) 19 | video.tags = snippet_data.get("tags", None) 20 | video.category_id = snippet_data.get("categoryId", None) 21 | video.live_broadcast_content = snippet_data.get( 22 | "liveBroadcastContent", None 23 | ) 24 | video.default_language = snippet_data.get("defaultLanguage", None) 25 | video.default_audio_language = snippet_data.get( 26 | "defaultAudioLanguage", None 27 | ) 28 | 29 | # contentDetails 30 | content_details_data = data.get("contentDetails", False) 31 | if content_details_data: 32 | video.duration = content_details_data.get("duration", None) 33 | video.dimension = content_details_data.get("dimension", None) 34 | video.definition = content_details_data.get("definition", None) 35 | video.caption = content_details_data.get("caption", None) 36 | video.licensed_content = content_details_data.get( 37 | "licensedContent", None 38 | ) 39 | video.projection = content_details_data.get("projection", None) 40 | video.has_custom_thumbnail = content_details_data.get( 41 | "hasCustomThumbnail", None 42 | ) 43 | 44 | # status 45 | status_data = data.get("status", False) 46 | if status_data: 47 | video.upload_status = status_data.get("uploadStatus", None) 48 | video.failure_reason = status_data.get("failureReason", None) 49 | video.rejection_reason = status_data.get("rejectionReason", None) 50 | video.privacy_status = status_data.get("privacyStatus", None) 51 | video.publish_at = status_data.get("publishAt", None) 52 | video.license = status_data.get("license", None) 53 | video.embeddable = status_data.get("embeddable", None) 54 | video.public_stats_viewable = status_data.get( 55 | "publicStatsViewable", None 56 | ) 57 | 58 | # statistics 59 | statistics_data = data.get("statistics", False) 60 | if statistics_data: 61 | video.view_count = statistics_data.get("viewCount", None) 62 | video.like_count = statistics_data.get("likeCount", None) 63 | video.dislike_count = statistics_data.get("dislikeCount", None) 64 | video.favorite_count = statistics_data.get("favoriteCount", None) 65 | video.comment_count = statistics_data.get("commentCount", None) 66 | 67 | # player 68 | player_data = data.get("player", False) 69 | if player_data: 70 | video.embed_html = player_data.get("embedHtml", None) 71 | video.embed_height = player_data.get("embedHeight", None) 72 | video.embed_width = player_data.get("embedWidth", None) 73 | 74 | # topicDetails 75 | topic_details_data = data.get("topicDetails", False) 76 | if topic_details_data: 77 | video.topic_ids = topic_details_data.get("topicIds", None) 78 | video.relevant_topic_ids = topic_details_data.get( 79 | "relevantTopicIds", None 80 | ) 81 | video.topic_categories = topic_details_data.get("topicCategories", None) 82 | 83 | # recordingDetails 84 | recording_details_data = data.get("recordingDetails", False) 85 | if recording_details_data: 86 | video.recording_date = recording_details_data.get("recordingDate", None) 87 | 88 | # fileDetails 89 | file_details_data = data.get("fileDetails", False) 90 | if file_details_data: 91 | video.file_name = file_details_data.get("fileName", None) 92 | video.file_size = file_details_data.get("fileSize", None) 93 | video.file_type = file_details_data.get("fileType", None) 94 | video.container = file_details_data.get("container", None) 95 | video.video_streams = file_details_data.get("videoStreams", None) 96 | video.audio_streams = file_details_data.get("audioStreams", None) 97 | video.duration_ms = file_details_data.get("durationMs", None) 98 | video.bitrate_bps = file_details_data.get("bitrateBps", None) 99 | video.creation_time = file_details_data.get("creationTime", None) 100 | 101 | # processingDetails 102 | processing_details_data = data.get("processingDetails", False) 103 | if processing_details_data: 104 | video.processing_status = processing_details_data.get( 105 | "processingStatus", None 106 | ) 107 | video.processing_failure_reason = processing_details_data.get( 108 | "processingFailureReason", None 109 | ) 110 | video.file_details_availability = processing_details_data.get( 111 | "fileDetailsAvailability", None 112 | ) 113 | video.processing_issues_availability = processing_details_data.get( 114 | "processingIssuesAvailability", None 115 | ) 116 | video.tag_suggestions_availability = processing_details_data.get( 117 | "tagSuggestionsAvailability", None 118 | ) 119 | video.editor_suggestions_availability = processing_details_data.get( 120 | "editorSuggestionsAvailability", None 121 | ) 122 | video.thumbnails_availability = processing_details_data.get( 123 | "thumbnailsAvailability", None 124 | ) 125 | 126 | # suggestions 127 | suggestions_data = data.get("suggestions", False) 128 | if suggestions_data: 129 | video.processing_errors = suggestions_data.get("processingErrors", None) 130 | video.processing_warnings = suggestions_data.get( 131 | "processingWarnings", None 132 | ) 133 | video.processing_hints = suggestions_data.get("processingHints", None) 134 | video.tag_suggestions = suggestions_data.get("tagSuggestions", None) 135 | video.editor_suggestions = suggestions_data.get( 136 | "editorSuggestions", None 137 | ) 138 | 139 | # liveStreamingDetails 140 | live_streaming_details_data = data.get("liveStreamingDetails", False) 141 | if live_streaming_details_data: 142 | video.actual_start_time = live_streaming_details_data.get( 143 | "actualStartTime", None 144 | ) 145 | video.actual_end_time = live_streaming_details_data.get( 146 | "actualEndTime", None 147 | ) 148 | video.scheduled_start_time = live_streaming_details_data.get( 149 | "scheduledStartTime", None 150 | ) 151 | video.scheduled_end_time = live_streaming_details_data.get( 152 | "scheduledEndTime", None 153 | ) 154 | video.concurrent_viewers = live_streaming_details_data.get( 155 | "concurrentViewers", None 156 | ) 157 | video.active_live_chat_id = live_streaming_details_data.get( 158 | "activeLiveChatId", None 159 | ) 160 | video.localizations = data.get("localizations", None) 161 | 162 | return video 163 | 164 | 165 | def parse_categories(data): 166 | """ Parses categories """ 167 | categories = {} 168 | for item in data["items"]: 169 | if item["snippet"]["assignable"]: 170 | category_name = item["snippet"]["title"] 171 | category_id = item["id"] 172 | categories[category_name.lower()] = category_id 173 | 174 | return categories 175 | -------------------------------------------------------------------------------- /simple_youtube_api/youtube_constants.py: -------------------------------------------------------------------------------- 1 | '''YouTube constants''' 2 | import os 3 | 4 | DATA_PATH = ( 5 | os.path.dirname(os.path.abspath(__file__)) + os.sep + "data" + os.sep 6 | ) 7 | MAX_YOUTUBE_TITLE_LENGTH = 100 8 | MAX_YOUTUBE_DESCRIPTION_LENGTH = 5000 9 | MAX_YOUTUBE_TAGS_LENGTH = 500 10 | YOUTUBE_CATEGORIES = { 11 | "film": 1, 12 | "animation": 1, 13 | "autos": 2, 14 | "vehicles": 2, 15 | "music": 10, 16 | "pets": 15, 17 | "animals": 15, 18 | "sports": 17, 19 | "short movies": 18, 20 | "travel": 19, 21 | "events": 19, 22 | "gaming": 20, 23 | "videoblogging": 21, 24 | "people": 22, 25 | "blogs": 22, 26 | "comedy": 23, 27 | "entertainment": 24, 28 | "news": 25, 29 | "politics": 25, 30 | "howto": 26, 31 | "style": 26, 32 | "education": 27, 33 | "science": 28, 34 | "technology": 28, 35 | "nonprofits": 29, 36 | "activism": 29, 37 | "movies": 30, 38 | "anime": 31, 39 | "action": 32, 40 | "adventure": 32, 41 | "classics": 33, 42 | "documentary": 35, 43 | "drama": 36, 44 | "family": 37, 45 | "foreign": 38, 46 | "horror": 39, 47 | "sci-fi": 40, 48 | "fantasy": 40, 49 | "thriller": 41, 50 | "shorts": 42, 51 | "shows": 43, 52 | "trailers": 44, 53 | } 54 | 55 | YOUTUBE_LICENCES = ["creativeCommon", "youtube"] 56 | 57 | SCOPE_YOUTUBE = "youtube" 58 | SCOPES = [ 59 | "https://www.googleapis.com/auth/youtube", 60 | "https://www.googleapis.com/auth/youtube.force-ssl", 61 | "https://www.googleapis.com/auth/youtube.readonly", 62 | "https://www.googleapis.com/auth/youtube.upload", 63 | "https://www.googleapis.com/auth/youtubepartner", 64 | "https://www.googleapis.com/auth/youtubepartner-channel-audit", 65 | ] 66 | API_SERVICE_NAME = "youtube" 67 | API_VERSION = "v3" 68 | VALID_PRIVACY_STATUS = ("public", "private", "unlisted") 69 | -------------------------------------------------------------------------------- /simple_youtube_api/youtube_video.py: -------------------------------------------------------------------------------- 1 | '''Query and update YouTube Video''' 2 | from simple_youtube_api import youtube_api 3 | from simple_youtube_api.decorators import ( 4 | require_channel_auth, 5 | require_youtube_auth, 6 | ) 7 | 8 | from simple_youtube_api.video import Video 9 | from simple_youtube_api.comment_thread import CommentThread, CommentThreadSchema 10 | 11 | 12 | # TODO add more functions 13 | class YouTubeVideo(Video): 14 | ''' Class for YouTube Video 15 | 16 | id 17 | Video id 18 | 19 | youtube 20 | YouTube authentication 21 | 22 | channel 23 | Channel autentication 24 | 25 | ''' 26 | 27 | def __init__(self, video_id=None, youtube=None, channel=None): 28 | Video.__init__(self) 29 | 30 | self.youtube = youtube 31 | self.channel = channel 32 | 33 | self.id = video_id 34 | 35 | # snippet 36 | self.channel_id = None 37 | 38 | # status 39 | self.made_for_kids = None 40 | 41 | def set_youtube_auth(self, youtube): 42 | """Sets authentication for video 43 | """ 44 | self.youtube = youtube 45 | 46 | def set_channel_auth(self, channel): 47 | """Sets channel authenticaton for video 48 | """ 49 | self.channel = channel 50 | 51 | # TODO add more values to be fetched 52 | # TODO add fetching some values that are only available to channel 53 | @require_youtube_auth 54 | def fetch( 55 | self, 56 | snippet=True, 57 | content_details=False, 58 | status=False, 59 | statistics=False, 60 | player=False, 61 | topic_details=False, 62 | recording_details=False, 63 | file_details=False, 64 | processing_details=False, 65 | suggestions=False, 66 | live_streaming_details=False, 67 | localizations=False, 68 | all_parts=False, 69 | ): 70 | """Fetches specified parts of video 71 | """ 72 | 73 | parts_list = [] 74 | youtube_perm_parts = [ 75 | (snippet, "snippet"), 76 | (status, "status"), 77 | (statistics, "statistics"), 78 | (player, "player"), 79 | (topic_details, "topicDetails"), 80 | (recording_details, "recordingDetails"), 81 | (live_streaming_details, "liveStreamingDetails"), 82 | (localizations, "localizations"), 83 | ] 84 | channel_perm_parts = [ 85 | (live_streaming_details, "liveStreamingDetails"), 86 | (processing_details, "processingDetails"), 87 | (suggestions, "suggestions"), 88 | ] 89 | 90 | # For youtube authenticated 91 | for part_tupple in youtube_perm_parts: 92 | if part_tupple[0] or all_parts: 93 | parts_list.append(part_tupple[1]) 94 | 95 | # For Channel authenticated 96 | # if False: 97 | # for part_tupple in channel_perm_parts: 98 | # if part_tupple[0] or all_parts: 99 | # parts_list.append(part_tupple[1]) 100 | 101 | part = ", ".join(parts_list) 102 | print(part) 103 | 104 | search_response = ( 105 | self.youtube.videos().list(part=part, id=self.id).execute() 106 | ) 107 | 108 | for search_result in search_response.get("items", []): 109 | if search_result["kind"] == "youtube#video": 110 | youtube_api.parse_youtube_video(self, search_result) 111 | 112 | # TODO Finish 113 | @require_channel_auth 114 | def update(self, title=None): 115 | """ Updates a part of video 116 | """ 117 | body = { 118 | "id": self.id, 119 | "snippet": {"title": "", "categoryId": 1}, 120 | } 121 | 122 | if title is not None: 123 | body["snippet"]["title"] = title 124 | print(body) 125 | response = ( 126 | self.channel.get_login() 127 | .videos() 128 | .update(body=body, part="snippet,status") 129 | .execute() 130 | ) 131 | 132 | print(response) 133 | 134 | @require_channel_auth 135 | def rate_video(self, rating: str): 136 | """Rates video, valid options are like, dislike and none 137 | """ 138 | if rating in ["like", "dislike", "none"]: 139 | request = self.channel.videos().rate( 140 | id="Ks-_Mh1QhMc", rating=rating 141 | ) 142 | request.execute() 143 | else: 144 | raise Exception("Not a valid rating:" + str(rating)) 145 | 146 | @require_channel_auth 147 | def like(self): 148 | """Likes video 149 | """ 150 | self.rate_video("like") 151 | 152 | @require_channel_auth 153 | def dislike(self): 154 | """Dislikes video 155 | """ 156 | self.rate_video("dislike") 157 | 158 | @require_channel_auth 159 | def remove_rating(self): 160 | """Removes rating 161 | """ 162 | self.rate_video("none") 163 | 164 | def fetch_comment_threads( 165 | self, snippet=True, replies=True 166 | ) -> CommentThread: 167 | """Fetches comment threads for video 168 | """ 169 | parts = "" 170 | if snippet: 171 | parts += "snippet" 172 | if replies: 173 | parts += ",replies" 174 | 175 | response = ( 176 | self.youtube.commentThreads() 177 | .list(part=parts, videoId=self.id) 178 | .execute() 179 | ) 180 | 181 | comment_threads = [] 182 | for item in response.get("items", []): 183 | comment_thread = CommentThread() 184 | CommentThreadSchema().from_dict(comment_thread, item) 185 | comment_threads.append(comment_thread) 186 | 187 | return comment_threads 188 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonnekaunisto/simple-youtube-api/704f136a4e9f56612caa14a9af128d88ed384202/tests/__init__.py -------------------------------------------------------------------------------- /tests/api_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonnekaunisto/simple-youtube-api/704f136a4e9f56612caa14a9af128d88ed384202/tests/api_test/__init__.py -------------------------------------------------------------------------------- /tests/api_test/test_channel.py: -------------------------------------------------------------------------------- 1 | """Test channel""" 2 | import os 3 | import datetime 4 | 5 | import urllib 6 | import pytest 7 | 8 | from simple_youtube_api import LocalVideo, Channel 9 | 10 | VIDEO_RESOURCE_URL = "http://commondatastorage.googleapis.com/ \ 11 | gtv-videos-bucket/sample/ForBiggerBlazes.mp4" 12 | VIDEO_NAME = "test_video.mp4" 13 | CLIENT_SECRET_NAME = "credentials/client_secret.json" 14 | CREDENTIALS = "credentials/credentials.storage" 15 | 16 | 17 | def download_video(): 18 | """Download example video""" 19 | urllib.request.urlretrieve(VIDEO_RESOURCE_URL, VIDEO_NAME) 20 | 21 | 22 | def test_channel_regular_function(): 23 | """Test channel constructor""" 24 | channel = Channel() 25 | assert channel 26 | 27 | 28 | def test_channel_fetch_uploads(): 29 | """Test fetching uploads""" 30 | channel = Channel() 31 | 32 | assert os.path.isfile(CLIENT_SECRET_NAME), "CLIENT SECRET_NAME not valid" 33 | 34 | assert os.path.isfile(CREDENTIALS), "CREDENTIALS is not valid" 35 | 36 | channel.login(CLIENT_SECRET_NAME, CREDENTIALS) 37 | 38 | videos = channel.fetch_uploads() 39 | 40 | for video in videos: 41 | print(video.title) 42 | 43 | 44 | def not_working_channel_upload_video(): 45 | """ 46 | testing this will make too many requests to google which will go over the 47 | query quota 48 | """ 49 | 50 | channel = Channel() 51 | 52 | assert os.path.isfile(CLIENT_SECRET_NAME), "CLIENT SECRET_NAME not valid" 53 | assert os.path.isfile(CREDENTIALS), "CREDENTIALS is not valid" 54 | 55 | title = "Time: " + str(datetime.datetime.now()) 56 | download_video() 57 | 58 | channel.login(CLIENT_SECRET_NAME, CREDENTIALS) 59 | video = LocalVideo(file_path=VIDEO_NAME) 60 | video.set_title(title) 61 | video.set_description("This is a description") 62 | video.set_tags(["this", "tag"]) 63 | video.set_category("film") 64 | video.set_privacy_status("private") 65 | 66 | assert channel.upload_video(video) 67 | 68 | 69 | if __name__ == "__main__": 70 | pytest.main() 71 | -------------------------------------------------------------------------------- /tests/api_test/test_youtube.py: -------------------------------------------------------------------------------- 1 | """Test youtube""" 2 | import pytest 3 | 4 | 5 | from simple_youtube_api import YouTube 6 | 7 | 8 | def test_regular_function(): 9 | """Test constructor""" 10 | with open("credentials/developer_key", "r") as myfile: 11 | developer_key = myfile.read().replace("\n", "") 12 | 13 | youtube = YouTube() 14 | youtube.login(developer_key) 15 | youtube.get_login() 16 | 17 | 18 | def test_youtube_search(): 19 | """Test youtube search""" 20 | youtube = get_youtube() 21 | 22 | videos = youtube.search("Your Search Term") 23 | 24 | for video in videos: 25 | print(video.title) 26 | 27 | video = youtube.search_by_video_id("Ks-_Mh1QhMc") 28 | print(video.title) 29 | 30 | response = youtube.search_by_video_id_raw("Ks-_Mh1QhMc") 31 | print(response) 32 | 33 | 34 | def test_fetch_categories(): 35 | """Test fetching categories""" 36 | youtube = get_youtube() 37 | youtube.fetch_categories() 38 | 39 | 40 | def get_youtube(): 41 | """Makes youtube object""" 42 | with open("credentials/developer_key", "r") as myfile: 43 | developer_key = myfile.read().replace("\n", "") 44 | 45 | youtube = YouTube() 46 | youtube.login(developer_key) 47 | 48 | return youtube 49 | 50 | 51 | if __name__ == "__main__": 52 | pytest.main() 53 | -------------------------------------------------------------------------------- /tests/api_test/test_youtube_video.py: -------------------------------------------------------------------------------- 1 | """ Testing youtube video """ 2 | import os 3 | import pytest 4 | 5 | from simple_youtube_api import YouTubeVideo, YouTube, Channel 6 | 7 | CLIENT_SECRET_NAME = "credentials/client_secret.json" 8 | CREDENTIALS = "credentials/credentials.storage" 9 | 10 | YOUTUBE_VIDEO_ID = "_i4fVYVqLbQ" 11 | 12 | 13 | def test_youtube_video_constructor(): 14 | """Test video constructor""" 15 | 16 | video_id = YOUTUBE_VIDEO_ID 17 | 18 | with open("credentials/developer_key", "r") as myfile: 19 | developer_key = myfile.read().replace("\n", "") 20 | 21 | youtube = YouTube() 22 | youtube.login(developer_key) 23 | 24 | video = YouTubeVideo(video_id=video_id, youtube=youtube.get_login()) 25 | 26 | video.set_youtube_auth(youtube) 27 | video.set_channel_auth(youtube) 28 | 29 | # assert video.video_id == video_id 30 | # assert video.title == title 31 | # assert video.description == description 32 | # assert video.tags == tags 33 | # assert video.category == id_category 34 | 35 | # for privacy_status in privacy_statuses: 36 | # video.set_privacy_status(privacy_status) 37 | # assert video.privacy_status == privacy_status 38 | 39 | 40 | def test_youtube_video_rating(): 41 | """Test rating youtube video""" 42 | video_id = YOUTUBE_VIDEO_ID 43 | 44 | channel = Channel() 45 | 46 | assert os.path.isfile(CLIENT_SECRET_NAME), "CLIENT SECRET_NAME not valid" 47 | assert os.path.isfile(CREDENTIALS), "CREDENTIALS is not valid" 48 | 49 | channel.login(CLIENT_SECRET_NAME, CREDENTIALS) 50 | 51 | video = YouTubeVideo(video_id=video_id, channel=channel.get_login()) 52 | 53 | video.dislike() 54 | video.remove_rating() 55 | video.like() 56 | 57 | with pytest.raises(Exception): 58 | video.rate_video("not_valid") 59 | 60 | 61 | def test_youtube_video_without_credentials(): 62 | """Test video without credentials""" 63 | video_id = YOUTUBE_VIDEO_ID 64 | 65 | video = YouTubeVideo(video_id) 66 | 67 | with pytest.raises(Exception): 68 | video.fetch() 69 | 70 | with pytest.raises(Exception): 71 | video.update() 72 | 73 | with pytest.raises(Exception): 74 | video.rate_video("like") 75 | 76 | with pytest.raises(Exception): 77 | video.like() 78 | 79 | with pytest.raises(Exception): 80 | video.dislike() 81 | 82 | with pytest.raises(Exception): 83 | video.remove_rating() 84 | 85 | 86 | def test_youtube_video_fetch(): 87 | """Test fetching video""" 88 | video_id = YOUTUBE_VIDEO_ID 89 | 90 | with open("credentials/developer_key", "r") as myfile: 91 | developer_key = myfile.read().replace("\n", "") 92 | 93 | youtube = YouTube() 94 | youtube.login(developer_key) 95 | 96 | video = YouTubeVideo(video_id=video_id, youtube=youtube.get_login()) 97 | video.fetch(all_parts=True) 98 | 99 | 100 | def test_youtube_video_fetch_comment_threads(): 101 | """Test fetching comment thread""" 102 | with open("credentials/developer_key", "r") as myfile: 103 | developer_key = myfile.read().replace("\n", "") 104 | 105 | youtube = YouTube() 106 | youtube.login(developer_key) 107 | 108 | video = YouTubeVideo(video_id=YOUTUBE_VIDEO_ID, youtube=youtube.get_login()) 109 | 110 | video.fetch_comment_threads() 111 | 112 | 113 | if __name__ == "__main__": 114 | pytest.main() 115 | -------------------------------------------------------------------------------- /tests/test_data/comment_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#comment", 3 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/7lRYhQc2_NmngYp9nMHCNON6BI0\"", 4 | "id": "UgzDE2tasfmrYLyNkGt4AaABAg.8g7oZbuBjXg8ilIKfmOr81", 5 | "snippet": { 6 | "authorDisplayName": "Burak Karakuş", 7 | "authorProfileImageUrl": "https://yt3.ggpht.com/-qoNUogjGHQg/AAAAAAAAAAI/AAAAAAAAAAA/5m7hSLsI8dE/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 8 | "authorChannelUrl": "http://www.youtube.com/channel/UCXPaHuq_I7IMjQanCONvzuw", 9 | "authorChannelId": { 10 | "value": "UCXPaHuq_I7IMjQanCONvzuw" 11 | }, 12 | "textDisplay": "\u003ca href=\"https://www.youtube.com/watch?v=7_2CJs_VZk4\"\u003ehttps://www.youtube.com/watch?v=7_2CJs_VZk4\u003c/a\u003e", 13 | "textOriginal": "https://www.youtube.com/watch?v=7_2CJs_VZk4", 14 | "parentId": "UgzDE2tasfmrYLyNkGt4AaABAg", 15 | "canRate": true, 16 | "viewerRating": "none", 17 | "likeCount": 0, 18 | "publishedAt": "2018-07-16T13:05:07.000Z", 19 | "updatedAt": "2018-07-16T13:05:07.000Z" 20 | } 21 | } -------------------------------------------------------------------------------- /tests/test_data/comment_thread_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#commentThread", 3 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/GadOd3XASoKqKL8OO_8iWg6NCj4\"", 4 | "id": "UgzZ35Ry_Nz-Qe54Jy54AaABAg", 5 | "snippet": { 6 | "videoId": "iBb9Io-E9A8", 7 | "topLevelComment": { 8 | "kind": "youtube#comment", 9 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/nWANvxxra5hHim1umoaKEUuHAks\"", 10 | "id": "UgzZ35Ry_Nz-Qe54Jy54AaABAg", 11 | "snippet": { 12 | "authorDisplayName": "Orange Juice Gaming", 13 | "authorProfileImageUrl": "https://yt3.ggpht.com/-866YL_6XbPQ/AAAAAAAAAAI/AAAAAAAAAAA/5rl24TNayQ0/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 14 | "authorChannelUrl": "http://www.youtube.com/channel/UC3S6nIDGJ5OtpC-mbvFA8Ew", 15 | "authorChannelId": { 16 | "value": "UC3S6nIDGJ5OtpC-mbvFA8Ew" 17 | }, 18 | "videoId": "iBb9Io-E9A8", 19 | "textDisplay": "If you have the Pass Royale and want to farm 300 crowns really quickly like I did, create a 3x Elixir tournament and invite all your clan mates. Make sure you all win equal amounts of time so you don't get stuck in match making. Trade crowns 3 for 2 every game.", 20 | "textOriginal": "If you have the Pass Royale and want to farm 300 crowns really quickly like I did, create a 3x Elixir tournament and invite all your clan mates. Make sure you all win equal amounts of time so you don't get stuck in match making. Trade crowns 3 for 2 every game.", 21 | "canRate": true, 22 | "viewerRating": "none", 23 | "likeCount": 557, 24 | "publishedAt": "2019-07-01T21:07:52.000Z", 25 | "updatedAt": "2019-07-01T21:08:44.000Z" 26 | } 27 | }, 28 | "canReply": true, 29 | "totalReplyCount": 32, 30 | "isPublic": true 31 | }, 32 | "replies": { 33 | "comments": [ 34 | { 35 | "kind": "youtube#comment", 36 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/OCYXYH_qraztq93vbWkPxFDD8Hw\"", 37 | "id": "UgzZ35Ry_Nz-Qe54Jy54AaABAg.8wrNncy6W8T8wuYh6Bz82C", 38 | "snippet": { 39 | "authorDisplayName": "PhatTw0", 40 | "authorProfileImageUrl": "https://yt3.ggpht.com/-m9AA_ESYstg/AAAAAAAAAAI/AAAAAAAAAAA/ttSnx-JlmU4/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 41 | "authorChannelUrl": "http://www.youtube.com/channel/UCHsS-to9iSQ-9NDkOmz9Z3Q", 42 | "authorChannelId": { 43 | "value": "UCHsS-to9iSQ-9NDkOmz9Z3Q" 44 | }, 45 | "videoId": "iBb9Io-E9A8", 46 | "textDisplay": "@TrAsH baNDiT got the same problem, any news?", 47 | "textOriginal": "@TrAsH baNDiT got the same problem, any news?", 48 | "parentId": "UgzZ35Ry_Nz-Qe54Jy54AaABAg", 49 | "canRate": true, 50 | "viewerRating": "none", 51 | "likeCount": 0, 52 | "publishedAt": "2019-07-03T02:40:48.000Z", 53 | "updatedAt": "2019-07-03T02:40:48.000Z" 54 | } 55 | }, 56 | { 57 | "kind": "youtube#comment", 58 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/U9xN4Pby2JA1DpR45zGXP7q6Ks8\"", 59 | "id": "UgzZ35Ry_Nz-Qe54Jy54AaABAg.8wrNncy6W8T8wttwQLbmfn", 60 | "snippet": { 61 | "authorDisplayName": "SETH MARCUM", 62 | "authorProfileImageUrl": "https://yt3.ggpht.com/-O0D4iUtRdtE/AAAAAAAAAAI/AAAAAAAAAAA/jd5z2Otblv4/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 63 | "authorChannelUrl": "http://www.youtube.com/channel/UClKpoGwd0HnpJHQNlxMpjdw", 64 | "authorChannelId": { 65 | "value": "UClKpoGwd0HnpJHQNlxMpjdw" 66 | }, 67 | "videoId": "iBb9Io-E9A8", 68 | "textDisplay": "The event for the legendary fisherman comes out ether Wednesday thru Friday this week", 69 | "textOriginal": "The event for the legendary fisherman comes out ether Wednesday thru Friday this week", 70 | "parentId": "UgzZ35Ry_Nz-Qe54Jy54AaABAg", 71 | "canRate": true, 72 | "viewerRating": "none", 73 | "likeCount": 1, 74 | "publishedAt": "2019-07-02T20:35:54.000Z", 75 | "updatedAt": "2019-07-02T20:35:54.000Z" 76 | } 77 | }, 78 | { 79 | "kind": "youtube#comment", 80 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/dUaSPW8zK2mWOUeMERX-enHwbE0\"", 81 | "id": "UgzZ35Ry_Nz-Qe54Jy54AaABAg.8wrNncy6W8T8wtnp6lMEje", 82 | "snippet": { 83 | "authorDisplayName": "Tanja Eremic", 84 | "authorProfileImageUrl": "https://yt3.ggpht.com/-8ruQH3zw6iw/AAAAAAAAAAI/AAAAAAAAAAA/kWKutc1nA58/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 85 | "authorChannelUrl": "http://www.youtube.com/channel/UCZzDLSn28CXFSdU4niBL6eA", 86 | "authorChannelId": { 87 | "value": "UCZzDLSn28CXFSdU4niBL6eA" 88 | }, 89 | "videoId": "iBb9Io-E9A8", 90 | "textDisplay": "My shop is broken if that offer goes away I'm braking my phone", 91 | "textOriginal": "My shop is broken if that offer goes away I'm braking my phone", 92 | "parentId": "UgzZ35Ry_Nz-Qe54Jy54AaABAg", 93 | "canRate": true, 94 | "viewerRating": "none", 95 | "likeCount": 0, 96 | "publishedAt": "2019-07-02T19:42:28.000Z", 97 | "updatedAt": "2019-07-02T19:42:28.000Z" 98 | } 99 | }, 100 | { 101 | "kind": "youtube#comment", 102 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/DjWDof9wtlwz8PxVWxFPfcjZp4I\"", 103 | "id": "UgzZ35Ry_Nz-Qe54Jy54AaABAg.8wrNncy6W8T8wtbYyITUu2", 104 | "snippet": { 105 | "authorDisplayName": "Casper Breer", 106 | "authorProfileImageUrl": "https://yt3.ggpht.com/-uoAl8_AUQyI/AAAAAAAAAAI/AAAAAAAAAAA/HRSkEyU5Td0/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 107 | "authorChannelUrl": "http://www.youtube.com/channel/UCnB5_JHkwc9x9bbB2uAbWSQ", 108 | "authorChannelId": { 109 | "value": "UCnB5_JHkwc9x9bbB2uAbWSQ" 110 | }, 111 | "videoId": "iBb9Io-E9A8", 112 | "textDisplay": "How do you create a private tournament?", 113 | "textOriginal": "How do you create a private tournament?", 114 | "parentId": "UgzZ35Ry_Nz-Qe54Jy54AaABAg", 115 | "canRate": true, 116 | "viewerRating": "none", 117 | "likeCount": 1, 118 | "publishedAt": "2019-07-02T17:55:16.000Z", 119 | "updatedAt": "2019-07-02T17:55:16.000Z" 120 | } 121 | } 122 | ] 123 | } 124 | } -------------------------------------------------------------------------------- /tests/test_data/video.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#video", 3 | "etag": "\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/083h0jILlJzm6KG23snsw7qq0eQ\"", 4 | "id": "IoYWO03b71M", 5 | "snippet": { 6 | "publishedAt": "2019-07-19T21:00:01.000Z", 7 | "channelId": "UCV9_KinVpV-snHe3C3n1hvA", 8 | "title": "The Return of Eugenia Cooney", 9 | "description": "Thank you for SUBSCRIBING \nhttps://www.youtube.com/user/shane?sub_confirmation=1\nEugenia Cooney\nhttps://www.youtube.com/user/eugeniacooney\nFOLLOW Andrew!\nhttps://www.instagram.com/andrewsiwicki/?hl=en\nKati Morton\nhttps://www.youtube.com/user/KatiMorton\nCheck out Kati’s Book - Are U Ok?\nhttps://www.amazon.com/gp/product/0738234990?tag=katimorton-20\nRyland\nhttps://www.youtube.com/channel/UC0CG8Kj2DqFc9bJld0hJKBA\n\nSend some love to EUGENIA!\nInstagram https://www.instagram.com/eugeniacooney/\nTwitter https://twitter.com/Eugenia_Cooney\nTwitch https://www.twitch.tv/eugeniacooney\n\nSONGS\n\n“IDK” by Bruce Wiegner\nInstagram: @BruceWiegner\nYouTube: https://youtu.be/8mWgJqtOMMY\nSpotify: https://open.spotify.com/artist/02mmPJCzXVNykiTwDgD5Pu?si=UzJfa5bMSxCxzZOLSozreg\n\nCatie Turner - “Breathe” \nhttps://www.youtube.com/watch?v=kLNNegUZ-F0\niTunes https://music.apple.com/us/artist/catie-turner/1238913200 \nSpotify https://open.spotify.com/artist/3nYYI90ObxhjLjdxaoXGSa\n\nAndrew Applepie\nhttps://andrewapplepie.com\nhttps://soundcloud.com/andrewapplepie\n\nVideos Featured:\n\nTo Eugenia Cooney - Karuna Satori ASMR\nhttps://www.youtube.com/watch?v=Y-BiVlYrqOs\nHow someone with an eating disorder feels - EnvyMaliceMIkki\nhttps://www.youtube.com/watch?v=1ofYSsK3xpM&t=271s\nLets talk Eugenia Cooney - JohnWiebe\nhttps://www.youtube.com/watch?v=vSUjZVLLjkg&t=261s\nPhil Defranco\nhttps://www.youtube.com/watch?v=g3aU3ZtQqs0&t=231s\nEugenia Cooney is Saved. - Yoel Rekts \nhttps://www.youtube.com/watch?v=pJvcjLKqt7c&t=44s", 10 | "thumbnails": { 11 | "default": { 12 | "url": "https://i.ytimg.com/vi/IoYWO03b71M/default.jpg", 13 | "width": 120, 14 | "height": 90 15 | }, 16 | "medium": { 17 | "url": "https://i.ytimg.com/vi/IoYWO03b71M/mqdefault.jpg", 18 | "width": 320, 19 | "height": 180 20 | }, 21 | "high": { 22 | "url": "https://i.ytimg.com/vi/IoYWO03b71M/hqdefault.jpg", 23 | "width": 480, 24 | "height": 360 25 | }, 26 | "standard": { 27 | "url": "https://i.ytimg.com/vi/IoYWO03b71M/sddefault.jpg", 28 | "width": 640, 29 | "height": 480 30 | }, 31 | "maxres": { 32 | "url": "https://i.ytimg.com/vi/IoYWO03b71M/maxresdefault.jpg", 33 | "width": 1280, 34 | "height": 720 35 | } 36 | }, 37 | "channelTitle": "shane", 38 | "tags": [ 39 | "shane", 40 | "dawson", 41 | "eugenia", 42 | "cooney", 43 | "journalism", 44 | "investigative journalism", 45 | "docuseries", 46 | "documentary" 47 | ], 48 | "categoryId": "23", 49 | "liveBroadcastContent": "none", 50 | "localized": { 51 | "title": "The Return of Eugenia Cooney", 52 | "description": "Thank you for SUBSCRIBING \nhttps://www.youtube.com/user/shane?sub_confirmation=1\nEugenia Cooney\nhttps://www.youtube.com/user/eugeniacooney\nFOLLOW Andrew!\nhttps://www.instagram.com/andrewsiwicki/?hl=en\nKati Morton\nhttps://www.youtube.com/user/KatiMorton\nCheck out Kati’s Book - Are U Ok?\nhttps://www.amazon.com/gp/product/0738234990?tag=katimorton-20\nRyland\nhttps://www.youtube.com/channel/UC0CG8Kj2DqFc9bJld0hJKBA\n\nSend some love to EUGENIA!\nInstagram https://www.instagram.com/eugeniacooney/\nTwitter https://twitter.com/Eugenia_Cooney\nTwitch https://www.twitch.tv/eugeniacooney\n\nSONGS\n\n“IDK” by Bruce Wiegner\nInstagram: @BruceWiegner\nYouTube: https://youtu.be/8mWgJqtOMMY\nSpotify: https://open.spotify.com/artist/02mmPJCzXVNykiTwDgD5Pu?si=UzJfa5bMSxCxzZOLSozreg\n\nCatie Turner - “Breathe” \nhttps://www.youtube.com/watch?v=kLNNegUZ-F0\niTunes https://music.apple.com/us/artist/catie-turner/1238913200 \nSpotify https://open.spotify.com/artist/3nYYI90ObxhjLjdxaoXGSa\n\nAndrew Applepie\nhttps://andrewapplepie.com\nhttps://soundcloud.com/andrewapplepie\n\nVideos Featured:\n\nTo Eugenia Cooney - Karuna Satori ASMR\nhttps://www.youtube.com/watch?v=Y-BiVlYrqOs\nHow someone with an eating disorder feels - EnvyMaliceMIkki\nhttps://www.youtube.com/watch?v=1ofYSsK3xpM&t=271s\nLets talk Eugenia Cooney - JohnWiebe\nhttps://www.youtube.com/watch?v=vSUjZVLLjkg&t=261s\nPhil Defranco\nhttps://www.youtube.com/watch?v=g3aU3ZtQqs0&t=231s\nEugenia Cooney is Saved. - Yoel Rekts \nhttps://www.youtube.com/watch?v=pJvcjLKqt7c&t=44s" 53 | } 54 | }, 55 | "contentDetails": { 56 | "duration": "PT1H17S", 57 | "dimension": "2d", 58 | "definition": "hd", 59 | "caption": "false", 60 | "licensedContent": true, 61 | "projection": "rectangular" 62 | }, 63 | "status": { 64 | "uploadStatus": "processed", 65 | "privacyStatus": "public", 66 | "license": "youtube", 67 | "embeddable": true, 68 | "publicStatsViewable": true 69 | }, 70 | "statistics": { 71 | "viewCount": "4595366", 72 | "likeCount": "868477", 73 | "dislikeCount": "6714", 74 | "favoriteCount": "0", 75 | "commentCount": "123034" 76 | }, 77 | "topicDetails": { 78 | "topicIds": [ 79 | "/m/02jjt" 80 | ], 81 | "relevantTopicIds": [ 82 | "/m/02jjt" 83 | ], 84 | "topicCategories": [ 85 | "https://en.wikipedia.org/wiki/Entertainment" 86 | ] 87 | }, 88 | "recordingDetails": { 89 | "TODO": true 90 | }, 91 | "fileDetails": { 92 | "TODO": true 93 | }, 94 | "processingDetails": { 95 | "TODO": true 96 | }, 97 | "suggestions": { 98 | "TODO": true 99 | }, 100 | "liveStreamingDetails": { 101 | "TODO": true 102 | } 103 | } -------------------------------------------------------------------------------- /tests/unit_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonnekaunisto/simple-youtube-api/704f136a4e9f56612caa14a9af128d88ed384202/tests/unit_test/__init__.py -------------------------------------------------------------------------------- /tests/unit_test/test_documentation.py: -------------------------------------------------------------------------------- 1 | """Testing Readme file""" 2 | import os 3 | import pytest 4 | import rstcheck 5 | from docutils.utils import Reporter 6 | 7 | README_FILE_NAME = "README.rst" 8 | file_path = os.path.dirname(os.path.realpath(__file__)) 9 | project_path = file_path + os.sep + ".." + os.sep + ".." 10 | README_PATH = project_path + os.sep + README_FILE_NAME 11 | 12 | 13 | def test_readme_format(): 14 | """Testing readme format""" 15 | with open(README_PATH, "r") as rst_file: 16 | text = rst_file.read() 17 | errors = list(rstcheck.check(text, report_level=Reporter.WARNING_LEVEL)) 18 | assert len(errors) == 0, errors 19 | 20 | 21 | if __name__ == "__main__": 22 | pytest.main() 23 | -------------------------------------------------------------------------------- /tests/unit_test/test_local_video.py: -------------------------------------------------------------------------------- 1 | """Testing local video""" 2 | import os 3 | 4 | import pytest 5 | 6 | from simple_youtube_api import LocalVideo 7 | from simple_youtube_api.channel import generate_upload_body 8 | from simple_youtube_api import youtube_constants 9 | 10 | 11 | def test_local_video_regular_function(): 12 | """Testing function""" 13 | file_path = ( 14 | os.path.dirname(os.path.abspath(__file__)) 15 | + os.sep 16 | + "test_local_video.py" 17 | ) 18 | title = "this is a title" 19 | description = "this is a description" 20 | tags = ["this", "is", "a", "tag"] 21 | string_category = "film" 22 | id_category = 1 23 | privacy_statuses = ["public", "private", "unlisted"] 24 | playlist_id = "some_playlist_id" 25 | 26 | video = LocalVideo(file_path) 27 | 28 | video.set_title(title) 29 | video.set_description(description) 30 | video.set_tags(tags) 31 | video.set_category(string_category) 32 | video.set_playlist(playlist_id) 33 | 34 | assert video.file_path == file_path 35 | assert video.title == title 36 | assert video.description == description 37 | assert video.tags == tags 38 | assert video.category == id_category 39 | assert video.playlist_id == playlist_id 40 | 41 | video.set_category(id_category) 42 | assert video.category == id_category 43 | 44 | for privacy_status in privacy_statuses: 45 | video.set_privacy_status(privacy_status) 46 | assert video.privacy_status == privacy_status 47 | 48 | # TODO: add stronger check 49 | assert generate_upload_body(video) 50 | 51 | 52 | def test_local_video_negative_function(): 53 | """Testing negative cases""" 54 | # snippet variables 55 | file_path = os.path.realpath(__file__) 56 | bad_file_path = "not_valid" 57 | thumbnail_path = "not_valid" 58 | title = "-" * (youtube_constants.MAX_YOUTUBE_TITLE_LENGTH + 1) 59 | description = "-" * (youtube_constants.MAX_YOUTUBE_DESCRIPTION_LENGTH + 1) 60 | tags = ["-" * (youtube_constants.MAX_YOUTUBE_TAGS_LENGTH + 1)] 61 | string_category = "not a category" 62 | id_category = -1 63 | default_language = False 64 | 65 | # status variables 66 | embeddable = "not_valid" 67 | video_license = "not_valid" 68 | privacy_status = "not_valid" 69 | public_stats_viewable = "not_valid" 70 | publish_at = False 71 | 72 | video = LocalVideo(file_path) 73 | 74 | # TODO: add stronger checks 75 | 76 | # misc test 77 | with pytest.raises(Exception): 78 | video.set_file_path(bad_file_path) 79 | with pytest.raises(Exception): 80 | video.set_thumbnail_path(thumbnail_path) 81 | 82 | # snippet test 83 | with pytest.raises(Exception): 84 | video.set_title(title) 85 | with pytest.raises(Exception): 86 | video.set_title(True) 87 | with pytest.raises(Exception): 88 | video.set_description(description) 89 | with pytest.raises(Exception): 90 | video.set_description(True) 91 | with pytest.raises(Exception): 92 | video.set_tags(tags) 93 | with pytest.raises(Exception): 94 | video.set_tags(True) 95 | with pytest.raises(Exception): 96 | video.set_category(string_category) 97 | with pytest.raises(Exception): 98 | video.set_category(id_category) 99 | with pytest.raises(Exception): 100 | video.set_default_language(default_language) 101 | 102 | # status test 103 | with pytest.raises(Exception): 104 | video.set_embeddable(embeddable) 105 | with pytest.raises(Exception): 106 | video.set_license(video_license) 107 | with pytest.raises(Exception): 108 | video.set_privacy_status(privacy_status) 109 | with pytest.raises(Exception): 110 | video.set_public_stats_viewable(public_stats_viewable) 111 | with pytest.raises(Exception): 112 | video.set_publish_at(publish_at) 113 | 114 | 115 | def test_local_video_constructor(): 116 | """Testing constructor""" 117 | file_path = ( 118 | os.path.dirname(os.path.abspath(__file__)) 119 | + os.sep 120 | + "test_local_video.py" 121 | ) 122 | title = "this is a title" 123 | description = "this is a description" 124 | tags = ["this", "is", "a", "tag"] 125 | string_category = "film" 126 | id_category = 1 127 | default_language = "english" 128 | 129 | # status variables 130 | embeddable = True 131 | video_license = "youtube" 132 | privacy_status = "public" 133 | public_stats_viewable = True 134 | publish_at = "9" 135 | 136 | # snippet test 137 | video = LocalVideo( 138 | file_path=file_path, 139 | title=title, 140 | description=description, 141 | tags=tags, 142 | category=string_category, 143 | default_language="english", 144 | ) 145 | 146 | assert video.file_path == file_path, "Wrong file path: " + str( 147 | video.file_path 148 | ) 149 | assert video.title == title, "Wrong title:" + str(video.title) 150 | assert video.description == description, "Wrong description: " + str( 151 | video.description 152 | ) 153 | assert video.tags == tags, "Wrong tags: " + str(video.tags) 154 | assert video.category == id_category, "Wrong category: " + str( 155 | video.category 156 | ) 157 | assert ( 158 | video.default_language == default_language 159 | ), "Wrong language: " + str(video.default_language) 160 | 161 | assert video.snippet_set is True, "Wrong snippet set: " + str( 162 | video.snippet_set 163 | ) 164 | 165 | # status test 166 | assert video.status_set is False, "Wrong status set " + str(video.status_set) 167 | 168 | video.set_embeddable(embeddable) 169 | video.set_license(video_license) 170 | video.set_privacy_status(privacy_status) 171 | video.set_public_stats_viewable(public_stats_viewable) 172 | video.set_publish_at(publish_at) 173 | 174 | assert video.embeddable == embeddable 175 | assert video.license == video_license 176 | assert ( 177 | video.privacy_status == privacy_status 178 | ), "Privacy Wrong: " + str(video.privacy_status) 179 | assert video.public_stats_viewable == public_stats_viewable 180 | assert video.publish_at == publish_at 181 | 182 | assert video.status_set is True, "Wrong video status: " + str( 183 | video.status_set 184 | ) 185 | 186 | video.set_thumbnail_path(file_path) 187 | assert video.thumbnail_path == file_path 188 | 189 | str(video) 190 | 191 | 192 | if __name__ == "__main__": 193 | pytest.main() 194 | -------------------------------------------------------------------------------- /tests/unit_test/test_youtube_api.py: -------------------------------------------------------------------------------- 1 | """ Testing youtube api """ 2 | 3 | import os 4 | import json 5 | 6 | import pytest 7 | 8 | from simple_youtube_api.comment import Comment, CommentSchema 9 | from simple_youtube_api.comment_thread import CommentThread, CommentThreadSchema 10 | from simple_youtube_api import youtube_api, YouTubeVideo 11 | 12 | 13 | data_dir = os.sep + ".." + os.sep + "test_data" 14 | 15 | 16 | def test_parse_video(): 17 | """Testing parsing video""" 18 | data_path = ( 19 | os.path.dirname(os.path.abspath(__file__)) 20 | + os.sep 21 | + data_dir 22 | + os.sep 23 | + "video.json" 24 | ) 25 | 26 | with open(data_path, "r", encoding="utf8") as file: 27 | data = json.loads(file.read()) 28 | 29 | video = YouTubeVideo() 30 | video = youtube_api.parse_youtube_video(video, data) 31 | 32 | assert video.id == data["id"] 33 | assert video.title == data["snippet"]["title"] 34 | assert video.definition == data["contentDetails"]["definition"] 35 | assert video.license == data["status"]["license"] 36 | assert video.view_count == data["statistics"]["viewCount"] 37 | 38 | 39 | def test_parse_comment_thread(): 40 | """Testing parsing comment thread""" 41 | data_path = ( 42 | os.path.dirname(os.path.abspath(__file__)) 43 | + os.sep 44 | + data_dir 45 | + os.sep 46 | + "comment_thread_test.json" 47 | ) 48 | 49 | with open(data_path, "r") as file: 50 | data = json.loads(file.read()) 51 | 52 | comment_thread = CommentThread() 53 | CommentThreadSchema().from_dict(comment_thread, data) 54 | 55 | assert comment_thread.id == data["id"] 56 | 57 | # snippet 58 | snippet_data = data.get("snippet", False) 59 | if snippet_data: 60 | assert comment_thread.channel_id == snippet_data.get("channelId", None) 61 | assert comment_thread.video_id == snippet_data.get("videoId", None) 62 | assert comment_thread.can_reply == snippet_data["canReply"] 63 | assert ( 64 | comment_thread.total_reply_count == snippet_data["totalReplyCount"] 65 | ) 66 | assert comment_thread.is_public == snippet_data["isPublic"] 67 | 68 | assert str(comment_thread) 69 | 70 | 71 | def test_parse_comment(): 72 | """Testing parsing comment""" 73 | data_path = ( 74 | os.path.dirname(os.path.abspath(__file__)) 75 | + os.sep 76 | + data_dir 77 | + os.sep 78 | + "comment_test.json" 79 | ) 80 | 81 | with open(data_path, "r") as file: 82 | data = json.loads(file.read()) 83 | 84 | comment = Comment() 85 | CommentSchema().from_dict(comment, data) 86 | 87 | assert comment.etag == data["etag"] 88 | assert comment.id == data["id"] 89 | 90 | # snippet 91 | snippet_data = data.get("snippet", False) 92 | if snippet_data: 93 | assert comment.author_display_name == snippet_data["authorDisplayName"] 94 | assert ( 95 | comment.author_profile_image_url 96 | == snippet_data["authorProfileImageUrl"] 97 | ) 98 | assert comment.author_channel_url == snippet_data["authorChannelUrl"] 99 | assert ( 100 | comment.author_channel_id 101 | == snippet_data["authorChannelId"]["value"] 102 | ) 103 | assert comment.channel_id == snippet_data.get("videoId", None) 104 | assert comment.video_id == snippet_data.get("videoId", None) 105 | assert comment.text_display == snippet_data["textDisplay"] 106 | assert comment.text_original == snippet_data["textOriginal"] 107 | assert comment.parent_id == snippet_data.get("parentId", None) 108 | assert comment.can_rate == snippet_data["canRate"] 109 | assert comment.viewer_rating == snippet_data["viewerRating"] 110 | assert comment.like_count == snippet_data["likeCount"] 111 | assert comment.moderation_status == snippet_data.get( 112 | "moderationStatus", None 113 | ) 114 | assert comment.published_at == snippet_data["publishedAt"] 115 | assert comment.updated_at == snippet_data["updatedAt"] 116 | 117 | assert str(comment) 118 | 119 | 120 | if __name__ == "__main__": 121 | pytest.main() 122 | -------------------------------------------------------------------------------- /tools/autogenerate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | import json 5 | import re 6 | 7 | SCHEMA_PATH = os.path.abspath("../resources/schema") 8 | 9 | 10 | def main(): 11 | parser = argparse.ArgumentParser(description="Do stuff") 12 | parser.add_argument("--schema", default=None) 13 | arguments = parser.parse_args() 14 | schema_file = arguments.schema 15 | schema_file_path = SCHEMA_PATH + os.sep + schema_file 16 | 17 | name = schema_file.replace(".json", "") 18 | 19 | with open(schema_file_path, "r") as myfile: 20 | schema = myfile.read() 21 | 22 | schema_json = json.loads(schema) 23 | 24 | autogenerate_parse(schema_json, name) 25 | 26 | 27 | def autogenerate_parse(schema_json, name): 28 | myfile = open(name + ".py", "w") 29 | final_string = "\n" 30 | 31 | final_string += "def parse_{0}({0}, data):\n".format(name) 32 | 33 | for key in schema_json.keys(): 34 | py_key = convert_var(key) 35 | key_json = schema_json[key] 36 | 37 | if key == "kind": 38 | continue 39 | 40 | if type(schema_json[key]) is not dict: 41 | final_string += "{3}{0}.{2} = data['{1}']\n".format( 42 | name, key, py_key, 4 * " " 43 | ) 44 | continue 45 | 46 | final_string += "\n" 47 | final_string += 4 * " " + "# " + key + "\n" 48 | final_string += "{2}{1}_data = data.get('{0}', False)\n".format( 49 | key, py_key, 4 * " " 50 | ) 51 | final_string += "{1}if {0}_data:\n".format(py_key, 4 * " ") 52 | 53 | for key2 in key_json.keys(): 54 | if type(key_json[key2]) is dict: 55 | continue 56 | py_key2 = convert_var(key2) 57 | key2_json = key_json[key2] 58 | final_string += "{4}{0}.{3} = {2}_data.get('{1}', None)\n".format( 59 | name, key2, py_key, py_key2, 8 * " " 60 | ) 61 | 62 | final_string += "\n" 63 | final_string += "{1}return {0}".format(name, 4 * " ") 64 | 65 | myfile.write(final_string) 66 | myfile.close() 67 | # print(final_string) 68 | 69 | 70 | def convert_var(var_name): 71 | return re.sub("(?