├── .bumpversion.cfg ├── .coveragerc ├── .github ├── hack │ ├── changelog.sh │ └── version.sh └── workflows │ ├── docs.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.rst ├── docs ├── docs │ ├── CHANGELOG.md │ ├── authorization.md │ ├── getting_started.md │ ├── images │ │ ├── auth-1-chose-account.png │ │ ├── auth-2-not-approval.png │ │ ├── auth-3-advanced.png │ │ ├── auth-4-allow-permission.png │ │ ├── gt-create-app-1.png │ │ ├── gt-create-app-2.png │ │ ├── gt-create-app-3.png │ │ ├── gt-create-app-4.png │ │ ├── gt-create-app-5.png │ │ ├── gt-create-app-6.png │ │ └── structure-uml.png │ ├── index.md │ ├── installation.md │ ├── introduce-new-structure.md │ └── usage │ │ ├── work-with-api.md │ │ └── work-with-client.md └── mkdocs.yml ├── examples ├── README.md ├── __init__.py ├── apis │ ├── __init__.py │ ├── channel_videos.py │ ├── get_all_videos_id_with_channel_by_search.py │ ├── get_subscription_with_oauth.py │ └── oauth_flow.py └── clients │ ├── __init__.py │ ├── channel_info.py │ ├── oauth_flow.py │ └── upload_video.py ├── pyproject.toml ├── pytest.ini ├── pyyoutube ├── __init__.py ├── __version__.py ├── api.py ├── client.py ├── error.py ├── media.py ├── models │ ├── __init__.py │ ├── activity.py │ ├── auth.py │ ├── base.py │ ├── caption.py │ ├── category.py │ ├── channel.py │ ├── channel_banner.py │ ├── channel_section.py │ ├── comment.py │ ├── comment_thread.py │ ├── common.py │ ├── i18n.py │ ├── member.py │ ├── memberships_level.py │ ├── mixins.py │ ├── playlist.py │ ├── playlist_item.py │ ├── search_result.py │ ├── subscription.py │ ├── video.py │ ├── video_abuse_report_reason.py │ └── watermark.py ├── resources │ ├── __init__.py │ ├── activities.py │ ├── base_resource.py │ ├── captions.py │ ├── channel_banners.py │ ├── channel_sections.py │ ├── channels.py │ ├── comment_threads.py │ ├── comments.py │ ├── i18n_languages.py │ ├── i18n_regions.py │ ├── members.py │ ├── membership_levels.py │ ├── playlist_items.py │ ├── playlists.py │ ├── search.py │ ├── subscriptions.py │ ├── thumbnails.py │ ├── video_abuse_report_reasons.py │ ├── video_categories.py │ ├── videos.py │ └── watermarks.py ├── utils │ ├── __init__.py │ ├── constants.py │ └── params_checker.py └── youtube_utils.py ├── testdata ├── apidata │ ├── abuse_reasons │ │ └── abuse_reason.json │ ├── access_token.json │ ├── activities │ │ ├── activities_by_channel_p1.json │ │ ├── activities_by_channel_p2.json │ │ ├── activities_by_mine_p1.json │ │ └── activities_by_mine_p2.json │ ├── captions │ │ ├── captions_by_video.json │ │ ├── captions_filter_by_id.json │ │ ├── insert_response.json │ │ └── update_response.json │ ├── categories │ │ ├── guide_categories_by_region.json │ │ ├── guide_category_multi.json │ │ ├── guide_category_single.json │ │ ├── video_category_by_region.json │ │ ├── video_category_multi.json │ │ └── video_category_single.json │ ├── channel_banners │ │ └── insert_response.json │ ├── channel_info_multi.json │ ├── channel_info_single.json │ ├── channel_sections │ │ ├── channel_sections_by_channel.json │ │ ├── channel_sections_by_id.json │ │ ├── channel_sections_by_ids.json │ │ └── insert_resp.json │ ├── channels │ │ ├── info.json │ │ ├── info_multiple.json │ │ └── update_resp.json │ ├── client_secrets │ │ ├── client_secret_installed_bad.json │ │ ├── client_secret_installed_good.json │ │ ├── client_secret_unsupported.json │ │ └── client_secret_web.json │ ├── comment_threads │ │ ├── comment_thread_single.json │ │ ├── comment_threads_all_to_me.json │ │ ├── comment_threads_by_channel.json │ │ ├── comment_threads_by_video_paged_1.json │ │ ├── comment_threads_by_video_paged_2.json │ │ ├── comment_threads_multi.json │ │ ├── comment_threads_with_search.json │ │ └── insert_response.json │ ├── comments │ │ ├── comments_by_parent_paged_1.json │ │ ├── comments_by_parent_paged_2.json │ │ ├── comments_multi.json │ │ ├── comments_single.json │ │ └── insert_response.json │ ├── error_permission_resp.json │ ├── i18ns │ │ ├── language_res.json │ │ └── regions_res.json │ ├── members │ │ ├── members_data.json │ │ └── membership_levels.json │ ├── playlist_items │ │ ├── insert_response.json │ │ ├── playlist_items_filter_video.json │ │ ├── playlist_items_multi.json │ │ ├── playlist_items_paged_1.json │ │ ├── playlist_items_paged_2.json │ │ └── playlist_items_single.json │ ├── playlists │ │ ├── insert_response.json │ │ ├── playlists_mine.json │ │ ├── playlists_multi.json │ │ ├── playlists_paged_1.json │ │ ├── playlists_paged_2.json │ │ └── playlists_single.json │ ├── search │ │ ├── search_by_developer.json │ │ ├── search_by_event.json │ │ ├── search_by_keywords_p1.json │ │ ├── search_by_keywords_p2.json │ │ ├── search_by_location.json │ │ ├── search_by_mine.json │ │ ├── search_by_related_video.json │ │ ├── search_channels.json │ │ └── search_videos_by_channel.json │ ├── subscriptions │ │ ├── insert_response.json │ │ ├── subscription_zero.json │ │ ├── subscriptions_by_channel_p1.json │ │ ├── subscriptions_by_channel_p2.json │ │ ├── subscriptions_by_channel_with_filter.json │ │ ├── subscriptions_by_id.json │ │ ├── subscriptions_by_mine_filter.json │ │ ├── subscriptions_by_mine_p1.json │ │ └── subscriptions_by_mine_p2.json │ ├── user_profile.json │ └── videos │ │ ├── get_rating_response.json │ │ ├── insert_response.json │ │ ├── videos_chart_paged_1.json │ │ ├── videos_chart_paged_2.json │ │ ├── videos_info_multi.json │ │ ├── videos_info_single.json │ │ ├── videos_myrating_paged_1.json │ │ └── videos_myrating_paged_2.json ├── error_response.json ├── error_response_simple.json └── modeldata │ ├── abuse_report_reason │ ├── abuse_reason.json │ └── abuse_reason_res.json │ ├── activities │ ├── activity.json │ ├── activity_contentDetails.json │ ├── activity_response.json │ └── activity_snippet.json │ ├── captions │ ├── caption.json │ ├── caption_response.json │ └── caption_snippet.json │ ├── categories │ ├── guide_category_info.json │ ├── guide_category_response.json │ ├── video_category_info.json │ └── video_category_response.json │ ├── channel_sections │ ├── channel_section_info.json │ └── channel_section_response.json │ ├── channels │ ├── channel_api_response.json │ ├── channel_branding_settings.json │ ├── channel_content_details.json │ ├── channel_info.json │ ├── channel_snippet.json │ ├── channel_statistics.json │ ├── channel_status.json │ └── channel_topic_details.json │ ├── comments │ ├── comment_api_response.json │ ├── comment_info.json │ ├── comment_snippet.json │ ├── comment_thread_api_response.json │ ├── comment_thread_info.json │ ├── comment_thread_replies.json │ └── comment_thread_snippet.json │ ├── common │ ├── thumbnail_info.json │ └── thumbnails_info.json │ ├── i18ns │ ├── language_info.json │ ├── language_res.json │ ├── region_info.json │ └── region_res.json │ ├── members │ ├── member_info.json │ └── membership_level.json │ ├── playlist_items │ ├── playlist_item_api_response.json │ ├── playlist_item_content_details.json │ ├── playlist_item_info.json │ ├── playlist_item_snippet.json │ └── playlist_item_status.json │ ├── playlists │ ├── playlist_api_response.json │ ├── playlist_content_details.json │ ├── playlist_info.json │ ├── playlist_snippet.json │ └── playlist_status.json │ ├── search_result │ ├── search_result.json │ ├── search_result_api_response.json │ ├── search_result_id.json │ └── search_result_snippet.json │ ├── subscriptions │ ├── contentDetails.json │ ├── resp.json │ ├── snippet.json │ ├── subscriberSnippet.json │ └── subscription.json │ ├── users │ ├── access_token.json │ └── user_profile.json │ └── videos │ ├── video_api_response.json │ ├── video_category_info.json │ ├── video_content_details.json │ ├── video_info.json │ ├── video_recording_details.json │ ├── video_snippet.json │ ├── video_statistics.json │ ├── video_status.json │ └── video_topic_details.json └── tests ├── __init__.py ├── apis ├── __init__.py ├── test_activities.py ├── test_auth.py ├── test_captions.py ├── test_categories.py ├── test_channel_sections.py ├── test_channels.py ├── test_comment_threads.py ├── test_comments.py ├── test_i18ns.py ├── test_members.py ├── test_playlist_items.py ├── test_playlists.py ├── test_search.py ├── test_subscriptions.py ├── test_video_abuse_reason.py └── test_videos.py ├── clients ├── __init__.py ├── base.py ├── test_activities.py ├── test_captions.py ├── test_channel_banners.py ├── test_channel_sections.py ├── test_channels.py ├── test_client.py ├── test_comment_threads.py ├── test_comments.py ├── test_i18n.py ├── test_media.py ├── test_members.py ├── test_membership_levels.py ├── test_playlist_items.py ├── test_playlists.py ├── test_search.py ├── test_subscriptions.py ├── test_thumbnails.py ├── test_video_abuse_report_reasons.py ├── test_video_categories.py ├── test_videos.py └── test_watermarks.py ├── conftest.py ├── models ├── __init__.py ├── test_abuse_reason.py ├── test_activities.py ├── test_auth_models.py ├── test_captions.py ├── test_category.py ├── test_channel.py ├── test_channel_sections.py ├── test_comments.py ├── test_i18n_models.py ├── test_members.py ├── test_playlist.py ├── test_playlist_item.py ├── test_search_result.py ├── test_subscriptions.py └── test_videos.py ├── test_error_handling.py ├── test_youtube_utils.py └── utils ├── __init__.py └── test_params_checker.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.9.7 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:pyyoutube/__version__.py] 7 | 8 | [bumpversion:file:pyproject.toml] 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = pyyoutube/__version__.py -------------------------------------------------------------------------------- /.github/hack/changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | MARKER_PREFIX="## Version" 4 | VERSION=$(echo "$1" | sed 's/^v//g') 5 | 6 | IFS='' 7 | found=0 8 | 9 | while read -r "line"; do 10 | # If not found and matching heading 11 | if [ $found -eq 0 ] && echo "$line" | grep -q "$MARKER_PREFIX $VERSION"; then 12 | echo "$line" 13 | found=1 14 | continue 15 | fi 16 | 17 | # If needed version if found, and reaching next delimter - stop 18 | if [ $found -eq 1 ] && echo "$line" | grep -q -E "$MARKER_PREFIX [[:digit:]]+\.[[:digit:]]+\.[[:digit:]]"; then 19 | found=0 20 | break 21 | fi 22 | 23 | # Keep printing out lines as no other version delimiter found 24 | if [ $found -eq 1 ]; then 25 | echo "$line" 26 | fi 27 | done < CHANGELOG.md -------------------------------------------------------------------------------- /.github/hack/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | LATEST_TAG_REV=$(git rev-list --tags --max-count=1) 4 | LATEST_COMMIT_REV=$(git rev-list HEAD --max-count=1) 5 | 6 | if [ -n "$LATEST_TAG_REV" ]; then 7 | LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)") 8 | else 9 | LATEST_TAG="v0.0.0" 10 | fi 11 | 12 | if [ "$LATEST_TAG_REV" != "$LATEST_COMMIT_REV" ]; then 13 | echo "$LATEST_TAG+$(git rev-list HEAD --max-count=1 --abbrev-commit)" 14 | else 15 | echo "$LATEST_TAG" 16 | fi -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Publish docs via GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | name: Deploy docs 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Deploy docs 15 | uses: mhausenblas/mkdocs-deploy-gh-pages@master 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | CONFIG_FILE: docs/mkdocs.yml 19 | EXTRA_PACKAGES: build-base 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Pypi 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Build and publish to pypi 12 | uses: JRubics/poetry-publish@v1.17 13 | with: 14 | pypi_token: ${{ secrets.PYPI_TOKEN }} 15 | 16 | - name: Generate Changelog 17 | run: | 18 | VERSION=$(.github/hack/version.sh) 19 | .github/hack/changelog.sh $VERSION > NEW-VERSION-CHANGELOG.md 20 | - name: Publish 21 | uses: softprops/action-gh-release@v1 22 | with: 23 | body_path: NEW-VERSION-CHANGELOG.md 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-20.04 12 | strategy: 13 | matrix: 14 | python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] 15 | include: 16 | - python-version: '3.8' 17 | update-coverage: true 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Cache pip 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.cache/pip 29 | key: ${{ matrix.python-version }}-poetry-${{ hashFiles('pyproject.toml') }} 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip poetry 33 | poetry install 34 | - name: Test with pytest 35 | run: | 36 | poetry run pytest 37 | - name: Upload coverage to Codecov 38 | if: ${{ matrix.update-coverage }} 39 | uses: codecov/codecov-action@v4 40 | with: 41 | file: ./coverage.xml 42 | fail_ci_if_error: true 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | 45 | lint: 46 | name: black 47 | runs-on: ubuntu-latest 48 | 49 | steps: 50 | - uses: actions/checkout@v2 51 | - uses: actions/setup-python@v2 52 | with: 53 | python-version: 3.8 54 | - name: Cache pip 55 | uses: actions/cache@v2 56 | with: 57 | path: ~/.cache/pip 58 | key: lintenv-v2 59 | - name: Install dependencies 60 | run: python -m pip install --upgrade pip black 61 | - name: Black test 62 | run: make lint-check 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # pipenv 81 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 82 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 83 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 84 | # install all needed dependencies. 85 | #Pipfile.lock 86 | 87 | poetry.lock 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | .spyproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | 117 | 118 | # PyCharm 119 | .idea/ 120 | 121 | # for git commitizen 122 | node_modules/ 123 | package-lock.json 124 | package.json 125 | 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 sns-sdks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: help clean lint test 2 | 3 | .PHONY: all 4 | 5 | help: 6 | @echo " env install all dependencies" 7 | @echo " clean remove unwanted stuff" 8 | @echo " docs build documentation" 9 | @echo " lint check style with black" 10 | @echo " test run tests with cov" 11 | 12 | env: 13 | pip install --upgrade pip 14 | pip install poetry 15 | poetry install 16 | 17 | clean: clean-build clean-pyc clean-test 18 | 19 | clean-build: 20 | rm -fr build/ 21 | rm -fr dist/ 22 | rm -fr .eggs/ 23 | find . -name '*.egg-info' -exec rm -fr {} + 24 | find . -name '*.egg' -exec rm -f {} + 25 | 26 | clean-pyc: 27 | find . -name '*.pyc' -exec rm -f {} + 28 | find . -name '*.pyo' -exec rm -f {} + 29 | find . -name '*~' -exec rm -f {} + 30 | find . -name '__pycache__' -exec rm -fr {} + 31 | 32 | clean-test: 33 | rm -fr .pytest_cache 34 | rm -f .coverage 35 | rm -fr htmlcov/ 36 | 37 | docs: 38 | $(MAKE) -C docs html 39 | 40 | lint: 41 | black . 42 | 43 | lint-check: 44 | black --check . 45 | 46 | test: 47 | pytest -s 48 | 49 | tests-html: 50 | pytest -s --cov-report term --cov-report html 51 | 52 | # v0.1.0 -> v0.2.0 53 | bump-minor: 54 | bump2version minor 55 | 56 | # v0.1.0 -> v0.1.1 57 | bump-patch: 58 | bump2version patch 59 | -------------------------------------------------------------------------------- /docs/docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.md -------------------------------------------------------------------------------- /docs/docs/getting_started.md: -------------------------------------------------------------------------------- 1 | This doc is a simple tutorial to show how to use this library to get data from YouTube DATA API. 2 | 3 | You can get the whole description for YouTube API at [YouTube API Reference](https://developers.google.com/youtube/v3/docs/). 4 | 5 | ## Prerequisite 6 | 7 | At the beginning. You need to create a [Google Project](https://console.cloud.google.com) by your google account. 8 | 9 | Every new account has 12 project to cost. 10 | 11 | ## Create your project 12 | 13 | Click the `Select a project-> NEW PROJECT` to create a new project to use our library. 14 | 15 | Fill the basic info to finish created. 16 | 17 | ![gt-create-app-1](images/gt-create-app-1.png) 18 | 19 | ## Enable YouTube DATA API service 20 | 21 | Once the project created, the browser will redirect project home page. 22 | 23 | Then click the `≡≡` symbol on the left top. Chose the `APIs & Services` tab. 24 | 25 | You will see follow info. 26 | 27 | ![gt-create-app-2](images/gt-create-app-2.png) 28 | 29 | Click the `+ ENABLE APIS AND SERVICES` symbol. And input `YouTube DATA API` to search. 30 | 31 | ![gt-create-app-3](images/gt-create-app-3.png) 32 | 33 | Then chose the ``YouTube DATA API`` item. 34 | 35 | ![gt-create-app-4](images/gt-create-app-4.png) 36 | 37 | Then click the `ENABLE` blue button. Now the service has been activated. 38 | 39 | ## Create credentials 40 | 41 | To use this API, you may need credentials. Click 'Create credentials' to get started. 42 | 43 | ![gt-create-app-5](images/gt-create-app-5.png) 44 | 45 | You need to fill in some information to create credentials. 46 | 47 | Just chose `YouTube DATA API v3`, `Other non-UI (e.g. cron job, daemon)` and `Public data`. 48 | 49 | Then click the blue button `What credentials do I need?` to create. 50 | 51 | ![gt-create-app-6](images/gt-create-app-6.png) 52 | 53 | Now you have generated one api key. 54 | 55 | Use this key. You can retrieve public data for YouTube data by our library 56 | 57 | ```python 58 | from pyyoutube import Client 59 | 60 | cli = Client(api_key="your api key") 61 | ``` 62 | 63 | If you want to get some examples to see, check out the [examples](https://github.com/sns-sdks/python-youtube/tree/master/examples). 64 | 65 | If you have an opens source application using python-youtube, send me a link, and I am very happy to add a link to it here. 66 | 67 | But if you want to get user data by OAuth. You need create the credential for ``OAuth client ID``. 68 | 69 | And get more info at next page for [Authorization](authorization.md). 70 | -------------------------------------------------------------------------------- /docs/docs/images/auth-1-chose-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/auth-1-chose-account.png -------------------------------------------------------------------------------- /docs/docs/images/auth-2-not-approval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/auth-2-not-approval.png -------------------------------------------------------------------------------- /docs/docs/images/auth-3-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/auth-3-advanced.png -------------------------------------------------------------------------------- /docs/docs/images/auth-4-allow-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/auth-4-allow-permission.png -------------------------------------------------------------------------------- /docs/docs/images/gt-create-app-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/gt-create-app-1.png -------------------------------------------------------------------------------- /docs/docs/images/gt-create-app-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/gt-create-app-2.png -------------------------------------------------------------------------------- /docs/docs/images/gt-create-app-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/gt-create-app-3.png -------------------------------------------------------------------------------- /docs/docs/images/gt-create-app-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/gt-create-app-4.png -------------------------------------------------------------------------------- /docs/docs/images/gt-create-app-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/gt-create-app-5.png -------------------------------------------------------------------------------- /docs/docs/images/gt-create-app-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/gt-create-app-6.png -------------------------------------------------------------------------------- /docs/docs/images/structure-uml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/docs/docs/images/structure-uml.png -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Python-Youtube's documentation! 2 | 3 | **A Python wrapper around for YouTube Data API.** 4 | 5 | Author: IkarosKun 6 | 7 | ## Introduction 8 | 9 | 10 | With the YouTube Data API, you can add a variety of YouTube features to your application. 11 | 12 | Use the API to upload videos, manage playlists and subscriptions, update channel settings, and more. 13 | 14 | This library provides a Python interface for the [YouTube DATA API](https://developers.google.com/youtube/v3). 15 | 16 | Library could work on Python 3.6+. 17 | 18 | !!! tip "Tips" 19 | 20 | This library only supports `DATA API`, It does not support `Analytics and Reporting APIs` and `Live Streaming API`. 21 | -------------------------------------------------------------------------------- /docs/docs/installation.md: -------------------------------------------------------------------------------- 1 | This library supports Python 3.6 and newer. 2 | 3 | ## Dependencies 4 | 5 | These following distributions will be installed automatically when installing Python-Youtube. 6 | 7 | - [requests](https://2.python-requests.org/en/master/): is an elegant and simple HTTP library for Python, built for human beings. 8 | - [Requests-OAuthlib](https://requests-oauthlib.readthedocs.io/en/latest/): uses the Python Requests and OAuthlib libraries to provide an easy-to-use Python interface for building OAuth1 and OAuth2 clients. 9 | - [isodate](https://pypi.org/project/isodate/): implements ISO 8601 date, time and duration parsing. 10 | 11 | ## Installation 12 | 13 | You can install this library from **PyPI** 14 | 15 | ```shell 16 | $ pip install --upgrade python-youtube 17 | ``` 18 | 19 | 20 | Also, you can build this library from source code 21 | 22 | ```shell 23 | $ git clone https://github.com/sns-sdks/python-youtube.git 24 | $ cd python-youtube 25 | $ make env 26 | $ make build 27 | ``` 28 | 29 | ## Testing 30 | 31 | If you have been installing the requirements use ``make env``. 32 | You can use following command to test the code 33 | 34 | ```shell 35 | $ make tests-html 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/docs/introduce-new-structure.md: -------------------------------------------------------------------------------- 1 | This doc will show you the new api structure for this library. 2 | 3 | ## Brief 4 | 5 | To make the package easier to maintain and easy to use. We are shifted to using classes for different YouTube resources in an easier, higher-level programming experience. 6 | 7 | ![structure-uml](images/structure-uml.png) 8 | 9 | 10 | In this structure, every resource will have self class. And to operate with YouTube API. 11 | 12 | ## Simple usage 13 | 14 | 15 | ### Initial Client 16 | 17 | ```python 18 | from pyyoutube import Client 19 | 20 | client = Client(api_key="your api key") 21 | ``` 22 | 23 | ### Get data. 24 | 25 | for example to get channel data. 26 | 27 | ```python 28 | resp = client.channels.list( 29 | parts=["id", "snippet"], 30 | channel_id="UCa-vrCLQHviTOVnEKDOdetQ" 31 | ) 32 | # resp output 33 | # ChannelListResponse(kind='youtube#channelListResponse') 34 | # resp.items[0].id output 35 | # UCa-vrCLQHviTOVnEKDOdetQ 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Python-Youtube Docs 2 | site_description: Docs for python-youtube library 3 | site_url: https://sns-sdks.github.io/python-youtube/ 4 | repo_url: https://github.com/sns-sdks/python-youtube 5 | copyright: Copyright © 2019 - 2021 Ikaros kun 6 | 7 | 8 | theme: 9 | name: material 10 | features: 11 | - navigation.tabs 12 | palette: 13 | # Light mode 14 | - media: "(prefers-color-scheme: light)" 15 | scheme: default 16 | primary: indigo 17 | accent: indigo 18 | toggle: 19 | icon: material/toggle-switch-off-outline 20 | name: Switch to dark mode 21 | 22 | # Dark mode 23 | - media: "(prefers-color-scheme: dark)" 24 | scheme: slate 25 | primary: blue 26 | accent: blue 27 | toggle: 28 | icon: material/toggle-switch 29 | name: Switch to light mode 30 | 31 | nav: 32 | - Introduction: index.md 33 | - Introduce Structure: introduce-new-structure.md 34 | - Usage: 35 | - Work With `Api`: usage/work-with-api.md 36 | - Work With `Client`: usage/work-with-client.md 37 | - Installation: installation.md 38 | - Getting Started: getting_started.md 39 | - Authorization: authorization.md 40 | - Changelog: CHANGELOG.md 41 | 42 | extra: 43 | social: 44 | - icon: fontawesome/brands/twitter 45 | link: https://twitter.com/realllkk520 46 | - icon: fontawesome/brands/github 47 | link: https://github.com/sns-sdks/python-youtube 48 | 49 | 50 | markdown_extensions: 51 | - codehilite 52 | - admonition 53 | - pymdownx.superfences 54 | - pymdownx.emoji 55 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Now we provide two entry for operate YouTube DATA API. 4 | 5 | - Use Api `from pyyoutube import Api`: This is an old implementation used to be compatible with older versions of code. 6 | - Use Client `from pyyoutube import Client`: This is a new implementation for operating the API and provides additional 7 | capabilities. 8 | 9 | # Basic Usage 10 | 11 | ## API 12 | 13 | ```python 14 | from pyyoutube import Api 15 | 16 | api = Api(api_key="your key") 17 | api.get_channel_info(channel_id="id for channel") 18 | # ChannelListResponse(kind='youtube#channelListResponse') 19 | ``` 20 | 21 | You can get more examples at [this](/examples/apis/). 22 | 23 | ## Client 24 | 25 | ```python 26 | from pyyoutube import Client 27 | 28 | cli = Client(api_key="your key") 29 | cli.channels.list(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw") 30 | # ChannelListResponse(kind='youtube#channelListResponse') 31 | ``` 32 | 33 | You can get more examples at [this](/examples/clients/). 34 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/examples/__init__.py -------------------------------------------------------------------------------- /examples/apis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/examples/apis/__init__.py -------------------------------------------------------------------------------- /examples/apis/channel_videos.py: -------------------------------------------------------------------------------- 1 | """ 2 | Retrieve some videos info from given channel. 3 | 4 | Use pyyoutube.api.get_channel_info to get channel video uploads playlist id. 5 | Then use pyyoutube.api.get_playlist_items to get playlist's videos id. 6 | Last use get_video_by_id to get videos data. 7 | """ 8 | 9 | import pyyoutube 10 | 11 | API_KEY = "xxx" # replace this with your api key. 12 | 13 | 14 | def get_videos(channel_id): 15 | api = pyyoutube.Api(api_key=API_KEY) 16 | channel_info = api.get_channel_info(channel_id=channel_id) 17 | 18 | playlist_id = channel_info.items[0].contentDetails.relatedPlaylists.uploads 19 | 20 | uploads_playlist_items = api.get_playlist_items( 21 | playlist_id=playlist_id, count=10, limit=6 22 | ) 23 | 24 | videos = [] 25 | for item in uploads_playlist_items.items: 26 | video_id = item.contentDetails.videoId 27 | video = api.get_video_by_id(video_id=video_id) 28 | videos.extend(video.items) 29 | return videos 30 | 31 | 32 | def processor(): 33 | channel_id = "UC_x5XG1OV2P6uZZ5FSM9Ttw" 34 | videos = get_videos(channel_id) 35 | 36 | with open("videos.json", "w+") as f: 37 | for video in videos: 38 | f.write(video.to_json()) 39 | f.write("\n") 40 | 41 | 42 | if __name__ == "__main__": 43 | processor() 44 | -------------------------------------------------------------------------------- /examples/apis/get_all_videos_id_with_channel_by_search.py: -------------------------------------------------------------------------------- 1 | """ 2 | Retrieve channel's videos by search api. 3 | 4 | Note Quota impact: A call to this method has a quota cost of 100 units. 5 | """ 6 | 7 | import pyyoutube 8 | 9 | API_KEY = "xxx" # replace this with your api key. 10 | 11 | 12 | def get_all_videos_id_by_channel(channel_id, limit=50, count=50): 13 | api = pyyoutube.Api(api_key=API_KEY) 14 | 15 | videos = [] 16 | next_page = None 17 | 18 | while True: 19 | res = api.search( 20 | channel_id=channel_id, 21 | limit=limit, 22 | count=count, 23 | page_token=next_page, 24 | ) 25 | 26 | next_page = res.nextPageToken 27 | 28 | for item in res.items: 29 | if item.id.videoId: 30 | videos.append(item.id.videoId) 31 | 32 | if not next_page: 33 | break 34 | 35 | return videos 36 | -------------------------------------------------------------------------------- /examples/apis/get_subscription_with_oauth.py: -------------------------------------------------------------------------------- 1 | """ 2 | This demo show how to use this library to do authorization and get your subscription. 3 | """ 4 | 5 | import pyyoutube 6 | import webbrowser 7 | 8 | CLIENT_ID = "your app id" 9 | CLIENT_SECRET = "your app secret" 10 | 11 | 12 | def get_subscriptions(): 13 | api = pyyoutube.Api(client_id=CLIENT_ID, client_secret=CLIENT_SECRET) 14 | 15 | # need follows scope 16 | scope = ["https://www.googleapis.com/auth/youtube.readonly"] 17 | 18 | url, _ = api.get_authorization_url(scope=scope) 19 | 20 | print( 21 | "Try to start a browser to visit the authorization page. If not opened. you can copy and visit by hand:\n" 22 | f"{url}" 23 | ) 24 | webbrowser.open(url) 25 | 26 | auth_response = input( 27 | "\nCopy the whole url if you finished the step to authorize:\n" 28 | ) 29 | 30 | api.generate_access_token(authorization_response=auth_response, scope=scope) 31 | 32 | sub_res = api.get_subscription_by_me(mine=True, parts="id,snippet", count=None) 33 | 34 | with open("subscriptions.json", "w+") as f: 35 | f.write(sub_res.to_json()) 36 | 37 | print("Finished.") 38 | 39 | 40 | if __name__ == "__main__": 41 | get_subscriptions() 42 | -------------------------------------------------------------------------------- /examples/apis/oauth_flow.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates how to perform authorization. 3 | """ 4 | 5 | from pyyoutube import Api 6 | 7 | CLIENT_ID = "xxx" # Your app id 8 | CLIENT_SECRET = "xxx" # Your app secret 9 | SCOPE = [ 10 | "https://www.googleapis.com/auth/youtube", 11 | "https://www.googleapis.com/auth/youtube.force-ssl", 12 | "https://www.googleapis.com/auth/userinfo.profile", 13 | ] 14 | 15 | 16 | def do_authorize(): 17 | api = Api(client_id=CLIENT_ID, client_secret=CLIENT_SECRET) 18 | 19 | authorize_url, state = api.get_authorization_url(scope=SCOPE) 20 | print(f"Click url to do authorize: {authorize_url}") 21 | 22 | response_uri = input("Input youtube redirect uri:\n") 23 | 24 | token = api.generate_access_token(authorization_response=response_uri, scope=SCOPE) 25 | print(f"Your token: {token}") 26 | 27 | # get data 28 | profile = api.get_profile() 29 | print(f"Your channel id: {profile.id}") 30 | 31 | 32 | if __name__ == "__main__": 33 | do_authorize() 34 | -------------------------------------------------------------------------------- /examples/clients/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/examples/clients/__init__.py -------------------------------------------------------------------------------- /examples/clients/channel_info.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates how to retrieve information for a channel. 3 | """ 4 | 5 | from pyyoutube import Client 6 | 7 | API_KEY = "Your key" # replace this with your api key. 8 | 9 | 10 | def get_channel_info(): 11 | cli = Client(api_key=API_KEY) 12 | 13 | channel_id = "UC_x5XG1OV2P6uZZ5FSM9Ttw" 14 | 15 | resp = cli.channels.list( 16 | channel_id=channel_id, parts=["id", "snippet", "statistics"], return_json=True 17 | ) 18 | print(f"Channel info: {resp['items'][0]}") 19 | 20 | 21 | if __name__ == "__main__": 22 | get_channel_info() 23 | -------------------------------------------------------------------------------- /examples/clients/oauth_flow.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates how to perform authorization. 3 | """ 4 | 5 | from pyyoutube import Client 6 | 7 | 8 | CLIENT_ID = "xxx" # Your app id 9 | CLIENT_SECRET = "xxx" # Your app secret 10 | CLIENT_SECRET_PATH = None # or your path/to/client_secret_web.json 11 | 12 | SCOPE = [ 13 | "https://www.googleapis.com/auth/youtube", 14 | "https://www.googleapis.com/auth/youtube.force-ssl", 15 | "https://www.googleapis.com/auth/userinfo.profile", 16 | ] 17 | 18 | 19 | def do_authorize(): 20 | cli = Client(client_id=CLIENT_ID, client_secret=CLIENT_SECRET) 21 | # or if you want to use a web type client_secret.json 22 | # cli = Client(client_secret_path=CLIENT_SECRET_PATH) 23 | 24 | authorize_url, state = cli.get_authorize_url(scope=SCOPE) 25 | print(f"Click url to do authorize: {authorize_url}") 26 | 27 | response_uri = input("Input youtube redirect uri:\n") 28 | 29 | token = cli.generate_access_token(authorization_response=response_uri, scope=SCOPE) 30 | print(f"Your token: {token}") 31 | 32 | # get data 33 | resp = cli.channels.list(mine=True) 34 | print(f"Your channel id: {resp.items[0].id}") 35 | 36 | 37 | if __name__ == "__main__": 38 | do_authorize() 39 | -------------------------------------------------------------------------------- /examples/clients/upload_video.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates how to upload a video. 3 | """ 4 | 5 | import pyyoutube.models as mds 6 | from pyyoutube import Client 7 | from pyyoutube.media import Media 8 | 9 | # Access token with scope: 10 | # https://www.googleapis.com/auth/youtube.upload 11 | # https://www.googleapis.com/auth/youtube 12 | # https://www.googleapis.com/auth/youtube.force-ssl 13 | ACCESS_TOKEN = "xxx" 14 | 15 | 16 | def upload_video(): 17 | cli = Client(access_token=ACCESS_TOKEN) 18 | 19 | body = mds.Video( 20 | snippet=mds.VideoSnippet(title="video title", description="video description") 21 | ) 22 | 23 | media = Media(filename="target_video.mp4") 24 | 25 | upload = cli.videos.insert( 26 | body=body, media=media, parts=["snippet"], notify_subscribers=True 27 | ) 28 | 29 | response = None 30 | while response is None: 31 | print(f"Uploading video...") 32 | status, response = upload.next_chunk() 33 | if status is not None: 34 | print(f"Uploading video progress: {status.progress()}...") 35 | 36 | # Use video class to representing the video resource. 37 | video = mds.Video.from_dict(response) 38 | print(f"Video id {video.id} was successfully uploaded.") 39 | 40 | 41 | if __name__ == "__main__": 42 | upload_video() 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "python-youtube" 3 | version = "0.9.7" 4 | description = "A Python wrapper around for YouTube Data API." 5 | authors = ["ikaroskun "] 6 | license = "MIT" 7 | keywords = ["youtube-api", "youtube-v3-api", "youtube-data-api", "youtube-sdk"] 8 | readme = "README.rst" 9 | homepage = "https://github.com/sns-sdks/python-youtube" 10 | repository = "https://github.com/sns-sdks/python-youtube" 11 | classifiers = [ 12 | "Intended Audience :: Developers", 13 | "License :: OSI Approved :: Apache Software License", 14 | "Topic :: Software Development :: Libraries :: Python Modules", 15 | "Programming Language :: Python", 16 | "Programming Language :: Python :: 3.6", 17 | "Programming Language :: Python :: 3.7", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: Implementation :: CPython", 24 | "Programming Language :: Python :: Implementation :: PyPy", 25 | ] 26 | 27 | packages = [ 28 | { include = "pyyoutube" }, 29 | { include = "tests", format = "sdist" }, 30 | ] 31 | 32 | [tool.poetry.dependencies] 33 | python = "^3.6" 34 | requests = "^2.24.0" 35 | requests-oauthlib = "=1.3.0,<3.0.0" 36 | isodate = ">=0.6.0,<=0.7.2" 37 | dataclasses-json = [ 38 | { version = "^0.5.3", python = "<3.7" }, 39 | { version = "^0.6.0", python = ">=3.7" } 40 | ] 41 | 42 | [tool.poetry.dev-dependencies] 43 | responses = [ 44 | { version = "^0.17.0", python = "<3.7" }, 45 | { version = "^0.23.0", python = ">=3.7" } 46 | ] 47 | pytest = [ 48 | { version = "^6.2", python = "<3.7" }, 49 | { version = "^7.1", python = ">=3.7" } 50 | ] 51 | pytest-cov = [ 52 | { version = "^2.10.1", python = "<3.7" }, 53 | { version = "^3.0.0", python = ">=3.7" } 54 | ] 55 | 56 | [build-system] 57 | requires = ["poetry-core>=1.0.0"] 58 | build-backend = "poetry.core.masonry.api" 59 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --cov=pyyoutube --cov-report xml -------------------------------------------------------------------------------- /pyyoutube/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import Api # noqa 2 | from .client import Client # noqa 3 | from .error import * # noqa 4 | from .models import * # noqa 5 | from .utils.constants import TOPICS # noqa 6 | -------------------------------------------------------------------------------- /pyyoutube/__version__.py: -------------------------------------------------------------------------------- 1 | # d8888b. db db d888888b db db .d88b. d8b db db db .d88b. db db d888888b db db d8888b. d88888b 2 | # 88 `8D `8b d8' `~~88~~' 88 88 .8P Y8. 888o 88 `8b d8' .8P Y8. 88 88 `~~88~~' 88 88 88 `8D 88' 3 | # 88oodD' `8bd8' 88 88ooo88 88 88 88V8o 88 `8bd8' 88 88 88 88 88 88 88 88oooY' 88ooooo 4 | # 88~~~ 88 88 88~~~88 88 88 88 V8o88 88 88 88 88 88 88 88 88 88~~~b. 88~~~~~ 5 | # 88 88 88 88 88 `8b d8' 88 V888 88 `8b d8' 88b d88 88 88b d88 88 8D 88. 6 | # 88 YP YP YP YP `Y88P' VP V8P YP `Y88P' ~Y8888P' YP ~Y8888P' Y8888P' Y88888P 7 | 8 | __version__ = "0.9.7" 9 | -------------------------------------------------------------------------------- /pyyoutube/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .activity import * # noqa 2 | from .auth import AccessToken, UserProfile 3 | from .caption import * # noqa 4 | from .category import * # noqa 5 | from .channel import * # noqa 6 | from .channel_banner import * # noqa 7 | from .channel_section import * # noqa 8 | from .comment import * # noqa 9 | from .comment_thread import * # noqa 10 | from .i18n import * # noqa 11 | from .member import * # noqa 12 | from .memberships_level import * # noqa 13 | from .playlist_item import * # noqa 14 | from .playlist import * # noqa 15 | from .search_result import * # noqa 16 | from .subscription import * # noqa 17 | from .video_abuse_report_reason import * # noqa 18 | from .video import * # noqa 19 | from .watermark import * # noqa 20 | -------------------------------------------------------------------------------- /pyyoutube/models/auth.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List, Optional 3 | 4 | from .base import BaseModel 5 | 6 | 7 | @dataclass 8 | class AccessToken(BaseModel): 9 | """ 10 | A class representing for access token. 11 | Refer: https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#exchange-authorization-code 12 | """ 13 | 14 | access_token: Optional[str] = field(default=None) 15 | expires_in: Optional[int] = field(default=None) 16 | refresh_token: Optional[str] = field(default=None, repr=False) 17 | scope: Optional[List[str]] = field(default=None, repr=False) 18 | token_type: Optional[str] = field(default=None) 19 | expires_at: Optional[float] = field(default=None, repr=False) 20 | 21 | 22 | @dataclass 23 | class UserProfile(BaseModel): 24 | """ 25 | A class representing for user profile. 26 | Refer: https://any-api.com/googleapis_com/oauth2/docs/userinfo/oauth2_userinfo_v2_me_get 27 | """ 28 | 29 | id: Optional[str] = field(default=None) 30 | name: Optional[str] = field(default=None) 31 | given_name: Optional[str] = field(default=None, repr=False) 32 | family_name: Optional[str] = field(default=None, repr=False) 33 | picture: Optional[str] = field(default=None, repr=False) 34 | locale: Optional[str] = field(default=None, repr=False) 35 | -------------------------------------------------------------------------------- /pyyoutube/models/base.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, asdict 2 | from typing import Type, TypeVar 3 | 4 | from dataclasses_json import DataClassJsonMixin 5 | from dataclasses_json.core import Json, _decode_dataclass 6 | 7 | A = TypeVar("A", bound="DataClassJsonMixin") 8 | 9 | 10 | @dataclass 11 | class BaseModel(DataClassJsonMixin): 12 | """Base model class for instance use.""" 13 | 14 | @classmethod 15 | def from_dict(cls: Type[A], kvs: Json, *, infer_missing=False) -> A: 16 | # save original data for lookup 17 | cls._json = kvs 18 | return _decode_dataclass(cls, kvs, infer_missing) 19 | 20 | def to_dict_ignore_none(self): 21 | return asdict( 22 | obj=self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None} 23 | ) 24 | -------------------------------------------------------------------------------- /pyyoutube/models/caption.py: -------------------------------------------------------------------------------- 1 | """ 2 | These are caption related models 3 | """ 4 | 5 | from dataclasses import dataclass, field 6 | from typing import List, Optional 7 | 8 | from .base import BaseModel 9 | from .mixins import DatetimeTimeMixin 10 | from .common import BaseResource, BaseApiResponse 11 | 12 | 13 | @dataclass 14 | class CaptionSnippet(BaseModel, DatetimeTimeMixin): 15 | """ 16 | A class representing the caption snippet resource info. 17 | 18 | Refer: https://developers.google.com/youtube/v3/docs/captions#snippet 19 | """ 20 | 21 | videoId: Optional[str] = field(default=None) 22 | lastUpdated: Optional[str] = field(default=None) 23 | trackKind: Optional[str] = field(default=None, repr=False) 24 | language: Optional[str] = field(default=None, repr=False) 25 | name: Optional[str] = field(default=None, repr=False) 26 | audioTrackType: Optional[str] = field(default=None, repr=False) 27 | isCC: Optional[bool] = field(default=None, repr=False) 28 | isLarge: Optional[bool] = field(default=None, repr=False) 29 | isEasyReader: Optional[bool] = field(default=None, repr=False) 30 | isDraft: Optional[bool] = field(default=None, repr=False) 31 | isAutoSynced: Optional[bool] = field(default=None, repr=False) 32 | status: Optional[str] = field(default=None, repr=False) 33 | failureReason: Optional[str] = field(default=None, repr=False) 34 | 35 | 36 | @dataclass 37 | class Caption(BaseResource): 38 | """ 39 | A class representing the caption resource info. 40 | 41 | Refer: https://developers.google.com/youtube/v3/docs/captions 42 | """ 43 | 44 | snippet: Optional[CaptionSnippet] = field(default=None) 45 | 46 | 47 | @dataclass 48 | class CaptionListResponse(BaseApiResponse): 49 | """ 50 | A class representing the activity response info. 51 | 52 | Refer: https://developers.google.com/youtube/v3/docs/captions/list?#response_1 53 | """ 54 | 55 | items: Optional[List[Caption]] = field(default=None, repr=False) 56 | -------------------------------------------------------------------------------- /pyyoutube/models/category.py: -------------------------------------------------------------------------------- 1 | """ 2 | These are category related models. 3 | Include VideoCategory 4 | """ 5 | 6 | from dataclasses import dataclass, field 7 | from typing import List, Optional 8 | 9 | from .base import BaseModel 10 | from .common import BaseApiResponse, BaseResource 11 | 12 | 13 | @dataclass 14 | class CategorySnippet(BaseModel): 15 | """ 16 | This is base category snippet for video and guide. 17 | """ 18 | 19 | channelId: Optional[str] = field(default=None) 20 | title: Optional[str] = field(default=None) 21 | 22 | 23 | @dataclass 24 | class VideoCategorySnippet(CategorySnippet): 25 | """ 26 | A class representing video category snippet info. 27 | 28 | Refer: https://developers.google.com/youtube/v3/docs/videoCategories#snippet 29 | """ 30 | 31 | assignable: Optional[bool] = field(default=None, repr=False) 32 | 33 | 34 | @dataclass 35 | class VideoCategory(BaseResource): 36 | """ 37 | A class representing video category info. 38 | 39 | Refer: https://developers.google.com/youtube/v3/docs/videoCategories 40 | """ 41 | 42 | snippet: Optional[VideoCategorySnippet] = field(default=None, repr=False) 43 | 44 | 45 | @dataclass 46 | class VideoCategoryListResponse(BaseApiResponse): 47 | """ 48 | A class representing the video category's retrieve response info. 49 | 50 | Refer: https://developers.google.com/youtube/v3/docs/videoCategories/list#response_1 51 | """ 52 | 53 | items: Optional[List[VideoCategory]] = field(default=None, repr=False) 54 | -------------------------------------------------------------------------------- /pyyoutube/models/channel_banner.py: -------------------------------------------------------------------------------- 1 | """ 2 | There are channel banner related models 3 | 4 | References: https://developers.google.com/youtube/v3/docs/channelBanners#properties 5 | """ 6 | 7 | from dataclasses import dataclass, field 8 | from typing import List, Optional 9 | 10 | from .base import BaseModel 11 | 12 | 13 | @dataclass 14 | class ChannelBanner(BaseModel): 15 | """ 16 | A class representing the channel banner's info. 17 | 18 | References: https://developers.google.com/youtube/v3/docs/channelBanners#resource 19 | """ 20 | 21 | kind: Optional[str] = field(default=None) 22 | etag: Optional[str] = field(default=None, repr=False) 23 | url: Optional[str] = field(default=None) 24 | -------------------------------------------------------------------------------- /pyyoutube/models/channel_section.py: -------------------------------------------------------------------------------- 1 | """ 2 | Those are models related to channel sections. 3 | """ 4 | 5 | from dataclasses import dataclass, field 6 | from typing import List, Optional 7 | 8 | from .base import BaseModel 9 | from .common import BaseResource, BaseApiResponse 10 | 11 | 12 | @dataclass 13 | class ChannelSectionSnippet(BaseModel): 14 | """ 15 | A class representing the channel section snippet info. 16 | 17 | Refer: https://developers.google.com/youtube/v3/docs/channelSections#snippet 18 | """ 19 | 20 | type: Optional[str] = field(default=None) 21 | channelId: Optional[str] = field(default=None, repr=False) 22 | title: Optional[str] = field(default=None, repr=False) 23 | position: Optional[int] = field(default=None) 24 | 25 | 26 | @dataclass 27 | class ChannelSectionContentDetails(BaseModel): 28 | """ 29 | A class representing the channel section content details info. 30 | 31 | Refer: https://developers.google.com/youtube/v3/docs/channelSections#contentDetails 32 | """ 33 | 34 | playlists: Optional[List[str]] = field(default=None, repr=False) 35 | channels: Optional[List[str]] = field(default=None) 36 | 37 | 38 | @dataclass 39 | class ChannelSection(BaseResource): 40 | """ 41 | A class representing the channel section info. 42 | 43 | Refer: https://developers.google.com/youtube/v3/docs/channelSections#properties 44 | """ 45 | 46 | snippet: Optional[ChannelSectionSnippet] = field(default=None, repr=False) 47 | contentDetails: Optional[ChannelSectionContentDetails] = field( 48 | default=None, repr=False 49 | ) 50 | 51 | 52 | @dataclass 53 | class ChannelSectionResponse(BaseApiResponse): 54 | """ 55 | A class representing the channel section's retrieve response info. 56 | 57 | Refer: https://developers.google.com/youtube/v3/docs/channelSections/list?#properties_1 58 | """ 59 | 60 | items: Optional[List[ChannelSection]] = field(default=None, repr=False) 61 | 62 | 63 | @dataclass 64 | class ChannelSectionListResponse(ChannelSectionResponse): ... 65 | -------------------------------------------------------------------------------- /pyyoutube/models/comment_thread.py: -------------------------------------------------------------------------------- 1 | """ 2 | These are comment threads related models. 3 | """ 4 | 5 | from dataclasses import dataclass, field 6 | from typing import Optional, List 7 | 8 | from .base import BaseModel 9 | from .common import BaseResource, BaseApiResponse 10 | from .comment import Comment 11 | 12 | 13 | @dataclass 14 | class CommentThreadSnippet(BaseModel): 15 | """A class representing comment tread snippet info. 16 | 17 | References: https://developers.google.com/youtube/v3/docs/commentThreads#snippet 18 | """ 19 | 20 | channelId: Optional[str] = field(default=None) 21 | videoId: Optional[str] = field(default=None) 22 | topLevelComment: Optional[Comment] = field(default=None, repr=False) 23 | canReply: Optional[bool] = field(default=None, repr=False) 24 | totalReplyCount: Optional[int] = field(default=None, repr=False) 25 | isPublic: Optional[bool] = field(default=None, repr=False) 26 | 27 | 28 | @dataclass 29 | class CommentThreadReplies(BaseModel): 30 | """ 31 | A class representing comment tread replies info. 32 | 33 | Refer: https://developers.google.com/youtube/v3/docs/commentThreads#replies 34 | """ 35 | 36 | comments: Optional[List[Comment]] = field(default=None, repr=False) 37 | 38 | 39 | @dataclass 40 | class CommentThread(BaseResource): 41 | """ 42 | A class representing comment thread info. 43 | 44 | Refer: https://developers.google.com/youtube/v3/docs/commentThreads 45 | """ 46 | 47 | snippet: Optional[CommentThreadSnippet] = field(default=None, repr=False) 48 | replies: Optional[CommentThreadReplies] = field(default=None, repr=False) 49 | 50 | 51 | @dataclass 52 | class CommentThreadListResponse(BaseApiResponse): 53 | """ 54 | A class representing the comment thread's retrieve response info. 55 | 56 | Refer: https://developers.google.com/youtube/v3/docs/commentThreads/list#response_1 57 | """ 58 | 59 | items: Optional[List[CommentThread]] = field(default=None, repr=False) 60 | -------------------------------------------------------------------------------- /pyyoutube/models/i18n.py: -------------------------------------------------------------------------------- 1 | """ 2 | These are i18n language and region related models. 3 | """ 4 | 5 | from dataclasses import dataclass, field 6 | from typing import List, Optional 7 | 8 | from .base import BaseModel 9 | from .common import BaseResource, BaseApiResponse 10 | 11 | 12 | @dataclass 13 | class I18nRegionSnippet(BaseModel): 14 | """ 15 | A class representing the I18n region snippet info. 16 | 17 | Refer: https://developers.google.com/youtube/v3/docs/i18nRegions#snippet 18 | """ 19 | 20 | gl: Optional[str] = field(default=None) 21 | name: Optional[str] = field(default=None) 22 | 23 | 24 | @dataclass 25 | class I18nRegion(BaseResource): 26 | """ 27 | A class representing the I18n region info. 28 | 29 | Refer: https://developers.google.com/youtube/v3/docs/i18nRegions#resource-representation 30 | """ 31 | 32 | snippet: Optional[I18nRegionSnippet] = field(default=None) 33 | 34 | 35 | @dataclass 36 | class I18nRegionListResponse(BaseApiResponse): 37 | """ 38 | A class representing the I18n region list response info. 39 | 40 | Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages/list#response_1 41 | """ 42 | 43 | items: Optional[List[I18nRegion]] = field(default=None, repr=False) 44 | 45 | 46 | @dataclass 47 | class I18nLanguageSnippet(BaseModel): 48 | """ 49 | A class representing the I18n language snippet info. 50 | 51 | Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages#snippet 52 | """ 53 | 54 | hl: Optional[str] = field(default=None) 55 | name: Optional[str] = field(default=None) 56 | 57 | 58 | @dataclass 59 | class I18nLanguage(BaseResource): 60 | """ 61 | A class representing the I18n language info. 62 | 63 | Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages#resource-representation 64 | """ 65 | 66 | snippet: Optional[I18nLanguageSnippet] = field(default=None) 67 | 68 | 69 | @dataclass 70 | class I18nLanguageListResponse(BaseApiResponse): 71 | """ 72 | A class representing the I18n language list response info. 73 | 74 | Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages/list#response_1 75 | """ 76 | 77 | items: Optional[List[I18nLanguage]] = field(default=None, repr=False) 78 | -------------------------------------------------------------------------------- /pyyoutube/models/memberships_level.py: -------------------------------------------------------------------------------- 1 | """ 2 | These are membership level related models. 3 | """ 4 | 5 | from dataclasses import dataclass, field 6 | from typing import List, Optional 7 | 8 | from .base import BaseModel 9 | from .common import BaseResource, BaseApiResponse 10 | 11 | 12 | @dataclass 13 | class MembershipLevelSnippetLevelDetails(BaseModel): 14 | displayName: Optional[str] = field(default=None) 15 | 16 | 17 | @dataclass 18 | class MembershipsLevelSnippet(BaseModel): 19 | """ 20 | A class representing the membership level snippet. 21 | 22 | Refer: https://developers.google.com/youtube/v3/docs/membershipsLevels#snippet 23 | """ 24 | 25 | creatorChannelId: Optional[str] = field(default=None) 26 | levelDetails: Optional[MembershipLevelSnippetLevelDetails] = field( 27 | default=None, repr=False 28 | ) 29 | 30 | 31 | @dataclass 32 | class MembershipsLevel(BaseResource): 33 | """ 34 | A class representing the membership level. 35 | 36 | Refer: https://developers.google.com/youtube/v3/docs/membershipsLevels 37 | """ 38 | 39 | snippet: Optional[MembershipsLevelSnippet] = field(default=None, repr=False) 40 | 41 | 42 | @dataclass 43 | class MembershipsLevelListResponse(BaseApiResponse): 44 | """ 45 | A class representing the memberships level's retrieve response info. 46 | 47 | Refer: https://developers.google.com/youtube/v3/docs/membershipsLevels/list#response 48 | """ 49 | 50 | items: Optional[List[MembershipsLevel]] = field(default=None, repr=False) 51 | -------------------------------------------------------------------------------- /pyyoutube/models/mixins.py: -------------------------------------------------------------------------------- 1 | """ 2 | These are some mixin for models 3 | """ 4 | 5 | import datetime 6 | from typing import Optional 7 | 8 | import isodate 9 | from isodate.isoerror import ISO8601Error 10 | 11 | from pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException 12 | 13 | 14 | class DatetimeTimeMixin: 15 | @staticmethod 16 | def string_to_datetime(dt_str: Optional[str]) -> Optional[datetime.datetime]: 17 | """ 18 | Convert datetime string to datetime instance. 19 | original string format is YYYY-MM-DDThh:mm:ss.sZ. 20 | :return: 21 | """ 22 | if not dt_str: 23 | return None 24 | try: 25 | r = isodate.parse_datetime(dt_str) 26 | except ISO8601Error as e: 27 | raise PyYouTubeException( 28 | ErrorMessage(status_code=ErrorCode.INVALID_PARAMS, message=e.args[0]) 29 | ) 30 | else: 31 | return r 32 | -------------------------------------------------------------------------------- /pyyoutube/models/search_result.py: -------------------------------------------------------------------------------- 1 | """ 2 | These are search result related models. 3 | """ 4 | 5 | from dataclasses import dataclass, field 6 | from typing import Optional, List 7 | 8 | from .base import BaseModel 9 | from .common import BaseApiResponse, BaseResource, Thumbnails 10 | from .mixins import DatetimeTimeMixin 11 | 12 | 13 | @dataclass 14 | class SearchResultSnippet(BaseModel, DatetimeTimeMixin): 15 | """ 16 | A class representing the search result snippet info. 17 | 18 | Refer: https://developers.google.com/youtube/v3/docs/search#snippet 19 | """ 20 | 21 | publishedAt: Optional[str] = field(default=None, repr=False) 22 | channelId: Optional[str] = field(default=None) 23 | title: Optional[str] = field(default=None) 24 | description: Optional[str] = field(default=None, repr=False) 25 | thumbnails: Optional[Thumbnails] = field(default=None, repr=False) 26 | channelTitle: Optional[str] = field(default=None, repr=False) 27 | liveBroadcastContent: Optional[str] = field(default=None, repr=False) 28 | 29 | 30 | @dataclass 31 | class SearchResultId(BaseModel): 32 | """ 33 | A class representing the search result id info. 34 | 35 | Refer: https://developers.google.com/youtube/v3/docs/search#id 36 | """ 37 | 38 | kind: Optional[str] = field(default=None) 39 | videoId: Optional[str] = field(default=None, repr=False) 40 | channelId: Optional[str] = field(default=None, repr=False) 41 | playlistId: Optional[str] = field(default=None, repr=False) 42 | 43 | 44 | @dataclass 45 | class SearchResult(BaseResource): 46 | """ 47 | A class representing the search result's info. 48 | 49 | Refer: https://developers.google.com/youtube/v3/docs/search 50 | """ 51 | 52 | id: Optional[SearchResultId] = field(default=None, repr=False) 53 | snippet: Optional[SearchResultSnippet] = field(default=None, repr=False) 54 | 55 | 56 | @dataclass 57 | class SearchListResponse(BaseApiResponse): 58 | """ 59 | A class representing the channel's retrieve response info. 60 | 61 | Refer: https://developers.google.com/youtube/v3/docs/channels/list#response_1 62 | """ 63 | 64 | regionCode: Optional[str] = field(default=None, repr=False) 65 | items: Optional[List[SearchResult]] = field(default=None, repr=False) 66 | -------------------------------------------------------------------------------- /pyyoutube/models/video_abuse_report_reason.py: -------------------------------------------------------------------------------- 1 | """ 2 | These are video abuse report reason related models. 3 | """ 4 | 5 | from dataclasses import dataclass, field 6 | from typing import Optional, List 7 | 8 | from .base import BaseModel 9 | from .common import BaseResource, BaseApiResponse 10 | 11 | 12 | @dataclass 13 | class SecondaryReason(BaseModel): 14 | """ 15 | A class representing the video abuse report reason info 16 | 17 | Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons#snippet.secondaryReasons 18 | """ 19 | 20 | id: Optional[str] = field(default=None) 21 | label: Optional[str] = field(default=None, repr=True) 22 | 23 | 24 | @dataclass 25 | class VideoAbuseReportReasonSnippet(BaseModel): 26 | """ 27 | A class representing the video abuse report snippet info 28 | 29 | Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons#snippet 30 | """ 31 | 32 | label: Optional[str] = field(default=None) 33 | secondaryReasons: Optional[List[SecondaryReason]] = field(default=None, repr=True) 34 | 35 | 36 | @dataclass 37 | class VideoAbuseReportReason(BaseResource): 38 | """ 39 | A class representing the video abuse report info 40 | 41 | Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons 42 | """ 43 | 44 | snippet: Optional[VideoAbuseReportReasonSnippet] = field(default=None) 45 | 46 | 47 | @dataclass 48 | class VideoAbuseReportReasonListResponse(BaseApiResponse): 49 | """ 50 | A class representing the I18n language list response info. 51 | 52 | Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons/list#response_1 53 | """ 54 | 55 | items: Optional[List[VideoAbuseReportReason]] = field(default=None, repr=False) 56 | -------------------------------------------------------------------------------- /pyyoutube/models/watermark.py: -------------------------------------------------------------------------------- 1 | """ 2 | These are watermark related models. 3 | """ 4 | 5 | from dataclasses import dataclass, field 6 | from typing import Optional 7 | 8 | from .base import BaseModel 9 | 10 | 11 | @dataclass 12 | class WatermarkTiming(BaseModel): 13 | type: Optional[str] = field(default=None) 14 | offsetMs: Optional[int] = field(default=None, repr=False) 15 | durationMs: Optional[int] = field(default=None, repr=False) 16 | 17 | 18 | @dataclass 19 | class WatermarkPosition(BaseModel): 20 | type: Optional[str] = field(default=None) 21 | cornerPosition: Optional[str] = field(default=None, repr=False) 22 | 23 | 24 | @dataclass 25 | class Watermark(BaseModel): 26 | """ 27 | A class representing the watermark info. 28 | 29 | References: https://developers.google.com/youtube/v3/docs/watermarks#resource-representation 30 | """ 31 | 32 | timing: Optional[WatermarkTiming] = field(default=None, repr=False) 33 | position: Optional[WatermarkPosition] = field(default=None, repr=False) 34 | imageUrl: Optional[str] = field(default=None) 35 | imageBytes: Optional[bytes] = field(default=None, repr=False) 36 | targetChannelId: Optional[str] = field(default=None, repr=False) 37 | -------------------------------------------------------------------------------- /pyyoutube/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from .activities import ActivitiesResource # noqa 2 | from .captions import CaptionsResource # noqa 3 | from .channel_banners import ChannelBannersResource # noqa 4 | from .channels import ChannelsResource # noqa 5 | from .channel_sections import ChannelSectionsResource # noqa 6 | from .comments import CommentsResource # noqa 7 | from .comment_threads import CommentThreadsResource # noqa 8 | from .i18n_languages import I18nLanguagesResource # noqa 9 | from .i18n_regions import I18nRegionsResource # noqa 10 | from .members import MembersResource # noqa 11 | from .membership_levels import MembershipLevelsResource # noqa 12 | from .playlist_items import PlaylistItemsResource # noqa 13 | from .playlists import PlaylistsResource # noqa 14 | from .search import SearchResource # noqa 15 | from .subscriptions import SubscriptionsResource # noqa 16 | from .thumbnails import ThumbnailsResource # noqa 17 | from .video_abuse_report_reasons import VideoAbuseReportReasonsResource # noqa 18 | from .video_categories import VideoCategoriesResource # noqa 19 | from .videos import VideosResource # noqa 20 | from .watermarks import WatermarksResource # noqa 21 | -------------------------------------------------------------------------------- /pyyoutube/resources/base_resource.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base resource class. 3 | """ 4 | 5 | from typing import Optional, TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | from pyyoutube import Client # pragma: no cover 9 | 10 | 11 | class Resource: 12 | """Resource base class""" 13 | 14 | def __init__(self, client: Optional["Client"] = None): 15 | self._client = client 16 | 17 | @property 18 | def access_token(self): 19 | return self._client.access_token 20 | 21 | @property 22 | def api_key(self): 23 | return self._client.api_key 24 | -------------------------------------------------------------------------------- /pyyoutube/resources/channel_banners.py: -------------------------------------------------------------------------------- 1 | """ 2 | Channel banners resource implementation. 3 | """ 4 | 5 | from typing import Optional 6 | 7 | from pyyoutube.resources.base_resource import Resource 8 | from pyyoutube.media import Media, MediaUpload 9 | 10 | 11 | class ChannelBannersResource(Resource): 12 | """A channelBanner resource contains the URL that you would use to set a newly uploaded image as 13 | the banner image for a channel. 14 | 15 | References: https://developers.google.com/youtube/v3/docs/channelBanners 16 | """ 17 | 18 | def insert( 19 | self, 20 | media: Media, 21 | on_behalf_of_content_owner: Optional[str] = None, 22 | **kwargs: Optional[dict], 23 | ) -> MediaUpload: 24 | """Uploads a channel banner image to YouTube. 25 | 26 | Args: 27 | media: 28 | Banner media data. 29 | on_behalf_of_content_owner: 30 | The onBehalfOfContentOwner parameter indicates that the request's authorization 31 | credentials identify a YouTube CMS user who is acting on behalf of the content 32 | owner specified in the parameter value. This parameter is intended for YouTube 33 | content partners that own and manage many different YouTube channels. It allows 34 | content owners to authenticate once and get access to all their video and channel 35 | data, without having to provide authentication credentials for each individual channel. 36 | The CMS account that the user authenticates with must be linked to the specified YouTube content owner. 37 | **kwargs: 38 | Additional parameters for system parameters. 39 | Refer: https://cloud.google.com/apis/docs/system-parameters. 40 | 41 | Returns: 42 | Channel banner data. 43 | """ 44 | params = {"onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs} 45 | # Build a media upload instance. 46 | media_upload = MediaUpload( 47 | client=self._client, 48 | resource="channelBanners/insert", 49 | media=media, 50 | params=params, 51 | ) 52 | return media_upload 53 | -------------------------------------------------------------------------------- /pyyoutube/resources/i18n_languages.py: -------------------------------------------------------------------------------- 1 | """ 2 | i18n language resource implementation. 3 | """ 4 | 5 | from typing import Optional, Union 6 | 7 | from pyyoutube.resources.base_resource import Resource 8 | from pyyoutube.models import I18nLanguageListResponse 9 | from pyyoutube.utils.params_checker import enf_parts 10 | 11 | 12 | class I18nLanguagesResource(Resource): 13 | """An i18nLanguage resource identifies an application language that the YouTube website supports. 14 | The application language can also be referred to as a UI language 15 | 16 | References: https://developers.google.com/youtube/v3/docs/i18nLanguages 17 | """ 18 | 19 | def list( 20 | self, 21 | parts: Optional[Union[str, list, tuple, set]] = None, 22 | hl: Optional[str] = None, 23 | return_json: bool = False, 24 | **kwargs: Optional[dict], 25 | ) -> Union[dict, I18nLanguageListResponse]: 26 | """Returns a list of application languages that the YouTube website supports. 27 | 28 | Args: 29 | parts: 30 | Comma-separated list of one or more i18n languages resource properties. 31 | Accepted values: snippet. 32 | hl: 33 | Specifies the language that should be used for text values in the API response. 34 | The default value is en_US. 35 | return_json: 36 | Type for returned data. If you set True JSON data will be returned. 37 | **kwargs: 38 | Additional parameters for system parameters. 39 | Refer: https://cloud.google.com/apis/docs/system-parameters. 40 | 41 | Returns: 42 | i18n language data 43 | """ 44 | params = { 45 | "part": enf_parts(resource="i18nLanguages", value=parts), 46 | "hl": hl, 47 | **kwargs, 48 | } 49 | response = self._client.request(path="i18nLanguages", params=params) 50 | data = self._client.parse_response(response=response) 51 | return data if return_json else I18nLanguageListResponse.from_dict(data) 52 | -------------------------------------------------------------------------------- /pyyoutube/resources/i18n_regions.py: -------------------------------------------------------------------------------- 1 | """ 2 | i18n regions resource implementation. 3 | """ 4 | 5 | from typing import Optional, Union 6 | 7 | from pyyoutube.resources.base_resource import Resource 8 | from pyyoutube.models import I18nRegionListResponse 9 | from pyyoutube.utils.params_checker import enf_parts 10 | 11 | 12 | class I18nRegionsResource(Resource): 13 | """An i18nRegion resource identifies a geographic area that a YouTube user can select as 14 | the preferred content region. 15 | 16 | References: https://developers.google.com/youtube/v3/docs/i18nRegions 17 | """ 18 | 19 | def list( 20 | self, 21 | parts: Optional[Union[str, list, tuple, set]] = None, 22 | hl: Optional[str] = None, 23 | return_json: bool = False, 24 | **kwargs: Optional[dict], 25 | ) -> Union[dict, I18nRegionListResponse]: 26 | """Returns a list of content regions that the YouTube website supports. 27 | 28 | Args: 29 | parts: 30 | Comma-separated list of one or more i18n regions resource properties. 31 | Accepted values: snippet. 32 | hl: 33 | Specifies the language that should be used for text values in the API response. 34 | The default value is en_US. 35 | return_json: 36 | Type for returned data. If you set True JSON data will be returned. 37 | **kwargs: 38 | Additional parameters for system parameters. 39 | Refer: https://cloud.google.com/apis/docs/system-parameters. 40 | 41 | Returns: 42 | i18n regions data. 43 | """ 44 | params = { 45 | "part": enf_parts(resource="i18nRegions", value=parts), 46 | "hl": hl, 47 | **kwargs, 48 | } 49 | response = self._client.request(path="i18nRegions", params=params) 50 | data = self._client.parse_response(response=response) 51 | return data if return_json else I18nRegionListResponse.from_dict(data) 52 | -------------------------------------------------------------------------------- /pyyoutube/resources/membership_levels.py: -------------------------------------------------------------------------------- 1 | """ 2 | Membership levels resource implementation. 3 | """ 4 | 5 | from typing import Optional, Union 6 | 7 | from pyyoutube.models import MembershipsLevelListResponse 8 | from pyyoutube.resources.base_resource import Resource 9 | from pyyoutube.utils.params_checker import enf_parts 10 | 11 | 12 | class MembershipLevelsResource(Resource): 13 | """A membershipsLevel resource identifies a pricing level managed by the creator that authorized the API request. 14 | 15 | References: https://developers.google.com/youtube/v3/docs/membershipsLevels 16 | """ 17 | 18 | def list( 19 | self, 20 | parts: Optional[Union[str, list, tuple, set]] = None, 21 | return_json: bool = False, 22 | **kwargs: Optional[dict], 23 | ) -> Union[dict, MembershipsLevelListResponse]: 24 | """Lists membership levels for the channel that authorized the request. 25 | 26 | Args: 27 | parts: 28 | Comma-separated list of one or more channel resource properties. 29 | Accepted values: id,snippet 30 | return_json: 31 | Type for returned data. If you set True JSON data will be returned. 32 | **kwargs: 33 | Additional parameters for system parameters. 34 | Refer: https://cloud.google.com/apis/docs/system-parameters. 35 | 36 | Returns: 37 | Membership levels data. 38 | 39 | """ 40 | params = { 41 | "part": enf_parts(resource="membershipsLevels", value=parts), 42 | **kwargs, 43 | } 44 | response = self._client.request(path="membershipsLevels", params=params) 45 | data = self._client.parse_response(response=response) 46 | return data if return_json else MembershipsLevelListResponse.from_dict(data) 47 | -------------------------------------------------------------------------------- /pyyoutube/resources/thumbnails.py: -------------------------------------------------------------------------------- 1 | """ 2 | Thumbnails resources implementation. 3 | """ 4 | 5 | from typing import Optional 6 | 7 | from pyyoutube.resources.base_resource import Resource 8 | from pyyoutube.media import Media, MediaUpload 9 | 10 | 11 | class ThumbnailsResource(Resource): 12 | """A thumbnail resource identifies different thumbnail image sizes associated with a resource. 13 | 14 | References: https://developers.google.com/youtube/v3/docs/thumbnails 15 | """ 16 | 17 | def set( 18 | self, 19 | video_id: str, 20 | media: Media, 21 | on_behalf_of_content_owner: Optional[str] = None, 22 | **kwargs: Optional[dict], 23 | ) -> MediaUpload: 24 | params = { 25 | "videoId": video_id, 26 | "onBehalfOfContentOwner": on_behalf_of_content_owner, 27 | **kwargs, 28 | } 29 | # Build a media upload instance. 30 | media_upload = MediaUpload( 31 | client=self._client, 32 | resource="thumbnails/set", 33 | media=media, 34 | params=params, 35 | ) 36 | return media_upload 37 | -------------------------------------------------------------------------------- /pyyoutube/resources/video_abuse_report_reasons.py: -------------------------------------------------------------------------------- 1 | """ 2 | Video abuse report reasons resource implementation. 3 | """ 4 | 5 | from typing import Optional, Union 6 | 7 | from pyyoutube.resources.base_resource import Resource 8 | from pyyoutube.models import VideoAbuseReportReasonListResponse 9 | from pyyoutube.utils.params_checker import enf_parts 10 | 11 | 12 | class VideoAbuseReportReasonsResource(Resource): 13 | """A videoAbuseReportReason resource contains information about a reason that a video would be flagged 14 | for containing abusive content. 15 | 16 | References: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons 17 | """ 18 | 19 | def list( 20 | self, 21 | parts: Optional[Union[str, list, tuple, set]] = None, 22 | hl: Optional[str] = None, 23 | return_json: bool = False, 24 | **kwargs: Optional[dict], 25 | ) -> Union[dict, VideoAbuseReportReasonListResponse]: 26 | """Retrieve a list of reasons that can be used to report abusive videos. 27 | 28 | Args: 29 | parts: 30 | Comma-separated list of one or more channel resource properties. 31 | Accepted values: id,snippet 32 | hl: 33 | Specifies the language that should be used for text values in the API response. 34 | The default value is en_US. 35 | return_json: 36 | Type for returned data. If you set True JSON data will be returned. 37 | **kwargs: 38 | Additional parameters for system parameters. 39 | Refer: https://cloud.google.com/apis/docs/system-parameters. 40 | 41 | Returns: 42 | reasons data. 43 | """ 44 | params = { 45 | "part": enf_parts(resource="videoAbuseReportReasons", value=parts), 46 | "hl": hl, 47 | **kwargs, 48 | } 49 | response = self._client.request(path="videoAbuseReportReasons", params=params) 50 | data = self._client.parse_response(response=response) 51 | return ( 52 | data if return_json else VideoAbuseReportReasonListResponse.from_dict(data) 53 | ) 54 | -------------------------------------------------------------------------------- /pyyoutube/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/pyyoutube/utils/__init__.py -------------------------------------------------------------------------------- /pyyoutube/youtube_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | This provide some common utils methods for YouTube resource. 3 | """ 4 | 5 | import isodate 6 | from isodate.isoerror import ISO8601Error 7 | 8 | from pyyoutube.error import ErrorMessage, PyYouTubeException 9 | 10 | 11 | def get_video_duration(duration: str) -> int: 12 | """ 13 | Parse video ISO 8601 duration to seconds. 14 | Refer: https://developers.google.com/youtube/v3/docs/videos#contentDetails.duration 15 | 16 | Args: 17 | duration(str) 18 | Videos ISO 8601 duration. Like: PT14H23M42S 19 | Returns: 20 | integer for seconds. 21 | """ 22 | try: 23 | seconds = isodate.parse_duration(duration).total_seconds() 24 | return int(seconds) 25 | except ISO8601Error as e: 26 | raise PyYouTubeException( 27 | ErrorMessage( 28 | status_code=10001, 29 | message=f"Exception in convert video duration: {duration}. errors: {e}", 30 | ) 31 | ) 32 | -------------------------------------------------------------------------------- /testdata/apidata/access_token.json: -------------------------------------------------------------------------------- 1 | {"access_token":"access_token","expires_in":3599,"refresh_token":"refresh_token","scope":["https://www.googleapis.com/auth/youtube","https://www.googleapis.com/auth/userinfo.profile"],"token_type":"Bearer","expires_at":1640180492.4104881} -------------------------------------------------------------------------------- /testdata/apidata/activities/activities_by_mine_p1.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#activityListResponse", 3 | "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/le7hKns0ey3G45tVxUz8WPZskcQ\"", 4 | "nextPageToken": "CAEQAA", 5 | "pageInfo": { 6 | "totalResults": 2, 7 | "resultsPerPage": 1 8 | }, 9 | "items": [ 10 | { 11 | "kind": "youtube#activity", 12 | "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZVGvURQNCh-0EGNyS2UTdedzrhM\"", 13 | "id": "MTUxNTc0OTk2MjI3NzE4NDU5NjEyODA4MA==", 14 | "snippet": { 15 | "publishedAt": "2019-11-29T02:57:07.000Z", 16 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 17 | "title": "华山日出", 18 | "description": "冷冷的山头", 19 | "thumbnails": { 20 | "default": { 21 | "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/default.jpg", 22 | "width": 120, 23 | "height": 90 24 | }, 25 | "medium": { 26 | "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/mqdefault.jpg", 27 | "width": 320, 28 | "height": 180 29 | }, 30 | "high": { 31 | "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/hqdefault.jpg", 32 | "width": 480, 33 | "height": 360 34 | }, 35 | "standard": { 36 | "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/sddefault.jpg", 37 | "width": 640, 38 | "height": 480 39 | }, 40 | "maxres": { 41 | "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/maxresdefault.jpg", 42 | "width": 1280, 43 | "height": 720 44 | } 45 | }, 46 | "channelTitle": "ikaros-life", 47 | "type": "upload" 48 | } 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /testdata/apidata/activities/activities_by_mine_p2.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#activityListResponse", 3 | "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/6yncT242auTddLxSe4dfDTC-4xE\"", 4 | "prevPageToken": "CAEQAQ", 5 | "pageInfo": { 6 | "totalResults": 2, 7 | "resultsPerPage": 1 8 | }, 9 | "items": [ 10 | { 11 | "kind": "youtube#activity", 12 | "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZshL2QThX_bnTHhpJy5emGCWHcE\"", 13 | "id": "MTUxNTc0OTk1OTAyNDkwNjA3MjU5NzQ1Ng==", 14 | "snippet": { 15 | "publishedAt": "2019-11-29T02:51:42.000Z", 16 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 17 | "title": "海上日出", 18 | "description": "美美美", 19 | "thumbnails": { 20 | "default": { 21 | "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/default.jpg", 22 | "width": 120, 23 | "height": 90 24 | }, 25 | "medium": { 26 | "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/mqdefault.jpg", 27 | "width": 320, 28 | "height": 180 29 | }, 30 | "high": { 31 | "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/hqdefault.jpg", 32 | "width": 480, 33 | "height": 360 34 | }, 35 | "standard": { 36 | "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/sddefault.jpg", 37 | "width": 640, 38 | "height": 480 39 | }, 40 | "maxres": { 41 | "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/maxresdefault.jpg", 42 | "width": 1280, 43 | "height": 720 44 | } 45 | }, 46 | "channelTitle": "ikaros-life", 47 | "type": "upload" 48 | } 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /testdata/apidata/captions/captions_by_video.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#captionListResponse", 3 | "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/bB4ewYNN7bQHonV-K7efrgBqh8M\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#caption", 7 | "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\"", 8 | "id": "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I", 9 | "snippet": { 10 | "videoId": "oHR3wURdJ94", 11 | "lastUpdated": "2020-01-14T09:40:49.981Z", 12 | "trackKind": "standard", 13 | "language": "en", 14 | "name": "", 15 | "audioTrackType": "unknown", 16 | "isCC": false, 17 | "isLarge": false, 18 | "isEasyReader": false, 19 | "isDraft": false, 20 | "isAutoSynced": false, 21 | "status": "serving" 22 | } 23 | }, 24 | { 25 | "kind": "youtube#caption", 26 | "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/iRxIplZcCiX0oujr5gSVMXkij8M\"", 27 | "id": "fPMuDm722CIRcUAT3NTPQHQZJZJxt39kU7JvrHk8Kzs=", 28 | "snippet": { 29 | "videoId": "oHR3wURdJ94", 30 | "lastUpdated": "2020-01-14T09:39:46.991Z", 31 | "trackKind": "standard", 32 | "language": "zh-Hans", 33 | "name": "", 34 | "audioTrackType": "unknown", 35 | "isCC": false, 36 | "isLarge": false, 37 | "isEasyReader": false, 38 | "isDraft": false, 39 | "isAutoSynced": false, 40 | "status": "serving" 41 | } 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /testdata/apidata/captions/captions_filter_by_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#captionListResponse", 3 | "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/4OU1z5mciyh4emins-W6FGneNdM\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#caption", 7 | "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\"", 8 | "id": "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I", 9 | "snippet": { 10 | "videoId": "oHR3wURdJ94", 11 | "lastUpdated": "2020-01-14T09:40:49.981Z", 12 | "trackKind": "standard", 13 | "language": "en", 14 | "name": "", 15 | "audioTrackType": "unknown", 16 | "isCC": false, 17 | "isLarge": false, 18 | "isEasyReader": false, 19 | "isDraft": false, 20 | "isAutoSynced": false, 21 | "status": "serving" 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /testdata/apidata/captions/insert_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#caption", 3 | "etag": "R7KYT4aJbHp2wxlTmtFuKJ4pmF8", 4 | "id": "AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA", 5 | "snippet": { 6 | "videoId": "zxTVeyG1600", 7 | "lastUpdated": "2022-12-13T08:20:45.636548Z", 8 | "trackKind": "standard", 9 | "language": "ja", 10 | "name": "\\u65e5\\u6587\\u5b57\\u5e55", 11 | "audioTrackType": "unknown", 12 | "isCC": false, 13 | "isLarge": false, 14 | "isEasyReader": false, 15 | "isDraft": true, 16 | "isAutoSynced": false, 17 | "status": "serving" 18 | } 19 | } -------------------------------------------------------------------------------- /testdata/apidata/captions/update_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#caption", 3 | "etag": "R7KYT4aJbHp2wxlTmtFuKJ4pmF8", 4 | "id": "AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA", 5 | "snippet": { 6 | "videoId": "zxTVeyG1600", 7 | "lastUpdated": "2022-12-13T08:20:45.636548Z", 8 | "trackKind": "standard", 9 | "language": "ja", 10 | "name": "\\u65e5\\u6587\\u5b57\\u5e55", 11 | "audioTrackType": "unknown", 12 | "isCC": false, 13 | "isLarge": false, 14 | "isEasyReader": false, 15 | "isDraft": false, 16 | "isAutoSynced": false, 17 | "status": "serving" 18 | } 19 | } -------------------------------------------------------------------------------- /testdata/apidata/categories/guide_category_multi.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#guideCategoryListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/JvKaPPmX316HuCUpJddmxaDPomo\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#guideCategory", 7 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\"", 8 | "id": "GCQmVzdCBvZiBZb3VUdWJl", 9 | "snippet": { 10 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 11 | "title": "Best of YouTube" 12 | } 13 | }, 14 | { 15 | "kind": "youtube#guideCategory", 16 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ImrTevQ0UvryyOmvjPFFu85AKCU\"", 17 | "id": "GCQ3JlYXRvciBvbiB0aGUgUmlzZQ", 18 | "snippet": { 19 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 20 | "title": "Creator on the Rise" 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /testdata/apidata/categories/guide_category_single.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#guideCategoryListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/KIJAFi2jsRHVBmAk3XYhyRKynjw\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#guideCategory", 7 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\"", 8 | "id": "GCQmVzdCBvZiBZb3VUdWJl", 9 | "snippet": { 10 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 11 | "title": "Best of YouTube" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /testdata/apidata/categories/video_category_multi.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#videoCategoryListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/QhsRsql8vvkcmFdomppeHDbsV0Q\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#videoCategory", 7 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", 8 | "id": "17", 9 | "snippet": { 10 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 11 | "title": "Sports", 12 | "assignable": true 13 | } 14 | }, 15 | { 16 | "kind": "youtube#videoCategory", 17 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/FJwVpGCVZ1yiJrqZbpqe68Sy_OE\"", 18 | "id": "18", 19 | "snippet": { 20 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 21 | "title": "Short Movies", 22 | "assignable": false 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /testdata/apidata/categories/video_category_single.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#videoCategoryListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/0_wT9Ta0iZu7ETYC3E6Xi_B4mtA\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#videoCategory", 7 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", 8 | "id": "17", 9 | "snippet": { 10 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 11 | "title": "Sports", 12 | "assignable": true 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /testdata/apidata/channel_banners/insert_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channelBannerResource", 3 | "etag": "ezPZq6gkoCbM-5C4P-ved0Irol0", 4 | "url": "https://yt3.googleusercontent.com/1mrHHBsTG4JhGAQg_dmFf3ByELNVnXu7qCvmuhC81TFemB8XpaDgYuMgh5w220bh4APAj-xDeA" 5 | } -------------------------------------------------------------------------------- /testdata/apidata/channel_info_single.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channelListResponse", 3 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/0lqbdkIcLGXAPiLsJ3FTHo96TDg\"", 4 | "pageInfo": { 5 | "totalResults": 1, 6 | "resultsPerPage": 1 7 | }, 8 | "items": [ 9 | { 10 | "kind": "youtube#channel", 11 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/HUbWoTqNN1LPZKmbyCzPgvjVuR4\"", 12 | "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 13 | "snippet": { 14 | "title": "Google Developers", 15 | "description": "The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.", 16 | "customUrl": "@googledevelopers", 17 | "publishedAt": "2007-08-23T00:34:43.000Z", 18 | "thumbnails": { 19 | "default": { 20 | "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo", 21 | "width": 88, 22 | "height": 88 23 | }, 24 | "medium": { 25 | "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo", 26 | "width": 240, 27 | "height": 240 28 | }, 29 | "high": { 30 | "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo", 31 | "width": 800, 32 | "height": 800 33 | } 34 | }, 35 | "localized": { 36 | "title": "Google Developers", 37 | "description": "The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms." 38 | }, 39 | "country": "US" 40 | }, 41 | "statistics": { 42 | "viewCount": "160361638", 43 | "commentCount": "0", 44 | "subscriberCount": "1927873", 45 | "hiddenSubscriberCount": false, 46 | "videoCount": "5026" 47 | } 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /testdata/apidata/channel_sections/channel_sections_by_channel.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channelSectionListResponse", 3 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/IG4AAhdP913_ibNr3xxa2XjZhAU\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#channelSection", 7 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/JNSONRhMV8b1OaalB42ZUtVBZ44\"", 8 | "id": "UCa-vrCLQHviTOVnEKDOdetQ.jNQXAC9IVRw", 9 | "snippet": { 10 | "type": "recentUploads", 11 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 12 | "position": 0 13 | } 14 | }, 15 | { 16 | "kind": "youtube#channelSection", 17 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/bcTK2_pxKS22pZizMGDGgnCcdeQ\"", 18 | "id": "UCa-vrCLQHviTOVnEKDOdetQ.LeAltgu_pbM", 19 | "snippet": { 20 | "type": "allPlaylists", 21 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 22 | "position": 1 23 | } 24 | }, 25 | { 26 | "kind": "youtube#channelSection", 27 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/lkkZaRpGqH1OLyeS4UMzEQkz5IU\"", 28 | "id": "UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY", 29 | "snippet": { 30 | "type": "multiplePlaylists", 31 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 32 | "title": "我的操作诶", 33 | "position": 2 34 | }, 35 | "contentDetails": { 36 | "playlists": [ 37 | "PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS", 38 | "PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g" 39 | ] 40 | } 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /testdata/apidata/channel_sections/channel_sections_by_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channelSectionListResponse", 3 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Oysqp4SfBtVFI8-0LVzUEHn8LN4\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#channelSection", 7 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Z3z8l_2oWLi9cWlfGTNMxsVwOTw\"", 8 | "id": "UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY", 9 | "snippet": { 10 | "type": "multiplePlaylists", 11 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 12 | "title": "我的操作诶", 13 | "position": 2 14 | }, 15 | "contentDetails": { 16 | "playlists": [ 17 | "PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS", 18 | "PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g" 19 | ] 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /testdata/apidata/channel_sections/channel_sections_by_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channelSectionListResponse", 3 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Nvmls-WhS6tunMyp9v6ZIEFrgRI\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#channelSection", 7 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/RSxEQQPXGQo3MTN75toyRTUTEmY\"", 8 | "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.npYvuMz0_es", 9 | "snippet": { 10 | "type": "recentUploads", 11 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 12 | "position": 9 13 | } 14 | }, 15 | { 16 | "kind": "youtube#channelSection", 17 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/zaHbYWO-Q1zjW4IYjza-bTrqeIc\"", 18 | "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.9_wU0qhEPR8", 19 | "snippet": { 20 | "type": "singlePlaylist", 21 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 22 | "position": 8 23 | }, 24 | "contentDetails": { 25 | "playlists": [ 26 | "PLOU2XLYxmsIKKMtrYD-IfPdlVunyPl9GM" 27 | ] 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /testdata/apidata/channel_sections/insert_resp.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channelSection", 3 | "etag": "VNVb0NhdJ8VHoZaVCqGVqfaRrVU", 4 | "id": "UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM", 5 | "snippet": { 6 | "type": "multipleplaylists", 7 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 8 | "position": 4 9 | }, 10 | "contentDetails": { 11 | "playlists": [ 12 | "PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /testdata/apidata/channels/info.json: -------------------------------------------------------------------------------- 1 | {"kind":"youtube#channelListResponse","etag":"DovVRc4nTNzGShQkXoC7R2ab3JQ","pageInfo":{"totalResults":1,"resultsPerPage":5},"items":[{"kind":"youtube#channel","etag":"Cxi25U626ZmPs7h8MsS4D8GzfV8","id":"UC_x5XG1OV2P6uZZ5FSM9Ttw","snippet":{"title":"Google Developers","description":"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\n\nSubscribe to Google Developers → https://goo.gle/developers\n","customUrl":"@googledevelopers","publishedAt":"2007-08-23T00:34:43Z","thumbnails":{"default":{"url":"https://yt3.ggpht.com/ytc/AMLnZu-oDvWEJ-WfN9bgxQB2YAlnjC2uqN_c7JQZvX9Ikfg=s88-c-k-c0x00ffffff-no-rj","width":88,"height":88},"medium":{"url":"https://yt3.ggpht.com/ytc/AMLnZu-oDvWEJ-WfN9bgxQB2YAlnjC2uqN_c7JQZvX9Ikfg=s240-c-k-c0x00ffffff-no-rj","width":240,"height":240},"high":{"url":"https://yt3.ggpht.com/ytc/AMLnZu-oDvWEJ-WfN9bgxQB2YAlnjC2uqN_c7JQZvX9Ikfg=s800-c-k-c0x00ffffff-no-rj","width":800,"height":800}},"localized":{"title":"Google Developers","description":"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\n\nSubscribe to Google Developers → https://goo.gle/developers\n"},"country":"US"}}]} -------------------------------------------------------------------------------- /testdata/apidata/channels/update_resp.json: -------------------------------------------------------------------------------- 1 | {"kind":"youtube#channel","etag":"qlk0Tup07Hsl_Dz8nMefxFRUiEU","id":"UCa-vrCLQHviTOVnEKDOdetQ","brandingSettings":{"channel":{"title":"ikaros data","description":"This is a test channel.","keywords":"life 学习 测试","defaultLanguage":"en","country":"CN"},"image":{"bannerExternalUrl":"https://yt3.ggpht.com/t_A-_WuHfqjHqNp8Zbi1Xwed864ix3fD7zWGpkC3huniGjSHe4GEDFPg-dmc0LGpWvrtQZgPBg"}}} -------------------------------------------------------------------------------- /testdata/apidata/client_secrets/client_secret_installed_bad.json: -------------------------------------------------------------------------------- 1 | { 2 | "installed": { 3 | "client_id": "client_id", 4 | "project_id": "project_id", 5 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 6 | "token_uri": "https://oauth2.googleapis.com/token", 7 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs" 8 | } 9 | } -------------------------------------------------------------------------------- /testdata/apidata/client_secrets/client_secret_installed_good.json: -------------------------------------------------------------------------------- 1 | { 2 | "installed": { 3 | "client_id": "client_id", 4 | "project_id": "project_id", 5 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 6 | "token_uri": "https://oauth2.googleapis.com/token", 7 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 8 | "client_secret": "client_secret" 9 | } 10 | } -------------------------------------------------------------------------------- /testdata/apidata/client_secrets/client_secret_unsupported.json: -------------------------------------------------------------------------------- 1 | { 2 | "unsupported": { 3 | "client_id": "client_id", 4 | "project_id": "project_id", 5 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 6 | "token_uri": "https://oauth2.googleapis.com/token", 7 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs" 8 | } 9 | } -------------------------------------------------------------------------------- /testdata/apidata/client_secrets/client_secret_web.json: -------------------------------------------------------------------------------- 1 | { 2 | "web": { 3 | "client_id": "client_id", 4 | "project_id": "project_id", 5 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 6 | "token_uri": "https://oauth2.googleapis.com/token", 7 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 8 | "client_secret": "client_secret", 9 | "redirect_uris": [ 10 | "http://localhost:5000/oauth2callback" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /testdata/apidata/comment_threads/comment_thread_single.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#commentThreadListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/VQfUfBFenzO3S8AzxaX0A2cOK_w\"", 4 | "pageInfo": { 5 | "totalResults": 1, 6 | "resultsPerPage": 20 7 | }, 8 | "items": [ 9 | { 10 | "kind": "youtube#commentThread", 11 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Bov8ITX91R0QmVGZN70wbJ5_hOs\"", 12 | "id": "UgxKREWxIgDrw8w2e_Z4AaABAg", 13 | "snippet": { 14 | "videoId": "D-lhorsDlUQ", 15 | "topLevelComment": { 16 | "kind": "youtube#comment", 17 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/W05nAf1QR8i2AYeULDR019ku3Lg\"", 18 | "id": "UgxKREWxIgDrw8w2e_Z4AaABAg", 19 | "snippet": { 20 | "authorDisplayName": "Hieu Nguyen", 21 | "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7-oQeqHcOEyt0l2rBBZH1qAiBNKNn1UmmGk5Q=s48-c-k-c0xffffffff-no-rj-mo", 22 | "authorChannelUrl": "http://www.youtube.com/channel/UClfzT4CU_yaZjJaI4pKqSjQ", 23 | "authorChannelId": { 24 | "value": "UClfzT4CU_yaZjJaI4pKqSjQ" 25 | }, 26 | "videoId": "D-lhorsDlUQ", 27 | "textDisplay": "Super video !!!\u003cbr /\u003eWith full power skil thank a lot ... \u003cbr /\u003eVery nice , coupe \u003cbr /\u003ecan i give Luke Davit and JESSICA EARl- CHA some tea and tall a more ...", 28 | "textOriginal": "Super video !!!\nWith full power skil thank a lot ... \nVery nice , coupe \ncan i give Luke Davit and JESSICA EARl- CHA some tea and tall a more ...", 29 | "canRate": true, 30 | "viewerRating": "none", 31 | "likeCount": 0, 32 | "publishedAt": "2019-04-20T01:03:39.000Z", 33 | "updatedAt": "2019-04-20T01:03:39.000Z" 34 | } 35 | }, 36 | "canReply": true, 37 | "totalReplyCount": 0, 38 | "isPublic": true 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /testdata/apidata/comment_threads/comment_threads_with_search.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#commentThreadListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Sb1s9ORYlw5LpXhXM1osg1jeYgM\"", 4 | "pageInfo": { 5 | "totalResults": 1, 6 | "resultsPerPage": 20 7 | }, 8 | "items": [ 9 | { 10 | "kind": "youtube#commentThread", 11 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/o0Du7va0vUQliAR76OFrtcgOjOc\"", 12 | "id": "UgyUBI0HsgL9emxcZpR4AaABAg", 13 | "snippet": { 14 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 15 | "topLevelComment": { 16 | "kind": "youtube#comment", 17 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/CD_Xk4X_gxANaxestqfTSanwWrk\"", 18 | "id": "UgyUBI0HsgL9emxcZpR4AaABAg", 19 | "snippet": { 20 | "authorDisplayName": "DevRagz", 21 | "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo", 22 | "authorChannelUrl": "http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw", 23 | "authorChannelId": { 24 | "value": "UCTWlvHQQXUs-4IVfdnGOUbw" 25 | }, 26 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 27 | "textDisplay": "Hello", 28 | "textOriginal": "Hello", 29 | "canRate": true, 30 | "viewerRating": "none", 31 | "likeCount": 0, 32 | "publishedAt": "2019-06-23T02:49:00.000Z", 33 | "updatedAt": "2019-06-23T02:49:00.000Z" 34 | } 35 | }, 36 | "canReply": true, 37 | "totalReplyCount": 0, 38 | "isPublic": true 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /testdata/apidata/comment_threads/insert_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#commentThread", 3 | "etag": "AMgl2io48I4z6Ulu9kv4C43sVvk", 4 | "id": "Ugx_5P8rmn4vKbN6wwt4AaABAg", 5 | "snippet": { 6 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 7 | "videoId": "JE8xdDp5B8Q", 8 | "topLevelComment": { 9 | "kind": "youtube#comment", 10 | "etag": "I_E2on6NOdGkpW0WodB74OVCU_E", 11 | "id": "Ugx_5P8rmn4vKbN6wwt4AaABAg", 12 | "snippet": { 13 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 14 | "videoId": "JE8xdDp5B8Q", 15 | "textDisplay": "Sun from the api", 16 | "textOriginal": "Sun from the api", 17 | "authorDisplayName": "ikaros data", 18 | "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s48-c-k-c0x00ffffff-no-rj", 19 | "authorChannelUrl": "http://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ", 20 | "authorChannelId": { 21 | "value": "UCa-vrCLQHviTOVnEKDOdetQ" 22 | }, 23 | "canRate": true, 24 | "viewerRating": "none", 25 | "likeCount": 0, 26 | "publishedAt": "2022-11-15T02:20:01Z", 27 | "updatedAt": "2022-11-15T02:20:01Z" 28 | } 29 | }, 30 | "canReply": true, 31 | "totalReplyCount": 0, 32 | "isPublic": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /testdata/apidata/comments/comments_by_parent_paged_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#commentListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/WG_t4MaQHNzJKOhVkfr0prByYcE\"", 4 | "nextPageToken": "R0FJeVZnbzBJTl9zNXRxNXlPWUNNaWtRQUJpQ3RNeW4wcFBtQWlBQktBTXdDam9XT1RGNlZETmpXV0kxUWpJNU1YcGhOV1ZLZUhwek1SSWVDQVVTR2xWbmR6VjZXVlUyYmpsd2JVbG5RVnBYZGs0MFFXRkJRa0ZuT2lBSUFSSWNOVHBWWjNjMWVsbFZObTQ1Y0cxSlowRmFWM1pPTkVGaFFVSkJadw==", 5 | "pageInfo": { 6 | "resultsPerPage": 2 7 | }, 8 | "items": [ 9 | { 10 | "kind": "youtube#comment", 11 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/MKybcC4eKVsCy4dNSdJpsCB2f9I\"", 12 | "id": "Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za6voUoRh", 13 | "snippet": { 14 | "authorDisplayName": "kun liu", 15 | "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo", 16 | "authorChannelUrl": "http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw", 17 | "authorChannelId": { 18 | "value": "UCNvMBmCASzTNNX8lW3JRMbw" 19 | }, 20 | "textDisplay": "this is the third reply!", 21 | "textOriginal": "this is the third reply!", 22 | "parentId": "Ugw5zYU6n9pmIgAZWvN4AaABAg", 23 | "canRate": true, 24 | "viewerRating": "none", 25 | "likeCount": 0, 26 | "publishedAt": "2019-12-01T04:46:31.000Z", 27 | "updatedAt": "2019-12-01T04:46:31.000Z" 28 | } 29 | }, 30 | { 31 | "kind": "youtube#comment", 32 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Hnby-TPwSCvcPfoqxAwocl44Ijw\"", 33 | "id": "Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za5eJxzs1", 34 | "snippet": { 35 | "authorDisplayName": "kun liu", 36 | "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo", 37 | "authorChannelUrl": "http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw", 38 | "authorChannelId": { 39 | "value": "UCNvMBmCASzTNNX8lW3JRMbw" 40 | }, 41 | "textDisplay": "this is the second reply!", 42 | "textOriginal": "this is the second reply!", 43 | "parentId": "Ugw5zYU6n9pmIgAZWvN4AaABAg", 44 | "canRate": true, 45 | "viewerRating": "none", 46 | "likeCount": 0, 47 | "publishedAt": "2019-12-01T04:46:20.000Z", 48 | "updatedAt": "2019-12-01T04:46:20.000Z" 49 | } 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /testdata/apidata/comments/comments_by_parent_paged_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#commentListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/pCAXgQBtRsyN8PLsTBsg6O6diuY\"", 4 | "pageInfo": { 5 | "resultsPerPage": 2 6 | }, 7 | "items": [ 8 | { 9 | "kind": "youtube#comment", 10 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/0CMWJ6jK5tTSWIBuH6KQLYVM9xI\"", 11 | "id": "Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za3E-eR-l", 12 | "snippet": { 13 | "authorDisplayName": "kun liu", 14 | "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo", 15 | "authorChannelUrl": "http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw", 16 | "authorChannelId": { 17 | "value": "UCNvMBmCASzTNNX8lW3JRMbw" 18 | }, 19 | "textDisplay": "hey, this is the replay", 20 | "textOriginal": "hey, this is the replay", 21 | "parentId": "Ugw5zYU6n9pmIgAZWvN4AaABAg", 22 | "canRate": true, 23 | "viewerRating": "none", 24 | "likeCount": 0, 25 | "publishedAt": "2019-12-01T04:46:00.000Z", 26 | "updatedAt": "2019-12-01T04:46:00.000Z" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /testdata/apidata/comments/comments_multi.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#commentListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/KGqYDVMiabGgL8yNEbUJOwtrsqY\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#comment", 7 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/N8dTotfuHXvcw1YZ3SZNWPHYxvM\"", 8 | "id": "UgyUBI0HsgL9emxcZpR4AaABAg", 9 | "snippet": { 10 | "authorDisplayName": "DevRagz", 11 | "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo", 12 | "authorChannelUrl": "http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw", 13 | "authorChannelId": { 14 | "value": "UCTWlvHQQXUs-4IVfdnGOUbw" 15 | }, 16 | "textDisplay": "Hello", 17 | "textOriginal": "Hello", 18 | "canRate": true, 19 | "viewerRating": "none", 20 | "likeCount": 0, 21 | "publishedAt": "2019-06-23T02:49:00.000Z", 22 | "updatedAt": "2019-06-23T02:49:00.000Z" 23 | } 24 | }, 25 | { 26 | "kind": "youtube#comment", 27 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/U1u_QcNKFwxVznA5x7CNZ9nXECc\"", 28 | "id": "Ugzi3lkqDPfIOirGFLh4AaABAg", 29 | "snippet": { 30 | "authorDisplayName": "EclipZe Muzik", 31 | "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7-JV7pt6X2WsxOdaD6nzK_rAj_2FVAjLyNR1Q=s48-c-k-c0xffffffff-no-rj-mo", 32 | "authorChannelUrl": "http://www.youtube.com/channel/UCCuQ3wts9ASkk0qZJyEjYbw", 33 | "authorChannelId": { 34 | "value": "UCCuQ3wts9ASkk0qZJyEjYbw" 35 | }, 36 | "textDisplay": "exceptional content!", 37 | "textOriginal": "exceptional content!", 38 | "canRate": true, 39 | "viewerRating": "none", 40 | "likeCount": 0, 41 | "publishedAt": "2018-05-26T20:11:30.000Z", 42 | "updatedAt": "2018-05-26T20:11:30.000Z" 43 | } 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /testdata/apidata/comments/comments_single.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#commentListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/A2nkVdtP_3bQZi4INK9lZ7XTYXs\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#comment", 7 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/N8dTotfuHXvcw1YZ3SZNWPHYxvM\"", 8 | "id": "UgyUBI0HsgL9emxcZpR4AaABAg", 9 | "snippet": { 10 | "authorDisplayName": "DevRagz", 11 | "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo", 12 | "authorChannelUrl": "http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw", 13 | "authorChannelId": { 14 | "value": "UCTWlvHQQXUs-4IVfdnGOUbw" 15 | }, 16 | "textDisplay": "Hello", 17 | "textOriginal": "Hello", 18 | "canRate": true, 19 | "viewerRating": "none", 20 | "likeCount": 0, 21 | "publishedAt": "2019-06-23T02:49:00.000Z", 22 | "updatedAt": "2019-06-23T02:49:00.000Z" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /testdata/apidata/comments/insert_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#comment", 3 | "etag": "lTl2Wjqipb6KqrmPU04DLigrzrg", 4 | "id": "Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt", 5 | "snippet": { 6 | "textDisplay": "wow", 7 | "textOriginal": "wow", 8 | "parentId": "Ugy_CAftKrIUCyPr9GR4AaABAg", 9 | "authorDisplayName": "ikaros data", 10 | "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s48-c-k-c0x00ffffff-no-rj", 11 | "authorChannelUrl": "http://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ", 12 | "authorChannelId": { 13 | "value": "UCa-vrCLQHviTOVnEKDOdetQ" 14 | }, 15 | "canRate": true, 16 | "viewerRating": "none", 17 | "likeCount": 0, 18 | "publishedAt": "2022-11-14T10:23:02Z", 19 | "updatedAt": "2022-11-14T10:23:02Z" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /testdata/apidata/error_permission_resp.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "code": 403, 4 | "message": "The caller does not have permission", 5 | "errors": [ 6 | { 7 | "message": "Permission denied.", 8 | "domain": "youtube.CoreErrorDomain", 9 | "reason": "SERVICE_UNAVAILABLE" 10 | } 11 | ], 12 | "status": "PERMISSION_DENIED" 13 | } 14 | } -------------------------------------------------------------------------------- /testdata/apidata/i18ns/language_res.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#i18nLanguageListResponse", 3 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/qgFy24yvs-L_dNjr2d-Rd_Xcfw4\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#i18nLanguage", 7 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/wD2SRT6G7gbFH07ePlumHAynSRo\"", 8 | "id": "zh-CN", 9 | "snippet": { 10 | "hl": "zh-CN", 11 | "name": "Chinese" 12 | } 13 | }, 14 | { 15 | "kind": "youtube#i18nLanguage", 16 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/6fRre896XUzNQjc89q329PaFKjE\"", 17 | "id": "zh-TW", 18 | "snippet": { 19 | "hl": "zh-TW", 20 | "name": "Chinese (Taiwan)" 21 | } 22 | }, 23 | { 24 | "kind": "youtube#i18nLanguage", 25 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/MbipoDEFiRRUlYr5UzjpCwXXRMc\"", 26 | "id": "zh-HK", 27 | "snippet": { 28 | "hl": "zh-HK", 29 | "name": "Chinese (Hong Kong)" 30 | } 31 | }, 32 | { 33 | "kind": "youtube#i18nLanguage", 34 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/XBmnabKsnR1WSWcZvNxC2NvrLYo\"", 35 | "id": "ja", 36 | "snippet": { 37 | "hl": "ja", 38 | "name": "Japanese" 39 | } 40 | }, 41 | { 42 | "kind": "youtube#i18nLanguage", 43 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/l06bH0pLscIVm87oyBnJ3aZR4Ts\"", 44 | "id": "ko", 45 | "snippet": { 46 | "hl": "ko", 47 | "name": "Korean" 48 | } 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /testdata/apidata/i18ns/regions_res.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#i18nRegionListResponse", 3 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/q85_wZeDyKDzYtt-LhNaozyi_sk\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#i18nRegion", 7 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/VLBm14P6cRurVqVIS2Z9SfyDJdU\"", 8 | "id": "VE", 9 | "snippet": { 10 | "gl": "VE", 11 | "name": "Venezuela" 12 | } 13 | }, 14 | { 15 | "kind": "youtube#i18nRegion", 16 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/PecEyDpgDPYWfvmuJVxuVgwxQwU\"", 17 | "id": "VN", 18 | "snippet": { 19 | "gl": "VN", 20 | "name": "Vietnam" 21 | } 22 | }, 23 | { 24 | "kind": "youtube#i18nRegion", 25 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Pzd0Bx5oG8rW9CkFMKHP0X12ywM\"", 26 | "id": "YE", 27 | "snippet": { 28 | "gl": "YE", 29 | "name": "Yemen" 30 | } 31 | }, 32 | { 33 | "kind": "youtube#i18nRegion", 34 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/u5oySmRytTPqfZBg8zIE7G7jvRs\"", 35 | "id": "ZW", 36 | "snippet": { 37 | "gl": "ZW", 38 | "name": "Zimbabwe" 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /testdata/apidata/members/membership_levels.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#membershipsLevelListResponse", 3 | "etag": "etag", 4 | "items": [ 5 | { 6 | "kind": "youtube#membershipsLevel", 7 | "etag": "etag", 8 | "id": "id", 9 | "snippet": { 10 | "creatorChannelId": "UCa-vrCLQHviTOVnEKDOdetQ", 11 | "levelDetails": { 12 | "displayName": "high" 13 | } 14 | } 15 | }, 16 | { 17 | "kind": "youtube#membershipsLevel", 18 | "etag": "etag", 19 | "id": "id", 20 | "snippet": { 21 | "creatorChannelId": "UCa-vrCLQHviTOVnEKDOdetQ", 22 | "levelDetails": { 23 | "displayName": "low" 24 | } 25 | } 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /testdata/apidata/playlist_items/insert_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#playlistItem", 3 | "etag": "4Bl2u6s8N1Jkkz1AHN4E-tw4OQQ", 4 | "id": "UExCYWlkdDBpbENNYW5HRElLcjhVVkJGWndOX1V2TUt2Uy4wMTcyMDhGQUE4NTIzM0Y5", 5 | "snippet": { 6 | "publishedAt": "2022-11-15T13:38:09Z", 7 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 8 | "title": "Lecture 6: Version Control (git) (2020)", 9 | "description": "You can find the lecture notes and exercises for this lecture at https://missing.csail.mit.edu/2020/version-control/\n\nHelp us caption & translate this video!\n\nhttps://amara.org/v/C1Ef9/", 10 | "thumbnails": { 11 | "default": { 12 | "url": "https://i.ytimg.com/vi/2sjqTHE0zok/default.jpg", 13 | "width": 120, 14 | "height": 90 15 | }, 16 | "medium": { 17 | "url": "https://i.ytimg.com/vi/2sjqTHE0zok/mqdefault.jpg", 18 | "width": 320, 19 | "height": 180 20 | }, 21 | "high": { 22 | "url": "https://i.ytimg.com/vi/2sjqTHE0zok/hqdefault.jpg", 23 | "width": 480, 24 | "height": 360 25 | }, 26 | "standard": { 27 | "url": "https://i.ytimg.com/vi/2sjqTHE0zok/sddefault.jpg", 28 | "width": 640, 29 | "height": 480 30 | }, 31 | "maxres": { 32 | "url": "https://i.ytimg.com/vi/2sjqTHE0zok/maxresdefault.jpg", 33 | "width": 1280, 34 | "height": 720 35 | } 36 | }, 37 | "channelTitle": "ikaros data", 38 | "playlistId": "PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS", 39 | "position": 0, 40 | "resourceId": { 41 | "kind": "youtube#video", 42 | "videoId": "2sjqTHE0zok" 43 | }, 44 | "videoOwnerChannelTitle": "Missing Semester", 45 | "videoOwnerChannelId": "UCuXy5tCgEninup9cGplbiFw" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /testdata/apidata/playlist_items/playlist_items_filter_video.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#playlistItemListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/L2uYevt91mZBw1hKeHio9_Aamz8\"", 4 | "pageInfo": { 5 | "totalResults": 1, 6 | "resultsPerPage": 5 7 | }, 8 | "items": [ 9 | { 10 | "kind": "youtube#playlistItem", 11 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Kib3kvf3c_Bq79UyVpa2pHYzV_U\"", 12 | "id": "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4wMTcyMDhGQUE4NTIzM0Y5", 13 | "snippet": { 14 | "publishedAt": "2019-05-11T00:55:44.000Z", 15 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 16 | "title": "Google I/O'19 - I/O Live (Day 3 Composite)", 17 | "description": "Relive moments from I/O Live, Day 3, at Google I/O'19\n\nGoogle I/O 2019 All Sessions Playlist → https://goo.gle/io19allsessions \nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to the Google Developers Channel → https://goo.gle/developers", 18 | "thumbnails": { 19 | "default": { 20 | "url": "https://i.ytimg.com/vi/VCv-KKIkLns/default.jpg", 21 | "width": 120, 22 | "height": 90 23 | }, 24 | "medium": { 25 | "url": "https://i.ytimg.com/vi/VCv-KKIkLns/mqdefault.jpg", 26 | "width": 320, 27 | "height": 180 28 | }, 29 | "high": { 30 | "url": "https://i.ytimg.com/vi/VCv-KKIkLns/hqdefault.jpg", 31 | "width": 480, 32 | "height": 360 33 | }, 34 | "standard": { 35 | "url": "https://i.ytimg.com/vi/VCv-KKIkLns/sddefault.jpg", 36 | "width": 640, 37 | "height": 480 38 | }, 39 | "maxres": { 40 | "url": "https://i.ytimg.com/vi/VCv-KKIkLns/maxresdefault.jpg", 41 | "width": 1280, 42 | "height": 720 43 | } 44 | }, 45 | "channelTitle": "Google Developers", 46 | "playlistId": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", 47 | "position": 2, 48 | "resourceId": { 49 | "kind": "youtube#video", 50 | "videoId": "VCv-KKIkLns" 51 | } 52 | } 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /testdata/apidata/playlist_items/playlist_items_single.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#playlistItemListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/YVsjtCTnqysTcNJy7jZglowaNYM\"", 4 | "pageInfo": { 5 | "totalResults": 1, 6 | "resultsPerPage": 5 7 | }, 8 | "items": [ 9 | { 10 | "kind": "youtube#playlistItem", 11 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/E5rTjxNaKfzDc-GFs2Cb9jkKlGM\"", 12 | "id": "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2", 13 | "snippet": { 14 | "publishedAt": "2019-05-11T00:27:38.000Z", 15 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 16 | "title": "Google I/O'19 - I/O Live (Day 1 Composite)", 17 | "description": "Relive moments from I/O Live, Day 1, at Google I/O'19\n\nGoogle I/O 2019 All Sessions Playlist → https://goo.gle/io19allsessions \nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to the Google Developers Channel → https://goo.gle/developers", 18 | "thumbnails": { 19 | "default": { 20 | "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg", 21 | "width": 120, 22 | "height": 90 23 | }, 24 | "medium": { 25 | "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg", 26 | "width": 320, 27 | "height": 180 28 | }, 29 | "high": { 30 | "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg", 31 | "width": 480, 32 | "height": 360 33 | }, 34 | "standard": { 35 | "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg", 36 | "width": 640, 37 | "height": 480 38 | }, 39 | "maxres": { 40 | "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg", 41 | "width": 1280, 42 | "height": 720 43 | } 44 | }, 45 | "channelTitle": "Google Developers", 46 | "playlistId": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", 47 | "position": 0, 48 | "resourceId": { 49 | "kind": "youtube#video", 50 | "videoId": "H1HZyvc0QnI" 51 | } 52 | } 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /testdata/apidata/playlists/insert_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#playlist", 3 | "etag": "Gw0SW_V3Hy1XNqjAJB1v1Q0ZmB4", 4 | "id": "PLBaidt0ilCMZN8XPVB5iXY6FlSYGeyn2n", 5 | "snippet": { 6 | "publishedAt": "2022-11-16T04:12:59Z", 7 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 8 | "title": "Test playlist", 9 | "description": "", 10 | "thumbnails": { 11 | "default": { 12 | "url": "https://i.ytimg.com/img/no_thumbnail.jpg", 13 | "width": 120, 14 | "height": 90 15 | }, 16 | "medium": { 17 | "url": "https://i.ytimg.com/img/no_thumbnail.jpg", 18 | "width": 320, 19 | "height": 180 20 | }, 21 | "high": { 22 | "url": "https://i.ytimg.com/img/no_thumbnail.jpg", 23 | "width": 480, 24 | "height": 360 25 | } 26 | }, 27 | "channelTitle": "ikaros data", 28 | "localized": { 29 | "title": "Test playlist", 30 | "description": "" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /testdata/apidata/playlists/playlists_single.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#playlistListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/fk7i9mk8dlQehbz-BVBy0SzccWY\"", 4 | "pageInfo": { 5 | "totalResults": 1, 6 | "resultsPerPage": 5 7 | }, 8 | "items": [ 9 | { 10 | "kind": "youtube#playlist", 11 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/gdl_m86JfWCd37Wtcc2dte9hrEg\"", 12 | "id": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", 13 | "snippet": { 14 | "publishedAt": "2019-05-10T00:18:56.000Z", 15 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 16 | "title": "I/O Live - Show Composite", 17 | "description": "", 18 | "thumbnails": { 19 | "default": { 20 | "url": "https://i.ytimg.com/vi/5NgsfxIWNls/default.jpg", 21 | "width": 120, 22 | "height": 90 23 | }, 24 | "medium": { 25 | "url": "https://i.ytimg.com/vi/5NgsfxIWNls/mqdefault.jpg", 26 | "width": 320, 27 | "height": 180 28 | }, 29 | "high": { 30 | "url": "https://i.ytimg.com/vi/5NgsfxIWNls/hqdefault.jpg", 31 | "width": 480, 32 | "height": 360 33 | }, 34 | "standard": { 35 | "url": "https://i.ytimg.com/vi/5NgsfxIWNls/sddefault.jpg", 36 | "width": 640, 37 | "height": 480 38 | }, 39 | "maxres": { 40 | "url": "https://i.ytimg.com/vi/5NgsfxIWNls/maxresdefault.jpg", 41 | "width": 1280, 42 | "height": 720 43 | } 44 | }, 45 | "channelTitle": "Google Developers", 46 | "localized": { 47 | "title": "I/O Live - Show Composite", 48 | "description": "" 49 | } 50 | } 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /testdata/apidata/subscriptions/insert_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#subscription", 3 | "etag": "BBbHqFIch0N1EhR1bwn0s3MofFg", 4 | "id": "POsnRIYsMcp1Cghr_Fsh-6uFZRcIHmTKzzByiv9ZAro", 5 | "snippet": { 6 | "publishedAt": "2022-11-16T11:02:09.19802Z", 7 | "title": "iQIYI 综艺精选", 8 | "description": "www.iq.com\n\niQIYI is an innovative market-leading online entertainment service and one of the largest internet companies in terms of user base in China. Over 1500 hit films and 180 TV shows are available FOR FREE on our global platform with multilingual subtitles in Mandarin, English, Malay, Indonesian, Thai and Vietnamese. \nWebsite: http://bit.ly/iqjxweb\n\nClick the link below to download iQIYI App and explore thousands of highly popular original and professionally-produced content.\nApp: http://bit.ly/iqjxapp\n\nFollow us on Facebook and know everything about your favorite shows!\nFacebook: https://bit.ly/iqiyifb\nInstagram: https://bit.ly/iqiyiins\nTwitter: https://bit.ly/iqiyitw", 9 | "resourceId": { 10 | "kind": "youtube#channel", 11 | "channelId": "UCQ6ptCagG3W0Bf4lexvnBEg" 12 | }, 13 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 14 | "thumbnails": { 15 | "default": { 16 | "url": "https://yt3.ggpht.com/kszyCtmb0Bsjo-Pwp6lhZnAA6wMP8gzqNU9qZOj-b9p2GvwF4yygtPBzuRpmGUPtymByYol9Oj8=s88-c-k-c0x00ffffff-no-rj" 17 | }, 18 | "medium": { 19 | "url": "https://yt3.ggpht.com/kszyCtmb0Bsjo-Pwp6lhZnAA6wMP8gzqNU9qZOj-b9p2GvwF4yygtPBzuRpmGUPtymByYol9Oj8=s240-c-k-c0x00ffffff-no-rj" 20 | }, 21 | "high": { 22 | "url": "https://yt3.ggpht.com/kszyCtmb0Bsjo-Pwp6lhZnAA6wMP8gzqNU9qZOj-b9p2GvwF4yygtPBzuRpmGUPtymByYol9Oj8=s800-c-k-c0x00ffffff-no-rj" 23 | } 24 | } 25 | }, 26 | "contentDetails": { 27 | "totalItemCount": 6986, 28 | "newItemCount": 0, 29 | "activityType": "all" 30 | }, 31 | "subscriberSnippet": { 32 | "title": "ikaros data", 33 | "description": "This is a test channel.", 34 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 35 | "thumbnails": { 36 | "default": { 37 | "url": "https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s88-c-k-c0x00ffffff-no-rj" 38 | }, 39 | "medium": { 40 | "url": "https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s240-c-k-c0x00ffffff-no-rj" 41 | }, 42 | "high": { 43 | "url": "https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s800-c-k-c0x00ffffff-no-rj" 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /testdata/apidata/subscriptions/subscription_zero.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#subscriptionListResponse", 3 | "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ewwRz0VbTYpp2EGbOkvZ5M_1mbo\"", 4 | "pageInfo": { 5 | "totalResults": 0, 6 | "resultsPerPage": 5 7 | }, 8 | "items": [] 9 | } -------------------------------------------------------------------------------- /testdata/apidata/subscriptions/subscriptions_by_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#subscriptionListResponse", 3 | "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/USyhytrL1qAH8AxBqW22EUor8kw\"", 4 | "pageInfo": { 5 | "totalResults": 2, 6 | "resultsPerPage": 5 7 | }, 8 | "items": [ 9 | { 10 | "kind": "youtube#subscription", 11 | "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/VIabsyP8MBhapi7K0fjjRX5bM2U\"", 12 | "id": "zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo", 13 | "snippet": { 14 | "publishedAt": "2018-09-11T11:35:04.568Z", 15 | "title": "PyCon 2015", 16 | "description": "", 17 | "resourceId": { 18 | "kind": "youtube#channel", 19 | "channelId": "UCgxzjK6GuOHVKR_08TT4hJQ" 20 | }, 21 | "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", 22 | "thumbnails": { 23 | "default": { 24 | "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" 25 | }, 26 | "medium": { 27 | "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" 28 | }, 29 | "high": { 30 | "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" 31 | } 32 | } 33 | } 34 | }, 35 | { 36 | "kind": "youtube#subscription", 37 | "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/pYbP9RYZzTnefaJtv-B2uQwsR4A\"", 38 | "id": "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo", 39 | "snippet": { 40 | "publishedAt": "2019-11-29T03:00:56.380Z", 41 | "title": "ikaros-life", 42 | "description": "This is a test channel.", 43 | "resourceId": { 44 | "kind": "youtube#channel", 45 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ" 46 | }, 47 | "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", 48 | "thumbnails": { 49 | "default": { 50 | "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" 51 | }, 52 | "medium": { 53 | "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" 54 | }, 55 | "high": { 56 | "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" 57 | } 58 | } 59 | } 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /testdata/apidata/user_profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "family_name": "liu", 3 | "name": "kun liu", 4 | "picture": "https://lh3.googleusercontent.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAACY/1E9uN31I7cE/photo.jpg", 5 | "locale": "zh-CN", 6 | "given_name": "kun", 7 | "id": "12345678910" 8 | } -------------------------------------------------------------------------------- /testdata/apidata/videos/get_rating_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#videoGetRatingResponse", 3 | "etag": "jHmA6WPghQxwUKfIGg5LVYotT3Y", 4 | "items": [ 5 | { 6 | "videoId": "D-lhorsDlUQ", 7 | "rating": "none" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /testdata/apidata/videos/videos_myrating_paged_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#videoListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Uj_O0S-h8FpI0EG1DLVrKzjMiqM\"", 4 | "prevPageToken": "CAIQAQ", 5 | "pageInfo": { 6 | "totalResults": 3, 7 | "resultsPerPage": 2 8 | }, 9 | "items": [ 10 | { 11 | "kind": "youtube#video", 12 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/97veyDdoO33-JR5D_HUtdm_rAP0\"", 13 | "id": "7mIDiKK4eyo", 14 | "snippet": { 15 | "publishedAt": "2019-08-28T09:29:43.000Z", 16 | "channelId": "UC-mexo-76-J1MlQM8NkWCYw", 17 | "title": "Insta Snapshot Tests Introduction", 18 | "description": "Shows how snapshot testing in the rust insta library works.", 19 | "thumbnails": { 20 | "default": { 21 | "url": "https://i.ytimg.com/vi/7mIDiKK4eyo/default.jpg", 22 | "width": 120, 23 | "height": 90 24 | }, 25 | "medium": { 26 | "url": "https://i.ytimg.com/vi/7mIDiKK4eyo/mqdefault.jpg", 27 | "width": 320, 28 | "height": 180 29 | }, 30 | "high": { 31 | "url": "https://i.ytimg.com/vi/7mIDiKK4eyo/hqdefault.jpg", 32 | "width": 480, 33 | "height": 360 34 | }, 35 | "standard": { 36 | "url": "https://i.ytimg.com/vi/7mIDiKK4eyo/sddefault.jpg", 37 | "width": 640, 38 | "height": 480 39 | }, 40 | "maxres": { 41 | "url": "https://i.ytimg.com/vi/7mIDiKK4eyo/maxresdefault.jpg", 42 | "width": 1280, 43 | "height": 720 44 | } 45 | }, 46 | "channelTitle": "Armin Ronacher", 47 | "tags": [ 48 | "rust", 49 | "insta", 50 | "snapshot testing" 51 | ], 52 | "categoryId": "28", 53 | "liveBroadcastContent": "none", 54 | "localized": { 55 | "title": "Insta Snapshot Tests Introduction", 56 | "description": "Shows how snapshot testing in the rust insta library works." 57 | } 58 | }, 59 | "player": { 60 | "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/7mIDiKK4eyo\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" 61 | } 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /testdata/error_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "errors": [ 4 | { 5 | "domain": "usageLimits", 6 | "reason": "keyInvalid", 7 | "message": "Bad Request" 8 | } 9 | ], 10 | "code": 400, 11 | "message": "Bad Request" 12 | } 13 | } -------------------------------------------------------------------------------- /testdata/error_response_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "error message" 3 | } -------------------------------------------------------------------------------- /testdata/modeldata/abuse_report_reason/abuse_reason.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#videoAbuseReportReason", 3 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/_WIvuNJwlISvQNQt_ukh2m0kt2Y\"", 4 | "id": "N", 5 | "snippet": { 6 | "label": "Sex or nudity", 7 | "secondaryReasons": [ 8 | { 9 | "id": "32", 10 | "label": "Graphic sex or nudity" 11 | }, 12 | { 13 | "id": "33", 14 | "label": "Content involving minors" 15 | }, 16 | { 17 | "id": "34", 18 | "label": "Other sexual content" 19 | } 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /testdata/modeldata/activities/activity.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#activity", 3 | "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/Jy79IfTqdSUQSMOkAA9ynak3zOI\"", 4 | "id": "MTUxNTc0OTk2MjI3Mjg1OTU3Nzk0MzQzODQ=", 5 | "snippet": { 6 | "publishedAt": "2019-11-29T02:57:07.000Z", 7 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 8 | "title": "华山日出", 9 | "description": "冷冷的山头", 10 | "thumbnails": { 11 | "default": { 12 | "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/default.jpg", 13 | "width": 120, 14 | "height": 90 15 | }, 16 | "medium": { 17 | "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/mqdefault.jpg", 18 | "width": 320, 19 | "height": 180 20 | }, 21 | "high": { 22 | "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/hqdefault.jpg", 23 | "width": 480, 24 | "height": 360 25 | }, 26 | "standard": { 27 | "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/sddefault.jpg", 28 | "width": 640, 29 | "height": 480 30 | }, 31 | "maxres": { 32 | "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/maxresdefault.jpg", 33 | "width": 1280, 34 | "height": 720 35 | } 36 | }, 37 | "channelTitle": "ikaros-life", 38 | "type": "upload" 39 | }, 40 | "contentDetails": { 41 | "upload": { 42 | "videoId": "JE8xdDp5B8Q" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /testdata/modeldata/activities/activity_contentDetails.json: -------------------------------------------------------------------------------- 1 | { 2 | "upload": { 3 | "videoId": "LDXYRzerjzU" 4 | } 5 | } -------------------------------------------------------------------------------- /testdata/modeldata/activities/activity_snippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "publishedAt": "2019-12-30T20:00:02.000Z", 3 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 4 | "title": "2019 Year in Review - The Developer Show", 5 | "description": "Here to bring you the latest developer news from across Google this year is Developer Advocate Timothy Jordan. In this last week of the year, we’re taking a look back at some of the coolest and biggest announcements we covered in 2019! \n\nFollow Google Developers on Instagram → https://goo.gle/googledevs\n\nWatch more #DevShow → https://goo.gle/GDevShow\nSubscribe to Google Developers → https://goo.gle/developers", 6 | "thumbnails": { 7 | "default": { 8 | "url": "https://i.ytimg.com/vi/DQGSZTxLVrI/default.jpg", 9 | "width": 120, 10 | "height": 90 11 | }, 12 | "medium": { 13 | "url": "https://i.ytimg.com/vi/DQGSZTxLVrI/mqdefault.jpg", 14 | "width": 320, 15 | "height": 180 16 | }, 17 | "high": { 18 | "url": "https://i.ytimg.com/vi/DQGSZTxLVrI/hqdefault.jpg", 19 | "width": 480, 20 | "height": 360 21 | }, 22 | "standard": { 23 | "url": "https://i.ytimg.com/vi/DQGSZTxLVrI/sddefault.jpg", 24 | "width": 640, 25 | "height": 480 26 | }, 27 | "maxres": { 28 | "url": "https://i.ytimg.com/vi/DQGSZTxLVrI/maxresdefault.jpg", 29 | "width": 1280, 30 | "height": 720 31 | } 32 | }, 33 | "channelTitle": "Google Developers", 34 | "type": "upload" 35 | } 36 | -------------------------------------------------------------------------------- /testdata/modeldata/captions/caption.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#caption", 3 | "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\"", 4 | "id": "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I", 5 | "snippet": { 6 | "videoId": "oHR3wURdJ94", 7 | "lastUpdated": "2020-01-14T09:40:49.981Z", 8 | "trackKind": "standard", 9 | "language": "en", 10 | "name": "", 11 | "audioTrackType": "unknown", 12 | "isCC": false, 13 | "isLarge": false, 14 | "isEasyReader": false, 15 | "isDraft": false, 16 | "isAutoSynced": false, 17 | "status": "serving" 18 | } 19 | } -------------------------------------------------------------------------------- /testdata/modeldata/captions/caption_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#captionListResponse", 3 | "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/bB4ewYNN7bQHonV-K7efrgBqh8M\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#caption", 7 | "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\"", 8 | "id": "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I", 9 | "snippet": { 10 | "videoId": "oHR3wURdJ94", 11 | "lastUpdated": "2020-01-14T09:40:49.981Z", 12 | "trackKind": "standard", 13 | "language": "en", 14 | "name": "", 15 | "audioTrackType": "unknown", 16 | "isCC": false, 17 | "isLarge": false, 18 | "isEasyReader": false, 19 | "isDraft": false, 20 | "isAutoSynced": false, 21 | "status": "serving" 22 | } 23 | }, 24 | { 25 | "kind": "youtube#caption", 26 | "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/iRxIplZcCiX0oujr5gSVMXkij8M\"", 27 | "id": "fPMuDm722CIRcUAT3NTPQHQZJZJxt39kU7JvrHk8Kzs=", 28 | "snippet": { 29 | "videoId": "oHR3wURdJ94", 30 | "lastUpdated": "2020-01-14T09:39:46.991Z", 31 | "trackKind": "standard", 32 | "language": "zh-Hans", 33 | "name": "", 34 | "audioTrackType": "unknown", 35 | "isCC": false, 36 | "isLarge": false, 37 | "isEasyReader": false, 38 | "isDraft": false, 39 | "isAutoSynced": false, 40 | "status": "serving" 41 | } 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /testdata/modeldata/captions/caption_snippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "videoId": "oHR3wURdJ94", 3 | "lastUpdated": "2020-01-14T09:40:49.981Z", 4 | "trackKind": "standard", 5 | "language": "en", 6 | "name": "", 7 | "audioTrackType": "unknown", 8 | "isCC": false, 9 | "isLarge": false, 10 | "isEasyReader": false, 11 | "isDraft": false, 12 | "isAutoSynced": false, 13 | "status": "serving" 14 | } 15 | -------------------------------------------------------------------------------- /testdata/modeldata/categories/guide_category_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#guideCategory", 3 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/fnL4T7wf3HKS8VCeb2Mui5q9zeM\"", 4 | "id": "GCQmVzdCBvZiBZb3VUdWJl", 5 | "snippet": { 6 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 7 | "title": "Best of YouTube" 8 | } 9 | } -------------------------------------------------------------------------------- /testdata/modeldata/categories/guide_category_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#guideCategoryListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/KIJAFi2jsRHVBmAk3XYhyRKynjw\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#guideCategory", 7 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\"", 8 | "id": "GCQmVzdCBvZiBZb3VUdWJl", 9 | "snippet": { 10 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 11 | "title": "Best of YouTube" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /testdata/modeldata/categories/video_category_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#videoCategory", 3 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", 4 | "id": "17", 5 | "snippet": { 6 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 7 | "title": "Sports", 8 | "assignable": true 9 | } 10 | } -------------------------------------------------------------------------------- /testdata/modeldata/categories/video_category_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#videoCategoryListResponse", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/0_wT9Ta0iZu7ETYC3E6Xi_B4mtA\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#videoCategory", 7 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", 8 | "id": "17", 9 | "snippet": { 10 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 11 | "title": "Sports", 12 | "assignable": true 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /testdata/modeldata/channel_sections/channel_section_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channelSection", 3 | "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/5bNXeieMoiNVa4NokOortBf50ZA\"", 4 | "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE", 5 | "snippet": { 6 | "type": "multipleChannels", 7 | "style": "horizontalRow", 8 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 9 | "title": "A channel for every type of developer...", 10 | "position": 0, 11 | "localized": { 12 | "title": "A channel for every type of developer..." 13 | } 14 | }, 15 | "contentDetails": { 16 | "channels": [ 17 | "UCVHFbqXqoYvEWM1Ddxl0QDg", 18 | "UCJS9pqu9BzkAMNTmzNMNhvg", 19 | "UCBmwzQnSoj9b6HzNmFrg_yw", 20 | "UCnUYZLuoy1rq1aVMwx4aTzw", 21 | "UCWf2ZlNsCGDS89VBF_awNvA", 22 | "UCP4bf6IHJJQehibu6ai__cg", 23 | "UC0rqucBdTuFTjJiefW5t-IQ", 24 | "UC8QMvQrV1bsK7WO37QpSxSg", 25 | "UClKO7be7O9cUGL94PHnAeOA", 26 | "UCwXdFgeE9KYzlDdR7TG9cMw", 27 | "UCorTyjVGM-PV5CCKbosONow", 28 | "UCXDc-ckqru8BgppXbCt0APw", 29 | "UCXPBsjgKKG2HqsKBhWA4uQw", 30 | "UCdIiCSqXuybzwGwJwrpHPqw", 31 | "UCVhDYDVo3AqyMIKtMLSrcEg", 32 | "UCK8sQmJBp8GCxrOtXWBpyEA" 33 | ] 34 | }, 35 | "localizations": { 36 | "zh-Hans": { 37 | "title": "中文" 38 | }, 39 | "en-Us": { 40 | "title": "english" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /testdata/modeldata/channels/channel_api_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channelListResponse", 3 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/0lqbdkIcLGXAPiLsJ3FTHo96TDg\"", 4 | "pageInfo": { 5 | "totalResults": 1, 6 | "resultsPerPage": 1 7 | }, 8 | "items": [ 9 | { 10 | "kind": "youtube#channel", 11 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/HUbWoTqNN1LPZKmbyCzPgvjVuR4\"", 12 | "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 13 | "snippet": { 14 | "title": "Google Developers", 15 | "description": "The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.", 16 | "customUrl": "googledevelopers", 17 | "publishedAt": "2007-08-23T00:34:43.000Z", 18 | "thumbnails": { 19 | "default": { 20 | "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo", 21 | "width": 88, 22 | "height": 88 23 | }, 24 | "medium": { 25 | "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo", 26 | "width": 240, 27 | "height": 240 28 | }, 29 | "high": { 30 | "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo", 31 | "width": 800, 32 | "height": 800 33 | } 34 | }, 35 | "localized": { 36 | "title": "Google Developers", 37 | "description": "The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms." 38 | }, 39 | "country": "US" 40 | }, 41 | "statistics": { 42 | "viewCount": "160361638", 43 | "commentCount": "0", 44 | "subscriberCount": "1927873", 45 | "hiddenSubscriberCount": false, 46 | "videoCount": "5026" 47 | } 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /testdata/modeldata/channels/channel_content_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "relatedPlaylists": { 3 | "uploads": "UU_x5XG1OV2P6uZZ5FSM9Ttw", 4 | "watchHistory": "HL", 5 | "watchLater": "WL" 6 | } 7 | } -------------------------------------------------------------------------------- /testdata/modeldata/channels/channel_snippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Google Developers", 3 | "description": "The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.", 4 | "customUrl": "googledevelopers", 5 | "publishedAt": "2007-08-23T00:34:43.000Z", 6 | "thumbnails": { 7 | "default": { 8 | "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo", 9 | "width": 88, 10 | "height": 88 11 | }, 12 | "medium": { 13 | "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo", 14 | "width": 240, 15 | "height": 240 16 | }, 17 | "high": { 18 | "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo", 19 | "width": 800, 20 | "height": 800 21 | } 22 | }, 23 | "localized": { 24 | "title": "Google Developers", 25 | "description": "The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms." 26 | }, 27 | "country": "US" 28 | } -------------------------------------------------------------------------------- /testdata/modeldata/channels/channel_statistics.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewCount": 160361638, 3 | "commentCount": "0", 4 | "subscriberCount": "1927873", 5 | "hiddenSubscriberCount": false, 6 | "videoCount": "5026" 7 | } -------------------------------------------------------------------------------- /testdata/modeldata/channels/channel_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "privacyStatus": "public", 3 | "isLinked": true, 4 | "longUploadsStatus": "longUploadsUnspecified" 5 | } -------------------------------------------------------------------------------- /testdata/modeldata/channels/channel_topic_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "topicIds": [ 3 | "/m/019_rr", 4 | "/m/07c1v", 5 | "/m/02jjt", 6 | "/m/019_rr", 7 | "/m/07c1v", 8 | "/m/02jjt" 9 | ], 10 | "topicCategories": [ 11 | "https://en.wikipedia.org/wiki/Entertainment", 12 | "https://en.wikipedia.org/wiki/Technology", 13 | "https://en.wikipedia.org/wiki/Lifestyle_(sociology)" 14 | ] 15 | } -------------------------------------------------------------------------------- /testdata/modeldata/comments/comment_api_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#commentListResponse", 3 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/WGjMjz47HiC5hiv290at1ES2VhM\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#comment", 7 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/DNyaTRe4NG3pWMwjpCUwPAYb9uk\"", 8 | "id": "UgxKREWxIgDrw8w2e_Z4AaABAg", 9 | "snippet": { 10 | "authorDisplayName": "Hieu Nguyen", 11 | "authorProfileImageUrl": "https://yt3.ggpht.com/-N1uydT1LhpA/AAAAAAAAAAI/AAAAAAAAAAA/nvwONlQ4ZsE/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 12 | "authorChannelUrl": "http://www.youtube.com/channel/UClfzT4CU_yaZjJaI4pKqSjQ", 13 | "authorChannelId": { 14 | "value": "UClfzT4CU_yaZjJaI4pKqSjQ" 15 | }, 16 | "textDisplay": "Super video !!!\u003cbr /\u003eWith full power skil thank a lot ... \u003cbr /\u003eVery nice , coupe \u003cbr /\u003ecan i give Luke Davit and JESSICA EARl- CHA some tea and tall a more ...", 17 | "textOriginal": "Super video !!!\nWith full power skil thank a lot ... \nVery nice , coupe \ncan i give Luke Davit and JESSICA EARl- CHA some tea and tall a more ...", 18 | "canRate": true, 19 | "viewerRating": "none", 20 | "likeCount": 0, 21 | "publishedAt": "2019-04-20T01:03:39.000Z", 22 | "updatedAt": "2019-04-20T01:03:39.000Z" 23 | } 24 | }, 25 | { 26 | "kind": "youtube#comment", 27 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/bmZin9yRcFlXNixJ4nHaVlISbZM\"", 28 | "id": "UgyrVQaFfEdvaSzstj14AaABAg", 29 | "snippet": { 30 | "authorDisplayName": "Mani Kanta", 31 | "authorProfileImageUrl": "https://yt3.ggpht.com/-8VVOkpYv6O4/AAAAAAAAAAI/AAAAAAAAAAA/9asGD8pGx7Y/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 32 | "authorChannelUrl": "http://www.youtube.com/channel/UCJBxRADq6jctX-YdhjkB6PA", 33 | "authorChannelId": { 34 | "value": "UCJBxRADq6jctX-YdhjkB6PA" 35 | }, 36 | "textDisplay": "super", 37 | "textOriginal": "super", 38 | "canRate": true, 39 | "viewerRating": "none", 40 | "likeCount": 0, 41 | "publishedAt": "2019-04-04T04:14:44.000Z", 42 | "updatedAt": "2019-04-04T04:14:44.000Z" 43 | } 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /testdata/modeldata/comments/comment_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#comment", 3 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/4cvUO3bQNuuOby5VnN9ZtVUJfk8\"", 4 | "id": "UgwxApqcfzZzF_C5Zqx4AaABAg", 5 | "snippet": { 6 | "authorDisplayName": "Oeurn Ravuth", 7 | "authorProfileImageUrl": "https://yt3.ggpht.com/-FTjrEZu33Cg/AAAAAAAAAAI/AAAAAAAAAAA/74aahJJl02c/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 8 | "authorChannelUrl": "http://www.youtube.com/channel/UCqPku3cxM-ED3poX8YtGqeg", 9 | "authorChannelId": { 10 | "value": "UCqPku3cxM-ED3poX8YtGqeg" 11 | }, 12 | "videoId": "wtLJPvx7-ys", 13 | "textDisplay": "This video is awesome! GOOD", 14 | "textOriginal": "This video is awesome! GOOD", 15 | "canRate": true, 16 | "viewerRating": "none", 17 | "likeCount": 0, 18 | "publishedAt": "2019-03-28T11:33:46.000Z", 19 | "updatedAt": "2019-03-28T11:33:46.000Z" 20 | } 21 | } -------------------------------------------------------------------------------- /testdata/modeldata/comments/comment_snippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "authorDisplayName": "Oeurn Ravuth", 3 | "authorProfileImageUrl": "https://yt3.ggpht.com/-FTjrEZu33Cg/AAAAAAAAAAI/AAAAAAAAAAA/74aahJJl02c/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 4 | "authorChannelUrl": "http://www.youtube.com/channel/UCqPku3cxM-ED3poX8YtGqeg", 5 | "authorChannelId": { 6 | "value": "UCqPku3cxM-ED3poX8YtGqeg" 7 | }, 8 | "videoId": "wtLJPvx7-ys", 9 | "textDisplay": "This video is awesome! GOOD", 10 | "textOriginal": "This video is awesome! GOOD", 11 | "canRate": true, 12 | "viewerRating": "none", 13 | "likeCount": 0, 14 | "publishedAt": "2019-03-28T11:33:46.000Z", 15 | "updatedAt": "2019-03-28T11:33:46.000Z" 16 | } -------------------------------------------------------------------------------- /testdata/modeldata/comments/comment_thread_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#commentThread", 3 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Bu9ED8YytJP_F5IxgbsfRJg0CZI\"", 4 | "id": "UgydxWWoeA7F1OdqypJ4AaABAg", 5 | "snippet": { 6 | "videoId": "D-lhorsDlUQ", 7 | "topLevelComment": { 8 | "kind": "youtube#comment", 9 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Vcd3llXDKJW8UrWr8ndIHHDBk8g\"", 10 | "id": "UgydxWWoeA7F1OdqypJ4AaABAg", 11 | "snippet": { 12 | "authorDisplayName": "Loren Robilio", 13 | "authorProfileImageUrl": "https://yt3.ggpht.com/-dVa9HLlQcNs/AAAAAAAAAAI/AAAAAAAAAAA/lxKAIuHR-20/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 14 | "authorChannelUrl": "http://www.youtube.com/channel/UCe9i1nJCcevTa6KJa55KYog", 15 | "authorChannelId": { 16 | "value": "UCe9i1nJCcevTa6KJa55KYog" 17 | }, 18 | "videoId": "D-lhorsDlUQ", 19 | "textDisplay": "Actions.ai", 20 | "textOriginal": "Actions.ai", 21 | "canRate": true, 22 | "viewerRating": "none", 23 | "likeCount": 0, 24 | "publishedAt": "2019-06-23T08:24:24.000Z", 25 | "updatedAt": "2019-06-23T08:24:24.000Z" 26 | } 27 | }, 28 | "canReply": true, 29 | "totalReplyCount": 1, 30 | "isPublic": true 31 | }, 32 | "replies": { 33 | "comments": [ 34 | { 35 | "kind": "youtube#comment", 36 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Z_3RVDklwNvCP3pgufc11jc5ud0\"", 37 | "id": "UgydxWWoeA7F1OdqypJ4AaABAg.8wWQ3tdHcFx8xcDheui-qb", 38 | "snippet": { 39 | "authorDisplayName": "Dian Anggraeni", 40 | "authorProfileImageUrl": "https://yt3.ggpht.com/-WLAKDA-bqa8/AAAAAAAAAAI/AAAAAAAAAAA/4VOHyI34fuU/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 41 | "authorChannelUrl": "http://www.youtube.com/channel/UCsl2A_QzcSnFD2hpCyVwXxA", 42 | "authorChannelId": { 43 | "value": "UCsl2A_QzcSnFD2hpCyVwXxA" 44 | }, 45 | "videoId": "D-lhorsDlUQ", 46 | "textDisplay": "#", 47 | "textOriginal": "#", 48 | "parentId": "UgydxWWoeA7F1OdqypJ4AaABAg", 49 | "canRate": true, 50 | "viewerRating": "none", 51 | "likeCount": 1, 52 | "publishedAt": "2019-07-20T20:22:27.000Z", 53 | "updatedAt": "2019-07-20T20:22:27.000Z" 54 | } 55 | } 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /testdata/modeldata/comments/comment_thread_replies.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": [ 3 | { 4 | "kind": "youtube#comment", 5 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Z_3RVDklwNvCP3pgufc11jc5ud0\"", 6 | "id": "UgydxWWoeA7F1OdqypJ4AaABAg.8wWQ3tdHcFx8xcDheui-qb", 7 | "snippet": { 8 | "authorDisplayName": "Dian Anggraeni", 9 | "authorProfileImageUrl": "https://yt3.ggpht.com/-WLAKDA-bqa8/AAAAAAAAAAI/AAAAAAAAAAA/4VOHyI34fuU/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 10 | "authorChannelUrl": "http://www.youtube.com/channel/UCsl2A_QzcSnFD2hpCyVwXxA", 11 | "authorChannelId": { 12 | "value": "UCsl2A_QzcSnFD2hpCyVwXxA" 13 | }, 14 | "videoId": "D-lhorsDlUQ", 15 | "textDisplay": "#", 16 | "textOriginal": "#", 17 | "parentId": "UgydxWWoeA7F1OdqypJ4AaABAg", 18 | "canRate": true, 19 | "viewerRating": "none", 20 | "likeCount": 1, 21 | "publishedAt": "2019-07-20T20:22:27.000Z", 22 | "updatedAt": "2019-07-20T20:22:27.000Z" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /testdata/modeldata/comments/comment_thread_snippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "videoId": "D-lhorsDlUQ", 3 | "topLevelComment": { 4 | "kind": "youtube#comment", 5 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Vcd3llXDKJW8UrWr8ndIHHDBk8g\"", 6 | "id": "UgydxWWoeA7F1OdqypJ4AaABAg", 7 | "snippet": { 8 | "authorDisplayName": "Loren Robilio", 9 | "authorProfileImageUrl": "https://yt3.ggpht.com/-dVa9HLlQcNs/AAAAAAAAAAI/AAAAAAAAAAA/lxKAIuHR-20/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", 10 | "authorChannelUrl": "http://www.youtube.com/channel/UCe9i1nJCcevTa6KJa55KYog", 11 | "authorChannelId": { 12 | "value": "UCe9i1nJCcevTa6KJa55KYog" 13 | }, 14 | "videoId": "D-lhorsDlUQ", 15 | "textDisplay": "Actions.ai", 16 | "textOriginal": "Actions.ai", 17 | "canRate": true, 18 | "viewerRating": "none", 19 | "likeCount": 0, 20 | "publishedAt": "2019-06-23T08:24:24.000Z", 21 | "updatedAt": "2019-06-23T08:24:24.000Z" 22 | } 23 | }, 24 | "canReply": true, 25 | "totalReplyCount": 1, 26 | "isPublic": true 27 | } 28 | -------------------------------------------------------------------------------- /testdata/modeldata/common/thumbnail_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s88-c-k-c0xffffffff-no-rj-mo", 3 | "width": 88, 4 | "height": 88 5 | } -------------------------------------------------------------------------------- /testdata/modeldata/common/thumbnails_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s88-c-k-c0xffffffff-no-rj-mo", 4 | "width": 88, 5 | "height": 88 6 | }, 7 | "medium": { 8 | "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s240-c-k-c0xffffffff-no-rj-mo", 9 | "width": 240, 10 | "height": 240 11 | }, 12 | "high": { 13 | "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s800-c-k-c0xffffffff-no-rj-mo", 14 | "width": 800, 15 | "height": 800 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /testdata/modeldata/i18ns/language_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#i18nLanguage", 3 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/GMrwiM1f-4KHxMka40cB3lysLgY\"", 4 | "id": "af", 5 | "snippet": { 6 | "hl": "af", 7 | "name": "Afrikaans" 8 | } 9 | } -------------------------------------------------------------------------------- /testdata/modeldata/i18ns/language_res.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#i18nLanguageListResponse", 3 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/qgFy24yvs-L_dNjr2d-Rd_Xcfw4\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#i18nLanguage", 7 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/GMrwiM1f-4KHxMka40cB3lysLgY\"", 8 | "id": "af", 9 | "snippet": { 10 | "hl": "af", 11 | "name": "Afrikaans" 12 | } 13 | }, 14 | { 15 | "kind": "youtube#i18nLanguage", 16 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/wOlCLE4kfCyCca9_ssuNDceE0yk\"", 17 | "id": "az", 18 | "snippet": { 19 | "hl": "az", 20 | "name": "Azerbaijani" 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /testdata/modeldata/i18ns/region_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#i18nRegion", 3 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/R_GB1d7CQi3LIpoHKbakFDisvoA\"", 4 | "id": "DZ", 5 | "snippet": { 6 | "gl": "DZ", 7 | "name": "Algeria" 8 | } 9 | } -------------------------------------------------------------------------------- /testdata/modeldata/i18ns/region_res.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#i18nRegionListResponse", 3 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/q85_wZeDyKDzYtt-LhNaozyi_sk\"", 4 | "items": [ 5 | { 6 | "kind": "youtube#i18nRegion", 7 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/R_GB1d7CQi3LIpoHKbakFDisvoA\"", 8 | "id": "DZ", 9 | "snippet": { 10 | "gl": "DZ", 11 | "name": "Algeria" 12 | } 13 | }, 14 | { 15 | "kind": "youtube#i18nRegion", 16 | "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/w6ci5tJWSaqFmjn3xsM2loOjo2o\"", 17 | "id": "AR", 18 | "snippet": { 19 | "gl": "AR", 20 | "name": "Argentina" 21 | } 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /testdata/modeldata/members/member_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#member", 3 | "etag": "etag", 4 | "snippet": { 5 | "creatorChannelId": "UCa-vrCLQHviTOVnEKDOdetQ", 6 | "memberDetails": { 7 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", 8 | "channelUrl": "https://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ", 9 | "displayName": "ikaros-life", 10 | "profileImageUrl": "https://yt3.ggpht.com/a-/AOh14Gg1_gYcI03VLDd3FMLUY5cb5O9RC9sElj26-1SR=s288-c-k-c0xffffffff-no-rj-mo" 11 | }, 12 | "membershipsDetails": { 13 | "highestAccessibleLevel": "string", 14 | "highestAccessibleLevelDisplayName": "string", 15 | "accessibleLevels": [ 16 | "string" 17 | ], 18 | "membershipsDuration": { 19 | "memberSince": "2007-08-23T00:34:43Z", 20 | "memberTotalDurationMonths": 5 21 | }, 22 | "membershipsDurationAtLevel": [ 23 | { 24 | "level": "string", 25 | "memberSince": "2007-08-23T00:34:43Z", 26 | "memberTotalDurationMonths": 6 27 | } 28 | ] 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /testdata/modeldata/members/membership_level.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#membershipsLevel", 3 | "etag": "etag", 4 | "id": "id", 5 | "snippet": { 6 | "creatorChannelId": "UCa-vrCLQHviTOVnEKDOdetQ", 7 | "levelDetails": { 8 | "displayName": "high" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /testdata/modeldata/playlist_items/playlist_item_content_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "videoId": "D-lhorsDlUQ", 3 | "videoPublishedAt": "2019-03-21T20:37:49.000Z" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/modeldata/playlist_items/playlist_item_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#playlistItem", 3 | "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/lAPls3tzYIP4Re0-vMkPDF4whaw\"", 4 | "id": "UExPVTJYTFl4bXNJSnB1ZmVNSG5jblF2Rk9lMEszTWhWcC41NkI0NEY2RDEwNTU3Q0M2", 5 | "snippet": { 6 | "publishedAt": "2019-05-16T18:46:20.000Z", 7 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 8 | "title": "What are Actions on Google (Assistant on Air)", 9 | "description": "In the first episode of Assistant on Air, Luke Davis and guest Jessica Dene Early-Cha introduce the concept of Actions on Google, and talk about common terminology.\n\nActions on Google docs → http://bit.ly/2YabdS5\n\nSubscribe to Google Devs for the latest Actions on Google videos → https://bit.ly/googledevs", 10 | "thumbnails": { 11 | "default": { 12 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", 13 | "width": 120, 14 | "height": 90 15 | }, 16 | "medium": { 17 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", 18 | "width": 320, 19 | "height": 180 20 | }, 21 | "high": { 22 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", 23 | "width": 480, 24 | "height": 360 25 | }, 26 | "standard": { 27 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", 28 | "width": 640, 29 | "height": 480 30 | }, 31 | "maxres": { 32 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", 33 | "width": 1280, 34 | "height": 720 35 | } 36 | }, 37 | "channelTitle": "Google Developers", 38 | "playlistId": "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp", 39 | "position": 0, 40 | "resourceId": { 41 | "kind": "youtube#video", 42 | "videoId": "D-lhorsDlUQ" 43 | }, 44 | "videoOwnerChannelTitle": "Google Developers", 45 | "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" 46 | }, 47 | "contentDetails": { 48 | "videoId": "D-lhorsDlUQ", 49 | "videoPublishedAt": "2019-03-21T20:37:49.000Z" 50 | }, 51 | "status": { 52 | "privacyStatus": "public" 53 | } 54 | } -------------------------------------------------------------------------------- /testdata/modeldata/playlist_items/playlist_item_snippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "publishedAt": "2019-05-16T18:46:20.000Z", 3 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 4 | "title": "What are Actions on Google (Assistant on Air)", 5 | "description": "In the first episode of Assistant on Air, Luke Davis and guest Jessica Dene Early-Cha introduce the concept of Actions on Google, and talk about common terminology.\n\nActions on Google docs → http://bit.ly/2YabdS5\n\nSubscribe to Google Devs for the latest Actions on Google videos → https://bit.ly/googledevs", 6 | "thumbnails": { 7 | "default": { 8 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", 9 | "width": 120, 10 | "height": 90 11 | }, 12 | "medium": { 13 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", 14 | "width": 320, 15 | "height": 180 16 | }, 17 | "high": { 18 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", 19 | "width": 480, 20 | "height": 360 21 | }, 22 | "standard": { 23 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", 24 | "width": 640, 25 | "height": 480 26 | }, 27 | "maxres": { 28 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", 29 | "width": 1280, 30 | "height": 720 31 | } 32 | }, 33 | "channelTitle": "Google Developers", 34 | "playlistId": "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp", 35 | "position": 0, 36 | "resourceId": { 37 | "kind": "youtube#video", 38 | "videoId": "D-lhorsDlUQ" 39 | } 40 | } -------------------------------------------------------------------------------- /testdata/modeldata/playlist_items/playlist_item_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "privacyStatus": "public" 3 | } -------------------------------------------------------------------------------- /testdata/modeldata/playlists/playlist_content_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemCount": 4 3 | } -------------------------------------------------------------------------------- /testdata/modeldata/playlists/playlist_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#playlist", 3 | "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/XooPPPPffp2qIyK-PJIIwE8GJuM\"", 4 | "id": "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp", 5 | "snippet": { 6 | "publishedAt": "2019-05-16T18:46:20.000Z", 7 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 8 | "title": "Assistant on Air", 9 | "description": "The Assistant on Air series covers everything you need to know about the Google Assistant. From common terminology to creating your own Google Action, make sure to tune in to find out more!", 10 | "thumbnails": { 11 | "default": { 12 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", 13 | "width": 120, 14 | "height": 90 15 | }, 16 | "medium": { 17 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", 18 | "width": 320, 19 | "height": 180 20 | }, 21 | "high": { 22 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", 23 | "width": 480, 24 | "height": 360 25 | }, 26 | "standard": { 27 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", 28 | "width": 640, 29 | "height": 480 30 | }, 31 | "maxres": { 32 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", 33 | "width": 1280, 34 | "height": 720 35 | } 36 | }, 37 | "channelTitle": "Google Developers", 38 | "localized": { 39 | "title": "Assistant on Air", 40 | "description": "The Assistant on Air series covers everything you need to know about the Google Assistant. From common terminology to creating your own Google Action, make sure to tune in to find out more!" 41 | } 42 | }, 43 | "status": { 44 | "privacyStatus": "public" 45 | }, 46 | "contentDetails": { 47 | "itemCount": 4 48 | } 49 | } -------------------------------------------------------------------------------- /testdata/modeldata/playlists/playlist_snippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "publishedAt": "2019-05-16T18:46:20.000Z", 3 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 4 | "title": "Assistant on Air", 5 | "description": "The Assistant on Air series covers everything you need to know about the Google Assistant. From common terminology to creating your own Google Action, make sure to tune in to find out more!", 6 | "thumbnails": { 7 | "default": { 8 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", 9 | "width": 120, 10 | "height": 90 11 | }, 12 | "medium": { 13 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", 14 | "width": 320, 15 | "height": 180 16 | }, 17 | "high": { 18 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", 19 | "width": 480, 20 | "height": 360 21 | }, 22 | "standard": { 23 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", 24 | "width": 640, 25 | "height": 480 26 | }, 27 | "maxres": { 28 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", 29 | "width": 1280, 30 | "height": 720 31 | } 32 | }, 33 | "channelTitle": "Google Developers", 34 | "localized": { 35 | "title": "Assistant on Air", 36 | "description": "The Assistant on Air series covers everything you need to know about the Google Assistant. From common terminology to creating your own Google Action, make sure to tune in to find out more!" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /testdata/modeldata/playlists/playlist_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "privacyStatus": "public" 3 | } -------------------------------------------------------------------------------- /testdata/modeldata/search_result/search_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#searchResult", 3 | "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/vbYWvy5RlqHHhMVjeHUTwJcQQWg\"", 4 | "id": { 5 | "kind": "youtube#video", 6 | "videoId": "fq4N0hgOWzU" 7 | }, 8 | "snippet": { 9 | "publishedAt": "2018-02-23T15:00:09.000Z", 10 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 11 | "title": "Introducing Flutter", 12 | "description": "Get started at https://flutter.io today. Flutter is Google's mobile UI framework for crafting high-quality native interfaces on iOS and Android in record time. Flutter ...", 13 | "thumbnails": { 14 | "default": { 15 | "url": "https://i.ytimg.com/vi/fq4N0hgOWzU/default.jpg", 16 | "width": 120, 17 | "height": 90 18 | }, 19 | "medium": { 20 | "url": "https://i.ytimg.com/vi/fq4N0hgOWzU/mqdefault.jpg", 21 | "width": 320, 22 | "height": 180 23 | }, 24 | "high": { 25 | "url": "https://i.ytimg.com/vi/fq4N0hgOWzU/hqdefault.jpg", 26 | "width": 480, 27 | "height": 360 28 | } 29 | }, 30 | "channelTitle": "Google Developers", 31 | "liveBroadcastContent": "none" 32 | } 33 | } -------------------------------------------------------------------------------- /testdata/modeldata/search_result/search_result_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#playlist", 3 | "playlistId": "PLOU2XLYxmsIKX0pUJV3uqp6N3NeHwHh0c" 4 | } -------------------------------------------------------------------------------- /testdata/modeldata/search_result/search_result_snippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "publishedAt": "2016-03-30T16:59:12.000Z", 3 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 4 | "title": "Hello World - Machine Learning Recipes #1", 5 | "description": "Six lines of Python is all it takes to write your first machine learning program! In this episode, we'll briefly introduce what machine learning is and why it's ...", 6 | "thumbnails": { 7 | "default": { 8 | "url": "https://i.ytimg.com/vi/cKxRvEZd3Mw/default.jpg", 9 | "width": 120, 10 | "height": 90 11 | }, 12 | "medium": { 13 | "url": "https://i.ytimg.com/vi/cKxRvEZd3Mw/mqdefault.jpg", 14 | "width": 320, 15 | "height": 180 16 | }, 17 | "high": { 18 | "url": "https://i.ytimg.com/vi/cKxRvEZd3Mw/hqdefault.jpg", 19 | "width": 480, 20 | "height": 360 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /testdata/modeldata/subscriptions/contentDetails.json: -------------------------------------------------------------------------------- 1 | { 2 | "totalItemCount": 2, 3 | "newItemCount": 0, 4 | "activityType": "all" 5 | } -------------------------------------------------------------------------------- /testdata/modeldata/subscriptions/snippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "publishedAt": "2018-12-25T09:12:18.265Z", 3 | "title": "Next Day Video", 4 | "description": "", 5 | "resourceId": { 6 | "kind": "youtube#channel", 7 | "channelId": "UCQ7dFBzZGlBvtU2hCecsBBg" 8 | }, 9 | "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", 10 | "thumbnails": { 11 | "default": { 12 | "url": "https://yt3.ggpht.com/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" 13 | }, 14 | "medium": { 15 | "url": "https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" 16 | }, 17 | "high": { 18 | "url": "https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /testdata/modeldata/subscriptions/subscriberSnippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "kun liu", 3 | "description": "", 4 | "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", 5 | "thumbnails": { 6 | "default": { 7 | "url": "https://yt3.ggpht.com/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" 8 | }, 9 | "medium": { 10 | "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" 11 | }, 12 | "high": { 13 | "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /testdata/modeldata/subscriptions/subscription.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#subscription", 3 | "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/hZAeF0AETpmxML6TuUZfYWXtNzQ\"", 4 | "id": "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo", 5 | "snippet": { 6 | "publishedAt": "2019-11-29T03:00:56.380Z", 7 | "title": "ikaros-life", 8 | "description": "This is a test channel.", 9 | "resourceId": { 10 | "kind": "youtube#channel", 11 | "channelId": "UCa-vrCLQHviTOVnEKDOdetQ" 12 | }, 13 | "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", 14 | "thumbnails": { 15 | "default": { 16 | "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" 17 | }, 18 | "medium": { 19 | "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" 20 | }, 21 | "high": { 22 | "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" 23 | } 24 | } 25 | }, 26 | "contentDetails": { 27 | "totalItemCount": 2, 28 | "newItemCount": 0, 29 | "activityType": "all" 30 | }, 31 | "subscriberSnippet": { 32 | "title": "kun liu", 33 | "description": "", 34 | "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", 35 | "thumbnails": { 36 | "default": { 37 | "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" 38 | }, 39 | "medium": { 40 | "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" 41 | }, 42 | "high": { 43 | "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /testdata/modeldata/users/access_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "access_token", 3 | "id_token": "id_token", 4 | "expires_in": 3600, 5 | "token_type": "Bearer", 6 | "scope": [ 7 | "https://www.googleapis.com/auth/userinfo.profile", 8 | "https://www.googleapis.com/auth/youtube" 9 | ], 10 | "refresh_token": "refresh_token" 11 | } -------------------------------------------------------------------------------- /testdata/modeldata/users/user_profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "family_name": "liu", 3 | "name": "kun liu", 4 | "picture": "https://lh3.googleusercontent.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAACY/1E9uN31I7cE/photo.jpg", 5 | "locale": "zh-CN", 6 | "given_name": "kun", 7 | "id": "12345678910" 8 | } -------------------------------------------------------------------------------- /testdata/modeldata/videos/video_category_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#videoCategory", 3 | "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", 4 | "id": "17", 5 | "snippet": { 6 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ", 7 | "title": "Sports", 8 | "assignable": true 9 | } 10 | } -------------------------------------------------------------------------------- /testdata/modeldata/videos/video_content_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "duration": "PT21M7S", 3 | "dimension": "2d", 4 | "definition": "hd", 5 | "caption": "true", 6 | "licensedContent": false, 7 | "projection": "rectangular" 8 | } -------------------------------------------------------------------------------- /testdata/modeldata/videos/video_recording_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "recordingDate": "2024-07-03T00:00:00Z" 3 | } 4 | -------------------------------------------------------------------------------- /testdata/modeldata/videos/video_snippet.json: -------------------------------------------------------------------------------- 1 | { 2 | "publishedAt": "2019-03-21T20:37:49.000Z", 3 | "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", 4 | "title": "What are Actions on Google (Assistant on Air)", 5 | "description": "In the first episode of Assistant on Air, Luke Davis and guest Jessica Dene Early-Cha introduce the concept of Actions on Google, and talk about common terminology.\n\nActions on Google docs → http://bit.ly/2YabdS5\n\nSubscribe to Google Devs for the latest Actions on Google videos → https://bit.ly/googledevs", 6 | "thumbnails": { 7 | "default": { 8 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", 9 | "width": 120, 10 | "height": 90 11 | }, 12 | "medium": { 13 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", 14 | "width": 320, 15 | "height": 180 16 | }, 17 | "high": { 18 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", 19 | "width": 480, 20 | "height": 360 21 | }, 22 | "standard": { 23 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", 24 | "width": 640, 25 | "height": 480 26 | }, 27 | "maxres": { 28 | "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", 29 | "width": 1280, 30 | "height": 720 31 | } 32 | }, 33 | "channelTitle": "Google Developers", 34 | "tags": [ 35 | "Google", 36 | "developers", 37 | "aog", 38 | "Actions on Google", 39 | "Assistant", 40 | "Google Assistant", 41 | "actions", 42 | "google home", 43 | "actions on google", 44 | "google assistant developers", 45 | "google assistant sdk", 46 | "Actions on google developers", 47 | "smarthome developers", 48 | "common terminology", 49 | "custom action on google", 50 | "google assistant in your app", 51 | "add google assistant", 52 | "assistant on air", 53 | "how to use google assistant on air", 54 | "Actions on Google how to" 55 | ], 56 | "categoryId": "28", 57 | "liveBroadcastContent": "none", 58 | "defaultLanguage": "en", 59 | "localized": { 60 | "title": "What are Actions on Google (Assistant on Air)", 61 | "description": "In the first episode of Assistant on Air, Luke Davis and guest Jessica Dene Early-Cha introduce the concept of Actions on Google, and talk about common terminology.\n\nActions on Google docs → http://bit.ly/2YabdS5\n\nSubscribe to Google Devs for the latest Actions on Google videos → https://bit.ly/googledevs" 62 | }, 63 | "defaultAudioLanguage": "en" 64 | } -------------------------------------------------------------------------------- /testdata/modeldata/videos/video_statistics.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewCount": 8087, 3 | "likeCount": "190", 4 | "dislikeCount": "23", 5 | "favoriteCount": "0", 6 | "commentCount": "32" 7 | } -------------------------------------------------------------------------------- /testdata/modeldata/videos/video_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "uploadStatus": "processed", 3 | "privacyStatus": "public", 4 | "license": "youtube", 5 | "embeddable": true, 6 | "publicStatsViewable": true, 7 | "publishAt": "2019-03-21T20:37:49.000Z", 8 | "madeForKids": false 9 | } -------------------------------------------------------------------------------- /testdata/modeldata/videos/video_topic_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "relevantTopicIds": [ 3 | "/m/02jjt" 4 | ], 5 | "topicCategories": [ 6 | "https://en.wikipedia.org/wiki/Entertainment" 7 | ] 8 | } -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/tests/__init__.py -------------------------------------------------------------------------------- /tests/apis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/tests/apis/__init__.py -------------------------------------------------------------------------------- /tests/apis/test_captions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import responses 5 | 6 | import pyyoutube 7 | 8 | 9 | class ApiCaptionsTest(unittest.TestCase): 10 | BASE_PATH = "testdata/apidata/captions/" 11 | BASE_URL = "https://www.googleapis.com/youtube/v3/captions" 12 | 13 | with open(BASE_PATH + "captions_by_video.json", "rb") as f: 14 | CAPTIONS_BY_VIDEO = json.loads(f.read().decode("utf-8")) 15 | with open(BASE_PATH + "captions_filter_by_id.json", "rb") as f: 16 | CAPTIONS_FILTER_ID = json.loads(f.read().decode("utf-8")) 17 | 18 | def setUp(self) -> None: 19 | self.api_with_access_token = pyyoutube.Api(access_token="token") 20 | 21 | def testGetCaptionByVideo(self) -> None: 22 | video_id = "oHR3wURdJ94" 23 | 24 | # test parts 25 | with self.assertRaises(pyyoutube.PyYouTubeException): 26 | self.api_with_access_token.get_captions_by_video( 27 | video_id=video_id, 28 | parts="id,not_part", 29 | ) 30 | 31 | # test by video 32 | with responses.RequestsMock() as m: 33 | m.add("GET", self.BASE_URL, json=self.CAPTIONS_BY_VIDEO) 34 | 35 | res = self.api_with_access_token.get_captions_by_video( 36 | video_id=video_id, 37 | parts="id,snippet", 38 | return_json=True, 39 | ) 40 | self.assertEqual(len(res["items"]), 2) 41 | self.assertEqual(res["items"][0]["snippet"]["videoId"], video_id) 42 | 43 | # test filter id 44 | with responses.RequestsMock() as m: 45 | m.add("GET", self.BASE_URL, json=self.CAPTIONS_FILTER_ID) 46 | 47 | res = self.api_with_access_token.get_captions_by_video( 48 | video_id=video_id, 49 | parts=["id", "snippet"], 50 | caption_id="SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I", 51 | ) 52 | 53 | self.assertEqual(len(res.items), 1) 54 | self.assertEqual(res.items[0].snippet.videoId, video_id) 55 | -------------------------------------------------------------------------------- /tests/apis/test_categories.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import responses 5 | 6 | import pyyoutube 7 | 8 | 9 | class ApiVideoCategoryTest(unittest.TestCase): 10 | BASE_PATH = "testdata/apidata/categories/" 11 | BASE_URL = "https://www.googleapis.com/youtube/v3/videoCategories" 12 | 13 | with open(BASE_PATH + "video_category_single.json", "rb") as f: 14 | VIDEO_CATEGORY_SINGLE = json.loads(f.read().decode("utf-8")) 15 | with open(BASE_PATH + "video_category_multi.json", "rb") as f: 16 | VIDEO_CATEGORY_MULTI = json.loads(f.read().decode("utf-8")) 17 | with open(BASE_PATH + "video_category_by_region.json", "rb") as f: 18 | VIDEO_CATEGORY_BY_REGION = json.loads(f.read().decode("utf-8")) 19 | 20 | def setUp(self) -> None: 21 | self.api = pyyoutube.Api(api_key="api key") 22 | 23 | def testGetVideoCategories(self) -> None: 24 | # test params 25 | with self.assertRaises(pyyoutube.PyYouTubeException): 26 | self.api.get_video_categories() 27 | # test parts 28 | with self.assertRaises(pyyoutube.PyYouTubeException): 29 | self.api.get_video_categories(category_id="id", parts="id,not_part") 30 | 31 | with responses.RequestsMock() as m: 32 | m.add("GET", self.BASE_URL, json=self.VIDEO_CATEGORY_SINGLE) 33 | m.add("GET", self.BASE_URL, json=self.VIDEO_CATEGORY_MULTI) 34 | m.add("GET", self.BASE_URL, json=self.VIDEO_CATEGORY_BY_REGION) 35 | 36 | res_by_single = self.api.get_video_categories( 37 | category_id="17", 38 | parts=["snippet"], 39 | return_json=True, 40 | ) 41 | self.assertEqual(res_by_single["kind"], "youtube#videoCategoryListResponse") 42 | self.assertEqual(len(res_by_single["items"]), 1) 43 | self.assertEqual(res_by_single["items"][0]["id"], "17") 44 | 45 | res_by_multi = self.api.get_video_categories( 46 | category_id=["17", "18"], 47 | parts="snippet", 48 | ) 49 | self.assertEqual(len(res_by_multi.items), 2) 50 | self.assertEqual(res_by_multi.items[1].id, "18") 51 | 52 | res_by_region = self.api.get_video_categories( 53 | region_code="US", 54 | parts="snippet", 55 | ) 56 | self.assertEqual(len(res_by_region.items), 32) 57 | self.assertEqual(res_by_region.items[0].id, "1") 58 | -------------------------------------------------------------------------------- /tests/apis/test_i18ns.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import responses 5 | import pyyoutube 6 | 7 | 8 | class ApiI18nTest(unittest.TestCase): 9 | BASE_PATH = "testdata/apidata/i18ns/" 10 | REGION_URL = "https://www.googleapis.com/youtube/v3/i18nRegions" 11 | LANGUAGE_URL = "https://www.googleapis.com/youtube/v3/i18nLanguages" 12 | 13 | with open(BASE_PATH + "regions_res.json", "rb") as f: 14 | REGIONS_RES = json.loads(f.read().decode("utf-8")) 15 | with open(BASE_PATH + "language_res.json", "rb") as f: 16 | LANGUAGE_RES = json.loads(f.read().decode("utf-8")) 17 | 18 | def setUp(self) -> None: 19 | self.api = pyyoutube.Api(api_key="api key") 20 | 21 | def testGetI18nRegions(self) -> None: 22 | with responses.RequestsMock() as m: 23 | m.add("GET", self.REGION_URL, json=self.REGIONS_RES) 24 | 25 | regions = self.api.get_i18n_regions(parts=["snippet"]) 26 | self.assertEqual(regions.kind, "youtube#i18nRegionListResponse") 27 | self.assertEqual(len(regions.items), 4) 28 | self.assertEqual(regions.items[0].id, "VE") 29 | 30 | regions_json = self.api.get_i18n_regions(return_json=True) 31 | self.assertEqual(len(regions_json["items"]), 4) 32 | 33 | def testGetI18nLanguages(self) -> None: 34 | with responses.RequestsMock() as m: 35 | m.add("GET", self.LANGUAGE_URL, json=self.LANGUAGE_RES) 36 | 37 | languages = self.api.get_i18n_languages(parts=["snippet"]) 38 | self.assertEqual(len(languages.items), 5) 39 | self.assertEqual(languages.items[0].id, "zh-CN") 40 | 41 | languages_json = self.api.get_i18n_languages(return_json=True) 42 | self.assertEqual(len(languages_json["items"]), 5) 43 | -------------------------------------------------------------------------------- /tests/apis/test_members.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import responses 5 | import pyyoutube 6 | 7 | 8 | class ApiMembersTest(unittest.TestCase): 9 | BASE_PATH = "testdata/apidata/members/" 10 | MEMBERS_URL = "https://www.googleapis.com/youtube/v3/members" 11 | MEMBERSHIP_LEVEL_URL = "https://www.googleapis.com/youtube/v3/membershipsLevels" 12 | 13 | with open(BASE_PATH + "members_data.json", "rb") as f: 14 | MEMBERS_RES = json.loads(f.read().decode("utf-8")) 15 | with open(BASE_PATH + "membership_levels.json", "rb") as f: 16 | MEMBERSHIP_LEVEL_RES = json.loads(f.read().decode("utf-8")) 17 | 18 | def setUp(self) -> None: 19 | self.api = pyyoutube.Api(access_token="Authorize token") 20 | 21 | def testGetMembers(self) -> None: 22 | with responses.RequestsMock() as m: 23 | m.add("GET", self.MEMBERS_URL, json=self.MEMBERS_RES) 24 | 25 | members = self.api.get_members(parts=["snippet"]) 26 | self.assertEqual(members.kind, "youtube#memberListResponse") 27 | self.assertEqual(len(members.items), 2) 28 | 29 | members_json = self.api.get_members( 30 | page_token="token", 31 | count=None, 32 | has_access_to_level="high", 33 | filter_by_member_channel_id="id", 34 | return_json=True, 35 | ) 36 | self.assertEqual(len(members_json["items"]), 2) 37 | 38 | def testGetMembershipLevels(self) -> None: 39 | with responses.RequestsMock() as m: 40 | m.add("GET", self.MEMBERSHIP_LEVEL_URL, json=self.MEMBERSHIP_LEVEL_RES) 41 | 42 | membership_levels = self.api.get_membership_levels(parts=["id", "snippet"]) 43 | self.assertEqual( 44 | membership_levels.kind, "youtube#membershipsLevelListResponse" 45 | ) 46 | self.assertEqual(len(membership_levels.items), 2) 47 | 48 | membership_levels_json = self.api.get_membership_levels(return_json=True) 49 | self.assertEqual(len(membership_levels_json["items"]), 2) 50 | -------------------------------------------------------------------------------- /tests/apis/test_video_abuse_reason.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import responses 5 | import pyyoutube 6 | 7 | 8 | class ApiVideoAbuseReason(unittest.TestCase): 9 | BASE_PATH = "testdata/apidata/abuse_reasons/" 10 | BASE_URL = "https://www.googleapis.com/youtube/v3/videoAbuseReportReasons" 11 | 12 | with open(BASE_PATH + "abuse_reason.json", "rb") as f: 13 | ABUSE_REASON_RES = json.loads(f.read().decode("utf-8")) 14 | 15 | def setUp(self) -> None: 16 | self.api_with_token = pyyoutube.Api(access_token="access token") 17 | 18 | def testGetVideoAbuseReportReason(self) -> None: 19 | with responses.RequestsMock() as m: 20 | m.add("GET", self.BASE_URL, json=self.ABUSE_REASON_RES) 21 | 22 | abuse_res = self.api_with_token.get_video_abuse_report_reason( 23 | parts=["id", "snippet"], 24 | ) 25 | 26 | self.assertEqual( 27 | abuse_res.kind, "youtube#videoAbuseReportReasonListResponse" 28 | ) 29 | self.assertEqual(len(abuse_res.items), 3) 30 | 31 | abuse_res_json = self.api_with_token.get_video_abuse_report_reason( 32 | return_json=True 33 | ) 34 | 35 | self.assertEqual(len(abuse_res_json["items"]), 3) 36 | -------------------------------------------------------------------------------- /tests/clients/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/tests/clients/__init__.py -------------------------------------------------------------------------------- /tests/clients/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base class 3 | """ 4 | 5 | 6 | class BaseTestCase: 7 | BASE_PATH = "testdata/apidata" 8 | BASE_URL = "https://www.googleapis.com/youtube/v3" 9 | RESOURCE = "CHANNELS" 10 | 11 | @property 12 | def url(self): 13 | return f"{self.BASE_URL}/{self.RESOURCE}" 14 | 15 | def load_json(self, filename, helpers): 16 | return helpers.load_json(f"{self.BASE_PATH}/{filename}") 17 | -------------------------------------------------------------------------------- /tests/clients/test_activities.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import responses 3 | 4 | from .base import BaseTestCase 5 | from pyyoutube.error import PyYouTubeException 6 | 7 | 8 | class TestActivitiesResource(BaseTestCase): 9 | RESOURCE = "activities" 10 | 11 | def test_list(self, helpers, authed_cli): 12 | with pytest.raises(PyYouTubeException): 13 | authed_cli.activities.list() 14 | 15 | with responses.RequestsMock() as m: 16 | m.add( 17 | method="GET", 18 | url=self.url, 19 | json=self.load_json( 20 | "activities/activities_by_channel_p1.json", helpers 21 | ), 22 | ) 23 | res = authed_cli.activities.list( 24 | parts=["id", "snippet"], 25 | channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", 26 | max_results=10, 27 | ) 28 | assert len(res.items) == 10 29 | assert authed_cli.activities.access_token == "access token" 30 | 31 | res = authed_cli.activities.list( 32 | parts=["id", "snippet"], mine=True, max_results=10 33 | ) 34 | assert res.items[0].snippet.type == "upload" 35 | -------------------------------------------------------------------------------- /tests/clients/test_channel_banners.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for channel banners 3 | """ 4 | 5 | import io 6 | 7 | from .base import BaseTestCase 8 | from pyyoutube.media import Media 9 | 10 | 11 | class TestChannelBanners(BaseTestCase): 12 | def test_insert(self, helpers, authed_cli): 13 | media = Media(fd=io.StringIO("jpg content"), mimetype="image/jpeg") 14 | upload = authed_cli.channelBanners.insert(media=media) 15 | 16 | assert upload.resumable_progress == 0 17 | -------------------------------------------------------------------------------- /tests/clients/test_i18n.py: -------------------------------------------------------------------------------- 1 | import responses 2 | 3 | from .base import BaseTestCase 4 | 5 | 6 | class TestI18nLanguagesResource(BaseTestCase): 7 | RESOURCE = "i18nLanguages" 8 | 9 | def test_list(self, helpers, key_cli): 10 | with responses.RequestsMock() as m: 11 | m.add( 12 | method="GET", 13 | url=self.url, 14 | json=self.load_json("i18ns/language_res.json", helpers), 15 | ) 16 | res = key_cli.i18nLanguages.list( 17 | parts=["snippet"], 18 | ) 19 | assert res.items[0].snippet.name == "Chinese" 20 | 21 | 22 | class TestI18nRegionsResource(BaseTestCase): 23 | RESOURCE = "i18nRegions" 24 | 25 | def test_list(self, helpers, key_cli): 26 | with responses.RequestsMock() as m: 27 | m.add( 28 | method="GET", 29 | url=self.url, 30 | json=self.load_json("i18ns/regions_res.json", helpers), 31 | ) 32 | res = key_cli.i18nRegions.list( 33 | parts=["snippet"], 34 | ) 35 | assert res.items[0].snippet.name == "Venezuela" 36 | -------------------------------------------------------------------------------- /tests/clients/test_members.py: -------------------------------------------------------------------------------- 1 | import responses 2 | 3 | from .base import BaseTestCase 4 | 5 | 6 | class TestMembersResource(BaseTestCase): 7 | RESOURCE = "members" 8 | 9 | def test_list(self, helpers, authed_cli): 10 | with responses.RequestsMock() as m: 11 | m.add( 12 | method="GET", 13 | url=self.url, 14 | json=self.load_json("members/members_data.json", helpers), 15 | ) 16 | 17 | res = authed_cli.members.list( 18 | parts=["snippet"], 19 | mode="all_current", 20 | max_results=5, 21 | ) 22 | assert len(res.items) == 2 23 | -------------------------------------------------------------------------------- /tests/clients/test_membership_levels.py: -------------------------------------------------------------------------------- 1 | import responses 2 | 3 | from .base import BaseTestCase 4 | 5 | 6 | class TestMembershipLevelsResource(BaseTestCase): 7 | RESOURCE = "membershipsLevels" 8 | 9 | def test_list(self, helpers, authed_cli): 10 | with responses.RequestsMock() as m: 11 | m.add( 12 | method="GET", 13 | url=self.url, 14 | json=self.load_json("members/membership_levels.json", helpers), 15 | ) 16 | 17 | res = authed_cli.membershipsLevels.list( 18 | parts=["id", "snippet"], 19 | ) 20 | assert len(res.items) == 2 21 | -------------------------------------------------------------------------------- /tests/clients/test_search.py: -------------------------------------------------------------------------------- 1 | import responses 2 | 3 | from .base import BaseTestCase 4 | 5 | 6 | class TestSearchResource(BaseTestCase): 7 | RESOURCE = "search" 8 | 9 | def test_list(self, helpers, authed_cli, key_cli): 10 | with responses.RequestsMock() as m: 11 | m.add( 12 | method="GET", 13 | url=self.url, 14 | json=self.load_json("search/search_by_developer.json", helpers), 15 | ) 16 | 17 | res = authed_cli.search.list( 18 | parts=["snippet"], 19 | for_content_owner=True, 20 | ) 21 | assert res.items[0].id.videoId == "WuyFniRMrxY" 22 | 23 | res = authed_cli.search.list( 24 | for_developer=True, 25 | max_results=5, 26 | ) 27 | assert len(res.items) == 5 28 | 29 | with responses.RequestsMock() as m: 30 | m.add( 31 | method="GET", 32 | url=self.url, 33 | json=self.load_json("search/search_by_mine.json", helpers), 34 | ) 35 | res = authed_cli.search.list(for_mine=True, max_results=5) 36 | assert res.items[0].snippet.channelId == "UCa-vrCLQHviTOVnEKDOdetQ" 37 | 38 | with responses.RequestsMock() as m: 39 | m.add( 40 | method="GET", 41 | url=self.url, 42 | json=self.load_json("search/search_by_related_video.json", helpers), 43 | ) 44 | res = authed_cli.search.list( 45 | related_to_video_id="Ks-_Mh1QhMc", 46 | region_code="US", 47 | relevance_language="en", 48 | safe_search="moderate", 49 | max_results=5, 50 | ) 51 | assert res.items[0].id.videoId == "eIho2S0ZahI" 52 | 53 | with responses.RequestsMock() as m: 54 | m.add( 55 | method="GET", 56 | url=self.url, 57 | json=self.load_json("search/search_by_keywords_p1.json", helpers), 58 | ) 59 | res = key_cli.search.list( 60 | q="surfing", 61 | parts=["snippet"], 62 | count=25, 63 | ) 64 | assert len(res.items) == 25 65 | -------------------------------------------------------------------------------- /tests/clients/test_thumbnails.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for thumbnails. 3 | """ 4 | 5 | import io 6 | 7 | from .base import BaseTestCase 8 | from pyyoutube.media import Media 9 | 10 | 11 | class TestThumbnailsResource(BaseTestCase): 12 | RESOURCE = "thumbnails" 13 | 14 | def test_set(self, authed_cli): 15 | video_id = "zxTVeyG1600" 16 | media = Media(fd=io.StringIO("jpeg content"), mimetype="image/jpeg") 17 | 18 | upload = authed_cli.thumbnails.set( 19 | video_id=video_id, 20 | media=media, 21 | ) 22 | assert upload.resumable_progress == 0 23 | -------------------------------------------------------------------------------- /tests/clients/test_video_abuse_report_reasons.py: -------------------------------------------------------------------------------- 1 | import responses 2 | 3 | from .base import BaseTestCase 4 | 5 | 6 | class TestVideoAbuseReportReasonsResource(BaseTestCase): 7 | RESOURCE = "videoAbuseReportReasons" 8 | 9 | def test_list(self, helpers, authed_cli): 10 | with responses.RequestsMock() as m: 11 | m.add( 12 | method="GET", 13 | url=self.url, 14 | json=self.load_json("abuse_reasons/abuse_reason.json", helpers), 15 | ) 16 | 17 | res = authed_cli.videoAbuseReportReasons.list( 18 | parts=["id", "snippet"], 19 | ) 20 | assert res.items[0].id == "N" 21 | -------------------------------------------------------------------------------- /tests/clients/test_video_categories.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import responses 3 | 4 | from .base import BaseTestCase 5 | from pyyoutube.error import PyYouTubeException 6 | 7 | 8 | class TestVideoCategoriesResource(BaseTestCase): 9 | RESOURCE = "videoCategories" 10 | 11 | def test_list(self, helpers, key_cli): 12 | with pytest.raises(PyYouTubeException): 13 | key_cli.videoCategories.list() 14 | 15 | with responses.RequestsMock() as m: 16 | m.add( 17 | method="GET", 18 | url=self.url, 19 | json=self.load_json( 20 | "categories/video_category_by_region.json", helpers 21 | ), 22 | ) 23 | res = key_cli.videoCategories.list( 24 | parts=["snippet"], 25 | region_code="US", 26 | ) 27 | assert res.items[0].snippet.title == "Film & Animation" 28 | 29 | with responses.RequestsMock() as m: 30 | m.add( 31 | method="GET", 32 | url=self.url, 33 | json=self.load_json("categories/video_category_multi.json", helpers), 34 | ) 35 | res = key_cli.videoCategories.list( 36 | parts=["snippet"], 37 | category_id=["17", "18"], 38 | ) 39 | assert len(res.items) == 2 40 | -------------------------------------------------------------------------------- /tests/clients/test_watermarks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for watermarks. 3 | """ 4 | 5 | import io 6 | 7 | import pytest 8 | import responses 9 | 10 | import pyyoutube.models as mds 11 | from .base import BaseTestCase 12 | from pyyoutube.error import PyYouTubeException 13 | from pyyoutube.media import Media 14 | 15 | 16 | class TestWatermarksResource(BaseTestCase): 17 | RESOURCE = "watermarks" 18 | 19 | def test_set(self, authed_cli): 20 | body = mds.Watermark( 21 | timing=mds.WatermarkTiming( 22 | type="offsetFromStart", 23 | offsetMs=1000, 24 | durationMs=3000, 25 | ), 26 | position=mds.WatermarkPosition( 27 | type="corner", 28 | cornerPosition="topRight", 29 | ), 30 | ) 31 | media = Media(fd=io.StringIO("image content"), mimetype="image/jpeg") 32 | 33 | upload = authed_cli.watermarks.set( 34 | channel_id="id", 35 | body=body, 36 | media=media, 37 | ) 38 | assert upload.resumable_progress == 0 39 | 40 | def test_unset(self, helpers, authed_cli): 41 | with responses.RequestsMock() as m: 42 | m.add(method="POST", url=f"{self.url}/unset", status=204) 43 | assert authed_cli.watermarks.unset(channel_id="id") 44 | 45 | with pytest.raises(PyYouTubeException): 46 | with responses.RequestsMock() as m: 47 | m.add( 48 | method="POST", 49 | url=f"{self.url}/unset", 50 | status=403, 51 | json=self.load_json("error_permission_resp.json", helpers), 52 | ) 53 | assert authed_cli.watermarks.unset(channel_id="id") 54 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from pyyoutube import Client 6 | 7 | 8 | class Helpers: 9 | @staticmethod 10 | def load_json(filename): 11 | with open(filename, "rb") as f: 12 | return json.loads(f.read().decode("utf-8")) 13 | 14 | @staticmethod 15 | def load_file_binary(filename): 16 | with open(filename, "rb") as f: 17 | return f.read() 18 | 19 | 20 | @pytest.fixture 21 | def helpers(): 22 | return Helpers() 23 | 24 | 25 | @pytest.fixture(scope="class") 26 | def authed_cli(): 27 | return Client(access_token="access token") 28 | 29 | 30 | @pytest.fixture(scope="class") 31 | def key_cli(): 32 | return Client(api_key="api key") 33 | -------------------------------------------------------------------------------- /tests/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/tests/models/__init__.py -------------------------------------------------------------------------------- /tests/models/test_abuse_reason.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import pyyoutube.models as models 5 | 6 | 7 | class AbuseReasonModelTest(unittest.TestCase): 8 | BASE_PATH = "testdata/modeldata/abuse_report_reason/" 9 | 10 | with open(BASE_PATH + "abuse_reason.json", "rb") as f: 11 | ABUSE_REASON = json.loads(f.read().decode("utf-8")) 12 | with open(BASE_PATH + "abuse_reason_res.json", "rb") as f: 13 | ABUSE_REASON_RES = json.loads(f.read().decode("utf-8")) 14 | 15 | def testAbuseReason(self) -> None: 16 | m = models.VideoAbuseReportReason.from_dict(self.ABUSE_REASON) 17 | 18 | self.assertEqual(m.id, "N") 19 | self.assertEqual(m.snippet.label, "Sex or nudity") 20 | self.assertEqual(len(m.snippet.secondaryReasons), 3) 21 | 22 | def testAbuseReasonResponse(self) -> None: 23 | m = models.VideoAbuseReportReasonListResponse.from_dict(self.ABUSE_REASON_RES) 24 | 25 | self.assertEqual(m.kind, "youtube#videoAbuseReportReasonListResponse") 26 | self.assertEqual(len(m.items), 3) 27 | -------------------------------------------------------------------------------- /tests/models/test_activities.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import pyyoutube.models as models 5 | 6 | 7 | class ActivityModelTest(unittest.TestCase): 8 | BASE_PATH = "testdata/modeldata/activities/" 9 | 10 | with open(BASE_PATH + "activity_contentDetails.json", "rb") as f: 11 | ACTIVITY_CONTENT_DETAILS = json.loads(f.read().decode("utf-8")) 12 | with open(BASE_PATH + "activity_snippet.json", "rb") as f: 13 | ACTIVITY_SNIPPET = json.loads(f.read().decode("utf-8")) 14 | with open(BASE_PATH + "activity.json", "rb") as f: 15 | ACTIVITY = json.loads(f.read().decode("utf-8")) 16 | with open(BASE_PATH + "activity_response.json", "rb") as f: 17 | ACTIVITY_RESPONSE = json.loads(f.read().decode("utf-8")) 18 | 19 | def testActivityContentDetails(self) -> None: 20 | m = models.ActivityContentDetails.from_dict(self.ACTIVITY_CONTENT_DETAILS) 21 | 22 | self.assertEqual(m.upload.videoId, "LDXYRzerjzU") 23 | 24 | def testActivitySnippet(self) -> None: 25 | m = models.ActivitySnippet.from_dict(self.ACTIVITY_SNIPPET) 26 | 27 | self.assertEqual(m.channelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw") 28 | self.assertEqual( 29 | m.thumbnails.default.url, "https://i.ytimg.com/vi/DQGSZTxLVrI/default.jpg" 30 | ) 31 | 32 | def testActivity(self) -> None: 33 | m = models.Activity.from_dict(self.ACTIVITY) 34 | 35 | self.assertEqual(m.snippet.channelId, "UCa-vrCLQHviTOVnEKDOdetQ") 36 | self.assertEqual(m.contentDetails.upload.videoId, "JE8xdDp5B8Q") 37 | 38 | def testActivityListResponse(self) -> None: 39 | m = models.ActivityListResponse.from_dict(self.ACTIVITY_RESPONSE) 40 | 41 | self.assertEqual(m.kind, "youtube#activityListResponse") 42 | self.assertEqual(m.pageInfo.totalResults, 2) 43 | self.assertEqual(len(m.items), 2) 44 | -------------------------------------------------------------------------------- /tests/models/test_auth_models.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | import pyyoutube.models as models 4 | 5 | 6 | class AuthModelTest(unittest.TestCase): 7 | BASE_PATH = "testdata/modeldata/users/" 8 | with open(BASE_PATH + "access_token.json", "rb") as f: 9 | ACCESS_TOKEN_INFO = json.loads(f.read().decode("utf-8")) 10 | with open(BASE_PATH + "user_profile.json", "rb") as f: 11 | USER_PROFILE_INFO = json.loads(f.read().decode("utf-8")) 12 | 13 | def testAccessToken(self) -> None: 14 | m = models.AccessToken.from_dict(self.ACCESS_TOKEN_INFO) 15 | 16 | self.assertEqual(m.access_token, "access_token") 17 | 18 | def testUserProfile(self) -> None: 19 | m = models.UserProfile.from_dict(self.USER_PROFILE_INFO) 20 | 21 | self.assertEqual(m.id, "12345678910") 22 | 23 | origin_data = json.dumps(self.USER_PROFILE_INFO, sort_keys=True) 24 | d = m.to_json(sort_keys=True, allow_nan=False) 25 | self.assertEqual(origin_data, d) 26 | -------------------------------------------------------------------------------- /tests/models/test_captions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import pyyoutube.models as models 5 | 6 | 7 | class CaptionModelTest(unittest.TestCase): 8 | BASE_PATH = "testdata/modeldata/captions/" 9 | 10 | with open(BASE_PATH + "caption_snippet.json", "rb") as f: 11 | CAPTION_SNIPPET = json.loads(f.read().decode("utf-8")) 12 | with open(BASE_PATH + "caption.json", "rb") as f: 13 | CAPTION_INFO = json.loads(f.read().decode("utf-8")) 14 | with open(BASE_PATH + "caption_response.json", "rb") as f: 15 | CAPTION_RESPONSE = json.loads(f.read().decode("utf-8")) 16 | 17 | def testCaptionSnippet(self): 18 | m = models.CaptionSnippet.from_dict(self.CAPTION_SNIPPET) 19 | 20 | self.assertEqual(m.videoId, "oHR3wURdJ94") 21 | self.assertEqual( 22 | m.string_to_datetime(m.lastUpdated).isoformat(), 23 | "2020-01-14T09:40:49.981000+00:00", 24 | ) 25 | 26 | def testCaption(self): 27 | m = models.Caption.from_dict(self.CAPTION_INFO) 28 | 29 | self.assertEqual(m.id, "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I") 30 | self.assertEqual(m.snippet.videoId, "oHR3wURdJ94") 31 | 32 | def testCaptionListResponse(self): 33 | m = models.CaptionListResponse.from_dict(self.CAPTION_RESPONSE) 34 | 35 | self.assertEqual(m.kind, "youtube#captionListResponse") 36 | self.assertEqual(len(m.items), 2) 37 | self.assertEqual(m.items[0].id, "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I") 38 | -------------------------------------------------------------------------------- /tests/models/test_category.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import pyyoutube.models as models 5 | 6 | 7 | class CategoryModelTest(unittest.TestCase): 8 | BASE_PATH = "testdata/modeldata/categories/" 9 | 10 | with open(BASE_PATH + "video_category_info.json", "rb") as f: 11 | VIDEO_CATEGORY_INFO = json.loads(f.read().decode("utf-8")) 12 | with open(BASE_PATH + "video_category_response.json", "rb") as f: 13 | VIDEO_CATEGORY_RESPONSE = json.loads(f.read().decode("utf-8")) 14 | with open(BASE_PATH + "guide_category_info.json", "rb") as f: 15 | GUIDE_CATEGORY_INFO = json.loads(f.read().decode("utf-8")) 16 | with open(BASE_PATH + "guide_category_response.json", "rb") as f: 17 | GUIDE_CATEGORY_RESPONSE = json.loads(f.read().decode("utf-8")) 18 | 19 | def testVideoCategory(self) -> None: 20 | m = models.VideoCategory.from_dict(self.VIDEO_CATEGORY_INFO) 21 | self.assertEqual(m.id, "17") 22 | self.assertEqual(m.snippet.title, "Sports") 23 | 24 | def testVideoCategoryListResponse(self) -> None: 25 | m = models.VideoCategoryListResponse.from_dict(self.VIDEO_CATEGORY_RESPONSE) 26 | self.assertEqual(m.kind, "youtube#videoCategoryListResponse") 27 | self.assertEqual(len(m.items), 1) 28 | self.assertEqual(m.items[0].id, "17") 29 | -------------------------------------------------------------------------------- /tests/models/test_channel_sections.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import pyyoutube.models as models 5 | 6 | 7 | class ChannelSectionModelTest(unittest.TestCase): 8 | BASE_PATH = "testdata/modeldata/channel_sections/" 9 | 10 | with open(BASE_PATH + "channel_section_info.json", "rb") as f: 11 | CHANNEL_SECTION_INFO = json.loads(f.read().decode("utf-8")) 12 | with open(BASE_PATH + "channel_section_response.json", "rb") as f: 13 | CHANNEL_SECTION_RESPONSE = json.loads(f.read().decode("utf-8")) 14 | 15 | def testChannelSection(self) -> None: 16 | m = models.ChannelSection.from_dict(self.CHANNEL_SECTION_INFO) 17 | 18 | self.assertEqual(m.id, "UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE") 19 | self.assertEqual(m.snippet.type, "multipleChannels") 20 | self.assertEqual(len(m.contentDetails.channels), 16) 21 | 22 | def testChannelSectionResponse(self) -> None: 23 | m = models.ChannelSectionResponse.from_dict(self.CHANNEL_SECTION_RESPONSE) 24 | 25 | self.assertEqual(m.kind, "youtube#channelSectionListResponse") 26 | self.assertEqual(len(m.items), 10) 27 | -------------------------------------------------------------------------------- /tests/models/test_i18n_models.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import unittest 4 | 5 | import pyyoutube.models as models 6 | 7 | 8 | class I18nModelTest(unittest.TestCase): 9 | BASE_PATH = "testdata/modeldata/i18ns/" 10 | 11 | with open(BASE_PATH + "region_info.json", "rb") as f: 12 | REGION_INFO = json.loads(f.read().decode("utf-8")) 13 | with open(BASE_PATH + "region_res.json", "rb") as f: 14 | REGION_RES = json.loads(f.read().decode("utf-8")) 15 | with open(BASE_PATH + "language_info.json", "rb") as f: 16 | LANGUAGE_INFO = json.loads(f.read().decode("utf-8")) 17 | with open(BASE_PATH + "language_res.json", "rb") as f: 18 | LANGUAGE_RES = json.loads(f.read().decode("utf-8")) 19 | 20 | def testI18nRegion(self) -> None: 21 | m = models.I18nRegion.from_dict(self.REGION_INFO) 22 | 23 | self.assertEqual(m.id, "DZ") 24 | self.assertEqual(m.snippet.gl, "DZ") 25 | 26 | def testI18nRegionResponse(self) -> None: 27 | m = models.I18nRegionListResponse.from_dict(self.REGION_RES) 28 | 29 | self.assertEqual(m.kind, "youtube#i18nRegionListResponse") 30 | self.assertEqual(len(m.items), 2) 31 | 32 | def testI18nLanguage(self) -> None: 33 | m = models.I18nLanguage.from_dict(self.LANGUAGE_INFO) 34 | 35 | self.assertEqual(m.id, "af") 36 | self.assertEqual(m.snippet.hl, "af") 37 | 38 | def testI18nLanguageResponse(self) -> None: 39 | m = models.I18nRegionListResponse.from_dict(self.LANGUAGE_RES) 40 | 41 | self.assertEqual(m.kind, "youtube#i18nLanguageListResponse") 42 | self.assertEqual(len(m.items), 2) 43 | -------------------------------------------------------------------------------- /tests/models/test_members.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import pyyoutube.models as models 5 | 6 | 7 | class MemberModelTest(unittest.TestCase): 8 | BASE_PATH = "testdata/modeldata/members/" 9 | 10 | with open(BASE_PATH + "member_info.json", "rb") as f: 11 | MEMBER_INFO = json.loads(f.read().decode("utf-8")) 12 | 13 | def testMember(self) -> None: 14 | m = models.Member.from_dict(self.MEMBER_INFO) 15 | 16 | self.assertEqual(m.kind, "youtube#member") 17 | self.assertEqual(m.snippet.memberDetails.channelId, "UCa-vrCLQHviTOVnEKDOdetQ") 18 | self.assertEqual(m.snippet.membershipsDetails.highestAccessibleLevel, "string") 19 | 20 | 21 | class MembershipLevelModelTest(unittest.TestCase): 22 | BASE_PATH = "testdata/modeldata/members/" 23 | 24 | with open(BASE_PATH + "membership_level.json", "rb") as f: 25 | MEMBERSHIP_LEVEL_INFO = json.loads(f.read().decode("utf-8")) 26 | 27 | def testMembershipLevel(self) -> None: 28 | m = models.MembershipsLevel.from_dict(self.MEMBERSHIP_LEVEL_INFO) 29 | 30 | self.assertEqual(m.kind, "youtube#membershipsLevel") 31 | self.assertEqual(m.snippet.levelDetails.displayName, "high") 32 | -------------------------------------------------------------------------------- /tests/models/test_playlist.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import pyyoutube.models as models 5 | 6 | 7 | class PlaylistModelTest(unittest.TestCase): 8 | BASE_PATH = "testdata/modeldata/playlists/" 9 | 10 | with open(BASE_PATH + "playlist_content_details.json", "rb") as f: 11 | CONTENT_DETAILS_INFO = json.loads(f.read().decode("utf-8")) 12 | with open(BASE_PATH + "playlist_snippet.json", "rb") as f: 13 | SNIPPET_INFO = json.loads(f.read().decode("utf-8")) 14 | with open(BASE_PATH + "playlist_status.json", "rb") as f: 15 | STATUS_INFO = json.loads(f.read().decode("utf-8")) 16 | with open(BASE_PATH + "playlist_info.json", "rb") as f: 17 | PLAYLIST_INFO = json.loads(f.read().decode("utf-8")) 18 | with open(BASE_PATH + "playlist_api_response.json", "rb") as f: 19 | PLAYLIST_RESPONSE_INFO = json.loads(f.read().decode("utf-8")) 20 | 21 | def testPlayListContentDetails(self) -> None: 22 | m = models.PlaylistContentDetails.from_dict(self.CONTENT_DETAILS_INFO) 23 | 24 | self.assertEqual(m.itemCount, 4) 25 | 26 | def testPlayListSnippet(self) -> None: 27 | m = models.PlaylistSnippet.from_dict(self.SNIPPET_INFO) 28 | 29 | self.assertEqual( 30 | m.string_to_datetime(m.publishedAt).isoformat(), "2019-05-16T18:46:20+00:00" 31 | ) 32 | self.assertEqual(m.title, "Assistant on Air") 33 | self.assertEqual( 34 | m.thumbnails.default.url, "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg" 35 | ) 36 | self.assertEqual(m.localized.title, "Assistant on Air") 37 | 38 | def testPlayListStatus(self) -> None: 39 | m = models.PlaylistStatus.from_dict(self.STATUS_INFO) 40 | 41 | self.assertEqual(m.privacyStatus, "public") 42 | 43 | def testPlayList(self) -> None: 44 | m = models.Playlist.from_dict(self.PLAYLIST_INFO) 45 | 46 | self.assertEqual(m.id, "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp") 47 | self.assertEqual(m.player, None) 48 | self.assertEqual(m.snippet.title, "Assistant on Air") 49 | 50 | def testPlaylistListResponse(self) -> None: 51 | m = models.PlaylistListResponse.from_dict(self.PLAYLIST_RESPONSE_INFO) 52 | 53 | self.assertEqual(m.kind, "youtube#playlistListResponse") 54 | self.assertEqual(m.pageInfo.totalResults, 416) 55 | self.assertEqual(m.items[0].id, "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp") 56 | -------------------------------------------------------------------------------- /tests/models/test_search_result.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import pyyoutube.models as models 5 | 6 | 7 | class SearchResultModelTest(unittest.TestCase): 8 | BASE_PATH = "testdata/modeldata/search_result/" 9 | 10 | with open(BASE_PATH + "search_result_id.json", "rb") as f: 11 | SEARCH_RES_ID_INFO = json.loads(f.read().decode("utf-8")) 12 | with open(BASE_PATH + "search_result_snippet.json", "rb") as f: 13 | SEARCH_RES_SNIPPET_INFO = json.loads(f.read().decode("utf-8")) 14 | with open(BASE_PATH + "search_result.json", "rb") as f: 15 | SEARCH_RES_INFO = json.loads(f.read().decode("utf-8")) 16 | with open(BASE_PATH + "search_result_api_response.json", "rb") as f: 17 | SEARCH_RES_API_INFO = json.loads(f.read().decode("utf-8")) 18 | 19 | def testSearchResultId(self): 20 | m = models.SearchResultId.from_dict(self.SEARCH_RES_ID_INFO) 21 | self.assertEqual(m.kind, "youtube#playlist") 22 | 23 | def testSearchResultSnippet(self): 24 | m = models.SearchResultSnippet.from_dict(self.SEARCH_RES_SNIPPET_INFO) 25 | 26 | self.assertEqual(m.channelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw") 27 | self.assertEqual( 28 | m.string_to_datetime(m.publishedAt).isoformat(), 29 | "2016-03-30T16:59:12+00:00", 30 | ) 31 | self.assertEqual( 32 | m.thumbnails.default.url, "https://i.ytimg.com/vi/cKxRvEZd3Mw/default.jpg" 33 | ) 34 | 35 | def testSearchResult(self): 36 | m = models.SearchResult.from_dict(self.SEARCH_RES_INFO) 37 | self.assertEqual(m.kind, "youtube#searchResult") 38 | self.assertEqual(m.id.videoId, "fq4N0hgOWzU") 39 | 40 | def testSearchListResponse(self): 41 | m = models.SearchListResponse.from_dict(self.SEARCH_RES_API_INFO) 42 | 43 | self.assertEqual(m.kind, "youtube#searchListResponse") 44 | self.assertEqual(m.regionCode, "US") 45 | self.assertEqual(m.pageInfo.totalResults, 489126) 46 | self.assertEqual(len(m.items), 5) 47 | -------------------------------------------------------------------------------- /tests/test_error_handling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from requests import Response 4 | 5 | from pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException 6 | 7 | 8 | class ErrorTest(unittest.TestCase): 9 | BASE_PATH = "testdata/" 10 | with open(BASE_PATH + "error_response.json", "rb") as f: 11 | ERROR_DATA = f.read() 12 | 13 | with open(BASE_PATH + "error_response_simple.json", "rb") as f: 14 | ERROR_DATA_SIMPLE = f.read() 15 | 16 | def testResponseError(self) -> None: 17 | response = Response() 18 | response.status_code = 400 19 | response._content = self.ERROR_DATA 20 | 21 | ex = PyYouTubeException(response=response) 22 | 23 | self.assertEqual(ex.status_code, 400) 24 | self.assertEqual(ex.message, "Bad Request") 25 | self.assertEqual(ex.error_type, "YouTubeException") 26 | error_msg = "YouTubeException(status_code=400,message=Bad Request)" 27 | self.assertEqual(repr(ex), error_msg) 28 | self.assertTrue(str(ex), error_msg) 29 | 30 | def testResponseErrorSimple(self) -> None: 31 | response = Response() 32 | response.status_code = 400 33 | response._content = self.ERROR_DATA_SIMPLE 34 | 35 | ex = PyYouTubeException(response=response) 36 | self.assertEqual(ex.status_code, 400) 37 | 38 | def testErrorMessage(self): 39 | response = ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message="error") 40 | 41 | ex = PyYouTubeException(response=response) 42 | 43 | self.assertEqual(ex.status_code, 10000) 44 | self.assertEqual(ex.message, "error") 45 | self.assertEqual(ex.error_type, "PyYouTubeException") 46 | -------------------------------------------------------------------------------- /tests/test_youtube_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pyyoutube import youtube_utils as utils 4 | from pyyoutube.error import PyYouTubeException 5 | 6 | 7 | class UtilsTest(unittest.TestCase): 8 | def testDurationConvert(self): 9 | duration = "PT14H23M42S" 10 | self.assertEqual(utils.get_video_duration(duration), 51822) 11 | 12 | duration = "PT14H23M42" 13 | with self.assertRaises(PyYouTubeException): 14 | utils.get_video_duration(duration) 15 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sns-sdks/python-youtube/61d0a4dfa6cad3469e64a1b6733590659e5f9591/tests/utils/__init__.py -------------------------------------------------------------------------------- /tests/utils/test_params_checker.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import pyyoutube 4 | from pyyoutube.utils.params_checker import enf_comma_separated, enf_parts 5 | 6 | 7 | class ParamCheckerTest(unittest.TestCase): 8 | def testEnfCommaSeparated(self) -> None: 9 | self.assertIsNone(enf_comma_separated("id", None)) 10 | self.assertEqual(enf_comma_separated("id", "my_id"), "my_id") 11 | self.assertEqual(enf_comma_separated("id", "id1,id2"), "id1,id2") 12 | self.assertEqual(enf_comma_separated("id", ["id1", "id2"]), "id1,id2") 13 | self.assertEqual(enf_comma_separated("id", ("id1", "id2")), "id1,id2") 14 | self.assertTrue(enf_comma_separated("id", {"id1", "id2"})) 15 | 16 | with self.assertRaises(pyyoutube.PyYouTubeException): 17 | enf_comma_separated("id", 1) 18 | with self.assertRaises(pyyoutube.PyYouTubeException): 19 | enf_comma_separated("id", [None, None]) 20 | 21 | def testEnfParts(self) -> None: 22 | self.assertTrue(enf_parts(resource="channels", value=None)) 23 | self.assertTrue(enf_parts(resource="channels", value="id"), "id") 24 | self.assertTrue(enf_parts(resource="channels", value="id,snippet")) 25 | self.assertTrue(enf_parts(resource="channels", value=["id", "snippet"])) 26 | self.assertTrue(enf_parts(resource="channels", value=("id", "snippet"))) 27 | self.assertTrue(enf_parts(resource="channels", value={"id", "snippet"})) 28 | 29 | with self.assertRaises(pyyoutube.PyYouTubeException): 30 | enf_parts(resource="channels", value=1) 31 | 32 | with self.assertRaises(pyyoutube.PyYouTubeException): 33 | enf_parts(resource="channels", value="not_part") 34 | --------------------------------------------------------------------------------