Repository: sns-sdks/python-youtube Branch: master Commit: 1694c2c91820 Files: 309 Total size: 1.0 MB Directory structure: gitextract_o3azcgt3/ ├── .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/ │ │ ├── authorization.md │ │ ├── getting_started.md │ │ ├── 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 │ ├── oauth_refreshing.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_paid_product_placement_details.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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bumpversion.cfg ================================================ [bumpversion] current_version = 0.9.9 commit = True tag = True [bumpversion:file:pyyoutube/__version__.py] [bumpversion:file:pyproject.toml] ================================================ FILE: .coveragerc ================================================ [run] omit = pyyoutube/__version__.py ================================================ FILE: .github/hack/changelog.sh ================================================ #!/bin/sh MARKER_PREFIX="## Version" VERSION=$(echo "$1" | sed 's/^v//g') IFS='' found=0 while read -r "line"; do # If not found and matching heading if [ $found -eq 0 ] && echo "$line" | grep -q "$MARKER_PREFIX $VERSION"; then echo "$line" found=1 continue fi # If needed version if found, and reaching next delimter - stop if [ $found -eq 1 ] && echo "$line" | grep -q -E "$MARKER_PREFIX [[:digit:]]+\.[[:digit:]]+\.[[:digit:]]"; then found=0 break fi # Keep printing out lines as no other version delimiter found if [ $found -eq 1 ]; then echo "$line" fi done < CHANGELOG.md ================================================ FILE: .github/hack/version.sh ================================================ #!/bin/sh LATEST_TAG_REV=$(git rev-list --tags --max-count=1) LATEST_COMMIT_REV=$(git rev-list HEAD --max-count=1) if [ -n "$LATEST_TAG_REV" ]; then LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)") else LATEST_TAG="v0.0.0" fi if [ "$LATEST_TAG_REV" != "$LATEST_COMMIT_REV" ]; then echo "$LATEST_TAG+$(git rev-list HEAD --max-count=1 --abbrev-commit)" else echo "$LATEST_TAG" fi ================================================ FILE: .github/workflows/docs.yaml ================================================ name: Publish docs via GitHub Pages on: push: branches: - master jobs: build: name: Deploy docs runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Deploy docs uses: mhausenblas/mkdocs-deploy-gh-pages@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CONFIG_FILE: docs/mkdocs.yml EXTRA_PACKAGES: build-base ================================================ FILE: .github/workflows/release.yaml ================================================ name: Publish Pypi on: push: tags: - 'v*.*.*' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build and publish to pypi uses: JRubics/poetry-publish@v1.17 with: pypi_token: ${{ secrets.PYPI_TOKEN }} - name: Generate Changelog run: | VERSION=$(.github/hack/version.sh) .github/hack/changelog.sh $VERSION > NEW-VERSION-CHANGELOG.md - name: Publish uses: softprops/action-gh-release@v1 with: body_path: NEW-VERSION-CHANGELOG.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/test.yaml ================================================ name: Test on: pull_request: push: branches: - master jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] include: - python-version: '3.12' update-coverage: true steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ matrix.python-version }}-poetry-${{ hashFiles('pyproject.toml') }} - name: Install dependencies run: | python -m pip install --upgrade pip poetry poetry install - name: Test with pytest run: | poetry run pytest - name: Upload coverage to Codecov if: ${{ matrix.update-coverage }} uses: codecov/codecov-action@v5 with: file: ./coverage.xml fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} lint: name: black runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - name: Cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: lintenv-v2 - name: Install dependencies run: python -m pip install --upgrade pip black - name: Black test run: make lint-check ================================================ FILE: .gitignore ================================================ # Created by .ignore support plugin (hsz.mobi) ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock poetry.lock # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # PyCharm .idea/ # for git commitizen node_modules/ package-lock.json package.json ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. ## Version 0.9.9 (2026-04-17) ### What's New - Add new part field `paidProductPlacementDetail` for video resource. Thanks for [@sarod](https://github.com/sarod) - Add new method to subscribe or unsubscribe to a YouTube channel's push notifications via PubSubHubbub. - Upgrade to python 3.9 ## Version 0.9.8 (2025-08-22) ### What's New - Fix dependencies and update docs. Thanks for [@sagarvora](https://github.com/sagarvora) ## Version 0.9.7 (2024-10-28) ### What's New - Fix dependencies. ## Version 0.9.6 (2024-09-09) ### What's New -Add new part field `recordingDetails` for video resource. Thanks for [@vmx](https://github.com/vmx) ## Version 0.9.5 (2024-08-09) ### What's New - Make video regionRestriction fields to Optional. Thanks for [@pidi3000](https://github.com/pidi3000) - Modify some examples. Thanks for [@pidi3000](https://github.com/pidi3000) - fix enf_parts for part with whitespaces. Thanks for [@pidi3000](https://github.com/pidi3000) ## Version 0.9.4 (2024-02-18) ### What's New - Add new parameter `for_handle` to get channel by handle. - fix some wrong error message. ## Version 0.9.3 (2023-11-22) ### What's New - Add initial client with client_secret file. Thanks for [@pidi3000](https://github.com/pidi3000) ## Version 0.9.2 (2023-09-26) ### What's New - Add new parameter for search method - Mark some parameter or method to be deprecated. ## Version 0.9.1 (2023-07-19) ### What's New - upgrade poetry. Thanks for [@blaggacao](https://github.com/blaggacao) ## Version 0.9.0 (2022-12-26) ### What's New - Introduce new `Client` to operate YouTube DATA API. [#120](https://github.com/sns-sdks/python-youtube/issues/120). - More example to show library usage. ## Version 0.8.3 (2022-10-17) ### What's New - Add parts for video, thanks for [@Omer](https://github.com/dusking) ## Version 0.8.2 (2022-03-16) ### What's New - Update OAuthorize functions. - Update for examples. ## Version 0.8.1 (2021-05-14) ### Deprecation Detail at: https://developers.google.com/youtube/v3/revision_history#may-12,-2021 - Remove channel resource in brandingSettings for channel. - Remove localizations,targeting resource and some snippet resource for channelSection. - Remove tags in snippet for playlist. ### Broken Change Methods `get_channel_sections_by_channel`, `get_channel_section_by_id` has remove parameter `hl`. ## Version 0.8.0 ### Broken Change Modify the auth flow methods. ### What's New 1. add python3.9 tests 2. New docs ## Version 0.7.0 ### What's New 1. Add api methods for members and membership levels 2. Add more examples for api 3. Add fields for playlist item api 4. fix some. ## Version 0.6.1 ### What's New Remove deprecated api. ## Version 0.6.0 ### What's New Provide remain get apis. like activities, captions, channel_sections, i18n, video_abuse_report_reason, search resource and so on. You can see the `README`_ to get more detail for those api. ## Version 0.5.3 ### What's New Provide the page token parameter to skip data have retrieved. This for follow api methods ```python api.get_playlists() api.get_playlist_items() api.get_videos_by_chart() api.get_videos_by_myrating() api.get_comment_threads() api.get_comments() api.get_subscription_by_channel() api.get_subscription_by_me() ``` example ``` In[1]: r = api.get_subscription_by_channel(channel_id="UCAuUUnT6oDeKwE6v1NGQxug", limit=5, count=None, page_token="CAUQAA") In[2]:r.prevPageToken Out[2]: 'CAUQAQ' ``` ## Version 0.5.2 ### What's New Now you can use authorized access token to get your subscriptions. You can to the demo [A demo for get my subscription](https://github.com/sns-sdks/python-youtube/blob/master/examples/subscription.py) to see simple usage. Or you can see the [subscriptions usage](https://github.com/sns-sdks/python-youtube/blob/master/README.rst#subscriptions) docs. #43 add api for get my subscriptions #41 add api for channel subscriptions ## Version 0.5.1 ### What's New Now some apis can get all target items just by one method call. For example, you can get playlist's all items by follow call ``` In [1]: r = api.get_playlist_items(playlist_id="PLWz5rJ2EKKc_xXXubDti2eRnIKU0p7wHd", parts=["id", "snippet"], count=None) In [2]: r.pageInfo Out[2]: PageInfo(totalResults=73, resultsPerPage=50) In [3]: len(r.items) Out[4]: 73 ``` You can see the [README](https://github.com/sns-sdks/python-youtube/blob/master/README.rst) to find which methods support this. ## Version 0.5.0 ### **Broken Change** Now introduce new model ApiResponse representing the response from youtube, so previous usage has been invalidated. You need to read the docs [README](https://github.com/sns-sdks/python-youtube/blob/master/README.rst) to get the simple new usage. ### What's New Split some method into multiple usage, for example get video has been split three methods: * api.get_video_by_id() * api.get_videos_by_chart() * api.get_videos_by_myrating() ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 sns-sdks Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ all: help clean lint test .PHONY: all help: @echo " env install all dependencies" @echo " clean remove unwanted stuff" @echo " docs build documentation" @echo " lint check style with black" @echo " test run tests with cov" env: pip install --upgrade pip pip install poetry poetry install clean: clean-build clean-pyc clean-test clean-build: rm -fr build/ rm -fr dist/ rm -fr .eggs/ find . -name '*.egg-info' -exec rm -fr {} + find . -name '*.egg' -exec rm -f {} + clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + find . -name '__pycache__' -exec rm -fr {} + clean-test: rm -fr .pytest_cache rm -f .coverage rm -fr htmlcov/ docs: $(MAKE) -C docs html lint: black . lint-check: black --check . test: pytest -s tests-html: pytest -s --cov-report term --cov-report html # v0.1.0 -> v0.2.0 bump-minor: bump2version minor # v0.1.0 -> v0.1.1 bump-patch: bump2version patch ================================================ FILE: README.rst ================================================ Python YouTube A Python wrapper for the YouTube Data API V3. .. image:: https://github.com/sns-sdks/python-youtube/workflows/Test/badge.svg :target: https://github.com/sns-sdks/python-youtube/actions .. image:: https://img.shields.io/badge/Docs-passing-brightgreen :target: https://sns-sdks.github.io/python-youtube/ :alt: Documentation Status .. image:: https://codecov.io/gh/sns-sdks/python-youtube/branch/master/graph/badge.svg :target: https://codecov.io/gh/sns-sdks/python-youtube .. image:: https://img.shields.io/pypi/v/python-youtube.svg :target: https://img.shields.io/pypi/v/python-youtube ====== THANKS ====== Inspired by `Python-Twitter `_. Thanks a lot to Python-Twitter Developers. ============ Introduction ============ This library provides an easy way to use the YouTube Data API V3. .. We have recently been working on the new structure for the library. `Read docs `_ to get more detail. ============= Documentation ============= You can view the latest ``python-youtube`` documentation at: https://sns-sdks.github.io/python-youtube/. Also view the full ``YouTube DATA API`` docs at: https://developers.google.com/youtube/v3/docs/. ========== Installing ========== You can install this lib from PyPI: .. code:: shell pip install --upgrade python-youtube # ✨🍰✨ ===== Using ===== The library covers all resource methods, including ``insert``,``update``, and so on. We recommend using the ``pyyoutube.Client`` to operate DATA API. It is more modern and feature rich than ``pyyoutube.Api``. Work with Client ---------------- You can initialize with an api key: .. code-block:: python >>> from pyyoutube import Client >>> client = Client(api_key="your api key") To access additional data that requires authorization, you need to initialize with an access token: .. code-block:: python >>> from pyyoutube import Client >>> client = Client(access_token='your access token') You can read the docs to see how to get an access token. Or you can ask for user to do OAuth: .. code-block:: python >>> from pyyoutube import Client >>> client = Client(client_id="client key", client_secret="client secret") >>> client.get_authorize_url() ('https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=id&redirect_uri=https%3A%2F%2Flocalhost%2F&scope=scope&state=PyYouTube&access_type=offline&prompt=select_account', 'PyYouTube') >>> client.generate_access_token(authorization_response="link for response") AccessToken(access_token='token', expires_in=3599, token_type='Bearer') Now you can use the instance to get data from YouTube. Get channel detail: >>> cli.channels.list(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw") ChannelListResponse(kind='youtube#channelListResponse') >>> cli.channels.list(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", return_json=True) {'kind': 'youtube#channelListResponse', 'etag': 'eHSYpB_FqHX8vJiGi_sLCu0jkmE', ... } See the `client docs `_, or `client examples `_, for additional usage Work with API ---------------- .. For compatibility with older code, we continue to support the old way. You can just initialize with an api key: .. code-block:: python >>> from pyyoutube import Api >>> api = Api(api_key="your api key") To access additional data that requires authorization, you need to initialize with an access token: .. code-block:: python >>> from pyyoutube import Api >>> api = Api(access_token='your access token') You can read the docs to see how to get an access token. Or you can ask for user to do OAuth flow: .. code-block:: python >>> from pyyoutube import Api >>> api = Api(client_id="client key", client_secret="client secret") # Get authorization url >>> api.get_authorization_url() ('https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=id&redirect_uri=https%3A%2F%2Flocalhost%2F&scope=scope&state=PyYouTube&access_type=offline&prompt=select_account', 'PyYouTube') # user to do # copy the response url >>> api.generate_access_token(authorization_response="link for response") AccessToken(access_token='token', expires_in=3599, token_type='Bearer') Now you can use the instance to get data from YouTube. Get channel detail: .. code-block:: python >>> channel_by_id = api.get_channel_info(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw") >>> channel_by_id.items [Channel(kind='youtube#channel', id='UC_x5XG1OV2P6uZZ5FSM9Ttw')] >>> channel_by_id.items[0].to_dict() {'kind': 'youtube#channel', 'etag': '"j6xRRd8dTPVVptg711_CSPADRfg/AW8QEqbNRoIJv9KuzCIg0CG6aJA"', '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.', 'customUrl': 'googlecode', 'publishedAt': '2007-08-23T00:34:43.000Z', 'thumbnails': {'default': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo', 'width': 88, 'height': 88}, 'medium': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo', 'width': 240, 'height': 240}, 'high': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo', 'width': 800, 'height': 800}, 'standard': None, 'maxres': None}, 'defaultLanguage': None, '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.'}, 'country': 'US'}, ... } # Get json response from youtube >>> api.get_channel_info(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", return_json=True) {'kind': 'youtube#channelListResponse', 'etag': '17FOkdjp-_FPTiIJXdawBS4jWtc', ... } See the `api docs `_, or `api examples `_, for additional usage. ================================================ FILE: docs/docs/authorization.md ================================================ If you want to get more data for your channel, You need provide the authorization. This doc shows how to authorize a client. ## Prerequisite To begin with, you must know what authorization is. You can see some information at the [Official Documentation](https://developers.google.com/youtube/v3/guides/authentication). You will need to create an app with [Access scope](https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#identify-access-scopes) approval by YouTube. Once complete, you will be able to do a simple authorize with `Python-Youtube` library. ## Get authorization url Suppose now we want to get user's permission to manage their YouTube account. For the `Python-YouTube` library, the default scopes are: - https://www.googleapis.com/auth/youtube - https://www.googleapis.com/auth/userinfo.profile You can get more scope information at [Access scopes](https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#identify-access-scopes). (The defailt redirect URI used in PyYoutube is `https://localhost/`) We can now perform the following steps: Initialize the api instance with your app credentials ``` In [1]: from pyyoutube import Client In [2]: cli = Client(client_id="you client id", client_secret="you client secret") In [3]: cli.get_authorize_url() Out[3]: ('https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=client_id&redirect_uri=https%3A%2F%2Flocalhost%2F&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyoutube+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&state=PyYouTube&access_type=offline&prompt=select_account', 'PyYouTube') ``` Open your broswer of choice and copy the link returned by `get_authorize_url()` into the searchbar. ## Do authorization On entering the URL, you will see the following: ![auth-1-chose-account](images/auth-1-chose-account.png) Select the account to authorize your app to read data from. If your app is not approved for use, you will recieve a warning. You can prevent this by adding your chosen Google account as a test member on your created OAuth application. Otherwise, you will see the following: ![auth-2-not-approval](images/auth-2-not-approval.png) You will need to click ``Advanced``, then click the ``Go to Python-YouTube (unsafe)``. ![auth-3-advanced](images/auth-3-advanced.png) You should now see a window to select permissions granted to the application. ![auth-4-allow-permission](images/auth-4-allow-permission.png) Click `allow` to give the permission. You will see a Connection Error, as the link is redirecting to `localhost`. This is standard behaviour, so don't close the window or return to a previous page! ## Retrieve access token Copy the full redicted URL from the browser address bar, and return to your original console. ``` In [4]: token = cli.generate_access_token(authorization_response="$redirect_url") In [5]: token Out[5]: AccessToken(access_token='access token', expires_in=3600, token_type='Bearer') ``` (Replace `$redirect_url` with the URL you copied) You now have an access token to view your account data. ## Get your data For example, you can get your playlists. ``` In [6]: playlists = cli.playlists.list(mine=True) In [7]: playlists.items Out[7]: [Playlist(kind='youtube#playlist', id='PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS'), Playlist(kind='youtube#playlist', id='PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g')] ``` !!! note "Tips" If you are confused, it is beneficial to read the [Authorize Requests](https://developers.google.com/youtube/v3/guides/authentication) guide first. ================================================ FILE: docs/docs/getting_started.md ================================================ This document is a simple tutorial to show how to use this library to get data from YouTube data API. You can get the whole description for the YouTube API at [YouTube API Reference](https://developers.google.com/youtube/v3/docs/). ## Prerequisite To begin, you need to create a [Google Project](https://console.cloud.google.com) with your google account. Every new account has a free quota of 12 projects. ## Create your project Click `Select a project-> NEW PROJECT` to create a new project to use our library. Fill in the basic info and create the project. ![gt-create-app-1](images/gt-create-app-1.png) ## Enable YouTube DATA API service Once the project created, the browser will redirect you to the project home page. Click the `≡≡` symbol on the top left and select the `APIs & Services` tab. You will see following info: ![gt-create-app-2](images/gt-create-app-2.png) Click the `+ ENABLE APIS AND SERVICES` symbol, and input `YouTube DATA API` to search. ![gt-create-app-3](images/gt-create-app-3.png) Chose the ``YouTube DATA API`` item. ![gt-create-app-4](images/gt-create-app-4.png) Then click the `ENABLE` blue button. After a short period where the API is added to your project, the service will be activated. ## Create credentials To use this API, you need credentials. Click `Create credentials` to get started. ![gt-create-app-5](images/gt-create-app-5.png) You need to fill in some information to create credentials. Just chose `YouTube DATA API v3`, `Other non-UI (e.g. cron job, daemon)` and `Public data`. Then click the blue button `What credentials do I need?` to create. ![gt-create-app-6](images/gt-create-app-6.png) You have now generated an api key. Using this key, you can retrieve public YouTube data with our library ```python from pyyoutube import Client cli = Client(api_key="your api key") ``` Check out the [examples](https://github.com/sns-sdks/python-youtube/tree/master/examples) directory for some examples of using the library. If you have an open source application using python-youtube, send me a link. I am very happy to add a link to it here. If you want to get user data by OAuth. You need create the credential for ``OAuth client ID``. You will find more information on OAth at the [Authorization](authorization.md) page. ================================================ FILE: docs/docs/index.md ================================================ # Welcome to Python-Youtube's documentation! **A Python wrapper around for YouTube Data API.** Author: IkarosKun ## Introduction With the YouTube Data API, you can add a variety of YouTube features to your application. Use the API to upload videos, manage playlists and subscriptions, update channel settings, and more. This library provides a Python interface for the [YouTube DATA API](https://developers.google.com/youtube/v3). This library has works on all Python versions 3.6 and newer. !!! tip "Tips" This library only supports `DATA API`, It does not support `Analytics and Reporting APIs` and `Live Streaming API`. ================================================ FILE: docs/docs/installation.md ================================================ This library supports Python 3.6 and newer. ## Dependencies These following distributions will be installed automatically when installing Python-Youtube. - [requests](https://2.python-requests.org/en/master/): is an elegant and simple HTTP library for Python, built for human beings. - [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. - [isodate](https://pypi.org/project/isodate/): implements ISO 8601 date, time and duration parsing. ## Installation You can install this library from **PyPI** ```shell $ pip install --upgrade python-youtube ``` You can also build this library from source ```shell $ git clone https://github.com/sns-sdks/python-youtube.git $ cd python-youtube $ make env $ make build ``` ## Testing Run `make env` after you have installed the project requirements. Once completed, you can run code tests with ```shell $ make tests-html ``` ================================================ FILE: docs/docs/introduce-new-structure.md ================================================ This doc will show you the new api structure for this library. ## Brief To make the package easier to maintain and easy to use. We have shifted to using classes for different YouTube resources in an easier, higher-level, programming experience. ![structure-uml](images/structure-uml.png) In this structure, every resource has a self class. ## Simple usage ### Initial Client ```python from pyyoutube import Client client = Client(api_key="your api key") ``` ### Get data. for example to get channel data. ```python resp = client.channels.list( parts=["id", "snippet"], channel_id="UCa-vrCLQHviTOVnEKDOdetQ" ) # resp output # ChannelListResponse(kind='youtube#channelListResponse') # resp.items[0].id output # UCa-vrCLQHviTOVnEKDOdetQ ``` ================================================ FILE: docs/docs/usage/work-with-api.md ================================================ # Work with Api !!! note "Tips" This is the previous version to operate YouTube DATA API. We recommend using the latest version of methods to operate YouTube DATA API. The API is exposed via the ``pyyoutube.Api`` class. ## INSTANTIATE We provide two method to create instances of the ``pyyoutube.Api``. You can just initialize with an api key. ``` >>> from pyyoutube import Api >>> api = Api(api_key="your api key") ``` If you want to get authorization data, you will need to initialize with an access token. ``` >>> from pyyoutube import Api >>> api = Api(access_token='your api key') ``` You can read the docs to see how to get an access token. Or you can ask for the user to do oauth flow: ``` >>> from pyyoutube import Api >>> api = Api(client_id="client key", client_secret="client secret") # Get authorization url >>> api.get_authorization_url() # ('https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=id&redirect_uri=https%3A%2F%2Flocalhost%2F&scope=scope&state=PyYouTube&access_type=offline&prompt=select_account', 'PyYouTube') # user to do # copy the response url >>> api.generate_access_token(authorization_response="link for response") # AccessToken(access_token='token', expires_in=3599, token_type='Bearer') ``` ## Usage Now you can use the instance to get data from YouTube. ### CHANNEL DATA The library provides several ways to get a channels data. If a channel is not found, the property ``items`` will return an empty list. You can use channel id: ``` >>> channel_by_id = api.get_channel_info(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw") >>> channel_by_id.items [Channel(kind='youtube#channel', id='UC_x5XG1OV2P6uZZ5FSM9Ttw')] >>> channel_by_id.items[0].to_dict() {'kind': 'youtube#channel', 'etag': '"j6xRRd8dTPVVptg711_CSPADRfg/AW8QEqbNRoIJv9KuzCIg0CG6aJA"', '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.', 'customUrl': 'googlecode', 'publishedAt': '2007-08-23T00:34:43.000Z', 'thumbnails': {'default': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo', 'width': 88, 'height': 88}, 'medium': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo', 'width': 240, 'height': 240}, 'high': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo', 'width': 800, 'height': 800}, 'standard': None, 'maxres': None}, 'defaultLanguage': None, '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.'}, 'country': 'US'}, ... } ``` To get multiple channels, you can pass any of: a string containing comma-seperated ids; or an enumarable (list, tuple, or set) of ids Many other methods also provide this functionality. with ids: ``` >>> channel_by_ids = api.get_channel_info(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw,UCa-vrCLQHviTOVnEKDOdetQ") >>> channel_by_ids.items [Channel(kind='youtube#channel', id='UC_x5XG1OV2P6uZZ5FSM9Ttw'), Channel(kind='youtube#channel', id='UCa-vrCLQHviTOVnEKDOdetQ')] ``` You can also use a channel name: ``` >>> channel_by_username = api.get_channel_info(for_username="GoogleDevelopers") >>> channel_by_username.items[0] Channel(kind='youtube#channel', id='UC_x5XG1OV2P6uZZ5FSM9Ttw') ``` If you have authorized your client, you can get your channels directly: ``` >>> channel_by_mine = api_with_authorization.get_channel_info(mine=True) >>> channel_by_mine.items[0] Channel(kind='youtube#channel', id='UCa-vrCLQHviTOVnEKDOdetQ') ``` !!! note "Tips" To get your channel, you must do authorization first, otherwise you will get an error. ### PLAYLIST There are methods to get playlists by playlist id, channel id, or get your own playlists. Get playlists by id: ``` >>> playlists_by_id = api.get_playlist_by_id(playlist_id="PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw") >>> playlists_by_id.items [Playlist(kind='youtube#playlist', id='PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw')] ``` Get playlists by channel (If you want to get all playlists for the target channels, provide the parameter `count=None`): ``` >>> playlists_by_channel = api.get_playlists(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw") >>> playlists_by_channel.items [Playlist(kind='youtube#playlist', id='PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw'), Playlist(kind='youtube#playlist', id='PLOU2XLYxmsIJO83u2UmyC8ud41AvUnhgj'), Playlist(kind='youtube#playlist', id='PLOU2XLYxmsILfV1LiUhDjbh1jkFjQWrYB'), Playlist(kind='youtube#playlist', id='PLOU2XLYxmsIKNr3Wfhm8o0TSojW7hEPPY'), Playlist(kind='youtube#playlist', id='PLOU2XLYxmsIJ8ItHmK4bRlY4GCzMgXLAJ')] ``` Get your playlists (this requires authorization): ``` >>> playlists_by_mine = api.get_playlists(mine=True) ``` ### PLAYLIST ITEM Similarly, you can get playlist items by playlist item id or playlist id. Get playlist items by id: ``` >>> playlist_item_by_id = api.get_playlist_item_by_id(playlist_item_id="UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA" ... "1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2") >>> playlist_item_by_id.items [PlaylistItem(kind='youtube#playlistItem', id='UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2')] ``` Get playlist items by playlist id (If you want to get return all items in a playlist, provide the parameter `count=None`): ``` >>> playlist_item_by_playlist = api.get_playlist_items(playlist_id="PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", count=2) >>> playlist_item_by_playlist.items [PlaylistItem(kind='youtube#playlistItem', id='UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2'), PlaylistItem(kind='youtube#playlistItem', id='UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy4yODlGNEE0NkRGMEEzMEQy')] >>> playlist_item_by_id.items[0].snippet.resourceId ResourceId(kind='youtube#video', videoId='CvTApw9X8aA') ``` ### VIDEO You can get a video's information by several methods. Get videos by video id(s): ``` >>> video_by_id = api.get_video_by_id(video_id="CvTApw9X8aA") >>> video_by_id VideoListResponse(kind='youtube#videoListResponse') >>> video_by_id.items [Video(kind='youtube#video', id='CvTApw9X8aA')] ``` Get videos by chart (If you want to get all videos, just provide the parameter `count=None`): ``` >>> video_by_chart = api.get_videos_by_chart(chart="mostPopular", region_code="US", count=2) >>> video_by_chart.items [Video(kind='youtube#video', id='RwnN2FVaHmw'), Video(kind='youtube#video', id='hDeuSfo_Ys0')] ``` Get videos by your rating (this requires authorization. If you also want to get all videos, provide the parameter `count=None`): ``` >>> videos_by_rating = api.get_videos_by_myrating(rating="like", count=2) ``` ### COMMENT THREAD You can get comment thread information by id or by a filter. Get comment thread by id(s): ``` >>> ct_by_id = api.get_comment_thread_by_id(comment_thread_id='Ugz097FRhsQy5CVhAjp4AaABAg,UgzhytyP79_Pwa ... Dd4UB4AaABAg') >>> ct_by_id.items [CommentThread(kind='youtube#commentThread', id='Ugz097FRhsQy5CVhAjp4AaABAg'), CommentThread(kind='youtube#commentThread', id='UgzhytyP79_PwaDd4UB4AaABAg')] ``` Get all comment threads related to a channel (including comment threads for the channel's video. If you want to get all comment threads, provide the parameter `count=None`): ``` >>> ct_by_all = api.get_comment_threads(all_to_channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", count=2) >>> ct_by_all.items [CommentThread(kind='youtube#commentThread', id='UgwlB_Cza9WtzUWahYN4AaABAg'), CommentThread(kind='youtube#commentThread', id='UgyvoQJ2LsxCBwGEpMB4AaABAg')] ``` Get comment threads only for the channel (If you want to get all comment threads, provide the parameter `count=None`): ``` >>> ct_by_channel = api.get_comment_threads(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", count=2) >>> ct_by_channel.items [CommentThread(kind='youtube#commentThread', id='UgyUBI0HsgL9emxcZpR4AaABAg'), CommentThread(kind='youtube#commentThread', id='Ugzi3lkqDPfIOirGFLh4AaABAg')] ``` Get comment threads only for the video (If you want to get all comment threads, provide the parameter `count=None`): ``` >>> ct_by_video = api.get_comment_threads(video_id="D-lhorsDlUQ", count=2) >>> ct_by_video.items [CommentThread(kind='youtube#commentThread', id='UgydxWWoeA7F1OdqypJ4AaABAg'), CommentThread(kind='youtube#commentThread', id='UgxKREWxIgDrw8w2e_Z4AaABAg')] ``` ### COMMENT You can get comment information by id or use the top-level comment id to get replies. !!! note "Tips" The reply has the same structure as a comment. Get comments by id(s): ``` >>> comment_by_id = api.get_comment_by_id(comment_id='UgxKREWxIgDrw8w2e_Z4AaABAg,UgyrVQaFfEdvaSzstj14AaABAg') >>> comment_by_id.items [Comment(kind='youtube#comment', id='UgxKREWxIgDrw8w2e_Z4AaABAg', snippet=CommentSnippet(authorDisplayName='Hieu Nguyen', likeCount=0)), Comment(kind='youtube#comment', id='UgyrVQaFfEdvaSzstj14AaABAg', snippet=CommentSnippet(authorDisplayName='Mani Kanta', likeCount=0))] ``` Get replies by comment id (If you want to get all comments, just provide the parameter `count=None`): ``` >>> comment_by_parent = api.get_comments(parent_id="UgwYjZXfNCUTKPq9CZp4AaABAg") >>> comment_by_parent.items [Comment(kind='youtube#comment', id='UgwYjZXfNCUTKPq9CZp4AaABAg.8yxhlQJogG18yz_cXK9Kcj', snippet=CommentSnippet(authorDisplayName='Marlon López', likeCount=0))] ``` ### VIDEO CATEGORY You can get video category with id or region. Get video categories with id(s): ``` >>> video_category_by_id = api.get_video_categories(category_id="17,18") >>> video_category_by_id.items [VideoCategory(kind='youtube#videoCategory', id='17'), VideoCategory(kind='youtube#videoCategory', id='18')] ``` Get video categories with region code: ``` >>> video_categories_by_region = api.get_video_categories(region_code="US") >>> video_categories_by_region.items [VideoCategory(kind='youtube#videoCategory', id='1'), VideoCategory(kind='youtube#videoCategory', id='2'), VideoCategory(kind='youtube#videoCategory', id='10'), VideoCategory(kind='youtube#videoCategory', id='15'), ...] ``` ### SUBSCRIPTIONS You can get subscription information by id, by point channel, or your own. !!! note "Tips" If you want to get the non-public subscriptions, you need to authorize and obtain the access token first. See the demo [A demo for get my subscription](examples/subscription.py). To get subscription info by id(s), your token needs to have the permission for the subscriptions belonging to a channel or user: ``` >>> r = api.get_subscription_by_id( ... subscription_id=[ ... "zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo", ... "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo"]) >>> r SubscriptionListResponse(kind='youtube#subscriptionListResponse') >>> r.items [Subscription(kind='youtube#subscription', id='zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo', snippet=SubscriptionSnippet(title='PyCon 2015', description='')), Subscription(kind='youtube#subscription', id='zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo', snippet=SubscriptionSnippet(title='ikaros-life', description='This is a test channel.'))] ``` Get your own subscriptions, you need to authorize first, and supply the token: ``` >>> r = api.get_subscription_by_me( ... mine=True, ... parts=["id", "snippet"], ... count=2 ... ) >>> r SubscriptionListResponse(kind='youtube#subscriptionListResponse') >>> r.items [Subscription(kind='youtube#subscription', id='zqShTXi-2-Tx7TtwQqhCBwtJ-Aho6DZeutqZiP4Q79Q', snippet=SubscriptionSnippet(title='Next Day Video', description='')), Subscription(kind='youtube#subscription', id='zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo', snippet=SubscriptionSnippet(title='PyCon 2015', description=''))] ``` Get public channel subscriptions: ``` >>> r = api.get_subscription_by_channel( ... channel_id="UCAuUUnT6oDeKwE6v1NGQxug", ... parts="id,snippet", ... count=2 ... ) >>> r SubscriptionListResponse(kind='youtube#subscriptionListResponse') >>> r.items [Subscription(kind='youtube#subscription', id='FMP3Mleijt-52zZDGkHtR5KhwkvCcdQKWWWIA1j5eGc', snippet=SubscriptionSnippet(title='TEDx Talks', description="TEDx is an international community that organizes TED-style events anywhere and everywhere -- celebrating locally-driven ideas and elevating them to a global stage. TEDx events are produced independently of TED conferences, each event curates speakers on their own, but based on TED's format and rules.\n\nFor more information on using TED for commercial purposes (e.g. employee learning, in a film, or in an online course), please submit a media request using the link below.")), Subscription(kind='youtube#subscription', id='FMP3Mleijt_ZKvy5M-HhRlsqI4wXY7VmP5g8lvmRhVU', snippet=SubscriptionSnippet(title='TED Residency', description='The TED Residency program is an incubator for breakthrough ideas. It is free and open to all via a semi-annual competitive application. Those chosen as TED Residents spend four months at TED headquarters in New York City, working on their idea. Selection criteria include the strength of their idea, their character, and their ability to bring a fresh perspective and positive contribution to the diverse TED community.'))] ``` ### ACTIVITIES You can get activities by channel id. You can also get your own activities after you have completed authorization. Get public channel activities: ``` >>> r = api.get_activities_by_channel(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", count=2) >>> r ActivityListResponse(kind='youtube#activityListResponse') >>> r.items [Activity(kind='youtube#activity', id='MTUxNTc3NzM2MDAyODIxOTQxNDM0NjAwMA==', snippet=ActivitySnippet(title='2019 Year in Review - The Developer Show', 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')), Activity(kind='youtube#activity', id='MTUxNTc3MTI4NzIzODIxOTQxNDM0NzI4MA==', snippet=ActivitySnippet(title='GDE Promo - Lara Martin', description='Meet Lara Martin, a Flutter/Dart Google Developers Expert and get inspired by her journey. Watch now for a preview of her story! #GDESpotlights #IncludedWithGoogle\n\nLearn about the GDE program → https://goo.gle/2qWOvAy\n\nGoogle Developers Experts → https://goo.gle/GDE\nSubscribe to Google Developers → https://goo.gle/developers'))] ``` Get your activities: ``` >>> r = api_with_token.get_activities_by_me() >>> r.items [Activity(kind='youtube#activity', id='MTUxNTc0OTk2MjI3NDE0MjYwMDY1NjAwODA=', snippet=ActivitySnippet(title='华山日出', description='冷冷的山头')), Activity(kind='youtube#activity', id='MTUxNTc0OTk1OTAyNDE0MjYwMDY1NTc2NDg=', snippet=ActivitySnippet(title='海上日出', description='美美美'))] ``` Get your video captions: ``` >>> r = api.get_captions_by_video(video_id="oHR3wURdJ94", parts=["id", "snippet"]) >>> r CaptionListResponse(kind='youtube#captionListResponse') >>> r.items [Caption(kind='youtube#caption', id='SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I', snippet=CaptionSnippet(videoId='oHR3wURdJ94', lastUpdated='2020-01-14T09:40:49.981Z')), Caption(kind='youtube#caption', id='fPMuDm722CIRcUAT3NTPQHQZJZJxt39kU7JvrHk8Kzs=', snippet=CaptionSnippet(videoId='oHR3wURdJ94', lastUpdated='2020-01-14T09:39:46.991Z'))] ``` If you already have caption id(s), you can get video caption by id(s): ``` >>> r = api.get_captions_by_video(video_id="oHR3wURdJ94", parts=["id", "snippet"], caption_id="SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I") >>> r CaptionListResponse(kind='youtube#captionListResponse') >>> r.items [Caption(kind='youtube#caption', id='SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I', snippet=CaptionSnippet(videoId='oHR3wURdJ94', lastUpdated='2020-01-14T09:40:49.981Z'))] ``` ### CHANNEL SECTIONS You can get channel sections by channel id, section id, or your own channel. Get channel sections by channel id: ``` >>> r = api.get_channel_sections_by_channel(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw") >>>> r ChannelSectionResponse(kind='youtube#channelSectionListResponse') >>> r.items [ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE'), ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.B8DTd9ZXJqM'), ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.MfvRjkWLxgk'), ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.fEjJOXRoWwg'), ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.PvTmxDBxtLs'), ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.pmcIOsL7s98'), ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.c3r3vYf9uD0'), ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.ZJpkBl-mXfM'), ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.9_wU0qhEPR8'), ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.npYvuMz0_es')] ``` Get authorized user's channel sections: ``` >>> r = api.get_channel_sections_by_channel(mine=True) >>> r.items [ChannelSection(kind='youtube#channelSection', id='UCa-vrCLQHviTOVnEKDOdetQ.jNQXAC9IVRw'), ChannelSection(kind='youtube#channelSection', id='UCa-vrCLQHviTOVnEKDOdetQ.LeAltgu_pbM'), ChannelSection(kind='youtube#channelSection', id='UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY')] ``` Get channel section detail info by id: ``` >>> r = api.get_channel_section_by_id(section_id="UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE") >>> r ChannelSectionResponse(kind='youtube#channelSectionListResponse') >>> r1.items [ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE')] ``` ### I18N RESOURCE You can get a list of content regions that the YouTube website supports: ``` >>> r = api.get_i18n_regions(parts=["snippet"]) >>> r.items [I18nRegion(kind='youtube#i18nRegion', id='DZ', snippet=I18nRegionSnippet(gl='DZ', name='Algeria')), I18nRegion(kind='youtube#i18nRegion', id='AR', snippet=I18nRegionSnippet(gl='AR', name='Argentina')), I18nRegion(kind='youtube#i18nRegion', id='AU', snippet=I18nRegionSnippet(gl='AU', name='Australia')) ...] ``` You can get a list of application languages that the YouTube website supports: ``` >>> r = api.get_i18n_languages(parts=["snippet"]) >>> r.items [I18nLanguage(kind='youtube#i18nLanguage', id='af', snippet=I18nLanguageSnippet(hl='af', name='Afrikaans')), I18nLanguage(kind='youtube#i18nLanguage', id='az', snippet=I18nLanguageSnippet(hl='az', name='Azerbaijani')), I18nLanguage(kind='youtube#i18nLanguage', id='id', snippet=I18nLanguageSnippet(hl='id', name='Indonesian')), ...] ``` ### MEMBER The API request must be authorized by the channel owner. You can retrieve a list of members (formerly known as "sponsors") for a channel: ``` >>> r = api_with_token.get_members(parts=["snippet"]) >>> r.items [MemberListResponse(kind='youtube#memberListResponse'), MemberListResponse(kind='youtube#memberListResponse')] ``` ### MEMBERSHIP LEVEL The API request must be authorized by the channel owner. You can retrieve a list membership levels for a channel: ``` >>> r = api_with_token.get_membership_levels(parts=["snippet"]) >>> r.items [MembershipsLevelListResponse(kind='youtube#membershipsLevelListResponse'), MembershipsLevelListResponse(kind='youtube#membershipsLevelListResponse')] ``` ### VIDEO ABUSE REPORT REASON You can retrieve a list of reasons that can be used to report abusive videos: ``` >>> r = api_with_token.get_video_abuse_report_reason(parts=["snippet"]) >>> r.items [VideoAbuseReportReason(kind='youtube#videoAbuseReportReason'), VideoAbuseReportReason(kind='youtube#videoAbuseReportReason')] ``` ### SEARCH You can use those methods to search the video, playlist, or channel data. For more info, you can see the [Search Request Docs](https://developers.google.com/youtube/v3/docs/search/list). You can search different type of resource with keywords: ``` >>> r = api.search_by_keywords(q="surfing", search_type=["channel","video", "playlist"], count=5, limit=5) >>> r.items [SearchResult(kind='youtube#searchResult'), SearchResult(kind='youtube#searchResult'), SearchResult(kind='youtube#searchResult'), SearchResult(kind='youtube#searchResult'), SearchResult(kind='youtube#searchResult')] ``` You can search your app send videos: ``` >>> r = api_with_token.search_by_developer(q="news", count=1) >>> r.items [SearchResult(kind='youtube#searchResult')] ``` You can search your videos: ``` >>> r = api_with_token.search_by_mine(q="news", count=1) >>> r.items [SearchResult(kind='youtube#searchResult')] ``` Or you can build your request using the `search` method: ``` >>> r = api.search( ... location="21.5922529, -158.1147114", ... location_radius="10mi", ... q="surfing", ... parts=["snippet"], ... count=5, ... published_after="2020-02-01T00:00:00Z", ... published_before="2020-03-01T00:00:00Z", ... safe_search="moderate", ... search_type="video") >>> r.items [SearchResult(kind='youtube#searchResult'), SearchResult(kind='youtube#searchResult'), SearchResult(kind='youtube#searchResult'), SearchResult(kind='youtube#searchResult'), SearchResult(kind='youtube#searchResult')] >>> r = api.search( ... event_type="live", ... q="news", ... count=3, ... parts=["snippet"], ... search_type="video", ... topic_id="/m/09s1f", ... order="viewCount") >>> r.items [SearchResult(kind='youtube#searchResult'), SearchResult(kind='youtube#searchResult'), SearchResult(kind='youtube#searchResult')] ``` ================================================ FILE: docs/docs/usage/work-with-client.md ================================================ # Work with Client We have refactored the project code to support more methods and improve code usability. And new structure like follows. ![structure-uml](../images/structure-uml.png) In this structure, we identify each entity as a class of resources and perform operations on the resources. ## INSTANTIATE Client is exposed via the ``pyyoutube.Client`` class. You can initialize it with `api key`, to get public data. ```python from pyyoutube import Client cli = Client(api_key="your api key") ``` If you want to update your channel data. or upload video. You need to initialize with `access token`, or do the auth flow. ```python from pyyoutube import Client cli = Client(access_token="Access Token with permissions") ``` ```python from pyyoutube import Client cli = Client(client_id="ID for app", client_secret="Secret for app") # Get authorization url cli.get_authorize_url() # ('https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=id&redirect_uri=https%3A%2F%2Flocalhost%2F&scope=scope&state=PyYouTube&access_type=offline&prompt=select_account', 'PyYouTube') # Click url and give permissions. # Copy the redirected url. cli.generate_access_token(authorization_response="redirected url") # AccessToken(access_token='token', expires_in=3599, token_type='Bearer') ``` ### from client_secret Only `web` and some `installed` type client_secrets are supported. The fields `client_id` and `client_secret` must be set. `Client.DEFAULT_REDIRECT_URI` will be set the first entry of the field `redirect_uris`. ```python from pyyoutube import Client file_path = "path/to/client_secret.json" cli = Client(client_secret_path=file_path) # Then go through auth flow descriped above ``` Once initialize to the client, you can operate the API to get data. ## Usage ### Channel Resource The API supports the following methods for the `channels` resources: - list: Returns a collection of zero or more channel resources that match the request criteria. - update: Updates a channel's metadata. Note that this method currently only supports updates to the channel resource's brandingSettings and invideoPromotion objects and their child properties #### List channel data ```python resp = cli.channels.list(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw") # ChannelListResponse(kind='youtube#channelListResponse') print(resp.items) # [Channel(kind='youtube#channel', id='UC_x5XG1OV2P6uZZ5FSM9Ttw')] ``` #### update channel metadata ```python import pyyoutube.models as mds body = mds.Channel( id="channel id", brandingSettings=mds.ChannelBrandingSetting( image=mds.ChannelBrandingSettingImage( bannerExternalUrl="new banner url" ) ) ) channel = cli.channels.update( part="brandingSettings", body=body ) print(channel.brandingSettings.image.bannerExternalUrl) # 'https://yt3.googleusercontent.com/AegVxoIusdXEmsJ9j3bcJR3zuImOd6TngNw58iJAP0AOAXCnb1xHPcuEDOQC8J85SCZvt5i8A_g' ``` ### Video Resource The API supports the following methods for `videos` resources. #### getRating Retrieves the ratings that the authorized user gave to a list of specified videos. ```python resp = cli.videos.get_rating(video_id="Z56Jmr9Z34Q") print(resp.items) # [VideoRatingItem(videoId='Z56Jmr9Z34Q', rating='none')] ``` #### list Returns a list of videos that match the API request parameters. ```python resp = cli.videos.list(video_id="Z56Jmr9Z34Q") print(resp.items) # [Video(kind='youtube#video', id='Z56Jmr9Z34Q')] ``` #### insert Uploads a video to YouTube and optionally sets the video's metadata. ```python import pyyoutube.models as mds from pyyoutube.media import Media body = mds.Video( snippet=mds.VideoSnippet( title="video title", description="video description" ) ) media = Media(filename="video.mp4") upload = cli.videos.insert( body=body, media=media, parts=["snippet"], notify_subscribers=True ) video_body = None while video_body is None: status, video_body = upload.next_chunk() if status: print(f"Upload progress: {status.progress()}") print(video_body) # {"kind": "youtube#video", "etag": "17W46NjVxoxtaoh1E6GmbQ2hv5c",....} ``` #### update Updates a video's metadata. ```python import pyyoutube.models as mds body = mds.Video( id="fTK1Jj6QlDw", snippet=mds.VideoSnippet( title="What a nice day", description="Blue sky with cloud. updated.", categoryId="1", ) ) resp = cli.videos.update( parts=["snippet"], body=body, return_json=True, ) print(resp) # {"kind": "youtube#video", "etag": "BQUtovVd0TBJwC5S8-Pu-dK_I6s", "id": "fTK1Jj6QlDw", "snippet": {"publishedAt": "2022-12-15T03:45:16Z", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "What a nice day", "description": "Blue sky with cloud. updated.", "thumbnails": {"default": {"url": "https://i.ytimg.com/vi/fTK1Jj6QlDw/default.jpg", "width": 120, "height": 90}, "medium": {"url": "https://i.ytimg.com/vi/fTK1Jj6QlDw/mqdefault.jpg", "width": 320, "height": 180}, "high": {"url": "https://i.ytimg.com/vi/fTK1Jj6QlDw/hqdefault.jpg", "width": 480, "height": 360}, "standard": {"url": "https://i.ytimg.com/vi/fTK1Jj6QlDw/sddefault.jpg", "width": 640, "height": 480}, "maxres": {"url": "https://i.ytimg.com/vi/fTK1Jj6QlDw/maxresdefault.jpg", "width": 1280, "height": 720}}, "channelTitle": "ikaros data", "categoryId": "1", "liveBroadcastContent": "none", "localized": {"title": "What a nice day", "description": "Blue sky with cloud. updated."}, "defaultAudioLanguage": "en-US"}} ``` #### delete Deletes a YouTube video. ```python cli.videos.delete(video_id="fTK1Jj6QlDw") # True ``` #### rate Add a like or dislike rating to a video or remove a rating from a video. ```python cli.videos.rate(video_id="fTK1Jj6QlDw", rating="like") # True ``` #### reportAbuse Report a video for containing abusive content. ```python import pyyoutube.models as mds body = mds.VideoReportAbuse( videoId="fTK1Jj6QlDw", reasonId="32" ) cli.videos.report_abuse(body=body) # True ``` ================================================ FILE: docs/mkdocs.yml ================================================ site_name: Python-Youtube Docs site_description: Docs for python-youtube library site_url: https://sns-sdks.github.io/python-youtube/ repo_url: https://github.com/sns-sdks/python-youtube copyright: Copyright © 2019 - 2021 Ikaros kun theme: name: material features: - navigation.tabs palette: # Light mode - media: "(prefers-color-scheme: light)" scheme: default primary: indigo accent: indigo toggle: icon: material/toggle-switch-off-outline name: Switch to dark mode # Dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: blue accent: blue toggle: icon: material/toggle-switch name: Switch to light mode nav: - Introduction: index.md - Introduce Structure: introduce-new-structure.md - Usage: - Work With `Api`: usage/work-with-api.md - Work With `Client`: usage/work-with-client.md - Installation: installation.md - Getting Started: getting_started.md - Authorization: authorization.md - Changelog: CHANGELOG.md extra: social: - icon: fontawesome/brands/twitter link: https://twitter.com/realllkk520 - icon: fontawesome/brands/github link: https://github.com/sns-sdks/python-youtube markdown_extensions: - codehilite - admonition - pymdownx.superfences - pymdownx.emoji ================================================ FILE: examples/README.md ================================================ # Examples We provide two entry points to operate the YouTube DATA API. - Api `from pyyoutube import Api`: This is an old implementation used to be compatible with older versions of code. - Client `from pyyoutube import Client`: This is a new implementation for operating the API and provides additional capabilities. # Basic Usage ## API ```python from pyyoutube import Api api = Api(api_key="your key") api.get_channel_info(channel_id="id for channel") # ChannelListResponse(kind='youtube#channelListResponse') ``` You can get more examples at [api examples](/examples/apis/). ## Client ```python from pyyoutube import Client cli = Client(api_key="your key") cli.channels.list(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw") # ChannelListResponse(kind='youtube#channelListResponse') ``` You can get more examples at [client examples](/examples/clients/). ================================================ FILE: examples/__init__.py ================================================ ================================================ FILE: examples/apis/__init__.py ================================================ ================================================ FILE: examples/apis/channel_videos.py ================================================ """ Retrieve some videos info from given channel. Use pyyoutube.api.get_channel_info to get channel video uploads playlist id. Then use pyyoutube.api.get_playlist_items to get playlist's videos id. Last use get_video_by_id to get videos data. """ import pyyoutube API_KEY = "xxx" # replace this with your api key. def get_videos(channel_id): api = pyyoutube.Api(api_key=API_KEY) channel_info = api.get_channel_info(channel_id=channel_id) playlist_id = channel_info.items[0].contentDetails.relatedPlaylists.uploads uploads_playlist_items = api.get_playlist_items( playlist_id=playlist_id, count=10, limit=6 ) videos = [] for item in uploads_playlist_items.items: video_id = item.contentDetails.videoId video = api.get_video_by_id(video_id=video_id) videos.extend(video.items) return videos def processor(): channel_id = "UC_x5XG1OV2P6uZZ5FSM9Ttw" videos = get_videos(channel_id) with open("videos.json", "w+") as f: for video in videos: f.write(video.to_json()) f.write("\n") if __name__ == "__main__": processor() ================================================ FILE: examples/apis/get_all_videos_id_with_channel_by_search.py ================================================ """ Retrieve channel's videos by search api. Note Quota impact: A call to this method has a quota cost of 100 units. """ import pyyoutube API_KEY = "xxx" # replace this with your api key. def get_all_videos_id_by_channel(channel_id, limit=50, count=50): api = pyyoutube.Api(api_key=API_KEY) videos = [] next_page = None while True: res = api.search( channel_id=channel_id, limit=limit, count=count, page_token=next_page, ) next_page = res.nextPageToken for item in res.items: if item.id.videoId: videos.append(item.id.videoId) if not next_page: break return videos ================================================ FILE: examples/apis/get_subscription_with_oauth.py ================================================ """ This demo show how to use this library to do authorization and get your subscription. """ import pyyoutube import webbrowser CLIENT_ID = "your app id" CLIENT_SECRET = "your app secret" def get_subscriptions(): api = pyyoutube.Api(client_id=CLIENT_ID, client_secret=CLIENT_SECRET) # need follows scope scope = ["https://www.googleapis.com/auth/youtube.readonly"] url, _ = api.get_authorization_url(scope=scope) print( "Try to start a browser to visit the authorization page. If not opened. you can copy and visit by hand:\n" f"{url}" ) webbrowser.open(url) auth_response = input( "\nCopy the whole url if you finished the step to authorize:\n" ) api.generate_access_token(authorization_response=auth_response, scope=scope) sub_res = api.get_subscription_by_me(mine=True, parts="id,snippet", count=None) with open("subscriptions.json", "w+") as f: f.write(sub_res.to_json()) print("Finished.") if __name__ == "__main__": get_subscriptions() ================================================ FILE: examples/apis/oauth_flow.py ================================================ """ This example demonstrates how to perform authorization. """ from pyyoutube import Api CLIENT_ID = "xxx" # Your app id CLIENT_SECRET = "xxx" # Your app secret SCOPE = [ "https://www.googleapis.com/auth/youtube", "https://www.googleapis.com/auth/youtube.force-ssl", "https://www.googleapis.com/auth/userinfo.profile", ] def do_authorize(): api = Api(client_id=CLIENT_ID, client_secret=CLIENT_SECRET) authorize_url, state = api.get_authorization_url(scope=SCOPE) print(f"Click url to do authorize: {authorize_url}") response_uri = input("Input youtube redirect uri:\n") token = api.generate_access_token(authorization_response=response_uri, scope=SCOPE) print(f"Your token: {token}") # get data profile = api.get_profile() print(f"Your channel id: {profile.id}") if __name__ == "__main__": do_authorize() ================================================ FILE: examples/clients/__init__.py ================================================ ================================================ FILE: examples/clients/channel_info.py ================================================ """ This example demonstrates how to retrieve information for a channel. """ from pyyoutube import Client API_KEY = "Your key" # replace this with your api key. def get_channel_info(): cli = Client(api_key=API_KEY) channel_id = "UC_x5XG1OV2P6uZZ5FSM9Ttw" resp = cli.channels.list( channel_id=channel_id, parts=["id", "snippet", "statistics"], return_json=True ) print(f"Channel info: {resp['items'][0]}") if __name__ == "__main__": get_channel_info() ================================================ FILE: examples/clients/oauth_flow.py ================================================ """ This example demonstrates how to perform authorization. """ from pyyoutube import Client CLIENT_ID = "xxx" # Your app id CLIENT_SECRET = "xxx" # Your app secret CLIENT_SECRET_PATH = None # or your path/to/client_secret_web.json SCOPE = [ "https://www.googleapis.com/auth/youtube", "https://www.googleapis.com/auth/youtube.force-ssl", "https://www.googleapis.com/auth/userinfo.profile", ] def do_authorize(): cli = Client(client_id=CLIENT_ID, client_secret=CLIENT_SECRET) # or if you want to use a web type client_secret.json # cli = Client(client_secret_path=CLIENT_SECRET_PATH) authorize_url, state = cli.get_authorize_url(scope=SCOPE) print(f"Click url to do authorize: {authorize_url}") response_uri = input("Input youtube redirect uri:\n") token = cli.generate_access_token(authorization_response=response_uri, scope=SCOPE) print(f"Your token: {token}") # get data resp = cli.channels.list(mine=True) print(f"Your channel id: {resp.items[0].id}") if __name__ == "__main__": do_authorize() ================================================ FILE: examples/clients/oauth_refreshing.py ================================================ """ This example demonstrates how to automatically (re)generate tokens for continuous OAuth. We store the Access Token in a seperate .env file to be used later. """ from pyyoutube import Client from json import loads, dumps from pathlib import Path CLIENT_ID = "xxx" # Your app id CLIENT_SECRET = "xxx" # Your app secret CLIENT_SECRET_PATH = None # or your path/to/client_secret_web.json TOKEN_PERSISTENT_PATH = None # path/to/persistent_token_storage_location SCOPE = [ "https://www.googleapis.com/auth/youtube", "https://www.googleapis.com/auth/youtube.force-ssl", "https://www.googleapis.com/auth/userinfo.profile", ] def do_refresh(): token_location = Path(TOKEN_PERSISTENT_PATH) # Read the persistent token data if it exists token_data = {} if token_location.exists(): token_data = loads(token_location.read_text()) cli = Client( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_token=token_data.get("access_token"), refresh_token=token_data.get("refresh_token"), ) # or if you want to use a web type client_secret.json # cli = Client( # client_secret_path=CLIENT_SECRET_PATH, # access_token=token_data.get("access_token"), # refresh_token=token_data.get("refresh_token") # ) # If no access token is provided, this is the same as oauth_flow.py if not cli._has_auth_credentials(): authorize_url, state = cli.get_authorize_url(scope=SCOPE) print(f"Click url to do authorize: {authorize_url}") response_uri = input("Input youtube redirect uri:\n") token = cli.generate_access_token( authorization_response=response_uri, scope=SCOPE ) print(f"Your token: {token}") # Otherwise, refresh the access token if it has expired else: token = cli.refresh_access_token(cli.refresh_token) # we add the token data to the client and token objects so that they are complete token.refresh_token = cli.refresh_token cli.access_token = token.access_token print(f"Your token: {token}") # Write the token data to the persistent location to be used again, ensuring the file exists token_location.mkdir(parents=True, exist_ok=True) token_location.write_text( dumps( {"access_token": token.access_token, "refresh_token": token.refresh_token} ) ) # Now you can do things with the client resp = cli.channels.list(mine=True) print(f"Your channel id: {resp.items[0].id}") if __name__ == "__main__": do_refresh() ================================================ FILE: examples/clients/upload_video.py ================================================ """ This example demonstrates how to upload a video. """ import pyyoutube.models as mds from pyyoutube import Client from pyyoutube.media import Media # Access token with scope: # https://www.googleapis.com/auth/youtube.upload # https://www.googleapis.com/auth/youtube # https://www.googleapis.com/auth/youtube.force-ssl ACCESS_TOKEN = "xxx" def upload_video(): cli = Client(access_token=ACCESS_TOKEN) body = mds.Video( snippet=mds.VideoSnippet(title="video title", description="video description") ) media = Media(filename="target_video.mp4") upload = cli.videos.insert( body=body, media=media, parts=["snippet"], notify_subscribers=True ) response = None while response is None: print(f"Uploading video...") status, response = upload.next_chunk() if status is not None: print(f"Uploading video progress: {status.progress()}...") # Use video class to representing the video resource. video = mds.Video.from_dict(response) print(f"Video id {video.id} was successfully uploaded.") if __name__ == "__main__": upload_video() ================================================ FILE: pyproject.toml ================================================ [tool.poetry] name = "python-youtube" version = "0.9.9" description = "A Python wrapper around for YouTube Data API." authors = ["ikaroskun "] license = "MIT" keywords = ["youtube-api", "youtube-v3-api", "youtube-data-api", "youtube-sdk"] readme = "README.rst" homepage = "https://github.com/sns-sdks/python-youtube" repository = "https://github.com/sns-sdks/python-youtube" classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] packages = [ { include = "pyyoutube" }, { include = "tests", format = "sdist" }, ] [tool.poetry.dependencies] python = "^3.9" requests = ">=2.28.0,<3.0.0" requests-oauthlib = ">=1.3.0,<3.0.0" isodate = ">=0.6.1,<1.0.0" dataclasses-json = ">=0.6.0,<1.0.0" [tool.poetry.group.dev.dependencies] responses = "^0.25.0" pytest = "^8.4.0" pytest-cov = "^6.2.0" [build-system] requires = ["poetry-core>=2.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: pytest.ini ================================================ [pytest] addopts = --cov=pyyoutube --cov-report xml ================================================ FILE: pyyoutube/__init__.py ================================================ from .api import Api # noqa from .client import Client # noqa from .error import * # noqa from .models import * # noqa from .utils.constants import TOPICS # noqa ================================================ FILE: pyyoutube/__version__.py ================================================ # d8888b. db db d888888b db db .d88b. d8b db db db .d88b. db db d888888b db db d8888b. d88888b # 88 `8D `8b d8' `~~88~~' 88 88 .8P Y8. 888o 88 `8b d8' .8P Y8. 88 88 `~~88~~' 88 88 88 `8D 88' # 88oodD' `8bd8' 88 88ooo88 88 88 88V8o 88 `8bd8' 88 88 88 88 88 88 88 88oooY' 88ooooo # 88~~~ 88 88 88~~~88 88 88 88 V8o88 88 88 88 88 88 88 88 88 88~~~b. 88~~~~~ # 88 88 88 88 88 `8b d8' 88 V888 88 `8b d8' 88b d88 88 88b d88 88 8D 88. # 88 YP YP YP YP `Y88P' VP V8P YP `Y88P' ~Y8888P' YP ~Y8888P' Y8888P' Y88888P __version__ = "0.9.9" ================================================ FILE: pyyoutube/api.py ================================================ """ Main Api implementation. """ from typing import Optional, List, Union import requests from requests.auth import HTTPBasicAuth from requests.models import Response from requests_oauthlib.oauth2_session import OAuth2Session from pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException from pyyoutube.models import ( AccessToken, UserProfile, ActivityListResponse, CaptionListResponse, ChannelListResponse, ChannelSectionResponse, PlaylistListResponse, PlaylistItemListResponse, VideoListResponse, CommentThreadListResponse, CommentListResponse, VideoCategoryListResponse, SearchListResponse, SubscriptionListResponse, I18nRegionListResponse, I18nLanguageListResponse, MemberListResponse, MembershipsLevelListResponse, VideoAbuseReportReasonListResponse, ) from pyyoutube.utils.params_checker import enf_comma_separated, enf_parts class Api(object): """ Example usage: To create an instance of pyyoutube.Api class: >>> import pyyoutube >>> api = pyyoutube.Api(api_key="your api key") To get one channel info: >>> res = api.get_channel_info(channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw") >>> print(res.items[0]) Now this api provide methods as follows: >>> api.get_authorization_url() >>> api.generate_access_token() >>> api.refresh_token() >>> api.get_channel_info() >>> api.get_playlist_by_id() >>> api.get_playlists() >>> api.get_playlist_item_by_id() >>> api.get_playlist_items() >>> api.get_video_by_id() >>> api.get_videos_by_chart() >>> api.get_videos_by_myrating() >>> api.get_comment_thread_by_id() >>> api.get_comment_threads() >>> api.get_comment_by_id() >>> api.get_comments() >>> api.get_video_categories() >>> api.get_subscription_by_id() >>> api.get_subscription_by_channel() >>> api.get_subscription_by_me() >>> api.get_activities_by_channel() >>> api.get_activities_by_me() >>> api.get_captions_by_video() >>> api.get_channel_sections_by_id() >>> api.get_channel_sections_by_channel() >>> api.get_i18n_regions() >>> api.get_i18n_languages() >>> api.get_video_abuse_report_reason() >>> api.search() >>> api.search_by_keywords() >>> api.search_by_developer() >>> api.search_by_mine() >>> api.search_by_related_video() """ BASE_URL = "https://www.googleapis.com/youtube/v3/" AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/v2/auth" EXCHANGE_ACCESS_TOKEN_URL = "https://oauth2.googleapis.com/token" USER_INFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo" DEFAULT_REDIRECT_URI = "https://localhost/" DEFAULT_SCOPE = [ "https://www.googleapis.com/auth/youtube", "https://www.googleapis.com/auth/userinfo.profile", ] DEFAULT_STATE = "PyYouTube" DEFAULT_TIMEOUT = 10 DEFAULT_QUOTA = 10000 # this quota reset at 00:00:00(GMT-7) every day. def __init__( self, client_id: Optional[str] = None, client_secret: Optional[str] = None, api_key: Optional[str] = None, access_token: Optional[str] = None, timeout: Optional[int] = None, proxies: Optional[dict] = None, ) -> None: """ This Api provide two method to work. Use api key or use access token. Args: client_id(str, optional): Your google app's ID. client_secret (str, optional): Your google app's secret. api_key(str, optional): The api key which you create from google api console. access_token(str, optional): If you not provide api key, you can do authorization to get an access token. If all api key and access token provided. Use access token first. timeout(int, optional): The request timeout. proxies(dict, optional): If you want use proxy, need point this param. param style like requests lib style. Refer https://2.python-requests.org//en/latest/user/advanced/#proxies Returns: YouTube Api instance. """ self._client_id = client_id self._client_secret = client_secret self._api_key = api_key self._access_token = access_token self._refresh_token = None # This keep current user's refresh token. self._timeout = timeout self.session = requests.Session() self.proxies = proxies if not ( (self._client_id and self._client_secret) or self._api_key or self._access_token ): raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Must specify either client key info or api key.", ) ) if self._timeout is None: self._timeout = self.DEFAULT_TIMEOUT def _get_oauth_session( self, redirect_uri: Optional[str] = None, scope: Optional[List[str]] = None, **kwargs, ) -> OAuth2Session: """ Build a request session for OAuth. Args: redirect_uri(str, optional) Determines how Google's authorization server sends a response to your app. If not provide will use default https://localhost/ scope (list, optional) The scope you want give permission. If you not provide, will use default scope. kwargs(dict, optional) Some other params you want provide. Returns: OAuth2 Session """ if redirect_uri is None: redirect_uri = self.DEFAULT_REDIRECT_URI if scope is None: scope = self.DEFAULT_SCOPE return OAuth2Session( client_id=self._client_id, scope=scope, redirect_uri=redirect_uri, state=self.DEFAULT_STATE, **kwargs, ) def get_authorization_url( self, redirect_uri: Optional[str] = None, scope: Optional[List[str]] = None, **kwargs, ) -> (str, str): """ Build authorization url to do authorize. Args: redirect_uri(str, optional) Determines how Google's authorization server sends a response to your app. If not provide will use default https://localhost/ scope (list, optional) The scope you want give permission. If you not provide, will use default scope. kwargs(dict, optional) Some other params you want provide. Returns: The uri you can open on browser to do authorize. """ oauth_session = self._get_oauth_session( redirect_uri=redirect_uri, scope=scope, **kwargs, ) authorization_url, state = oauth_session.authorization_url( self.AUTHORIZATION_URL, access_type="offline", prompt="select_account", **kwargs, ) return authorization_url, state def generate_access_token( self, authorization_response: str, redirect_uri: Optional[str] = None, scope: Optional[List[str]] = None, return_json: bool = False, **kwargs, ) -> Union[dict, AccessToken]: """ Use the google auth response to get access token Args: authorization_response (str) The response url which google redirect. redirect_uri(str, optional) Determines how Google's authorization server sends a response to your app. If not provide will use default https://localhost/ scope (list, optional) The scope you want give permission. If you not provide, will use default scope. return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.AccessToken kwargs(dict, optional) Some other params you want provide. Return: Retrieved access token's info, pyyoutube.AccessToken instance. """ oauth_session = self._get_oauth_session( redirect_uri=redirect_uri, scope=scope, **kwargs, ) token = oauth_session.fetch_token( self.EXCHANGE_ACCESS_TOKEN_URL, client_secret=self._client_secret, authorization_response=authorization_response, proxies=self.proxies, ) self._access_token = oauth_session.access_token self._refresh_token = oauth_session.token["refresh_token"] if return_json: return token else: return AccessToken.from_dict(token) def refresh_token( self, refresh_token: Optional[str] = None, return_json: bool = False ) -> Union[dict, AccessToken]: """ Refresh token by api return refresh token. Args: refresh_token (str) The refresh token which the api returns. return_json (bool, optional): If True JSON data will be returned, instead of pyyoutube.AccessToken Return: Retrieved new access token's info, pyyoutube.AccessToken instance. """ refresh_token = refresh_token if refresh_token else self._refresh_token if refresh_token is None: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message=f"Must provide the refresh token or api has been authorized.", ) ) oauth_session = OAuth2Session(client_id=self._client_id) auth = HTTPBasicAuth(self._client_id, self._client_secret) new_token = oauth_session.refresh_token( self.EXCHANGE_ACCESS_TOKEN_URL, refresh_token=refresh_token, auth=auth, ) self._access_token = oauth_session.access_token if return_json: return new_token else: return AccessToken.from_dict(new_token) @staticmethod def _parse_response(response: Response) -> dict: """ Parse response data and check whether errors exists. Args: response (Response) The response which the request return. Return: response's data """ data = response.json() if "error" in data: raise PyYouTubeException(response) return data @staticmethod def _parse_data(data: Optional[dict]) -> Union[dict, list]: """ Parse resp data. Args: data (dict) The response data by response.json() Return: response's items """ items = data["items"] return items def _request( self, resource, method=None, args=None, post_args=None, enforce_auth=True ) -> Response: """ Main request sender. Args: resource(str) Resource field is which type data you want to retrieve. Such as channels,videos and so on. method(str, optional) The method this request to send request. Default is 'GET' args(dict, optional) The url params for this request. post_args(dict, optional) The Post params for this request. enforce_auth(bool, optional) Whether use google credentials Returns: response """ if method is None: method = "GET" if args is None: args = dict() if post_args is not None: method = "POST" key = None access_token = None if self._api_key is not None: key = "key" access_token = self._api_key if self._access_token is not None: key = "access_token" access_token = self._access_token if access_token is None and enforce_auth: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="You must provide your credentials.", ) ) if enforce_auth: if method == "POST" and key not in post_args: post_args[key] = access_token elif method == "GET" and key not in args: args[key] = access_token try: response = self.session.request( method=method, url=self.BASE_URL + resource, timeout=self._timeout, params=args, data=post_args, proxies=self.proxies, ) except requests.HTTPError as e: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message=e.args[0]) ) else: return response def get_profile( self, access_token: Optional[str] = None, return_json: Optional[bool] = False ) -> Union[dict, UserProfile]: """ Get token user info. Args: access_token(str, optional) user access token. If not provide, use api instance access token return_json(bool, optional) The return data type. If you set True JSON data will be returned. False will return pyyoutube.UserProfile Returns: The data for you given access token's user info. """ if access_token is None: access_token = self._access_token if access_token is None: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message=f"Must provide the access token or api has been authorized.", ) ) try: response = self.session.get( self.USER_INFO_URL, params={"access_token": access_token}, timeout=self._timeout, proxies=self.proxies, ) except requests.HTTPError as e: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message=e.args[0]) ) data = self._parse_response(response) if return_json: return data else: return UserProfile.from_dict(data) def paged_by_page_token( self, resource: str, args: dict, count: Optional[int] = None, ): """ Response paged by response's page token. If not provide response token Args: resource (str): The resource string need to retrieve data. args (dict) The args for api. count (int, optional): The count for result items you want to get. If provide this with None, will retrieve all items. Note: The all items maybe too much. Notice your app's cost. Returns: Data api origin response. """ res_data: Optional[dict] = None current_items: List[dict] = [] page_token: Optional[str] = None now_items_count: int = 0 while True: if page_token is not None: args["pageToken"] = page_token resp = self._request(resource=resource, method="GET", args=args) data = self._parse_response(resp) # origin response # set page token page_token = data.get("nextPageToken") prev_page_token = data.get("prevPageToken") # parse results. items = self._parse_data(data) current_items.extend(items) now_items_count += len(items) if res_data is None: res_data = data # first check the count if satisfies. if count is not None: if now_items_count >= count: current_items = current_items[:count] break # if have no page token, mean no more data. if page_token is None: break res_data["items"] = current_items # use last request page token res_data["nextPageToken"] = page_token res_data["prevPageToken"] = prev_page_token return res_data def get_activities_by_channel( self, *, channel_id: str, parts: Optional[Union[str, list, tuple, set]] = None, before: Optional[str] = None, after: Optional[str] = None, region_code: Optional[str] = None, count: Optional[int] = 20, limit: int = 20, page_token: Optional[str] = None, return_json: bool = False, ): """ Retrieve given channel's activities data. Args: channel_id (str): The id for channel which you want to get activities data. parts ((str,list,tuple,set) optional): The resource parts for activities you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. before (str, optional): Set this will only return the activities occurred before this timestamp. This need specified in ISO 8601 (YYYY-MM-DDThh:mm:ss.sZ) format. after (str, optional): Set this will only return the activities occurred after this timestamp. This need specified in ISO 8601 (YYYY-MM-DDThh:mm:ss.sZ) format. region_code (str, optional): Set this will only return the activities for the specified country. This need specified with an ISO 3166-1 alpha-2 country code. count (int, optional): The count will retrieve activities data. Default is 20. If provide this with None, will retrieve all activities. limit (int, optional): The maximum number of items each request retrieve. For activities, this should not be more than 50. Default is 20. page_token (str, optional): The token of the page of activities result to retrieve. You can use this retrieve point result page directly. And you should know about the page result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.ActivityListResponse instance. Returns: ActivityListResponse or original data. """ if count is None: limit = 50 # for activities the max limit for per request is 50 else: limit = min(count, limit) args = { "channelId": channel_id, "part": enf_parts(resource="activities", value=parts), "maxResults": limit, } if before: args["publishedBefore"] = before if after: args["publishedAfter"] = after if region_code: args["regionCode"] = region_code if page_token is not None: args["pageToken"] = page_token res_data = self.paged_by_page_token( resource="activities", args=args, count=count ) if return_json: return res_data else: return ActivityListResponse.from_dict(res_data) def get_activities_by_me( self, *, parts: Optional[Union[str, list, tuple, set]] = None, before: Optional[str] = None, after: Optional[str] = None, region_code: Optional[str] = None, count: Optional[int] = 20, limit: int = 20, page_token: Optional[str] = None, return_json: bool = False, ): """ Retrieve authorized user's activities. Note: This need you do authorize first. Args: parts ((str,list,tuple,set) optional): The resource parts for activities you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. before (str, optional): Set this will only return the activities occurred before this timestamp. This need specified in ISO 8601 (YYYY-MM-DDThh:mm:ss.sZ) format. after (str, optional): Set this will only return the activities occurred after this timestamp. This need specified in ISO 8601 (YYYY-MM-DDThh:mm:ss.sZ) format. region_code (str, optional): Set this will only return the activities for the specified country. This need specified with an ISO 3166-1 alpha-2 country code. count (int, optional): The count will retrieve activities data. Default is 20. If provide this with None, will retrieve all activities. limit (int, optional): The maximum number of items each request retrieve. For activities, this should not be more than 50. Default is 20. page_token (str, optional): The token of the page of activities result to retrieve. You can use this retrieve point result page directly. And you should know about the page result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.ActivityListResponse instance. Returns: ActivityListResponse or original data. """ if count is None: limit = 50 # for activities the max limit for per request is 50 else: limit = min(count, limit) args = { "mine": True, "part": enf_parts(resource="activities", value=parts), "maxResults": limit, } if before: args["publishedBefore"] = before if after: args["publishedAfter"] = after if region_code: args["regionCode"] = region_code if page_token is not None: args["pageToken"] = page_token res_data = self.paged_by_page_token( resource="activities", args=args, count=count ) if return_json: return res_data else: return ActivityListResponse.from_dict(res_data) def get_captions_by_video( self, *, video_id: str, parts: Optional[Union[str, list, tuple, set]] = None, caption_id: Optional[Union[str, list, tuple, set]] = None, return_json: bool = False, ): """ Retrieve authorized user's video's caption data. Note: This need you do authorize first. Args: video_id (str): The id for video which you want to get caption. parts ((str,list,tuple,set) optional): The resource parts for caption you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. caption_id ((str,list,tuple,set)): The id for caption that you want to get data. You can pass this with single id str,comma-separated id str, or list, tuple, set of id str. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.CaptionListResponse instance. Returns: CaptionListResponse or original data. """ args = { "videoId": video_id, "part": enf_parts("captions", parts), } if caption_id is not None: args["id"] = enf_comma_separated("caption_id", caption_id) resp = self._request(resource="captions", method="GET", args=args) data = self._parse_response(resp) if return_json: return data else: return CaptionListResponse.from_dict(data) def get_channel_info( self, *, channel_id: Optional[Union[str, list, tuple, set]] = None, for_handle: Optional[str] = None, for_username: Optional[str] = None, mine: Optional[bool] = None, parts: Optional[Union[str, list, tuple, set]] = None, hl: str = "en_US", return_json: Optional[bool] = False, ): """ Retrieve channel data from YouTube Data API. Note: 1. Don't know why, but now you couldn't get channel list by given an guide category. You can only get list by parameters mine,forUsername,id. Refer: https://developers.google.com/youtube/v3/guides/implementation/channels 2. The origin maxResult param not work for these filter method. Args: channel_id ((str,list,tuple,set), optional): The id or comma-separated id string for youtube channel which you want to get. You can also pass this with an id list, tuple, set. for_handle (str, optional): The parameter specifies a YouTube handle, thereby requesting the channel associated with that handle. The parameter value can be prepended with an @ symbol. For example, to retrieve the resource for the "Google for Developers" channel, set the forHandle parameter value to either GoogleDevelopers or @GoogleDevelopers. for_username (str, optional): The name for YouTube username which you want to get. Note: This name may the old youtube version's channel's user's username, Not the the channel name. Refer: https://developers.google.com/youtube/v3/guides/working_with_channel_ids mine (bool, optional): If you have give the authorization. Will return your channels. Must provide the access token. parts (str, optional): Comma-separated list of one or more channel resource properties. If not provided. will use default public properties. hl (str, optional): If provide this. Will return channel's language localized info. This value need https://developers.google.com/youtube/v3/docs/i18nLanguages. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.ChannelListResponse instance. Returns: ChannelListResponse instance or original data. """ args = { "part": enf_parts(resource="channels", value=parts), "hl": hl, } if for_handle is not None: args["forHandle"] = for_handle elif for_username is not None: args["forUsername"] = for_username elif channel_id is not None: args["id"] = enf_comma_separated("channel_id", channel_id) elif mine is not None: args["mine"] = mine else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message=f"Specify at least one of channel_id,channel_name or mine", ) ) resp = self._request(resource="channels", method="GET", args=args) data = self._parse_response(resp) if return_json: return data else: return ChannelListResponse.from_dict(data) def get_channel_sections_by_id( self, *, section_id: Union[str, list, tuple, set], parts: Optional[Union[str, list, tuple, set]] = None, return_json: Optional[bool] = False, ) -> Union[ChannelSectionResponse, dict]: """ Retrieve channel section info by his ids(s). Args: section_id: The id(s) for channel sections. You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids. parts: The resource parts for channel section you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. return_json: The return data type. If you set True JSON data will be returned. False will return a pyyoutube.ChannelSectionResponse instance. Returns: ChannelSectionResponse or original data. """ args = { "id": enf_comma_separated(field="section_id", value=section_id), "part": enf_parts(resource="channelSections", value=parts), } resp = self._request(resource="channelSections", args=args) data = self._parse_response(resp) if return_json: return data else: return ChannelSectionResponse.from_dict(data) def get_channel_sections_by_channel( self, *, channel_id: Optional[str] = None, mine: bool = False, parts: Optional[Union[str, list, tuple, set]] = None, return_json: Optional[bool] = False, ) -> Union[ChannelSectionResponse, dict]: """ Retrieve channel sections by channel id. Args: channel_id: The id for channel which you want to get channel sections. mine: If you want to get your channel's sections, set this with True. And this need your authorization. parts: The resource parts for channel section you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. return_json: The return data type. If you set True JSON data will be returned. False will return a pyyoutube.ChannelSectionResponse instance. Returns: ChannelSectionResponse or original data. """ args = { "part": enf_parts(resource="channelSections", value=parts), } if mine: args["mine"] = mine else: args["channelId"] = channel_id resp = self._request(resource="channelSections", args=args) data = self._parse_response(resp) if return_json: return data else: return ChannelSectionResponse.from_dict(data) def get_comment_by_id( self, *, comment_id: Union[str, list, tuple, set], parts: Optional[Union[str, list, tuple, set]] = None, text_format: Optional[str] = "html", return_json: Optional[bool] = False, ): """ Retrieve comment info by given comment id str. Args: comment_id (str, optional): The id for comment that you want to retrieve data. You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids. parts ((str,list,tuple,set), optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. text_format (str, optional): Comments left by users format style. Acceptable values are: html, plainText. Default is html. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.CommentListResponse instance. Returns: CommentListResponse or original data """ args = { "id": enf_comma_separated(field="comment_id", value=comment_id), "part": enf_parts(resource="comments", value=parts), "textFormat": text_format, } resp = self._request(resource="comments", method="GET", args=args) data = self._parse_response(resp) if return_json: return data else: return CommentListResponse.from_dict(data) def get_comments( self, *, parent_id: str, parts: Optional[Union[str, list, tuple, set]] = None, text_format: Optional[str] = "html", count: Optional[int] = 20, limit: Optional[int] = 20, page_token: Optional[str] = None, return_json: Optional[bool] = False, ): """ Retrieve comments info by given parent id. Note: YouTube currently supports replies only for top-level comments. However, replies to replies may be supported in the future. Args: parent_id (str): Provide the ID of the comment for which replies should be retrieved. parts ((str,list,tuple,set), optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. text_format (str, optional): Comments left by users format style. Acceptable values are: html, plainText. Default is html. count (int, optional): The count will retrieve videos data. Default is 20. If provide this with None, will retrieve all comments. limit (int, optional): The maximum number of items each request retrieve. For comments, this should not be more than 100. Default is 20. page_token(str, optional): The token of the page of comments result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.CommentListResponse instance. Returns: CommentListResponse or original data """ if count is None: limit = 100 # for comments the max limit for per request is 100 else: limit = min(count, limit) args = { "parentId": parent_id, "part": enf_parts(resource="comments", value=parts), "textFormat": text_format, "maxResults": limit, } if page_token is not None: args["pageToken"] = page_token res_data = self.paged_by_page_token(resource="comments", args=args, count=count) if return_json: return res_data else: return CommentListResponse.from_dict(res_data) def get_comment_thread_by_id( self, *, comment_thread_id: Union[str, list, tuple, set], parts: Optional[Union[str, list, tuple, set]] = None, text_format: Optional[str] = "html", return_json: Optional[bool] = False, ): """ Retrieve the comment thread info by given id. Args: comment_thread_id ((str,list,tuple,set)): The id for comment thread that you want to retrieve data. You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids. parts ((str,list,tuple,set), optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. text_format (str, optional): Comments left by users format style. Acceptable values are: html, plainText. Default is html. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.CommentThreadListResponse instance. Returns: CommentThreadListResponse or original data """ args = { "id": enf_comma_separated("comment_thread_id", comment_thread_id), "part": enf_parts(resource="commentThreads", value=parts), "textFormat": text_format, } resp = self._request(resource="commentThreads", method="GET", args=args) data = self._parse_response(resp) if return_json: return data else: return CommentThreadListResponse.from_dict(data) def get_comment_threads( self, *, all_to_channel_id: Optional[str] = None, channel_id: Optional[str] = None, video_id: Optional[str] = None, parts: Optional[Union[str, list, tuple, set]] = None, moderation_status: Optional[str] = None, order: Optional[str] = None, search_terms: Optional[str] = None, text_format: Optional[str] = "html", count: Optional[int] = 20, limit: Optional[int] = 20, page_token: Optional[str] = None, return_json: Optional[bool] = False, ): """ Retrieve the comment threads info by given filter condition. Args: all_to_channel_id (str, optional): If you provide this with a channel id, will return all comment threads associated with the channel. The response can include comments about the channel or about the channel's videos. channel_id (str, optional): If you provide this with a channel id, will return the comment threads associated with the channel. But the response not include comments about the channel's videos. video_id (str, optional): If you provide this with a video id, will return the comment threads associated with the video. parts ((str,list,tuple,set), optional) The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. moderation_status (str, optional): This parameter must used with authorization. If you provide this. the response will return comment threads match this filter only. Acceptable values are: - heldForReview: Retrieve comment threads that are awaiting review by a moderator. - likelySpam: Retrieve comment threads classified as likely to be spam. - published: Retrieve threads of published comments. this is default for all. See more: https://developers.google.com/youtube/v3/docs/commentThreads/list#parameters order (str, optional): Order parameter specifies the order in which the API response should list comment threads. Acceptable values are: - time: Comment threads are ordered by time. This is the default behavior. - relevance: Comment threads are ordered by relevance. search_terms (str, optional): The searchTerms parameter instructs the API to limit the API response to only contain comments that contain the specified search terms. text_format (str, optional): Comments left by users format style. Acceptable values are: html, plainText. Default is html. count (int, optional): The count will retrieve comment threads data. Default is 20. If provide this with None, will retrieve all comment threads. limit (int, optional): The maximum number of items each request retrieve. For comment threads, this should not be more than 100. Default is 20. page_token(str, optional): The token of the page of commentThreads result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.CommentThreadListResponse instance. Returns: CommentThreadListResponse or original data """ if count is None: limit = 100 # for commentThreads the max limit for per request is 100 else: limit = min(count, limit) args = { "part": enf_parts(resource="commentThreads", value=parts), "maxResults": limit, "textFormat": text_format, } if all_to_channel_id: args["allThreadsRelatedToChannelId"] = (all_to_channel_id,) elif channel_id: args["channelId"] = channel_id elif video_id: args["videoId"] = video_id else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message=f"Specify at least one of all_to_channel_id, channel_id or video_id", ) ) if moderation_status: args["moderationStatus"] = moderation_status if order: args["order"] = order if search_terms: args["searchTerms"] = search_terms if page_token is not None: args["pageToken"] = page_token res_data = self.paged_by_page_token( resource="commentThreads", args=args, count=count ) if return_json: return res_data else: return CommentThreadListResponse.from_dict(res_data) def get_i18n_languages( self, *, parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = "en_US", return_json: Optional[bool] = False, ) -> Union[I18nLanguageListResponse, dict]: """ Returns a list of application languages that the YouTube website supports. Args: parts: The resource parts for i18n language you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. hl: If provide this. Will return i18n language's language localized info. This value need https://developers.google.com/youtube/v3/docs/i18nLanguages. return_json: The return data type. If you set True JSON data will be returned. False will return a pyyoutube.I18nLanguageListResponse instance. Returns: I18nLanguageListResponse or original data. """ args = {"hl": hl, "part": enf_parts(resource="i18nLanguages", value=parts)} resp = self._request(resource="i18nLanguages", args=args) data = self._parse_response(resp) if return_json: return data else: return I18nLanguageListResponse.from_dict(data) def get_i18n_regions( self, *, parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = "en_US", return_json: Optional[bool] = False, ) -> Union[I18nRegionListResponse, dict]: """ Retrieve all available regions. Args: parts: The resource parts for i18n region you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. hl: If provide this. Will return i18n region's language localized info. This value need https://developers.google.com/youtube/v3/docs/i18nLanguages. return_json: The return data type. If you set True JSON data will be returned. False will return a pyyoutube.I18nRegionListResponse instance. Returns: I18nRegionListResponse or origin data """ args = {"hl": hl, "part": enf_parts(resource="i18nRegions", value=parts)} resp = self._request(resource="i18nRegions", args=args) data = self._parse_response(resp) if return_json: return data else: return I18nRegionListResponse.from_dict(data) def get_members( self, *, parts: Optional[Union[str, list, tuple, set]] = None, mode: Optional[str] = "all_current", count: Optional[int] = 5, limit: Optional[int] = 5, page_token: Optional[str] = None, has_access_to_level: Optional[str] = None, filter_by_member_channel_id: Optional[Union[str, list, tuple, set]] = None, return_json: Optional[bool] = False, ) -> Union[MemberListResponse, dict]: """ Retrieve a list of members for a channel. Args: parts ((str,list,tuple,set) optional): The resource parts for member you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. mode: The mode parameter indicates which members will be included in the API response. Set the parameter value to one of the following values: - all_current (default): List current members, from newest to oldest. When this value is used, the end of the list is reached when the API response does not contain a nextPageToken. - updates : List only members that joined or upgraded since the previous API call. Note: The first call starts a new stream of updates but does not actually return any members. To start retrieving the membership updates, you need to poll the endpoint using the nextPageToken at your desired frequency. Note that when this value is used, the API response always contains a nextPageToken. count (int, optional): The count will retrieve videos data. Default is 5. limit (int, optional): The maximum number of items each request retrieve. For members, this should not be more than 1000. Default is 5. page_token (str, optional): The token of the page of search result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. has_access_to_level (str, optional): The hasAccessToLevel parameter value is a level ID that specifies the minimum level that members in the result set should have. filter_by_member_channel_id ((str,list,tuple,set) optional): A list of channel IDs that can be used to check the membership status of specific users. A maximum of 100 channels can be specified per call. return_json (bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.MemberListResponse instance. Returns: MemberListResponse or original data """ if count is None: limit = 1000 else: limit = min(count, limit) args = { "part": enf_parts(resource="members", value=parts), "maxResults": limit, } if mode: args["mode"] = mode if page_token is not None: args["pageToken"] = page_token if has_access_to_level: args["hasAccessToLevel"] = has_access_to_level if filter_by_member_channel_id: args["filterByMemberChannelId"] = enf_parts( resource="filterByMemberChannelId", value=filter_by_member_channel_id, check=False, ) res_data = self.paged_by_page_token( resource="members", args=args, count=count, ) if return_json: return res_data else: return MemberListResponse.from_dict(res_data) def get_membership_levels( self, *, parts: Optional[Union[str, list, tuple, set]] = None, return_json: Optional[bool] = False, ) -> Union[MembershipsLevelListResponse, dict]: """ Retrieve membership levels for a channel Notes: This requires your authorization. Args: parts ((str,list,tuple,set) optional): The resource parts for membership level you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. return_json (bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.MembershipsLevelListResponse instance. Returns: MembershipsLevelListResponse or original data """ args = { "part": enf_parts(resource="membershipsLevels", value=parts), } resp = self._request(resource="membershipsLevels", args=args) data = self._parse_response(resp) if return_json: return data else: return MembershipsLevelListResponse.from_dict(data) def get_playlist_item_by_id( self, *, playlist_item_id: Union[str, list, tuple, set], parts: Optional[Union[str, list, tuple, set]] = None, return_json: Optional[bool] = False, ): """ Retrieve playlist Items info by your given id Args: playlist_item_id ((str,list,tuple,set)): The id for playlist item that you want to retrieve info. You can pass this with single id str, comma-separated id str. Or a list,tuple,set of ids. parts ((str,list,tuple,set) optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.PlayListItemApiResponse instance. Returns: PlaylistItemListResponse or original data """ args = { "id": enf_comma_separated("playlist_item_id", playlist_item_id), "part": enf_parts(resource="playlistItems", value=parts), } resp = self._request(resource="playlistItems", method="GET", args=args) data = self._parse_response(resp) if return_json: return data else: return PlaylistItemListResponse.from_dict(data) def get_playlist_items( self, *, playlist_id: str, parts: Optional[Union[str, list, tuple, set]] = None, video_id: Optional[str] = None, count: Optional[int] = 5, limit: Optional[int] = 5, page_token: Optional[str] = None, return_json: Optional[bool] = False, ): """ Retrieve playlist Items info by your given playlist id Args: playlist_id (str): The id for playlist that you want to retrieve items data. parts ((str,list,tuple,set) optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. video_id (str, Optional): Specifies that the request should return only the playlist items that contain the specified video. count (int, optional): The count will retrieve playlist items data. Default is 5. If provide this with None, will retrieve all playlist items. limit (int, optional): The maximum number of items each request retrieve. For playlistItem, this should not be more than 50. Default is 5 page_token(str, optional): The token of the page of playlist items result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.PlayListItemApiResponse instance. Returns: PlaylistItemListResponse or original data """ if count is None: limit = 50 # for playlistItems the max limit for per request is 50 else: limit = min(count, limit) args = { "playlistId": playlist_id, "part": enf_parts(resource="playlistItems", value=parts), "maxResults": limit, } if video_id is not None: args["videoId"] = video_id if page_token is not None: args["pageToken"] = page_token res_data = self.paged_by_page_token( resource="playlistItems", args=args, count=count ) if return_json: return res_data else: return PlaylistItemListResponse.from_dict(res_data) def get_playlist_by_id( self, *, playlist_id: Union[str, list, tuple, set], parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = "en_US", return_json: Optional[bool] = False, ): """ Retrieve playlist data by given playlist id. Args: playlist_id ((str,list,tuple,set)): The id for playlist that you want to retrieve data. You can pass this with single id str,comma-separated id str, or list, tuple, set of id str. parts (str, optional): Comma-separated list of one or more playlist resource properties. You can also pass this with list, tuple, set of part str. If not provided. will use default public properties. hl (str, optional): If provide this. Will return playlist's language localized info. This value need https://developers.google.com/youtube/v3/docs/i18nLanguages. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.PlaylistListResponse instance Returns: PlaylistListResponse or original data """ args = { "id": enf_comma_separated("playlist_id", playlist_id), "part": enf_parts(resource="playlists", value=parts), "hl": hl, } resp = self._request(resource="playlists", method="GET", args=args) data = self._parse_response(resp) if return_json: return data else: return PlaylistListResponse.from_dict(data) def get_playlists( self, *, channel_id: Optional[str] = None, mine: Optional[bool] = None, parts: Optional[Union[str, list, tuple, set]] = None, count: Optional[int] = 5, limit: Optional[int] = 5, hl: Optional[str] = "en_US", page_token: Optional[str] = None, return_json: Optional[bool] = False, ): """ Retrieve channel playlists info from youtube data api. Args: channel_id (str, optional): If provide channel id, this will return pointed channel's playlist info. mine (bool, optional): If you have given the authorization. Will return your playlists. Must provide the access token. parts (str, optional): Comma-separated list of one or more playlist resource properties. You can also pass this with list, tuple, set of part str. If not provided. will use default public properties. count (int, optional): The count will retrieve playlist data. Default is 5. If provide this with None, will retrieve all playlists. limit (int, optional): The maximum number of items each request to retrieve. For playlist, this should not be more than 50. Default is 5 hl (str, optional): If provide this. Will return playlist's language localized info. This value need https://developers.google.com/youtube/v3/docs/i18nLanguages. page_token(str, optional): The token of the page of playlists result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.PlaylistListResponse instance. Returns: PlaylistListResponse or original data """ if count is None: limit = 50 # for playlists the max limit for per request is 50 else: limit = min(count, limit) args = { "part": enf_parts(resource="playlists", value=parts), "hl": hl, "maxResults": limit, } if channel_id is not None: args["channelId"] = channel_id elif mine is not None: args["mine"] = mine else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message=f"Specify at least one of channel_id,playlist_id or mine", ) ) if page_token is not None: args["pageToken"] = page_token res_data = self.paged_by_page_token( resource="playlists", args=args, count=count ) if return_json: return res_data else: return PlaylistListResponse.from_dict(res_data) def search( self, *, parts: Optional[Union[str, list, tuple, set]] = None, for_developer: Optional[bool] = None, for_mine: Optional[bool] = None, related_to_video_id: Optional[str] = None, channel_id: Optional[str] = None, channel_type: Optional[str] = None, event_type: Optional[str] = None, location: Optional[str] = None, location_radius: Optional[str] = None, count: Optional[int] = 10, limit: Optional[int] = 10, order: Optional[str] = None, published_after: Optional[str] = None, published_before: Optional[str] = None, q: Optional[str] = None, region_code: Optional[str] = None, relevance_language: Optional[str] = None, safe_search: Optional[str] = None, topic_id: Optional[str] = None, search_type: Optional[Union[str, list, tuple, set]] = None, video_caption: Optional[str] = None, video_category_id: Optional[str] = None, video_definition: Optional[str] = None, video_dimension: Optional[str] = None, video_duration: Optional[str] = None, video_embeddable: Optional[str] = None, video_license: Optional[str] = None, video_paid_product_placement: Optional[str] = None, video_syndicated: Optional[str] = None, video_type: Optional[str] = None, page_token: Optional[str] = None, return_json: Optional[bool] = False, ) -> Union[SearchListResponse, dict]: """ Main search api implementation. You can find all parameters description at https://developers.google.com/youtube/v3/docs/search/list#parameters Returns: SearchListResponse or original data """ parts = enf_parts(resource="search", value=parts) if search_type is None: search_type = "video,channel,playlist" else: search_type = enf_comma_separated(field="search_type", value=search_type) args = { "part": parts, "maxResults": min(limit, count), } if for_developer: args["forDeveloper"] = for_developer if for_mine: args["forMine"] = for_mine if related_to_video_id: args["relatedToVideoId"] = related_to_video_id if channel_id: args["channelId"] = channel_id if channel_type: args["channelType"] = channel_type if event_type: args["eventType"] = event_type if location: args["location"] = location if location_radius: args["locationRadius"] = location_radius if order: args["order"] = order if published_after: args["publishedAfter"] = published_after if published_before: args["publishedBefore"] = published_before if q: args["q"] = q if region_code: args["regionCode"] = region_code if relevance_language: args["relevanceLanguage"] = relevance_language if safe_search: args["safeSearch"] = safe_search if topic_id: args["topicId"] = topic_id if search_type: args["type"] = search_type if video_caption: args["videoCaption"] = video_caption if video_category_id: args["videoCategoryId"] = video_category_id if video_definition: args["videoDefinition"] = video_definition if video_dimension: args["videoDimension"] = video_dimension if video_duration: args["videoDuration"] = video_duration if video_embeddable: args["videoEmbeddable"] = video_embeddable if video_license: args["videoLicense"] = video_license if video_paid_product_placement: args["videoPaidProductPlacement"] = video_paid_product_placement if video_syndicated: args["videoSyndicated"] = video_syndicated if video_type: args["videoType"] = video_type if page_token: args["pageToken"] = page_token res_data = self.paged_by_page_token(resource="search", args=args, count=count) if return_json: return res_data else: return SearchListResponse.from_dict(res_data) def search_by_keywords( self, *, q: Optional[str], parts: Optional[Union[str, list, tuple, set]] = None, search_type: Optional[Union[str, list, tuple, set]] = None, count: Optional[int] = 25, limit: Optional[int] = 25, page_token: Optional[str] = None, return_json: Optional[bool] = False, **kwargs: Optional[dict], ) -> Union[SearchListResponse, dict]: """ This is simplest usage for search api. You can only passed the keywords to retrieve data from YouTube. And the result will include videos,playlists and channels. Note: A call to this method has a quota cost of 100 units. Args: q (str): Your keywords can also use the Boolean NOT (-) and OR (|) operators to exclude videos or to find videos that are associated with one of several search terms. For example, to search for videos matching either "boating" or "sailing", set the q parameter value to boating|sailing. Similarly, to search for videos matching either "boating" or "sailing" but not "fishing", set the q parameter value to boating|sailing -fishing. Note that the pipe character must be URL-escaped when it is sent in your API request. The URL-escaped value for the pipe character is %7C. parts ((str,list,tuple,set) optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. search_type ((str,list,tuple,set), optional): Parameter restricts a search query to only retrieve a particular type of resource. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. The default value is video,channel,playlist Acceptable values are: - channel - playlist - video count (int, optional): The count will retrieve videos data. Default is 25. limit (int, optional): The maximum number of items each request retrieve. For search, this should not be more than 50. Default is 25. page_token (str, optional): The token of the page of search result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.SearchListResponse instance. kwargs: If you want use this pass more args. You can use this. Returns: SearchListResponse or original data """ return self.search( parts=parts, q=q, search_type=search_type, count=count, limit=limit, page_token=page_token, return_json=return_json, **kwargs, ) def search_by_developer( self, *, parts: Optional[Union[str, list, tuple, set]], q: Optional[str] = None, count: Optional[int] = 25, limit: Optional[int] = 25, page_token: Optional[str] = None, return_json: Optional[bool] = False, **kwargs, ) -> Union[SearchListResponse, dict]: """ Parameter restricts the search to only retrieve videos uploaded via the developer's application or website. Args: parts: The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. q: Your keywords can also use the Boolean NOT (-) and OR (|) operators to exclude videos or to find videos that are associated with one of several search terms. For example, to search for videos matching either "boating" or "sailing", set the q parameter value to boating|sailing. Similarly, to search for videos matching either "boating" or "sailing" but not "fishing", set the q parameter value to boating|sailing -fishing. Note that the pipe character must be URL-escaped when it is sent in your API request. The URL-escaped value for the pipe character is %7C. count: The count will retrieve videos data. Default is 25. limit: The maximum number of items each request retrieve. For search, this should not be more than 50. Default is 25. page_token: The token of the page of search result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json: The return data type. If you set True JSON data will be returned. False will return a pyyoutube.SearchListResponse instance. kwargs: If you want use this pass more args. You can use this. Returns: SearchListResponse or original data """ return self.search( for_developer=True, search_type="video", parts=parts, q=q, count=count, limit=limit, page_token=page_token, return_json=return_json, **kwargs, ) def search_by_mine( self, *, parts: Optional[Union[str, list, tuple, set]], q: Optional[str] = None, count: Optional[int] = 25, limit: Optional[int] = 25, page_token: Optional[str] = None, return_json: Optional[bool] = False, **kwargs, ) -> Union[SearchListResponse, dict]: """ Parameter restricts the search to only retrieve videos owned by the authenticated user. Note: This methods can not use following parameters: video_definition, video_dimension, video_duration, video_license, video_embeddable, video_syndicated, video_type. Args: q: Your keywords can also use the Boolean NOT (-) and OR (|) operators to exclude videos or to find videos that are associated with one of several search terms. For example, to search for videos matching either "boating" or "sailing", set the q parameter value to boating|sailing. Similarly, to search for videos matching either "boating" or "sailing" but not "fishing", set the q parameter value to boating|sailing -fishing. Note that the pipe character must be URL-escaped when it is sent in your API request. The URL-escaped value for the pipe character is %7C. parts ((str,list,tuple,set) optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. count (int, optional): The count will retrieve videos data. Default is 25. limit (int, optional): The maximum number of items each request retrieve. For search, this should not be more than 50. Default is 25. page_token (str, optional): The token of the page of search result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.SearchListResponse instance. kwargs: If you want use this pass more args. You can use this. Returns: SearchListResponse or original data """ return self.search( for_mine=True, search_type="video", parts=parts, q=q, count=count, limit=limit, page_token=page_token, return_json=return_json, **kwargs, ) def search_by_related_video( self, *, related_to_video_id: str, parts: Optional[Union[str, list, tuple, set]] = None, region_code: Optional[str] = None, relevance_language: Optional[str] = None, safe_search: Optional[str] = None, count: Optional[int] = 25, limit: Optional[int] = 25, page_token: Optional[str] = None, return_json: Optional[bool] = False, ) -> Union[SearchListResponse, dict]: """ Retrieve a list of videos related to that video. Args: related_to_video_id: A YouTube video ID which result associated with. parts: The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. region_code: Parameter instructs the API to return search results for videos that can be viewed in the specified country. relevance_language: Parameter instructs the API to return search results that are most relevant to the specified language. safe_search: Parameter indicates whether the search results should include restricted content as well as standard content. Acceptable values are: - moderate – YouTube will filter some content from search results and, at the least, will filter content that is restricted in your locale. Based on their content, search results could be removed from search results or demoted in search results. This is the default parameter value. - none – YouTube will not filter the search result set. - strict – YouTube will try to exclude all restricted content from the search result set. Based on their content, search results could be removed from search results or demoted in search results. count: The count will retrieve videos data. Default is 25. limit: The maximum number of items each request retrieve. For search, this should not be more than 50. Default is 25. page_token: The token of the page of search result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json: The return data type. If you set True JSON data will be returned. False will return a pyyoutube.SearchListResponse instance. Returns: If you want use this pass more args. You can use this. """ return self.search( parts=parts, related_to_video_id=related_to_video_id, search_type="video", region_code=region_code, relevance_language=relevance_language, safe_search=safe_search, count=count, limit=limit, page_token=page_token, return_json=return_json, ) def get_subscription_by_id( self, *, subscription_id: Union[str, list, tuple, set], parts: Optional[Union[str, list, tuple, set]] = None, return_json: Optional[bool] = False, ): """ Retrieve subscriptions by given subscription id(s). Note: This need authorized access token. or you will get no data. Args: subscription_id ((str,list,tuple,set)): The id for subscription that you want to retrieve data. You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids. parts ((str,list,tuple,set), optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.SubscriptionListResponse instance. Returns: SubscriptionListResponse or original data. """ args = { "id": enf_comma_separated(field="subscription_id", value=subscription_id), "part": enf_parts(resource="subscriptions", value=parts), } resp = self._request(resource="subscriptions", method="GET", args=args) data = self._parse_response(resp) if return_json: return data else: return SubscriptionListResponse.from_dict(data) def get_subscription_by_channel( self, *, channel_id: str, parts: Optional[Union[str, list, tuple, set]] = None, for_channel_id: Optional[Union[str, list, tuple, set]] = None, order: Optional[str] = "relevance", count: Optional[int] = 20, limit: Optional[int] = 20, page_token: Optional[str] = None, return_json: Optional[bool] = False, ): """ Retrieve the specified channel's subscriptions. Note: The API returns a 403 (Forbidden) HTTP response code if the specified channel does not publicly expose its subscriptions and the request is not authorized by the channel's owner. Args: channel_id (str): The id for channel which you want to get subscriptions. parts ((str,list,tuple,set) optional): The resource parts for subscription you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. for_channel_id ((str,list,tuple,set) optional): The parameter specifies a comma-separated list of channel IDs. and will then only contain subscriptions matching those channels. You can pass this with single part str, comma-separated parts str or a list,tuple,set of channel ids. order (str, optional): The parameter specifies the method that will be used to sort resources in the API response. Acceptable values are: alphabetical – Sort alphabetically. relevance – Sort by relevance. unread – Sort by order of activity. Default is relevance count (int, optional): The count will retrieve subscriptions data. Default is 20. If provide this with None, will retrieve all subscriptions. limit (int, optional): The maximum number of items each request retrieve. For comment threads, this should not be more than 50. Default is 20. page_token(str, optional): The token of the page of subscriptions result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.SubscriptionListResponse instance. Returns: SubscriptionListResponse or original data. """ if count is None: limit = 50 # for subscriptions the max limit for per request is 50 else: limit = min(count, limit) args = { "channelId": channel_id, "part": enf_parts(resource="subscriptions", value=parts), "order": order, "maxResults": limit, } if for_channel_id is not None: args["forChannelId"] = enf_comma_separated( field="for_channel_id", value=for_channel_id ) if page_token is not None: args["pageToken"] = page_token res_data = self.paged_by_page_token( resource="subscriptions", args=args, count=count ) if return_json: return res_data else: return SubscriptionListResponse.from_dict(res_data) def get_subscription_by_me( self, *, mine: Optional[bool] = None, recent_subscriber: Optional[bool] = None, subscriber: Optional[bool] = None, parts: Optional[Union[str, list, tuple, set]] = None, for_channel_id: Optional[Union[str, list, tuple, set]] = None, order: Optional[str] = "relevance", count: Optional[int] = 20, limit: Optional[int] = 20, page_token: Optional[str] = None, return_json: Optional[bool] = False, ): """ Retrieve your subscriptions. Note: This can only used in a properly authorized request. And for me test the parameter `recent_subscriber` and `subscriber` maybe not working. Use the `mine` first. Args: mine (bool, optional): Set this parameter's value to True to retrieve a feed of the authenticated user's subscriptions. recent_subscriber (bool, optional): Set this parameter's value to true to retrieve a feed of the subscribers of the authenticated user in reverse chronological order (newest first). And this can only get most recent 1000 subscribers. subscriber (bool, optional): Set this parameter's value to true to retrieve a feed of the subscribers of the authenticated user in no particular order. parts ((str,list,tuple,set) optional): The resource parts for subscription you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. for_channel_id ((str,list,tuple,set) optional): The parameter specifies a comma-separated list of channel IDs. and will then only contain subscriptions matching those channels. You can pass this with single part str, comma-separated parts str or a list,tuple,set of channel ids. order (str, optional): The parameter specifies the method that will be used to sort resources in the API response. Acceptable values are: alphabetical – Sort alphabetically. relevance – Sort by relevance. unread – Sort by order of activity. Default is relevance count (int, optional): The count will retrieve subscriptions data. Default is 20. If provide this with None, will retrieve all subscriptions. limit (int, optional): The maximum number of items each request retrieve. For subscriptions, this should not be more than 50. Default is 20. page_token(str, optional): The token of the page of subscriptions result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.SubscriptionListResponse instance. Returns: SubscriptionListResponse or original data. """ if count is None: limit = 50 # for subscriptions the max limit for per request is 50 else: limit = min(count, limit) args = { "part": enf_parts(resource="subscriptions", value=parts), "order": order, "maxResults": limit, } if mine is not None: args["mine"] = mine elif recent_subscriber is not None: args["myRecentSubscribers"] = recent_subscriber elif subscriber is not None: args["mySubscribers"] = subscriber else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message=f"Must specify at least one of mine,recent_subscriber,subscriber.", ) ) if for_channel_id is not None: args["forChannelId"] = enf_comma_separated( field="for_channel_id", value=for_channel_id ) if page_token is not None: args["pageToken"] = page_token res_data = self.paged_by_page_token( resource="subscriptions", args=args, count=count ) if return_json: return res_data else: return SubscriptionListResponse.from_dict(res_data) def get_video_abuse_report_reason( self, *, parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = "en_US", return_json: Optional[bool] = False, ) -> Union[VideoAbuseReportReasonListResponse, dict]: """ Retrieve a list of reasons that can be used to report abusive videos. Notes: This requires your authorization. Args: parts: The resource parts for abuse reason you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. hl: If provide this. Will return report reason's language localized info. This value need https://developers.google.com/youtube/v3/docs/i18nLanguages. return_json: The return data type. If you set True JSON data will be returned. False will return a pyyoutube.VideoAbuseReportReasonListResponse instance. Returns: VideoAbuseReportReasonListResponse or original data. """ args = { "part": enf_parts(resource="videoAbuseReportReasons", value=parts), "hl": hl, } resp = self._request(resource="videoAbuseReportReasons", args=args) data = self._parse_response(resp) if return_json: return data else: return VideoAbuseReportReasonListResponse.from_dict(data) def get_video_categories( self, *, category_id: Optional[Union[str, list, tuple, set]] = None, region_code: Optional[str] = None, parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = "en_US", return_json: Optional[bool] = False, ): """ Retrieve video categories by category id or region code. Args: category_id ((str,list,tuple,set), optional): The id for video category thread that you want to retrieve data. You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids. region_code (str, optional): The region code that you want to retrieve guide categories. The parameter value is an ISO 3166-1 alpha-2 country code. Refer: https://www.iso.org/iso-3166-country-codes.html parts ((str,list,tuple,set) optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. hl (str, optional): If provide this. Will return video category's language localized info. This value need https://developers.google.com/youtube/v3/docs/i18nLanguages. Default is en_US. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.VideoCategoryListResponse instance. Returns: VideoCategoryListResponse or original data """ args = { "part": enf_parts(resource="videoCategories", value=parts), "hl": hl, } if category_id is not None: args["id"] = enf_comma_separated(field="category_id", value=category_id) elif region_code is not None: args["regionCode"] = region_code else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Specify at least one of category_id or region_code", ) ) resp = self._request(resource="videoCategories", method="GET", args=args) data = self._parse_response(resp) if return_json: return data else: return VideoCategoryListResponse.from_dict(data) def get_video_by_id( self, *, video_id: Union[str, list, tuple, set], parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = "en_US", max_height: Optional[int] = None, max_width: Optional[int] = None, return_json: Optional[bool] = False, ): """ Retrieve video data by given video id. Args: video_id ((str,list,tuple,set)): The id for video that you want to retrieve data. You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids. parts ((str,list,tuple,set), optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. hl (str, optional): If provide this. Will return video's language localized info. This value need https://developers.google.com/youtube/v3/docs/i18nLanguages. max_height (int, optional): Specifies the maximum height of the embedded player returned in the player.embedHtml property. Acceptable values are 72 to 8192, inclusive. max_width (int, optional): Specifies the maximum width of the embedded player returned in the player.embedHtml property. Acceptable values are 72 to 8192, inclusive. If provide max_height at the same time. This will may be shorter than max_height. For more https://developers.google.com/youtube/v3/docs/videos/list#parameters. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.VideoListResponse instance. Returns: VideoListResponse or original data """ args = { "id": enf_comma_separated(field="video_id", value=video_id), "part": enf_parts(resource="videos", value=parts), "hl": hl, } if max_height is not None: args["maxHeight"] = max_height if max_width is not None: args["maxWidth"] = max_width resp = self._request(resource="videos", method="GET", args=args) data = self._parse_response(resp) if return_json: return data else: return VideoListResponse.from_dict(data) def get_videos_by_chart( self, *, chart: str, parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = "en_US", max_height: Optional[int] = None, max_width: Optional[int] = None, region_code: Optional[str] = None, category_id: Optional[str] = "0", count: Optional[int] = 5, limit: Optional[int] = 5, page_token: Optional[str] = None, return_json: Optional[bool] = False, ): """ Retrieve a list of YouTube's most popular videos. Args: chart (str): The chart string for you want to retrieve data. Acceptable values are: mostPopular parts ((str,list,tuple,set), optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. hl (str, optional): If provide this. Will return playlist's language localized info. This value need https://developers.google.com/youtube/v3/docs/i18nLanguages. max_height (int, optional): Specifies the maximum height of the embedded player returned in the player.embedHtml property. Acceptable values are 72 to 8192, inclusive. max_width (int, optional): Specifies the maximum width of the embedded player returned in the player.embedHtml property. Acceptable values are 72 to 8192, inclusive. If provide max_height at the same time. This will may be shorter than max_height. For more https://developers.google.com/youtube/v3/docs/videos/list#parameters. region_code (str, optional): This parameter instructs the API to select a video chart available in the specified region. Value is an ISO 3166-1 alpha-2 country code. category_id (str, optional): The id for video category that you want to filter. Default is 0. count (int, optional): The count will retrieve videos data. Default is 5. If provide this with None, will retrieve all videos. limit (int, optional): The maximum number of items each request retrieve. For videos, this should not be more than 50. Default is 5. page_token(str, optional): The token of the page of videos result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.PlaylistListResponse instance. Returns: VideoListResponse or original data """ if count is None: limit = 50 # for videos the max limit for per request is 50 else: limit = min(count, limit) args = { "chart": chart, "part": enf_parts(resource="videos", value=parts), "hl": hl, "maxResults": limit, "videoCategoryId": category_id, } if max_height is not None: args["maxHeight"] = max_height if max_width is not None: args["maxWidth"] = max_width if region_code: args["regionCode"] = region_code if page_token is not None: args["pageToken"] = page_token res_data = self.paged_by_page_token(resource="videos", args=args, count=count) if return_json: return res_data else: return VideoListResponse.from_dict(res_data) def get_videos_by_myrating( self, *, rating: str, parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = "en_US", max_height: Optional[int] = None, max_width: Optional[int] = None, count: Optional[int] = 5, limit: Optional[int] = 5, page_token: Optional[str] = None, return_json: Optional[bool] = False, ): """ Retrieve video data by my ration. Args: rating (str): The rating string for you to retrieve data. Acceptable values are: dislike, like parts ((str,list,tuple,set), optional): The resource parts for you want to retrieve. If not provide, use default public parts. You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts. hl (str, optional): If provide this. Will return video's language localized info. This value need https://developers.google.com/youtube/v3/docs/i18nLanguages. max_height (int, optional): Specifies the maximum height of the embedded player returned in the player.embedHtml property. Acceptable values are 72 to 8192, inclusive. max_width (int, optional): Specifies the maximum width of the embedded player returned in the player.embedHtml property. Acceptable values are 72 to 8192, inclusive. If provide max_height at the same time. This will may be shorter than max_height. For more https://developers.google.com/youtube/v3/docs/videos/list#parameters. count (int, optional): The count will retrieve videos data. Default is 5. If provide this with None, will retrieve all videos. limit (int, optional): The maximum number of items each request retrieve. For videos, this should not be more than 50. Default is 5. page_token(str, optional): The token of the page of videos result to retrieve. You can use this retrieve point result page directly. And you should know about the the result set for YouTube. return_json(bool, optional): The return data type. If you set True JSON data will be returned. False will return a pyyoutube.VideoListResponse instance. Returns: VideoListResponse or original data """ if self._access_token is None: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.NEED_AUTHORIZATION, message="This method can only used with authorization", ) ) if count is None: limit = 50 # for videos the max limit for per request is 50 else: limit = min(count, limit) args = { "myRating": rating, "part": enf_parts(resource="videos", value=parts), "hl": hl, "maxResults": limit, } if max_height is not None: args["maxHeight"] = max_height if max_width is not None: args["maxWidth"] = max_width if page_token is not None: args["pageToken"] = page_token res_data = self.paged_by_page_token(resource="videos", args=args, count=count) if return_json: return res_data else: return VideoListResponse.from_dict(res_data) ================================================ FILE: pyyoutube/client.py ================================================ """ New Client for YouTube API """ import inspect import json from typing import List, Optional, Tuple, Union import requests from requests import Response from requests.sessions import merge_setting from requests.structures import CaseInsensitiveDict from requests_oauthlib.oauth2_session import OAuth2Session import pyyoutube.resources as resources from pyyoutube.models.base import BaseModel from pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException from pyyoutube.models import ( AccessToken, ) from pyyoutube.resources.base_resource import Resource def _is_resource_endpoint(obj): return isinstance(obj, Resource) class Client: """Client for YouTube resource""" BASE_URL = "https://www.googleapis.com/youtube/v3/" BASE_UPLOAD_URL = "https://www.googleapis.com/upload/youtube/v3/" AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/v2/auth" EXCHANGE_ACCESS_TOKEN_URL = "https://oauth2.googleapis.com/token" REVOKE_TOKEN_URL = "https://oauth2.googleapis.com/revoke" HUB_URL = "https://pubsubhubbub.appspot.com/subscribe" DEFAULT_REDIRECT_URI = "https://localhost/" DEFAULT_SCOPE = [ "https://www.googleapis.com/auth/youtube", "https://www.googleapis.com/auth/userinfo.profile", ] DEFAULT_STATE = "Python-YouTube" activities = resources.ActivitiesResource() captions = resources.CaptionsResource() channels = resources.ChannelsResource() channelBanners = resources.ChannelBannersResource() channelSections = resources.ChannelSectionsResource() comments = resources.CommentsResource() commentThreads = resources.CommentThreadsResource() i18nLanguages = resources.I18nLanguagesResource() i18nRegions = resources.I18nRegionsResource() members = resources.MembersResource() membershipsLevels = resources.MembershipLevelsResource() playlistItems = resources.PlaylistItemsResource() playlists = resources.PlaylistsResource() search = resources.SearchResource() subscriptions = resources.SubscriptionsResource() thumbnails = resources.ThumbnailsResource() videoAbuseReportReasons = resources.VideoAbuseReportReasonsResource() videoCategories = resources.VideoCategoriesResource() videos = resources.VideosResource() watermarks = resources.WatermarksResource() def __new__(cls, *args, **kwargs): self = super().__new__(cls) sub_resources = inspect.getmembers(self, _is_resource_endpoint) for name, resource in sub_resources: resource_cls = type(resource) resource = resource_cls(self) setattr(self, name, resource) return self def __init__( self, client_id: Optional[str] = None, client_secret: Optional[str] = None, access_token: Optional[str] = None, refresh_token: Optional[str] = None, api_key: Optional[str] = None, client_secret_path: Optional[str] = None, timeout: Optional[int] = None, proxies: Optional[dict] = None, headers: Optional[dict] = None, ) -> None: """Class initial Args: client_id: ID for your app. client_secret: Secret for your app. access_token: Access token for user authorized with your app. refresh_token: Refresh Token for user. api_key: API key for your app which generated from api console. client_secret_path: path to the client_secret.json file provided by google console timeout: Timeout for every request. proxies: Proxies for every request. headers: Headers for every request. Raises: PyYouTubeException: Missing either credentials. """ self.client_id = client_id self.client_secret = client_secret self.access_token = access_token self.refresh_token = refresh_token self.api_key = api_key self.timeout = timeout self.proxies = proxies self.headers = headers self.session = requests.Session() self.merge_headers() if not self._has_client_data() and client_secret_path is not None: # try to use client_secret file self._from_client_secrets_file(client_secret_path) # Auth settings if not (self._has_auth_credentials() or self._has_client_data()): raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Must specify either client key info or api key.", ) ) def _from_client_secrets_file(self, client_secret_path: str): """Set credentials from client_sectet file Args: client_secret_path: path to the client_secret.json file, provided by google console Raises: PyYouTubeException: missing required key, client_secret file not in 'web' format. """ with open(client_secret_path, "r") as f: secrets_data = json.load(f) credentials = None for secrets_type in ["web", "installed"]: if secrets_type in secrets_data: credentials = secrets_data[secrets_type] if not credentials: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message="Only 'web' and 'installed' type client_secret files are supported.", ) ) # check for reqiered fields for field in ["client_secret", "client_id"]: if field not in credentials: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message=f"missing required field '{field}'.", ) ) self.client_id = credentials["client_id"] self.client_secret = credentials["client_secret"] # Set default redirect to first defined in client_secrets file if any if "redirect_uris" in credentials and len(credentials["redirect_uris"]) > 0: self.DEFAULT_REDIRECT_URI = credentials["redirect_uris"][0] def _has_auth_credentials(self) -> bool: return self.api_key or self.access_token def _has_client_data(self) -> bool: return self.client_id and self.client_secret def merge_headers(self): """Merge custom headers to session.""" if self.headers: self.session.headers = merge_setting( request_setting=self.session.headers, session_setting=self.headers, dict_class=CaseInsensitiveDict, ) @staticmethod def parse_response(response: Response) -> dict: """Response parser Args: response: Response from the Response. Returns: Response dict data. Raises: PyYouTubeException: If response has errors. """ data = response.json() if "error" in data: raise PyYouTubeException(response) return data def request( self, path: str, method: str = "GET", params: Optional[dict] = None, data: Optional[dict] = None, json: Optional[dict] = None, enforce_auth: bool = True, is_upload: bool = False, **kwargs, ): """Send request to YouTube. Args: path: Resource or url for YouTube data. such as channels,videos and so on. method: Method for the request. params: Object to send in the query string of the request. data: Object to send in the body of the request. json: Object json to send in the body of the request. enforce_auth: Whether to use user credentials. is_upload: Whether it is an upload job. kwargs: Additional parameters for request. Returns: Response for request. Raises: PyYouTubeException: Missing credentials when need credentials. Request http error. """ if not path.startswith("http"): base_url = self.BASE_UPLOAD_URL if is_upload else self.BASE_URL path = base_url + path # Add credentials to request if enforce_auth: if self.api_key is None and self.access_token is None: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="You must provide your credentials.", ) ) else: self.add_token_to_headers() params = self.add_api_key_to_params(params=params) # If json is dataclass convert to dict if isinstance(json, BaseModel): json = json.to_dict_ignore_none() try: response = self.session.request( method=method, url=path, params=params, data=data, json=json, proxies=self.proxies, timeout=self.timeout, **kwargs, ) except requests.HTTPError as e: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message=e.args[0]) ) else: return response def add_token_to_headers(self): if self.access_token: self.session.headers.update( {"Authorization": f"Bearer {self.access_token}"} ) def add_api_key_to_params(self, params: Optional[dict] = None): if not self.api_key: return params if params is None: params = {"key": self.api_key} else: params["key"] = self.api_key return params def _get_oauth_session( self, redirect_uri: Optional[str] = None, scope: Optional[List[str]] = None, state: Optional[str] = None, **kwargs, ) -> OAuth2Session: """Build request session for authorization Args: redirect_uri: Determines how Google's authorization server sends a response to your app. If not provide will use default https://localhost/ scope: Permission scope for authorization. see more: https://developers.google.com/identity/protocols/oauth2/scopes#youtube state: State sting for authorization. **kwargs: Additional parameters for session. Returns: OAuth2.0 Session """ redirect_uri = ( redirect_uri if redirect_uri is not None else self.DEFAULT_REDIRECT_URI ) scope = scope if scope is not None else self.DEFAULT_SCOPE state = state if state is not None else self.DEFAULT_STATE return OAuth2Session( client_id=self.client_id, scope=scope, redirect_uri=redirect_uri, state=state, **kwargs, ) def get_authorize_url( self, redirect_uri: Optional[str] = None, scope: Optional[List[str]] = None, access_type: str = "offline", state: Optional[str] = None, include_granted_scopes: Optional[bool] = None, login_hint: Optional[str] = None, prompt: Optional[str] = None, **kwargs, ) -> Tuple[str, str]: """Get authorize url for user. Args: redirect_uri: Determines how Google's authorization server sends a response to your app. If not provide will use default https://localhost/ scope: The scope you want user to grant permission. access_type: Indicates whether your application can refresh access tokens when the user is not present at the browser. Valid parameter are `online` and `offline`. state: State string between your authorization request and the authorization server's response. include_granted_scopes: Enables applications to use incremental authorization to request access to additional scopes in context. Set true to enable. login_hint: Set the parameter value to an email address or sub identifier, which is equivalent to the user's Google ID. prompt: A space-delimited, case-sensitive list of prompts to present the user. Possible values are: - none: Do not display any authentication or consent screens. Must not be specified with other values. - consent: Prompt the user for consent. - select_account: Prompt the user to select an account. **kwargs: Additional parameters for authorize session. Returns: A tuple of (url, state) url: Authorize url for user. state: State string for authorization. References: https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps """ session = self._get_oauth_session( redirect_uri=redirect_uri, scope=scope, state=state, **kwargs, ) authorize_url, state = session.authorization_url( url=self.AUTHORIZATION_URL, access_type=access_type, include_granted_scopes=include_granted_scopes, login_hint=login_hint, prompt=prompt, ) return authorize_url, state def generate_access_token( self, authorization_response: Optional[str] = None, code: Optional[str] = None, redirect_uri: Optional[str] = None, scope: Optional[List[str]] = None, state: Optional[str] = None, return_json: bool = False, **kwargs, ) -> Union[dict, AccessToken]: """Exchange the authorization code or authorization response for an access token. Args: authorization_response: Response url for YouTune redirected to. code: Authorization code from authorization_response. redirect_uri: Determines how Google's authorization server sends a response to your app. If not provide will use default https://localhost/ scope: The scope you want user to grant permission. state: State string between your authorization request and the authorization server's response. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for authorize session. Returns: Access token data. """ session = self._get_oauth_session( redirect_uri=redirect_uri, scope=scope, state=state, **kwargs, ) token = session.fetch_token( token_url=self.EXCHANGE_ACCESS_TOKEN_URL, client_secret=self.client_secret, authorization_response=authorization_response, code=code, proxies=self.proxies, ) self.access_token = token["access_token"] self.refresh_token = token.get("refresh_token") return token if return_json else AccessToken.from_dict(token) def refresh_access_token( self, refresh_token: str, return_json: bool = False, **kwargs ) -> Union[dict, AccessToken]: """Refresh new access token. Args: refresh_token: The refresh token returned from the authorization code exchange. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for request. Returns: Access token data. """ response = self.request( method="POST", path=self.EXCHANGE_ACCESS_TOKEN_URL, data={ "client_id": self.client_id, "client_secret": self.client_secret, "refresh_token": refresh_token, "grant_type": "refresh_token", }, enforce_auth=False, **kwargs, ) data = self.parse_response(response) return data if return_json else AccessToken.from_dict(data) def revoke_access_token( self, token: str, ) -> bool: """Revoke token. Notes: If the token is an access token which has a corresponding refresh token, the refresh token will also be revoked. Args: token: Can be an access token or a refresh token. Returns: Revoked status Raises: PyYouTubeException: When occur errors. """ response = self.request( method="POST", path=self.REVOKE_TOKEN_URL, params={"token": token}, enforce_auth=False, ) if response.ok: return True self.parse_response(response) def subscribe_push_notification( self, channel_id: str, callback_url: str, mode: str = "subscribe", lease_seconds: Optional[int] = None, secret: Optional[str] = None, verify: str = "async", ) -> bool: """Subscribe or unsubscribe to a YouTube channel's push notifications via PubSubHubbub. When a subscribed channel publishes a new video or updates an existing one, Google will send a notification to the callback_url. Args: channel_id: The YouTube channel ID to subscribe to. callback_url: The URL that will receive push notifications from the hub. Must be publicly accessible. mode: Either "subscribe" or "unsubscribe". lease_seconds: How long (in seconds) the subscription should remain active. If omitted, the hub uses its own default (typically ~432000, i.e. 5 days). secret: A secret string used to compute an HMAC-SHA1 signature on each notification, allowing you to verify the payload came from the hub. verify: Verification mode. Either "async" (default) or "sync". Returns: True if the hub accepted the request (HTTP 202 Accepted). Raises: PyYouTubeException: If the hub returns an error response. References: https://developers.google.com/youtube/v3/guides/push_notifications https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html """ topic_url = ( f"https://www.youtube.com/xml/feeds/videos.xml?channel_id={channel_id}" ) data = { "hub.callback": callback_url, "hub.mode": mode, "hub.topic": topic_url, "hub.verify": verify, } if lease_seconds is not None: data["hub.lease_seconds"] = str(lease_seconds) if secret is not None: data["hub.secret"] = secret response = self.request( method="POST", path=self.HUB_URL, data=data, enforce_auth=False, ) # Hub returns 202 Accepted on success (async) or 204 No Content (sync) if response.status_code in (202, 204): return True self.parse_response(response) ================================================ FILE: pyyoutube/error.py ================================================ from dataclasses import dataclass from typing import Optional, Union from requests import Response __all__ = ["ErrorCode", "ErrorMessage", "PyYouTubeException"] class ErrorCode: HTTP_ERROR = 10000 MISSING_PARAMS = 10001 INVALID_PARAMS = 10002 NEED_AUTHORIZATION = 10003 AUTHORIZE_URL_FIRST = 10004 @dataclass class ErrorMessage: status_code: Optional[int] = None message: Optional[str] = None class PyYouTubeException(Exception): """ This is a return demo: {'error': {'errors': [{'domain': 'youtube.parameter', 'reason': 'missingRequiredParameter', 'message': 'No filter selected. Expected one of: forUsername, managedByMe, categoryId, mine, mySubscribers, id, idParam', 'locationType': 'parameter', 'location': ''}], 'code': 400, 'message': 'No filter selected. Expected one of: forUsername, managedByMe, categoryId, mine, mySubscribers, id, idParam'}} """ def __init__(self, response: Optional[Union[ErrorMessage, Response]]): self.status_code: Optional[int] = None self.error_type: Optional[str] = None self.message: Optional[str] = None self.response: Optional[Union[ErrorMessage, Response]] = response self.error_handler() def error_handler(self): """ Error has two big type(but not the error type.): This module's error, Api return error. So This will change two error to one format """ if isinstance(self.response, ErrorMessage): self.status_code = self.response.status_code self.message = self.response.message self.error_type = "PyYouTubeException" elif isinstance(self.response, Response): res_data = self.response.json() if "error" in res_data: error = res_data["error"] if isinstance(error, dict): self.status_code = res_data["error"]["code"] self.message = res_data["error"]["message"] else: self.status_code = self.response.status_code self.message = error self.error_type = "YouTubeException" def __repr__(self): return ( f"{self.error_type}(status_code={self.status_code},message={self.message})" ) def __str__(self): return self.__repr__() ================================================ FILE: pyyoutube/media.py ================================================ """ Media object to upload. """ import mimetypes import os from typing import IO, Optional, Tuple from requests import Response from pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode DEFAULT_CHUNK_SIZE = 20 * 1024 * 1024 class Media: def __init__( self, fd: Optional[IO] = None, mimetype: Optional[str] = None, filename: Optional[str] = None, chunk_size: int = DEFAULT_CHUNK_SIZE, ) -> None: """Media representing a file to upload with metadata. Args: fd: The source of the bytes to upload. mimetype: Mime-type of the file. filename: Name of the file. At least one of the `fd` or `filename`. chunk_size: File will be uploaded in chunks of this many bytes. Only used if resumable=True. """ if fd is not None: self.fd = fd elif filename is not None: self._filename = filename self.fd = open(self._filename, "rb") else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Specify at least one of fd or filename", ) ) if mimetype is None and filename is not None: mimetype, _ = mimetypes.guess_type(filename) if mimetype is None: # Guess failed, use octet-stream. mimetype = "application/octet-stream" self.mimetype = mimetype self.chunk_size = chunk_size self.fd.seek(0, os.SEEK_END) self.size = self.fd.tell() def get_bytes(self, begin: int, length: int) -> bytes: """Get bytes from the media. Args: begin: Offset from beginning of file. length: Number of bytes to read, starting at begin. Returns: A string of bytes read. May be shorted than length if EOF was reached first. """ self.fd.seek(begin) return self.fd.read(length) class MediaUploadProgress: def __init__(self, progressed_seize: int, total_size: int): """ Args: progressed_seize: Bytes sent so far. total_size: Total bytes in complete upload, or None if the total upload size isn't known ahead of time. """ self.progressed_seize = progressed_seize self.total_size = total_size def progress(self) -> float: """Percent of upload completed, as a float. Returns: the percentage complete as a float, returning 0.0 if the total size of the upload is unknown. """ if self.total_size is not None and self.total_size != 0: return float(self.progressed_seize) / float(self.total_size) else: return 0.0 def __repr__(self) -> str: return f"Media upload {int(self.progress() * 100)} complete." class MediaUpload: def __init__( self, client, resource: str, media: Media, params: Optional[dict] = None, body: Optional[dict] = None, ) -> None: """Constructor for upload a file. Args: client: Client instance. resource: Resource like videos,captions and so on. media: Media instance. params: Parameters for the request. body: Body for the request. """ self.client = client self.media = media self.params = params self.body = body self.resource = resource if self.params is not None: self.params["uploadType"] = "resumable" self.resumable_uri = None # Real uri to upload media. self.resumable_progress = 0 # The bytes that have been uploaded. def next_chunk(self) -> Tuple[Optional[MediaUploadProgress], Optional[dict]]: """Execute the next step of a resumable upload. Returns: The body will be None until the resumable media is fully uploaded. """ size = str(self.media.size) if self.resumable_uri is None: start_headers = { "X-Upload-Content-Type": self.media.mimetype, "X-Upload-Content-Length": size, "content-length": str(len(str(self.body or ""))), } resp = self.client.request( method="POST", path=self.resource, params=self.params, json=self.body, is_upload=True, headers=start_headers, ) if resp.status_code == 200 and "location" in resp.headers: self.resumable_uri = resp.headers["location"] else: raise PyYouTubeException(resp) data = self.media.get_bytes(self.resumable_progress, self.media.chunk_size) # A short read implies that we are at EOF, so finish the upload. if len(data) < self.media.chunk_size: size = str(self.resumable_progress + len(data)) chunk_end = self.resumable_progress + len(data) - 1 headers = { "Content-Length": str(chunk_end - self.resumable_progress + 1), } # An empty file results in chunk_end = -1 and size = 0 # sending "bytes 0--1/0" results in an invalid request # Only add header "Content-Range" if chunk_end != -1 if chunk_end != -1: headers["Content-Range"] = ( f"bytes {self.resumable_progress}-{chunk_end}/{size}" ) resp = self.client.request( path=self.resumable_uri, method="PUT", data=data, headers=headers, ) return self.process_response(resp) def process_response( self, resp: Response ) -> Tuple[Optional[MediaUploadProgress], Optional[dict]]: """Process the response from chunk upload. Args: resp: Response for request. Returns: The body will be None until the resumable media is fully uploaded. """ if resp.status_code in [200, 201]: return None, self.client.parse_response(response=resp) elif resp.status_code == 308: try: self.resumable_progress = int(resp.headers["range"].split("-")[1]) + 1 except KeyError: # If resp doesn't contain range header, resumable progress is 0 self.resumable_progress = 0 if "location" in resp.headers: self.resumable_uri = resp.headers["location"] else: raise PyYouTubeException(resp) return ( MediaUploadProgress(self.resumable_progress, self.media.size), None, ) ================================================ FILE: pyyoutube/models/__init__.py ================================================ from .activity import * # noqa from .auth import AccessToken, UserProfile from .caption import * # noqa from .category import * # noqa from .channel import * # noqa from .channel_banner import * # noqa from .channel_section import * # noqa from .comment import * # noqa from .comment_thread import * # noqa from .common import * # noqa from .i18n import * # noqa from .member import * # noqa from .memberships_level import * # noqa from .playlist_item import * # noqa from .playlist import * # noqa from .search_result import * # noqa from .subscription import * # noqa from .video_abuse_report_reason import * # noqa from .video import * # noqa from .watermark import * # noqa ================================================ FILE: pyyoutube/models/activity.py ================================================ """ These are activity related models. """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .common import BaseApiResponse, BaseResource, ResourceId, Thumbnails from .mixins import DatetimeTimeMixin @dataclass class ActivityContentDetailsUpload(BaseModel): """ A class representing the activity contentDetails upload resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.upload """ videoId: Optional[str] = field(default=None) @dataclass class ActivityContentDetailsLike(BaseModel): """ A class representing the activity contentDetails like resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.like """ resourceId: Optional[ResourceId] = field(default=None) @dataclass class ActivityContentDetailsFavorite(BaseModel): """ A class representing the activity contentDetails favorite resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.favorite """ resourceId: Optional[ResourceId] = field(default=None) @dataclass class ActivityContentDetailsComment(BaseModel): """ A class representing the activity contentDetails comment resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.comment """ resourceId: Optional[ResourceId] = field(default=None) @dataclass class ActivityContentDetailsSubscription(BaseModel): """ A class representing the activity contentDetails subscription resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.subscription """ resourceId: Optional[ResourceId] = field(default=None) @dataclass class ActivityContentDetailsPlaylistItem(BaseModel): """ A class representing the activity contentDetails playlistItem resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.playlistItem """ resourceId: Optional[ResourceId] = field(default=None) playlistId: Optional[str] = field(default=None) playlistItemId: Optional[str] = field(default=None) @dataclass class ActivityContentDetailsRecommendation(BaseModel): """ A class representing the activity contentDetails recommendation resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.recommendation """ resourceId: Optional[ResourceId] = field(default=None) reason: Optional[str] = field(default=None) seedResourceId: Optional[ResourceId] = field(default=None) @dataclass class ActivityContentDetailsBulletin(BaseModel): """ A class representing the activity contentDetails bulletin resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.bulletin """ resourceId: Optional[ResourceId] = field(default=None) @dataclass class ActivityContentDetailsSocial(BaseModel): """ A class representing the activity contentDetails social resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.social """ type: Optional[str] = field(default=None) resourceId: Optional[ResourceId] = field(default=None) author: Optional[str] = field(default=None) referenceUrl: Optional[str] = field(default=None) imageUrl: Optional[str] = field(default=None) @dataclass class ActivityContentDetailsChannelItem(BaseModel): """ A class representing the activity contentDetails channelItem resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.channelItem """ resourceId: Optional[ResourceId] = field(default=None) @dataclass class ActivitySnippet(BaseModel, DatetimeTimeMixin): """ A class representing the activity snippet resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#snippet """ publishedAt: Optional[str] = field(default=None, repr=False) channelId: Optional[str] = field(default=None, repr=False) title: Optional[str] = field(default=None) description: Optional[str] = field(default=None) thumbnails: Optional[Thumbnails] = field(default=None, repr=False) channelTitle: Optional[str] = field(default=None, repr=False) type: Optional[str] = field(default=None, repr=False) groupId: Optional[str] = field(default=None, repr=False) @dataclass class ActivityContentDetails(BaseModel): """ A class representing the activity contentDetails resource info. Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails """ upload: Optional[ActivityContentDetailsUpload] = field(default=None) like: Optional[ActivityContentDetailsLike] = field(default=None, repr=False) favorite: Optional[ActivityContentDetailsFavorite] = field(default=None, repr=False) comment: Optional[ActivityContentDetailsComment] = field(default=None, repr=False) subscription: Optional[ActivityContentDetailsSubscription] = field( default=None, repr=False ) playlistItem: Optional[ActivityContentDetailsPlaylistItem] = field( default=None, repr=False ) recommendation: Optional[ActivityContentDetailsRecommendation] = field( default=None, repr=False ) bulletin: Optional[ActivityContentDetailsBulletin] = field(default=None, repr=False) social: Optional[ActivityContentDetailsSocial] = field(default=None, repr=False) channelItem: Optional[ActivityContentDetailsChannelItem] = field( default=None, repr=False ) @dataclass class Activity(BaseResource): """ A class representing the activity resource info. Refer: https://developers.google.com/youtube/v3/docs/activities """ snippet: Optional[ActivitySnippet] = field(default=None) contentDetails: Optional[ActivityContentDetails] = field(default=None, repr=False) @dataclass class ActivityListResponse(BaseApiResponse): """ A class representing the activity response info. Refer: https://developers.google.com/youtube/v3/docs/activities/list#response_1 """ items: Optional[List[Activity]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/auth.py ================================================ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel @dataclass class AccessToken(BaseModel): """ A class representing for access token. Refer: https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#exchange-authorization-code """ access_token: Optional[str] = field(default=None) expires_in: Optional[int] = field(default=None) refresh_token: Optional[str] = field(default=None, repr=False) scope: Optional[List[str]] = field(default=None, repr=False) token_type: Optional[str] = field(default=None) expires_at: Optional[float] = field(default=None, repr=False) @dataclass class UserProfile(BaseModel): """ A class representing for user profile. Refer: https://any-api.com/googleapis_com/oauth2/docs/userinfo/oauth2_userinfo_v2_me_get """ id: Optional[str] = field(default=None) name: Optional[str] = field(default=None) given_name: Optional[str] = field(default=None, repr=False) family_name: Optional[str] = field(default=None, repr=False) picture: Optional[str] = field(default=None, repr=False) locale: Optional[str] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/base.py ================================================ from dataclasses import dataclass, asdict from typing import Type, TypeVar from dataclasses_json import DataClassJsonMixin from dataclasses_json.core import Json, _decode_dataclass A = TypeVar("A", bound="DataClassJsonMixin") @dataclass class BaseModel(DataClassJsonMixin): """Base model class for instance use.""" @classmethod def from_dict(cls: Type[A], kvs: Json, *, infer_missing=False) -> A: # save original data for lookup cls._json = kvs return _decode_dataclass(cls, kvs, infer_missing) def to_dict_ignore_none(self): return asdict( obj=self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None} ) ================================================ FILE: pyyoutube/models/caption.py ================================================ """ These are caption related models """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .mixins import DatetimeTimeMixin from .common import BaseResource, BaseApiResponse @dataclass class CaptionSnippet(BaseModel, DatetimeTimeMixin): """ A class representing the caption snippet resource info. Refer: https://developers.google.com/youtube/v3/docs/captions#snippet """ videoId: Optional[str] = field(default=None) lastUpdated: Optional[str] = field(default=None) trackKind: Optional[str] = field(default=None, repr=False) language: Optional[str] = field(default=None, repr=False) name: Optional[str] = field(default=None, repr=False) audioTrackType: Optional[str] = field(default=None, repr=False) isCC: Optional[bool] = field(default=None, repr=False) isLarge: Optional[bool] = field(default=None, repr=False) isEasyReader: Optional[bool] = field(default=None, repr=False) isDraft: Optional[bool] = field(default=None, repr=False) isAutoSynced: Optional[bool] = field(default=None, repr=False) status: Optional[str] = field(default=None, repr=False) failureReason: Optional[str] = field(default=None, repr=False) @dataclass class Caption(BaseResource): """ A class representing the caption resource info. Refer: https://developers.google.com/youtube/v3/docs/captions """ snippet: Optional[CaptionSnippet] = field(default=None) @dataclass class CaptionListResponse(BaseApiResponse): """ A class representing the activity response info. Refer: https://developers.google.com/youtube/v3/docs/captions/list?#response_1 """ items: Optional[List[Caption]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/category.py ================================================ """ These are category related models. Include VideoCategory """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .common import BaseApiResponse, BaseResource @dataclass class CategorySnippet(BaseModel): """ This is base category snippet for video and guide. """ channelId: Optional[str] = field(default=None) title: Optional[str] = field(default=None) @dataclass class VideoCategorySnippet(CategorySnippet): """ A class representing video category snippet info. Refer: https://developers.google.com/youtube/v3/docs/videoCategories#snippet """ assignable: Optional[bool] = field(default=None, repr=False) @dataclass class VideoCategory(BaseResource): """ A class representing video category info. Refer: https://developers.google.com/youtube/v3/docs/videoCategories """ snippet: Optional[VideoCategorySnippet] = field(default=None, repr=False) @dataclass class VideoCategoryListResponse(BaseApiResponse): """ A class representing the video category's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/videoCategories/list#response_1 """ items: Optional[List[VideoCategory]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/channel.py ================================================ """ These are channel related models. References: https://developers.google.com/youtube/v3/docs/channels#properties """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .common import ( BaseResource, BaseTopicDetails, Thumbnails, BaseApiResponse, Localized, ) from .mixins import DatetimeTimeMixin @dataclass class RelatedPlaylists(BaseModel): """ A class representing the channel's related playlists info References: https://developers.google.com/youtube/v3/docs/channels#contentDetails.relatedPlaylists """ likes: Optional[str] = field(default=None, repr=False) uploads: Optional[str] = field(default=None) @dataclass class ChannelBrandingSettingChannel(BaseModel): """ A class representing the channel branding setting's channel info. References: https://developers.google.com/youtube/v3/docs/channels#brandingSettings.channel """ title: Optional[str] = field(default=None) description: Optional[str] = field(default=None) keywords: Optional[str] = field(default=None, repr=False) trackingAnalyticsAccountId: Optional[str] = field(default=None, repr=False) # Important: # moderateComments has been deprecated at March 7, 2024. moderateComments: Optional[bool] = field(default=None, repr=False) unsubscribedTrailer: Optional[str] = field(default=None, repr=False) defaultLanguage: Optional[str] = field(default=None, repr=False) country: Optional[str] = field(default=None, repr=False) @dataclass class ChannelBrandingSettingImage(BaseModel): """ A class representing the channel branding setting's image info. References: https://developers.google.com/youtube/v3/docs/channels#brandingSettings.image """ bannerExternalUrl: Optional[str] = field(default=None, repr=False) @dataclass class ChannelSnippet(BaseModel, DatetimeTimeMixin): """ A class representing the channel snippet info. References: https://developers.google.com/youtube/v3/docs/channels#snippet """ title: Optional[str] = field(default=None) description: Optional[str] = field(default=None) customUrl: Optional[str] = field(default=None, repr=False) publishedAt: Optional[str] = field(default=None, repr=False) thumbnails: Optional[Thumbnails] = field(default=None, repr=False) defaultLanguage: Optional[str] = field(default=None, repr=False) localized: Optional[Localized] = field(default=None, repr=False) country: Optional[str] = field(default=None, repr=False) @dataclass class ChannelContentDetails(BaseModel): """ A class representing the channel's content info. References: https://developers.google.com/youtube/v3/docs/channels#contentDetails """ relatedPlaylists: Optional[RelatedPlaylists] = field(default=None) @dataclass class ChannelStatistics(BaseModel): """ A class representing the Channel's statistics info. References: https://developers.google.com/youtube/v3/docs/channels#statistics """ viewCount: Optional[int] = field(default=None) subscriberCount: Optional[int] = field(default=None) hiddenSubscriberCount: Optional[bool] = field(default=None, repr=False) videoCount: Optional[int] = field(default=None, repr=False) @dataclass class ChannelTopicDetails(BaseTopicDetails): """ A class representing the channel's topic detail info. References: https://developers.google.com/youtube/v3/docs/channels#topicDetails """ # Important: # topicIds maybe has deprecated. # see more: https://developers.google.com/youtube/v3/revision_history#november-10-2016 topicIds: Optional[List[str]] = field(default=None, repr=False) topicCategories: Optional[List[str]] = field(default=None) @dataclass class ChannelStatus(BaseModel): """ A class representing the channel's status info. References: https://developers.google.com/youtube/v3/docs/channels#status """ privacyStatus: Optional[str] = field(default=None) isLinked: Optional[bool] = field(default=None, repr=False) longUploadsStatus: Optional[str] = field(default=None, repr=False) madeForKids: Optional[bool] = field(default=None, repr=False) selfDeclaredMadeForKids: Optional[bool] = field(default=None, repr=False) @dataclass class ChannelBrandingSetting(BaseModel): """ A class representing the channel branding settings info. References: https://developers.google.com/youtube/v3/docs/channels#brandingSettings """ channel: Optional[ChannelBrandingSettingChannel] = field(default=None) image: Optional[ChannelBrandingSettingImage] = field(default=None) @dataclass class ChannelAuditDetails(BaseModel): """A class representing the channel audit details info. References: https://developers.google.com/youtube/v3/docs/channels#auditDetails """ overallGoodStanding: Optional[bool] = field(default=None) communityGuidelinesGoodStanding: Optional[bool] = field(default=None, repr=True) copyrightStrikesGoodStanding: Optional[bool] = field(default=None, repr=True) contentIdClaimsGoodStanding: Optional[bool] = field(default=None, repr=True) @dataclass class ChannelContentOwnerDetails(BaseModel): """A class representing the channel data relevant for YouTube Partners. References: https://developers.google.com/youtube/v3/docs/channels#contentOwnerDetails """ contentOwner: Optional[str] = field(default=None) timeLinked: Optional[str] = field(default=None) @dataclass class Channel(BaseResource): """ A class representing the channel's info. References: https://developers.google.com/youtube/v3/docs/channels """ snippet: Optional[ChannelSnippet] = field(default=None, repr=False) contentDetails: Optional[ChannelContentDetails] = field(default=None, repr=False) statistics: Optional[ChannelStatistics] = field(default=None, repr=False) topicDetails: Optional[ChannelTopicDetails] = field(default=None, repr=False) status: Optional[ChannelStatus] = field(default=None, repr=False) brandingSettings: Optional[ChannelBrandingSetting] = field(default=None, repr=False) auditDetails: Optional[ChannelAuditDetails] = field(default=None, repr=False) contentOwnerDetails: Optional[ChannelContentOwnerDetails] = field( default=None, repr=False ) localizations: Optional[dict] = field(default=None, repr=False) @dataclass class ChannelListResponse(BaseApiResponse): """ A class representing the channel's retrieve response info. References: https://developers.google.com/youtube/v3/docs/channels/list#response """ items: Optional[List[Channel]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/channel_banner.py ================================================ """ There are channel banner related models References: https://developers.google.com/youtube/v3/docs/channelBanners#properties """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel @dataclass class ChannelBanner(BaseModel): """ A class representing the channel banner's info. References: https://developers.google.com/youtube/v3/docs/channelBanners#resource """ kind: Optional[str] = field(default=None) etag: Optional[str] = field(default=None, repr=False) url: Optional[str] = field(default=None) ================================================ FILE: pyyoutube/models/channel_section.py ================================================ """ Those are models related to channel sections. """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .common import BaseResource, BaseApiResponse @dataclass class ChannelSectionSnippet(BaseModel): """ A class representing the channel section snippet info. Refer: https://developers.google.com/youtube/v3/docs/channelSections#snippet """ type: Optional[str] = field(default=None) channelId: Optional[str] = field(default=None, repr=False) title: Optional[str] = field(default=None, repr=False) position: Optional[int] = field(default=None) @dataclass class ChannelSectionContentDetails(BaseModel): """ A class representing the channel section content details info. Refer: https://developers.google.com/youtube/v3/docs/channelSections#contentDetails """ playlists: Optional[List[str]] = field(default=None, repr=False) channels: Optional[List[str]] = field(default=None) @dataclass class ChannelSection(BaseResource): """ A class representing the channel section info. Refer: https://developers.google.com/youtube/v3/docs/channelSections#properties """ snippet: Optional[ChannelSectionSnippet] = field(default=None, repr=False) contentDetails: Optional[ChannelSectionContentDetails] = field( default=None, repr=False ) @dataclass class ChannelSectionResponse(BaseApiResponse): """ A class representing the channel section's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/channelSections/list?#properties_1 """ items: Optional[List[ChannelSection]] = field(default=None, repr=False) @dataclass class ChannelSectionListResponse(ChannelSectionResponse): ... ================================================ FILE: pyyoutube/models/comment.py ================================================ """ These are comment related models. """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .mixins import DatetimeTimeMixin from .common import BaseApiResponse, BaseResource @dataclass class CommentSnippetAuthorChannelId(BaseModel): """ A class representing comment's snippet authorChannelId info. Refer: https://developers.google.com/youtube/v3/docs/comments#snippet.authorChannelId """ value: Optional[str] = field(default=None) @dataclass class CommentSnippet(BaseModel, DatetimeTimeMixin): """ A class representing comment's snippet info. Refer: https://developers.google.com/youtube/v3/docs/comments#snippet """ authorDisplayName: Optional[str] = field(default=None) authorProfileImageUrl: Optional[str] = field(default=None, repr=False) authorChannelUrl: Optional[str] = field(default=None, repr=False) authorChannelId: Optional[CommentSnippetAuthorChannelId] = field( default=None, repr=False ) channelId: Optional[str] = field(default=None, repr=False) # videoId has deprecated, see https://developers.google.com/youtube/v3/revision_history#november-09,-2023 videoId: Optional[str] = field(default=None, repr=False) textDisplay: Optional[str] = field(default=None, repr=False) textOriginal: Optional[str] = field(default=None, repr=False) parentId: Optional[str] = field(default=None, repr=False) canRate: Optional[bool] = field(default=None, repr=False) viewerRating: Optional[str] = field(default=None, repr=False) likeCount: Optional[int] = field(default=None) moderationStatus: Optional[str] = field(default=None, repr=False) publishedAt: Optional[str] = field(default=None, repr=False) updatedAt: Optional[str] = field(default=None, repr=False) @dataclass class Comment(BaseResource): """ A class representing comment info. Refer: https://developers.google.com/youtube/v3/docs/comments """ snippet: Optional[CommentSnippet] = field(default=None) @dataclass class CommentListResponse(BaseApiResponse): """ A class representing the comment's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/comments/list#response_1 """ items: Optional[List[Comment]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/comment_thread.py ================================================ """ These are comment threads related models. """ from dataclasses import dataclass, field from typing import Optional, List from .base import BaseModel from .common import BaseResource, BaseApiResponse from .comment import Comment @dataclass class CommentThreadSnippet(BaseModel): """A class representing comment tread snippet info. References: https://developers.google.com/youtube/v3/docs/commentThreads#snippet """ channelId: Optional[str] = field(default=None) videoId: Optional[str] = field(default=None) topLevelComment: Optional[Comment] = field(default=None, repr=False) canReply: Optional[bool] = field(default=None, repr=False) totalReplyCount: Optional[int] = field(default=None, repr=False) isPublic: Optional[bool] = field(default=None, repr=False) @dataclass class CommentThreadReplies(BaseModel): """ A class representing comment tread replies info. Refer: https://developers.google.com/youtube/v3/docs/commentThreads#replies """ comments: Optional[List[Comment]] = field(default=None, repr=False) @dataclass class CommentThread(BaseResource): """ A class representing comment thread info. Refer: https://developers.google.com/youtube/v3/docs/commentThreads """ snippet: Optional[CommentThreadSnippet] = field(default=None, repr=False) replies: Optional[CommentThreadReplies] = field(default=None, repr=False) @dataclass class CommentThreadListResponse(BaseApiResponse): """ A class representing the comment thread's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/commentThreads/list#response_1 """ items: Optional[List[CommentThread]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/common.py ================================================ """ These are common models for multi resource. """ from dataclasses import dataclass, field from typing import Optional, List from .base import BaseModel @dataclass class Thumbnail(BaseModel): """ A class representing the thumbnail resource info. Refer: https://developers.google.com/youtube/v3/docs/channels#snippet.thumbnails.(key).url """ url: Optional[str] = field(default=None) width: Optional[int] = field(default=None, repr=False) height: Optional[int] = field(default=None, repr=False) @dataclass class Thumbnails(BaseModel): """ A class representing the multi thumbnail resource info. Refer: https://developers.google.com/youtube/v3/docs/channels#snippet.thumbnails """ default: Optional[Thumbnail] = field(default=None) medium: Optional[Thumbnail] = field(default=None, repr=False) high: Optional[Thumbnail] = field(default=None, repr=False) standard: Optional[Thumbnail] = field(default=None, repr=False) maxres: Optional[Thumbnail] = field(default=None, repr=False) @dataclass class Topic(BaseModel): """ A class representing the channel topic info. this model also suitable for video. Refer: https://developers.google.com/youtube/v3/docs/channels#topicDetails.topicIds[] https://developers.google.com/youtube/v3/docs/videos#topicDetails.topicIds[] This model is customized for parsing topic id. YouTube Data Api not return this. """ id: Optional[str] = field(default=None) description: Optional[str] = field(default=None) @dataclass class BaseTopicDetails(BaseModel): """ This is the base model for channel or video topic details. """ topicIds: List[str] = field(default=None, repr=False) def get_full_topics(self): """ Convert topicIds list to Topic model list :return: List[Topic] """ from pyyoutube import TOPICS r: List[Topic] = [] if self.topicIds: for topic_id in self.topicIds: topic = Topic.from_dict( {"id": topic_id, "description": TOPICS.get(topic_id)} ) r.append(topic) return r @dataclass class Localized(BaseModel): """ A class representing the channel or video snippet localized info. Refer: https://developers.google.com/youtube/v3/docs/channels#snippet.localized https://developers.google.com/youtube/v3/docs/videos#snippet.localized """ title: Optional[str] = field(default=None) description: Optional[str] = field(default=None, repr=False) @dataclass class PageInfo(BaseModel): """ This is data model for save paging data. Note: totalResults is only an approximation/estimate. Refer: https://stackoverflow.com/questions/43507281/totalresults-count-doesnt-match-with-the-actual-results-returned-in-youtube-v3 """ totalResults: Optional[int] = field(default=None) resultsPerPage: Optional[int] = field(default=None) @dataclass class BaseApiResponse(BaseModel): """ This is Data Api response structure when retrieve data. They both have same response structure, but items. Refer: https://developers.google.com/youtube/v3/docs/channels/list#response_1 https://developers.google.com/youtube/v3/docs/playlistItems/list#response_1 """ kind: Optional[str] = field(default=None) etag: Optional[str] = field(default=None, repr=False) nextPageToken: Optional[str] = field(default=None, repr=False) prevPageToken: Optional[str] = field(default=None, repr=False) pageInfo: Optional[PageInfo] = field(default=None, repr=False) @dataclass class BaseResource(BaseModel): """ This is a base model for different resource type. Refer: https://developers.google.com/youtube/v3/docs#resource-types """ kind: Optional[str] = field(default=None) etag: Optional[str] = field(default=None, repr=False) id: Optional[str] = field(default=None) @dataclass class ResourceId(BaseModel): """ A class representing the subscription snippet resource info. Refer: 1. https://developers.google.com/youtube/v3/docs/playlistItems#snippet.resourceId 2. https://developers.google.com/youtube/v3/docs/subscriptions#snippet.resourceId 3. https://developers.google.com/youtube/v3/docs/activities#contentDetails.social.resourceId """ kind: Optional[str] = field(default=None) videoId: Optional[str] = field(default=None) channelId: Optional[str] = field(default=None) playlistId: Optional[str] = field(default=None) @dataclass class Player(BaseModel): """ A class representing the video,playlist player info. Refer: https://developers.google.com/youtube/v3/docs/videos#player """ embedHtml: Optional[str] = field(default=None) # Important: # follows attributions maybe not exists. embedHeight: Optional[int] = field(default=None, repr=False) embedWidth: Optional[int] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/i18n.py ================================================ """ These are i18n language and region related models. """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .common import BaseResource, BaseApiResponse @dataclass class I18nRegionSnippet(BaseModel): """ A class representing the I18n region snippet info. Refer: https://developers.google.com/youtube/v3/docs/i18nRegions#snippet """ gl: Optional[str] = field(default=None) name: Optional[str] = field(default=None) @dataclass class I18nRegion(BaseResource): """ A class representing the I18n region info. Refer: https://developers.google.com/youtube/v3/docs/i18nRegions#resource-representation """ snippet: Optional[I18nRegionSnippet] = field(default=None) @dataclass class I18nRegionListResponse(BaseApiResponse): """ A class representing the I18n region list response info. Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages/list#response_1 """ items: Optional[List[I18nRegion]] = field(default=None, repr=False) @dataclass class I18nLanguageSnippet(BaseModel): """ A class representing the I18n language snippet info. Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages#snippet """ hl: Optional[str] = field(default=None) name: Optional[str] = field(default=None) @dataclass class I18nLanguage(BaseResource): """ A class representing the I18n language info. Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages#resource-representation """ snippet: Optional[I18nLanguageSnippet] = field(default=None) @dataclass class I18nLanguageListResponse(BaseApiResponse): """ A class representing the I18n language list response info. Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages/list#response_1 """ items: Optional[List[I18nLanguage]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/member.py ================================================ """ These are member related models. """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .common import BaseApiResponse from .mixins import DatetimeTimeMixin @dataclass class MemberSnippetMemberDetails(BaseModel): """ A class representing the member snippet member detail. Refer: https://developers.google.com/youtube/v3/docs/members#snippet.memberDetails """ channelId: Optional[str] = field(default=None) channelUrl: Optional[str] = field(default=None, repr=False) displayName: Optional[str] = field(default=None, repr=False) profileImageUrl: Optional[str] = field(default=None, repr=False) @dataclass class MemberSnippetMembershipsDuration(BaseModel, DatetimeTimeMixin): memberSince: Optional[str] = field(default=None) memberTotalDurationMonths: Optional[int] = field(default=None, repr=False) @dataclass class MemberSnippetMembershipsDurationAtLevel(BaseModel): level: Optional[str] = field(default=None) memberSince: Optional[str] = field(default=None, repr=False) memberTotalDurationMonths: Optional[int] = field(default=None, repr=False) @dataclass class MemberSnippetMembershipsDetails(BaseModel): """ A class representing the member snippet membership detail. Refer: https://developers.google.com/youtube/v3/docs/members#snippet.membershipsDetails """ highestAccessibleLevel: Optional[str] = field(default=None) highestAccessibleLevelDisplayName: Optional[str] = field(default=None) accessibleLevels: Optional[List[str]] = field(default=None, repr=False) membershipsDuration: Optional[MemberSnippetMembershipsDuration] = field( default=None, repr=False ) membershipsDurationAtLevel: Optional[ List[MemberSnippetMembershipsDurationAtLevel] ] = field(default=None, repr=False) @dataclass class MemberSnippet(BaseModel): """ A class representing the member snippet info. Refer: https://developers.google.com/youtube/v3/docs/members#snippet """ creatorChannelId: Optional[str] = field(default=None) memberDetails: Optional[MemberSnippetMemberDetails] = field( default=None, repr=False ) membershipsDetails: Optional[MemberSnippetMembershipsDetails] = field( default=None, repr=False ) @dataclass class Member(BaseModel): """ A class representing the member info. Refer: https://developers.google.com/youtube/v3/docs/members """ kind: Optional[str] = field(default=None) etag: Optional[str] = field(default=None, repr=False) snippet: Optional[MemberSnippet] = field(default=None, repr=False) @dataclass class MemberListResponse(BaseApiResponse): """ A class representing the member's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/members/list#response """ items: Optional[List[Member]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/memberships_level.py ================================================ """ These are membership level related models. """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .common import BaseResource, BaseApiResponse @dataclass class MembershipLevelSnippetLevelDetails(BaseModel): displayName: Optional[str] = field(default=None) @dataclass class MembershipsLevelSnippet(BaseModel): """ A class representing the membership level snippet. Refer: https://developers.google.com/youtube/v3/docs/membershipsLevels#snippet """ creatorChannelId: Optional[str] = field(default=None) levelDetails: Optional[MembershipLevelSnippetLevelDetails] = field( default=None, repr=False ) @dataclass class MembershipsLevel(BaseResource): """ A class representing the membership level. Refer: https://developers.google.com/youtube/v3/docs/membershipsLevels """ snippet: Optional[MembershipsLevelSnippet] = field(default=None, repr=False) @dataclass class MembershipsLevelListResponse(BaseApiResponse): """ A class representing the memberships level's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/membershipsLevels/list#response """ items: Optional[List[MembershipsLevel]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/mixins.py ================================================ """ These are some mixin for models """ import datetime from typing import Optional import isodate from isodate.isoerror import ISO8601Error from pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException class DatetimeTimeMixin: @staticmethod def string_to_datetime(dt_str: Optional[str]) -> Optional[datetime.datetime]: """ Convert datetime string to datetime instance. original string format is YYYY-MM-DDThh:mm:ss.sZ. :return: """ if not dt_str: return None try: r = isodate.parse_datetime(dt_str) except ISO8601Error as e: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.INVALID_PARAMS, message=e.args[0]) ) else: return r ================================================ FILE: pyyoutube/models/playlist.py ================================================ """ These are playlist related models. """ from dataclasses import dataclass, field from typing import Optional, List from .base import BaseModel from .common import BaseApiResponse, BaseResource, Localized, Player, Thumbnails from .mixins import DatetimeTimeMixin @dataclass class PlaylistContentDetails(BaseModel): """ A class representing playlist's content details info. Refer: https://developers.google.com/youtube/v3/docs/playlists#contentDetails """ itemCount: Optional[int] = field(default=None) @dataclass class PlaylistSnippet(BaseModel, DatetimeTimeMixin): """ A class representing the playlist snippet info. Refer: https://developers.google.com/youtube/v3/docs/playlists#snippet """ publishedAt: Optional[str] = field(default=None, repr=False) channelId: Optional[str] = field(default=None, repr=False) title: Optional[str] = field(default=None) description: Optional[str] = field(default=None) thumbnails: Optional[Thumbnails] = field(default=None, repr=False) channelTitle: Optional[str] = field(default=None, repr=False) defaultLanguage: Optional[str] = field(default=None, repr=False) localized: Optional[Localized] = field(default=None, repr=False) @dataclass class PlaylistStatus(BaseModel): """ A class representing the playlist status info. Refer: https://developers.google.com/youtube/v3/docs/playlists#status """ privacyStatus: Optional[str] = field(default=None) @dataclass class Playlist(BaseResource): """ A class representing the playlist info. Refer: https://developers.google.com/youtube/v3/docs/playlists """ snippet: Optional[PlaylistSnippet] = field(default=None, repr=False) status: Optional[PlaylistStatus] = field(default=None, repr=False) contentDetails: Optional[PlaylistContentDetails] = field(default=None, repr=False) player: Optional[Player] = field(default=None, repr=False) @dataclass class PlaylistListResponse(BaseApiResponse): """ A class representing the playlist's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/playlists/list#response_1 """ items: Optional[List[Playlist]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/playlist_item.py ================================================ """ These are playlistItem related models. """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .mixins import DatetimeTimeMixin from .common import BaseApiResponse, BaseResource, ResourceId, Thumbnails @dataclass class PlaylistItemContentDetails(BaseModel, DatetimeTimeMixin): """ A class representing the playlist item's content details info. Refer: https://developers.google.com/youtube/v3/docs/playlistItems#contentDetails """ videoId: Optional[str] = field(default=None) note: Optional[str] = field(default=None, repr=False) videoPublishedAt: Optional[str] = field(default=None) startAt: Optional[str] = field(default=None, repr=False) endAt: Optional[str] = field(default=None, repr=False) @dataclass class PlaylistItemSnippet(BaseModel, DatetimeTimeMixin): """ A class representing the playlist item's snippet info. Refer: https://developers.google.com/youtube/v3/docs/playlistItems#snippet """ publishedAt: Optional[str] = field(default=None, repr=False) channelId: Optional[str] = field(default=None, repr=False) title: Optional[str] = field(default=None) description: Optional[str] = field(default=None) thumbnails: Optional[Thumbnails] = field(default=None, repr=False) channelTitle: Optional[str] = field(default=None, repr=False) videoOwnerChannelTitle: Optional[str] = field(default=None, repr=False) videoOwnerChannelId: Optional[str] = field(default=None, repr=False) playlistId: Optional[str] = field(default=None, repr=False) position: Optional[int] = field(default=None, repr=False) resourceId: Optional[ResourceId] = field(default=None, repr=False) @dataclass class PlaylistItemStatus(BaseModel): """ A class representing the playlist item's status info. Refer: https://developers.google.com/youtube/v3/docs/playlistItems#status """ privacyStatus: Optional[str] = field(default=None) @dataclass class PlaylistItem(BaseResource): """ A class representing the playlist item's info. Refer: https://developers.google.com/youtube/v3/docs/playlistItems """ snippet: Optional[PlaylistItemSnippet] = field(default=None, repr=False) contentDetails: Optional[PlaylistItemContentDetails] = field( default=None, repr=False ) status: Optional[PlaylistItemStatus] = field(default=None, repr=False) @dataclass class PlaylistItemListResponse(BaseApiResponse): """ A class representing the playlist item's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/playlistItems/list#response_1 """ items: Optional[List[PlaylistItem]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/search_result.py ================================================ """ These are search result related models. """ from dataclasses import dataclass, field from typing import Optional, List from .base import BaseModel from .common import BaseApiResponse, BaseResource, Thumbnails from .mixins import DatetimeTimeMixin @dataclass class SearchResultSnippet(BaseModel, DatetimeTimeMixin): """ A class representing the search result snippet info. Refer: https://developers.google.com/youtube/v3/docs/search#snippet """ publishedAt: Optional[str] = field(default=None, repr=False) channelId: Optional[str] = field(default=None) title: Optional[str] = field(default=None) description: Optional[str] = field(default=None, repr=False) thumbnails: Optional[Thumbnails] = field(default=None, repr=False) channelTitle: Optional[str] = field(default=None, repr=False) liveBroadcastContent: Optional[str] = field(default=None, repr=False) @dataclass class SearchResultId(BaseModel): """ A class representing the search result id info. Refer: https://developers.google.com/youtube/v3/docs/search#id """ kind: Optional[str] = field(default=None) videoId: Optional[str] = field(default=None, repr=False) channelId: Optional[str] = field(default=None, repr=False) playlistId: Optional[str] = field(default=None, repr=False) @dataclass class SearchResult(BaseResource): """ A class representing the search result's info. Refer: https://developers.google.com/youtube/v3/docs/search """ id: Optional[SearchResultId] = field(default=None, repr=False) snippet: Optional[SearchResultSnippet] = field(default=None, repr=False) @dataclass class SearchListResponse(BaseApiResponse): """ A class representing the channel's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/channels/list#response_1 """ regionCode: Optional[str] = field(default=None, repr=False) items: Optional[List[SearchResult]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/subscription.py ================================================ """ These are subscription related models. """ from dataclasses import dataclass, field from typing import List, Optional from .base import BaseModel from .common import BaseApiResponse, BaseResource, ResourceId, Thumbnails from .mixins import DatetimeTimeMixin @dataclass class SubscriptionSnippet(BaseModel, DatetimeTimeMixin): """ A class representing the subscription snippet info. Refer: https://developers.google.com/youtube/v3/docs/subscriptions#snippet """ publishedAt: Optional[str] = field(default=None, repr=False) channelTitle: Optional[str] = field(default=None, repr=False) title: Optional[str] = field(default=None) description: Optional[str] = field(default=None) resourceId: Optional[ResourceId] = field(default=None, repr=False) channelId: Optional[str] = field(default=None, repr=False) thumbnails: Optional[Thumbnails] = field(default=None, repr=False) @dataclass class SubscriptionContentDetails(BaseModel): """ A class representing the subscription contentDetails info. Refer: https://developers.google.com/youtube/v3/docs/subscriptions#contentDetails """ totalItemCount: Optional[int] = field(default=None) newItemCount: Optional[int] = field(default=None) activityType: Optional[str] = field(default=None, repr=False) @dataclass class SubscriptionSubscriberSnippet(BaseModel): """ A class representing the subscription subscriberSnippet info. Refer: https://developers.google.com/youtube/v3/docs/subscriptions#subscriberSnippet """ title: Optional[str] = field(default=None) description: Optional[str] = field(default=None) channelId: Optional[str] = field(default=None, repr=False) thumbnails: Optional[Thumbnails] = field(default=None, repr=False) @dataclass class Subscription(BaseResource): """ A class representing the subscription info. Refer: https://developers.google.com/youtube/v3/docs/subscriptions """ snippet: Optional[SubscriptionSnippet] = field(default=None) contentDetails: Optional[SubscriptionContentDetails] = field( default=None, repr=False ) subscriberSnippet: Optional[SubscriptionSubscriberSnippet] = field( default=None, repr=False ) @dataclass class SubscriptionListResponse(BaseApiResponse): """ A class representing the subscription's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/subscriptions/list#response_1 """ items: Optional[List[Subscription]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/video.py ================================================ """ These are video related models. """ from dataclasses import dataclass, field from typing import Optional, List import isodate from isodate import ISO8601Error from pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException from .base import BaseModel from .common import ( BaseApiResponse, BaseTopicDetails, BaseResource, Localized, Player, Thumbnails, ) from .mixins import DatetimeTimeMixin @dataclass class RegionRestriction(BaseModel): """ A class representing the video content details region restriction info Refer: https://developers.google.com/youtube/v3/docs/videos#contentDetails.regionRestriction """ allowed: Optional[List[str]] = field(default=None) blocked: Optional[List[str]] = field(default=None, repr=False) # TODO get detail rating description class ContentRating(BaseModel): """ A class representing the video content rating info. Refer: https://developers.google.com/youtube/v3/docs/videos#contentDetails.contentRating """ acbRating: Optional[str] = field(default=None, repr=False) agcomRating: Optional[str] = field(default=None, repr=False) anatelRating: Optional[str] = field(default=None, repr=False) bbfcRating: Optional[str] = field(default=None, repr=False) bfvcRating: Optional[str] = field(default=None, repr=False) bmukkRating: Optional[str] = field(default=None, repr=False) catvRating: Optional[str] = field(default=None, repr=False) catvfrRating: Optional[str] = field(default=None, repr=False) cbfcRating: Optional[str] = field(default=None, repr=False) cccRating: Optional[str] = field(default=None, repr=False) cceRating: Optional[str] = field(default=None, repr=False) chfilmRating: Optional[str] = field(default=None, repr=False) chvrsRating: Optional[str] = field(default=None, repr=False) cicfRating: Optional[str] = field(default=None, repr=False) cnaRating: Optional[str] = field(default=None, repr=False) cncRating: Optional[str] = field(default=None, repr=False) csaRating: Optional[str] = field(default=None, repr=False) cscfRating: Optional[str] = field(default=None, repr=False) czfilmRating: Optional[str] = field(default=None, repr=False) djctqRating: Optional[str] = field(default=None, repr=False) djctqRatingReasons: List[str] = field(default=None, repr=False) ecbmctRating: Optional[str] = field(default=None, repr=False) eefilmRating: Optional[str] = field(default=None, repr=False) egfilmRating: Optional[str] = field(default=None, repr=False) eirinRating: Optional[str] = field(default=None, repr=False) fcbmRating: Optional[str] = field(default=None, repr=False) fcoRating: Optional[str] = field(default=None, repr=False) fpbRating: Optional[str] = field(default=None, repr=False) fpbRatingReasons: List[str] = field(default=None, repr=False) fskRating: Optional[str] = field(default=None, repr=False) grfilmRating: Optional[str] = field(default=None, repr=False) icaaRating: Optional[str] = field(default=None, repr=False) ifcoRating: Optional[str] = field(default=None, repr=False) ilfilmRating: Optional[str] = field(default=None, repr=False) incaaRating: Optional[str] = field(default=None, repr=False) kfcbRating: Optional[str] = field(default=None, repr=False) kijkwijzerRating: Optional[str] = field(default=None, repr=False) kmrbRating: Optional[str] = field(default=None, repr=False) lsfRating: Optional[str] = field(default=None, repr=False) mccaaRating: Optional[str] = field(default=None, repr=False) mccypRating: Optional[str] = field(default=None, repr=False) mcstRating: Optional[str] = field(default=None, repr=False) mdaRating: Optional[str] = field(default=None, repr=False) medietilsynetRating: Optional[str] = field(default=None, repr=False) mekuRating: Optional[str] = field(default=None, repr=False) mibacRating: Optional[str] = field(default=None, repr=False) mocRating: Optional[str] = field(default=None, repr=False) moctwRating: Optional[str] = field(default=None, repr=False) mpaaRating: Optional[str] = field(default=None, repr=False) mpaatRating: Optional[str] = field(default=None, repr=False) mtrcbRating: Optional[str] = field(default=None, repr=False) nbcRating: Optional[str] = field(default=None, repr=False) nfrcRating: Optional[str] = field(default=None, repr=False) nfvcbRating: Optional[str] = field(default=None, repr=False) nkclvRating: Optional[str] = field(default=None, repr=False) oflcRating: Optional[str] = field(default=None, repr=False) pefilmRating: Optional[str] = field(default=None, repr=False) resorteviolenciaRating: Optional[str] = field(default=None, repr=False) rtcRating: Optional[str] = field(default=None, repr=False) rteRating: Optional[str] = field(default=None, repr=False) russiaRating: Optional[str] = field(default=None, repr=False) skfilmRating: Optional[str] = field(default=None, repr=False) smaisRating: Optional[str] = field(default=None, repr=False) smsaRating: Optional[str] = field(default=None, repr=False) tvpgRating: Optional[str] = field(default=None, repr=False) ytRating: Optional[str] = field(default=None) @dataclass class VideoContentDetails(BaseModel): """ A class representing the video content details info. Refer: https://developers.google.com/youtube/v3/docs/videos#contentDetails """ duration: Optional[str] = field(default=None) dimension: Optional[str] = field(default=None) definition: Optional[str] = field(default=None, repr=False) caption: Optional[str] = field(default=None, repr=False) licensedContent: Optional[bool] = field(default=None, repr=False) regionRestriction: Optional[RegionRestriction] = field(default=None, repr=False) contentRating: Optional[ContentRating] = field(default=None, repr=False) projection: Optional[str] = field(default=None, repr=False) hasCustomThumbnail: Optional[bool] = field(default=None, repr=False) def get_video_seconds_duration(self): if not self.duration: return None try: seconds = isodate.parse_duration(self.duration).total_seconds() except ISO8601Error as e: raise PyYouTubeException( ErrorMessage(status_code=ErrorCode.INVALID_PARAMS, message=e.args[0]) ) else: return int(seconds) @dataclass class VideoTopicDetails(BaseTopicDetails): """ A class representing video's topic detail info. Refer: https://developers.google.com/youtube/v3/docs/videos#topicDetails """ # Important: # This property has been deprecated as of November 10, 2016. # Any topics associated with a video are now returned by the topicDetails.relevantTopicIds[] property value. topicIds: Optional[List[str]] = field(default=None, repr=False) relevantTopicIds: Optional[List[str]] = field(default=None, repr=False) topicCategories: Optional[List[str]] = field(default=None) def __post_init__(self): """ If topicIds is not return and relevantTopicIds has return. let relevantTopicIds for topicIds. This is for the get_full_topics method. :return: """ if self.topicIds is None and self.relevantTopicIds is not None: self.topicIds = self.relevantTopicIds @dataclass class VideoSnippet(BaseModel, DatetimeTimeMixin): """ A class representing the video snippet info. Refer: https://developers.google.com/youtube/v3/docs/videos#snippet """ publishedAt: Optional[str] = field(default=None, repr=False) channelId: Optional[str] = field(default=None, repr=False) title: Optional[str] = field(default=None) description: Optional[str] = field(default=None) thumbnails: Optional[Thumbnails] = field(default=None, repr=False) channelTitle: Optional[str] = field(default=None, repr=False) tags: Optional[List[str]] = field(default=None, repr=False) categoryId: Optional[str] = field(default=None, repr=False) liveBroadcastContent: Optional[str] = field(default=None, repr=False) defaultLanguage: Optional[str] = field(default=None, repr=False) localized: Optional[Localized] = field(default=None, repr=False) defaultAudioLanguage: Optional[str] = field(default=None, repr=False) @dataclass class VideoStatistics(BaseModel): """ A class representing the video statistics info. Refer: https://developers.google.com/youtube/v3/docs/videos#statistics """ viewCount: Optional[int] = field(default=None) likeCount: Optional[int] = field(default=None) dislikeCount: Optional[int] = field(default=None, repr=False) commentCount: Optional[int] = field(default=None, repr=False) @dataclass class VideoStatus(BaseModel, DatetimeTimeMixin): """ A class representing the video status info. Refer: https://developers.google.com/youtube/v3/docs/videos#status """ uploadStatus: Optional[str] = field(default=None) failureReason: Optional[str] = field(default=None, repr=False) rejectionReason: Optional[str] = field(default=None, repr=False) privacyStatus: Optional[str] = field(default=None) publishAt: Optional[str] = field(default=None, repr=False) license: Optional[str] = field(default=None, repr=False) embeddable: Optional[bool] = field(default=None, repr=False) publicStatsViewable: Optional[bool] = field(default=None, repr=False) madeForKids: Optional[bool] = field(default=None, repr=False) selfDeclaredMadeForKids: Optional[bool] = field(default=None, repr=False) @dataclass class VideoRecordingDetails(BaseModel, DatetimeTimeMixin): """ A class representing the video recording details. Refer: https://developers.google.com/youtube/v3/docs/videos#recordingDetails """ recordingDate: Optional[str] = field(default=None, repr=False) @dataclass class VideoLiveStreamingDetails(BaseModel, DatetimeTimeMixin): """ A class representing the video live streaming details. Refer: https://developers.google.com/youtube/v3/docs/videos#liveStreamingDetails """ actualStartTime: Optional[str] = field(default=None, repr=False) actualEndTime: Optional[str] = field(default=None, repr=False) scheduledStartTime: Optional[str] = field(default=None, repr=False) scheduledEndTime: Optional[str] = field(default=None, repr=False) concurrentViewers: Optional[int] = field(default=None) activeLiveChatId: Optional[str] = field(default=None, repr=False) @dataclass class PaidProductPlacementDetail(BaseModel): hasPaidProductPlacement: Optional[dataclass] = field(default=None, repr=False) @dataclass class Video(BaseResource): """ A class representing the video info. Refer: https://developers.google.com/youtube/v3/docs/videos """ snippet: Optional[VideoSnippet] = field(default=None, repr=False) contentDetails: Optional[VideoContentDetails] = field(default=None, repr=False) status: Optional[VideoStatus] = field(default=None, repr=False) statistics: Optional[VideoStatistics] = field(default=None, repr=False) topicDetails: Optional[VideoTopicDetails] = field(default=None, repr=False) player: Optional[Player] = field(default=None, repr=False) recordingDetails: Optional[VideoRecordingDetails] = field(default=None, repr=False) liveStreamingDetails: Optional[VideoLiveStreamingDetails] = field( default=None, repr=False ) paidProductPlacementDetail: Optional[PaidProductPlacementDetail] = field( default=None, repr=False ) @dataclass class VideoListResponse(BaseApiResponse): """ A class representing the video's retrieve response info. Refer: https://developers.google.com/youtube/v3/docs/videos/list#response_1 """ items: Optional[List[Video]] = field(default=None, repr=False) @dataclass class VideoReportAbuse(BaseModel): """ A class representing the video report abuse body. """ videoId: Optional[str] = field(default=None) reasonId: Optional[str] = field(default=None) secondaryReasonId: Optional[str] = field(default=None) comments: Optional[str] = field(default=None) language: Optional[str] = field(default=None) @dataclass class VideoRatingItem(BaseModel): """ A class representing the video rating item info. """ videoId: Optional[str] = field(default=None) rating: Optional[str] = field(default=None) @dataclass class VideoGetRatingResponse(BaseApiResponse): """ A class representing the video rating response. References: https://developers.google.com/youtube/v3/docs/videos/getRating#properties """ items: Optional[List[VideoRatingItem]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/video_abuse_report_reason.py ================================================ """ These are video abuse report reason related models. """ from dataclasses import dataclass, field from typing import Optional, List from .base import BaseModel from .common import BaseResource, BaseApiResponse @dataclass class SecondaryReason(BaseModel): """ A class representing the video abuse report reason info Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons#snippet.secondaryReasons """ id: Optional[str] = field(default=None) label: Optional[str] = field(default=None, repr=True) @dataclass class VideoAbuseReportReasonSnippet(BaseModel): """ A class representing the video abuse report snippet info Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons#snippet """ label: Optional[str] = field(default=None) secondaryReasons: Optional[List[SecondaryReason]] = field(default=None, repr=True) @dataclass class VideoAbuseReportReason(BaseResource): """ A class representing the video abuse report info Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons """ snippet: Optional[VideoAbuseReportReasonSnippet] = field(default=None) @dataclass class VideoAbuseReportReasonListResponse(BaseApiResponse): """ A class representing the I18n language list response info. Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons/list#response_1 """ items: Optional[List[VideoAbuseReportReason]] = field(default=None, repr=False) ================================================ FILE: pyyoutube/models/watermark.py ================================================ """ These are watermark related models. """ from dataclasses import dataclass, field from typing import Optional from .base import BaseModel @dataclass class WatermarkTiming(BaseModel): type: Optional[str] = field(default=None) offsetMs: Optional[int] = field(default=None, repr=False) durationMs: Optional[int] = field(default=None, repr=False) @dataclass class WatermarkPosition(BaseModel): type: Optional[str] = field(default=None) cornerPosition: Optional[str] = field(default=None, repr=False) @dataclass class Watermark(BaseModel): """ A class representing the watermark info. References: https://developers.google.com/youtube/v3/docs/watermarks#resource-representation """ timing: Optional[WatermarkTiming] = field(default=None, repr=False) position: Optional[WatermarkPosition] = field(default=None, repr=False) imageUrl: Optional[str] = field(default=None) imageBytes: Optional[bytes] = field(default=None, repr=False) targetChannelId: Optional[str] = field(default=None, repr=False) ================================================ FILE: pyyoutube/resources/__init__.py ================================================ from .activities import ActivitiesResource # noqa from .captions import CaptionsResource # noqa from .channel_banners import ChannelBannersResource # noqa from .channels import ChannelsResource # noqa from .channel_sections import ChannelSectionsResource # noqa from .comments import CommentsResource # noqa from .comment_threads import CommentThreadsResource # noqa from .i18n_languages import I18nLanguagesResource # noqa from .i18n_regions import I18nRegionsResource # noqa from .members import MembersResource # noqa from .membership_levels import MembershipLevelsResource # noqa from .playlist_items import PlaylistItemsResource # noqa from .playlists import PlaylistsResource # noqa from .search import SearchResource # noqa from .subscriptions import SubscriptionsResource # noqa from .thumbnails import ThumbnailsResource # noqa from .video_abuse_report_reasons import VideoAbuseReportReasonsResource # noqa from .video_categories import VideoCategoriesResource # noqa from .videos import VideosResource # noqa from .watermarks import WatermarksResource # noqa ================================================ FILE: pyyoutube/resources/activities.py ================================================ """ Activities resource implementation """ from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode from pyyoutube.resources.base_resource import Resource from pyyoutube.models import ActivityListResponse from pyyoutube.utils.params_checker import enf_parts class ActivitiesResource(Resource): """An activity resource contains information about an action that a particular channel, or user, has taken on YouTube. References: https://developers.google.com/youtube/v3/docs/activities """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, channel_id: Optional[str] = None, mine: Optional[bool] = None, max_results: Optional[int] = None, page_token: Optional[str] = None, published_after: Optional[str] = None, published_before: Optional[str] = None, region_code: Optional[str] = None, return_json: bool = False, **kwargs, ) -> Union[dict, ActivityListResponse]: """Returns a list of channel activity events that match the request criteria. Args: parts: Comma-separated list of one or more activity resource properties. channel_id: The channelId parameter specifies a unique YouTube channel ID. mine: This parameter can only be used in a properly authorized request. Set this parameter's value to true to retrieve a feed of the authenticated user's activities. max_results: The parameter specifies the maximum number of items that should be returned the result set. Acceptable values are 0 to 50, inclusive. The default value is 5. page_token: The parameter identifies a specific page in the result set that should be returned. published_after: The parameter specifies the earliest date and time that an activity could have occurred for that activity to be included in the API response. published_before: The parameter specifies the date and time before which an activity must have occurred for that activity to be included in the API response. region_code: The parameter instructs the API to return results for the specified country. return_json: Type for returned data. If you set True JSON data will be returned. kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Activities data """ params = { "part": enf_parts(resource="activities", value=parts), "maxResults": max_results, "pageToken": page_token, "publishedAfter": published_after, "publishedBefore": published_before, "regionCode": region_code, **kwargs, } if channel_id is not None: params["channelId"] = channel_id elif mine is not None: params["mine"] = mine else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Specify at least one of channel_id or mine", ) ) response = self._client.request(path="activities", params=params) data = self._client.parse_response(response=response) return data if return_json else ActivityListResponse.from_dict(data) ================================================ FILE: pyyoutube/resources/base_resource.py ================================================ """ Base resource class. """ from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: from pyyoutube import Client # pragma: no cover class Resource: """Resource base class""" def __init__(self, client: Optional["Client"] = None): self._client = client @property def access_token(self): return self._client.access_token @property def api_key(self): return self._client.api_key ================================================ FILE: pyyoutube/resources/captions.py ================================================ """ Captions resource implementation """ from typing import Optional, Union from requests import Response from pyyoutube.resources.base_resource import Resource from pyyoutube.media import Media, MediaUpload from pyyoutube.models import Caption, CaptionListResponse from pyyoutube.utils.params_checker import enf_comma_separated, enf_parts class CaptionsResource(Resource): """A caption resource represents a YouTube caption track References: https://developers.google.com/youtube/v3/docs/captions """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, video_id: Optional[str] = None, caption_id: Optional[Union[str, list, tuple, set]] = None, on_behalf_of_content_owner: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, CaptionListResponse]: """Returns a list of caption tracks that are associated with a specified video. Args: parts: Comma-separated list of one or more caption resource properties. video_id: The parameter specifies the YouTube video ID of the video for which the API should return caption tracks. caption_id: The id parameter specifies a comma-separated list of IDs that identify the caption resources that should be retrieved. on_behalf_of_content_owner: This parameter can only be used in a properly authorized request. Note: This parameter is intended exclusively for YouTube content partners. return_json: Type for returned data. If you set True JSON data will be returned. kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Caption data """ params = { "part": enf_parts(resource="captions", value=parts), "videoId": video_id, "id": enf_comma_separated(field="caption_id", value=caption_id), "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request(path="captions", params=params) data = self._client.parse_response(response=response) return data if return_json else CaptionListResponse.from_dict(data) def insert( self, body: Union[dict, Caption], media: Media, parts: Optional[Union[str, list, tuple, set]] = None, on_behalf_of_content_owner: Optional[str] = None, sync: Optional[bool] = None, **kwargs, ) -> MediaUpload: """Uploads a caption track. Args: body: Provide caption data in the request body. You can give dataclass or just a dict with data. media: Caption media data to upload. parts: The part parameter specifies the caption resource parts that the API response will include. Set the parameter value to snippet. on_behalf_of_content_owner: This parameter can only be used in a properly authorized request. Note: This parameter is intended exclusively for YouTube content partners. sync: The sync parameter indicates whether YouTube should automatically synchronize the caption file with the audio track of the video. If you set the value to true, YouTube will disregard any time codes that are in the uploaded caption file and generate new time codes for the captions. Important: This parameter will be deprecated at April 12, 2024. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Caption data. """ params = { "part": enf_parts(resource="captions", value=parts), "onBehalfOfContentOwner": on_behalf_of_content_owner, "sync": sync, **kwargs, } # Build a media upload instance. media_upload = MediaUpload( client=self._client, resource="captions", media=media, params=params, body=body.to_dict_ignore_none(), ) return media_upload def update( self, body: Union[dict, Caption], media: Optional[Media] = None, parts: Optional[Union[str, list, tuple, set]] = None, on_behalf_of_content_owner: Optional[str] = None, sync: Optional[bool] = None, return_json: bool = False, **kwargs, ) -> Union[dict, Caption, MediaUpload]: """Updates a caption track. Args: body: Provide caption data in the request body. You can give dataclass or just a dict with data. media: New caption media. parts: The part parameter specifies the caption resource parts that the API response will include. Set the parameter value to snippet. on_behalf_of_content_owner: This parameter can only be used in a properly authorized request. Note: This parameter is intended exclusively for YouTube content partners. sync: The sync parameter indicates whether YouTube should automatically synchronize the caption file with the audio track of the video. If you set the value to true, YouTube will disregard any time codes that are in the uploaded caption file and generate new time codes for the captions. Important: This parameter will be deprecated at April 12, 2024. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Caption data. """ params = { "part": enf_parts(resource="captions", value=parts), "onBehalfOfContentOwner": on_behalf_of_content_owner, "sync": sync, **kwargs, } if media is not None: # Build a media upload instance. media_upload = MediaUpload( client=self._client, resource="captions", media=media, params=params, body=body.to_dict_ignore_none(), ) return media_upload response = self._client.request( method="PUT", path="captions", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else Caption.from_dict(data) def download( self, caption_id: str, on_behalf_of_content_owner: Optional[str] = None, tfmt: Optional[str] = None, tlang: Optional[str] = None, **kwargs, ) -> Response: """Downloads a caption track. Args: caption_id: ID for the caption track that is being deleted. on_behalf_of_content_owner: This parameter can only be used in a properly authorized request. Note: This parameter is intended exclusively for YouTube content partners. tfmt: Specifies that the caption track should be returned in a specific format. Supported values are: sbv – SubViewer subtitle scc – Scenarist Closed Caption format srt – SubRip subtitle ttml – Timed Text Markup Language caption vtt – Web Video Text Tracks caption tlang: Specifies that the API response should return a translation of the specified caption track. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Response form YouTube. """ params = { "id": caption_id, "onBehalfOfContentOwner": on_behalf_of_content_owner, "tfmt": tfmt, "tlang": tlang, **kwargs, } response = self._client.request( path=f"captions/{caption_id}", params=params, ) return response def delete( self, caption_id: str, on_behalf_of_content_owner: Optional[str] = None, **kwargs, ) -> bool: """Deletes a specified caption track. Args: caption_id: ID for the caption track that is being deleted. on_behalf_of_content_owner: This parameter can only be used in a properly authorized request. Note: This parameter is intended exclusively for YouTube content partners. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Delete status Raises: PyYouTubeException: Request not success. """ params = { "id": caption_id, "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request(path="captions", method="DELETE", params=params) if response.ok: return True self._client.parse_response(response=response) ================================================ FILE: pyyoutube/resources/channel_banners.py ================================================ """ Channel banners resource implementation. """ from typing import Optional from pyyoutube.resources.base_resource import Resource from pyyoutube.media import Media, MediaUpload class ChannelBannersResource(Resource): """A channelBanner resource contains the URL that you would use to set a newly uploaded image as the banner image for a channel. References: https://developers.google.com/youtube/v3/docs/channelBanners """ def insert( self, media: Media, on_behalf_of_content_owner: Optional[str] = None, **kwargs: Optional[dict], ) -> MediaUpload: """Uploads a channel banner image to YouTube. Args: media: Banner media data. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many different YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Channel banner data. """ params = {"onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs} # Build a media upload instance. media_upload = MediaUpload( client=self._client, resource="channelBanners/insert", media=media, params=params, ) return media_upload ================================================ FILE: pyyoutube/resources/channel_sections.py ================================================ """ Channel Section resource implementation. """ from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode from pyyoutube.resources.base_resource import Resource from pyyoutube.models import ChannelSection, ChannelSectionListResponse from pyyoutube.utils.params_checker import enf_comma_separated, enf_parts class ChannelSectionsResource(Resource): """A channelSection resource contains information about a set of videos that a channel has chosen to feature. References: https://developers.google.com/youtube/v3/docs/channelSections """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, channel_id: Optional[str] = None, section_id: Optional[Union[str, list, tuple, set]] = None, mine: Optional[bool] = None, hl: Optional[str] = None, on_behalf_of_content_owner: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, ChannelSectionListResponse]: """Returns a list of channelSection resources that match the API request criteria. Args: parts: Comma-separated list of one or more channel resource properties. channel_id: ID for the channel which you want to retrieve sections. section_id: Specifies a comma-separated list of IDs that uniquely identify the channelSection resources that are being retrieved. mine: Set this parameter's value to true to retrieve a feed of the channel sections associated with the authenticated user's YouTube channel. hl: The hl parameter provided support for retrieving localized metadata for a channel section. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many different YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Channel section data. Raises: PyYouTubeException: Missing filter parameter. """ params = { "part": enf_parts(resource="channelSections", value=parts), "hl": hl, "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } if channel_id is not None: params["channelId"] = channel_id elif section_id is not None: params["id"] = enf_comma_separated(field="section_id", value=section_id) elif mine is not None: params["mine"] = mine else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Specify at least one of channel_id, section_id or mine", ) ) response = self._client.request(path="channelSections", params=params) data = self._client.parse_response(response=response) return data if return_json else ChannelSectionListResponse.from_dict(data) def insert( self, body: Union[dict, ChannelSection], parts: Optional[Union[str, list, tuple, set]] = None, on_behalf_of_content_owner: Optional[str] = None, on_behalf_of_content_owner_channel: Optional[str] = None, return_json: bool = False, **kwargs, ) -> Union[dict, ChannelSection]: """Adds a channel section to the authenticated user's channel. A channel can create a maximum of 10 shelves. Args: parts: The part parameter serves two purposes in this operation. It identifies the properties that the write operation will set as well as the properties that the API response will include. Accept values: - id - contentDetails - snippet body: Provide a channelSection resource in the request body. You can give dataclass or just a dict with data. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many different YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. on_behalf_of_content_owner_channel: The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel to which a video is being added. This parameter is required when a request specifies a value for the onBehalfOfContentOwner parameter, and it can only be used in conjunction with that parameter. In addition, the request must be authorized using a CMS account that is linked to the content owner that the onBehalfOfContentOwner parameter specifies. Finally, the channel that the onBehalfOfContentOwnerChannel parameter value specifies must be linked to the content owner that the onBehalfOfContentOwner parameter specifies. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Channel section data. """ params = { "part": enf_parts(resource="channelSections", value=parts), "onBehalfOfContentOwner": on_behalf_of_content_owner, "onBehalfOfContentOwnerChannel": on_behalf_of_content_owner_channel, **kwargs, } response = self._client.request( method="POST", path="channelSections", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else ChannelSection.from_dict(data) def update( self, body: Union[dict, ChannelSection], parts: Optional[Union[str, list, tuple, set]] = None, on_behalf_of_content_owner: Optional[str] = None, return_json: bool = False, **kwargs, ) -> Union[dict, ChannelSection]: """Updates a channel section. Args: parts: The part parameter serves two purposes in this operation. It identifies the properties that the write operation will set as well as the properties that the API response will include. Accept values: - id - contentDetails - snippet body: Provide a channelSection resource in the request body. You can give dataclass or just a dict with data. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many different YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Channel section data. """ params = { "part": enf_parts(resource="channelSections", value=parts), "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="PUT", path="channelSections", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else ChannelSection.from_dict(data) def delete( self, section_id: str, on_behalf_of_content_owner: Optional[str] = None, **kwargs, ) -> bool: """Deletes a channel section. Args: section_id: ID for the target channel section. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many different YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Channel section delete status """ params = { "id": section_id, "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="DELETE", path="channelSections", params=params, ) if response.ok: return True self._client.parse_response(response=response) ================================================ FILE: pyyoutube/resources/channels.py ================================================ """ Channel resource implementation. """ from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode from pyyoutube.resources.base_resource import Resource from pyyoutube.models import Channel, ChannelListResponse from pyyoutube.utils.params_checker import enf_comma_separated, enf_parts class ChannelsResource(Resource): """A channel resource contains information about a YouTube channel. References: https://developers.google.com/youtube/v3/docs/channels """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, for_handle: Optional[str] = None, for_username: Optional[str] = None, channel_id: Optional[Union[str, list, tuple, set]] = None, managed_by_me: Optional[bool] = None, mine: Optional[bool] = None, hl: Optional[str] = None, max_results: Optional[int] = None, on_behalf_of_content_owner: Optional[str] = None, page_token: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, ChannelListResponse]: """Returns a collection of zero or more channel resources that match the request criteria. Args: parts: Comma-separated list of one or more channel resource properties. Accepted values: id,auditDetails,brandingSettings,contentDetails,contentOwnerDetails, localizations,snippet,statistics,status,topicDetails for_handle: The parameter specifies a YouTube handle, thereby requesting the channel associated with that handle. The parameter value can be prepended with an @ symbol. For example, to retrieve the resource for the "Google for Developers" channel, set the forHandle parameter value to either GoogleDevelopers or @GoogleDevelopers. for_username: The parameter specifies a YouTube username, thereby requesting the channel associated with that username. channel_id: The parameter specifies a comma-separated list of the YouTube channel ID(s) for the resource(s) that are being retrieved. managed_by_me: Set this parameter's value to true to instruct the API to only return channels managed by the content owner that the onBehalfOfContentOwner parameter specifies. The user must be authenticated as a CMS account linked to the specified content owner and onBehalfOfContentOwner must be provided. mine: Set this parameter's value to true to instruct the API to only return channels owned by the authenticated user. hl: The hl parameter instructs the API to retrieve localized resource metadata for a specific application language that the YouTube website supports. The parameter value must be a language code included in the list returned by the i18nLanguages.list method. max_results: The parameter specifies the maximum number of items that should be returned the result set. Acceptable values are 0 to 50, inclusive. The default value is 5. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. page_token: The parameter identifies a specific page in the result set that should be returned. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Channel data Raises: PyYouTubeException: Missing filter parameter. Request not success. """ params = { "part": enf_parts(resource="channels", value=parts), "hl": hl, "maxResults": max_results, "onBehalfOfContentOwner": on_behalf_of_content_owner, "pageToken": page_token, **kwargs, } if for_handle is not None: params["forHandle"] = for_handle elif for_username is not None: params["forUsername"] = for_username elif channel_id is not None: params["id"] = enf_comma_separated(field="channel_id", value=channel_id) elif managed_by_me is not None: params["managedByMe"] = managed_by_me elif mine is not None: params["mine"] = mine else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Specify at least one of for_handle,for_username,channel_id,managedByMe or mine", ) ) response = self._client.request(path="channels", params=params) data = self._client.parse_response(response=response) return data if return_json else ChannelListResponse.from_dict(data) def update( self, part: str, body: Union[dict, Channel], on_behalf_of_content_owner: Optional[str] = None, return_json: bool = False, **kwargs, ) -> Union[dict, Channel]: """Updates a channel's metadata. Note that this method currently only supports updates to the channel resource's brandingSettings, invideoPromotion, and localizations objects and their child properties. Args: part: The part parameter serves two purposes in this operation. It identifies the properties that the write operation will set as well as the properties that the API response will include. body: Provide channel data in the request body. You can give dataclass or just a dict with data. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many different YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Channel updated data. """ params = { "part": part, "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="PUT", path="channels", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else Channel.from_dict(data) ================================================ FILE: pyyoutube/resources/comment_threads.py ================================================ """ Comment threads resource implementation. """ from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode from pyyoutube.resources.base_resource import Resource from pyyoutube.models import CommentThread, CommentThreadListResponse from pyyoutube.utils.params_checker import enf_parts class CommentThreadsResource(Resource): """A commentThread resource contains information about a YouTube comment thread, which comprises a top-level comment and replies, if any exist, to that comment References: https://developers.google.com/youtube/v3/docs/commentThreads """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, all_threads_related_to_channel_id: Optional[str] = None, channel_id: Optional[str] = None, thread_id: Optional[Union[str, list, tuple, set]] = None, video_id: Optional[str] = None, max_results: Optional[int] = None, moderation_status: Optional[str] = None, order: Optional[str] = None, page_token: Optional[str] = None, search_terms: Optional[str] = None, text_format: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, CommentThreadListResponse]: """Returns a list of comment threads that match the API request parameters. Args: parts: Comma-separated list of one or more comment thread resource properties. all_threads_related_to_channel_id: Instructs the API to return all comment threads associated with the specified channel. channel_id: Instructs the API to return comment threads containing comments about the specified channel thread_id: Specifies a comma-separated list of comment thread IDs for the resources that should be retrieved. video_id: Instructs the API to return comment threads associated with the specified video ID. max_results: The parameter specifies the maximum number of items that should be returned the result set. Acceptable values are 1 to 100, inclusive. The default value is 20. moderation_status: Set this parameter to limit the returned comment threads to a particular moderation state. The default value is published. Note: This parameter is not supported for use in conjunction with the id parameter. order: Specifies the order in which the API response should list comment threads. Valid values are: - time: Comment threads are ordered by time. This is the default behavior. - relevance: Comment threads are ordered by relevance. Notes: This parameter is not supported for use in conjunction with the `id` parameter. page_token: Identifies a specific page in the result set that should be returned. Notes: This parameter is not supported for use in conjunction with the `id` parameter. search_terms: Instructs the API to limit the API response to only contain comments that contain the specified search terms. Notes: This parameter is not supported for use in conjunction with the `id` parameter. text_format: Set this parameter's value to html or plainText to instruct the API to return the comments left by users in html formatted or in plain text. The default value is html. Acceptable values are: – html: Returns the comments in HTML format. This is the default value. – plainText: Returns the comments in plain text format. Notes: This parameter is not supported for use in conjunction with the `id` parameter. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Comment threads data. Raises: PyYouTubeException: Missing filter parameter. """ params = { "part": enf_parts(resource="commentThreads", value=parts), "maxResults": max_results, "moderationStatus": moderation_status, "order": order, "pageToken": page_token, "searchTerms": search_terms, "textFormat": text_format, **kwargs, } if all_threads_related_to_channel_id is not None: params["allThreadsRelatedToChannelId"] = all_threads_related_to_channel_id elif channel_id: params["channelId"] = channel_id elif thread_id: params["id"] = thread_id elif video_id: params["videoId"] = video_id else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Specify at least one of all_threads_related_to_channel_id,channel_id,thread_id or video_id", ) ) response = self._client.request(path="commentThreads", params=params) data = self._client.parse_response(response=response) return data if return_json else CommentThreadListResponse.from_dict(data) def insert( self, body: Union[dict, CommentThread], parts: Optional[Union[str, list, tuple, set]] = None, return_json: bool = False, **kwargs, ) -> Union[dict, CommentThread]: """Creates a new top-level comment. Notes: To add a reply to an existing comment, use the comments.insert method instead. Args: body: Provide a commentThread resource in the request body. You can give dataclass or just a dict with data. parts: Comma-separated list of one or more comment thread resource properties. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Channel thread data. """ params = { "part": enf_parts(resource="commentThreads", value=parts), **kwargs, } response = self._client.request( method="POST", path="commentThreads", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else CommentThread.from_dict(data) ================================================ FILE: pyyoutube/resources/comments.py ================================================ """ Comment resource implementation. """ from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode from pyyoutube.resources.base_resource import Resource from pyyoutube.models import Comment, CommentListResponse from pyyoutube.utils.params_checker import enf_comma_separated, enf_parts class CommentsResource(Resource): """A comment resource contains information about a single YouTube comment. References: https://developers.google.com/youtube/v3/docs/comments """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, comment_id: Optional[Union[str, list, tuple, set]] = None, parent_id: Optional[str] = None, max_results: Optional[int] = None, text_format: Optional[str] = None, page_token: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, CommentListResponse]: """Returns a list of comments that match the API request parameters. Args: parts: Comma-separated list of one or more comment resource properties. comment_id: Specifies a comma-separated list of comment IDs for the resources that are being retrieved. parent_id: Specifies the ID of the comment for which replies should be retrieved. max_results: The parameter specifies the maximum number of items that should be returned the result set. This parameter is not supported for use in conjunction with the comment_id parameter. Acceptable values are 1 to 100, inclusive. The default value is 20. text_format: Whether the API should return comments formatted as HTML or as plain text. The default value is html. Acceptable values are: - html: Returns the comments in HTML format. - plainText: Returns the comments in plain text format. page_token: The parameter identifies a specific page in the result set that should be returned. return_json: Type for returned data. If you set True JSON data will be returned. kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Comments data Raises: PyYouTubeException: Missing filter parameter. """ params = { "part": enf_parts(resource="comments", value=parts), "maxResults": max_results, "textFormat": text_format, "pageToken": page_token, **kwargs, } if comment_id is not None: params["id"] = enf_comma_separated(field="comment_id", value=comment_id) elif parent_id is not None: params["parentId"] = parent_id else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Specify at least one of comment_id, or parent_id", ) ) response = self._client.request(path="comments", params=params) data = self._client.parse_response(response=response) return data if return_json else CommentListResponse.from_dict(data) def insert( self, body: Union[dict, Comment], parts: Optional[Union[str, list, tuple, set]] = None, return_json: bool = False, **kwargs, ) -> Union[dict, Comment]: """Creates a reply to an existing comment. Notes: To create a top-level comment, use the commentThreads.insert method. Args: body: Provide a comment resource in the request body. You can give dataclass or just a dict with data. parts: Comma-separated list of one or more comment resource properties. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Comment data. """ params = {"part": enf_parts(resource="comments", value=parts), **kwargs} response = self._client.request( method="POST", path="comments", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else Comment.from_dict(data) def update( self, body: Union[dict, Comment], parts: Optional[Union[str, list, tuple, set]] = None, return_json: bool = False, **kwargs, ) -> Union[dict, Comment]: """Modifies a comment. Args: body: Provide a comment resource in the request body. You can give dataclass or just a dict with data. parts: Comma-separated list of one or more comment resource properties. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Comment updated data. """ params = {"part": enf_parts(resource="comments", value=parts), **kwargs} response = self._client.request( method="PUT", path="comments", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else Comment.from_dict(data) def mark_as_spam( self, comment_id: str, **kwargs, ) -> bool: """Expresses the caller's opinion that one or more comments should be flagged as spam. Deprecated at [2023.09.12](https://developers.google.com/youtube/v3/revision_history#september-12,-2023) Args: comment_id: ID for the target comment. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Mark as spam status. """ params = {"id": comment_id, **kwargs} response = self._client.request( method="POST", path="comments/markAsSpam", params=params, ) if response.ok: return True self._client.parse_response(response=response) def set_moderation_status( self, comment_id: str, moderation_status: str, ban_author: Optional[bool] = None, **kwargs, ) -> bool: """Sets the moderation status of one or more comments. Args: comment_id: ID for the target comment. moderation_status: Identifies the new moderation status of the specified comments. Acceptable values: - heldForReview: Marks a comment as awaiting review by a moderator. - published: Clears a comment for public display. - rejected: Rejects a comment as being unfit for display. This action also effectively hides all replies to the rejected comment. ban_author: Set the parameter value to true to ban the author. This parameter is only valid if the moderationStatus parameter is also set to rejected. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Moderation status set status. """ params = { "id": comment_id, "moderationStatus": moderation_status, "banAuthor": ban_author, **kwargs, } response = self._client.request( method="POST", path="comments/setModerationStatus", params=params, ) if response.ok: return True self._client.parse_response(response=response) def delete( self, comment_id: str, **kwargs, ) -> bool: """Deletes a comment. Args: comment_id: ID for the target comment. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Comment delete status. """ params = {"id": comment_id, **kwargs} response = self._client.request( method="DELETE", path="comments", params=params, ) if response.ok: return True self._client.parse_response(response=response) ================================================ FILE: pyyoutube/resources/i18n_languages.py ================================================ """ i18n language resource implementation. """ from typing import Optional, Union from pyyoutube.resources.base_resource import Resource from pyyoutube.models import I18nLanguageListResponse from pyyoutube.utils.params_checker import enf_parts class I18nLanguagesResource(Resource): """An i18nLanguage resource identifies an application language that the YouTube website supports. The application language can also be referred to as a UI language References: https://developers.google.com/youtube/v3/docs/i18nLanguages """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, I18nLanguageListResponse]: """Returns a list of application languages that the YouTube website supports. Args: parts: Comma-separated list of one or more i18n languages resource properties. Accepted values: snippet. hl: Specifies the language that should be used for text values in the API response. The default value is en_US. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: i18n language data """ params = { "part": enf_parts(resource="i18nLanguages", value=parts), "hl": hl, **kwargs, } response = self._client.request(path="i18nLanguages", params=params) data = self._client.parse_response(response=response) return data if return_json else I18nLanguageListResponse.from_dict(data) ================================================ FILE: pyyoutube/resources/i18n_regions.py ================================================ """ i18n regions resource implementation. """ from typing import Optional, Union from pyyoutube.resources.base_resource import Resource from pyyoutube.models import I18nRegionListResponse from pyyoutube.utils.params_checker import enf_parts class I18nRegionsResource(Resource): """An i18nRegion resource identifies a geographic area that a YouTube user can select as the preferred content region. References: https://developers.google.com/youtube/v3/docs/i18nRegions """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, I18nRegionListResponse]: """Returns a list of content regions that the YouTube website supports. Args: parts: Comma-separated list of one or more i18n regions resource properties. Accepted values: snippet. hl: Specifies the language that should be used for text values in the API response. The default value is en_US. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: i18n regions data. """ params = { "part": enf_parts(resource="i18nRegions", value=parts), "hl": hl, **kwargs, } response = self._client.request(path="i18nRegions", params=params) data = self._client.parse_response(response=response) return data if return_json else I18nRegionListResponse.from_dict(data) ================================================ FILE: pyyoutube/resources/members.py ================================================ """ Members resource implementation. """ from typing import Optional, Union from pyyoutube.resources.base_resource import Resource from pyyoutube.models import MemberListResponse from pyyoutube.utils.params_checker import enf_parts, enf_comma_separated class MembersResource(Resource): """A member resource represents a channel member for a YouTube channel. References: https://developers.google.com/youtube/v3/docs/members """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, mode: Optional[str] = None, max_results: Optional[int] = None, page_token: Optional[str] = None, has_access_to_level: Optional[str] = None, filter_by_member_channel_id: Optional[Union[str, list, tuple, set]] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, MemberListResponse]: """Lists members (formerly known as "sponsors") for a channel. Args: parts: Comma-separated list of one or more channel resource properties. Accepted values: snippet mode: Indicates which members will be included in the API response. Accepted values: - all_current: List current members, from newest to oldest. - updates: List only members that joined or upgraded since the previous API call. max_results: The parameter specifies the maximum number of items that should be returned the result set. Acceptable values are 0 to 1000, inclusive. The default value is 5. page_token: The parameter identifies a specific page in the result set that should be returned. has_access_to_level: A level ID that specifies the minimum level that members in the result set should have. filter_by_member_channel_id: specifies a comma-separated list of channel IDs that can be used to check the membership status of specific users. Maximum of 100 channels can be specified per call. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Members data. """ params = { "part": enf_parts(resource="members", value=parts), "mode": mode, "maxResults": max_results, "pageToken": page_token, "hasAccessToLevel": has_access_to_level, "filterByMemberChannelId": enf_comma_separated( field="filter_by_member_channel_id", value=filter_by_member_channel_id ), **kwargs, } response = self._client.request(path="members", params=params) data = self._client.parse_response(response=response) return data if return_json else MemberListResponse.from_dict(data) ================================================ FILE: pyyoutube/resources/membership_levels.py ================================================ """ Membership levels resource implementation. """ from typing import Optional, Union from pyyoutube.models import MembershipsLevelListResponse from pyyoutube.resources.base_resource import Resource from pyyoutube.utils.params_checker import enf_parts class MembershipLevelsResource(Resource): """A membershipsLevel resource identifies a pricing level managed by the creator that authorized the API request. References: https://developers.google.com/youtube/v3/docs/membershipsLevels """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, MembershipsLevelListResponse]: """Lists membership levels for the channel that authorized the request. Args: parts: Comma-separated list of one or more channel resource properties. Accepted values: id,snippet return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Membership levels data. """ params = { "part": enf_parts(resource="membershipsLevels", value=parts), **kwargs, } response = self._client.request(path="membershipsLevels", params=params) data = self._client.parse_response(response=response) return data if return_json else MembershipsLevelListResponse.from_dict(data) ================================================ FILE: pyyoutube/resources/playlist_items.py ================================================ """ Playlist items resource implementation. """ from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorCode, ErrorMessage from pyyoutube.resources.base_resource import Resource from pyyoutube.models import PlaylistItem, PlaylistItemListResponse from pyyoutube.utils.params_checker import enf_parts, enf_comma_separated class PlaylistItemsResource(Resource): """A playlistItem resource identifies another resource, such as a video, that is included in a playlist. In addition, the playlistItem resource contains details about the included resource that pertain specifically to how that resource is used in that playlist. References: https://developers.google.com/youtube/v3/docs/playlistItems """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, playlist_item_id: Optional[Union[str, list, tuple, set]] = None, playlist_id: Optional[str] = None, max_results: Optional[int] = None, on_behalf_of_content_owner: Optional[str] = None, page_token: Optional[str] = None, video_id: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, PlaylistItemListResponse]: """Returns a collection of playlist items that match the API request parameters. Args: parts: Comma-separated list of one or more channel resource properties. Accepted values: id,contentDetails,snippet,snippet playlist_item_id: Specifies a comma-separated list of one or more unique playlist item IDs. playlist_id: Specifies the unique ID of the playlist for which you want to retrieve playlist items. max_results: The parameter specifies the maximum number of items that should be returned the result set. Acceptable values are 0 to 50, inclusive. The default value is 5. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. page_token: The parameter identifies a specific page in the result set that should be returned. video_id: Specifies that the request should return only the playlist items that contain the specified video. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Playlist items data. Raises: PyYouTubeException: Missing filter parameter. """ params = { "part": enf_parts(resource="playlistItems", value=parts), "maxResults": max_results, "onBehalfOfContentOwner": on_behalf_of_content_owner, "videoId": video_id, "pageToken": page_token, **kwargs, } if playlist_item_id is not None: params["id"] = enf_comma_separated( field="playlist_item_id", value=playlist_item_id ) elif playlist_id is not None: params["playlistId"] = playlist_id else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message=f"Specify at least one of playlist_item_id or playlist_id", ) ) response = self._client.request(path="playlistItems", params=params) data = self._client.parse_response(response=response) return data if return_json else PlaylistItemListResponse.from_dict(data) def insert( self, body: Union[dict, PlaylistItem], parts: Optional[Union[str, list, tuple, set]] = None, on_behalf_of_content_owner: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, PlaylistItem]: """Adds a resource to a playlist. Args: body: Provide playlist item data in the request body. You can give dataclass or just a dict with data. parts: Comma-separated list of one or more channel resource properties. Accepted values: id,contentDetails,snippet,snippet on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Playlist item data. """ params = { "part": enf_parts(resource="playlistItems", value=parts), "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="POST", path="playlistItems", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else PlaylistItem.from_dict(data) def update( self, body: Union[dict, PlaylistItem], parts: Optional[Union[str, list, tuple, set]] = None, on_behalf_of_content_owner: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, PlaylistItem]: """Modifies a playlist item. For example, you could update the item's position in the playlist. Args: body: Provide playlist item data in the request body. You can give dataclass or just a dict with data. parts: Comma-separated list of one or more channel resource properties. Accepted values: id,contentDetails,snippet,snippet on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Playlist item update data. """ params = { "part": enf_parts(resource="playlistItems", value=parts), "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="PUT", path="playlistItems", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else PlaylistItem.from_dict(data) def delete( self, playlist_item_id: str, on_behalf_of_content_owner: Optional[str] = None, **kwargs: Optional[dict], ) -> bool: """Deletes a playlist item. Args: playlist_item_id: Specifies the YouTube playlist item ID for the playlist item that is being deleted. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Playlist item delete status. """ params = { "id": playlist_item_id, "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="DELETE", path="playlistItems", params=params, ) if response.ok: return True self._client.parse_response(response=response) ================================================ FILE: pyyoutube/resources/playlists.py ================================================ """ Playlist resource implementation. """ from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorCode, ErrorMessage from pyyoutube.resources.base_resource import Resource from pyyoutube.models import Playlist, PlaylistListResponse from pyyoutube.utils.params_checker import enf_comma_separated, enf_parts class PlaylistsResource(Resource): """A playlist resource represents a YouTube playlist. References: https://developers.google.com/youtube/v3/docs/playlists """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, channel_id: Optional[str] = None, playlist_id: Optional[Union[str, list, tuple, set]] = None, mine: Optional[bool] = None, hl: Optional[str] = None, max_results: Optional[int] = None, on_behalf_of_content_owner: Optional[str] = None, on_behalf_of_content_owner_channel: Optional[str] = None, page_token: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, PlaylistListResponse]: """Returns a collection of playlists that match the API request parameters. Args: parts: Comma-separated list of one or more channel resource properties. Accepted values: id,contentDetails,localizations,player,snippet,status channel_id: Indicates that the API should only return the specified channel's playlists. playlist_id: Specifies a comma-separated list of the YouTube playlist ID(s) for the resource(s) that are being retrieved. mine: Set this parameter's value to true to instruct the API to only return playlists owned by the authenticated user. hl: The hl parameter instructs the API to retrieve localized resource metadata for a specific application language that the YouTube website supports. The parameter value must be a language code included in the list returned by the i18nLanguages.list method. max_results: The parameter specifies the maximum number of items that should be returned the result set. Acceptable values are 0 to 50, inclusive. The default value is 5. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. on_behalf_of_content_owner_channel: The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel to which a video is being added. This parameter is required when a request specifies a value for the onBehalfOfContentOwner parameter, and it can only be used in conjunction with that parameter. In addition, the request must be authorized using a CMS account that is linked to the content owner that the onBehalfOfContentOwner parameter specifies. Finally, the channel that the onBehalfOfContentOwnerChannel parameter value specifies must be linked to the content owner that the onBehalfOfContentOwner parameter specifies. page_token: The parameter identifies a specific page in the result set that should be returned. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Playlist data. Raises: PyYouTubeException: Missing filter parameter. """ params = { "part": enf_parts(resource="playlists", value=parts), "hl": hl, "maxResults": max_results, "onBehalfOfContentOwner": on_behalf_of_content_owner, "onBehalfOfContentOwnerChannel": on_behalf_of_content_owner_channel, "pageToken": page_token, **kwargs, } if channel_id is not None: params["channelId"] = channel_id elif playlist_id is not None: params["id"] = enf_comma_separated(field="playlist_id", value=playlist_id) elif mine is not None: params["mine"] = mine else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Specify at least one of channel_id, playlist_id or mine", ) ) response = self._client.request(path="playlists", params=params) data = self._client.parse_response(response=response) return data if return_json else PlaylistListResponse.from_dict(data) def insert( self, body: Union[dict, Playlist], parts: Optional[Union[str, list, tuple, set]] = None, on_behalf_of_content_owner: Optional[str] = None, on_behalf_of_content_owner_channel: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, Playlist]: """Creates a playlist. Args: body: Provide playlist data in the request body. You can give dataclass or just a dict with data. parts: The part parameter serves two purposes in this operation. It identifies the properties that the write operation will set as well as the properties that the API response will include. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. on_behalf_of_content_owner_channel: The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel to which a video is being added. This parameter is required when a request specifies a value for the onBehalfOfContentOwner parameter, and it can only be used in conjunction with that parameter. In addition, the request must be authorized using a CMS account that is linked to the content owner that the onBehalfOfContentOwner parameter specifies. Finally, the channel that the onBehalfOfContentOwnerChannel parameter value specifies must be linked to the content owner that the onBehalfOfContentOwner parameter specifies. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: playlist data. """ params = { "part": enf_parts(resource="playlists", value=parts), "onBehalfOfContentOwner": on_behalf_of_content_owner, "onBehalfOfContentOwnerChannel": on_behalf_of_content_owner_channel, **kwargs, } response = self._client.request( method="POST", path="playlists", params=params, json=body ) data = self._client.parse_response(response=response) return data if return_json else Playlist.from_dict(data) def update( self, body: Union[dict, Playlist], parts: Optional[Union[str, list, tuple, set]] = None, on_behalf_of_content_owner: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, Playlist]: """Modifies a playlist. Args: body: Provide playlist data in the request body. You can give dataclass or just a dict with data. parts: The part parameter serves two purposes in this operation. It identifies the properties that the write operation will set as well as the properties that the API response will include. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Playlist updated data. """ params = { "part": enf_parts(resource="playlists", value=parts), "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="PUT", path="playlists", params=params, json=body ) data = self._client.parse_response(response=response) return data if return_json else Playlist.from_dict(data) def delete( self, playlist_id: str, on_behalf_of_content_owner: Optional[str] = None, **kwargs: Optional[dict], ) -> bool: """Deletes a playlist. Args: playlist_id: Specifies the YouTube playlist ID for the playlist that is being deleted. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: playlist delete status """ params = { "id": playlist_id, "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="DELETE", path="playlists", params=params, ) if response.ok: return True self._client.parse_response(response=response) ================================================ FILE: pyyoutube/resources/search.py ================================================ """ Search resource implementation. """ from typing import Optional, Union from pyyoutube.resources.base_resource import Resource from pyyoutube.models import SearchListResponse from pyyoutube.utils.params_checker import enf_parts class SearchResource(Resource): """A search result contains information about a YouTube video, channel, or playlist that matches the search parameters specified in an API request References: https://developers.google.com/youtube/v3/docs/search """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, for_content_owner: Optional[bool] = None, for_developer: Optional[bool] = None, for_mine: Optional[bool] = None, related_to_video_id: Optional[str] = None, channel_id: Optional[str] = None, channel_type: Optional[str] = None, event_type: Optional[str] = None, location: Optional[str] = None, location_radius: Optional[str] = None, max_results: Optional[int] = None, on_behalf_of_content_owner: Optional[str] = None, order: Optional[str] = None, page_token: Optional[str] = None, published_after: Optional[str] = None, published_before: Optional[str] = None, q: Optional[str] = None, region_code: Optional[str] = None, relevance_language: Optional[str] = None, safe_search: Optional[str] = None, topic_id: Optional[str] = None, type: Optional[Union[str, list, tuple, set]] = None, video_caption: Optional[str] = None, video_category_id: Optional[str] = None, video_definition: Optional[str] = None, video_dimension: Optional[str] = None, video_duration: Optional[str] = None, video_embeddable: Optional[str] = None, video_license: Optional[str] = None, video_paid_product_placement: Optional[str] = None, video_syndicated: Optional[str] = None, video_type: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, SearchListResponse]: """Returns a collection of search results that match the query parameters specified in the API request. Notes: Search API is very complex. If you want to search, You may need to read the parameter description with the docs: https://developers.google.com/youtube/v3/docs/search/list#parameters Args: parts: Comma-separated list of one or more channel resource properties. Accepted values: snippet for_content_owner: Parameter restricts the search to only retrieve videos owned by the content owner identified by the onBehalfOfContentOwner parameter. for_developer: Parameter restricts the search to only retrieve videos uploaded via the developer's application or website. for_mine: Parameter restricts the search to only retrieve videos owned by the authenticated user. related_to_video_id: Parameter retrieves a list of videos that are related to the video that the parameter value identifies. Deprecated at [2023.08.07](https://developers.google.com/youtube/v3/revision_history#august-7,-2023) channel_id: Indicates that the API response should only contain resources created by the channel. channel_type: Parameter lets you restrict a search to a particular type of channel. Acceptable values are: - any: Return all channels. - show: Only retrieve shows. event_type: Parameter restricts a search to broadcast events. Acceptable values are: - completed: Only include completed broadcasts. - live: Only include active broadcasts. - upcoming: Only include upcoming broadcasts. location: Parameter value identifies the point at the center of the area. location_radius: Specifies the maximum distance that the location associated with a video can be from that point for the video to still be included in the search results. max_results: The parameter specifies the maximum number of items that should be returned the result set. Acceptable values are 0 to 50, inclusive. The default value is 5. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. order: Specifies the method that will be used to order resources in the API response. The default value is relevance. Acceptable values are: - date: Resources are sorted in reverse chronological order based on the date they were created. - rating: Resources are sorted from highest to lowest rating. - relevance: Resources are sorted based on their relevance to the search query. - title: Resources are sorted alphabetically by title. - videoCount: Channels are sorted in descending order of their number of uploaded videos. - viewCount: Resources are sorted from highest to lowest number of views. For live broadcasts, videos are sorted by number of concurrent viewers while the broadcasts are ongoing. page_token: The parameter identifies a specific page in the result set that should be returned. published_after: Indicates that the API response should only contain resources created at or after the specified time. published_before: Indicates that the API response should only contain resources created before or at the specified time. q: Specifies the query term to search for. region_code: Instructs the API to return search results for videos that can be viewed in the specified country. relevance_language: Instructs the API to return search results that are most relevant to the specified language. safe_search: Indicates whether the search results should include restricted content as well as standard content. Acceptable values are: - moderate: YouTube will filter some content from search results and, at the least, will filter content that is restricted in your locale. Based on their content, search results could be removed from search results or demoted in search results. This is the default parameter value. - none: YouTube will not filter the search result set. - strict: YouTube will try to exclude all restricted content from the search result set. Based on their content, search results could be removed from search results or demoted in search results. topic_id: Indicates that the API response should only contain resources associated with the specified topic. type: Parameter restricts a search query to only retrieve a particular type of resource. The value is a comma-separated list of resource types. Acceptable values are: channel,playlist,video video_caption: Indicates whether the API should filter video search results based on whether they have captions. Acceptable values are: - any: Do not filter results based on caption availability. - closedCaption: Only include videos that have captions. - none: Only include videos that do not have captions. video_category_id: Parameter filters video search results based on their category. video_definition: Parameter lets you restrict a search to only include either high definition (HD) or standard definition (SD) videos. Acceptable values are: - any: Return all videos, regardless of their resolution. - high: Only retrieve HD videos. - standard: Only retrieve videos in standard definition. video_dimension: Parameter lets you restrict a search to only retrieve 2D or 3D videos. Acceptable values are: - 2d: Restrict search results to exclude 3D videos. - 3d: Restrict search results to only include 3D videos. - any: Include both 3D and non-3D videos in returned results. This is the default value. video_duration: Parameter filters video search results based on their duration. Acceptable values are: - any: Do not filter video search results based on their duration. This is the default value. - long: Only include videos longer than 20 minutes. - medium: Only include videos that are between four and 20 minutes long (inclusive). - short: Only include videos that are less than four minutes long. video_embeddable: Parameter lets you to restrict a search to only videos that can be embedded into a webpage. Acceptable values are: - any: Return all videos, embeddable or not. - true: Only retrieve embeddable videos. video_license: Parameter filters search results to only include videos with a particular license. Acceptable values are: - any – Return all videos, regardless of which license they have, that match the query parameters. - creativeCommon – Only return videos that have a Creative Commons license. Users can reuse videos with this license in other videos that they create. Learn more. - youtube – Only return videos that have the standard YouTube license. video_paid_product_placement: Parameter filters search results to only include videos that the creator has denoted as having a paid promotion. Acceptable values are: - any – Return all videos, regardless of whether they contain paid promotions. - true – Only retrieve videos with paid promotions. video_syndicated: Parameter lets you to restrict a search to only videos that can be played outside youtube.com. Acceptable values are: - any: Return all videos, syndicated or not. - true: Only retrieve syndicated videos. video_type: Parameter lets you restrict a search to a particular type of videos. Acceptable values are: - any: Return all videos. - episode: Only retrieve episodes of shows. - movie: Only retrieve movies. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Search result data """ params = { "part": enf_parts(resource="search", value=parts), "channelId": channel_id, "channelType": channel_type, "eventType": event_type, "location": location, "locationRadius": location_radius, "maxResults": max_results, "onBehalfOfContentOwner": on_behalf_of_content_owner, "order": order, "pageToken": page_token, "publishedAfter": published_after, "publishedBefore": published_before, "q": q, "regionCode": region_code, "relevanceLanguage": relevance_language, "safeSearch": safe_search, "topicId": topic_id, "type": type, "videoCaption": video_caption, "videoCategoryId": video_category_id, "videoDefinition": video_definition, "videoDimension": video_dimension, "videoDuration": video_duration, "videoEmbeddable": video_embeddable, "videoLicense": video_license, "videoPaidProductPlacement": video_paid_product_placement, "videoSyndicated": video_syndicated, "videoType": video_type, **kwargs, } if for_content_owner is not None: params["forContentOwner"] = for_content_owner elif for_developer is not None: params["forDeveloper"] = for_developer elif for_mine is not None: params["forMine"] = for_mine elif related_to_video_id is not None: params["relatedToVideoId"] = related_to_video_id response = self._client.request(path="search", params=params) data = self._client.parse_response(response=response) return data if return_json else SearchListResponse.from_dict(data) ================================================ FILE: pyyoutube/resources/subscriptions.py ================================================ """ Subscription resource implementation. """ from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorCode, ErrorMessage from pyyoutube.resources.base_resource import Resource from pyyoutube.models import Subscription, SubscriptionListResponse from pyyoutube.utils.params_checker import enf_parts, enf_comma_separated class SubscriptionsResource(Resource): """A subscription resource contains information about a YouTube user subscription. References: https://developers.google.com/youtube/v3/docs/subscriptions """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, channel_id: Optional[str] = None, subscription_id: Optional[Union[str, list, tuple, set]] = None, mine: Optional[bool] = None, my_recent_subscribers: Optional[bool] = None, my_subscribers: Optional[bool] = None, for_channel_id: Optional[Union[str, list, tuple, set]] = None, max_results: Optional[int] = None, on_behalf_of_content_owner: Optional[str] = None, on_behalf_of_content_owner_channel: Optional[str] = None, order: Optional[str] = None, page_token: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, SubscriptionListResponse]: """Returns subscription resources that match the API request criteria. Args: parts: Comma-separated list of one or more channel resource properties. Accepted values: id,contentDetails,snippet,subscriberSnippet channel_id: Specifies a YouTube channel ID. The API will only return that channel's subscriptions. subscription_id: Specifies a comma-separated list of the YouTube subscription ID(s) for the resource(s) that are being retrieved. mine: Set this parameter's value to true to retrieve a feed of the authenticated user's subscriptions. my_recent_subscribers: Set this parameter's value to true to retrieve a feed of the subscribers of the authenticated user in reverse chronological order (the newest first). my_subscribers: Set this parameter's value to true to retrieve a feed of the subscribers of the authenticated user in no particular order. for_channel_id: Specifies a comma-separated list of channel IDs. The API response will then only contain subscriptions matching those channels. max_results: The parameter specifies the maximum number of items that should be returned the result set. Acceptable values are 0 to 50, inclusive. The default value is 5. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. on_behalf_of_content_owner_channel: The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel to which a video is being added. This parameter is required when a request specifies a value for the onBehalfOfContentOwner parameter, and it can only be used in conjunction with that parameter. In addition, the request must be authorized using a CMS account that is linked to the content owner that the onBehalfOfContentOwner parameter specifies. Finally, the channel that the onBehalfOfContentOwnerChannel parameter value specifies must be linked to the content owner that the onBehalfOfContentOwner parameter specifies. order: Specifies the method that will be used to sort resources in the API response. Acceptable values are: - alphabetical: Sort alphabetically. - relevance: Sort by relevance. Default. - unread: Sort by order of activity. page_token: The parameter identifies a specific page in the result set that should be returned. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Subscriptions data. Raises: PyYouTubeException: Missing filter parameter. """ params = { "part": enf_parts(resource="subscriptions", value=parts), "forChannelId": enf_comma_separated( field="for_channel_id", value=for_channel_id ), "maxResults": max_results, "onBehalfOfContentOwner": on_behalf_of_content_owner, "onBehalfOfContentOwnerChannel": on_behalf_of_content_owner_channel, "order": order, "pageToken": page_token, **kwargs, } if channel_id is not None: params["channelId"] = channel_id elif subscription_id is not None: params["id"] = enf_comma_separated( field="subscription_id", value=subscription_id ) elif mine is not None: params["mine"] = mine elif my_recent_subscribers is not None: params["myRecentSubscribers"] = my_recent_subscribers elif my_subscribers is not None: params["mySubscribers"] = my_subscribers else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message=f"Specify at least one of channel_id,subscription_id,mine,my_recent_subscribers or mySubscribers", ) ) response = self._client.request(path="subscriptions", params=params) data = self._client.parse_response(response=response) return data if return_json else SubscriptionListResponse.from_dict(data) def insert( self, body: Union[dict, Subscription], parts: Optional[Union[str, list, tuple, set]] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, Subscription]: """Adds a subscription for the authenticated user's channel. Args: body: Provide subscription data in the request body. You can give dataclass or just a dict with data. parts: The part parameter serves two purposes in this operation. It identifies the properties that the write operation will set as well as the properties that the API response will include. Accepted values: id,contentDetails,snippet,subscriberSnippet return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Subscription data """ params = { "part": enf_parts(resource="subscriptions", value=parts), **kwargs, } response = self._client.request( method="POST", path="subscriptions", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else Subscription.from_dict(data) def delete( self, subscription_id: str, **kwargs: Optional[dict], ) -> bool: """Deletes a subscription. Args: subscription_id: Specifies the YouTube subscription ID for the resource that is being deleted. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Subscription delete status. """ params = { "id": subscription_id, **kwargs, } response = self._client.request( method="DELETE", path="subscriptions", params=params ) if response.ok: return True self._client.parse_response(response=response) ================================================ FILE: pyyoutube/resources/thumbnails.py ================================================ """ Thumbnails resources implementation. """ from typing import Optional from pyyoutube.resources.base_resource import Resource from pyyoutube.media import Media, MediaUpload class ThumbnailsResource(Resource): """A thumbnail resource identifies different thumbnail image sizes associated with a resource. References: https://developers.google.com/youtube/v3/docs/thumbnails """ def set( self, video_id: str, media: Media, on_behalf_of_content_owner: Optional[str] = None, **kwargs: Optional[dict], ) -> MediaUpload: params = { "videoId": video_id, "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } # Build a media upload instance. media_upload = MediaUpload( client=self._client, resource="thumbnails/set", media=media, params=params, ) return media_upload ================================================ FILE: pyyoutube/resources/video_abuse_report_reasons.py ================================================ """ Video abuse report reasons resource implementation. """ from typing import Optional, Union from pyyoutube.resources.base_resource import Resource from pyyoutube.models import VideoAbuseReportReasonListResponse from pyyoutube.utils.params_checker import enf_parts class VideoAbuseReportReasonsResource(Resource): """A videoAbuseReportReason resource contains information about a reason that a video would be flagged for containing abusive content. References: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, hl: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, VideoAbuseReportReasonListResponse]: """Retrieve a list of reasons that can be used to report abusive videos. Args: parts: Comma-separated list of one or more channel resource properties. Accepted values: id,snippet hl: Specifies the language that should be used for text values in the API response. The default value is en_US. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: reasons data. """ params = { "part": enf_parts(resource="videoAbuseReportReasons", value=parts), "hl": hl, **kwargs, } response = self._client.request(path="videoAbuseReportReasons", params=params) data = self._client.parse_response(response=response) return ( data if return_json else VideoAbuseReportReasonListResponse.from_dict(data) ) ================================================ FILE: pyyoutube/resources/video_categories.py ================================================ """ Video categories resource implementation. """ from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode from pyyoutube.resources.base_resource import Resource from pyyoutube.models import VideoCategoryListResponse from pyyoutube.utils.params_checker import enf_comma_separated, enf_parts class VideoCategoriesResource(Resource): """A videoCategory resource identifies a category that has been or could be associated with uploaded videos. References: https://developers.google.com/youtube/v3/docs/videoCategories """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, category_id: Optional[Union[str, list, tuple, set]] = None, region_code: Optional[str] = None, hl: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, VideoCategoryListResponse]: """Returns a list of categories that can be associated with YouTube videos. Args: parts: Comma-separated list of one or more video category resource properties. Accepted values: snippet category_id: Specifies a comma-separated list of video category IDs for the resources that you are retrieving. region_code: Instructs the API to return the list of video categories available in the specified country. The parameter value is an ISO 3166-1 alpha-2 country code. hl: Specifies the language that should be used for text values in the API response. The default value is en_US. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Video category data. Raises: PyYouTubeException: Missing filter parameter. """ params = { "part": enf_parts(resource="videoCategories", value=parts), "hl": hl, **kwargs, } if category_id is not None: params["id"] = enf_comma_separated(field="category_id", value=category_id) elif region_code is not None: params["regionCode"] = region_code else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message=f"Specify at least one of category_id or region_code", ) ) response = self._client.request(path="videoCategories", params=params) data = self._client.parse_response(response=response) return data if return_json else VideoCategoryListResponse.from_dict(data) ================================================ FILE: pyyoutube/resources/videos.py ================================================ """ Videos resource implementation. """ from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorCode, ErrorMessage from pyyoutube.resources.base_resource import Resource from pyyoutube.media import Media, MediaUpload from pyyoutube.models import ( Video, VideoListResponse, VideoGetRatingResponse, VideoReportAbuse, ) from pyyoutube.utils.params_checker import enf_comma_separated, enf_parts class VideosResource(Resource): """A video resource represents a YouTube video. References: https://developers.google.com/youtube/v3/docs/videos """ def list( self, parts: Optional[Union[str, list, tuple, set]] = None, chart: Optional[str] = None, video_id: Optional[Union[str, list, tuple, set]] = None, my_rating: Optional[str] = None, hl: Optional[str] = None, max_height: Optional[int] = None, max_results: Optional[int] = None, max_width: Optional[int] = None, on_behalf_of_content_owner: Optional[str] = None, page_token: Optional[str] = None, region_code: Optional[str] = None, video_category_id: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, VideoListResponse]: """Returns a list of videos that match the API request parameters. Args: parts: Comma-separated list of one or more channel resource properties. Accepted values: id,contentDetails,fileDetails,liveStreamingDetails, localizations,paidProductPlacementDetails,player,processingDetails,recordingDetails,snippet,statistics, status,suggestions,topicDetails chart: Identifies the chart that you want to retrieve. Acceptable values are: - mostPopular: Return the most popular videos for the specified content region and video category. video_id: Specifies a comma-separated list of the YouTube video ID(s) for the resource(s) that are being retrieved. my_rating: Set this parameter's value to like or dislike to instruct the API to only return videos liked or disliked by the authenticated user. Acceptable values are: - dislike: Returns only videos disliked by the authenticated user. - like: Returns only video liked by the authenticated user. hl: Instructs the API to retrieve localized resource metadata for a specific application language that the YouTube website supports. max_height: Specifies the maximum height of the embedded player returned the player.embedHtml property. max_results: Specifies the maximum number of items that should be returned the result set. max_width: Specifies the maximum width of the embedded player returned the player.embedHtml property. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. page_token: The parameter identifies a specific page in the result set that should be returned. region_code: Instructs the API to select a video chart available in the specified region. video_category_id: Identifies the video category for which the chart should be retrieved. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Videos data. Raises: PyYouTubeException: Missing filter parameter. """ params = { "part": enf_parts(resource="videos", value=parts), "hl": hl, "maxHeight": max_height, "maxResults": max_results, "maxWidth": max_width, "onBehalfOfContentOwner": on_behalf_of_content_owner, "pageToken": page_token, "regionCode": region_code, "videoCategoryId": video_category_id, **kwargs, } if chart is not None: params["chart"] = chart elif video_id is not None: params["id"] = enf_comma_separated(field="video_id", value=video_id) elif my_rating is not None: params["myRating"] = my_rating else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.MISSING_PARAMS, message="Specify at least one of chart,video_id or my_rating", ) ) response = self._client.request(path="videos", params=params) data = self._client.parse_response(response=response) return data if return_json else VideoListResponse.from_dict(data) def insert( self, body: Union[dict, Video], media: Media, parts: Optional[Union[str, list, tuple, set]] = None, notify_subscribers: Optional[bool] = None, on_behalf_of_content_owner: Optional[str] = None, on_behalf_of_content_owner_channel: Optional[str] = None, **kwargs, ) -> MediaUpload: """Uploads a video to YouTube and optionally sets the video's metadata. Example: import pyyoutube.models as mds from pyyoutube.media import Media body = mds.Video( snippet=mds.VideoSnippet( title="video title", description="video description" ) ) media = Media(filename="video.mp4") upload = client.videos.insert( body=body, media=media, parts=["snippet"], ) response = None while response is None: status, response = upload.next_chunk() if status: print(f"Upload {int(status.progress() * 100)} complete.") print(f"Response body: {response}") Args: body: Provide video data in the request body. You can give dataclass or just a dict with data. media: Media data to upload. parts: Comma-separated list of one or more channel resource properties. Accepted values: id,contentDetails,fileDetails,liveStreamingDetails, localizations,player,processingDetails,recordingDetails,snippet,statistics, status,suggestions,topicDetails notify_subscribers: Indicates whether YouTube should send a notification about the new video to users who subscribe to the video's channel on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many different YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. on_behalf_of_content_owner_channel: The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel to which a video is being added. This parameter is required when a request specifies a value for the onBehalfOfContentOwner parameter, and it can only be used in conjunction with that parameter. In addition, the request must be authorized using a CMS account that is linked to the content owner that the onBehalfOfContentOwner parameter specifies. Finally, the channel that the onBehalfOfContentOwnerChannel parameter value specifies must be linked to the content owner that the onBehalfOfContentOwner parameter specifies. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Video data. """ params = { "part": enf_parts(resource="videos", value=parts), "notifySubscribers": notify_subscribers, "onBehalfOfContentOwner": on_behalf_of_content_owner, "onBehalfOfContentOwnerChannel": on_behalf_of_content_owner_channel, **kwargs, } # Build a media upload instance. media_upload = MediaUpload( client=self._client, resource="videos", media=media, params=params, body=body.to_dict_ignore_none(), ) return media_upload def update( self, body: Union[dict, Video], parts: Optional[Union[str, list, tuple, set]] = None, on_behalf_of_content_owner: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, Video]: """Updates a video's metadata. Args: body: Provide video data in the request body. You can give dataclass or just a dict with data. parts: Comma-separated list of one or more channel resource properties. Accepted values: id,contentDetails,fileDetails,liveStreamingDetails, localizations,player,processingDetails,recordingDetails,snippet,statistics, status,suggestions,topicDetails on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Video updated data. """ params = { "part": enf_parts(resource="videos", value=parts), "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="PUT", path="videos", params=params, json=body, ) data = self._client.parse_response(response=response) return data if return_json else Video.from_dict(data) def rate( self, video_id: str, rating: Optional[str] = None, **kwargs: Optional[dict], ) -> bool: """Add a like or dislike rating to a video or remove a rating from a video. Args: video_id: Specifies the YouTube video ID of the video that is being rated or having its rating removed. rating: Specifies the rating to record. Acceptable values are: - dislike: Records that the authenticated user disliked the video. - like: Records that the authenticated user liked the video. - none: Removes any rating that the authenticated user had previously set for the video. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Video rating status """ params = { "id": video_id, "rating": rating, **kwargs, } response = self._client.request( method="POST", path="videos/rate", params=params, ) if response.ok: return True self._client.parse_response(response=response) def get_rating( self, video_id: Union[str, list, tuple, set], on_behalf_of_content_owner: Optional[str] = None, return_json: bool = False, **kwargs: Optional[dict], ) -> Union[dict, VideoGetRatingResponse]: """Retrieves the ratings that the authorized user gave to a list of specified videos. Args: video_id: Specifies a comma-separated list of the YouTube video ID(s). on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many different YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. return_json: Type for returned data. If you set True JSON data will be returned. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Video rating data. """ params = { "id": enf_comma_separated(field="video_id", value=video_id), "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request(path="videos/getRating", params=params) data = self._client.parse_response(response=response) return data if return_json else VideoGetRatingResponse.from_dict(data) def report_abuse( self, body: Optional[Union[dict, VideoReportAbuse]], on_behalf_of_content_owner: Optional[str] = None, **kwargs: Optional[dict], ) -> bool: """Reports a video for containing abusive content. Args: body: Provide report abuse data in the request body. You can give dataclass or just a dict with data. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many different YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: report status. """ params = { "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="POST", path="videos/reportAbuse", params=params, json=body, ) if response.ok: return True self._client.parse_response(response=response) def delete( self, video_id: str, on_behalf_of_content_owner: Optional[str] = None, **kwargs: Optional[dict], ) -> bool: """Deletes a YouTube video. Args: video_id: Specifies the YouTube video ID for the resource that is being deleted. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many different YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: video delete status. """ params = { "id": video_id, "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="DELETE", path="videos", params=params, ) if response.ok: return True self._client.parse_response(response=response) ================================================ FILE: pyyoutube/resources/watermarks.py ================================================ """ Watermarks resource implementation. """ from typing import Optional, Union from pyyoutube.resources.base_resource import Resource from pyyoutube.media import Media, MediaUpload from pyyoutube.models import Watermark class WatermarksResource(Resource): def set( self, channel_id: str, body: Union[dict, Watermark], media: Media, on_behalf_of_content_owner: Optional[str] = None, **kwargs: Optional[dict], ) -> MediaUpload: """ Args: channel_id: Specifies the YouTube channel ID for which the watermark is being provided. body: Provide watermark data in the request body. You can give dataclass or just a dict with data. media: Media for watermark image. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: Watermark set status. """ params = { "channel_id": channel_id, "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } # Build a media upload instance. media_upload = MediaUpload( client=self._client, resource="watermarks/set", media=media, params=params, body=body.to_dict_ignore_none(), ) return media_upload def unset( self, channel_id: str, on_behalf_of_content_owner: Optional[str] = None, **kwargs: Optional[dict], ) -> bool: """Deletes a channel's watermark image. Args: channel_id: Specifies the YouTube channel ID for which the watermark is being unset. on_behalf_of_content_owner: The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value. This parameter is intended for YouTube content partners that own and manage many difference YouTube channels. It allows content owners to authenticate once and get access to all their video and channel data, without having to provide authentication credentials for each individual channel. The CMS account that the user authenticates with must be linked to the specified YouTube content owner. **kwargs: Additional parameters for system parameters. Refer: https://cloud.google.com/apis/docs/system-parameters. Returns: watermark unset status. """ params = { "channelId": channel_id, "onBehalfOfContentOwner": on_behalf_of_content_owner, **kwargs, } response = self._client.request( method="POST", path="watermarks/unset", params=params, ) if response.ok: return True self._client.parse_response(response=response) ================================================ FILE: pyyoutube/utils/__init__.py ================================================ ================================================ FILE: pyyoutube/utils/constants.py ================================================ """ some constants for YouTube """ ACTIVITIES_RESOURCE_PROPERTIES = {"id", "snippet", "contentDetails"} CAPTIONS_RESOURCE_PROPERTIES = {"id", "snippet"} CHANNEL_RESOURCE_PROPERTIES = { "id", "brandingSettings", "contentDetails", "localizations", "snippet", "statistics", "status", "topicDetails", } CHANNEL_SECTIONS_PROPERTIES = {"id", "contentDetails", "snippet"} COMMENT_RESOURCE_PROPERTIES = {"id", "snippet"} COMMENT_THREAD_RESOURCE_PROPERTIES = {"id", "replies", "snippet"} I18N_LANGUAGE_PROPERTIES = {"snippet"} I18N_REGION_PROPERTIES = {"snippet"} MEMBER_PROPERTIES = {"snippet"} MEMBERSHIP_LEVEL_PROPERTIES = {"id", "snippet"} PLAYLIST_ITEM_RESOURCE_PROPERTIES = {"id", "contentDetails", "snippet", "status"} PLAYLIST_RESOURCE_PROPERTIES = { "id", "contentDetails", "localizations", "player", "snippet", "status", } SEARCH_RESOURCE_PROPERTIES = {"snippet"} SUBSCRIPTION_RESOURCE_PROPERTIES = { "id", "snippet", "contentDetails", "subscriberSnippet", } VIDEO_ABUSE_REPORT_REASON_PROPERTIES = {"id", "snippet"} VIDEO_CATEGORY_RESOURCE_PROPERTIES = {"snippet"} VIDEO_RESOURCE_PROPERTIES = { "id", "contentDetails", "player", "snippet", "statistics", "status", "topicDetails", "recordingDetails", "liveStreamingDetails", "paidProductPlacementDetails", } GUIDE_CATEGORY_RESOURCE_PROPERTIES = {"id", "snippet"} RESOURCE_PARTS_MAPPING = { "activities": ACTIVITIES_RESOURCE_PROPERTIES, "captions": CAPTIONS_RESOURCE_PROPERTIES, "channels": CHANNEL_RESOURCE_PROPERTIES, "channelSections": CHANNEL_SECTIONS_PROPERTIES, "comments": COMMENT_RESOURCE_PROPERTIES, "commentThreads": COMMENT_THREAD_RESOURCE_PROPERTIES, "i18nLanguages": I18N_LANGUAGE_PROPERTIES, "i18nRegions": I18N_REGION_PROPERTIES, "members": MEMBER_PROPERTIES, "membershipsLevels": MEMBERSHIP_LEVEL_PROPERTIES, "playlistItems": PLAYLIST_ITEM_RESOURCE_PROPERTIES, "playlists": PLAYLIST_RESOURCE_PROPERTIES, "search": SEARCH_RESOURCE_PROPERTIES, "subscriptions": SUBSCRIPTION_RESOURCE_PROPERTIES, "videoAbuseReportReasons": VIDEO_ABUSE_REPORT_REASON_PROPERTIES, "videoCategories": VIDEO_CATEGORY_RESOURCE_PROPERTIES, "videos": VIDEO_RESOURCE_PROPERTIES, "guideCategories": GUIDE_CATEGORY_RESOURCE_PROPERTIES, } TOPICS = { # Music topics "/m/04rlf": "Music (parent topic)", "/m/02mscn": "Christian music", "/m/0ggq0m": "Classical music", "/m/01lyv": "Country", "/m/02lkt": "Electronic music", "/m/0glt670": "Hip hop music", "/m/05rwpb": "Independent music", "/m/03_d0": "Jazz", "/m/028sqc": "Music of Asia", "/m/0g293": "Music of Latin America", "/m/064t9": "Pop music", "/m/06cqb": "Reggae", "/m/06j6l": "Rhythm and blues", "/m/06by7": "Rock music", "/m/0gywn": "Soul music", # Gaming topics "/m/0bzvm2": "Gaming (parent topic)", "/m/025zzc": "Action game", "/m/02ntfj": "Action-adventure game", "/m/0b1vjn": "Casual game", "/m/02hygl": "Music video game", "/m/04q1x3q": "Puzzle video game", "/m/01sjng": "Racing video game", "/m/0403l3g": "Role-playing video game", "/m/021bp2": "Simulation video game", "/m/022dc6": "Sports game", "/m/03hf_rm": "Strategy video game", # Sports topics "/m/06ntj": "Sports (parent topic)", "/m/0jm_": "American football", "/m/018jz": "Baseball", "/m/018w8": "Basketball", "/m/01cgz": "Boxing", "/m/09xp_": "Cricket", "/m/02vx4": "Football", "/m/037hz": "Golf", "/m/03tmr": "Ice hockey", "/m/01h7lh": "Mixed martial arts", "/m/0410tth": "Motorsport", "/m/07bs0": "Tennis", "/m/07_53": "Volleyball", # Entertainment topics "/m/02jjt": "Entertainment (parent topic)", "/m/09kqc": "Humor", "/m/02vxn": "Movies", "/m/05qjc": "Performing arts", "/m/066wd": "Professional wrestling", "/m/0f2f9": "TV shows", # Lifestyle topics "/m/019_rr": "Lifestyle (parent topic)", "/m/032tl": "Fashion", "/m/027x7n": "Fitness", "/m/02wbm": "Food", "/m/03glg": "Hobby", "/m/068hy": "Pets", "/m/041xxh": "Physical attractiveness [Beauty]", "/m/07c1v": "Technology", "/m/07bxq": "Tourism", "/m/07yv9": "Vehicles", # Society topics "/m/098wr": "Society (parent topic)", "/m/09s1f": "Business", "/m/0kt51": "Health", "/m/01h6rj": "Military", "/m/05qt0": "Politics", "/m/06bvp": "Religion", # Other topics "/m/01k8wb": "Knowledge", } ================================================ FILE: pyyoutube/utils/params_checker.py ================================================ """ function's params checker. """ import logging from typing import Optional, Union from pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException from pyyoutube.utils.constants import RESOURCE_PARTS_MAPPING logger = logging.getLogger(__name__) def enf_comma_separated( field: str, value: Optional[Union[str, list, tuple, set]], ): """ Check to see if field's value type belong to correct type. If it is, return api need value, otherwise, raise a PyYouTubeException. Args: field (str): Name of the field you want to do check. value (str, list, tuple, set, Optional) Value for the field. Returns: Api needed string """ if value is None: return None try: if isinstance(value, str): return value elif isinstance(value, (list, tuple, set)): if isinstance(value, set): logging.warning(f"Note: The order of the set is unreliable.") return ",".join(value) else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message=f"Parameter ({field}) must be single str,comma-separated str,list,tuple or set", ) ) except (TypeError, ValueError): raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message=f"Parameter ({field}) must be single str,comma-separated str,list,tuple or set", ) ) def enf_parts(resource: str, value: Optional[Union[str, list, tuple, set]], check=True): """ Check to see if value type belong to correct type, and if resource support the given part. If it is, return api need value, otherwise, raise a PyYouTubeException. Args: resource (str): Name of the resource you want to retrieve. value (str, list, tuple, set, Optional): Value for the part. check (bool, optional): Whether check the resource properties. Returns: Api needed part string """ if value is None: parts = RESOURCE_PARTS_MAPPING[resource] elif isinstance(value, str): parts = set(value.split(",")) elif isinstance(value, (list, tuple, set)): parts = set(value) else: raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message=f"Parameter (parts) must be single str,comma-separated str,list,tuple or set", ) ) # Remove leading/trailing whitespaces parts = set({part.strip() for part in parts}) # check parts whether support. if check: support_parts = RESOURCE_PARTS_MAPPING[resource] if not support_parts.issuperset(parts): not_support_parts = ",".join(parts.difference(support_parts)) raise PyYouTubeException( ErrorMessage( status_code=ErrorCode.INVALID_PARAMS, message=f"Parts {not_support_parts} for resource {resource} not support", ) ) return ",".join(parts) ================================================ FILE: pyyoutube/youtube_utils.py ================================================ """ This provide some common utils methods for YouTube resource. """ import isodate from isodate.isoerror import ISO8601Error from pyyoutube.error import ErrorMessage, PyYouTubeException def get_video_duration(duration: str) -> int: """ Parse video ISO 8601 duration to seconds. Refer: https://developers.google.com/youtube/v3/docs/videos#contentDetails.duration Args: duration(str) Videos ISO 8601 duration. Like: PT14H23M42S Returns: integer for seconds. """ try: seconds = isodate.parse_duration(duration).total_seconds() return int(seconds) except ISO8601Error as e: raise PyYouTubeException( ErrorMessage( status_code=10001, message=f"Exception in convert video duration: {duration}. errors: {e}", ) ) ================================================ FILE: testdata/apidata/abuse_reasons/abuse_reason.json ================================================ { "kind": "youtube#videoAbuseReportReasonListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/YH398HlGf_qbYlJQUZVMRoL4RTE\"", "items": [ { "kind": "youtube#videoAbuseReportReason", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/_WIvuNJwlISvQNQt_ukh2m0kt2Y\"", "id": "N", "snippet": { "label": "Sex or nudity", "secondaryReasons": [ { "id": "32", "label": "Graphic sex or nudity" }, { "id": "33", "label": "Content involving minors" }, { "id": "34", "label": "Other sexual content" } ] } }, { "kind": "youtube#videoAbuseReportReason", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/9uBFtSRN_-W5oQDz_AhiTSN5sTE\"", "id": "S", "snippet": { "label": "Spam or misleading", "secondaryReasons": [ { "id": "27", "label": "Spam or mass advertising" }, { "id": "28", "label": "Misleading thumbnail" }, { "id": "29", "label": "Malware or phishing" }, { "id": "30", "label": "Pharmaceutical drugs for sale" }, { "id": "31", "label": "Other misleading info" } ] } }, { "kind": "youtube#videoAbuseReportReason", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/e6pyrZ9LzezCkkpfXAc0gkDdQ0Q\"", "id": "V", "snippet": { "label": "Violent, hateful, or dangerous", "secondaryReasons": [ { "id": "35", "label": "Promotes violence or hatred" }, { "id": "36", "label": "Promotes terrorism" }, { "id": "37", "label": "Bullying or abusing vulnerable individuals" }, { "id": "38", "label": "Suicide or self-injury" }, { "id": "39", "label": "Pharmaceutical or drug abuse" }, { "id": "40", "label": "Other violent, hateful, or dangerous acts" } ] } } ] } ================================================ FILE: testdata/apidata/access_token.json ================================================ {"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} ================================================ FILE: testdata/apidata/activities/activities_by_channel_p1.json ================================================ { "kind": "youtube#activityListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/wgXTq3L7ZY7nAhRFHl3U-cfbtIM\"", "nextPageToken": "CAoQAA", "pageInfo": { "totalResults": 13, "resultsPerPage": 10 }, "items": [ { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/Nx8UQRPToT_leC4ECgznp5ynzlw\"", "id": "MTUxNTcyNTU1NjEyNjk5ODAxNzY0MzM4MDg=", "snippet": { "publishedAt": "2019-10-31T21:00:12.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Android Developer Challenge, Google Maps Platform, New in Chrome 78", "description": "TL;DR 180 | The Google Developer News Show\n \nAnnouncements from the Android Developer Summit → https://goo.gle/2N2482c\n\nDev Show Top 5 from the Android Dev Summit 2019 → https://goo.gle/2JElnV9\n\nDev Challenge Announcement → https://goo.gle/2C3Etje\n\nGoogle Maps Platform YouTube Channel → https://goo.gle/320STez\n\nNew in Chrome 78 → https://goo.gle/34jIImW\n\nCloud AI Platform updates → https://goo.gle/2q5ytUL\n\nHere to bring you the latest developer news from across Google is Developer Advocate Todd Kerpelman.\n\nTune in every week for a new episode, and let us know what you think of the latest announcements in the comments below! 😃\n\nWatch more #DevShow → https://goo.gle/GDevShow\nSubscribe to Google Developers! → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Du7E0okmNlk/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Du7E0okmNlk/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Du7E0okmNlk/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/Du7E0okmNlk/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/Du7E0okmNlk/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/E5fFu6HZXDfgKmndxIQtR6b9U48\"", "id": "MTUxNTcyNDUyMjczNjk5ODAxNzY0MzY3NTI=", "snippet": { "publishedAt": "2019-10-30T16:17:53.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Developer Student Clubs 2019 Paris Leads Summit", "description": "Developer Student Club Leads are passionate leaders at their university who are dedicated to helping their peers learn and connect. Student leads across Europe gather as they kick off their academic year managing the clubs at their universities. \n\nDSC Naming Guidelines → https://goo.gle/2q8QHEx\nLearn more at → https://goo.gle/2MdD7sc\n\nDeveloper Student Club Leads → https://goo.gle/2KF0kCl\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Yg7woDxIeBY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Yg7woDxIeBY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Yg7woDxIeBY/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/Yg7woDxIeBY/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/Yg7woDxIeBY/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/dq9w5npCGyzsmwPILW3ywIxJEQk\"", "id": "MTUxNTcyMzAyNDEwNjk5ODAxNzY0MzY2MjQ=", "snippet": { "publishedAt": "2019-10-28T22:40:10.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Dev Show Top 5 from the Android Dev Summit 2019", "description": "If you missed the Android Dev Summit 2019 in Sunnyvale, Developer Advocate Florina Muntenescu (@FMuntenescu) has got you covered on the latest releases and highlights from the event. Watch now for a summary of the best new features!\n\nTop announcements from the summit: \nhttps://goo.gle/2Jsj1sD\nAndroid Developer Challenge: https://goo.gle/36ga535\n\n#AndroidDevSummit All Sessions → https://goo.gle/ADS19allsessions\nSubscribe to the Android Developers channel → https://goo.gle/AndroidDevs \n\nThe Developer Show - Events Spotlight → https://goo.gle/2od3GEy\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/6jUbPkgADMk/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/6jUbPkgADMk/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/6jUbPkgADMk/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/6jUbPkgADMk/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/6jUbPkgADMk/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/wqKEFFnFDHpoAbNWO-MhiM5jPuc\"", "id": "MTUxNTcyMDE5MjA3Njk5ODAxNzY0MzQxMjg=", "snippet": { "publishedAt": "2019-10-25T16:00:07.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Announcing the Google Maps Platform YouTube Channel!", "description": "We are so excited to announce the Google Maps Platform YouTube channel where we’ll be sharing updates, tutorials, user stories and more! \n\nWe hope that this new channel will help you boost your creativity by serving content that helps you explore real-world insights and build immersive location experiences.\n\nWhether you’re new to the platform or a Google Maps Platform expert, dive into our YouTube channel to learn more! → https://goo.gle/GoogleMapsPlatform", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/UcGP8xTXSoA/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/UcGP8xTXSoA/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/UcGP8xTXSoA/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/UcGP8xTXSoA/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/UcGP8xTXSoA/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/Km3fXXOj6Vh4NgpqCy9l73ALmbs\"", "id": "MTUxNTcxOTUzMzIyNjk5ODAxNzY0MzY4MTY=", "snippet": { "publishedAt": "2019-10-24T21:42:02.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Android NDK r21, Security Health Analytics for GCP, Bazel 1.0", "description": "TL;DR 179 | The Google Developer News Show\n \nIntroducing Android NDK r21 → https://goo.gle/2MJBjrj\n\nSecurity Health Analytics for GCP in beta → https://goo.gle/2N9d4BH\n\nWhat’s New in DevTools (Chrome 79) → https://goo.gle/363pGDi\n\nBazel 1.0 → https://goo.gle/2pP7i07\n\nHere to bring you the latest developer news from across Google is Developer Advocate Meghan Mehta.\n\nTune in every week for a new episode, and let us know what you think of the latest announcements in the comments below! 😃\n\nWatch more #DevShow → https://goo.gle/GDevShow\nSubscribe to Google Developers! → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/9wU8ML2pUBs/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/9wU8ML2pUBs/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/9wU8ML2pUBs/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/9wU8ML2pUBs/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/9wU8ML2pUBs/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/4XzMakgiRPmTS2ZhbAu1JiUFSqE\"", "id": "MTUxNTcxNjk4OTA0Njk5ODAxNzY0MzY5NDQ=", "snippet": { "publishedAt": "2019-10-21T23:01:44.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Amey learns to create a low-cost hearing aid with Android", "description": "Amey Nerkar, from Google Developer Groups Pune noticed when he was going to school in Nashik, there was a deaf class that took place on campus. Amey was curious about the effectiveness of communication to the students and with the help of the GDG community, Amey learned to create a low cost hearing aid with Android.\n\nFind a community near you: https://goo.gle/2J54we5\n\nGDG Community Champions → https://goo.gle/2qtiRKT\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/-tzNjiwhdfo/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/-tzNjiwhdfo/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/-tzNjiwhdfo/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/-tzNjiwhdfo/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/-tzNjiwhdfo/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/0U50OgvF2amEQZNR1ha9S1rkwws\"", "id": "MTUxNTcxNjczODQ2Njk5ODAxNzY0MzQxOTI=", "snippet": { "publishedAt": "2019-10-21T16:04:06.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Developing Accessible Routes for Google Maps", "description": "In 2009, Sasha Blair-Goldensohn was commuting to work through Central Park when an unfortunate accident changed his life and his worldview. Hear Sasha’s story and learn how he teamed up with fellow Googlers Dianna and Rio to collaboratively develop accessible routing in Google Maps. Accessibility should be built into your app from the ground up! \n\nLearn More:\nGTFS Static Overview: https://goo.gle/2oKNhrC\nReference: https://goo.gle/32Fy7SQ\nRio Akasaka's blog post announcing Accessible Routes: https://goo.gle/2MSSRjx\n\nSubscribe to Google Developers → https://goo.gle/developers\n\nAdditional Subway footage provided by Marvin Allen Devlin", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/xFJ4Q6MB8A8/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/xFJ4Q6MB8A8/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/xFJ4Q6MB8A8/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/xFJ4Q6MB8A8/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/xFJ4Q6MB8A8/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/dynLVmlq3kVelMwmuEA4oEVtMpA\"", "id": "MTUxNTcxMzUwMTA2Njk5ODAxNjIwNDA0MDA=", "snippet": { "publishedAt": "2019-10-17T22:08:26.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "#AndroidDevSummit, architecting on Google Cloud, Google Code-in 2019 Org, & more!", "description": "TL;DR 178 | The Google Developer News Show\n \nPreviewing #AndroidDevSummit: Sessions, App, & Livestream Details → https://goo.gle/2IWlXNK\n\nCompute Engine or Kubernetes Engine? New trainings teach you the basics of architecting on Google Cloud → https://goo.gle/2Mn9PaE\n\nHow to build your first Google Maps Platform integration with deck.gl → https://goo.gle/35Lpfgr\n\nAnnouncing verified publishers on pub.dev → https://goo.gle/31tMSXT\n\nROBEL: Robotics Benchmarks for Learning with Low-Cost Robots (AI) → https://goo.gle/2J0KGk3\n\nUnderstanding Scheduling Behavior with SchedViz → https://goo.gle/32rfsKr\n\nGoogle Code-in 2019 Org Applications are Open! → https://goo.gle/2VQXIpr\n\nHere to bring you the latest developer news from across Google is Developer Advocate Dan Galpin.\n\nTune in every week for a new episode, and let us know what you think of the latest announcements in the comments below! 😃\n\nWatch more #DevShow → https://goo.gle/GDevShow\nSubscribe to Google Developers! → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/SBFfJeJGQIM/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/SBFfJeJGQIM/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/SBFfJeJGQIM/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/SBFfJeJGQIM/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/SBFfJeJGQIM/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/HCZb7JyJ2UdJwxeCtHEB6qiYSt4\"", "id": "MTUxNTcxMTYwMjgxNjk5ODAxNjIwNDI3MDQ=", "snippet": { "publishedAt": "2019-10-15T17:24:41.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Developer Student Clubs 2019 South East Asia Leads Summit", "description": "Developer Student Club Leads are passionate leaders at their university who are dedicated to helping their peers learn and connect. These Leads may be pursuing various degrees but have a good foundational knowledge of software development concepts. \n\nLearn more at → https://goo.gle/2MdD7sc\n\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/lBMvXIsTVDQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/lBMvXIsTVDQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/lBMvXIsTVDQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/lBMvXIsTVDQ/sddefault.jpg", "width": 640, "height": 480 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/2oVIgF4Bamzs2ZNX_Q8sHInq0kQ\"", "id": "MTUxNTcwNzQzMzU3Njk5ODAxNjIwNDMzNDQ=", "snippet": { "publishedAt": "2019-10-10T21:35:57.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Android Emulator tools, updates to AutoML Vision Edge, AutoML Video, & Video Intelligence API", "description": "TL;DR 177 | The Google Developer News Show\n \nContinuous testing with new Android emulator tools → https://goo.gle/2MukEXt\n\nExtending Stackdriver Logging across clouds and providers with new BindPlane integration → https://goo.gle/2MqzNsI\n\nAnnouncing updates to AutoML Vision Edge, AutoML Video, and Video Intelligence API → https://goo.gle/2owwtnY\n\nNo more mixed messages about HTTPS → https://goo.gle/2ODlMuC\n\nReleasing PAWS and PAWS-X: Two New Datasets to Improve Natural Language Understanding Models → https://goo.gle/2q4Wmf5\n\nHere to bring you the latest developer news from across Google is Developer Programs Engineer Andrew Brogdon.\n\nTune in every week for a new episode, and let us know what you think of the latest announcements in the comments below! 😃\n\nWatch more #DevShow → https://goo.gle/GDevShow\nSubscribe to Google Developers! → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/FOtdgiw2Emo/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/FOtdgiw2Emo/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/FOtdgiw2Emo/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/FOtdgiw2Emo/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/FOtdgiw2Emo/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } } ] } ================================================ FILE: testdata/apidata/activities/activities_by_channel_p2.json ================================================ { "kind": "youtube#activityListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ivly37DC9305MS_NVi_P4LZl_5I\"", "prevPageToken": "CAoQAQ", "pageInfo": { "totalResults": 13, "resultsPerPage": 10 }, "items": [ { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/kZCGI2QUa5ta2c3L6EQevDFgRmo\"", "id": "MTUxNTcwMTQxNzc1MjMwMTQ0NDI5ODcyMTY=", "snippet": { "publishedAt": "2019-10-03T22:29:35.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google Play Pass, DevFest, Firebase Summit ‘19, & more!", "description": "TL;DR 176 | The Google Developer News Show\n \nUnlock your creativity with Google Play Pass → https://goo.gle/31JQpBX\n\nFrom Code to Community: Why developers call DevFest home → https://goo.gle/2nfUXBj\n\nWhat's new at Firebase Summit 2019 → https://goo.gle/2Ofpjie\n\nCloud Build brings advanced CI/CD capabilities to GitHub → https://goo.gle/2Vfz9lJ\n\nContributing Data to Deepfake Detection Research → https://goo.gle/2Ii4dMs\n\nHere to bring you the latest developer news from across Google is Developer Advocate Filip Hracek.\n\nTune in every week for a new episode, and let us know what you think of the latest announcements in the comments below! 😃\n\nWatch more #DevShow → https://goo.gle/GDevShow\nSubscribe to Google Developers! → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/3QroLKeXjzU/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/3QroLKeXjzU/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/3QroLKeXjzU/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/3QroLKeXjzU/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/3QroLKeXjzU/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/NM37ICD3UdOFmxzZT7Cxz8BLxK4\"", "id": "MTUxNTY5OTcxNzAyMjMwMTQ0NDI5ODQ0NjQ=", "snippet": { "publishedAt": "2019-10-01T23:15:02.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Thinking in 5G (Google I/O'19)", "description": "5G is coming. In this workshop you'll get the chance to learn about what 5G is, its impact, and how Google can help. During the workshop, you'll participate in design sprint exercises to creatively explore what 5G will mean for your app, and how you can make the most of this new technology.\n\nWatch more #io19 here: \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\nGet started at → https://developers.google.com/\n\nSpeaker(s): Xander Pollock, Bob Borchers, Francesco Grilli, Hassan Sipra, Bhavin Rokad\n\nT0009A", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/fn4SaN-lUns/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/fn4SaN-lUns/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/fn4SaN-lUns/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/fn4SaN-lUns/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/fn4SaN-lUns/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/EwpknHmWhnkbN7WZy_xnvbfcbWg\"", "id": "MTUxNTY5OTU2NDg1MjMwMTQ0NDI5ODUyMzI=", "snippet": { "publishedAt": "2019-10-01T19:01:25.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Dev Show Top 5 from the Firebase Summit ‘19", "description": "If you missed the Firebase Summit 2019 in Madrid, Developer Advocate Doug Stevenson (@CodingDoug) has got you covered on the latest releases and highlights from the event. Watch now for a summary of the best new features!\n\nRead the blog post: https://goo.gle/2ogkydo\n\nWatch all the sessions: https://goo.gle/2mIOI8T\nSubscribe to the Firebase YouTube channel: https://goo.gle/2RjXwNe\n\nThe Developer Show - Events Spotlight → https://goo.gle/2od3GEy\nSubscribe to Google Developers! → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/fNVT0G3ttTM/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/fNVT0G3ttTM/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/fNVT0G3ttTM/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/fNVT0G3ttTM/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/fNVT0G3ttTM/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } } ] } ================================================ FILE: testdata/apidata/activities/activities_by_mine_p1.json ================================================ { "kind": "youtube#activityListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/le7hKns0ey3G45tVxUz8WPZskcQ\"", "nextPageToken": "CAEQAA", "pageInfo": { "totalResults": 2, "resultsPerPage": 1 }, "items": [ { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZVGvURQNCh-0EGNyS2UTdedzrhM\"", "id": "MTUxNTc0OTk2MjI3NzE4NDU5NjEyODA4MA==", "snippet": { "publishedAt": "2019-11-29T02:57:07.000Z", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "华山日出", "description": "冷冷的山头", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "ikaros-life", "type": "upload" } } ] } ================================================ FILE: testdata/apidata/activities/activities_by_mine_p2.json ================================================ { "kind": "youtube#activityListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/6yncT242auTddLxSe4dfDTC-4xE\"", "prevPageToken": "CAEQAQ", "pageInfo": { "totalResults": 2, "resultsPerPage": 1 }, "items": [ { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZshL2QThX_bnTHhpJy5emGCWHcE\"", "id": "MTUxNTc0OTk1OTAyNDkwNjA3MjU5NzQ1Ng==", "snippet": { "publishedAt": "2019-11-29T02:51:42.000Z", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "海上日出", "description": "美美美", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "ikaros-life", "type": "upload" } } ] } ================================================ FILE: testdata/apidata/captions/captions_by_video.json ================================================ { "kind": "youtube#captionListResponse", "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/bB4ewYNN7bQHonV-K7efrgBqh8M\"", "items": [ { "kind": "youtube#caption", "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\"", "id": "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I", "snippet": { "videoId": "oHR3wURdJ94", "lastUpdated": "2020-01-14T09:40:49.981Z", "trackKind": "standard", "language": "en", "name": "", "audioTrackType": "unknown", "isCC": false, "isLarge": false, "isEasyReader": false, "isDraft": false, "isAutoSynced": false, "status": "serving" } }, { "kind": "youtube#caption", "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/iRxIplZcCiX0oujr5gSVMXkij8M\"", "id": "fPMuDm722CIRcUAT3NTPQHQZJZJxt39kU7JvrHk8Kzs=", "snippet": { "videoId": "oHR3wURdJ94", "lastUpdated": "2020-01-14T09:39:46.991Z", "trackKind": "standard", "language": "zh-Hans", "name": "", "audioTrackType": "unknown", "isCC": false, "isLarge": false, "isEasyReader": false, "isDraft": false, "isAutoSynced": false, "status": "serving" } } ] } ================================================ FILE: testdata/apidata/captions/captions_filter_by_id.json ================================================ { "kind": "youtube#captionListResponse", "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/4OU1z5mciyh4emins-W6FGneNdM\"", "items": [ { "kind": "youtube#caption", "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\"", "id": "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I", "snippet": { "videoId": "oHR3wURdJ94", "lastUpdated": "2020-01-14T09:40:49.981Z", "trackKind": "standard", "language": "en", "name": "", "audioTrackType": "unknown", "isCC": false, "isLarge": false, "isEasyReader": false, "isDraft": false, "isAutoSynced": false, "status": "serving" } } ] } ================================================ FILE: testdata/apidata/captions/insert_response.json ================================================ { "kind": "youtube#caption", "etag": "R7KYT4aJbHp2wxlTmtFuKJ4pmF8", "id": "AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA", "snippet": { "videoId": "zxTVeyG1600", "lastUpdated": "2022-12-13T08:20:45.636548Z", "trackKind": "standard", "language": "ja", "name": "\\u65e5\\u6587\\u5b57\\u5e55", "audioTrackType": "unknown", "isCC": false, "isLarge": false, "isEasyReader": false, "isDraft": true, "isAutoSynced": false, "status": "serving" } } ================================================ FILE: testdata/apidata/captions/update_response.json ================================================ { "kind": "youtube#caption", "etag": "R7KYT4aJbHp2wxlTmtFuKJ4pmF8", "id": "AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA", "snippet": { "videoId": "zxTVeyG1600", "lastUpdated": "2022-12-13T08:20:45.636548Z", "trackKind": "standard", "language": "ja", "name": "\\u65e5\\u6587\\u5b57\\u5e55", "audioTrackType": "unknown", "isCC": false, "isLarge": false, "isEasyReader": false, "isDraft": false, "isAutoSynced": false, "status": "serving" } } ================================================ FILE: testdata/apidata/categories/guide_categories_by_region.json ================================================ { "kind": "youtube#guideCategoryListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/cKe5cQFDAFacJgSsRH_6x7oGHZU\"", "items": [ { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\"", "id": "GCQmVzdCBvZiBZb3VUdWJl", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Best of YouTube" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ImrTevQ0UvryyOmvjPFFu85AKCU\"", "id": "GCQ3JlYXRvciBvbiB0aGUgUmlzZQ", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Creator on the Rise" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/mbUaBJAZzQQXrUy_F02c8idvkek\"", "id": "GCTXVzaWM", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Music" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/dhUpBnIOaCoCDniv0ZybQIGWa8k\"", "id": "GCQ29tZWR5", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Comedy" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/nPXKSW3gvbE_IGO9kQ0pVWWIX3E\"", "id": "GCRmlsbSAmIEVudGVydGFpbm1lbnQ", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Film & Entertainment" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/81fHLyWLDYMN1EQqlz89HXIdMs4\"", "id": "GCR2FtaW5n", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Gaming" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/H1ihcHpjpFnqgfFZnS7Y95-ELJE\"", "id": "GCQmVhdXR5ICYgRmFzaGlvbg", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Beauty & Fashion" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Pv5088ySWfRzEy9SLDhygXIXhag\"", "id": "GCU3BvcnRz", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Sports" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/gRB8nQk5gRYsCtZYQAPUufs9ElM\"", "id": "GCVGVjaA", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Tech" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/NECk71Pm-g9olSeb8f65ze2ElOc\"", "id": "GCQ29va2luZyAmIEhlYWx0aA", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Cooking & Health" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/AVwXb7tzi2-JuEetcsorNLk6tZg\"", "id": "GCTmV3cyAmIFBvbGl0aWNz", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "News & Politics" } } ] } ================================================ FILE: testdata/apidata/categories/guide_category_multi.json ================================================ { "kind": "youtube#guideCategoryListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/JvKaPPmX316HuCUpJddmxaDPomo\"", "items": [ { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\"", "id": "GCQmVzdCBvZiBZb3VUdWJl", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Best of YouTube" } }, { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ImrTevQ0UvryyOmvjPFFu85AKCU\"", "id": "GCQ3JlYXRvciBvbiB0aGUgUmlzZQ", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Creator on the Rise" } } ] } ================================================ FILE: testdata/apidata/categories/guide_category_single.json ================================================ { "kind": "youtube#guideCategoryListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/KIJAFi2jsRHVBmAk3XYhyRKynjw\"", "items": [ { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\"", "id": "GCQmVzdCBvZiBZb3VUdWJl", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Best of YouTube" } } ] } ================================================ FILE: testdata/apidata/categories/video_category_by_region.json ================================================ { "kind": "youtube#videoCategoryListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/S730Ilt-Fi-emsQJvJAAShlR6hM\"", "items": [ { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Xy1mB4_yLrHy_BmKmPBggty2mZQ\"", "id": "1", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Film & Animation", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/UZ1oLIIz2dxIhO45ZTFR3a3NyTA\"", "id": "2", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Autos & Vehicles", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/nqRIq97-xe5XRZTxbknKFVe5Lmg\"", "id": "10", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Music", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/HwXKamM1Q20q9BN-oBJavSGkfDI\"", "id": "15", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Pets & Animals", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", "id": "17", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Sports", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/FJwVpGCVZ1yiJrqZbpqe68Sy_OE\"", "id": "18", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Short Movies", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/M-3iD9dwK7YJCafRf_DkLN8CouA\"", "id": "19", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Travel & Events", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/WmA0qYEfjWsAoyJFSw2zinhn2wM\"", "id": "20", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Gaming", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/EapFaGYG7K0StIXVf8aba249tdM\"", "id": "21", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Videoblogging", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/xId8RX7vRN8rqkbYZbNIytUQDRo\"", "id": "22", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "People & Blogs", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/G9LHzQmx44rX2S5yaga_Aqtwz8M\"", "id": "23", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Comedy", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/UVB9oxX2Bvqa_w_y3vXSLVK5E_s\"", "id": "24", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Entertainment", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/QiLK0ZIrFoORdk_g2l_XR_ECjDc\"", "id": "25", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "News & Politics", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/r6Ck6Z0_L0rG37VJQR200SGNA_w\"", "id": "26", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Howto & Style", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/EoYkczo9I3RCf96RveKTOgOPkUM\"", "id": "27", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Education", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/w5HjcTD82G_XA3xBctS30zS-JpQ\"", "id": "28", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Science & Technology", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/SalkJoBWq_smSEqiAx_qyri6Wa8\"", "id": "29", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Nonprofits & Activism", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/lL7uWDr_071CHxifjYG1tJrp4Uo\"", "id": "30", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Movies", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/WnuVfjO-PyFLO7NTRQIbrGE62nk\"", "id": "31", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Anime/Animation", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ctpH2hGA_UZ3volJT_FTlOg9M00\"", "id": "32", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Action/Adventure", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/L0kR3-g1BAo5UD1PLVbQ7LkkDtQ\"", "id": "33", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Classics", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/pUZOAC_s9sfiwar639qr_wAB-aI\"", "id": "34", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Comedy", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Xb5JLhtyNRN3AQq021Ds-OV50Jk\"", "id": "35", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Documentary", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/u8WXzF4HIhtEi805__sqjuA4lEk\"", "id": "36", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Drama", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/D04PP4Gr7wc4IV_O9G66Z4A8KWQ\"", "id": "37", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Family", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/i5-_AceGXQCEEMWU0V8CcQm_vLQ\"", "id": "38", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Foreign", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/rtlxd0zOixA9QHdIZB26-St5qgQ\"", "id": "39", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Horror", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/N1TrDFLRppxZgBowCJfJCvh0Dpg\"", "id": "40", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Sci-Fi/Fantasy", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/7UMGi6zRySqXopr_rv4sZq6Za2E\"", "id": "41", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Thriller", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/RScXhi324h8usyIetreAVb-uKeM\"", "id": "42", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Shorts", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/0n9MJVCDLpA8q7aiGVrFsuFsd0A\"", "id": "43", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Shows", "assignable": false } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/x5NxSf5fz8hn4loSN4rvhwzD_pY\"", "id": "44", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Trailers", "assignable": false } } ] } ================================================ FILE: testdata/apidata/categories/video_category_multi.json ================================================ { "kind": "youtube#videoCategoryListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/QhsRsql8vvkcmFdomppeHDbsV0Q\"", "items": [ { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", "id": "17", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Sports", "assignable": true } }, { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/FJwVpGCVZ1yiJrqZbpqe68Sy_OE\"", "id": "18", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Short Movies", "assignable": false } } ] } ================================================ FILE: testdata/apidata/categories/video_category_single.json ================================================ { "kind": "youtube#videoCategoryListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/0_wT9Ta0iZu7ETYC3E6Xi_B4mtA\"", "items": [ { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", "id": "17", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Sports", "assignable": true } } ] } ================================================ FILE: testdata/apidata/channel_banners/insert_response.json ================================================ { "kind": "youtube#channelBannerResource", "etag": "ezPZq6gkoCbM-5C4P-ved0Irol0", "url": "https://yt3.googleusercontent.com/1mrHHBsTG4JhGAQg_dmFf3ByELNVnXu7qCvmuhC81TFemB8XpaDgYuMgh5w220bh4APAj-xDeA" } ================================================ FILE: testdata/apidata/channel_info_multi.json ================================================ { "kind": "youtube#channelListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/GaZ2FEuxLAqXsTHh13eEnkvWngM\"", "prevPageToken": "CAUQAQ", "pageInfo": { "totalResults": 2, "resultsPerPage": 2 }, "items": [ { "kind": "youtube#channel", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/MegdiM4iUe5XzO4555ucXsyo7aQ\"", "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.", "customUrl": "googlecode", "publishedAt": "2007-08-23T00:34:43.000Z", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo", "width": 88, "height": 88 }, "medium": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo", "width": 240, "height": 240 }, "high": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo", "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." }, "country": "US" } }, { "kind": "youtube#channel", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/YDtNMQVe1s66E1iSBpnrlXzvJgE\"", "id": "UCK8sQmJBp8GCxrOtXWBpyEA", "snippet": { "title": "Google", "description": "Experience the world of Google on our official YouTube channel. Watch videos about our products, technology, company happenings and more. Subscribe to get updates from all your favorite Google products and teams.", "customUrl": "google", "publishedAt": "2005-09-18T22:37:10.000Z", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s88-c-k-c0xffffffff-no-rj-mo", "width": 88, "height": 88 }, "medium": { "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s240-c-k-c0xffffffff-no-rj-mo", "width": 240, "height": 240 }, "high": { "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s800-c-k-c0xffffffff-no-rj-mo", "width": 800, "height": 800 } }, "localized": { "title": "Google", "description": "Experience the world of Google on our official YouTube channel. Watch videos about our products, technology, company happenings and more. Subscribe to get updates from all your favorite Google products and teams." } } } ] } ================================================ FILE: testdata/apidata/channel_info_single.json ================================================ { "kind": "youtube#channelListResponse", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/0lqbdkIcLGXAPiLsJ3FTHo96TDg\"", "pageInfo": { "totalResults": 1, "resultsPerPage": 1 }, "items": [ { "kind": "youtube#channel", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/HUbWoTqNN1LPZKmbyCzPgvjVuR4\"", "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.", "customUrl": "@googledevelopers", "publishedAt": "2007-08-23T00:34:43.000Z", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo", "width": 88, "height": 88 }, "medium": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo", "width": 240, "height": 240 }, "high": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo", "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." }, "country": "US" }, "statistics": { "viewCount": "160361638", "commentCount": "0", "subscriberCount": "1927873", "hiddenSubscriberCount": false, "videoCount": "5026" } } ] } ================================================ FILE: testdata/apidata/channel_sections/channel_sections_by_channel.json ================================================ { "kind": "youtube#channelSectionListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/IG4AAhdP913_ibNr3xxa2XjZhAU\"", "items": [ { "kind": "youtube#channelSection", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/JNSONRhMV8b1OaalB42ZUtVBZ44\"", "id": "UCa-vrCLQHviTOVnEKDOdetQ.jNQXAC9IVRw", "snippet": { "type": "recentUploads", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "position": 0 } }, { "kind": "youtube#channelSection", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/bcTK2_pxKS22pZizMGDGgnCcdeQ\"", "id": "UCa-vrCLQHviTOVnEKDOdetQ.LeAltgu_pbM", "snippet": { "type": "allPlaylists", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "position": 1 } }, { "kind": "youtube#channelSection", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/lkkZaRpGqH1OLyeS4UMzEQkz5IU\"", "id": "UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY", "snippet": { "type": "multiplePlaylists", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "我的操作诶", "position": 2 }, "contentDetails": { "playlists": [ "PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS", "PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g" ] } } ] } ================================================ FILE: testdata/apidata/channel_sections/channel_sections_by_id.json ================================================ { "kind": "youtube#channelSectionListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Oysqp4SfBtVFI8-0LVzUEHn8LN4\"", "items": [ { "kind": "youtube#channelSection", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Z3z8l_2oWLi9cWlfGTNMxsVwOTw\"", "id": "UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY", "snippet": { "type": "multiplePlaylists", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "我的操作诶", "position": 2 }, "contentDetails": { "playlists": [ "PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS", "PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g" ] } } ] } ================================================ FILE: testdata/apidata/channel_sections/channel_sections_by_ids.json ================================================ { "kind": "youtube#channelSectionListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Nvmls-WhS6tunMyp9v6ZIEFrgRI\"", "items": [ { "kind": "youtube#channelSection", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/RSxEQQPXGQo3MTN75toyRTUTEmY\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.npYvuMz0_es", "snippet": { "type": "recentUploads", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 9 } }, { "kind": "youtube#channelSection", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/zaHbYWO-Q1zjW4IYjza-bTrqeIc\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.9_wU0qhEPR8", "snippet": { "type": "singlePlaylist", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 8 }, "contentDetails": { "playlists": [ "PLOU2XLYxmsIKKMtrYD-IfPdlVunyPl9GM" ] } } ] } ================================================ FILE: testdata/apidata/channel_sections/insert_resp.json ================================================ { "kind": "youtube#channelSection", "etag": "VNVb0NhdJ8VHoZaVCqGVqfaRrVU", "id": "UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM", "snippet": { "type": "multipleplaylists", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "position": 4 }, "contentDetails": { "playlists": [ "PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g" ] } } ================================================ FILE: testdata/apidata/channels/info.json ================================================ {"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"}}]} ================================================ FILE: testdata/apidata/channels/info_multiple.json ================================================ {"kind":"youtube#channelListResponse","etag":"doLptdWt69-xv1D0XqhnNqKHg9o","pageInfo":{"totalResults":2,"resultsPerPage":5},"items":[{"kind":"youtube#channel","etag":"BSP3hQtvSS6Eo9sg31jocVuV4mg","id":"UCK8sQmJBp8GCxrOtXWBpyEA","snippet":{"title":"Google","description":"Experience the world of Google on our official YouTube channel. Watch videos about our products, technology, company happenings and more. Subscribe to get updates from all your favorite Google products and teams.","customUrl":"@google","publishedAt":"2005-09-18T22:37:10Z","thumbnails":{"default":{"url":"https://yt3.ggpht.com/ytc/AMLnZu_31rROBnB8bq9EJfk82OnclHISQ3Hrx6i1oWLai5o=s88-c-k-c0x00ffffff-no-rj","width":88,"height":88},"medium":{"url":"https://yt3.ggpht.com/ytc/AMLnZu_31rROBnB8bq9EJfk82OnclHISQ3Hrx6i1oWLai5o=s240-c-k-c0x00ffffff-no-rj","width":240,"height":240},"high":{"url":"https://yt3.ggpht.com/ytc/AMLnZu_31rROBnB8bq9EJfk82OnclHISQ3Hrx6i1oWLai5o=s800-c-k-c0x00ffffff-no-rj","width":800,"height":800}},"localized":{"title":"Google","description":"Experience the world of Google on our official YouTube channel. Watch videos about our products, technology, company happenings and more. Subscribe to get updates from all your favorite Google products and teams."}},"contentDetails":{"relatedPlaylists":{"likes":"","uploads":"UUK8sQmJBp8GCxrOtXWBpyEA"}},"statistics":{"viewCount":"3331930783","subscriberCount":"10700000","hiddenSubscriberCount":false,"videoCount":"2678"},"brandingSettings":{"channel":{"title":"Google","description":"Experience the world of Google on our official YouTube channel. Watch videos about our products, technology, company happenings and more. Subscribe to get updates from all your favorite Google products and teams.","keywords":"Google Technology Science Android \"Google app\" \"Google drive\" Gmail \"Google Maps\" Nexus \"Google Doodles\" \"Google Zeitgeist\"","trackingAnalyticsAccountId":"UA-7001471-1","unsubscribedTrailer":"hl4N6Yo6qWc"},"image":{"bannerExternalUrl":"https://yt3.ggpht.com/C7C_rceG0_dgSK1uRXoM6s1wCiOwDpsc_bJLELECJ7dVrNZNMhub9la_nhAL6aKpkdR0Z91d"}}},{"kind":"youtube#channel","etag":"-CUA2eUMiVEMMF7ru5xl_INNyfw","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"},"contentDetails":{"relatedPlaylists":{"likes":"","uploads":"UU_x5XG1OV2P6uZZ5FSM9Ttw"}},"statistics":{"viewCount":"208790084","subscriberCount":"2260000","hiddenSubscriberCount":false,"videoCount":"5652"},"brandingSettings":{"channel":{"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","keywords":"\"google developers\" developers \"Google developers videos\" \"google developer tutorials\" \"developer tutorials\" \"developer news\" android firebase tensorflow chrome web flutter \"google developer experts\" \"google launchpad\" \"developer updates\" google \"google design\"","trackingAnalyticsAccountId":"YT-9170156-1","unsubscribedTrailer":"CMN0rd1-uOM","country":"US"},"image":{"bannerExternalUrl":"https://yt3.ggpht.com/LMkDZSq0icg6yqyItLxe2c9tb_KjjI6jsrWE019X4L5TULPPLXJy6rtx7-nN7TB5EiHzoB0R5g"}}}]} ================================================ FILE: testdata/apidata/channels/update_resp.json ================================================ {"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"}}} ================================================ FILE: testdata/apidata/client_secrets/client_secret_installed_bad.json ================================================ { "installed": { "client_id": "client_id", "project_id": "project_id", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs" } } ================================================ FILE: testdata/apidata/client_secrets/client_secret_installed_good.json ================================================ { "installed": { "client_id": "client_id", "project_id": "project_id", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "client_secret" } } ================================================ FILE: testdata/apidata/client_secrets/client_secret_unsupported.json ================================================ { "unsupported": { "client_id": "client_id", "project_id": "project_id", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs" } } ================================================ FILE: testdata/apidata/client_secrets/client_secret_web.json ================================================ { "web": { "client_id": "client_id", "project_id": "project_id", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "client_secret", "redirect_uris": [ "http://localhost:5000/oauth2callback" ] } } ================================================ FILE: testdata/apidata/comment_threads/comment_thread_single.json ================================================ { "kind": "youtube#commentThreadListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/VQfUfBFenzO3S8AzxaX0A2cOK_w\"", "pageInfo": { "totalResults": 1, "resultsPerPage": 20 }, "items": [ { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Bov8ITX91R0QmVGZN70wbJ5_hOs\"", "id": "UgxKREWxIgDrw8w2e_Z4AaABAg", "snippet": { "videoId": "D-lhorsDlUQ", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/W05nAf1QR8i2AYeULDR019ku3Lg\"", "id": "UgxKREWxIgDrw8w2e_Z4AaABAg", "snippet": { "authorDisplayName": "Hieu Nguyen", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7-oQeqHcOEyt0l2rBBZH1qAiBNKNn1UmmGk5Q=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UClfzT4CU_yaZjJaI4pKqSjQ", "authorChannelId": { "value": "UClfzT4CU_yaZjJaI4pKqSjQ" }, "videoId": "D-lhorsDlUQ", "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 ...", "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 ...", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-04-20T01:03:39.000Z", "updatedAt": "2019-04-20T01:03:39.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } } ] } ================================================ FILE: testdata/apidata/comment_threads/comment_threads_all_to_me.json ================================================ { "kind": "youtube#commentThreadListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/9f4Mz6CLxsfPgItKiTYYywpU5pY\"", "pageInfo": { "totalResults": 4, "resultsPerPage": 20 }, "items": [ { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/W4DPat_BSfoIcTpeSjXLpOq8mRw\"", "id": "UgyWeTdgc4sc1xgmbld4AaABAg", "snippet": { "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "videoId": "JE8xdDp5B8Q", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/dQGpRBbLeR7VSsV4-nNtA7qKCcI\"", "id": "UgyWeTdgc4sc1xgmbld4AaABAg", "snippet": { "authorDisplayName": "kun liu", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw", "authorChannelId": { "value": "UCNvMBmCASzTNNX8lW3JRMbw" }, "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "videoId": "JE8xdDp5B8Q", "textDisplay": "Hope to go next time. yah!", "textOriginal": "Hope to go next time. yah!", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-12-01T03:36:55.000Z", "updatedAt": "2019-12-01T03:36:55.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Lu2JczYrJaDkKhajTXWW8SIdxJQ\"", "id": "Ugw5zYU6n9pmIgAZWvN4AaABAg", "snippet": { "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "videoId": "JE8xdDp5B8Q", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/dlWLs_1koF5w5gat2bc4UAvWn_U\"", "id": "Ugw5zYU6n9pmIgAZWvN4AaABAg", "snippet": { "authorDisplayName": "kun liu", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw", "authorChannelId": { "value": "UCNvMBmCASzTNNX8lW3JRMbw" }, "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "videoId": "JE8xdDp5B8Q", "textDisplay": "This is so beautiful !", "textOriginal": "This is so beautiful !", "canRate": true, "viewerRating": "like", "likeCount": 1, "publishedAt": "2019-12-01T03:36:10.000Z", "updatedAt": "2019-12-01T03:36:10.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/s8Hj1OPYiSO7BiFeS0Pc66OAw8E\"", "id": "Ugy0FhSzKMNMT9qs6zt4AaABAg", "snippet": { "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/FA35BA_7xSgoiY5DpoybVoEFtEc\"", "id": "Ugy0FhSzKMNMT9qs6zt4AaABAg", "snippet": { "authorDisplayName": "杨佳名", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l78ya-P2zYdCbefXtEDb1eMRGncYTsgAgBS9xQ=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCupGECpPBlTnanhGHLe6GpA", "authorChannelId": { "value": "UCupGECpPBlTnanhGHLe6GpA" }, "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "textDisplay": "大满贯", "textOriginal": "大满贯", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-11-29T02:58:25.000Z", "updatedAt": "2019-11-29T02:58:25.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/qkxnREu6fFm4Z803KhpgDZQJEeU\"", "id": "UgwSRpgkjCe6OHHe3IJ4AaABAg", "snippet": { "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/0zRJJZKVzaO9-IfxGda3Sbke03o\"", "id": "UgwSRpgkjCe6OHHe3IJ4AaABAg", "snippet": { "authorDisplayName": "杨佳名", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l78ya-P2zYdCbefXtEDb1eMRGncYTsgAgBS9xQ=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCupGECpPBlTnanhGHLe6GpA", "authorChannelId": { "value": "UCupGECpPBlTnanhGHLe6GpA" }, "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "textDisplay": "中午吃啥", "textOriginal": "中午吃啥", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-11-29T02:57:53.000Z", "updatedAt": "2019-11-29T02:57:53.000Z" } }, "canReply": true, "totalReplyCount": 1, "isPublic": true } } ] } ================================================ FILE: testdata/apidata/comment_threads/comment_threads_by_channel.json ================================================ { "kind": "youtube#commentThreadListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Wm2vP6ZY_91XACR5c9Vk8QFfpWY\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 20 }, "items": [ { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/o0Du7va0vUQliAR76OFrtcgOjOc\"", "id": "UgyUBI0HsgL9emxcZpR4AaABAg", "snippet": { "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/CD_Xk4X_gxANaxestqfTSanwWrk\"", "id": "UgyUBI0HsgL9emxcZpR4AaABAg", "snippet": { "authorDisplayName": "DevRagz", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw", "authorChannelId": { "value": "UCTWlvHQQXUs-4IVfdnGOUbw" }, "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "textDisplay": "Hello", "textOriginal": "Hello", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-06-23T02:49:00.000Z", "updatedAt": "2019-06-23T02:49:00.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/yHddGp8i_wDOcek2w4RkfBpgS7s\"", "id": "Ugzi3lkqDPfIOirGFLh4AaABAg", "snippet": { "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/jYGGL-8Rw6c0xU3CT-WWU1uHZyw\"", "id": "Ugzi3lkqDPfIOirGFLh4AaABAg", "snippet": { "authorDisplayName": "EclipZe Muzik", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7-JV7pt6X2WsxOdaD6nzK_rAj_2FVAjLyNR1Q=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCCuQ3wts9ASkk0qZJyEjYbw", "authorChannelId": { "value": "UCCuQ3wts9ASkk0qZJyEjYbw" }, "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "textDisplay": "exceptional content!", "textOriginal": "exceptional content!", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2018-05-26T20:11:30.000Z", "updatedAt": "2018-05-26T20:11:30.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } } ] } ================================================ FILE: testdata/apidata/comment_threads/comment_threads_by_video_paged_1.json ================================================ { "kind": "youtube#commentThreadListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/mj64hlcyhnGGcJWAccdWisvf8MY\"", "nextPageToken": "QURTSl9pMzdZOUVzMkI0czlmRmNjSVBPcTBTdzVzajUydDVnbE5SNElWS0l5WU12amYweVotdzF5c1hTNmxzUmVIcEZXbmVEVFMzNVJmWk82TVVwUlB2LWh5aUpOQlA5TGQzTWZEcHlTeTd2dlNGRUFZaVF0cmtJd01BTHlnOG0=", "pageInfo": { "totalResults": 5, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/-kLX-6-yrqngKqYMS46dOtyqEUE\"", "id": "UgyZ1jqkHKYvi1-ruOZ4AaABAg", "snippet": { "videoId": "F1UP7wRCPH8", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/qVtEMtZJzm2E2_ThbD7t3KLAsWc\"", "id": "UgyZ1jqkHKYvi1-ruOZ4AaABAg", "snippet": { "authorDisplayName": "himani agarwal", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l78sAaVkRzpOeLtaisG-MYI3gOvnvNL9iXZo3g=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCzj-CjKYmGlZ7vTL-vOe57A", "authorChannelId": { "value": "UCzj-CjKYmGlZ7vTL-vOe57A" }, "videoId": "F1UP7wRCPH8", "textDisplay": "\u003ca href=\"http://google.com/\"\u003eGoogle.com\u003c/a\u003e", "textOriginal": "Google.com", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-11-26T15:51:44.000Z", "updatedAt": "2019-11-26T15:51:44.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Iw6Eo0WB_HfKsyEl2jXWyOCWyKA\"", "id": "Ugy4OzAuz5uJuFt3FH54AaABAg", "snippet": { "videoId": "F1UP7wRCPH8", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/zqJ_eR8wdFdwY96FNFPKAVgyBKM\"", "id": "Ugy4OzAuz5uJuFt3FH54AaABAg", "snippet": { "authorDisplayName": "el bojo loco", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l79o1pzBIfl2qWbKEyhg__UhsQZ4V9Dsg-lRSg=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCGBUkig1Ala-Z2gF73WSTdw", "authorChannelId": { "value": "UCGBUkig1Ala-Z2gF73WSTdw" }, "videoId": "F1UP7wRCPH8", "textDisplay": "Please use CLS on your own search results page..", "textOriginal": "Please use CLS on your own search results page..", "canRate": true, "viewerRating": "none", "likeCount": 1, "publishedAt": "2019-11-21T19:18:39.000Z", "updatedAt": "2019-11-21T19:18:39.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/vBjDNfPUz4zXiio50mRS5MC19bg\"", "id": "UgysQP-vp089eFP0Stl4AaABAg", "snippet": { "videoId": "F1UP7wRCPH8", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ebOBApCraUZPu-TA4vuCJbwNjz4\"", "id": "UgysQP-vp089eFP0Stl4AaABAg", "snippet": { "authorDisplayName": "Andres Jorquera", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_71xCslORfWq9-j9dIPbw1Sa3M0hw_G7gCNA=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCzZ87J8wYVpOADFviqyFKhw", "authorChannelId": { "value": "UCzZ87J8wYVpOADFviqyFKhw" }, "videoId": "F1UP7wRCPH8", "textDisplay": "Funny that they use heroku to show a demo instead of google cloud", "textOriginal": "Funny that they use heroku to show a demo instead of google cloud", "canRate": true, "viewerRating": "none", "likeCount": 4, "publishedAt": "2019-11-17T11:34:48.000Z", "updatedAt": "2019-11-17T11:34:48.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/TqNnjkgG07M6i2ikeu8X4rDTxZ0\"", "id": "UgyKv8ziPYFJDGytM_J4AaABAg", "snippet": { "videoId": "F1UP7wRCPH8", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/I5hbgLodwC0nmD3iOcByof5O7rk\"", "id": "UgyKv8ziPYFJDGytM_J4AaABAg", "snippet": { "authorDisplayName": "Masum Khan", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l79ngmGDoFG7XuL9cAzTmh6U8moqME8OHdh7pQ=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCHJQ8PHPGprvhKdLVHLwRPA", "authorChannelId": { "value": "UCHJQ8PHPGprvhKdLVHLwRPA" }, "videoId": "F1UP7wRCPH8", "textDisplay": "All account on Android store for data show the Google and YouTube video Android computer system fordata show\u003cbr /\u003e settings displayed", "textOriginal": "All account on Android store for data show the Google and YouTube video Android computer system fordata show\n settings displayed", "canRate": true, "viewerRating": "none", "likeCount": 1, "publishedAt": "2019-11-16T20:05:39.000Z", "updatedAt": "2019-11-16T20:05:39.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/5gYLe0DJitJqoq12oqE8gIeLiUw\"", "id": "UgwI_ylXVnzPS8Q-oAV4AaABAg", "snippet": { "videoId": "F1UP7wRCPH8", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/RETGsc5ySPyJ5IIAKVndyEC4R10\"", "id": "UgwI_ylXVnzPS8Q-oAV4AaABAg", "snippet": { "authorDisplayName": "matrix uduma", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_xsrGS2s0whxHHHAMbfAsHerh3HxH1VHZ3fg=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCACHW4qVHoE4zqhHxcVE3Kw", "authorChannelId": { "value": "UCACHW4qVHoE4zqhHxcVE3Kw" }, "videoId": "F1UP7wRCPH8", "textDisplay": "Lot's of good light coming in from here \u003ca href=\"http://www.youtube.com/results?search_query=%23ChromeDevSummit\"\u003e#ChromeDevSummit\u003c/a\u003e the web is evolving at a very fast rate, keeping pace with all this advancement can really get you gasping for breath. But here is \u003ca href=\"http://www.youtube.com/results?search_query=%23chromedevsummit\"\u003e#chromedevsummit\u003c/a\u003e packing it and shooting at me as one bullet. I really appreciate the work you guys are doing here.", "textOriginal": "Lot's of good light coming in from here #ChromeDevSummit the web is evolving at a very fast rate, keeping pace with all this advancement can really get you gasping for breath. But here is #chromedevsummit packing it and shooting at me as one bullet. I really appreciate the work you guys are doing here.", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-11-14T23:31:33.000Z", "updatedAt": "2019-11-14T23:31:33.000Z" } }, "canReply": true, "totalReplyCount": 2, "isPublic": true } } ] } ================================================ FILE: testdata/apidata/comment_threads/comment_threads_by_video_paged_2.json ================================================ { "kind": "youtube#commentThreadListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/mj64hlcyhnGGcJWAccdWisvf8MY\"", "pageInfo": { "totalResults": 5, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/-kLX-6-yrqngKqYMS46dOtyqEUE\"", "id": "UgyZ1jqkHKYvi1-ruOZ4AaABAg", "snippet": { "videoId": "F1UP7wRCPH8", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/qVtEMtZJzm2E2_ThbD7t3KLAsWc\"", "id": "UgyZ1jqkHKYvi1-ruOZ4AaABAg", "snippet": { "authorDisplayName": "himani agarwal", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l78sAaVkRzpOeLtaisG-MYI3gOvnvNL9iXZo3g=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCzj-CjKYmGlZ7vTL-vOe57A", "authorChannelId": { "value": "UCzj-CjKYmGlZ7vTL-vOe57A" }, "videoId": "F1UP7wRCPH8", "textDisplay": "\u003ca href=\"http://google.com/\"\u003eGoogle.com\u003c/a\u003e", "textOriginal": "Google.com", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-11-26T15:51:44.000Z", "updatedAt": "2019-11-26T15:51:44.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Iw6Eo0WB_HfKsyEl2jXWyOCWyKA\"", "id": "Ugy4OzAuz5uJuFt3FH54AaABAg", "snippet": { "videoId": "F1UP7wRCPH8", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/zqJ_eR8wdFdwY96FNFPKAVgyBKM\"", "id": "Ugy4OzAuz5uJuFt3FH54AaABAg", "snippet": { "authorDisplayName": "el bojo loco", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l79o1pzBIfl2qWbKEyhg__UhsQZ4V9Dsg-lRSg=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCGBUkig1Ala-Z2gF73WSTdw", "authorChannelId": { "value": "UCGBUkig1Ala-Z2gF73WSTdw" }, "videoId": "F1UP7wRCPH8", "textDisplay": "Please use CLS on your own search results page..", "textOriginal": "Please use CLS on your own search results page..", "canRate": true, "viewerRating": "none", "likeCount": 1, "publishedAt": "2019-11-21T19:18:39.000Z", "updatedAt": "2019-11-21T19:18:39.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/vBjDNfPUz4zXiio50mRS5MC19bg\"", "id": "UgysQP-vp089eFP0Stl4AaABAg", "snippet": { "videoId": "F1UP7wRCPH8", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ebOBApCraUZPu-TA4vuCJbwNjz4\"", "id": "UgysQP-vp089eFP0Stl4AaABAg", "snippet": { "authorDisplayName": "Andres Jorquera", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_71xCslORfWq9-j9dIPbw1Sa3M0hw_G7gCNA=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCzZ87J8wYVpOADFviqyFKhw", "authorChannelId": { "value": "UCzZ87J8wYVpOADFviqyFKhw" }, "videoId": "F1UP7wRCPH8", "textDisplay": "Funny that they use heroku to show a demo instead of google cloud", "textOriginal": "Funny that they use heroku to show a demo instead of google cloud", "canRate": true, "viewerRating": "none", "likeCount": 4, "publishedAt": "2019-11-17T11:34:48.000Z", "updatedAt": "2019-11-17T11:34:48.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/TqNnjkgG07M6i2ikeu8X4rDTxZ0\"", "id": "UgyKv8ziPYFJDGytM_J4AaABAg", "snippet": { "videoId": "F1UP7wRCPH8", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/I5hbgLodwC0nmD3iOcByof5O7rk\"", "id": "UgyKv8ziPYFJDGytM_J4AaABAg", "snippet": { "authorDisplayName": "Masum Khan", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l79ngmGDoFG7XuL9cAzTmh6U8moqME8OHdh7pQ=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCHJQ8PHPGprvhKdLVHLwRPA", "authorChannelId": { "value": "UCHJQ8PHPGprvhKdLVHLwRPA" }, "videoId": "F1UP7wRCPH8", "textDisplay": "All account on Android store for data show the Google and YouTube video Android computer system fordata show\u003cbr /\u003e settings displayed", "textOriginal": "All account on Android store for data show the Google and YouTube video Android computer system fordata show\n settings displayed", "canRate": true, "viewerRating": "none", "likeCount": 1, "publishedAt": "2019-11-16T20:05:39.000Z", "updatedAt": "2019-11-16T20:05:39.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/5gYLe0DJitJqoq12oqE8gIeLiUw\"", "id": "UgwI_ylXVnzPS8Q-oAV4AaABAg", "snippet": { "videoId": "F1UP7wRCPH8", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/RETGsc5ySPyJ5IIAKVndyEC4R10\"", "id": "UgwI_ylXVnzPS8Q-oAV4AaABAg", "snippet": { "authorDisplayName": "matrix uduma", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_xsrGS2s0whxHHHAMbfAsHerh3HxH1VHZ3fg=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCACHW4qVHoE4zqhHxcVE3Kw", "authorChannelId": { "value": "UCACHW4qVHoE4zqhHxcVE3Kw" }, "videoId": "F1UP7wRCPH8", "textDisplay": "Lot's of good light coming in from here \u003ca href=\"http://www.youtube.com/results?search_query=%23ChromeDevSummit\"\u003e#ChromeDevSummit\u003c/a\u003e the web is evolving at a very fast rate, keeping pace with all this advancement can really get you gasping for breath. But here is \u003ca href=\"http://www.youtube.com/results?search_query=%23chromedevsummit\"\u003e#chromedevsummit\u003c/a\u003e packing it and shooting at me as one bullet. I really appreciate the work you guys are doing here.", "textOriginal": "Lot's of good light coming in from here #ChromeDevSummit the web is evolving at a very fast rate, keeping pace with all this advancement can really get you gasping for breath. But here is #chromedevsummit packing it and shooting at me as one bullet. I really appreciate the work you guys are doing here.", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-11-14T23:31:33.000Z", "updatedAt": "2019-11-14T23:31:33.000Z" } }, "canReply": true, "totalReplyCount": 2, "isPublic": true } } ] } ================================================ FILE: testdata/apidata/comment_threads/comment_threads_multi.json ================================================ { "kind": "youtube#commentThreadListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/LKHkXHQZoSC-BXdpnHc829mN3QM\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 20 }, "items": [ { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/hSvCiIVcQzGAvouQ8s14qvX4vak\"", "id": "UgxKREWxIgDrw8w2e_Z4AaABAg", "snippet": { "videoId": "D-lhorsDlUQ", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/fcXJ4Y0FWzHufPruLqAlCMnzQ4A\"", "id": "UgxKREWxIgDrw8w2e_Z4AaABAg", "snippet": { "authorDisplayName": "Hieu Nguyen", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7-oQeqHcOEyt0l2rBBZH1qAiBNKNn1UmmGk5Q=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UClfzT4CU_yaZjJaI4pKqSjQ", "authorChannelId": { "value": "UClfzT4CU_yaZjJaI4pKqSjQ" }, "videoId": "D-lhorsDlUQ", "textDisplay": "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 ...", "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 ...", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-04-20T01:03:39.000Z", "updatedAt": "2019-04-20T01:03:39.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/hK5O5Tf4dzAUW8LQFP4yX6Wmsd8\"", "id": "UgyrVQaFfEdvaSzstj14AaABAg", "snippet": { "videoId": "D-lhorsDlUQ", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/bVR2Q3iGDhKQCFKVdH7yK0p4ogI\"", "id": "UgyrVQaFfEdvaSzstj14AaABAg", "snippet": { "authorDisplayName": "Mani Kanta", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_EFNV6q-NNu9BGJ58f-2Da9utFG8ISB8uKYg=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCJBxRADq6jctX-YdhjkB6PA", "authorChannelId": { "value": "UCJBxRADq6jctX-YdhjkB6PA" }, "videoId": "D-lhorsDlUQ", "textDisplay": "super", "textOriginal": "super", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-04-04T04:14:44.000Z", "updatedAt": "2019-04-04T04:14:44.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } } ] } ================================================ FILE: testdata/apidata/comment_threads/comment_threads_with_search.json ================================================ { "kind": "youtube#commentThreadListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Sb1s9ORYlw5LpXhXM1osg1jeYgM\"", "pageInfo": { "totalResults": 1, "resultsPerPage": 20 }, "items": [ { "kind": "youtube#commentThread", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/o0Du7va0vUQliAR76OFrtcgOjOc\"", "id": "UgyUBI0HsgL9emxcZpR4AaABAg", "snippet": { "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "topLevelComment": { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/CD_Xk4X_gxANaxestqfTSanwWrk\"", "id": "UgyUBI0HsgL9emxcZpR4AaABAg", "snippet": { "authorDisplayName": "DevRagz", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw", "authorChannelId": { "value": "UCTWlvHQQXUs-4IVfdnGOUbw" }, "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "textDisplay": "Hello", "textOriginal": "Hello", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-06-23T02:49:00.000Z", "updatedAt": "2019-06-23T02:49:00.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } } ] } ================================================ FILE: testdata/apidata/comment_threads/insert_response.json ================================================ { "kind": "youtube#commentThread", "etag": "AMgl2io48I4z6Ulu9kv4C43sVvk", "id": "Ugx_5P8rmn4vKbN6wwt4AaABAg", "snippet": { "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "videoId": "JE8xdDp5B8Q", "topLevelComment": { "kind": "youtube#comment", "etag": "I_E2on6NOdGkpW0WodB74OVCU_E", "id": "Ugx_5P8rmn4vKbN6wwt4AaABAg", "snippet": { "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "videoId": "JE8xdDp5B8Q", "textDisplay": "Sun from the api", "textOriginal": "Sun from the api", "authorDisplayName": "ikaros data", "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s48-c-k-c0x00ffffff-no-rj", "authorChannelUrl": "http://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ", "authorChannelId": { "value": "UCa-vrCLQHviTOVnEKDOdetQ" }, "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2022-11-15T02:20:01Z", "updatedAt": "2022-11-15T02:20:01Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } } ================================================ FILE: testdata/apidata/comments/comments_by_parent_paged_1.json ================================================ { "kind": "youtube#commentListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/WG_t4MaQHNzJKOhVkfr0prByYcE\"", "nextPageToken": "R0FJeVZnbzBJTl9zNXRxNXlPWUNNaWtRQUJpQ3RNeW4wcFBtQWlBQktBTXdDam9XT1RGNlZETmpXV0kxUWpJNU1YcGhOV1ZLZUhwek1SSWVDQVVTR2xWbmR6VjZXVlUyYmpsd2JVbG5RVnBYZGs0MFFXRkJRa0ZuT2lBSUFSSWNOVHBWWjNjMWVsbFZObTQ1Y0cxSlowRmFWM1pPTkVGaFFVSkJadw==", "pageInfo": { "resultsPerPage": 2 }, "items": [ { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/MKybcC4eKVsCy4dNSdJpsCB2f9I\"", "id": "Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za6voUoRh", "snippet": { "authorDisplayName": "kun liu", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw", "authorChannelId": { "value": "UCNvMBmCASzTNNX8lW3JRMbw" }, "textDisplay": "this is the third reply!", "textOriginal": "this is the third reply!", "parentId": "Ugw5zYU6n9pmIgAZWvN4AaABAg", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-12-01T04:46:31.000Z", "updatedAt": "2019-12-01T04:46:31.000Z" } }, { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Hnby-TPwSCvcPfoqxAwocl44Ijw\"", "id": "Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za5eJxzs1", "snippet": { "authorDisplayName": "kun liu", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw", "authorChannelId": { "value": "UCNvMBmCASzTNNX8lW3JRMbw" }, "textDisplay": "this is the second reply!", "textOriginal": "this is the second reply!", "parentId": "Ugw5zYU6n9pmIgAZWvN4AaABAg", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-12-01T04:46:20.000Z", "updatedAt": "2019-12-01T04:46:20.000Z" } } ] } ================================================ FILE: testdata/apidata/comments/comments_by_parent_paged_2.json ================================================ { "kind": "youtube#commentListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/pCAXgQBtRsyN8PLsTBsg6O6diuY\"", "pageInfo": { "resultsPerPage": 2 }, "items": [ { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/0CMWJ6jK5tTSWIBuH6KQLYVM9xI\"", "id": "Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za3E-eR-l", "snippet": { "authorDisplayName": "kun liu", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw", "authorChannelId": { "value": "UCNvMBmCASzTNNX8lW3JRMbw" }, "textDisplay": "hey, this is the replay", "textOriginal": "hey, this is the replay", "parentId": "Ugw5zYU6n9pmIgAZWvN4AaABAg", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-12-01T04:46:00.000Z", "updatedAt": "2019-12-01T04:46:00.000Z" } } ] } ================================================ FILE: testdata/apidata/comments/comments_multi.json ================================================ { "kind": "youtube#commentListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/KGqYDVMiabGgL8yNEbUJOwtrsqY\"", "items": [ { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/N8dTotfuHXvcw1YZ3SZNWPHYxvM\"", "id": "UgyUBI0HsgL9emxcZpR4AaABAg", "snippet": { "authorDisplayName": "DevRagz", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw", "authorChannelId": { "value": "UCTWlvHQQXUs-4IVfdnGOUbw" }, "textDisplay": "Hello", "textOriginal": "Hello", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-06-23T02:49:00.000Z", "updatedAt": "2019-06-23T02:49:00.000Z" } }, { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/U1u_QcNKFwxVznA5x7CNZ9nXECc\"", "id": "Ugzi3lkqDPfIOirGFLh4AaABAg", "snippet": { "authorDisplayName": "EclipZe Muzik", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7-JV7pt6X2WsxOdaD6nzK_rAj_2FVAjLyNR1Q=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCCuQ3wts9ASkk0qZJyEjYbw", "authorChannelId": { "value": "UCCuQ3wts9ASkk0qZJyEjYbw" }, "textDisplay": "exceptional content!", "textOriginal": "exceptional content!", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2018-05-26T20:11:30.000Z", "updatedAt": "2018-05-26T20:11:30.000Z" } } ] } ================================================ FILE: testdata/apidata/comments/comments_single.json ================================================ { "kind": "youtube#commentListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/A2nkVdtP_3bQZi4INK9lZ7XTYXs\"", "items": [ { "kind": "youtube#comment", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/N8dTotfuHXvcw1YZ3SZNWPHYxvM\"", "id": "UgyUBI0HsgL9emxcZpR4AaABAg", "snippet": { "authorDisplayName": "DevRagz", "authorProfileImageUrl": "https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo", "authorChannelUrl": "http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw", "authorChannelId": { "value": "UCTWlvHQQXUs-4IVfdnGOUbw" }, "textDisplay": "Hello", "textOriginal": "Hello", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-06-23T02:49:00.000Z", "updatedAt": "2019-06-23T02:49:00.000Z" } } ] } ================================================ FILE: testdata/apidata/comments/insert_response.json ================================================ { "kind": "youtube#comment", "etag": "lTl2Wjqipb6KqrmPU04DLigrzrg", "id": "Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt", "snippet": { "textDisplay": "wow", "textOriginal": "wow", "parentId": "Ugy_CAftKrIUCyPr9GR4AaABAg", "authorDisplayName": "ikaros data", "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s48-c-k-c0x00ffffff-no-rj", "authorChannelUrl": "http://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ", "authorChannelId": { "value": "UCa-vrCLQHviTOVnEKDOdetQ" }, "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2022-11-14T10:23:02Z", "updatedAt": "2022-11-14T10:23:02Z" } } ================================================ FILE: testdata/apidata/error_permission_resp.json ================================================ { "error": { "code": 403, "message": "The caller does not have permission", "errors": [ { "message": "Permission denied.", "domain": "youtube.CoreErrorDomain", "reason": "SERVICE_UNAVAILABLE" } ], "status": "PERMISSION_DENIED" } } ================================================ FILE: testdata/apidata/i18ns/language_res.json ================================================ { "kind": "youtube#i18nLanguageListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/qgFy24yvs-L_dNjr2d-Rd_Xcfw4\"", "items": [ { "kind": "youtube#i18nLanguage", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/wD2SRT6G7gbFH07ePlumHAynSRo\"", "id": "zh-CN", "snippet": { "hl": "zh-CN", "name": "Chinese" } }, { "kind": "youtube#i18nLanguage", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/6fRre896XUzNQjc89q329PaFKjE\"", "id": "zh-TW", "snippet": { "hl": "zh-TW", "name": "Chinese (Taiwan)" } }, { "kind": "youtube#i18nLanguage", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/MbipoDEFiRRUlYr5UzjpCwXXRMc\"", "id": "zh-HK", "snippet": { "hl": "zh-HK", "name": "Chinese (Hong Kong)" } }, { "kind": "youtube#i18nLanguage", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/XBmnabKsnR1WSWcZvNxC2NvrLYo\"", "id": "ja", "snippet": { "hl": "ja", "name": "Japanese" } }, { "kind": "youtube#i18nLanguage", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/l06bH0pLscIVm87oyBnJ3aZR4Ts\"", "id": "ko", "snippet": { "hl": "ko", "name": "Korean" } } ] } ================================================ FILE: testdata/apidata/i18ns/regions_res.json ================================================ { "kind": "youtube#i18nRegionListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/q85_wZeDyKDzYtt-LhNaozyi_sk\"", "items": [ { "kind": "youtube#i18nRegion", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/VLBm14P6cRurVqVIS2Z9SfyDJdU\"", "id": "VE", "snippet": { "gl": "VE", "name": "Venezuela" } }, { "kind": "youtube#i18nRegion", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/PecEyDpgDPYWfvmuJVxuVgwxQwU\"", "id": "VN", "snippet": { "gl": "VN", "name": "Vietnam" } }, { "kind": "youtube#i18nRegion", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Pzd0Bx5oG8rW9CkFMKHP0X12ywM\"", "id": "YE", "snippet": { "gl": "YE", "name": "Yemen" } }, { "kind": "youtube#i18nRegion", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/u5oySmRytTPqfZBg8zIE7G7jvRs\"", "id": "ZW", "snippet": { "gl": "ZW", "name": "Zimbabwe" } } ] } ================================================ FILE: testdata/apidata/members/members_data.json ================================================ { "kind": "youtube#memberListResponse", "etag": "etag", "pageInfo": { "totalResults": 2, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#member", "etag": "etag", "snippet": { "creatorChannelId": "UCa-vrCLQHviTOVnEKDOdetQ", "memberDetails": { "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "channelUrl": "https://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ", "displayName": "ikaros-life", "profileImageUrl": "https://yt3.ggpht.com/a-/AOh14Gg1_gYcI03VLDd3FMLUY5cb5O9RC9sElj26-1SR=s288-c-k-c0xffffffff-no-rj-mo" }, "membershipsDetails": { "highestAccessibleLevel": "string", "highestAccessibleLevelDisplayName": "string", "accessibleLevels": [ "string" ], "membershipsDuration": { "memberSince": "2007-08-23T00:34:43Z", "memberTotalDurationMonths": 5 }, "membershipsDurationAtLevel": [ { "level": "string", "memberSince": "2007-08-23T00:34:43Z", "memberTotalDurationMonths": 6 } ] } } }, { "kind": "youtube#member", "etag": "etag", "snippet": { "creatorChannelId": "UCa-vrCLQHviTOVnEKDOdet1", "memberDetails": { "channelId": "UCa-vrCLQHviTOVnEKDOdet1", "channelUrl": "https://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdet1", "displayName": "ikaros-life--1", "profileImageUrl": "https://yt3.ggpht.com/a-/AOh14Gg1_gYcI03VLDd3FMLUY5cb5O9RC9sElj26-1SR=s288-c-k-c0xffffffff-no-rj-mo" }, "membershipsDetails": { "highestAccessibleLevel": "string", "highestAccessibleLevelDisplayName": "string", "accessibleLevels": [ "string" ], "membershipsDuration": { "memberSince": "2007-08-23T00:34:43Z", "memberTotalDurationMonths": 5 }, "membershipsDurationAtLevel": [ { "level": "string", "memberSince": "2007-08-23T00:34:43Z", "memberTotalDurationMonths": 6 } ] } } } ] } ================================================ FILE: testdata/apidata/members/membership_levels.json ================================================ { "kind": "youtube#membershipsLevelListResponse", "etag": "etag", "items": [ { "kind": "youtube#membershipsLevel", "etag": "etag", "id": "id", "snippet": { "creatorChannelId": "UCa-vrCLQHviTOVnEKDOdetQ", "levelDetails": { "displayName": "high" } } }, { "kind": "youtube#membershipsLevel", "etag": "etag", "id": "id", "snippet": { "creatorChannelId": "UCa-vrCLQHviTOVnEKDOdetQ", "levelDetails": { "displayName": "low" } } } ] } ================================================ FILE: testdata/apidata/playlist_items/insert_response.json ================================================ { "kind": "youtube#playlistItem", "etag": "4Bl2u6s8N1Jkkz1AHN4E-tw4OQQ", "id": "UExCYWlkdDBpbENNYW5HRElLcjhVVkJGWndOX1V2TUt2Uy4wMTcyMDhGQUE4NTIzM0Y5", "snippet": { "publishedAt": "2022-11-15T13:38:09Z", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "Lecture 6: Version Control (git) (2020)", "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/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/2sjqTHE0zok/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/2sjqTHE0zok/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/2sjqTHE0zok/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/2sjqTHE0zok/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/2sjqTHE0zok/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "ikaros data", "playlistId": "PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS", "position": 0, "resourceId": { "kind": "youtube#video", "videoId": "2sjqTHE0zok" }, "videoOwnerChannelTitle": "Missing Semester", "videoOwnerChannelId": "UCuXy5tCgEninup9cGplbiFw" } } ================================================ FILE: testdata/apidata/playlist_items/playlist_items_filter_video.json ================================================ { "kind": "youtube#playlistItemListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/L2uYevt91mZBw1hKeHio9_Aamz8\"", "pageInfo": { "totalResults": 1, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Kib3kvf3c_Bq79UyVpa2pHYzV_U\"", "id": "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4wMTcyMDhGQUE4NTIzM0Y5", "snippet": { "publishedAt": "2019-05-11T00:55:44.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google I/O'19 - I/O Live (Day 3 Composite)", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/VCv-KKIkLns/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/VCv-KKIkLns/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/VCv-KKIkLns/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/VCv-KKIkLns/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/VCv-KKIkLns/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "position": 2, "resourceId": { "kind": "youtube#video", "videoId": "VCv-KKIkLns" } } } ] } ================================================ FILE: testdata/apidata/playlist_items/playlist_items_multi.json ================================================ { "kind": "youtube#playlistItemListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Rwbdfm4jH2tQT1_c-Jxyy8qh2Xs\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/E5rTjxNaKfzDc-GFs2Cb9jkKlGM\"", "id": "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2", "snippet": { "publishedAt": "2019-05-11T00:27:38.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google I/O'19 - I/O Live (Day 1 Composite)", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "position": 0, "resourceId": { "kind": "youtube#video", "videoId": "H1HZyvc0QnI" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/3JqJ3Bv7ZIVEu4ZoeH6ZGsUe7js\"", "id": "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4yODlGNEE0NkRGMEEzMEQy", "snippet": { "publishedAt": "2019-05-11T00:52:10.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google I/O'19 - I/O Live (Day 2 Composite)", "description": "Relive moments from I/O Live, Day 2, 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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "position": 1, "resourceId": { "kind": "youtube#video", "videoId": "5NgsfxIWNls" } } } ] } ================================================ FILE: testdata/apidata/playlist_items/playlist_items_paged_1.json ================================================ { "kind": "youtube#playlistItemListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/P4kQiZH0w8ID3LMajA0LjTQAypg\"", "nextPageToken": "CAoQAA", "pageInfo": { "totalResults": 13, "resultsPerPage": 10 }, "items": [ { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Qq9nexxDsu-O8I8Z-OYt-i7VhGw\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2", "snippet": { "publishedAt": "2019-11-21T22:17:43.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Welcome from Google Developers - Pittsburgh ML Summit ‘19", "description": "Kyle Paul, Regional Lead of North America at Google, opens the ML ‘19 Summit in Pittsburgh, PA with a warm welcome and talks about the Developer Ecosystems team and programs that are at Google. Join us for some great talks, event goals, and more! \n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/CvTApw9X8aA/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/CvTApw9X8aA/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/CvTApw9X8aA/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/CvTApw9X8aA/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/CvTApw9X8aA/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 0, "resourceId": { "kind": "youtube#video", "videoId": "CvTApw9X8aA" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/szvAfCSYgJYm3LDxkSSrYhC0WrM\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy4yODlGNEE0NkRGMEEzMEQy", "snippet": { "publishedAt": "2019-11-23T00:14:16.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Conversation AI - Pittsburgh ML Summit ‘19", "description": "Priyanka Vergadia, Developer Advocate for Google Cloud, talks about why conversational experiences can fail. Learn about systems and some pointers to build a great conversational experience.\n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Yh4EKaUY3gg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Yh4EKaUY3gg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Yh4EKaUY3gg/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/Yh4EKaUY3gg/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/Yh4EKaUY3gg/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 1, "resourceId": { "kind": "youtube#video", "videoId": "Yh4EKaUY3gg" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/SoE9Vp7LPueb-lVLD4JEnLvQyxY\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy4wMTcyMDhGQUE4NTIzM0Y5", "snippet": { "publishedAt": "2019-11-23T00:20:06.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Reinforcement Learning with TensorFlow and Unity - Pittsburgh ML Summit ‘19", "description": "Dan Goncharov, Head of 42 Robotics GDG Fremont, talks about Reinforcement Learning with TensorFlow and Unity. Also, see an overview of Machine Learning in general. Learn about all 3 types of ML and some examples of each and which is the most popular.\n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/S-MbpQiwfls/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/S-MbpQiwfls/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/S-MbpQiwfls/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/S-MbpQiwfls/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/S-MbpQiwfls/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 2, "resourceId": { "kind": "youtube#video", "videoId": "S-MbpQiwfls" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/mTmJckP7mIexrzn524dGpHfwpPA\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41MjE1MkI0OTQ2QzJGNzNG", "snippet": { "publishedAt": "2019-11-23T00:25:29.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Art + AI - Pittsburgh ML Summit ‘19", "description": "Victor Dibia, ML Research Engineer at Cloudera Fast Forward Labs, talks about what he does as GDE both at work and for the community. He combines his love of art using Generative Adversarial Networks. Join us and see how these two roads intersect.\n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/BBjVl1EETb0/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/BBjVl1EETb0/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/BBjVl1EETb0/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/BBjVl1EETb0/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/BBjVl1EETb0/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 3, "resourceId": { "kind": "youtube#video", "videoId": "BBjVl1EETb0" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/QJ09nQXlszEGNjJA4zMFr6KKXWE\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy4wOTA3OTZBNzVEMTUzOTMy", "snippet": { "publishedAt": "2019-11-23T00:28:46.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Code-Free probing of Machine Learning models - Pittsburgh ML Summit ‘19", "description": "Tolga Bolukbasi, Software Engineer, talks about his research and the tools he builds for Machine Learning model understanding and fairness. Learn about the company he works for, PAIR, and their mission in AI.\n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/h8CnO-oqqSs/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/h8CnO-oqqSs/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/h8CnO-oqqSs/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/h8CnO-oqqSs/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/h8CnO-oqqSs/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 4, "resourceId": { "kind": "youtube#video", "videoId": "h8CnO-oqqSs" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/oKKyBUNjbGZqwxRylpicJtxpL1w\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy4xMkVGQjNCMUM1N0RFNEUx", "snippet": { "publishedAt": "2019-11-23T00:32:55.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Neural Query Language - Pittsburgh ML Summit ‘19", "description": "William Cohen, Principal Scientist at Google, discusses the project of Neural Query Language and the differentiable KG queries in TensorFlow. \n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/2cHddIA4EvY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/2cHddIA4EvY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/2cHddIA4EvY/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/2cHddIA4EvY/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/2cHddIA4EvY/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 5, "resourceId": { "kind": "youtube#video", "videoId": "2cHddIA4EvY" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/B3KKp2wby9fWhYeq0Ho4GaF5hKE\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41MzJCQjBCNDIyRkJDN0VD", "snippet": { "publishedAt": "2019-11-23T00:35:29.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "The future of Mobile Learning - Pittsburgh ML Summit ‘19", "description": "Jingtao Wang, Research Scientist at Google, discusses the future of Mobile Learning during this talk. \n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/kTJUt9CmTXg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/kTJUt9CmTXg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/kTJUt9CmTXg/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/kTJUt9CmTXg/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/kTJUt9CmTXg/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 6, "resourceId": { "kind": "youtube#video", "videoId": "kTJUt9CmTXg" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/DhloJevNp-LgUKvIK-6UyahOQVQ\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy5DQUNERDQ2NkIzRUQxNTY1", "snippet": { "publishedAt": "2019-11-23T00:37:49.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Inclusive AI - Pittsburgh ML Summit ‘19", "description": "Eve Andersson, Director of ML Fairness and Accessibility Engineering at Google, discusses artificial intelligence and accessibility during this talk. \n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/o7_oJYZw2Hg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/o7_oJYZw2Hg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/o7_oJYZw2Hg/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/o7_oJYZw2Hg/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/o7_oJYZw2Hg/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 7, "resourceId": { "kind": "youtube#video", "videoId": "o7_oJYZw2Hg" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/pG62VWLcupRiuy-bO1NuaVKby3I\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy45NDk1REZENzhEMzU5MDQz", "snippet": { "publishedAt": "2019-11-23T00:40:19.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Serverless & AI/ML - Pittsburgh ML Summit ‘19", "description": "Charles Baer, Solutions Architect for Google Cloud, discusses solutions for serverless artificial intelligence and machine learning during this talk. \n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/vN6uK5Qm23c/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/vN6uK5Qm23c/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/vN6uK5Qm23c/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/vN6uK5Qm23c/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/vN6uK5Qm23c/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 8, "resourceId": { "kind": "youtube#video", "videoId": "vN6uK5Qm23c" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/7QBFXcO5f-kiWNCzM-byiACxvzw\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy5GNjNDRDREMDQxOThCMDQ2", "snippet": { "publishedAt": "2019-11-23T00:42:56.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Convolutional neural networks with Swift - Pittsburgh ML Summit ‘19", "description": "Brett Koonce, CTO at Quarkworks and Google Developers Expert, discusses convolutional neural networks with Swift and Python.\n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/o5LP2xzKkpg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/o5LP2xzKkpg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/o5LP2xzKkpg/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/o5LP2xzKkpg/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/o5LP2xzKkpg/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 9, "resourceId": { "kind": "youtube#video", "videoId": "o5LP2xzKkpg" } } } ] } ================================================ FILE: testdata/apidata/playlist_items/playlist_items_paged_2.json ================================================ { "kind": "youtube#playlistItemListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Mdch507Btu8hjUyr5r5qJbQgL1Y\"", "prevPageToken": "CAoQAQ", "pageInfo": { "totalResults": 13, "resultsPerPage": 10 }, "items": [ { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/SQwmGD19cBQb5iwOedC6-LApUEo\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy40NzZCMERDMjVEN0RFRThB", "snippet": { "publishedAt": "2019-11-23T00:45:05.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Vizier: Black-box optimization and AutoML - Pittsburgh ML Summit ‘19", "description": "Daniel Golovin, Software Engineer for Google Brain, will be talking about Vizier: a project on black-box optimization and AutoML.\n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/6aSG8SdvkoU/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/6aSG8SdvkoU/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/6aSG8SdvkoU/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/6aSG8SdvkoU/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/6aSG8SdvkoU/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 10, "resourceId": { "kind": "youtube#video", "videoId": "6aSG8SdvkoU" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/vDhnNoD_Q3xyw2m9CBl4hkab4cI\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy5EMEEwRUY5M0RDRTU3NDJC", "snippet": { "publishedAt": "2019-11-23T00:48:01.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Panel discussion - Pittsburgh ML Summit ‘19", "description": "Panel discussion at Pittsburgh ML Summit ‘19 featuring Jingtao Wang, Daniel Golovin and Boya Sun. \n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/ECGpZCFUL8s/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/ECGpZCFUL8s/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/ECGpZCFUL8s/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/ECGpZCFUL8s/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/ECGpZCFUL8s/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 11, "resourceId": { "kind": "youtube#video", "videoId": "ECGpZCFUL8s" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/gtg7bcS_SDwOLHtZwPMU8OBxAh4\"", "id": "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy45ODRDNTg0QjA4NkFBNkQy", "snippet": { "publishedAt": "2019-11-23T00:49:59.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Closing remarks - Pittsburgh ML Summit ‘19", "description": "Kyle Paul, Regional Lead of North America for Google, with the closing remarks for the Pittsburgh ML Summit ‘19.\n\nBeyond Big Data: AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field. \n\nWatch more Pittsburgh ML Summit ‘19 → https://goo.gle/339pgbR\nSubscribe to Google Developers → https://goo.gle/developers", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Fu2bldybpkA/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Fu2bldybpkA/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Fu2bldybpkA/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/Fu2bldybpkA/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/Fu2bldybpkA/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "position": 12, "resourceId": { "kind": "youtube#video", "videoId": "Fu2bldybpkA" } } } ] } ================================================ FILE: testdata/apidata/playlist_items/playlist_items_single.json ================================================ { "kind": "youtube#playlistItemListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/YVsjtCTnqysTcNJy7jZglowaNYM\"", "pageInfo": { "totalResults": 1, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/E5rTjxNaKfzDc-GFs2Cb9jkKlGM\"", "id": "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2", "snippet": { "publishedAt": "2019-05-11T00:27:38.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google I/O'19 - I/O Live (Day 1 Composite)", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "position": 0, "resourceId": { "kind": "youtube#video", "videoId": "H1HZyvc0QnI" } } } ] } ================================================ FILE: testdata/apidata/playlists/insert_response.json ================================================ { "kind": "youtube#playlist", "etag": "Gw0SW_V3Hy1XNqjAJB1v1Q0ZmB4", "id": "PLBaidt0ilCMZN8XPVB5iXY6FlSYGeyn2n", "snippet": { "publishedAt": "2022-11-16T04:12:59Z", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "Test playlist", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/img/no_thumbnail.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/img/no_thumbnail.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/img/no_thumbnail.jpg", "width": 480, "height": 360 } }, "channelTitle": "ikaros data", "localized": { "title": "Test playlist", "description": "" } } } ================================================ FILE: testdata/apidata/playlists/playlists_mine.json ================================================ { "kind": "youtube#playlistListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/LjFPI2nwRBxtabydhze2pwRj5LI\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/9CjYT6fC317YBYj1iRh_oZ3v27U\"", "id": "PLOU2XLYxmsIIOSO0eWuj-6yQmdakarUzN", "snippet": { "publishedAt": "2019-05-02T23:38:49.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Accessibility at Google I/O 2019", "description": "This playlist contains every Accessibiity session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://www.google.com/accessibility/for-developers/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Accessibility at Google I/O 2019", "description": "This playlist contains every Accessibiity session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://www.google.com/accessibility/for-developers/" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/V5b6wvRkyEXDBtrgywP5vk-SnuA\"", "id": "PLOU2XLYxmsIJ5Bl3HmuxKY5WE555cu9Uc", "snippet": { "publishedAt": "2019-05-02T23:38:06.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Inspiration at Google I/O 2019", "description": "This playlist contains every Inspirational session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/qL4U9Ygtxh8/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/qL4U9Ygtxh8/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/qL4U9Ygtxh8/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/qL4U9Ygtxh8/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/qL4U9Ygtxh8/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Inspiration at Google I/O 2019", "description": "This playlist contains every Inspirational session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/" } } } ] } ================================================ FILE: testdata/apidata/playlists/playlists_multi.json ================================================ { "kind": "youtube#playlistListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/tANZNtv_p2OkP0sqKDLjKwJQ4Cs\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/gdl_m86JfWCd37Wtcc2dte9hrEg\"", "id": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "snippet": { "publishedAt": "2019-05-10T00:18:56.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "I/O Live - Show Composite", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "I/O Live - Show Composite", "description": "" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/FChLS9OtNbF2YcyhMyWgTHL7VLg\"", "id": "PLOU2XLYxmsIJJVnHWmd1qfr0Caq4VZCu4", "snippet": { "publishedAt": "2019-05-10T00:18:07.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "I/O Live", "description": "Relive moments from Google I/O 2019", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/o-2RMd6WOg8/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/o-2RMd6WOg8/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/o-2RMd6WOg8/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/o-2RMd6WOg8/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/o-2RMd6WOg8/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "I/O Live", "description": "Relive moments from Google I/O 2019" } } } ] } ================================================ FILE: testdata/apidata/playlists/playlists_paged_1.json ================================================ { "kind": "youtube#playlistListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/jGXEiDmTtAs7vTBHF90lfHs9yOA\"", "nextPageToken": "CAoQAA", "pageInfo": { "totalResults": 422, "resultsPerPage": 10 }, "items": [ { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/z8ateNaRCjA2yCsvti1OTzDIvHo\"", "id": "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "snippet": { "publishedAt": "2019-11-21T22:17:38.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Pittsburgh ML Summit ‘19", "description": "AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/CvTApw9X8aA/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/CvTApw9X8aA/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/CvTApw9X8aA/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/CvTApw9X8aA/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/CvTApw9X8aA/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Pittsburgh ML Summit ‘19", "description": "AI/ML Summit is a unique opportunity for managers on every level to learn more about the opportunities of these technologies, while connecting with others in the industry. With a focus on trends and best practices, the event aims to explore strategies, best practices and technologies surrounding data analysis, artificial intelligence and machine learning while keeping in mind the implications of regulations, privacy, data protection, and ethics that govern this field." } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/8kvRpRGgdJloFkUOu_S12Vy00E4\"", "id": "PLOU2XLYxmsIJO83u2UmyC8ud41AvUnhgj", "snippet": { "publishedAt": "2019-11-18T20:00:47.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Kirkland ML Summit '19", "description": "The Kirkland ML Summit brings together developers from across the globe to discuss recent developments and get the latest news on everything Machine Learning. Join our many sessions to keep up with what’s going on in the Machine Learning world.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/c2gJxZ1Qa4k/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/c2gJxZ1Qa4k/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/c2gJxZ1Qa4k/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/c2gJxZ1Qa4k/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/c2gJxZ1Qa4k/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Kirkland ML Summit '19", "description": "The Kirkland ML Summit brings together developers from across the globe to discuss recent developments and get the latest news on everything Machine Learning. Join our many sessions to keep up with what’s going on in the Machine Learning world." } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ewmspkP9LrOg8_2nd1e71xcr0u8\"", "id": "PLOU2XLYxmsILfV1LiUhDjbh1jkFjQWrYB", "snippet": { "publishedAt": "2019-11-07T22:55:11.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google Developers Experts", "description": "A global program to recognize individuals who are experts and thought leaders in one or more Google technologies. These professionals actively contribute and support the developer and startup ecosystems around the world, helping them build and launch highly innovative apps.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/kFjxIgIgW80/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/kFjxIgIgW80/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/kFjxIgIgW80/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/kFjxIgIgW80/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/kFjxIgIgW80/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Google Developers Experts", "description": "A global program to recognize individuals who are experts and thought leaders in one or more Google technologies. These professionals actively contribute and support the developer and startup ecosystems around the world, helping them build and launch highly innovative apps." } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/c9OQR20t2PJhtUIJw79bb7CgGrU\"", "id": "PLOU2XLYxmsIKNr3Wfhm8o0TSojW7hEPPY", "snippet": { "publishedAt": "2019-10-01T20:57:43.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Cambridge ML Summit '19", "description": "Google Developers ML Summit '19 brings together industry professionals in Machine Learning and Artificial Intelligence. If you already work in the ML/AI field, and you are interested in enhancing your skills, while networking and learning from Google's ML/AI experts, this Summit is for you.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/KWefSoJDja8/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/KWefSoJDja8/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/KWefSoJDja8/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/KWefSoJDja8/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/KWefSoJDja8/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Cambridge ML Summit '19", "description": "Google Developers ML Summit '19 brings together industry professionals in Machine Learning and Artificial Intelligence. If you already work in the ML/AI field, and you are interested in enhancing your skills, while networking and learning from Google's ML/AI experts, this Summit is for you." } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/3Q4DeS3RXGlkoRoaDjRTzgP8hl0\"", "id": "PLOU2XLYxmsIJ8ItHmK4bRlY4GCzMgXLAJ", "snippet": { "publishedAt": "2019-09-27T22:16:57.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "#MyDomain", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/RiqYfiKU1Zo/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/RiqYfiKU1Zo/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/RiqYfiKU1Zo/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/RiqYfiKU1Zo/sddefault.jpg", "width": 640, "height": 480 } }, "channelTitle": "Google Developers", "localized": { "title": "#MyDomain", "description": "" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/wFrhKcRFkwQd7ocSg8Hy1VXnBnQ\"", "id": "PLOU2XLYxmsIIra6qDjAHl63jlWbq5me8i", "snippet": { "publishedAt": "2019-09-23T22:52:15.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "North America DSC Summit 2019", "description": "Developer Student Clubs (DSC) are community groups for students from any academic background in their undergraduate or graduate term. By joining a DSC, students build their professional and personal network, get access to Google developer resources, and work together to build solutions for local problems in a peer-to-peer learning environment.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/aiMb0sNmpdY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/aiMb0sNmpdY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/aiMb0sNmpdY/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/aiMb0sNmpdY/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/aiMb0sNmpdY/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "North America DSC Summit 2019", "description": "Developer Student Clubs (DSC) are community groups for students from any academic background in their undergraduate or graduate term. By joining a DSC, students build their professional and personal network, get access to Google developer resources, and work together to build solutions for local problems in a peer-to-peer learning environment." } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ytuOSx4rjsuY7yHeez5VCA7dGw4\"", "id": "PLOU2XLYxmsILHvpAkROp2dXz-jQi4S4_y", "snippet": { "publishedAt": "2019-09-11T22:29:53.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Introduction to ARCore Augmented Faces", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/8ih7eHwPoxM/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/8ih7eHwPoxM/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/8ih7eHwPoxM/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/8ih7eHwPoxM/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/8ih7eHwPoxM/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Introduction to ARCore Augmented Faces", "description": "" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/NvwWi2jAzPFc884j9G1IPW2_YIA\"", "id": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "snippet": { "publishedAt": "2019-05-10T00:18:56.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "I/O Live - Show Composite", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "I/O Live - Show Composite", "description": "" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/WdUKh80tT13sUtVr5ZIFMGc8M5I\"", "id": "PLOU2XLYxmsIJJVnHWmd1qfr0Caq4VZCu4", "snippet": { "publishedAt": "2019-05-10T00:18:07.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "I/O Live", "description": "Relive moments from Google I/O 2019", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/UVOhgly2VEc/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/UVOhgly2VEc/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/UVOhgly2VEc/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/UVOhgly2VEc/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/UVOhgly2VEc/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "I/O Live", "description": "Relive moments from Google I/O 2019" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/s7ZgGwVG5R5qa-Sxi9I4xeeVI3k\"", "id": "PLOU2XLYxmsIKW-llcbcFdpR9RjCfYHZaV", "snippet": { "publishedAt": "2019-05-02T23:39:42.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Machine Learning at Google I/O 2019", "description": "This playlist contains every Machine Learning session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/machine-learning/guides/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/pM9u9xcM_cs/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/pM9u9xcM_cs/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/pM9u9xcM_cs/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/pM9u9xcM_cs/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/pM9u9xcM_cs/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Machine Learning at Google I/O 2019", "description": "This playlist contains every Machine Learning session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/machine-learning/guides/" } } } ] } ================================================ FILE: testdata/apidata/playlists/playlists_paged_2.json ================================================ { "kind": "youtube#playlistListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/LjFPI2nwRBxtabydhze2pwRj5LI\"", "prevPageToken": "CAoQAQ", "pageInfo": { "totalResults": 422, "resultsPerPage": 10 }, "items": [ { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/9CjYT6fC317YBYj1iRh_oZ3v27U\"", "id": "PLOU2XLYxmsIIOSO0eWuj-6yQmdakarUzN", "snippet": { "publishedAt": "2019-05-02T23:38:49.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Accessibility at Google I/O 2019", "description": "This playlist contains every Accessibiity session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://www.google.com/accessibility/for-developers/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Accessibility at Google I/O 2019", "description": "This playlist contains every Accessibiity session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://www.google.com/accessibility/for-developers/" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/V5b6wvRkyEXDBtrgywP5vk-SnuA\"", "id": "PLOU2XLYxmsIJ5Bl3HmuxKY5WE555cu9Uc", "snippet": { "publishedAt": "2019-05-02T23:38:06.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Inspiration at Google I/O 2019", "description": "This playlist contains every Inspirational session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/qL4U9Ygtxh8/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/qL4U9Ygtxh8/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/qL4U9Ygtxh8/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/qL4U9Ygtxh8/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/qL4U9Ygtxh8/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Inspiration at Google I/O 2019", "description": "This playlist contains every Inspirational session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/7N7BKt6QAueeE4_8csdQyUXvhdY\"", "id": "PLOU2XLYxmsIK1qyYq0gzScqXT8JmHQLs4", "snippet": { "publishedAt": "2019-05-02T23:37:27.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Gaming at Google I/O 2019", "description": "This playlist contains every gaming session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/games/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/RY7wXC_b0R8/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/RY7wXC_b0R8/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/RY7wXC_b0R8/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/RY7wXC_b0R8/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/RY7wXC_b0R8/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Gaming at Google I/O 2019", "description": "This playlist contains every gaming session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/games/" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/jcbtyLPKyuXpapzBMBE5rr0VfEU\"", "id": "PLOU2XLYxmsIIICsWVglOZDtXpK7HLYswc", "snippet": { "publishedAt": "2019-05-02T23:35:47.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "IoT at Google I/O 2019", "description": "This playlist contains every IoT session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/iot/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/QRzvINzJTyQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/QRzvINzJTyQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/QRzvINzJTyQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/QRzvINzJTyQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/QRzvINzJTyQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "IoT at Google I/O 2019", "description": "This playlist contains every IoT session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/iot/" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ykZ3hvrD6AmkKbNvXwpeTV_hYYI\"", "id": "PLOU2XLYxmsIK5JcIMUhrPH7T0lHBrKbER", "snippet": { "publishedAt": "2019-05-02T23:33:43.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Assistant at Google I/O 2019", "description": "This playlist contains every Google Assistant session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/actions/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/dpNrq_wiqGs/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/dpNrq_wiqGs/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/dpNrq_wiqGs/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/dpNrq_wiqGs/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/dpNrq_wiqGs/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Assistant at Google I/O 2019", "description": "This playlist contains every Google Assistant session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/actions/" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/xzIksSFchIg10f12PMD5bo5mmkk\"", "id": "PLOU2XLYxmsILVTiOlMJdo7RQS55jYhsMi", "snippet": { "publishedAt": "2019-05-01T00:43:22.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google I/O 2019 - All Sessions", "description": "This playlist contains every session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/lyRPyRKHO8M/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/lyRPyRKHO8M/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/lyRPyRKHO8M/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/lyRPyRKHO8M/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/lyRPyRKHO8M/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Google I/O 2019 - All Sessions", "description": "This playlist contains every session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/46hFFhvAepPrQMRR8dWjWyLB5zE\"", "id": "PLOU2XLYxmsIKP4Hh9gQO54naZ8V7mDEQi", "snippet": { "publishedAt": "2019-04-30T21:37:13.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Behind the Actions", "description": "In this Behind the Actions series, you will learn how to build, test, and publish Actions on Google.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/plr65MD-FBY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/plr65MD-FBY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/plr65MD-FBY/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/plr65MD-FBY/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/plr65MD-FBY/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Behind the Actions", "description": "In this Behind the Actions series, you will learn how to build, test, and publish Actions on Google." } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ZLqa7_rX9Czyvo5-8kj9d1QgARE\"", "id": "PLOU2XLYxmsIKORZ6mM3fQ02gKjqvSa2yd", "snippet": { "publishedAt": "2019-04-09T18:18:59.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "How I Code", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/6GMs_S3XIys/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/6GMs_S3XIys/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/6GMs_S3XIys/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/6GMs_S3XIys/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/6GMs_S3XIys/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "How I Code", "description": "" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/BXfcSFdRnnKPVwAAcTZOX9ffEZU\"", "id": "PLOU2XLYxmsILU6mHf5ERbUBpvKX6GL4rn", "snippet": { "publishedAt": "2019-03-22T00:05:51.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Assistant on Air", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Assistant on Air", "description": "" } } }, { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/OqmLB5ZBwHPYbJHQZa8ImdBv9RU\"", "id": "PLOU2XLYxmsIIfaDH4-GcZzWIPFyD7Lpt3", "snippet": { "publishedAt": "2019-03-18T16:48:03.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google @ Game Developers Conference 2019", "description": "Learn more at http://bit.ly/GDC19Google \n\nMOBILE DEVELOPER DAY | 3/18*\nJoin us for a full day of sessions covering tools and best practices to help build a successful mobile games business on Google Play. We’ll focus on game quality, effective monetization and growth strategies, and how to create, connect, and scale with Google.\n\nGOOGLE REVEAL | 3/19*\nAll will be revealed at the Google Keynote.\n\nCLOUD DEVELOPER DAY | 3/20*\nJoin Google Cloud and some of our key partners to learn more about the latest innovations in cloud technology for games.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/nUih5C5rOrA/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/nUih5C5rOrA/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/nUih5C5rOrA/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "localized": { "title": "Google @ Game Developers Conference 2019", "description": "Learn more at http://bit.ly/GDC19Google \n\nMOBILE DEVELOPER DAY | 3/18*\nJoin us for a full day of sessions covering tools and best practices to help build a successful mobile games business on Google Play. We’ll focus on game quality, effective monetization and growth strategies, and how to create, connect, and scale with Google.\n\nGOOGLE REVEAL | 3/19*\nAll will be revealed at the Google Keynote.\n\nCLOUD DEVELOPER DAY | 3/20*\nJoin Google Cloud and some of our key partners to learn more about the latest innovations in cloud technology for games." } } } ] } ================================================ FILE: testdata/apidata/playlists/playlists_single.json ================================================ { "kind": "youtube#playlistListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/fk7i9mk8dlQehbz-BVBy0SzccWY\"", "pageInfo": { "totalResults": 1, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#playlist", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/gdl_m86JfWCd37Wtcc2dte9hrEg\"", "id": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "snippet": { "publishedAt": "2019-05-10T00:18:56.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "I/O Live - Show Composite", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "I/O Live - Show Composite", "description": "" } } } ] } ================================================ FILE: testdata/apidata/search/search_by_developer.json ================================================ { "kind": "youtube#searchListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/-_uuZ-sNfL41nlDe7y-H3-hTVR8\"", "nextPageToken": "CAoQAA", "prevPageToken": "CAUQAQ", "regionCode": "US", "pageInfo": { "totalResults": 1000000, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/0Qdy2kYX0gwN6vW9FaXr5bKR1e8\"", "id": { "kind": "youtube#video", "videoId": "WuyFniRMrxY" }, "snippet": { "publishedAt": "2020-03-14T01:59:46.000Z", "channelId": "UCeY0bbntWzzVIaj2z3QigXg", "title": "NBC Nightly News Broadcast (Full) - March 13th, 2020 | NBC Nightly News", "description": "Coronavirus pandemic: President Trump declares national emergency to combat coronavirus, already strained hospitals worry about what's to come, and ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/WuyFniRMrxY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/WuyFniRMrxY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/WuyFniRMrxY/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "NBC News", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/VrmRau0XKBBIRmWgTtLIcifdHnQ\"", "id": { "kind": "youtube#video", "videoId": "VQIZkYkIynE" }, "snippet": { "publishedAt": "2020-03-15T12:43:32.000Z", "channelId": "UCknLrEdhRCp1aegoMqRaCZg", "title": "Coronavirus precautions sweep around the world | DW News", "description": "While authorities around the world take ever more drastic measures to try to slow the spread of the coronavirus, the number of cases is rising. Indonesia and ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/VQIZkYkIynE/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/VQIZkYkIynE/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/VQIZkYkIynE/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "DW News", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/ZQL6qzIJxT1j-e6wqOssMFgBC7g\"", "id": { "kind": "youtube#video", "videoId": "q_WM_pMp0Hg" }, "snippet": { "publishedAt": "2020-03-13T21:12:36.000Z", "channelId": "UCeY0bbntWzzVIaj2z3QigXg", "title": "Trump Holds News Conference On Coronavirus Pandemic | NBC News (Live Stream Recording)", "description": "President Donald Trump holds a news conference on the coronavirus pandemic which is spreading across the United States. The president is expected to ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/q_WM_pMp0Hg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/q_WM_pMp0Hg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/q_WM_pMp0Hg/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "NBC News", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/VKr-2JYTn80JU9wfted23CwUrJU\"", "id": { "kind": "youtube#video", "videoId": "wDbyThHkctA" }, "snippet": { "publishedAt": "2020-03-14T00:06:34.000Z", "channelId": "UCBi2mrWuNuyYy4gbM6fU18Q", "title": "ABC News Prime: Coronavirus national emergency, life under quarantine, stock market", "description": "coronavirus #quarantine #nationalemergency #nyse SUBSCRIBE to ABC NEWS: https://bit.ly/2vZb6yP Watch More on http://abcnews.go.com/ LIKE ABC News ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/wDbyThHkctA/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/wDbyThHkctA/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/wDbyThHkctA/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "ABC News", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/kubYi5AYaeyfTjRY02WZFb1xipE\"", "id": { "kind": "youtube#video", "videoId": "IQdYDkwtvoo" }, "snippet": { "publishedAt": "2020-03-13T22:41:09.000Z", "channelId": "UC16niRr50-MSBwiO3YDb3RA", "title": "Coronavirus: Europe at the epicentre of the pandemic - BBC News", "description": "The World Health Organisation says Europe is now the epicentre, of the global coronavirus pandemic. The news emerged as organised sport, both professional ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/IQdYDkwtvoo/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/IQdYDkwtvoo/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/IQdYDkwtvoo/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "BBC News", "liveBroadcastContent": "none" } } ] } ================================================ FILE: testdata/apidata/search/search_by_event.json ================================================ { "kind": "youtube#searchListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/SE083xj7h4uR9Tn6xMg-LwbsFw8\"", "nextPageToken": "CBkQAA", "regionCode": "US", "pageInfo": { "totalResults": 2342, "resultsPerPage": 25 }, "items": [ { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/5aX5VRI4Ow4wngR-oJGG0m5ghLY\"", "id": { "kind": "youtube#video", "videoId": "qgylp3Td1Bw" }, "snippet": { "publishedAt": "2020-01-30T01:54:33.000Z", "channelId": "UCDGiCfCZIV5phsoGiPwIcyQ", "title": "[LIVE] Coronavirus Pandemic: Real Time Counter, World Map, News", "description": "Novel coronavirus Live Streaming: Breaking news, world Map and live counter on confirmed cases, recovered cases(COVID-19). I started this live stream on Jan ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/qgylp3Td1Bw/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/qgylp3Td1Bw/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/qgylp3Td1Bw/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "Roylab Stats", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/KZp_3SD0kS28nlJqpCt7CKUKDC8\"", "id": { "kind": "youtube#video", "videoId": "9Auq9mYxFEE" }, "snippet": { "publishedAt": "2019-11-02T09:49:18.000Z", "channelId": "UCoMdktPbSTixAyNGwb-UYkQ", "title": "Watch Sky News live", "description": "Today's top stories: Health Secretary Matt Hancock has told Sky News that over-70s will be asked to self-isolate - potentially for months - \"in the coming weeks\".", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/9Auq9mYxFEE/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/9Auq9mYxFEE/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/9Auq9mYxFEE/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "Sky News", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/7a4VJgDJUaloBgxdFIsUXOj4HIE\"", "id": { "kind": "youtube#video", "videoId": "fy8iq-tib-U" }, "snippet": { "publishedAt": "2020-03-13T06:39:48.000Z", "channelId": "UCRWFSbif-RFENbBrSiez1DA", "title": "ABP News LIVE TV: Watch Top News Of The Day 24*7 on ABP News LIVE TV", "description": "Watch Top News Of The Day 24*7 on ABP News LIVE TV #ABPNewsHindi #ABPNews #ABPNewsLive Watch news LIVE in Hindi, only on ABP News.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/fy8iq-tib-U/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/fy8iq-tib-U/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/fy8iq-tib-U/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "ABP NEWS", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Qzvc-nqa6GICabLjplsOCUk2GCI\"", "id": { "kind": "youtube#video", "videoId": "fjxHTtZOAgQ" }, "snippet": { "publishedAt": "2020-03-15T09:42:02.000Z", "channelId": "UCttspZesZIDEwwpVIgoZtWQ", "title": "IndiaTV LIVE | Hindi News LIVE 24X7 | इंडिया टीवी LIVE", "description": "IndiaTV LIVE | Hindi News LIVE | इंडिया टीवी LIVE Subscribe to IndiaTV and don't forget to press \"THE BELL ICON\" to never miss any updates- ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/fjxHTtZOAgQ/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/fjxHTtZOAgQ/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/fjxHTtZOAgQ/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "IndiaTV", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/2rT6MY29RMrS3vcglPt5QxyLlmg\"", "id": { "kind": "youtube#video", "videoId": "Q6QR4979KIQ" }, "snippet": { "publishedAt": "2019-08-21T11:09:13.000Z", "channelId": "UCPXTXMecYqnRKNdqdVOGSFg", "title": "TV9 Telugu LIVE", "description": "TV9 Telugu Live is a 24/7 news channel in Andhra Pradesh and Telangana. Watch the latest Telugu news LIVE on the most subscribed news channel on ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Q6QR4979KIQ/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Q6QR4979KIQ/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Q6QR4979KIQ/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "TV9 Telugu Live", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/edP7aU3v1BL_vvpd84thza2b3z0\"", "id": { "kind": "youtube#video", "videoId": "E9mb9fwL1Pk" }, "snippet": { "publishedAt": "2020-03-10T22:19:59.000Z", "channelId": "UCH7nv1A9xIrAifZJNvt7cgA", "title": "ABP Majha LIVE | ABP Majha Latest Updates | एबीपी माझा | Marathi LIVE News", "description": "For latest breaking news ( #MahaPoliticsNews #MarathiNews #DelhiRiots2020 ) log on to: https://marathi.abplive.com/ Social Media Handles: Facebook: ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/E9mb9fwL1Pk/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/E9mb9fwL1Pk/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/E9mb9fwL1Pk/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "ABP Majha", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/bzzK40_g2ivEub0g_ttaakdT2E8\"", "id": { "kind": "youtube#video", "videoId": "w_Ma8oQLmSM" }, "snippet": { "publishedAt": "2020-02-12T00:32:37.000Z", "channelId": "UCBi2mrWuNuyYy4gbM6fU18Q", "title": "Watch the Latest News Headlines and Live Events l ABC News Live", "description": "News #LiveNews #StreamingNews #ABCNewsLive LATEST NEWS: https://abcnews.go.com/ SUBSCRIBE to ABC News on YouTube: https://bit.ly/2vZb6yP ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/w_Ma8oQLmSM/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/w_Ma8oQLmSM/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/w_Ma8oQLmSM/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "ABC News", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/rqvpQP9DNC1PWSy1xx_upyzfazo\"", "id": { "kind": "youtube#video", "videoId": "jdJoOhqCipA" }, "snippet": { "publishedAt": "2020-01-08T06:59:28.000Z", "channelId": "UC8dnBi4WUErqYQHZ4PfsLTg", "title": "TV9 KANNADA NEWS LIVE | ಟಿವಿ9 ಕನ್ನಡ ನ್ಯೂಸ್ ಲೈವ್", "description": "TV9 Kannada live is a 24-hour Kannada news channel. TV9 established its image as one of India's most-watched and credible regional news channels ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/jdJoOhqCipA/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/jdJoOhqCipA/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/jdJoOhqCipA/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "Tv9 Kannada", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/FHVxe0z-mMnJfh4WVeSC05g0qIo\"", "id": { "kind": "youtube#video", "videoId": "mcHRJAxmWhA" }, "snippet": { "publishedAt": "2020-03-15T02:19:37.000Z", "channelId": "UCE2606prvXQc_noEqKxVJXA", "title": "Metro Manila isinailalim sa quarantine kontra pagkalat ng COVID-19 | DZMM", "description": "ABSCBNNews #ABSCBNNewsLivestream Subscribe to the ABS-CBN News channel! - http://bit.ly/TheABSCBNNews Visit our website at ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/mcHRJAxmWhA/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/mcHRJAxmWhA/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/mcHRJAxmWhA/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "ABS-CBN News", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/jG__YPSLyRPg0Nanctder5zWPGc\"", "id": { "kind": "youtube#video", "videoId": "iL53Y28Rp84" }, "snippet": { "publishedAt": "2020-03-15T01:45:33.000Z", "channelId": "UCf8w5m0YsRa8MHQ5bwSGmbw", "title": "Asianet News Live TV | Malayalam News Live | ഏഷ്യാനെറ്റ് ന്യൂസ് ലൈവ് | Kerala News Live", "description": "MalaylamLiveTV #MalayalamLiveNews #AsianetnewsLiveTV #KeralaLiveNews #AsianetNewsLive Click Here to Subscribe! ▻ http://goo.gl/Y4yRZG Asianet ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/iL53Y28Rp84/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/iL53Y28Rp84/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/iL53Y28Rp84/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "asianetnews", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/AryGyghLx28-1KGCz8jCTkZr8jc\"", "id": { "kind": "youtube#video", "videoId": "l9ViEIip9q4" }, "snippet": { "publishedAt": "2020-03-02T09:47:22.000Z", "channelId": "UC9CYT9gSNLevX5ey2_6CK0Q", "title": "NDTV India LIVE TV - Watch Latest News in Hindi | हिंदी समाचार", "description": "देखें NDTV इंडिया लाइव, फ़्री डिश पर चैनल नं 45. NDTV India live stream is also available on https://khabar.ndtv.com/videos/live/channe...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/l9ViEIip9q4/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/l9ViEIip9q4/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/l9ViEIip9q4/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "NDTV India", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/ntEW0YTHOoO856jDvagg8Bl9BCU\"", "id": { "kind": "youtube#video", "videoId": "fraxH3rvU84" }, "snippet": { "publishedAt": "2020-01-29T12:06:30.000Z", "channelId": "UCdOSeEq9Cs2Pco7OCn2_i5w", "title": "TV9 Marathi Live | टीव्ही9 मराठी LIVE | Marathi News Updates", "description": "TV9 Marathi Live | टीव्ही9 मराठी LIVE | Marathi News Updates कोरोनाचं प्रश्नचक्र | Corona Special Show | कोरोनावर...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/fraxH3rvU84/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/fraxH3rvU84/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/fraxH3rvU84/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "TV9 Marathi", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/KHFdy6b3DLBhKnkc1JoLafO6rGc\"", "id": { "kind": "youtube#video", "videoId": "jjH6v95z3Nw" }, "snippet": { "publishedAt": "2020-01-27T05:05:51.000Z", "channelId": "UCP0uG-mcMImgKnJz-VjJZmQ", "title": "Manorama News LIVE TV | മനോരമ ന്യൂസ് ലൈവ് | Latest Kerala News Updates | Malayalam News LIVE Channel", "description": "Watch Manorama News Malayalam Channel Live Stream for Kerala Budget Updates, Latest Malayalam News Updates, Breaking News, Political News and ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/jjH6v95z3Nw/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/jjH6v95z3Nw/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/jjH6v95z3Nw/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "Manorama News", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/0b4I4C7PgvHQe5koF1Vq9qotwG4\"", "id": { "kind": "youtube#video", "videoId": "lhI934dror4" }, "snippet": { "publishedAt": "2020-03-13T19:54:16.000Z", "channelId": "UCeJWZgSMlzqYEDytDnvzHnw", "title": "Top News Stories From Gujarat, India and International | Tv9 Gujarati LIVE", "description": "Top News Stories From Gujarat, India and International | Tv9 Gujarati LIVE #TV9GujaratiLive #GujaratiNews #TV9News Tv9 ગુજરાતીની Youtube ચેનલને ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/lhI934dror4/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/lhI934dror4/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/lhI934dror4/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "TV9 Gujarati", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/cQAyqrmM5zSwQ8XaXdzC43IXXfA\"", "id": { "kind": "youtube#video", "videoId": "pc6kNspzvqI" }, "snippet": { "publishedAt": "2020-03-12T00:07:00.000Z", "channelId": "UCBwc2cbPpvxNCNEI2-8YrqQ", "title": "Madhya Pradesh Political Crisis LIVE Updates | News18 MP Chattisgarh Live | Chhattisgarh News", "description": "MPNewsLive #मध्यप्रदेशसमाचार #ChattisgarhNews #News18MadhyaPradesh #ChhattisgarhLiveTV #HindiNews News18 MP-Chhattisgarh में ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/pc6kNspzvqI/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/pc6kNspzvqI/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/pc6kNspzvqI/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "News18 MP Chhattisgarh", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/pJgY5U7TjUN1FipOXWJQxZsGbDw\"", "id": { "kind": "youtube#video", "videoId": "41e4fSihWG0" }, "snippet": { "publishedAt": "2019-01-25T03:49:13.000Z", "channelId": "UC-f7r46JhYv78q5pGrO6ivA", "title": "MediaOneTV Live | Latest Malayalam News & Live Updates | മീഡിയവണ്\u200d ടി.വി ലൈവ്", "description": "MediaOneTVLive #MalayalamNews #KeralaNews #MediaOneTVLive #MalayalamLiveNews MediaOneTVLive MalayalamLiveNews KeralaLiveNews ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/41e4fSihWG0/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/41e4fSihWG0/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/41e4fSihWG0/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "MediaoneTV Live", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/6diKV3o5THkuBEUSKUjjPkVsV3k\"", "id": { "kind": "youtube#video", "videoId": "H3AZDSGwqMM" }, "snippet": { "publishedAt": "2020-02-15T03:15:48.000Z", "channelId": "UC8Z-VjXBtDJTvq6aqkIskPg", "title": "🔴LIVE: Polimer News Live | Tamil News | Coronavirus|WHO|Rajini|Rajini Press Meet|Rajya Sabha MP", "description": "Watch Tamil news live on Polimer News, an exclusive news channel on YouTube which streams news related to current affairs of Tamil Nadu, Nation, and the ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/H3AZDSGwqMM/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/H3AZDSGwqMM/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/H3AZDSGwqMM/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "Polimer News", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/nFzCQm1JkyTW1-4NiDpjfSG--6I\"", "id": { "kind": "youtube#video", "videoId": "XxJKnDLYZz4" }, "snippet": { "publishedAt": "2015-01-16T02:28:27.000Z", "channelId": "UClIfopQZlkkSpM1VgCFLRJA", "title": "民視新聞直播 | Taiwan Formosa live news HD | 台湾のニュース放送HD | 대만 뉴스 방송HD", "description": "民視新聞#即時新聞#新聞直播四季線上影視https://4gtv.tv 民視新聞網影音https://bit.ly/2Jwxt2D 民視新聞網官網https://www.ftvnews.com.tw/ 每日新聞熱點...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/XxJKnDLYZz4/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/XxJKnDLYZz4/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/XxJKnDLYZz4/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "民視直播 FTVN Live 53", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/1nnCgS9I3pBRGbDod7prHOaUBZM\"", "id": { "kind": "youtube#video", "videoId": "coYw-eVU0Ks" }, "snippet": { "publishedAt": "2019-01-22T07:21:31.000Z", "channelId": "UCGCZAYq5Xxojl_tSXcVJhiQ", "title": "JapaNews24 ~日本のニュースを24時間配信", "description": "事件や政治、自然災害など時事問題から街のトレンドまで…。 日本国内の注目のニュースを中心にまとめた番組を30分ごとにお送りしています。...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/coYw-eVU0Ks/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/coYw-eVU0Ks/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/coYw-eVU0Ks/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "ANNnewsCH", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/EoBPwJQcSjFjwFDK4Glv5vltXLw\"", "id": { "kind": "youtube#video", "videoId": "r7r8zrbOOlk" }, "snippet": { "publishedAt": "2020-02-17T10:22:35.000Z", "channelId": "UCPP3etACgdUWvizcES1dJ8Q", "title": "News 18 India LIVE | Watch Latest News In India | हिंदी समाचार LIVE", "description": "News18India देखिये ताज़ातरीन खबर सिर्फ News 18 पर Watch all the current, latest and breaking Hindi news only on NEWS18 India Live TV.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/r7r8zrbOOlk/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/r7r8zrbOOlk/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/r7r8zrbOOlk/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "News18 India", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/tBnFfKm0QOcVSKMnY0tAeFgczwE\"", "id": { "kind": "youtube#video", "videoId": "ktNOFI35pwk" }, "snippet": { "publishedAt": "2020-03-15T01:22:34.000Z", "channelId": "UCGyZswzm4G-wEfRQHgMSAuw", "title": "🔴 News7 Tamil LIVE | Tamil News Live | News Live | நியூஸ்7 தமிழ் | Corona Virus | Rajini Press Meet", "description": "News7 Tamil LIVE | Tamil News Live | News Live | நியூஸ்7 தமிழ் | Corona Virus | Rajini Press Meet நியூஸ்7 தமிழ் நேரலை ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/ktNOFI35pwk/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/ktNOFI35pwk/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/ktNOFI35pwk/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "News7 Tamil", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/8Z5uE84SwcjOr1DlBT_WtsrfKBo\"", "id": { "kind": "youtube#video", "videoId": "1r2w-b5laYo" }, "snippet": { "publishedAt": "2020-01-14T09:05:15.000Z", "channelId": "UCv3rFzn-GHGtqzXiaq3sWNg", "title": "ABP Ananda News Live | দিনের সেরা খবর সরাসরি | Live Bangla New 24X7 | Latest Bengali News", "description": "Watch Live Bangla news on ABP Ananda Live #ABPAnanda #Bangla News #LiveNews Subscribe to our YouTube channel here: ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/1r2w-b5laYo/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/1r2w-b5laYo/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/1r2w-b5laYo/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "ABP ANANDA", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/3RwJf3LxGymy8-5v3YeqILn975Y\"", "id": { "kind": "youtube#video", "videoId": "zcrUCvBD16k" }, "snippet": { "publishedAt": "2019-12-08T14:22:54.000Z", "channelId": "UCup3etEdjyF1L3sRbU-rKLw", "title": "24 News Live TV 24/7| HD Live Streaming", "description": "24 News Live TV 24/7 | Live | HD Live Streaming ഏറ്റവും പുതിയ വാർത്തകൾക്കായി സന്ദർശിക്കുക == http://www.twentyfourne...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/zcrUCvBD16k/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/zcrUCvBD16k/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/zcrUCvBD16k/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "24 News", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/zGlB4yIoO_pevH6VjhhiuPlDnxs\"", "id": { "kind": "youtube#video", "videoId": "5s7AZZKobxo" }, "snippet": { "publishedAt": "2020-03-14T13:31:24.000Z", "channelId": "UCsgC5cbz3DE2Shh34gNKiog", "title": "92 News HD Live", "description": "92NewsHD Live, Pakistan's first HD Plus news channel brings you the crispiest live news, headlines, delineate and relevant updates, current affairs, viral news, ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/5s7AZZKobxo/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/5s7AZZKobxo/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/5s7AZZKobxo/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "92 News HD", "liveBroadcastContent": "live" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/sIoVFfBZD87MYz0G0e2TqvCXRdk\"", "id": { "kind": "youtube#video", "videoId": "HOgwUdcMgeY" }, "snippet": { "publishedAt": "2020-03-13T09:07:39.000Z", "channelId": "UC7wXt18f2iA3EDXeqAVuKng", "title": "Coronavirus Outbreak: Republic Bharat Live TV | Latest News in Hindi | रिपब्लिक भारत। लाइव टीवी", "description": "RepublicBharat #RepublicBharatLive #Coronavirus #HindiNews #IndiaNews #LatestNewsToday #LiveNews #HindiNews #Coronavirus #LiveTV ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/HOgwUdcMgeY/default_live.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/HOgwUdcMgeY/mqdefault_live.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/HOgwUdcMgeY/hqdefault_live.jpg", "width": 480, "height": 360 } }, "channelTitle": "Republic Bharat", "liveBroadcastContent": "live" } } ] } ================================================ FILE: testdata/apidata/search/search_by_keywords_p1.json ================================================ { "kind": "youtube#searchListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/HlaDCPqWxO8HJ_tV51UiHmzJdAs\"", "nextPageToken": "CBkQAA", "regionCode": "JP", "pageInfo": { "totalResults": 1000000, "resultsPerPage": 25 }, "items": [ { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/IUPje0AHo3kVWZPuaOELZDozfa0\"", "id": { "kind": "youtube#video", "videoId": "gQ_1zX-F_No" }, "snippet": { "publishedAt": "2019-12-05T19:00:10.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "SURFING HAWAII’S BEST WAVE WITH MY GIRLFRIEND (PIPELINE)", "description": "SOME FUN BARRELS IN THE MORNING TO SOME LONG BOARDING, RAFTING, AND BOARD TRANSFERS IN THE AFTERNOON! NEW STAY PSYCHED ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/gQ_1zX-F_No/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/gQ_1zX-F_No/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/gQ_1zX-F_No/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/yT8vWPNKatpC5KUNNeCaAUnhg78\"", "id": { "kind": "youtube#video", "videoId": "i2hewkFijuY" }, "snippet": { "publishedAt": "2019-12-04T16:00:08.000Z", "channelId": "UC_F4Iy5korq2mEWZDQhG07w", "title": "SURFING PERFECT PIPE & BACKDOOR || SUNRISE SHACK!", "description": "This is Livin' Movie Premiering at Surfer Bar on January 25th! Koa does the surfing at the pipeline makes the tubes he also does the big acai bowls and the ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/i2hewkFijuY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/i2hewkFijuY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/i2hewkFijuY/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Koa Rothman", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/exxw2qujoMBCF6s6v8geVAlQMgo\"", "id": { "kind": "youtube#video", "videoId": "_Z56nIAQj7Q" }, "snippet": { "publishedAt": "2019-12-06T07:54:12.000Z", "channelId": "UChuLeaTGRcfzo0UjL-2qSbQ", "title": "Taiwan Open of Surfing World Longboard Championships", "description": "WSL Subscribe to the WSL for more action: https://goo.gl/VllRuj Watch all the latest surfing action of the world's best surfers in the world's best waves. Heats on ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/_Z56nIAQj7Q/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/_Z56nIAQj7Q/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/_Z56nIAQj7Q/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "World Surf League", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/yfVam76dXHEx_BigZW-xV7NBxqE\"", "id": { "kind": "youtube#video", "videoId": "W7h-Yho8EB0" }, "snippet": { "publishedAt": "2019-06-15T15:21:45.000Z", "channelId": "UCqhnX4jA0A5paNd1v-zEysw", "title": "GoPro: Top 10 Surf Moments", "description": "Celebrate International Surf Day with GoPro's Top 10 Surf Moments. Shot 100% on GoPro: http://bit.ly/2wUMwfI Get stoked and subscribe: http://goo.gl/HgVXpQ ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/W7h-Yho8EB0/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/W7h-Yho8EB0/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/W7h-Yho8EB0/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "GoPro", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/XWyPkbiV-j7gjEJev9Cknl4DDQ0\"", "id": { "kind": "youtube#video", "videoId": "caj9z17kuyw" }, "snippet": { "publishedAt": "2019-11-07T19:00:04.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "SURFING GIANT WAVES IN HAWAII (PIPELINE)", "description": "WINTER IS HERE! WOKE UP TO GIANT SURF SO I HAD TO GET OUT THERE AND CATCH A FEW MYSELF! CHECK OUT MY MERCH: WWW.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/caj9z17kuyw/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/caj9z17kuyw/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/caj9z17kuyw/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/_mOBjnA5mb03qReqVQdkZxSdaUQ\"", "id": { "kind": "youtube#video", "videoId": "oci0CqUU5KI" }, "snippet": { "publishedAt": "2019-11-21T19:00:04.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "ENDLESS SURF ON THE NORTH SHORE (HAWAII)", "description": "SURFER AWARDS: HELP US PROVE THAT THE STAY PSYCHED COMMUNITY IS THE BEST VLOG FANS IN THE WORLD! VOTE HERE: ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/oci0CqUU5KI/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/oci0CqUU5KI/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/oci0CqUU5KI/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/OOD6SlB-NumCjkurCLUxN68r25E\"", "id": { "kind": "youtube#video", "videoId": "nkhpGC10OVw" }, "snippet": { "publishedAt": "2017-04-09T17:18:44.000Z", "channelId": "UCHeaHzQFLElUw__yG3SSzMg", "title": "World's best surfing 2017", "description": "World's best surfing 2017 — Enjoy the video. Rate, Comment, Share... Thanx Subscribe for new compilations: http://goo.gl/X017T If your Video is in this ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/nkhpGC10OVw/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/nkhpGC10OVw/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/nkhpGC10OVw/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Monthly Winners", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/WO5q6dSUt9m8pHRzGlR3fq9xvbA\"", "id": { "kind": "youtube#video", "videoId": "c6yOxWf3A6g" }, "snippet": { "publishedAt": "2017-08-20T07:00:02.000Z", "channelId": "UC1Ho5YvHCtyReazatbhBowA", "title": "HOW TO SURF: 7 BEGINNER MISTAKES AND HOW TO FIX THEM", "description": "In this video you will get to follow a beginners surfers journey trying to learn how to surf. I have tried surfing before but never with an expert showing how to do it ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/c6yOxWf3A6g/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/c6yOxWf3A6g/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/c6yOxWf3A6g/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Stomp It Tutorials", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/akBNz6Ly7vzE5-E5M5tZV6bn5BQ\"", "id": { "kind": "youtube#video", "videoId": "8Bha766qpNw" }, "snippet": { "publishedAt": "2019-12-03T03:51:46.000Z", "channelId": "UCnJ0mt5Cgx4ER_LhTijG_4A", "title": "2019 Vans World Cup of Surfing - Final Day Highlights | Triple Crown of Surfing | VANS", "description": "Australia's Jack Robinson finds some incredible barrels at Sunset, becoming the 2019 Vans World Cup of Surfing Champion. Check out the final day's highlights ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/8Bha766qpNw/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/8Bha766qpNw/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/8Bha766qpNw/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Vans", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/zaiyqHbBJdyoBqfoY2zz481clSU\"", "id": { "kind": "youtube#video", "videoId": "67QNw2xQlsk" }, "snippet": { "publishedAt": "2019-01-12T14:00:15.000Z", "channelId": "UCuZSTHZf3vd7eVehhnotcsg", "title": "Learn How To Surf In 10 Minutes", "description": "This video is for anyone who wants to learn how to surf! From choosing your equipment, all the way to a step by step guide to standing up. We show you it all.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/67QNw2xQlsk/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/67QNw2xQlsk/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/67QNw2xQlsk/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "How to Rip", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/E8GZG_CZfJeaVF75eZYmJHnGe0c\"", "id": { "kind": "youtube#video", "videoId": "rj7xMBxd5iY" }, "snippet": { "publishedAt": "2017-11-12T11:09:52.000Z", "channelId": "UCiiFGfvlKvX3uzMovO3unaw", "title": "BIG WAVE SURFING COMPILATION 2017", "description": "BIG WAVE SURFING COMPILATION 2017 ** REVISED **AMAZING FOOTAGE ** WITH 60-100FT- HUGE SURF Please Subscribe if You Would like to see More ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/rj7xMBxd5iY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/rj7xMBxd5iY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/rj7xMBxd5iY/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Absolutely Flawless", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/jd-wJK0NPMZT7aGsDpq-edAZRWk\"", "id": { "kind": "youtube#video", "videoId": "FeNPd168uqg" }, "snippet": { "publishedAt": "2019-06-22T01:41:06.000Z", "channelId": "UCJBWZDkunrjWh50Etxdys2g", "title": "Surfing San Diego Winter 2018-2019 Season (Short Film)", "description": "Shop SoCal Surfer! https://teespring.com/stores/socal-surfer This past season has been unreal! The swells made double to triple overhead waves at some of the ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/FeNPd168uqg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/FeNPd168uqg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/FeNPd168uqg/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "SoCal Surfer", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/WznjU0SpaKoMkx-E7Jf-iI4VIgs\"", "id": { "kind": "youtube#video", "videoId": "xU3iCjnlqx8" }, "snippet": { "publishedAt": "2019-01-16T00:05:33.000Z", "channelId": "UCKo-NbWOxnxBnU41b-AoKeA", "title": "The Best Surf Clips of 2018 | SURFER Magazine", "description": "Drawing from SURFER's “Clips of the Month” series, here's a (very arguable) list of the “Clips of the Year.” No matter how you slice it, 2018 was an incredible trip ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/xU3iCjnlqx8/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/xU3iCjnlqx8/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/xU3iCjnlqx8/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "SURFER", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/QRCpeAtifFokjELuUekSWYVIbtI\"", "id": { "kind": "youtube#video", "videoId": "PBEnhMlnRg0" }, "snippet": { "publishedAt": "2019-12-01T11:00:04.000Z", "channelId": "UCzcQOTuXYGuCvTekySb_CeQ", "title": "Bali Surf Journal - November 2019", "description": "As November rolled in it seemed like the swell was lost at sea. It's normal to have small conditions during this time of year but it felt like there was less size ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/PBEnhMlnRg0/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/PBEnhMlnRg0/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/PBEnhMlnRg0/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Surfers of Bali", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/VH1h1gqPQ5j194_Te6FEuy-mwwY\"", "id": { "kind": "youtube#video", "videoId": "ZjbJkdQV1XA" }, "snippet": { "publishedAt": "2019-11-25T19:00:05.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "RIVER SURFING IN HAWAII (WAIMEA BAY)", "description": "LET'S PROVE THAT JOBVLOGS HAS THE MOST PSYCHED VIEWERS BY VOTING!!! https://www.surfer.com/surfer-awards/ CHECK OUT MY MERCH: WWW.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/ZjbJkdQV1XA/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/ZjbJkdQV1XA/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/ZjbJkdQV1XA/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Qpy1ZO77GTQNi1QMa1iqS6NgODE\"", "id": { "kind": "youtube#video", "videoId": "vAryEy9QvgU" }, "snippet": { "publishedAt": "2019-07-08T01:11:29.000Z", "channelId": "UCJBWZDkunrjWh50Etxdys2g", "title": "Surfing ENDLESS Waves in Malibu!", "description": "Shop SoCal Surfer! https://teespring.com/stores/socal-surfer Todays forecast made for 2-3 waves at Malibu and tons of people. I will be up here this weekend ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/vAryEy9QvgU/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/vAryEy9QvgU/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/vAryEy9QvgU/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "SoCal Surfer", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/HBBY8t7e5zs0MgO37mJyflDMunA\"", "id": { "kind": "youtube#video", "videoId": "ddYARvl4PgE" }, "snippet": { "publishedAt": "2019-12-06T08:25:39.000Z", "channelId": "UCKo-NbWOxnxBnU41b-AoKeA", "title": "THE 2019 SURFER AWARDS", "description": "On December 5, 2019, SURFER Magazine will host the 49th Annual SURFER Awards on the North Shore of Oahu. While the SURFER Poll has had numerous ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/ddYARvl4PgE/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/ddYARvl4PgE/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/ddYARvl4PgE/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "SURFER", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/KLKwu7HjRM7EC2G2mPqropCIm6E\"", "id": { "kind": "youtube#video", "videoId": "2nX5Sjsd5To" }, "snippet": { "publishedAt": "2018-07-09T19:30:02.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "DANGEROUS SEWER DRAIN SURFING! | Jamie O'Brien", "description": "We finally return to the Sewer Drain poopies dropped in Who is Job 5 years ago. But this time we have a full on session! Make sure to like and subscribe for ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/2nX5Sjsd5To/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/2nX5Sjsd5To/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/2nX5Sjsd5To/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/wfwhWGYsHfq_0CKXxYsmHmvVK6M\"", "id": { "kind": "youtube#video", "videoId": "6GUtd7f1_Xo" }, "snippet": { "publishedAt": "2019-01-01T20:01:55.000Z", "channelId": "UCVk7uRN7g-q_uHMaAjHxk8A", "title": "THE GIRLS OF SURFING XIX", "description": "The Girls of Surfing 2018 edition Join the page here : https://www.facebook.com/thegirlsofsurfing/ Instagram @jolieoligny Twitter @jolieoligny Inspired by ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/6GUtd7f1_Xo/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/6GUtd7f1_Xo/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/6GUtd7f1_Xo/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "thegirlsofsurfing", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/k1xVqEDOCAGEo5gJVqHfGHm_xaY\"", "id": { "kind": "youtube#video", "videoId": "e0t9RpSje7w" }, "snippet": { "publishedAt": "2019-08-20T19:00:08.000Z", "channelId": "UCuwdplPbuTFZj_64d03tSBA", "title": "Young Thug - Surf ft. Gunna [Official Video]", "description": "Stream So Much Fun Now! https://youngthug.ffm.to/somuchfun Listen to So Much Fun on Youtube Music: ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/e0t9RpSje7w/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/e0t9RpSje7w/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/e0t9RpSje7w/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Young Thug", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Y3HcmuZG0Dx-64_AoDIlD6UArS0\"", "id": { "kind": "youtube#video", "videoId": "EaZ-uNoMRHw" }, "snippet": { "publishedAt": "2019-06-03T23:55:59.000Z", "channelId": "UCJBWZDkunrjWh50Etxdys2g", "title": "Surfers Catch INSANE Party waves in San Diego!", "description": "Shop SoCal Surfer! https://teespring.com/stores/socal-surfer Todays forecast made for 2-3 Insane party waves and hectic conditions in San Diego. Lots of fun ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/EaZ-uNoMRHw/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/EaZ-uNoMRHw/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/EaZ-uNoMRHw/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "SoCal Surfer", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Y5PFmyzA0s9UXLDwLVRHeeoGCcY\"", "id": { "kind": "youtube#video", "videoId": "JJJtW8tQvB0" }, "snippet": { "publishedAt": "2019-10-20T05:18:41.000Z", "channelId": "UCq650FWunrqAhM6pV5sncCg", "title": "FINGER SURFING!", "description": "I'v been getting comments to make a Finger Surfboard ever since I started making Fingerboard videos but I always thought it would be way to hard... well, I made ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/JJJtW8tQvB0/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/JJJtW8tQvB0/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/JJJtW8tQvB0/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "davidsjones", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/By2tddm9SOnD8bAg7OoeX2niVJU\"", "id": { "kind": "youtube#video", "videoId": "kD7QBPONnlY" }, "snippet": { "publishedAt": "2019-11-15T20:45:21.000Z", "channelId": "UCf5CA0OsvhhU-6AcSjT1oKQ", "title": "Surfing PERFECT river wave and GNARLY skim wedge !!!", "description": "Blair Conklin surfs a glassy river wave and the crew scores some gnarly skim wedges in Southern California ! Featured shredders include Johnny Redmond, ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/kD7QBPONnlY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/kD7QBPONnlY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/kD7QBPONnlY/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "BEEFS T.V.", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/dNwYM_CRPhW0nFjpEGUcAXSYfPw\"", "id": { "kind": "youtube#video", "videoId": "X9tU8ybzcFs" }, "snippet": { "publishedAt": "2017-07-15T07:03:37.000Z", "channelId": "UCsG5dkqFUHZO6eY9uOzQqow", "title": "The Dock", "description": "At four am we were up, towing out what's now known as \"The Dock” in Bali for this concept shoot. We asked ourselves, what does the future look like? A cashless ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/X9tU8ybzcFs/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/X9tU8ybzcFs/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/X9tU8ybzcFs/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Stab Magazine", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/yXQOdwn7hCIqAtPOI4sUHBbaNDA\"", "id": { "kind": "youtube#video", "videoId": "-2IlD-x8wvY" }, "snippet": { "publishedAt": "2019-09-01T17:16:15.000Z", "channelId": "UCeYue9Nbodzg3T1Nt88E3fg", "title": "Hawaiian Summer Surfing", "description": "Happy Birthday Mason Ho! This was filmed a couple days ago here in Hawaii. Location: North Shore, Oahu. Surfers: Mason Ho, Sheldon Paishon and Kamo ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/-2IlD-x8wvY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/-2IlD-x8wvY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/-2IlD-x8wvY/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Ho & Pringle Productions", "liveBroadcastContent": "none" } } ] } ================================================ FILE: testdata/apidata/search/search_by_keywords_p2.json ================================================ { "kind": "youtube#searchListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ALJWUURzLUpxp1uFSnJBAmUIj8g\"", "nextPageToken": "CDIQAA", "prevPageToken": "CBkQAQ", "regionCode": "JP", "pageInfo": { "totalResults": 1000000, "resultsPerPage": 25 }, "items": [ { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/yXQOdwn7hCIqAtPOI4sUHBbaNDA\"", "id": { "kind": "youtube#video", "videoId": "-2IlD-x8wvY" }, "snippet": { "publishedAt": "2019-09-01T17:16:15.000Z", "channelId": "UCeYue9Nbodzg3T1Nt88E3fg", "title": "Hawaiian Summer Surfing", "description": "Happy Birthday Mason Ho! This was filmed a couple days ago here in Hawaii. Location: North Shore, Oahu. Surfers: Mason Ho, Sheldon Paishon and Kamo ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/-2IlD-x8wvY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/-2IlD-x8wvY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/-2IlD-x8wvY/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Ho & Pringle Productions", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/KG1OvjtEBf5STAnjMIthCoYNgwE\"", "id": { "kind": "youtube#video", "videoId": "q0yVuNANAkY" }, "snippet": { "publishedAt": "2019-10-07T19:00:03.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "SCORING FUN SURF IN CALIFORNIA", "description": "SANTA CRUZ BOMB DROPS, NOVELTY WEDGES, AND OFF TO LOS ANGELES FOR THE WEDGE! WE ARE HAVING TOO MUCH FUN IN CALIFORNIA!", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/q0yVuNANAkY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/q0yVuNANAkY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/q0yVuNANAkY/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/i_OOiFOlnCtf_ALcsaAw7Ufo4bg\"", "id": { "kind": "youtube#video", "videoId": "SVDcJK8rSlQ" }, "snippet": { "publishedAt": "2019-10-24T19:00:02.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "PUMPING SURF IN OCTOBER", "description": "STARTED THE DAY OFF WITH LITTLE SHORE BREAK BARRELS AT KEIKI BEACH TO BIGGER SURFING BARRELS WITH JUST A FEW OF MY FRIENDS AT ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/SVDcJK8rSlQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/SVDcJK8rSlQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/SVDcJK8rSlQ/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/7LwrprFd6InKtKBmdjzvZahcxSc\"", "id": { "kind": "youtube#video", "videoId": "_azIoZ50zuM" }, "snippet": { "publishedAt": "2013-04-07T23:38:06.000Z", "channelId": "UCSZy7dboa_o9X8itlpQx7yw", "title": "Local Style - Best Surf Breaks in Bali Indonesia, Episode 9", "description": "http://www.thesurfchannel.com http://www.facebook.com/localstylesurf?fref=ts Music: 'Malia' by Sashamon Photo: Dalmas Bukit Peninsula in Bali is one of the ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/_azIoZ50zuM/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/_azIoZ50zuM/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/_azIoZ50zuM/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Surf Channel Television Network", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/IuYgBGy2WstW-sQM6YfVO11Tmvc\"", "id": { "kind": "youtube#video", "videoId": "GO1ZC_997MM" }, "snippet": { "publishedAt": "2019-09-01T22:16:28.000Z", "channelId": "UCuZSTHZf3vd7eVehhnotcsg", "title": "The TOP 5 Surfing Mistakes | Learning How To Surf", "description": "When learning to surf there are many mistakes that surfers can make. In this video we explore the most common mistakes and how you can avoid making them.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/GO1ZC_997MM/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/GO1ZC_997MM/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/GO1ZC_997MM/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "How to Rip", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/3kofrY51480WPJdJyTJxLSbE_bs\"", "id": { "kind": "youtube#video", "videoId": "vk0F8dHo3wU" }, "snippet": { "publishedAt": "2015-10-14T13:45:47.000Z", "channelId": "UC-Zt7GPzlrPPQexkG9-shPg", "title": ""Pacific Dreams" A California Surfing Film", "description": "\"Pacific Dreams\" is a surfing movie featuring my 2015 footage shot around the beautiful state of California. Buy Merch: ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/vk0F8dHo3wU/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/vk0F8dHo3wU/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/vk0F8dHo3wU/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Surf Rinse Repeat", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/qYSQyC-A4aYQyT_K8sa1LPdT5Ew\"", "id": { "kind": "youtube#video", "videoId": "GHzvMcYqJqk" }, "snippet": { "publishedAt": "2019-07-18T08:59:33.000Z", "channelId": "UCuZSTHZf3vd7eVehhnotcsg", "title": "The MOST Important SURFING Technique Used By The Pro’s", "description": "How to surf. The unending question of surfing performance - could it be as simple as identifying key aspects of body movement and mimicking those movements ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/GHzvMcYqJqk/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/GHzvMcYqJqk/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/GHzvMcYqJqk/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "How to Rip", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/zc8AwWji78nEFu-q-NDXkNgNKTw\"", "id": { "kind": "youtube#video", "videoId": "w7DoXnRYMUg" }, "snippet": { "publishedAt": "2014-05-14T13:00:04.000Z", "channelId": "UCblfuW_4rakIf2h6aqANefA", "title": "Subzero Surfing in Nova Scotia - Sally Stories Season 2 - Ep 5", "description": "Find the best swells in the world here: http://win.gs/1alYVe2 The world tour may be over for another year but Sally's quest for that elusive world title is ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/w7DoXnRYMUg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/w7DoXnRYMUg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/w7DoXnRYMUg/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Red Bull", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/K9rACPHz4VTQAV-0WRwPs5sOBo0\"", "id": { "kind": "youtube#video", "videoId": "hc0lYWSJ0-0" }, "snippet": { "publishedAt": "2018-11-24T05:48:39.000Z", "channelId": "UCuZSTHZf3vd7eVehhnotcsg", "title": "Small Wave Surfing Secrets", "description": "This is the next video in our Small Wave Surfing Series! We cover all the hints and tips you will need to make the most of small wave surfing! Ryan and Kale surf ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/hc0lYWSJ0-0/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/hc0lYWSJ0-0/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/hc0lYWSJ0-0/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "How to Rip", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/puaW26DfNs0BwdGhQ_kUAQr4Vb0\"", "id": { "kind": "youtube#video", "videoId": "dVr3m9S0cEk" }, "snippet": { "publishedAt": "2019-08-19T16:55:56.000Z", "channelId": "UCphTF9wHwhCt-BzIq-s4V-g", "title": "What If You Tried to Surf a Tsunami?", "description": "Is surfing a tsunami even possible? And have people done this before? Thanks, Audible! Start listening with a 30-day trial and your first audiobook plus two ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/dVr3m9S0cEk/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/dVr3m9S0cEk/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/dVr3m9S0cEk/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "What If", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/j9grghmhodulrxd04LMAYUHGG8g\"", "id": { "kind": "youtube#video", "videoId": "A_0tgAVjQPw" }, "snippet": { "publishedAt": "2018-12-18T03:46:00.000Z", "channelId": "UCnJ0mt5Cgx4ER_LhTijG_4A", "title": "2018 Billabong Pipe Masters - Final Day Highlights | Triple Crown of Surfing | VANS", "description": "The waves were going off, Medina was going off. Watch the best moments of the final day of the Billabong Pipe Masters! Vans Triple Crown of Surfing is going ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/A_0tgAVjQPw/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/A_0tgAVjQPw/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/A_0tgAVjQPw/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Vans", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/f4B92xUeptcNLBwHd4cnYEjNyZE\"", "id": { "kind": "youtube#video", "videoId": "eSwisMEtkBg" }, "snippet": { "publishedAt": "2018-09-02T01:56:19.000Z", "channelId": "UCfn_qdZ1XMLRKIfMhexjooA", "title": "What Surfing Is Actually Like", "description": "The Gorpo I Use: https://amzn.to/2COZ1hQ The Mouth Mount I Use: https://amzn.to/2ClyqYM My Free Vlog Like A Pro Course: http://startavlog.com Favorite ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/eSwisMEtkBg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/eSwisMEtkBg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/eSwisMEtkBg/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Atua Mo'e", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/dZ7qWwmgtXL4M38OuQz6PyTYomI\"", "id": { "kind": "youtube#video", "videoId": "koidZdKL2HY" }, "snippet": { "publishedAt": "2019-08-09T19:00:04.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "SURFING IN BALI WITH MY GIRLFRIEND", "description": "TINA WON THE FIRST HEAT AND IM BACK FOR ANOTHER SHOT BEFORE WE ABSOLUTELY SCORE PERFECT KERAMAS CHECK OUT MY MERCH: ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/koidZdKL2HY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/koidZdKL2HY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/koidZdKL2HY/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/PWh9CBJQJTtiT1eLEFdXuTraxUc\"", "id": { "kind": "youtube#video", "videoId": "4sqcVvWBK08" }, "snippet": { "publishedAt": "2019-11-29T18:30:02.000Z", "channelId": "UCsG5dkqFUHZO6eY9uOzQqow", "title": ""Go Easy On The Zambezi" River Surfing In Africa With Harry Bryant, Mikey Feb and Dylan Graves", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/4sqcVvWBK08/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/4sqcVvWBK08/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/4sqcVvWBK08/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Stab Magazine", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Xd0Upa2qCNmlJi3cBxzDUa8N_CY\"", "id": { "kind": "youtube#video", "videoId": "At4T3Ujv4xk" }, "snippet": { "publishedAt": "2019-04-02T21:00:00.000Z", "channelId": "UCZFhj_r-MjoPCFVUo3E1ZRg", "title": "10-Year-Old PRO Skater & Surfing PRODIGY | Sky Brown", "description": "Sponsored by #MilkIt! 10-year-old Sky Brown can skate & surf with the best of them! COMMENT with a sport you want to see on No Days Off next! NEW NO ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/At4T3Ujv4xk/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/At4T3Ujv4xk/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/At4T3Ujv4xk/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Whistle", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/AD7sXFmNIsziect608vZjRHozU8\"", "id": { "kind": "youtube#video", "videoId": "wxBtwCZtDAg" }, "snippet": { "publishedAt": "2018-11-21T01:00:11.000Z", "channelId": "UCZFhj_r-MjoPCFVUo3E1ZRg", "title": "13-Year-Old FEARLESS Surfing Prodigy", "description": "NEW No Days Off gear: https://whistle.video/NoDaysOffMerch 13-Year-Old Kai Williams was born to surf. Nobody works harder at their craft! COMMENT with ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/wxBtwCZtDAg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/wxBtwCZtDAg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/wxBtwCZtDAg/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Whistle", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/f78GSdxpM_beAxdRfJCNqKNd3ho\"", "id": { "kind": "youtube#video", "videoId": "oSR8irQRdWs" }, "snippet": { "publishedAt": "2019-03-06T12:23:21.000Z", "channelId": "UCLtWzMJSuuaTjBYe7IYVpCw", "title": "Surfing's most insane 10's", "description": "Check out Surfing's greatest airs ever here: https://youtu.be/Y3iogCFIUOo Subscribe for more frequent surf compilations and content. Instagram: slaptv Footage ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/oSR8irQRdWs/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/oSR8irQRdWs/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/oSR8irQRdWs/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "slaptv", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/31svwC9OwSMqzQiED2GkYCUOVa4\"", "id": { "kind": "youtube#video", "videoId": "i2xVrRrxzVE" }, "snippet": { "publishedAt": "2019-08-19T19:00:04.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "SURFING THE AUSTRALIAN WEDGE", "description": "CHASING SWELLS STRAIGHT FROM BALI TO AUSTRALIA, SURFING WEDGES WITH THE BOYS, STAYING REALLY PSYCHED, AND TALKING TO SHANE ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/i2xVrRrxzVE/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/i2xVrRrxzVE/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/i2xVrRrxzVE/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/-W0fYvWJUixRPkPGp05-2EWJNUQ\"", "id": { "kind": "youtube#video", "videoId": "IurVzNwneYo" }, "snippet": { "publishedAt": "2017-08-01T00:41:25.000Z", "channelId": "UCIGO64hbbGBs2o2wa8xXjwA", "title": "[SURF JAPAN] TOP 6 BEST SURFS SPOTS IN JAPAN", "description": "Une petite vidéo des 6 meilleurs des endroits au japon ou il y a les meilleurs spots pour surfer ! Beau Paysage et belle cascade vous attendent ! N'oubliez pas ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/IurVzNwneYo/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/IurVzNwneYo/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/IurVzNwneYo/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Yunako TV", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/AHSycI2tynUVfUL7B0qODZCTxCM\"", "id": { "kind": "youtube#video", "videoId": "zQVpzCzQa1U" }, "snippet": { "publishedAt": "2018-09-13T21:38:34.000Z", "channelId": "UCf5CA0OsvhhU-6AcSjT1oKQ", "title": "Surfing PERFECT waves at TEXAS Wavepool", "description": "I was fortunate enough to visit the BSR wave pool in Waco Texas with Catch Surf pro riders Tyler Stanaland, Blair Conklin, Kalani Robb, Johnny Redmond and ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/zQVpzCzQa1U/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/zQVpzCzQa1U/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/zQVpzCzQa1U/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "BEEFS T.V.", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/qPLoN7ozjPU6_u8uY29bI1821ZA\"", "id": { "kind": "youtube#video", "videoId": "wSsjla8BBHA" }, "snippet": { "publishedAt": "2019-08-22T19:00:08.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "SURFING AUSTRALIAN PIPELINE?", "description": "CHASING SWELLS STRAIGHT FROM BALI TO AUSTRALIA, SURFING WEDGES WITH THE BOYS, STAYING REALLY PSYCHED, AND TALKING TO SHANE ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/wSsjla8BBHA/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/wSsjla8BBHA/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/wSsjla8BBHA/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/hAvlnbuBu7SPUmCCf9BkV4uG97c\"", "id": { "kind": "youtube#video", "videoId": "BUhJwdet8Rs" }, "snippet": { "publishedAt": "2019-08-27T18:22:52.000Z", "channelId": "UCKo-NbWOxnxBnU41b-AoKeA", "title": "Why Alaska Might Be Surfing’s Greatest Frontier | WITHIN REACH (4K EDITION) | SURFER", "description": "Josh Mulcoy's first time to Alaska was for SURFER way back in the early '90s, and it landed him a cover Opens a New Window. . That trip also began an ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/BUhJwdet8Rs/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/BUhJwdet8Rs/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/BUhJwdet8Rs/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "SURFER", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/U1_e-wKYZrl5wfKA7Z31Jqo3i7s\"", "id": { "kind": "youtube#video", "videoId": "bbMCFIfskd4" }, "snippet": { "publishedAt": "2019-11-11T19:00:07.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "ALMOST BROKE MY LEG SURFING! (PIPELINE)", "description": "SURFING MASSIVE WAVES AT BANZAI PIPELINE TO SURFING LONG BOARD WAVES WITH TINA AND FRIENDS. CHECK OUT MY MERCH: WWW.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/bbMCFIfskd4/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/bbMCFIfskd4/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/bbMCFIfskd4/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/V3raa3z1l54O4nPc_yUcNXonaYk\"", "id": { "kind": "youtube#video", "videoId": "gm7eT0MGt2Y" }, "snippet": { "publishedAt": "2014-04-25T16:00:07.000Z", "channelId": "UCqhnX4jA0A5paNd1v-zEysw", "title": "GoPro: Endless Barrels - GoPro of the Winter 2013-14 powered by Surfline", "description": "Shot 100% on the HD HERO3+® camera from http://GoPro.com. Congratulations to Jamie O'Brien for his $20000 GoPro of the Winter winning clip. The GoPro of ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/gm7eT0MGt2Y/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/gm7eT0MGt2Y/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/gm7eT0MGt2Y/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "GoPro", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/pYFbBUoCwrwiRlGwtYHHdQYunSs\"", "id": { "kind": "youtube#video", "videoId": "y92SwP4odAk" }, "snippet": { "publishedAt": "2019-01-16T18:57:38.000Z", "channelId": "UCtinbF-Q-fVthA0qrFQTgXQ", "title": "Surfing with GREAT WHITE SHARKS at DUNGEONS SOUTH AFRICA", "description": "Animal Oceans - https://www.animalocean.co.za Dan - https://www.youtube.com/user/DanTheDirector1 Music - HUSBANDS: ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/y92SwP4odAk/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/y92SwP4odAk/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/y92SwP4odAk/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "CaseyNeistat", "liveBroadcastContent": "none" } } ] } ================================================ FILE: testdata/apidata/search/search_by_location.json ================================================ { "kind": "youtube#searchListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/KRABNKy-TvvaFRB9poJwJHu7_CA\"", "nextPageToken": "CAUQAA", "regionCode": "US", "pageInfo": { "totalResults": 83, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/z-4-8ZS7WBjxdAkQsCYIrkc2Yi4\"", "id": { "kind": "youtube#video", "videoId": "OU4d3O_VZCk" }, "snippet": { "publishedAt": "2020-02-06T19:00:04.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "SURFING WITH KELLY SLATER (HAWAII)", "description": "FUN BARRELS, BEATER SPINS, AND GOOD TIMES WITH SOME OF THE REDBULL SKATEBOARD TEAM! STAY PSYCHED MERCH! HTTP://WWW.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/OU4d3O_VZCk/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/OU4d3O_VZCk/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/OU4d3O_VZCk/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/ehUlorWN-KIwVssROIKJNyKAlts\"", "id": { "kind": "youtube#video", "videoId": "R5n6TgvOLgA" }, "snippet": { "publishedAt": "2020-02-17T19:00:03.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "DREAMY SURF AT BANZAI PIPELINE", "description": "CRISTAL CLEAR WATERS, NEW SURFBOARD, AND NO CROWD WITH SOME FUN SIZED WAVES... BEST DAY EVER!! CHECK OUT MY MERCH: ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/R5n6TgvOLgA/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/R5n6TgvOLgA/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/R5n6TgvOLgA/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/PYZ4gleO499ovnEt8_WXFyD2ZlA\"", "id": { "kind": "youtube#video", "videoId": "R86-eGa96bQ" }, "snippet": { "publishedAt": "2020-02-13T19:00:04.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "SURFING WITH 11x WORLD CHAMPION SURFER!! Scored Pipeline, Waimea River & Cleaned Up 300 lbs Of Trash", "description": "GOOD WEATHER OR BAD WEATHER, WE MAKE SURE WE HAVE THE MOST FUN EVERY SINGLE DAY. FUN SURF AT PIPELINE WITH KELLY SLATER ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/R86-eGa96bQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/R86-eGa96bQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/R86-eGa96bQ/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/yxmO5YZFScEXiQHvIJyAJc7Ophc\"", "id": { "kind": "youtube#video", "videoId": "dnFxU2mik74" }, "snippet": { "publishedAt": "2020-02-09T19:00:05.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "JAMIE O'BRIEN NEAR DEATH SURFING PIPELINE (OAHU, HAWAII)", "description": "SUBSCRIBE: https://www.youtube.com/jamieobrienjob FIND JAMIE ON INSTAGRAM: https://www.instagram.com/WHOISJOB/ EDITOR: ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/dnFxU2mik74/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/dnFxU2mik74/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/dnFxU2mik74/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Q4uSEXp12CwRiMVDqNa7TmsGeHw\"", "id": { "kind": "youtube#video", "videoId": "drQjl1rsj28" }, "snippet": { "publishedAt": "2020-02-20T19:00:04.000Z", "channelId": "UCo_q6aOlvPH7M-j_XGWVgXg", "title": "PERFECT BARRELS AT BANZAI PIPELINE (POINT OF VIEW)", "description": "WHAT IT REALLY LOOKS LIKE OUT AT BANZAI PIPELINE. BIG, STEEP, INTENSE WAVES, AND YOU GET THE FRONT ROW SEAT! CHECK OUT MY ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/drQjl1rsj28/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/drQjl1rsj28/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/drQjl1rsj28/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Jamie O'Brien", "liveBroadcastContent": "none" } } ] } ================================================ FILE: testdata/apidata/search/search_by_mine.json ================================================ { "kind": "youtube#searchListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/2d1a6UDYt8tuTka8VkEd_2F0uNM\"", "nextPageToken": "Cib3_fGxJP____9YZnJjZmlob194TQD_Af_-WGZyY2ZpaG9feE0AARACIQVwDsDzTq1QOQAAAADbTg4CSAFQAloLCZ9XfdhCQCTNEANguYajdA==", "pageInfo": { "totalResults": 2, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/NajmzO_gHVKkR5q0QtF_ia9PnT0\"", "id": { "kind": "youtube#video", "videoId": "JE8xdDp5B8Q" }, "snippet": { "publishedAt": "2019-11-29T02:56:49.000Z", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "华山日出", "description": "冷冷的山头", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "ikaros-life", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/AMxXvT-JEdxPv-H_aJmZX28aLQM\"", "id": { "kind": "youtube#video", "videoId": "Xfrcfiho_xM" }, "snippet": { "publishedAt": "2019-11-29T02:51:17.000Z", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "海上日出", "description": "美美美", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "ikaros-life", "liveBroadcastContent": "none" } } ] } ================================================ FILE: testdata/apidata/search/search_by_related_video.json ================================================ { "kind": "youtube#searchListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Jt-dx_Bx1OzzEBghaPZjhe1AeIc\"", "nextPageToken": "CAUQAA", "regionCode": "US", "pageInfo": { "totalResults": 119, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/egH1J4nEGcCF7cEL3aIwdZWZRvs\"", "id": { "kind": "youtube#video", "videoId": "eIho2S0ZahI" }, "snippet": { "publishedAt": "2014-06-27T14:18:00.000Z", "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "title": "How to speak so that people want to listen | Julian Treasure", "description": "Have you ever felt like you're talking, but nobody is listening? Here's Julian Treasure to help you fix that. As the sound expert demonstrates some useful vocal exercises and shares tips on how to speak with empathy, he offers his vision for a sonorous world of listening and understanding.\n\nGet TED Talks recommended just for you! Learn more at https://www.ted.com/signup.\n\nThe TED Talks channel features the best talks and performances from the TED Conference, where the world's leading thinkers and doers give the talk of their lives in 18 minutes (or less). Look for talks on Technology, Entertainment and Design -- plus science, business, global issues, the arts and more.\n\nFollow TED on Twitter: http://www.twitter.com/TEDTalks\nLike TED on Facebook: https://www.facebook.com/TED\n\nSubscribe to our channel: https://www.youtube.com/TED", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/eIho2S0ZahI/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/eIho2S0ZahI/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/eIho2S0ZahI/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "TED", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/OsttkLOQ3t6OgMhtZA2_80MOA30\"", "id": { "kind": "youtube#video", "videoId": "RcGyVTAoXEU" }, "snippet": { "publishedAt": "2013-09-03T21:19:54.000Z", "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "title": "How to make stress your friend | Kelly McGonigal", "description": "Stress. It makes your heart pound, your breathing quicken and your forehead sweat. But while stress has been made into a public health enemy, new research suggests that stress may only be bad for you if you believe that to be the case. Psychologist Kelly McGonigal urges us to see stress as a positive, and introduces us to an unsung mechanism for stress reduction: reaching out to others.\n\nGet TED Talks recommended just for you! Learn more at https://www.ted.com/signup.\n\nThe TED Talks channel features the best talks and performances from the TED Conference, where the world's leading thinkers and doers give the talk of their lives in 18 minutes (or less). Look for talks on Technology, Entertainment and Design -- plus science, business, global issues, the arts and more.\n\nFollow TED on Twitter: http://www.twitter.com/TEDTalks\nLike TED on Facebook: https://www.facebook.com/TED\n\nSubscribe to our channel: https://www.youtube.com/TED", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/RcGyVTAoXEU/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/RcGyVTAoXEU/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/RcGyVTAoXEU/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "TED", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/KmhbLy_Yw-hcmuSJ5wF0d3eoufQ\"", "id": { "kind": "youtube#video", "videoId": "VQRjouwKDlU" }, "snippet": { "publishedAt": "2016-10-07T16:20:19.000Z", "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "title": "4 reasons to learn a new language | John McWhorter", "description": "English is fast becoming the world's universal language, and instant translation technology is improving every year. So why bother learning a foreign language? Linguist and Columbia professor John McWhorter shares four alluring benefits of learning an unfamiliar tongue.\n\nTEDTalks is a daily video podcast of the best talks and performances from the TED Conference, where the world's leading thinkers and doers give the talk of their lives in 18 minutes (or less). Look for talks on Technology, Entertainment and Design -- plus science, business, global issues, the arts and much more.\nFind closed captions and translated subtitles in many languages at http://www.ted.com/translate\n\nFollow TED news on Twitter: http://www.twitter.com/tednews\nLike TED on Facebook: https://www.facebook.com/TED\n\nSubscribe to our channel: http://www.youtube.com/user/TEDtalksDirector", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/VQRjouwKDlU/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/VQRjouwKDlU/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/VQRjouwKDlU/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "TED", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/S-MWeTCziM6r5Zr1EynjTodYOhY\"", "id": { "kind": "youtube#video", "videoId": "_v36Vt9GmH8" }, "snippet": { "publishedAt": "2016-05-02T17:33:45.000Z", "channelId": "UCsT0YIqwnpJCM-mx7-gSA4Q", "title": "Body Language: The Key to Your Subconscious | Ann Washburn | TEDxIdahoFalls", "description": "How we hold our body both demonstrates and determines who we are and our level of success. What are you telling people about yourself? Or worse, what are you telling your self about yourself? \n\nHow we hold our body both demonstrates and determines who we are and our level of success. What are you telling people about yourself? Or worse, what are you telling your self about yourself? \n\nThis talk was given at a TEDx event using the TED conference format but independently organized by a local community. Learn more at http://ted.com/tedx", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/_v36Vt9GmH8/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/_v36Vt9GmH8/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/_v36Vt9GmH8/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "TEDx Talks", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/tr49eHnX6J7gk65hhZfaGoP6kYM\"", "id": { "kind": "youtube#video", "videoId": "9kxL9Cf46VM" }, "snippet": { "publishedAt": "2012-12-12T17:34:13.000Z", "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "title": "A Saudi, an Indian and an Iranian walk into a Qatari bar ... | Maz Jobrani", "description": "Iranian-American comedian Maz Jobrani takes to the TEDxSummit stage in Doha, Qatar to take on serious issues in the Middle East -- like how many kisses to give when saying \"Hi,\" and what not to say on an American airplane.\n\nGet TED Talks recommended just for you! Learn more at https://www.ted.com/signup.\n\nThe TED Talks channel features the best talks and performances from the TED Conference, where the world's leading thinkers and doers give the talk of their lives in 18 minutes (or less). Look for talks on Technology, Entertainment and Design -- plus science, business, global issues, the arts and more.\n\nFollow TED on Twitter: http://www.twitter.com/TEDTalks\nLike TED on Facebook: https://www.facebook.com/TED\n\nSubscribe to our channel: https://www.youtube.com/TED", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/9kxL9Cf46VM/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/9kxL9Cf46VM/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/9kxL9Cf46VM/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "TED", "liveBroadcastContent": "none" } } ] } ================================================ FILE: testdata/apidata/search/search_channels.json ================================================ { "kind": "youtube#searchListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/CvjFcA434WC9odlJ9qApNTqQE3Y\"", "nextPageToken": "CAUQAA", "regionCode": "US", "pageInfo": { "totalResults": 1000000, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/HjEU7hR_OXoK_1u0-g1CqVS1RH0\"", "id": { "kind": "youtube#channel", "channelId": "UCxRULEz6kS0PMxCzOY25GhQ" }, "snippet": { "publishedAt": "2009-09-02T23:35:38.000Z", "channelId": "UCxRULEz6kS0PMxCzOY25GhQ", "title": "NLaFourcadeVEVO", "description": "Consigue más música de Natalia Lafourcade aquí: http://smarturl.it/lafourcade Escucha su lista oficial en Spotify: http://smarturl.it/nloficialsp Síguela en redes ...", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yUgxca4sWt0/AAAAAAAAAAI/AAAAAAAAAAA/IE61lBuRSd8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yUgxca4sWt0/AAAAAAAAAAI/AAAAAAAAAAA/IE61lBuRSd8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yUgxca4sWt0/AAAAAAAAAAI/AAAAAAAAAAA/IE61lBuRSd8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } }, "channelTitle": "NLaFourcadeVEVO", "liveBroadcastContent": "upcoming" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/1pKCiT2tOMI9S7prcPE4MY-7nq4\"", "id": { "kind": "youtube#channel", "channelId": "UCBGVFobD9nXtTxBdMbDZfnQ" }, "snippet": { "publishedAt": "2007-11-15T15:12:06.000Z", "channelId": "UCBGVFobD9nXtTxBdMbDZfnQ", "title": "fritz5139", "description": "Channel with pop songs of the 70s Music was my first love, and it will be my last. Music unites the people all over the world In this channel I will upload 40 music ...", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-40EYesesZNE/AAAAAAAAAAI/AAAAAAAAAAA/AumRpR60plo/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-40EYesesZNE/AAAAAAAAAAI/AAAAAAAAAAA/AumRpR60plo/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-40EYesesZNE/AAAAAAAAAAI/AAAAAAAAAAA/AumRpR60plo/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } }, "channelTitle": "fritz5139", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/6NB-kVFErMEmn8dD_FAEK_O7JBQ\"", "id": { "kind": "youtube#channel", "channelId": "UCHFz3F_wTBHhOMqTTlAeNTA" }, "snippet": { "publishedAt": "2015-12-30T20:42:55.000Z", "channelId": "UCHFz3F_wTBHhOMqTTlAeNTA", "title": "The Big Jackpot", "description": "I like to play slot machines at the casino. From Top Dollar, to Lightning Link, to Huff N' Puff, to Black Widow, to Dragon Link, and hundreds more, I play ONLY ...", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-3G0oiIWgcIc/AAAAAAAAAAI/AAAAAAAAAAA/Y-QZ2wRDjus/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-3G0oiIWgcIc/AAAAAAAAAAI/AAAAAAAAAAA/Y-QZ2wRDjus/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-3G0oiIWgcIc/AAAAAAAAAAI/AAAAAAAAAAA/Y-QZ2wRDjus/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } }, "channelTitle": "The Big Jackpot", "liveBroadcastContent": "upcoming" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/phIwxWP55wQ6k2kQyoysNwCx694\"", "id": { "kind": "youtube#channel", "channelId": "UCiMhD4jzUqG-IgPzUmmytRQ" }, "snippet": { "publishedAt": "2008-07-07T13:35:27.000Z", "channelId": "UCiMhD4jzUqG-IgPzUmmytRQ", "title": "Queen Official", "description": "Welcome to the official Queen channel. Subscribe today for exclusive Queen videos, including live performances, interviews, official videos, behind-the-scenes ...", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-S1HWgr--7So/AAAAAAAAAAI/AAAAAAAAAAA/lqWNX9pJni8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-S1HWgr--7So/AAAAAAAAAAI/AAAAAAAAAAA/lqWNX9pJni8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-S1HWgr--7So/AAAAAAAAAAI/AAAAAAAAAAA/lqWNX9pJni8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } }, "channelTitle": "Queen Official", "liveBroadcastContent": "upcoming" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/LYJC4LDYx_LfXe6eJLgSPmrOCKc\"", "id": { "kind": "youtube#channel", "channelId": "UCEqbPy-ZcK2gR8X1KEtJwpQ" }, "snippet": { "publishedAt": "2006-09-10T17:13:54.000Z", "channelId": "UCEqbPy-ZcK2gR8X1KEtJwpQ", "title": "Robert Morecook", "description": "Military Music from around the World Musica Militar de todo del mundo Martial Music.", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-uv59_RwyuUo/AAAAAAAAAAI/AAAAAAAAAAA/srnldUUxBtk/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-uv59_RwyuUo/AAAAAAAAAAI/AAAAAAAAAAA/srnldUUxBtk/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-uv59_RwyuUo/AAAAAAAAAAI/AAAAAAAAAAA/srnldUUxBtk/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } }, "channelTitle": "Robert Morecook", "liveBroadcastContent": "none" } } ] } ================================================ FILE: testdata/apidata/search/search_videos_by_channel.json ================================================ { "kind": "youtube#searchListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/qDAZEJAlbMKs4-6fpruc8u6toc4\"", "nextPageToken": "CAUQAA", "regionCode": "US", "pageInfo": { "totalResults": 81297, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/N6Cmq8y3KhHdFk-mVd7aFwW_U34\"", "id": { "kind": "youtube#video", "videoId": "LrQWzOkC0XQ" }, "snippet": { "publishedAt": "2018-10-08T22:51:23.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Code Cleanup and Fixing Pub Versioning in Hacker News App (The Boring Flutter Dev Show, Ep. 8.2)", "description": "In this first segment, Filip and Emily update the app to pull Hacker News stories live from the Hacker News API, instead of presenting hardcoded stories; they ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/LrQWzOkC0XQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/LrQWzOkC0XQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/LrQWzOkC0XQ/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/R5xfc7-ZMB5vjNr4inKwmNeRuWM\"", "id": { "kind": "youtube#video", "videoId": "lVQ1EKR1v1I" }, "snippet": { "publishedAt": "2011-09-12T20:55:45.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "YouTube's API and The News", "description": "On July 20, 2011, YouTube and Link TV hosted a Hacks/Hackers meetup in San Francisco for a first-person look at innovative news projects using YouTube's ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/lVQ1EKR1v1I/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/lVQ1EKR1v1I/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/lVQ1EKR1v1I/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/hJOHKkFuHJmaRSOr6eukMOQ6QNY\"", "id": { "kind": "youtube#video", "videoId": "Bud7XR8crWw" }, "snippet": { "publishedAt": "2018-10-08T22:47:38.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Adding Caching to the Hacker News App (The Boring Flutter Development Show, Ep. 8.3)", "description": "In this episode of the Boring Show, Filip and Emily showcase a workaround for adding the total comment count, per story, in their Hacker News Reader app.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Bud7XR8crWw/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Bud7XR8crWw/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Bud7XR8crWw/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/9OAtGs7Xc-48ZJrJ20f7sgxOnp4\"", "id": { "kind": "youtube#video", "videoId": "k_NtkiMAC-o" }, "snippet": { "publishedAt": "2013-08-14T18:01:59.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "YouTube Developers Live: Storyful, The News Agency of The Social Media Age", "description": "Storfyful is the news agency of the social media age. It provides verified social media content using a mix of human and algorithmic techniques. This week we'll ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/k_NtkiMAC-o/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/k_NtkiMAC-o/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/k_NtkiMAC-o/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/LS7osmF_lfU6MCsh0sOh-Cetex4\"", "id": { "kind": "youtube#video", "videoId": "dfweWyVScaI" }, "snippet": { "publishedAt": "2014-09-03T00:53:44.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Breaking News at 1000ms with Patrick Hamann", "description": "Patrick is a senior client-side engineer at the Guardian in London where – amongst other things – he is helping to build the next generation of their web platform.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/dfweWyVScaI/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/dfweWyVScaI/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/dfweWyVScaI/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } } ] } ================================================ FILE: testdata/apidata/subscriptions/insert_response.json ================================================ { "kind": "youtube#subscription", "etag": "BBbHqFIch0N1EhR1bwn0s3MofFg", "id": "POsnRIYsMcp1Cghr_Fsh-6uFZRcIHmTKzzByiv9ZAro", "snippet": { "publishedAt": "2022-11-16T11:02:09.19802Z", "title": "iQIYI 综艺精选", "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", "resourceId": { "kind": "youtube#channel", "channelId": "UCQ6ptCagG3W0Bf4lexvnBEg" }, "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/kszyCtmb0Bsjo-Pwp6lhZnAA6wMP8gzqNU9qZOj-b9p2GvwF4yygtPBzuRpmGUPtymByYol9Oj8=s88-c-k-c0x00ffffff-no-rj" }, "medium": { "url": "https://yt3.ggpht.com/kszyCtmb0Bsjo-Pwp6lhZnAA6wMP8gzqNU9qZOj-b9p2GvwF4yygtPBzuRpmGUPtymByYol9Oj8=s240-c-k-c0x00ffffff-no-rj" }, "high": { "url": "https://yt3.ggpht.com/kszyCtmb0Bsjo-Pwp6lhZnAA6wMP8gzqNU9qZOj-b9p2GvwF4yygtPBzuRpmGUPtymByYol9Oj8=s800-c-k-c0x00ffffff-no-rj" } } }, "contentDetails": { "totalItemCount": 6986, "newItemCount": 0, "activityType": "all" }, "subscriberSnippet": { "title": "ikaros data", "description": "This is a test channel.", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s88-c-k-c0x00ffffff-no-rj" }, "medium": { "url": "https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s240-c-k-c0x00ffffff-no-rj" }, "high": { "url": "https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s800-c-k-c0x00ffffff-no-rj" } } } } ================================================ FILE: testdata/apidata/subscriptions/subscription_zero.json ================================================ { "kind": "youtube#subscriptionListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ewwRz0VbTYpp2EGbOkvZ5M_1mbo\"", "pageInfo": { "totalResults": 0, "resultsPerPage": 5 }, "items": [] } ================================================ FILE: testdata/apidata/subscriptions/subscriptions_by_channel_p1.json ================================================ { "kind": "youtube#subscriptionListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/NtDoQfEOjOm9UE8PH2xuzaVGuko\"", "nextPageToken": "CAUQAA", "pageInfo": { "totalResults": 7, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ah58pA0LZUrj8p8Ay6v9PqipM1E\"", "id": "FMP3Mleijt-52zZDGkHtR5KhwkvCcdQKWWWIA1j5eGc", "snippet": { "publishedAt": "2011-11-11T14:00:19.000Z", "title": "TEDx Talks", "description": "TEDx is an international community that organizes TED-style events anywhere and everywhere -- celebrating locally-driven ideas and elevating them to a global stage. TEDx events are produced independently of TED conferences, each event curates speakers on their own, but based on TED's format and rules.\n\nFor more information on using TED for commercial purposes (e.g. employee learning, in a film, or in an online course), please submit a media request using the link below.", "resourceId": { "kind": "youtube#channel", "channelId": "UCsT0YIqwnpJCM-mx7-gSA4Q" }, "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/5Fb7lQjSSevEAy86E2VxNZ43Kzg\"", "id": "FMP3Mleijt_ZKvy5M-HhRlsqI4wXY7VmP5g8lvmRhVU", "snippet": { "publishedAt": "2016-11-17T16:24:18.000Z", "title": "TED Residency", "description": "The TED Residency program is an incubator for breakthrough ideas. It is free and open to all via a semi-annual competitive application. Those chosen as TED Residents spend four months at TED headquarters in New York City, working on their idea. Selection criteria include the strength of their idea, their character, and their ability to bring a fresh perspective and positive contribution to the diverse TED community.", "resourceId": { "kind": "youtube#channel", "channelId": "UCtC8aQzdEHAmuw8YvtH1CcQ" }, "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/sr4uYsDNIOJdZ3aURvsOrmYGrCA\"", "id": "FMP3Mleijt-52zZDGkHtRypBo_RsBLYwvw2DbDoGUl8", "snippet": { "publishedAt": "2013-03-04T18:00:00.000Z", "title": "TED-Ed", "description": "TED-Ed’s commitment to creating lessons worth sharing is an extension of TED’s mission of spreading great ideas. Within TED-Ed’s growing library of TED-Ed animations, you will find carefully curated educational videos, many of which represent collaborations between talented educators and animators nominated through the TED-Ed website (ed.ted.com).\n\nWant to suggest an idea for a TED-Ed animation or get involved with TED-Ed? Visit our website at: http://ed.ted.com/get_involved.\n\nAlso, consider donating to us on Patreon! By doing so, you directly support our mission and receive some pretty awesome rewards: https://www.patreon.com/teded\n\nFor more information on using TED for commercial purposes (e.g. employee learning, in a film, or in an online course), please submit a Media Request using this link: https://media-requests.ted.com/", "resourceId": { "kind": "youtube#channel", "channelId": "UCsooa4yRKGN_zEE8iknghZA" }, "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-mnEpZE0uuus/AAAAAAAAAAI/AAAAAAAAAAA/SM5q4mSZgq4/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-mnEpZE0uuus/AAAAAAAAAAI/AAAAAAAAAAA/SM5q4mSZgq4/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-mnEpZE0uuus/AAAAAAAAAAI/AAAAAAAAAAA/SM5q4mSZgq4/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/UFZvm6H-6qrPRC_qJSp4vk5LWFc\"", "id": "FMP3Mleijt-52zZDGkHtR9rgHNN_tQoAYW8hkbgy-r4", "snippet": { "publishedAt": "2011-11-16T22:33:47.000Z", "title": "TEDxYouth", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UC-yTB2bUcin9mmah36sXiYA" }, "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yJAJEG_PrHk/AAAAAAAAAAI/AAAAAAAAAAA/nHtSZ9elZkM/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yJAJEG_PrHk/AAAAAAAAAAI/AAAAAAAAAAA/nHtSZ9elZkM/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yJAJEG_PrHk/AAAAAAAAAAI/AAAAAAAAAAA/nHtSZ9elZkM/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZAGz9909mfKsIPY3jbzi6Mw9rO4\"", "id": "FMP3Mleijt__etnGmWHe6HXi2qefpnvN-pKf2M_P728", "snippet": { "publishedAt": "2018-11-04T19:27:35.615Z", "title": "TED-Ed Educator Talks", "description": "At TED we believe that education is the ultimate idea worth spreading. That’s why this channel — designed for teachers, by teachers — is exclusively dedicated to celebrating and elevating the ideas of educators working in classrooms and schools throughout the world. Whether you’re here for professional or personal development, you can expect to discover excellent Talks from inspired educators published regularly. Subscribe (and turn on notifications) to learn, and to let educators everywhere know that you’re listening!\n\nAre you an educator interested in developing a Talk for this channel? Do you have a colleague who should give a Talk? Learn more here: ed.ted.com/masterclass\n\nEach featured educator on this channel has completed TED Masterclass, TED's latest program designed to help educators share their ideas by giving TED-style Talks. To learn more about bringing TED Masterclass to your school, district or organization, check out ed.ted.com/masterclass.", "resourceId": { "kind": "youtube#channel", "channelId": "UC9k9wQAp0SrWYZVORZnPIlg" }, "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-Tn5RxuSSd5w/AAAAAAAAAAI/AAAAAAAAAAA/jwPzKVK1pcA/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-Tn5RxuSSd5w/AAAAAAAAAAI/AAAAAAAAAAA/jwPzKVK1pcA/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-Tn5RxuSSd5w/AAAAAAAAAAI/AAAAAAAAAAA/jwPzKVK1pcA/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } } ] } ================================================ FILE: testdata/apidata/subscriptions/subscriptions_by_channel_p2.json ================================================ { "kind": "youtube#subscriptionListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/XhiVa6ArhD2_-Lydooagc6Tq7oc\"", "prevPageToken": "CAUQAQ", "pageInfo": { "totalResults": 7, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/tiNsWzC4jgYHHwZWw2qeoOgWpvY\"", "id": "FMP3Mleijt-52zZDGkHtRx2qxyD4FDSpFDSSTUlt8Hg", "snippet": { "publishedAt": "2011-11-16T22:34:15.000Z", "title": "TED Fellow", "description": "The TED Fellows program brings young innovators from around the world into the TED community in order to amplify the impact of their projects and activities. Learn more at www.ted.com/fellows", "resourceId": { "kind": "youtube#channel", "channelId": "UCBjBZmguQzn6WCYR7DQykLw" }, "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-aL55wuJJvNw/AAAAAAAAAAI/AAAAAAAAAAA/B2mD3TD72q8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-aL55wuJJvNw/AAAAAAAAAAI/AAAAAAAAAAA/B2mD3TD72q8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-aL55wuJJvNw/AAAAAAAAAAI/AAAAAAAAAAA/B2mD3TD72q8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/1P2EiA0I1WFh6jFbN-sYeZoa_rc\"", "id": "FMP3Mleijt-52zZDGkHtR907Ony4G2X5nk8s-zEHODE", "snippet": { "publishedAt": "2013-03-20T15:40:41.000Z", "title": "TEDPartners", "description": "TED is a knowledge-sharing platform -- where collaboration is the glue that binds people together. TED Partnerships allow corporations, TED speakers and the TED community to collaborate in a human-centered way. Learn more at http://partners.ted.com/", "resourceId": { "kind": "youtube#channel", "channelId": "UCDAdYdnCDt9zx3p3e_78lEQ" }, "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-c16Ofh8MU2U/AAAAAAAAAAI/AAAAAAAAAAA/8PIr4YGwkrQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-c16Ofh8MU2U/AAAAAAAAAAI/AAAAAAAAAAA/8PIr4YGwkrQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-c16Ofh8MU2U/AAAAAAAAAAI/AAAAAAAAAAA/8PIr4YGwkrQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } } ] } ================================================ FILE: testdata/apidata/subscriptions/subscriptions_by_channel_with_filter.json ================================================ { "kind": "youtube#subscriptionListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/Bpz_bu12-YiX5_pAavOTWX7GtDQ\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ah58pA0LZUrj8p8Ay6v9PqipM1E\"", "id": "FMP3Mleijt-52zZDGkHtR5KhwkvCcdQKWWWIA1j5eGc", "snippet": { "publishedAt": "2011-11-11T14:00:19.000Z", "title": "TEDx Talks", "description": "TEDx is an international community that organizes TED-style events anywhere and everywhere -- celebrating locally-driven ideas and elevating them to a global stage. TEDx events are produced independently of TED conferences, each event curates speakers on their own, but based on TED's format and rules.\n\nFor more information on using TED for commercial purposes (e.g. employee learning, in a film, or in an online course), please submit a media request using the link below.", "resourceId": { "kind": "youtube#channel", "channelId": "UCsT0YIqwnpJCM-mx7-gSA4Q" }, "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/5Fb7lQjSSevEAy86E2VxNZ43Kzg\"", "id": "FMP3Mleijt_ZKvy5M-HhRlsqI4wXY7VmP5g8lvmRhVU", "snippet": { "publishedAt": "2016-11-17T16:24:18.000Z", "title": "TED Residency", "description": "The TED Residency program is an incubator for breakthrough ideas. It is free and open to all via a semi-annual competitive application. Those chosen as TED Residents spend four months at TED headquarters in New York City, working on their idea. Selection criteria include the strength of their idea, their character, and their ability to bring a fresh perspective and positive contribution to the diverse TED community.", "resourceId": { "kind": "youtube#channel", "channelId": "UCtC8aQzdEHAmuw8YvtH1CcQ" }, "channelId": "UCAuUUnT6oDeKwE6v1NGQxug", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } } ] } ================================================ FILE: testdata/apidata/subscriptions/subscriptions_by_id.json ================================================ { "kind": "youtube#subscriptionListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/USyhytrL1qAH8AxBqW22EUor8kw\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/VIabsyP8MBhapi7K0fjjRX5bM2U\"", "id": "zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo", "snippet": { "publishedAt": "2018-09-11T11:35:04.568Z", "title": "PyCon 2015", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UCgxzjK6GuOHVKR_08TT4hJQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/pYbP9RYZzTnefaJtv-B2uQwsR4A\"", "id": "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo", "snippet": { "publishedAt": "2019-11-29T03:00:56.380Z", "title": "ikaros-life", "description": "This is a test channel.", "resourceId": { "kind": "youtube#channel", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } } ] } ================================================ FILE: testdata/apidata/subscriptions/subscriptions_by_mine_filter.json ================================================ { "kind": "youtube#subscriptionListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/VWi2sv7EuSXCkMveaGb_XUpimn4\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 10 }, "items": [ { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/UvJJpKjvHCalu8cZNUg40ji9hO4\"", "id": "zqShTXi-2-Tx7TtwQqhCBzrqBvZj94YvFZOGA9x6NuY", "snippet": { "publishedAt": "2019-08-23T08:49:39.958Z", "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.", "resourceId": { "kind": "youtube#channel", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/pYbP9RYZzTnefaJtv-B2uQwsR4A\"", "id": "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo", "snippet": { "publishedAt": "2019-11-29T03:00:56.380Z", "title": "ikaros-life", "description": "This is a test channel.", "resourceId": { "kind": "youtube#channel", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } } ] } ================================================ FILE: testdata/apidata/subscriptions/subscriptions_by_mine_p1.json ================================================ { "kind": "youtube#subscriptionListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/U5b9PTEHoZSQJmnmfpQ8uv2uVUY\"", "nextPageToken": "CAoQAA", "pageInfo": { "totalResults": 16, "resultsPerPage": 10 }, "items": [ { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/UvJJpKjvHCalu8cZNUg40ji9hO4\"", "id": "zqShTXi-2-Tx7TtwQqhCBzrqBvZj94YvFZOGA9x6NuY", "snippet": { "publishedAt": "2019-08-23T08:49:39.958Z", "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.", "resourceId": { "kind": "youtube#channel", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/3trn0EfS-9Z6JA1nFmKUAurRyvs\"", "id": "zqShTXi-2-Tx7TtwQqhCB-o3EGr8v1XwT0dqravfvQQ", "snippet": { "publishedAt": "2019-03-14T08:12:41.903Z", "title": "Hua Hua", "description": "huahua leetcode", "resourceId": { "kind": "youtube#channel", "channelId": "UC5xDNEcvb1vgw3lE21Ack2Q" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-X3Qmupq3CEM/AAAAAAAAAAI/AAAAAAAAAAA/NL2uBxl6-Rs/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-X3Qmupq3CEM/AAAAAAAAAAI/AAAAAAAAAAA/NL2uBxl6-Rs/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-X3Qmupq3CEM/AAAAAAAAAAI/AAAAAAAAAAA/NL2uBxl6-Rs/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/pYbP9RYZzTnefaJtv-B2uQwsR4A\"", "id": "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo", "snippet": { "publishedAt": "2019-11-29T03:00:56.380Z", "title": "ikaros-life", "description": "This is a test channel.", "resourceId": { "kind": "youtube#channel", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/haI133x3uuikRTFb40awqRK8EFk\"", "id": "zqShTXi-2-Tx7TtwQqhCB6lHuD0nnzgzuZ6TNDS9yAg", "snippet": { "publishedAt": "2018-05-29T16:24:05.203Z", "title": "JC狗魚", "description": "My name is Dofi.\nWelcome to my channel :D", "resourceId": { "kind": "youtube#channel", "channelId": "UC3X3vJLnMH1SaH2qFqFySVQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-Js7rJmRlfcY/AAAAAAAAAAI/AAAAAAAAAAA/pUSyWgcgKHg/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-Js7rJmRlfcY/AAAAAAAAAAI/AAAAAAAAAAA/pUSyWgcgKHg/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-Js7rJmRlfcY/AAAAAAAAAAI/AAAAAAAAAAA/pUSyWgcgKHg/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/d_cRIQsfQRumM5xTJFPl4oW9cgc\"", "id": "zqShTXi-2-Tx7TtwQqhCB1GDJbXiW2g3mMAB51HqwRo", "snippet": { "publishedAt": "2019-01-19T09:27:40.656Z", "title": "JP傑劈", "description": "This is JP\n\n►IG \nwww.instagram.com/swagasian0117\n►Facebook\nhttps://www.facebook.com/SwagasianJP\n►Twitch\nhttps://www.twitch.tv/lliikekr2000 \n\n商案合作\njerry47j@gmail.com\n\n--- 訂閱JP得JP ---", "resourceId": { "kind": "youtube#channel", "channelId": "UChsUVRskM42-wS1nLKd79RQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-EIrymuDbRaA/AAAAAAAAAAI/AAAAAAAAAAA/CSN-M6REJAQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-EIrymuDbRaA/AAAAAAAAAAI/AAAAAAAAAAA/CSN-M6REJAQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-EIrymuDbRaA/AAAAAAAAAAI/AAAAAAAAAAA/CSN-M6REJAQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/0SoEv7zYSOxgLjbYKlwMg57XI5w\"", "id": "zqShTXi-2-Tx7TtwQqhCB5anKv2QRa1fCDUiUZU8xbc", "snippet": { "publishedAt": "2018-05-18T09:39:14.951Z", "title": "moco", "description": "ヽ(✿゚▽゚)ノ \n謝謝你點進來我的頻道,我是Moco.\n用閒暇之餘偶爾拍拍影片,現實生活中是一枚、一隻、一個努力苦命的上班族?\n歡迎逛逛我的影片,如果你肯留下足跡那我會更高興ww\n真的非常歡迎你在影片下方留下意見或是閒聊也行的唷 ♥(´∀` )人\n\n(๑• . •๑) 如果你覺得我還不錯,歡迎留下你的訂閱唷", "resourceId": { "kind": "youtube#channel", "channelId": "UCLW-5M0C7ELByxDc0R_buNA" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-Pgr0igo62-A/AAAAAAAAAAI/AAAAAAAAAAA/YVClvG-F96U/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-Pgr0igo62-A/AAAAAAAAAAI/AAAAAAAAAAA/YVClvG-F96U/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-Pgr0igo62-A/AAAAAAAAAAI/AAAAAAAAAAA/YVClvG-F96U/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZN8TnM_SWw9N7ydHnSPYYPvaziQ\"", "id": "zqShTXi-2-Tx7TtwQqhCBwtJ-Aho6DZeutqZiP4Q79Q", "snippet": { "publishedAt": "2018-12-25T09:12:18.265Z", "title": "Next Day Video", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UCQ7dFBzZGlBvtU2hCecsBBg" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/VIabsyP8MBhapi7K0fjjRX5bM2U\"", "id": "zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo", "snippet": { "publishedAt": "2018-09-11T11:35:04.568Z", "title": "PyCon 2015", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UCgxzjK6GuOHVKR_08TT4hJQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/f-6302NaKYDgbG-CAYLW8arJosQ\"", "id": "zqShTXi-2-S50Nc0aJJ6zfe6vF6lHl-Vpk5bYUpybWc", "snippet": { "publishedAt": "2018-09-11T11:35:08.083Z", "title": "PyCon 2016", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UCwTD5zJbsQGJN75MwbykYNw" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-OpJADPm8uMw/AAAAAAAAAAI/AAAAAAAAAAA/OB26ecvpcC8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-OpJADPm8uMw/AAAAAAAAAAI/AAAAAAAAAAA/OB26ecvpcC8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-OpJADPm8uMw/AAAAAAAAAAI/AAAAAAAAAAA/OB26ecvpcC8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } } ] } ================================================ FILE: testdata/apidata/subscriptions/subscriptions_by_mine_p2.json ================================================ { "kind": "youtube#subscriptionListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/VTrpIDMd19kB85U38VXsGy5SZ4Q\"", "prevPageToken": "CAoQAQ", "pageInfo": { "totalResults": 16, "resultsPerPage": 10 }, "items": [ { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/Y5O3JqndJlfaSyZhUujH5416cis\"", "id": "zqShTXi-2-S50Nc0aJJ6zUmRLF6uD9ENBVmwRGs4Gms", "snippet": { "publishedAt": "2018-08-03T05:42:47.580Z", "title": "PyCon 2017", "description": "PyCon 2017 Conference tutorials and talks (https://us.pycon.org/2017/).", "resourceId": { "kind": "youtube#channel", "channelId": "UCrJhliKNQ8g0qoE_zvL8eVg" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-_nxlsgdzrYk/AAAAAAAAAAI/AAAAAAAAAAA/QK6pKl5EMIU/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-_nxlsgdzrYk/AAAAAAAAAAI/AAAAAAAAAAA/QK6pKl5EMIU/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-_nxlsgdzrYk/AAAAAAAAAAI/AAAAAAAAAAA/QK6pKl5EMIU/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/fkjXXxpircl7g4cT13YB6rICLpM\"", "id": "zqShTXi-2-S50Nc0aJJ6zcF_SE2B9QIeS7jGEhCPHtA", "snippet": { "publishedAt": "2018-09-11T11:35:09.217Z", "title": "PyCon 2018", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UCsX05-2sVSH7Nx3zuk3NYuQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-p7EsXAUc0No/AAAAAAAAAAI/AAAAAAAAAAA/DYVdKW67VVg/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-p7EsXAUc0No/AAAAAAAAAAI/AAAAAAAAAAA/DYVdKW67VVg/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-p7EsXAUc0No/AAAAAAAAAAI/AAAAAAAAAAA/DYVdKW67VVg/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/f_Y7vGxhlCLazdbPfhCUaJnO2Js\"", "id": "zqShTXi-2-Rya5uUxEp3ZpfEZoPHGpH2MBMMdN1Yl9Y", "snippet": { "publishedAt": "2019-05-28T01:19:42.921Z", "title": "PyCon 2019", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UCxs2IIVXaEHHA4BtTiWZ2mQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/wa29z1XTmr6G-xu3deE6-ncNstU\"", "id": "zqShTXi-2-S50Nc0aJJ6zWOFTjfx4UQx5_WL63MoVzo", "snippet": { "publishedAt": "2018-06-20T18:17:55.464Z", "title": "媛媛", "description": "哈囉大家好我是媛媛\n我很喜歡玩遊戲和分享遊戲過程\n希望大家會喜歡我的影片唷", "resourceId": { "kind": "youtube#channel", "channelId": "UC9A1upZ6Rk7Un-iHfGsW5xA" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-0Rp5ka2uQuI/AAAAAAAAAAI/AAAAAAAAAAA/0Mh8KYtyuOE/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-0Rp5ka2uQuI/AAAAAAAAAAI/AAAAAAAAAAA/0Mh8KYtyuOE/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-0Rp5ka2uQuI/AAAAAAAAAAI/AAAAAAAAAAA/0Mh8KYtyuOE/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/r61YOaAgy9R8JJbtbhyS6CC6_4E\"", "id": "zqShTXi-2-S50Nc0aJJ6zdHSZbM7XNml9y9B3V6WQ9A", "snippet": { "publishedAt": "2018-06-11T01:42:48.406Z", "title": "李永乐老师", "description": "欢迎关注我的微信公众号“李永乐老师”,上面有超多文字版科普内容和中学视频课程哦", "resourceId": { "kind": "youtube#channel", "channelId": "UCSs4A6HYKmHA2MG_0z-F0xw" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/rOQa6yHDQP-fdGrilGJGCnKHzVA\"", "id": "zqShTXi-2-S50Nc0aJJ6zVxXxmD5w9LtQOiqnv5ZnvI", "snippet": { "publishedAt": "2018-05-18T04:06:37.795Z", "title": "歐拉", "description": "各位觀眾大家好,我是歐拉~~高音\n我超級用心的去錄製以及剪輯每一集\n如果你們喜歡我的影片,一定要記得訂閱我和繼續支持我哟\n斗內歐拉拍更優質的影片:https://goo.gl/cLEy9f\n工商合作請洽詢:olaolaola1014@gmail.com", "resourceId": { "kind": "youtube#channel", "channelId": "UCPRuZhEnHRXt8_aHcm-Hl3Q" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-usnp0F0jA1o/AAAAAAAAAAI/AAAAAAAAAAA/pDEGoy1EEvc/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-usnp0F0jA1o/AAAAAAAAAAI/AAAAAAAAAAA/pDEGoy1EEvc/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-usnp0F0jA1o/AAAAAAAAAAI/AAAAAAAAAAA/pDEGoy1EEvc/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } } ] } ================================================ FILE: testdata/apidata/user_profile.json ================================================ { "family_name": "liu", "name": "kun liu", "picture": "https://lh3.googleusercontent.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAACY/1E9uN31I7cE/photo.jpg", "locale": "zh-CN", "given_name": "kun", "id": "12345678910" } ================================================ FILE: testdata/apidata/videos/get_rating_response.json ================================================ { "kind": "youtube#videoGetRatingResponse", "etag": "jHmA6WPghQxwUKfIGg5LVYotT3Y", "items": [ { "videoId": "D-lhorsDlUQ", "rating": "none" } ] } ================================================ FILE: testdata/apidata/videos/insert_response.json ================================================ { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/dbCtFPFQrd6OMTnWAYrcpZDPai0\"", "id": "D-lhorsDlUQ", "snippet": { "publishedAt": "2019-03-21T20:37:49.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "What are Actions on Google (Assistant on Air)", "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\nOther episodes of Assistant on Air → https://goo.gle/2X0nBqG\nSubscribe to Google Devs for the latest Actions on Google videos → https://bit.ly/googledevs", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "tags": [ "Google", "developers", "aog", "Actions on Google", "Assistant", "Google Assistant", "actions", "google home", "actions on google", "google assistant developers", "google assistant sdk", "Actions on google developers", "smarthome developers", "common terminology", "custom action on google", "google assistant in your app", "add google assistant", "assistant on air", "how to use google assistant on air", "Actions on Google how to" ], "categoryId": "28", "liveBroadcastContent": "none", "defaultLanguage": "en", "localized": { "title": "What are Actions on Google (Assistant on Air)", "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\nOther episodes of Assistant on Air → https://goo.gle/2X0nBqG\nSubscribe to Google Devs for the latest Actions on Google videos → https://bit.ly/googledevs" }, "defaultAudioLanguage": "en" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/D-lhorsDlUQ\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } } ================================================ FILE: testdata/apidata/videos/videos_chart_paged_1.json ================================================ { "kind": "youtube#videoListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/rzuXDNleIBUJYgD8CQtVBDeS0mU\"", "nextPageToken": "CAUQAA", "pageInfo": { "totalResults": 8, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/lPIldyZ8_QoOFUPkRMujFc1zoFA\"", "id": "hDeuSfo_Ys0", "snippet": { "publishedAt": "2019-11-29T20:00:11.000Z", "channelId": "UCWwWOFsW68TqXE-HZLC3WIA", "title": "GIDDY UP ( OFFICIAL MUSIC VIDEO )", "description": "Download \"Giddy Up\" on Apple Music: https://music.apple.com/us/album/giddy-up-single/1488449563?ls=1\nHelp get this song on the charts by downloading it on Apple Music!!!\n\nDownload \"Giddy Up\" on Spotify: http://open.spotify.com/album/3IlcCcQUnB7Stzcgohm9qV\n\nJOIN THE ACE FAMILY & SUBSCRIBE: http://bit.ly/THEACEFAMILY\n\nSTALK US :)\n\nCatherine's Instagram: https://www.instagram.com/catherinepaiz/\nCatherine's Twitter: http://twitter.com/catherinepaiz\nCatherine's SnapChat: Catherinepaiz\n\nAustin's Instagram: https://www.instagram.com/austinmcbroom/\nAustin's Twitter: https://twitter.com/AustinMcbroom\nAustin's SnapChat: TheRealMcBroom", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/hDeuSfo_Ys0/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/hDeuSfo_Ys0/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/hDeuSfo_Ys0/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/hDeuSfo_Ys0/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/hDeuSfo_Ys0/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "The ACE Family", "tags": [ "the ace family giddy up", "ace family giddy up", "ace family music", "ace family song", "giddy up", "giddy up song", "giddy up ace family", "new song giddy up", "giddy up challenge", "giddy up music video", "the ace family", "ace family" ], "categoryId": "22", "liveBroadcastContent": "none", "localized": { "title": "GIDDY UP ( OFFICIAL MUSIC VIDEO )", "description": "Download \"Giddy Up\" on Apple Music: https://music.apple.com/us/album/giddy-up-single/1488449563?ls=1\nHelp get this song on the charts by downloading it on Apple Music!!!\n\nDownload \"Giddy Up\" on Spotify: http://open.spotify.com/album/3IlcCcQUnB7Stzcgohm9qV\n\nJOIN THE ACE FAMILY & SUBSCRIBE: http://bit.ly/THEACEFAMILY\n\nSTALK US :)\n\nCatherine's Instagram: https://www.instagram.com/catherinepaiz/\nCatherine's Twitter: http://twitter.com/catherinepaiz\nCatherine's SnapChat: Catherinepaiz\n\nAustin's Instagram: https://www.instagram.com/austinmcbroom/\nAustin's Twitter: https://twitter.com/AustinMcbroom\nAustin's SnapChat: TheRealMcBroom" } }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/hDeuSfo_Ys0\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } }, { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/3jRkViNzm-GC4pj_ViD7TIB1ggQ\"", "id": "Ukd8dJNBvOA", "snippet": { "publishedAt": "2019-11-29T18:20:46.000Z", "channelId": "UC5LGPvoUOfwcLi4Ck8LiR4A", "title": "TRAPPED WITH THE PRINCE FAMILY REALITY SHOW TRAILER", "description": "Trapped With The Prince Family Reality Show Will Air Here On This Channel December 28th, 2019!! Subscribe Now & Turn On Notifications To Watch This Amazing Show!!\n\nThe Prince Family Merch: https://www.officialprincefamily.com\n\nSubscribe To DJ's Clubhouse: https://www.youtube.com/channel/UCqONv8hrKKTd0ELJcxOVNJQ\n\nFollow Damien:\nInstagram: https://instagram.com/DamienPrinceJr\nTwitter: https://twitter.com/DamienPrinceJr\nSnapChat: https://snapchat.com/add/DamienPrinceJr\nFacebook: https://facebook.com/DamienPrinceJr\n\nFollow Biannca: \nYouTube Channel: https://goo.gl/iCz7K8\nInstagram: https://instagram.com/x_bianncaraines\nTwitter: https://twitter.com/bianncarraines\nSnapChat: https://snapchat.com/add/BianncaRaines\n\nFollow Nova's Instagram: https://www.instagram.com/novagraceprince\n\nFollow Kyrie & DJ:\nInstagram: https://instagram.com/djandkyrieprince\nTwitter: https://twitter.com/daimon_kyrie\n\nBUSINESS INQUIRIES: ThePrinceFamilyInquiries@gmail.com\n\nFollow The Prince Family On Facebook: https://www.facebook.com/OfficalPrinceFamily\n\n#ThePrinceFamily", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Ukd8dJNBvOA/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Ukd8dJNBvOA/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Ukd8dJNBvOA/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/Ukd8dJNBvOA/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/Ukd8dJNBvOA/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "THE PRINCE FAMILY", "tags": [ "The Prince Family", "The Prince Family Vlogs", "The Prince Family Pranks", "Damien & Biannca", "Damien Prince", "Biannca Prince", "Biannca", "Damien", "the prince family reality show", "the prince family reality show 2019", "reality", "reality house", "reality tv", "youtube originals", "youtube original series", "d&b nation reality show", "d&b nation", "the ace family", "faze rug", "cj so cool", "the labrant family", "trapped", "trapped with the prince family", "trapped with the prince family reality show trailer", "trailer" ], "categoryId": "22", "liveBroadcastContent": "none", "defaultLanguage": "en", "localized": { "title": "TRAPPED WITH THE PRINCE FAMILY REALITY SHOW TRAILER", "description": "Trapped With The Prince Family Reality Show Will Air Here On This Channel December 28th, 2019!! Subscribe Now & Turn On Notifications To Watch This Amazing Show!!\n\nThe Prince Family Merch: https://www.officialprincefamily.com\n\nSubscribe To DJ's Clubhouse: https://www.youtube.com/channel/UCqONv8hrKKTd0ELJcxOVNJQ\n\nFollow Damien:\nInstagram: https://instagram.com/DamienPrinceJr\nTwitter: https://twitter.com/DamienPrinceJr\nSnapChat: https://snapchat.com/add/DamienPrinceJr\nFacebook: https://facebook.com/DamienPrinceJr\n\nFollow Biannca: \nYouTube Channel: https://goo.gl/iCz7K8\nInstagram: https://instagram.com/x_bianncaraines\nTwitter: https://twitter.com/bianncarraines\nSnapChat: https://snapchat.com/add/BianncaRaines\n\nFollow Nova's Instagram: https://www.instagram.com/novagraceprince\n\nFollow Kyrie & DJ:\nInstagram: https://instagram.com/djandkyrieprince\nTwitter: https://twitter.com/daimon_kyrie\n\nBUSINESS INQUIRIES: ThePrinceFamilyInquiries@gmail.com\n\nFollow The Prince Family On Facebook: https://www.facebook.com/OfficalPrinceFamily\n\n#ThePrinceFamily" }, "defaultAudioLanguage": "en-US" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/Ukd8dJNBvOA\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } }, { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/7ztVZ5Pnpj4PAv2sa37S_OQfckE\"", "id": "Pdgk3ERKdug", "snippet": { "publishedAt": "2019-11-28T14:46:03.000Z", "channelId": "UCkd0_tpDvgnBqfhUzM7Q0og", "title": "A Holiday Reunion – Xfinity 2019", "description": "After 37 years, E.T. comes back to visit his friend, Elliott, for the holidays. During his stay, E.T. learns that Elliott now has a family of his own and that technology has completely changed on Earth since his last visit. Learn more at xfinity.com/ET", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Pdgk3ERKdug/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Pdgk3ERKdug/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Pdgk3ERKdug/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/Pdgk3ERKdug/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/Pdgk3ERKdug/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Xfinity", "tags": [ "E.T.", "holiday", "Christmas", "Spielberg", "Xfinity", "extraterrestrial", "Xfinity X1", "Xfinity Internet", "Xfinity Stream", "Xfinity Home", "Xfinity Voice", "Xfinity Commercials", "Commercials", "Henry Thomas", "E.T. Phone Home", "Xfinity Voice Remote", "Holiday Movies", "Voice Command", "snowman", "TV", "Comcast", "Elliot", "Lance Acord" ], "categoryId": "24", "liveBroadcastContent": "none", "localized": { "title": "A Holiday Reunion – Xfinity 2019", "description": "After 37 years, E.T. comes back to visit his friend, Elliott, for the holidays. During his stay, E.T. learns that Elliott now has a family of his own and that technology has completely changed on Earth since his last visit. Learn more at xfinity.com/ET" }, "defaultAudioLanguage": "en" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/Pdgk3ERKdug\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } }, { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/MJoV_9qVK_qh6ZsU1O3tkxrwEM4\"", "id": "QmYNmNHrtCQ", "snippet": { "publishedAt": "2019-11-29T22:15:38.000Z", "channelId": "UCuVHOs0H5hvAHGr8O4yIBNQ", "title": "Black Friday Haul 2019! Niki and Gabi", "description": "AnOtHeR BLACK FRIDAY HAUL for 2019!\nthis is our annual black friday haul! we black friday shopped all night at your favorite stores, like AERIE, bath and body works, lush, ulta, and victorias secret! and we also shopped for you guys for our annual TWINTER giveaway, stay tuned for the announcement // giveaway reveal after the haul! \nSubscribe here ➜ http://bit.ly/2vxi9ch\nShop our MERCH➜ Fanjoy.co/collections/niki-gabi\nShop NIKNAKS ➜ http://shopniknaks.com \n\n#TWINTERGiveaway2019\nHow to enter:\n~take a screen shot of the giveaway you want to enter in the video: Niki or Gabi\n~post on instagram w/ #TwinterGiveaway2019\n~for bonus points, we look to see who is subscribed, follows us on instagram (@niki & @gabi), and who is actively liking / commenting on our posts *helps for bonus points*\n~winner is announced in our annual \"what we got for christmas\" video on christmas!\nGOODLUCK\n\nTO BINGE:\nOur \"Opposite Twin\" Challenge playlist➜ https://www.youtube.com/playlist?list...\nOur Fashion // Shopping playlist➜ https://www.youtube.com/playlist?list...\n\n**NEW VIDEOS EVERY SUNDAY and SOMETIMES WEDNESDAY**\n\nIf you see this, comment \"the shade in this video\" \nonly those who watch up to that point will know what this means ;)\n\nvlog channels:\nniki demar\nhttps://www.youtube.com/user/nikidemar\nfancy vlogs by gab https://www.youtube.com/channel/UCLGe...\n\nSOCIAL MEDIA \nInstagram➜ @NIKI / @GABI\nTwitter➜ @nikidemar / @gabcake\nTumblr➜ nikidemar / breakfastatchanel-starringgabi\nSnapchat➜ nikidemarrr / fancysnapsbygab\n\nWe’re Niki and Gabi! We hope you enjoyed our Black Friday Haul 2019 video! We’re twin sisters who are different with opposite fashion and styles, but we come together to make videos like challenges, swaps, closet swaps, DIY, swaps, shopping challenges, 24 hour challenges, diy, style, beauty, lifestyle, fashion, comedy, types of girls, music, and more!", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/QmYNmNHrtCQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/QmYNmNHrtCQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/QmYNmNHrtCQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/QmYNmNHrtCQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/QmYNmNHrtCQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Niki and Gabi", "tags": [ "niki and gabi", "black friday haul", "black friday haul 2019", "black friday shopping", "black friday", "shopping", "haul", "giveaway", "twinter giveaway", "niki and gabi haul", "aerie", "bath and body works", "victorias secret", "lush", "ulta" ], "categoryId": "26", "liveBroadcastContent": "none", "localized": { "title": "Black Friday Haul 2019! Niki and Gabi", "description": "AnOtHeR BLACK FRIDAY HAUL for 2019!\nthis is our annual black friday haul! we black friday shopped all night at your favorite stores, like AERIE, bath and body works, lush, ulta, and victorias secret! and we also shopped for you guys for our annual TWINTER giveaway, stay tuned for the announcement // giveaway reveal after the haul! \nSubscribe here ➜ http://bit.ly/2vxi9ch\nShop our MERCH➜ Fanjoy.co/collections/niki-gabi\nShop NIKNAKS ➜ http://shopniknaks.com \n\n#TWINTERGiveaway2019\nHow to enter:\n~take a screen shot of the giveaway you want to enter in the video: Niki or Gabi\n~post on instagram w/ #TwinterGiveaway2019\n~for bonus points, we look to see who is subscribed, follows us on instagram (@niki & @gabi), and who is actively liking / commenting on our posts *helps for bonus points*\n~winner is announced in our annual \"what we got for christmas\" video on christmas!\nGOODLUCK\n\nTO BINGE:\nOur \"Opposite Twin\" Challenge playlist➜ https://www.youtube.com/playlist?list...\nOur Fashion // Shopping playlist➜ https://www.youtube.com/playlist?list...\n\n**NEW VIDEOS EVERY SUNDAY and SOMETIMES WEDNESDAY**\n\nIf you see this, comment \"the shade in this video\" \nonly those who watch up to that point will know what this means ;)\n\nvlog channels:\nniki demar\nhttps://www.youtube.com/user/nikidemar\nfancy vlogs by gab https://www.youtube.com/channel/UCLGe...\n\nSOCIAL MEDIA \nInstagram➜ @NIKI / @GABI\nTwitter➜ @nikidemar / @gabcake\nTumblr➜ nikidemar / breakfastatchanel-starringgabi\nSnapchat➜ nikidemarrr / fancysnapsbygab\n\nWe’re Niki and Gabi! We hope you enjoyed our Black Friday Haul 2019 video! We’re twin sisters who are different with opposite fashion and styles, but we come together to make videos like challenges, swaps, closet swaps, DIY, swaps, shopping challenges, 24 hour challenges, diy, style, beauty, lifestyle, fashion, comedy, types of girls, music, and more!" }, "defaultAudioLanguage": "en" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/QmYNmNHrtCQ\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } }, { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/wNjkzpdYbrOsNIN9HtcKYlV-9ZA\"", "id": "X1jMMFOqxEw", "snippet": { "publishedAt": "2019-11-28T21:00:01.000Z", "channelId": "UCX6OQ3DkcsbYNE6H8uQQuVA", "title": "$50,000 Game Of Extreme Hide And Seek - Challenge", "description": "BUY NOW MrBeast, Chandler, Chris - http://youtooz.com\n\n\nENTER ON INSTAGRAM TOO https://www.instagram.com/mrbeast\n\nNew Merch - https://shopmrbeast.com/\n\nSUBSCRIBE OR ILL EAT YOUR THANKSGIVING LEFT OVERS\n\n----------------------------------------------------------------\nfollow all of these or i will kick you\n• Facebook - https://www.facebook.com/MrBeast6000/\n• Twitter - https://twitter.com/MrBeastYT\n• Instagram - https://www.instagram.com/mrbeast\n--------------------------------------------------------------------", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/X1jMMFOqxEw/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/X1jMMFOqxEw/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/X1jMMFOqxEw/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/X1jMMFOqxEw/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/X1jMMFOqxEw/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "MrBeast", "categoryId": "24", "liveBroadcastContent": "none", "localized": { "title": "$50,000 Game Of Extreme Hide And Seek - Challenge", "description": "BUY NOW MrBeast, Chandler, Chris - http://youtooz.com\n\n\nENTER ON INSTAGRAM TOO https://www.instagram.com/mrbeast\n\nNew Merch - https://shopmrbeast.com/\n\nSUBSCRIBE OR ILL EAT YOUR THANKSGIVING LEFT OVERS\n\n----------------------------------------------------------------\nfollow all of these or i will kick you\n• Facebook - https://www.facebook.com/MrBeast6000/\n• Twitter - https://twitter.com/MrBeastYT\n• Instagram - https://www.instagram.com/mrbeast\n--------------------------------------------------------------------" }, "defaultAudioLanguage": "en" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/X1jMMFOqxEw\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } } ] } ================================================ FILE: testdata/apidata/videos/videos_chart_paged_2.json ================================================ { "kind": "youtube#videoListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Ef75aANPGrASduOASo4wDAnehrs\"", "prevPageToken": "CAUQAQ", "pageInfo": { "totalResults": 8, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/VXWgiTAYvxnCZnyGoY0XGNQSb0Q\"", "id": "MKM90u7pf3U", "snippet": { "publishedAt": "2019-11-28T17:00:11.000Z", "channelId": "UCs6eXM7s8Vl5WcECcRHc2qQ", "title": "Kanye West - Closed On Sunday", "description": "https://kanyewest.lnk.to/JesusIsKing\n\nhttps://www.kanyewest.com/\n\nhttps://shop.kanyewest.com\n\nhttps://twitter.com/kanyewest\n\n#JESUSISKING\n\nDirector: Jake Schreier\nExecutive Producers: Jackie Kelman Bisbee and Cody Ryder\nProducer: Joe Faulstich \nDP: Adam Newport-Berra\nProduction Company: Park Pictures\nVFX: Chris Buongiorno \nColor: Matt Osborne / The Mill\nCreative Direction: Angel Boyd", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/MKM90u7pf3U/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/MKM90u7pf3U/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/MKM90u7pf3U/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/MKM90u7pf3U/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/MKM90u7pf3U/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Kanye West", "tags": [ "kanye", "kanyewest", "kanye west", "jesus is king", "jesusisking", "JIK", "jesus", "god", "gospel", "christ", "church", "closed on sunday", "closedonsunday", "chickfila", "Chick Fil A", "Chic Fil A", "kim kardashian", "kim k", "kris jenner", "kourtney kardashian", "north west", "chicago", "saint", "the wests", "thanksgiving" ], "categoryId": "22", "liveBroadcastContent": "none", "localized": { "title": "Kanye West - Closed On Sunday", "description": "https://kanyewest.lnk.to/JesusIsKing\n\nhttps://www.kanyewest.com/\n\nhttps://shop.kanyewest.com\n\nhttps://twitter.com/kanyewest\n\n#JESUSISKING\n\nDirector: Jake Schreier\nExecutive Producers: Jackie Kelman Bisbee and Cody Ryder\nProducer: Joe Faulstich \nDP: Adam Newport-Berra\nProduction Company: Park Pictures\nVFX: Chris Buongiorno \nColor: Matt Osborne / The Mill\nCreative Direction: Angel Boyd" }, "defaultAudioLanguage": "zh-Hans" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/MKM90u7pf3U\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } }, { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Ob11Wh0zRIrMStSut6dFK6iC9ds\"", "id": "4MK8usgnvfo", "snippet": { "publishedAt": "2019-11-29T18:30:01.000Z", "channelId": "UCi9cDo6239RAzPpBZO9y5SA", "title": "I'm Dating a Celebrity?! | Lele Pons & Juanpa Zurita", "description": "WATCH MORE ▶ https://youtube.com/playlist?list=PLmjMRs-v1tgTWmpYuBPugvLqBXVz5H-vP\n\nSUBSCRIBE HERE ▶ http://youtube.com/channel/UCi9cDo6239RAzPpBZO9y5SA?sub_confirmation=1\n\nTEXT ME HERE ▶ https://my.community.com/lelepons \n\nTHANKS FOR WATCHING! :) LIKE & SUBSCRIBE FOR MORE VIDEOS!\n-----------------------------------------------------------\nFIND ME ON:\nInstagram | http://instagram.com/lelepons\nTwitter | http://twitter.com/lelepons\nFacebook | http://facebook.com/lele \nMerch | https://lelepons.co/\nText Me | https://my.community.com/lelepons \n\nCAST: \nLele Pons | http://youtube.com/c/lelepons\nJuanpa Zurita | http://youtube.com/c/juanpa\nSandra Gutierrez | https://instagram.com/iamsandragutierrez\nKatherine Lucia | https://www.instagram.com/katherinelucia\n\nWE HIT 500 VIDEOS WITH OVER 1 MILLION VIEWS ▶ https://youtu.be/VOzQW_fff5A\n\nShots Studios Channels:\nAnwar Jibawi | http://youtube.com/c/anwar\nAwkward Puppets | http://youtube.com/c/awkwardpuppets\nDelaney Glazer | http://youtube.com/c/deeglazer\nHannah Stocking | http://youtube.com/c/hannahstocking\nJuanpa Zurita | http://youtube.com/c/juanpa\nLele Pons | http://youtube.com/c/lelepons\nRudy Mancuso | http://youtube.com/c/rudymancuso\nShots Studios | http://youtube.com/c/shots\nShots Studios Kids | http://youtube.com/c/ShotsStudiosKids\n\n#Lele\n#LelePons\n#Juanpa\n#JuanpaZurita", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/4MK8usgnvfo/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/4MK8usgnvfo/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/4MK8usgnvfo/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/4MK8usgnvfo/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/4MK8usgnvfo/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Lele Pons", "tags": [ "I'm Dating a Celebrity?! | Lele Pons & Juanpa Zurita", "dating a celeb", "dating", "lele pons", "lelepons", "lele", "pons", "juanpa", "zurita", "juanpazurita", "lele boyfriend", "eljuanpazurita", "shots studios", "mindie", "tarte", "check out my new collaboration", "meet our kids", "how to make a friend in 10 hours", "hannah stocking", "hannahstocking", "rudy mancuso", "anwar", "jibawi", "anwarjibawi", "rudy", "mancuso", "los puti" ], "categoryId": "23", "liveBroadcastContent": "none", "localized": { "title": "I'm Dating a Celebrity?! | Lele Pons & Juanpa Zurita", "description": "WATCH MORE ▶ https://youtube.com/playlist?list=PLmjMRs-v1tgTWmpYuBPugvLqBXVz5H-vP\n\nSUBSCRIBE HERE ▶ http://youtube.com/channel/UCi9cDo6239RAzPpBZO9y5SA?sub_confirmation=1\n\nTEXT ME HERE ▶ https://my.community.com/lelepons \n\nTHANKS FOR WATCHING! :) LIKE & SUBSCRIBE FOR MORE VIDEOS!\n-----------------------------------------------------------\nFIND ME ON:\nInstagram | http://instagram.com/lelepons\nTwitter | http://twitter.com/lelepons\nFacebook | http://facebook.com/lele \nMerch | https://lelepons.co/\nText Me | https://my.community.com/lelepons \n\nCAST: \nLele Pons | http://youtube.com/c/lelepons\nJuanpa Zurita | http://youtube.com/c/juanpa\nSandra Gutierrez | https://instagram.com/iamsandragutierrez\nKatherine Lucia | https://www.instagram.com/katherinelucia\n\nWE HIT 500 VIDEOS WITH OVER 1 MILLION VIEWS ▶ https://youtu.be/VOzQW_fff5A\n\nShots Studios Channels:\nAnwar Jibawi | http://youtube.com/c/anwar\nAwkward Puppets | http://youtube.com/c/awkwardpuppets\nDelaney Glazer | http://youtube.com/c/deeglazer\nHannah Stocking | http://youtube.com/c/hannahstocking\nJuanpa Zurita | http://youtube.com/c/juanpa\nLele Pons | http://youtube.com/c/lelepons\nRudy Mancuso | http://youtube.com/c/rudymancuso\nShots Studios | http://youtube.com/c/shots\nShots Studios Kids | http://youtube.com/c/ShotsStudiosKids\n\n#Lele\n#LelePons\n#Juanpa\n#JuanpaZurita" }, "defaultAudioLanguage": "en" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/4MK8usgnvfo\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } }, { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/6p6atbY5fVUJcaxj9N8Pwa47fFc\"", "id": "h107pApTY84", "snippet": { "publishedAt": "2019-11-29T20:00:34.000Z", "channelId": "UCBlbxksRa-KRSEKLi6foxjQ", "title": "Customizing 8 Apple Watches⌚️💦Then Giving Them Away!!", "description": "COP YOUR MERCH!! https://www.thesatisfied.com\nCOP YOUR MERCH!! https://www.thesatisfied.com\nCOP YOUR MERCH!! https://www.thesatisfied.com\nCOP YOUR MERCH!! https://www.thesatisfied.com\n\nHope you EnJoYed! Make SUrE tO eNtEr the GiVEawAy:) Thanks for WaChTing!\n\nFollow My Socials!\nInstagram : @ markoterzo\nSnapchat: @ markoterzic0018\nTwitter: @ MARKOTERZO_", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/h107pApTY84/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/h107pApTY84/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/h107pApTY84/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/h107pApTY84/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/h107pApTY84/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "MARKO", "tags": [ "MARKO", "MatTV", "Custom", "Shoes", "Sneakers", "Vans", "Air Force 1's", "marko", "custom vans", "copic", "pewdiepie", "PewdiePie", "custom shoes", "drawing", "ZHC", "custom sharpie", "sharpie shoes", "Hydro Dip", "HYDRO", "VANS", "Spray paint", "hydro dipping vans", "hydro dipping shoes", "hydro dip shoes", "hydro flask", "custom hydro flask", "painting hydro flask", "iphone", "iphone 11", "iphone 11 pro max", "custom iphone 11", "custom phone", "custom iphone", "markoterzo" ], "categoryId": "24", "liveBroadcastContent": "none", "defaultLanguage": "en", "localized": { "title": "Customizing 8 Apple Watches⌚️💦Then Giving Them Away!!", "description": "COP YOUR MERCH!! https://www.thesatisfied.com\nCOP YOUR MERCH!! https://www.thesatisfied.com\nCOP YOUR MERCH!! https://www.thesatisfied.com\nCOP YOUR MERCH!! https://www.thesatisfied.com\n\nHope you EnJoYed! Make SUrE tO eNtEr the GiVEawAy:) Thanks for WaChTing!\n\nFollow My Socials!\nInstagram : @ markoterzo\nSnapchat: @ markoterzic0018\nTwitter: @ MARKOTERZO_" }, "defaultAudioLanguage": "en" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/h107pApTY84\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } } ] } ================================================ FILE: testdata/apidata/videos/videos_info_multi.json ================================================ { "kind": "youtube#videoListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/gdL-V0hs_jHqrqpeSTPV4WGOc9Y\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 2 }, "items": [ { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/dbCtFPFQrd6OMTnWAYrcpZDPai0\"", "id": "D-lhorsDlUQ", "snippet": { "publishedAt": "2019-03-21T20:37:49.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "What are Actions on Google (Assistant on Air)", "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\nOther episodes of Assistant on Air → https://goo.gle/2X0nBqG\nSubscribe to Google Devs for the latest Actions on Google videos → https://bit.ly/googledevs", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "tags": [ "Google", "developers", "aog", "Actions on Google", "Assistant", "Google Assistant", "actions", "google home", "actions on google", "google assistant developers", "google assistant sdk", "Actions on google developers", "smarthome developers", "common terminology", "custom action on google", "google assistant in your app", "add google assistant", "assistant on air", "how to use google assistant on air", "Actions on Google how to" ], "categoryId": "28", "liveBroadcastContent": "none", "defaultLanguage": "en", "localized": { "title": "What are Actions on Google (Assistant on Air)", "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\nOther episodes of Assistant on Air → https://goo.gle/2X0nBqG\nSubscribe to Google Devs for the latest Actions on Google videos → https://bit.ly/googledevs" }, "defaultAudioLanguage": "en" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/D-lhorsDlUQ\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } }, { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/reY3Wnf12Q5myxn3EOXDguKzvns\"", "id": "ovdbrdCIP7U", "snippet": { "publishedAt": "2019-09-11T23:00:05.000Z", "channelId": "UCJdl3Paao2f3ha5JXMYUCIA", "title": "How EVERY Team Got Its Name & Identity!", "description": "Ever wonder how the NFL got to be where it is today? Sit back, relax, and enjoy the Evolution of the NFL.\n\n#NFL100\n\nThe NFL Throwback is your home for all things NFL history.\n\nCheck out our other channels:\nNFL Films - YouTube.com/NFLFilms\nNFL Network- YouTube.com/NFLNetwork\nNFL Rush - YouTube.com/NFLRush\nNFL - YouTube.com/NFL\n\n#NFL #NFLThrowback #NFLHistory #Football #AmericanFootball #NFLVault", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/ovdbrdCIP7U/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/ovdbrdCIP7U/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/ovdbrdCIP7U/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/ovdbrdCIP7U/sddefault.jpg", "width": 640, "height": 480 } }, "channelTitle": "NFL Throwback", "tags": [ "nfl", "american football", "nfl history", "nfl highlights", "nfl vault", "nfl throwback", "How EVERY NFL Team Got Its Name & Identity", "how every team got its name", "how every nfl team got its name", "evolution of the nfl", "nfl explained", "infograhic", "history of every tean", "history of nfl", "history of logos", "nfl 100", "nfl timeline", "dallas cowboys", "pittsburgh steelers", "cleveland browns", "green bay packers", "chicago bear", "new england patriots", "oakland raiders", "philadelphia eagles" ], "categoryId": "17", "liveBroadcastContent": "none", "localized": { "title": "How EVERY Team Got Its Name & Identity!", "description": "Ever wonder how the NFL got to be where it is today? Sit back, relax, and enjoy the Evolution of the NFL.\n\n#NFL100\n\nThe NFL Throwback is your home for all things NFL history.\n\nCheck out our other channels:\nNFL Films - YouTube.com/NFLFilms\nNFL Network- YouTube.com/NFLNetwork\nNFL Rush - YouTube.com/NFLRush\nNFL - YouTube.com/NFL\n\n#NFL #NFLThrowback #NFLHistory #Football #AmericanFootball #NFLVault" }, "defaultAudioLanguage": "en" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/ovdbrdCIP7U\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } } ] } ================================================ FILE: testdata/apidata/videos/videos_info_single.json ================================================ { "kind": "youtube#videoListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/tCG7DWpALbkZUGKS9l2aPYQwYRo\"", "pageInfo": { "totalResults": 1, "resultsPerPage": 1 }, "items": [ { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/dbCtFPFQrd6OMTnWAYrcpZDPai0\"", "id": "D-lhorsDlUQ", "snippet": { "publishedAt": "2019-03-21T20:37:49.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "What are Actions on Google (Assistant on Air)", "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\nOther episodes of Assistant on Air → https://goo.gle/2X0nBqG\nSubscribe to Google Devs for the latest Actions on Google videos → https://bit.ly/googledevs", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "tags": [ "Google", "developers", "aog", "Actions on Google", "Assistant", "Google Assistant", "actions", "google home", "actions on google", "google assistant developers", "google assistant sdk", "Actions on google developers", "smarthome developers", "common terminology", "custom action on google", "google assistant in your app", "add google assistant", "assistant on air", "how to use google assistant on air", "Actions on Google how to" ], "categoryId": "28", "liveBroadcastContent": "none", "defaultLanguage": "en", "localized": { "title": "What are Actions on Google (Assistant on Air)", "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\nOther episodes of Assistant on Air → https://goo.gle/2X0nBqG\nSubscribe to Google Devs for the latest Actions on Google videos → https://bit.ly/googledevs" }, "defaultAudioLanguage": "en" }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/D-lhorsDlUQ\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } } ] } ================================================ FILE: testdata/apidata/videos/videos_myrating_paged_1.json ================================================ { "kind": "youtube#videoListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/b3UF68xF07lN4fYS18DcwcBg3mE\"", "nextPageToken": "CAIQAA", "pageInfo": { "totalResults": 3, "resultsPerPage": 2 }, "items": [ { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/cTOUTPk5T0BpAE5u9yFR_Dd3UbI\"", "id": "P4IfFLAX9hY", "snippet": { "publishedAt": "2019-05-07T06:50:29.000Z", "channelId": "UCxs2IIVXaEHHA4BtTiWZ2mQ", "title": "Python Software Foundation Community Report and Community Service Awards - PyCon 2019", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/P4IfFLAX9hY/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/P4IfFLAX9hY/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/P4IfFLAX9hY/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/P4IfFLAX9hY/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/P4IfFLAX9hY/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "PyCon 2019", "categoryId": "22", "liveBroadcastContent": "none", "localized": { "title": "Python Software Foundation Community Report and Community Service Awards - PyCon 2019", "description": "" } }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/P4IfFLAX9hY\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } }, { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/FsRNzz1dKoCU6ct60MCxJtR-wpc\"", "id": "kNke39OZ2k0", "snippet": { "publishedAt": "2014-05-24T21:35:51.000Z", "channelId": "UC-mexo-76-J1MlQM8NkWCYw", "title": "Building Command Line Applications with Click", "description": "Quick introduction to building command line applications with click.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/kNke39OZ2k0/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/kNke39OZ2k0/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/kNke39OZ2k0/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/kNke39OZ2k0/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/kNke39OZ2k0/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Armin Ronacher", "tags": [ "Command-line Interface (Computing Platform)", "Click", "Python (Software)", "Software (Industry)" ], "categoryId": "28", "liveBroadcastContent": "none", "localized": { "title": "Building Command Line Applications with Click", "description": "Quick introduction to building command line applications with click." } }, "player": { "embedHtml": "\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/kNke39OZ2k0\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" } } ] } ================================================ FILE: testdata/apidata/videos/videos_myrating_paged_2.json ================================================ { "kind": "youtube#videoListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Uj_O0S-h8FpI0EG1DLVrKzjMiqM\"", "prevPageToken": "CAIQAQ", "pageInfo": { "totalResults": 3, "resultsPerPage": 2 }, "items": [ { "kind": "youtube#video", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/97veyDdoO33-JR5D_HUtdm_rAP0\"", "id": "7mIDiKK4eyo", "snippet": { "publishedAt": "2019-08-28T09:29:43.000Z", "channelId": "UC-mexo-76-J1MlQM8NkWCYw", "title": "Insta Snapshot Tests Introduction", "description": "Shows how snapshot testing in the rust insta library works.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/7mIDiKK4eyo/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/7mIDiKK4eyo/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/7mIDiKK4eyo/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/7mIDiKK4eyo/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/7mIDiKK4eyo/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Armin Ronacher", "tags": [ "rust", "insta", "snapshot testing" ], "categoryId": "28", "liveBroadcastContent": "none", "localized": { "title": "Insta Snapshot Tests Introduction", "description": "Shows how snapshot testing in the rust insta library works." } }, "player": { "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" } } ] } ================================================ FILE: testdata/error_response.json ================================================ { "error": { "errors": [ { "domain": "usageLimits", "reason": "keyInvalid", "message": "Bad Request" } ], "code": 400, "message": "Bad Request" } } ================================================ FILE: testdata/error_response_simple.json ================================================ { "error": "error message" } ================================================ FILE: testdata/modeldata/abuse_report_reason/abuse_reason.json ================================================ { "kind": "youtube#videoAbuseReportReason", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/_WIvuNJwlISvQNQt_ukh2m0kt2Y\"", "id": "N", "snippet": { "label": "Sex or nudity", "secondaryReasons": [ { "id": "32", "label": "Graphic sex or nudity" }, { "id": "33", "label": "Content involving minors" }, { "id": "34", "label": "Other sexual content" } ] } } ================================================ FILE: testdata/modeldata/abuse_report_reason/abuse_reason_res.json ================================================ { "kind": "youtube#videoAbuseReportReasonListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/YH398HlGf_qbYlJQUZVMRoL4RTE\"", "items": [ { "kind": "youtube#videoAbuseReportReason", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/_WIvuNJwlISvQNQt_ukh2m0kt2Y\"", "id": "N", "snippet": { "label": "Sex or nudity", "secondaryReasons": [ { "id": "32", "label": "Graphic sex or nudity" }, { "id": "33", "label": "Content involving minors" }, { "id": "34", "label": "Other sexual content" } ] } }, { "kind": "youtube#videoAbuseReportReason", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/9uBFtSRN_-W5oQDz_AhiTSN5sTE\"", "id": "S", "snippet": { "label": "Spam or misleading", "secondaryReasons": [ { "id": "27", "label": "Spam or mass advertising" }, { "id": "28", "label": "Misleading thumbnail" }, { "id": "29", "label": "Malware or phishing" }, { "id": "30", "label": "Pharmaceutical drugs for sale" }, { "id": "31", "label": "Other misleading info" } ] } }, { "kind": "youtube#videoAbuseReportReason", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/e6pyrZ9LzezCkkpfXAc0gkDdQ0Q\"", "id": "V", "snippet": { "label": "Violent, hateful, or dangerous", "secondaryReasons": [ { "id": "35", "label": "Promotes violence or hatred" }, { "id": "36", "label": "Promotes terrorism" }, { "id": "37", "label": "Bullying or abusing vulnerable individuals" }, { "id": "38", "label": "Suicide or self-injury" }, { "id": "39", "label": "Pharmaceutical or drug abuse" }, { "id": "40", "label": "Other violent, hateful, or dangerous acts" } ] } } ] } ================================================ FILE: testdata/modeldata/activities/activity.json ================================================ { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/Jy79IfTqdSUQSMOkAA9ynak3zOI\"", "id": "MTUxNTc0OTk2MjI3Mjg1OTU3Nzk0MzQzODQ=", "snippet": { "publishedAt": "2019-11-29T02:57:07.000Z", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "华山日出", "description": "冷冷的山头", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "ikaros-life", "type": "upload" }, "contentDetails": { "upload": { "videoId": "JE8xdDp5B8Q" } } } ================================================ FILE: testdata/modeldata/activities/activity_contentDetails.json ================================================ { "upload": { "videoId": "LDXYRzerjzU" } } ================================================ FILE: testdata/modeldata/activities/activity_response.json ================================================ { "kind": "youtube#activityListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/JK3LLzdEV9zduJqxK4pgzb55QfI\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 50 }, "items": [ { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/Jy79IfTqdSUQSMOkAA9ynak3zOI\"", "id": "MTUxNTc0OTk2MjI3Mjg1OTU3Nzk0MzQzODQ=", "snippet": { "publishedAt": "2019-11-29T02:57:07.000Z", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "华山日出", "description": "冷冷的山头", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/JE8xdDp5B8Q/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "ikaros-life", "type": "upload" }, "contentDetails": { "upload": { "videoId": "JE8xdDp5B8Q" } } }, { "kind": "youtube#activity", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/NT6mauHnBU6ymoM2jbKT7bfHlss\"", "id": "MTUxNTc0OTk1OTAyMjg1OTU3Nzk0MzY0MzI=", "snippet": { "publishedAt": "2019-11-29T02:51:42.000Z", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "title": "海上日出", "description": "美美美", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/Xfrcfiho_xM/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "ikaros-life", "type": "upload" }, "contentDetails": { "upload": { "videoId": "Xfrcfiho_xM" } } } ] } ================================================ FILE: testdata/modeldata/activities/activity_snippet.json ================================================ { "publishedAt": "2019-12-30T20:00:02.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "2019 Year in Review - The Developer Show", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/DQGSZTxLVrI/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/DQGSZTxLVrI/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/DQGSZTxLVrI/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/DQGSZTxLVrI/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/DQGSZTxLVrI/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "type": "upload" } ================================================ FILE: testdata/modeldata/captions/caption.json ================================================ { "kind": "youtube#caption", "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\"", "id": "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I", "snippet": { "videoId": "oHR3wURdJ94", "lastUpdated": "2020-01-14T09:40:49.981Z", "trackKind": "standard", "language": "en", "name": "", "audioTrackType": "unknown", "isCC": false, "isLarge": false, "isEasyReader": false, "isDraft": false, "isAutoSynced": false, "status": "serving" } } ================================================ FILE: testdata/modeldata/captions/caption_response.json ================================================ { "kind": "youtube#captionListResponse", "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/bB4ewYNN7bQHonV-K7efrgBqh8M\"", "items": [ { "kind": "youtube#caption", "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\"", "id": "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I", "snippet": { "videoId": "oHR3wURdJ94", "lastUpdated": "2020-01-14T09:40:49.981Z", "trackKind": "standard", "language": "en", "name": "", "audioTrackType": "unknown", "isCC": false, "isLarge": false, "isEasyReader": false, "isDraft": false, "isAutoSynced": false, "status": "serving" } }, { "kind": "youtube#caption", "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/iRxIplZcCiX0oujr5gSVMXkij8M\"", "id": "fPMuDm722CIRcUAT3NTPQHQZJZJxt39kU7JvrHk8Kzs=", "snippet": { "videoId": "oHR3wURdJ94", "lastUpdated": "2020-01-14T09:39:46.991Z", "trackKind": "standard", "language": "zh-Hans", "name": "", "audioTrackType": "unknown", "isCC": false, "isLarge": false, "isEasyReader": false, "isDraft": false, "isAutoSynced": false, "status": "serving" } } ] } ================================================ FILE: testdata/modeldata/captions/caption_snippet.json ================================================ { "videoId": "oHR3wURdJ94", "lastUpdated": "2020-01-14T09:40:49.981Z", "trackKind": "standard", "language": "en", "name": "", "audioTrackType": "unknown", "isCC": false, "isLarge": false, "isEasyReader": false, "isDraft": false, "isAutoSynced": false, "status": "serving" } ================================================ FILE: testdata/modeldata/categories/guide_category_info.json ================================================ { "kind": "youtube#guideCategory", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/fnL4T7wf3HKS8VCeb2Mui5q9zeM\"", "id": "GCQmVzdCBvZiBZb3VUdWJl", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Best of YouTube" } } ================================================ FILE: testdata/modeldata/categories/guide_category_response.json ================================================ { "kind": "youtube#guideCategoryListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/KIJAFi2jsRHVBmAk3XYhyRKynjw\"", "items": [ { "kind": "youtube#guideCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\"", "id": "GCQmVzdCBvZiBZb3VUdWJl", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Best of YouTube" } } ] } ================================================ FILE: testdata/modeldata/categories/video_category_info.json ================================================ { "kind": "youtube#videoCategory", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", "id": "17", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Sports", "assignable": true } } ================================================ FILE: testdata/modeldata/categories/video_category_response.json ================================================ { "kind": "youtube#videoCategoryListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/0_wT9Ta0iZu7ETYC3E6Xi_B4mtA\"", "items": [ { "kind": "youtube#videoCategory", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", "id": "17", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Sports", "assignable": true } } ] } ================================================ FILE: testdata/modeldata/channel_sections/channel_section_info.json ================================================ { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/5bNXeieMoiNVa4NokOortBf50ZA\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE", "snippet": { "type": "multipleChannels", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "A channel for every type of developer...", "position": 0, "localized": { "title": "A channel for every type of developer..." } }, "contentDetails": { "channels": [ "UCVHFbqXqoYvEWM1Ddxl0QDg", "UCJS9pqu9BzkAMNTmzNMNhvg", "UCBmwzQnSoj9b6HzNmFrg_yw", "UCnUYZLuoy1rq1aVMwx4aTzw", "UCWf2ZlNsCGDS89VBF_awNvA", "UCP4bf6IHJJQehibu6ai__cg", "UC0rqucBdTuFTjJiefW5t-IQ", "UC8QMvQrV1bsK7WO37QpSxSg", "UClKO7be7O9cUGL94PHnAeOA", "UCwXdFgeE9KYzlDdR7TG9cMw", "UCorTyjVGM-PV5CCKbosONow", "UCXDc-ckqru8BgppXbCt0APw", "UCXPBsjgKKG2HqsKBhWA4uQw", "UCdIiCSqXuybzwGwJwrpHPqw", "UCVhDYDVo3AqyMIKtMLSrcEg", "UCK8sQmJBp8GCxrOtXWBpyEA" ] }, "localizations": { "zh-Hans": { "title": "中文" }, "en-Us": { "title": "english" } } } ================================================ FILE: testdata/modeldata/channel_sections/channel_section_response.json ================================================ { "kind": "youtube#channelSectionListResponse", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/PSyTmUO7BRU2cPplSImsWGWgOz8\"", "items": [ { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/5bNXeieMoiNVa4NokOortBf50ZA\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE", "snippet": { "type": "multipleChannels", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "A channel for every type of developer...", "position": 0, "localized": { "title": "A channel for every type of developer..." } }, "contentDetails": { "channels": [ "UCVHFbqXqoYvEWM1Ddxl0QDg", "UCJS9pqu9BzkAMNTmzNMNhvg", "UCBmwzQnSoj9b6HzNmFrg_yw", "UCnUYZLuoy1rq1aVMwx4aTzw", "UCWf2ZlNsCGDS89VBF_awNvA", "UCP4bf6IHJJQehibu6ai__cg", "UC0rqucBdTuFTjJiefW5t-IQ", "UC8QMvQrV1bsK7WO37QpSxSg", "UClKO7be7O9cUGL94PHnAeOA", "UCwXdFgeE9KYzlDdR7TG9cMw", "UCorTyjVGM-PV5CCKbosONow", "UCXDc-ckqru8BgppXbCt0APw", "UCXPBsjgKKG2HqsKBhWA4uQw", "UCdIiCSqXuybzwGwJwrpHPqw", "UCVhDYDVo3AqyMIKtMLSrcEg", "UCK8sQmJBp8GCxrOtXWBpyEA" ] } }, { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/Qte7tDEpvtKqoGRJAwaCnZqMm3w\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.B8DTd9ZXJqM", "snippet": { "type": "singlePlaylist", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 1 }, "contentDetails": { "playlists": [ "PLOU2XLYxmsILTZ2vd-uxvunCJ1N761Oku" ] } }, { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/xAXz1Uyz3p9hcZqPjw74BF2xT1A\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.MfvRjkWLxgk", "snippet": { "type": "singlePlaylist", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 2 }, "contentDetails": { "playlists": [ "PLOU2XLYxmsII8REpkzsy1bJHj6G1WEVA1" ] } }, { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/LtytVi_D_TBebyD6FcajdZ-XeIg\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.fEjJOXRoWwg", "snippet": { "type": "singlePlaylist", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 3 }, "contentDetails": { "playlists": [ "PLOU2XLYxmsIJZVnmfwfcBhVIVfk1Ql4Do" ] } }, { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/MGoEWG4yrOMMQYvppLDDuO6blMw\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.PvTmxDBxtLs", "snippet": { "type": "singlePlaylist", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 4 }, "contentDetails": { "playlists": [ "PLOU2XLYxmsILKB7ob2wsml3HI6a4e1Qwd" ] } }, { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/CW1euDqQ430vaOh7swjuj4i3vZk\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.pmcIOsL7s98", "snippet": { "type": "singlePlaylist", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 5 }, "contentDetails": { "playlists": [ "PLOU2XLYxmsIKfARnhFe9dRCkZPgwOdZEj" ] } }, { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/XYv3zd3T7ZjN5nsRa5nEoSwVvlM\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.c3r3vYf9uD0", "snippet": { "type": "singlePlaylist", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 6 }, "contentDetails": { "playlists": [ "PLOU2XLYxmsIJs-bCAsrT21mTgen_DklG1" ] } }, { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/L_muP5V_N96m-jDnIT95WERLLVg\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.ZJpkBl-mXfM", "snippet": { "type": "singlePlaylist", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 7 }, "contentDetails": { "playlists": [ "PLOU2XLYxmsIJDw-9-88_LlOs0yR4b4Znv" ] } }, { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/zaHbYWO-Q1zjW4IYjza-bTrqeIc\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.9_wU0qhEPR8", "snippet": { "type": "singlePlaylist", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 8 }, "contentDetails": { "playlists": [ "PLOU2XLYxmsIKKMtrYD-IfPdlVunyPl9GM" ] } }, { "kind": "youtube#channelSection", "etag": "\"Fznwjl6JEQdo1MGvHOGaz_YanRU/RSxEQQPXGQo3MTN75toyRTUTEmY\"", "id": "UC_x5XG1OV2P6uZZ5FSM9Ttw.npYvuMz0_es", "snippet": { "type": "recentUploads", "style": "horizontalRow", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "position": 9 } } ] } ================================================ FILE: testdata/modeldata/channels/channel_api_response.json ================================================ { "kind": "youtube#channelListResponse", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/0lqbdkIcLGXAPiLsJ3FTHo96TDg\"", "pageInfo": { "totalResults": 1, "resultsPerPage": 1 }, "items": [ { "kind": "youtube#channel", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/HUbWoTqNN1LPZKmbyCzPgvjVuR4\"", "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.", "customUrl": "googledevelopers", "publishedAt": "2007-08-23T00:34:43.000Z", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo", "width": 88, "height": 88 }, "medium": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo", "width": 240, "height": 240 }, "high": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo", "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." }, "country": "US" }, "statistics": { "viewCount": "160361638", "commentCount": "0", "subscriberCount": "1927873", "hiddenSubscriberCount": false, "videoCount": "5026" } } ] } ================================================ FILE: testdata/modeldata/channels/channel_branding_settings.json ================================================ { "channel": { "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.", "keywords": "\"google developers\" developers \"Google developers videos\" \"google developer tutorials\" \"developer tutorials\" \"developer news\" android firebase tensorflow chrome web flutter \"google developer experts\" \"google launchpad\" \"developer updates\" google \"google design\"", "defaultTab": "Featured", "trackingAnalyticsAccountId": "YT-9170156-1", "moderateComments": true, "showRelatedChannels": true, "showBrowseView": true, "featuredChannelsTitle": "Featured Channels", "featuredChannelsUrls": [ "UCP4bf6IHJJQehibu6ai__cg", "UCVHFbqXqoYvEWM1Ddxl0QDg", "UCnUYZLuoy1rq1aVMwx4aTzw", "UClKO7be7O9cUGL94PHnAeOA", "UCdIiCSqXuybzwGwJwrpHPqw", "UCJS9pqu9BzkAMNTmzNMNhvg", "UCorTyjVGM-PV5CCKbosONow", "UCTspylBf8iNobZHgwUD4PXA", "UCeo-MamuQVFRcfQmS2N7fhw", "UCQqa5UIHtrnpiADC3eHFupw", "UCXPBsjgKKG2HqsKBhWA4uQw", "UCWf2ZlNsCGDS89VBF_awNvA" ], "unsubscribedTrailer": "lyRPyRKHO8M", "profileColor": "#000000", "country": "US" }, "image": { "bannerImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", "bannerMobileImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", "bannerTabletLowImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1138-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", "bannerTabletImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1707-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", "bannerTabletHdImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2276-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", "bannerTabletExtraHdImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", "bannerMobileLowImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", "bannerMobileMediumHdImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", "bannerMobileHdImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", "bannerMobileExtraHdImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", "bannerTvImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", "bannerTvLowImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", "bannerTvMediumImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", "bannerTvHighImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj" }, "hints": [ { "property": "channel.banner.mobile.medium.image.url", "value": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj" }, { "property": "channel.featured_tab.template.string", "value": "Everything" } ] } ================================================ FILE: testdata/modeldata/channels/channel_content_details.json ================================================ { "relatedPlaylists": { "uploads": "UU_x5XG1OV2P6uZZ5FSM9Ttw", "watchHistory": "HL", "watchLater": "WL" } } ================================================ FILE: testdata/modeldata/channels/channel_info.json ================================================ { "kind": "youtube#channel", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/HUbWoTqNN1LPZKmbyCzPgvjVuR4\"", "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.", "customUrl": "googledevelopers", "publishedAt": "2007-08-23T00:34:43.000Z", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo", "width": 88, "height": 88 }, "medium": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo", "width": 240, "height": 240 }, "high": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo", "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." }, "country": "US" }, "contentDetails": { "relatedPlaylists": { "uploads": "UU_x5XG1OV2P6uZZ5FSM9Ttw", "watchHistory": "HL", "watchLater": "WL" } }, "statistics": { "viewCount": "160361638", "commentCount": "0", "subscriberCount": "1927873", "hiddenSubscriberCount": false, "videoCount": "5026" }, "topicDetails": { "topicIds": [ "/m/019_rr", "/m/07c1v", "/m/02jjt", "/m/019_rr", "/m/07c1v", "/m/02jjt" ], "topicCategories": [ "https://en.wikipedia.org/wiki/Entertainment", "https://en.wikipedia.org/wiki/Technology", "https://en.wikipedia.org/wiki/Lifestyle_(sociology)" ] }, "status": { "privacyStatus": "public", "isLinked": true, "longUploadsStatus": "longUploadsUnspecified" }, "brandingSettings": { "channel": { "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.", "keywords": "\"google developers\" developers \"Google developers videos\" \"google developer tutorials\" \"developer tutorials\" \"developer news\" android firebase tensorflow chrome web flutter \"google developer experts\" \"google launchpad\" \"developer updates\" google \"google design\"", "defaultTab": "Featured", "trackingAnalyticsAccountId": "YT-9170156-1", "moderateComments": true, "showRelatedChannels": true, "showBrowseView": true, "featuredChannelsTitle": "Featured Channels", "featuredChannelsUrls": [ "UCP4bf6IHJJQehibu6ai__cg", "UCVHFbqXqoYvEWM1Ddxl0QDg", "UCnUYZLuoy1rq1aVMwx4aTzw", "UClKO7be7O9cUGL94PHnAeOA", "UCdIiCSqXuybzwGwJwrpHPqw", "UCJS9pqu9BzkAMNTmzNMNhvg", "UCorTyjVGM-PV5CCKbosONow", "UCTspylBf8iNobZHgwUD4PXA", "UCeo-MamuQVFRcfQmS2N7fhw", "UCQqa5UIHtrnpiADC3eHFupw", "UCXPBsjgKKG2HqsKBhWA4uQw", "UCWf2ZlNsCGDS89VBF_awNvA" ], "unsubscribedTrailer": "lyRPyRKHO8M", "profileColor": "#000000", "country": "US" }, "image": { "bannerImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", "bannerMobileImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", "bannerTabletLowImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1138-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", "bannerTabletImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1707-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", "bannerTabletHdImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2276-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", "bannerTabletExtraHdImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", "bannerMobileLowImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", "bannerMobileMediumHdImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", "bannerMobileHdImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", "bannerMobileExtraHdImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", "bannerTvImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", "bannerTvLowImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", "bannerTvMediumImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", "bannerTvHighImageUrl": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj" }, "hints": [ { "property": "channel.banner.mobile.medium.image.url", "value": "https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj" }, { "property": "channel.featured_tab.template.string", "value": "Everything" } ] } } ================================================ FILE: testdata/modeldata/channels/channel_snippet.json ================================================ { "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.", "customUrl": "googledevelopers", "publishedAt": "2007-08-23T00:34:43.000Z", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo", "width": 88, "height": 88 }, "medium": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo", "width": 240, "height": 240 }, "high": { "url": "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo", "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." }, "country": "US" } ================================================ FILE: testdata/modeldata/channels/channel_statistics.json ================================================ { "viewCount": 160361638, "commentCount": "0", "subscriberCount": "1927873", "hiddenSubscriberCount": false, "videoCount": "5026" } ================================================ FILE: testdata/modeldata/channels/channel_status.json ================================================ { "privacyStatus": "public", "isLinked": true, "longUploadsStatus": "longUploadsUnspecified" } ================================================ FILE: testdata/modeldata/channels/channel_topic_details.json ================================================ { "topicIds": [ "/m/019_rr", "/m/07c1v", "/m/02jjt", "/m/019_rr", "/m/07c1v", "/m/02jjt" ], "topicCategories": [ "https://en.wikipedia.org/wiki/Entertainment", "https://en.wikipedia.org/wiki/Technology", "https://en.wikipedia.org/wiki/Lifestyle_(sociology)" ] } ================================================ FILE: testdata/modeldata/comments/comment_api_response.json ================================================ { "kind": "youtube#commentListResponse", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/WGjMjz47HiC5hiv290at1ES2VhM\"", "items": [ { "kind": "youtube#comment", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/DNyaTRe4NG3pWMwjpCUwPAYb9uk\"", "id": "UgxKREWxIgDrw8w2e_Z4AaABAg", "snippet": { "authorDisplayName": "Hieu Nguyen", "authorProfileImageUrl": "https://yt3.ggpht.com/-N1uydT1LhpA/AAAAAAAAAAI/AAAAAAAAAAA/nvwONlQ4ZsE/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", "authorChannelUrl": "http://www.youtube.com/channel/UClfzT4CU_yaZjJaI4pKqSjQ", "authorChannelId": { "value": "UClfzT4CU_yaZjJaI4pKqSjQ" }, "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 ...", "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 ...", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-04-20T01:03:39.000Z", "updatedAt": "2019-04-20T01:03:39.000Z" } }, { "kind": "youtube#comment", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/bmZin9yRcFlXNixJ4nHaVlISbZM\"", "id": "UgyrVQaFfEdvaSzstj14AaABAg", "snippet": { "authorDisplayName": "Mani Kanta", "authorProfileImageUrl": "https://yt3.ggpht.com/-8VVOkpYv6O4/AAAAAAAAAAI/AAAAAAAAAAA/9asGD8pGx7Y/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", "authorChannelUrl": "http://www.youtube.com/channel/UCJBxRADq6jctX-YdhjkB6PA", "authorChannelId": { "value": "UCJBxRADq6jctX-YdhjkB6PA" }, "textDisplay": "super", "textOriginal": "super", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-04-04T04:14:44.000Z", "updatedAt": "2019-04-04T04:14:44.000Z" } } ] } ================================================ FILE: testdata/modeldata/comments/comment_info.json ================================================ { "kind": "youtube#comment", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/4cvUO3bQNuuOby5VnN9ZtVUJfk8\"", "id": "UgwxApqcfzZzF_C5Zqx4AaABAg", "snippet": { "authorDisplayName": "Oeurn Ravuth", "authorProfileImageUrl": "https://yt3.ggpht.com/-FTjrEZu33Cg/AAAAAAAAAAI/AAAAAAAAAAA/74aahJJl02c/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", "authorChannelUrl": "http://www.youtube.com/channel/UCqPku3cxM-ED3poX8YtGqeg", "authorChannelId": { "value": "UCqPku3cxM-ED3poX8YtGqeg" }, "videoId": "wtLJPvx7-ys", "textDisplay": "This video is awesome! GOOD", "textOriginal": "This video is awesome! GOOD", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-03-28T11:33:46.000Z", "updatedAt": "2019-03-28T11:33:46.000Z" } } ================================================ FILE: testdata/modeldata/comments/comment_snippet.json ================================================ { "authorDisplayName": "Oeurn Ravuth", "authorProfileImageUrl": "https://yt3.ggpht.com/-FTjrEZu33Cg/AAAAAAAAAAI/AAAAAAAAAAA/74aahJJl02c/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", "authorChannelUrl": "http://www.youtube.com/channel/UCqPku3cxM-ED3poX8YtGqeg", "authorChannelId": { "value": "UCqPku3cxM-ED3poX8YtGqeg" }, "videoId": "wtLJPvx7-ys", "textDisplay": "This video is awesome! GOOD", "textOriginal": "This video is awesome! GOOD", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-03-28T11:33:46.000Z", "updatedAt": "2019-03-28T11:33:46.000Z" } ================================================ FILE: testdata/modeldata/comments/comment_thread_api_response.json ================================================ { "kind": "youtube#commentThreadListResponse", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/x02Ynz4jiNB0_bPpgqOoltUaAIw\"", "pageInfo": { "totalResults": 2, "resultsPerPage": 20 }, "items": [ { "kind": "youtube#commentThread", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/YzERg5ywz5smx8eBpQ07fRaCWmo\"", "id": "Ugz097FRhsQy5CVhAjp4AaABAg", "snippet": { "videoId": "cD7NPxuuXYY", "topLevelComment": { "kind": "youtube#comment", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/cMMoinsp0Nc5wvMvENly9N9Llyo\"", "id": "Ugz097FRhsQy5CVhAjp4AaABAg", "snippet": { "authorDisplayName": "Paulo José Martínez", "authorProfileImageUrl": "https://yt3.ggpht.com/-1gM871v6gjw/AAAAAAAAAAI/AAAAAAAAAAA/golxu5t1oGQ/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", "authorChannelUrl": "http://www.youtube.com/channel/UCCz-SK7nwY_a3I2EqrplrLQ", "authorChannelId": { "value": "UCCz-SK7nwY_a3I2EqrplrLQ" }, "videoId": "cD7NPxuuXYY", "textDisplay": "I spend more than 3 days to understand how to fix gradle and react native in android studio. I need to know if exist a page that can see information about errors and how to fix. I am tired of visit Stack Overflow every day. help!", "textOriginal": "I spend more than 3 days to understand how to fix gradle and react native in android studio. I need to know if exist a page that can see information about errors and how to fix. I am tired of visit Stack Overflow every day. help!", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-08-23T14:06:16.000Z", "updatedAt": "2019-08-23T14:06:16.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } }, { "kind": "youtube#commentThread", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/PAJ4fUiE3D2BVdF7C8uieNxb_wc\"", "id": "UgzhytyP79_PwaDd4UB4AaABAg", "snippet": { "videoId": "Azt8Nc-mtKM", "topLevelComment": { "kind": "youtube#comment", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/oPcBe_mf_rOLgyZ3KY6oYJjdY_o\"", "id": "UgzhytyP79_PwaDd4UB4AaABAg", "snippet": { "authorDisplayName": "asdf7692", "authorProfileImageUrl": "https://yt3.ggpht.com/-46C1T2dCUzM/AAAAAAAAAAI/AAAAAAAAAAA/y0-4fvhE0hM/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", "authorChannelUrl": "http://www.youtube.com/channel/UC9Prn_6KFp1xu-t-rf-K8cA", "authorChannelId": { "value": "UC9Prn_6KFp1xu-t-rf-K8cA" }, "videoId": "Azt8Nc-mtKM", "textDisplay": "wait, that's the guy that wrote my textbook!", "textOriginal": "wait, that's the guy that wrote my textbook!", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-08-23T23:12:09.000Z", "updatedAt": "2019-08-23T23:12:09.000Z" } }, "canReply": true, "totalReplyCount": 0, "isPublic": true } } ] } ================================================ FILE: testdata/modeldata/comments/comment_thread_info.json ================================================ { "kind": "youtube#commentThread", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Bu9ED8YytJP_F5IxgbsfRJg0CZI\"", "id": "UgydxWWoeA7F1OdqypJ4AaABAg", "snippet": { "videoId": "D-lhorsDlUQ", "topLevelComment": { "kind": "youtube#comment", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Vcd3llXDKJW8UrWr8ndIHHDBk8g\"", "id": "UgydxWWoeA7F1OdqypJ4AaABAg", "snippet": { "authorDisplayName": "Loren Robilio", "authorProfileImageUrl": "https://yt3.ggpht.com/-dVa9HLlQcNs/AAAAAAAAAAI/AAAAAAAAAAA/lxKAIuHR-20/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", "authorChannelUrl": "http://www.youtube.com/channel/UCe9i1nJCcevTa6KJa55KYog", "authorChannelId": { "value": "UCe9i1nJCcevTa6KJa55KYog" }, "videoId": "D-lhorsDlUQ", "textDisplay": "Actions.ai", "textOriginal": "Actions.ai", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-06-23T08:24:24.000Z", "updatedAt": "2019-06-23T08:24:24.000Z" } }, "canReply": true, "totalReplyCount": 1, "isPublic": true }, "replies": { "comments": [ { "kind": "youtube#comment", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Z_3RVDklwNvCP3pgufc11jc5ud0\"", "id": "UgydxWWoeA7F1OdqypJ4AaABAg.8wWQ3tdHcFx8xcDheui-qb", "snippet": { "authorDisplayName": "Dian Anggraeni", "authorProfileImageUrl": "https://yt3.ggpht.com/-WLAKDA-bqa8/AAAAAAAAAAI/AAAAAAAAAAA/4VOHyI34fuU/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", "authorChannelUrl": "http://www.youtube.com/channel/UCsl2A_QzcSnFD2hpCyVwXxA", "authorChannelId": { "value": "UCsl2A_QzcSnFD2hpCyVwXxA" }, "videoId": "D-lhorsDlUQ", "textDisplay": "#", "textOriginal": "#", "parentId": "UgydxWWoeA7F1OdqypJ4AaABAg", "canRate": true, "viewerRating": "none", "likeCount": 1, "publishedAt": "2019-07-20T20:22:27.000Z", "updatedAt": "2019-07-20T20:22:27.000Z" } } ] } } ================================================ FILE: testdata/modeldata/comments/comment_thread_replies.json ================================================ { "comments": [ { "kind": "youtube#comment", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Z_3RVDklwNvCP3pgufc11jc5ud0\"", "id": "UgydxWWoeA7F1OdqypJ4AaABAg.8wWQ3tdHcFx8xcDheui-qb", "snippet": { "authorDisplayName": "Dian Anggraeni", "authorProfileImageUrl": "https://yt3.ggpht.com/-WLAKDA-bqa8/AAAAAAAAAAI/AAAAAAAAAAA/4VOHyI34fuU/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", "authorChannelUrl": "http://www.youtube.com/channel/UCsl2A_QzcSnFD2hpCyVwXxA", "authorChannelId": { "value": "UCsl2A_QzcSnFD2hpCyVwXxA" }, "videoId": "D-lhorsDlUQ", "textDisplay": "#", "textOriginal": "#", "parentId": "UgydxWWoeA7F1OdqypJ4AaABAg", "canRate": true, "viewerRating": "none", "likeCount": 1, "publishedAt": "2019-07-20T20:22:27.000Z", "updatedAt": "2019-07-20T20:22:27.000Z" } } ] } ================================================ FILE: testdata/modeldata/comments/comment_thread_snippet.json ================================================ { "videoId": "D-lhorsDlUQ", "topLevelComment": { "kind": "youtube#comment", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Vcd3llXDKJW8UrWr8ndIHHDBk8g\"", "id": "UgydxWWoeA7F1OdqypJ4AaABAg", "snippet": { "authorDisplayName": "Loren Robilio", "authorProfileImageUrl": "https://yt3.ggpht.com/-dVa9HLlQcNs/AAAAAAAAAAI/AAAAAAAAAAA/lxKAIuHR-20/s28-c-k-no-mo-rj-c0xffffff/photo.jpg", "authorChannelUrl": "http://www.youtube.com/channel/UCe9i1nJCcevTa6KJa55KYog", "authorChannelId": { "value": "UCe9i1nJCcevTa6KJa55KYog" }, "videoId": "D-lhorsDlUQ", "textDisplay": "Actions.ai", "textOriginal": "Actions.ai", "canRate": true, "viewerRating": "none", "likeCount": 0, "publishedAt": "2019-06-23T08:24:24.000Z", "updatedAt": "2019-06-23T08:24:24.000Z" } }, "canReply": true, "totalReplyCount": 1, "isPublic": true } ================================================ FILE: testdata/modeldata/common/thumbnail_info.json ================================================ { "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s88-c-k-c0xffffffff-no-rj-mo", "width": 88, "height": 88 } ================================================ FILE: testdata/modeldata/common/thumbnails_info.json ================================================ { "default": { "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s88-c-k-c0xffffffff-no-rj-mo", "width": 88, "height": 88 }, "medium": { "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s240-c-k-c0xffffffff-no-rj-mo", "width": 240, "height": 240 }, "high": { "url": "https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s800-c-k-c0xffffffff-no-rj-mo", "width": 800, "height": 800 } } ================================================ FILE: testdata/modeldata/i18ns/language_info.json ================================================ { "kind": "youtube#i18nLanguage", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/GMrwiM1f-4KHxMka40cB3lysLgY\"", "id": "af", "snippet": { "hl": "af", "name": "Afrikaans" } } ================================================ FILE: testdata/modeldata/i18ns/language_res.json ================================================ { "kind": "youtube#i18nLanguageListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/qgFy24yvs-L_dNjr2d-Rd_Xcfw4\"", "items": [ { "kind": "youtube#i18nLanguage", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/GMrwiM1f-4KHxMka40cB3lysLgY\"", "id": "af", "snippet": { "hl": "af", "name": "Afrikaans" } }, { "kind": "youtube#i18nLanguage", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/wOlCLE4kfCyCca9_ssuNDceE0yk\"", "id": "az", "snippet": { "hl": "az", "name": "Azerbaijani" } } ] } ================================================ FILE: testdata/modeldata/i18ns/region_info.json ================================================ { "kind": "youtube#i18nRegion", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/R_GB1d7CQi3LIpoHKbakFDisvoA\"", "id": "DZ", "snippet": { "gl": "DZ", "name": "Algeria" } } ================================================ FILE: testdata/modeldata/i18ns/region_res.json ================================================ { "kind": "youtube#i18nRegionListResponse", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/q85_wZeDyKDzYtt-LhNaozyi_sk\"", "items": [ { "kind": "youtube#i18nRegion", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/R_GB1d7CQi3LIpoHKbakFDisvoA\"", "id": "DZ", "snippet": { "gl": "DZ", "name": "Algeria" } }, { "kind": "youtube#i18nRegion", "etag": "\"SJZWTG6xR0eGuCOh2bX6w3s4F94/w6ci5tJWSaqFmjn3xsM2loOjo2o\"", "id": "AR", "snippet": { "gl": "AR", "name": "Argentina" } } ] } ================================================ FILE: testdata/modeldata/members/member_info.json ================================================ { "kind": "youtube#member", "etag": "etag", "snippet": { "creatorChannelId": "UCa-vrCLQHviTOVnEKDOdetQ", "memberDetails": { "channelId": "UCa-vrCLQHviTOVnEKDOdetQ", "channelUrl": "https://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ", "displayName": "ikaros-life", "profileImageUrl": "https://yt3.ggpht.com/a-/AOh14Gg1_gYcI03VLDd3FMLUY5cb5O9RC9sElj26-1SR=s288-c-k-c0xffffffff-no-rj-mo" }, "membershipsDetails": { "highestAccessibleLevel": "string", "highestAccessibleLevelDisplayName": "string", "accessibleLevels": [ "string" ], "membershipsDuration": { "memberSince": "2007-08-23T00:34:43Z", "memberTotalDurationMonths": 5 }, "membershipsDurationAtLevel": [ { "level": "string", "memberSince": "2007-08-23T00:34:43Z", "memberTotalDurationMonths": 6 } ] } } } ================================================ FILE: testdata/modeldata/members/membership_level.json ================================================ { "kind": "youtube#membershipsLevel", "etag": "etag", "id": "id", "snippet": { "creatorChannelId": "UCa-vrCLQHviTOVnEKDOdetQ", "levelDetails": { "displayName": "high" } } } ================================================ FILE: testdata/modeldata/playlist_items/playlist_item_api_response.json ================================================ { "kind": "youtube#playlistItemListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/2r7BiOpjx2NRuQ2KuoLxRoJmZUI\"", "pageInfo": { "totalResults": 3, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/E5rTjxNaKfzDc-GFs2Cb9jkKlGM\"", "id": "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2", "snippet": { "publishedAt": "2019-05-11T00:27:38.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google I/O'19 - I/O Live (Day 1 Composite)", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "position": 0, "resourceId": { "kind": "youtube#video", "videoId": "H1HZyvc0QnI" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/3JqJ3Bv7ZIVEu4ZoeH6ZGsUe7js\"", "id": "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4yODlGNEE0NkRGMEEzMEQy", "snippet": { "publishedAt": "2019-05-11T00:52:10.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google I/O'19 - I/O Live (Day 2 Composite)", "description": "Relive moments from I/O Live, Day 2, 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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/5NgsfxIWNls/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "position": 1, "resourceId": { "kind": "youtube#video", "videoId": "5NgsfxIWNls" } } }, { "kind": "youtube#playlistItem", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Kib3kvf3c_Bq79UyVpa2pHYzV_U\"", "id": "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4wMTcyMDhGQUE4NTIzM0Y5", "snippet": { "publishedAt": "2019-05-11T00:55:44.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Google I/O'19 - I/O Live (Day 3 Composite)", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/VCv-KKIkLns/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/VCv-KKIkLns/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/VCv-KKIkLns/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/VCv-KKIkLns/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/VCv-KKIkLns/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "position": 2, "resourceId": { "kind": "youtube#video", "videoId": "VCv-KKIkLns" } } } ] } ================================================ FILE: testdata/modeldata/playlist_items/playlist_item_content_details.json ================================================ { "videoId": "D-lhorsDlUQ", "videoPublishedAt": "2019-03-21T20:37:49.000Z" } ================================================ FILE: testdata/modeldata/playlist_items/playlist_item_info.json ================================================ { "kind": "youtube#playlistItem", "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/lAPls3tzYIP4Re0-vMkPDF4whaw\"", "id": "UExPVTJYTFl4bXNJSnB1ZmVNSG5jblF2Rk9lMEszTWhWcC41NkI0NEY2RDEwNTU3Q0M2", "snippet": { "publishedAt": "2019-05-16T18:46:20.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "What are Actions on Google (Assistant on Air)", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp", "position": 0, "resourceId": { "kind": "youtube#video", "videoId": "D-lhorsDlUQ" }, "videoOwnerChannelTitle": "Google Developers", "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" }, "contentDetails": { "videoId": "D-lhorsDlUQ", "videoPublishedAt": "2019-03-21T20:37:49.000Z" }, "status": { "privacyStatus": "public" } } ================================================ FILE: testdata/modeldata/playlist_items/playlist_item_snippet.json ================================================ { "publishedAt": "2019-05-16T18:46:20.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "What are Actions on Google (Assistant on Air)", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "playlistId": "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp", "position": 0, "resourceId": { "kind": "youtube#video", "videoId": "D-lhorsDlUQ" } } ================================================ FILE: testdata/modeldata/playlist_items/playlist_item_status.json ================================================ { "privacyStatus": "public" } ================================================ FILE: testdata/modeldata/playlists/playlist_api_response.json ================================================ { "kind": "youtube#playlistListResponse", "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/BfhLqBNRhhd1rjH-NUyOUzazr-4\"", "pageInfo": { "totalResults": 416, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#playlist", "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/XooPPPPffp2qIyK-PJIIwE8GJuM\"", "id": "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp", "snippet": { "publishedAt": "2019-05-16T18:46:20.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Assistant on Air", "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!", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Assistant on Air", "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!" } }, "status": { "privacyStatus": "public" }, "contentDetails": { "itemCount": 4 } }, { "kind": "youtube#playlist", "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/zik79It-4mLFCMBeiYtbHEkN330\"", "id": "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "snippet": { "publishedAt": "2019-05-10T00:18:56.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "I/O Live - Show Composite", "description": "", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "I/O Live - Show Composite", "description": "" } }, "status": { "privacyStatus": "public" }, "contentDetails": { "itemCount": 3 } }, { "kind": "youtube#playlist", "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/Q2iRVdwJ4gRZS6h0x8unUsejZvk\"", "id": "PLOU2XLYxmsIJJVnHWmd1qfr0Caq4VZCu4", "snippet": { "publishedAt": "2019-05-10T00:18:07.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "I/O Live", "description": "Relive moments from Google I/O 2019", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/UVOhgly2VEc/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/UVOhgly2VEc/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/UVOhgly2VEc/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/UVOhgly2VEc/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/UVOhgly2VEc/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "I/O Live", "description": "Relive moments from Google I/O 2019" } }, "status": { "privacyStatus": "public" }, "contentDetails": { "itemCount": 23 } }, { "kind": "youtube#playlist", "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/QXl0bYWnpSuOs3NHzhWJ1mF78_k\"", "id": "PLOU2XLYxmsIKW-llcbcFdpR9RjCfYHZaV", "snippet": { "publishedAt": "2019-05-02T23:39:42.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Machine Learning at Google I/O 2019", "description": "This playlist contains every Machine Learning session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/machine-learning/guides/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/pM9u9xcM_cs/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/pM9u9xcM_cs/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/pM9u9xcM_cs/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/pM9u9xcM_cs/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/pM9u9xcM_cs/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Machine Learning at Google I/O 2019", "description": "This playlist contains every Machine Learning session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://developers.google.com/machine-learning/guides/" } }, "status": { "privacyStatus": "public" }, "contentDetails": { "itemCount": 14 } }, { "kind": "youtube#playlist", "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/_vt3bEv3Nji--Q-pDnqQi_jpO24\"", "id": "PLOU2XLYxmsIIOSO0eWuj-6yQmdakarUzN", "snippet": { "publishedAt": "2019-05-02T23:38:49.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Accessibility at Google I/O 2019", "description": "This playlist contains every Accessibiity session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://www.google.com/accessibility/for-developers/", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/vnSDqh6zT6Y/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Accessibility at Google I/O 2019", "description": "This playlist contains every Accessibiity session from Google I/O 2019.\nLearn more on the I/O Website → https://google.com/io\n\nSubscribe to Google Devs → https://goo.gle/developers\nGet started at → https://www.google.com/accessibility/for-developers/" } }, "status": { "privacyStatus": "public" }, "contentDetails": { "itemCount": 4 } } ] } ================================================ FILE: testdata/modeldata/playlists/playlist_content_details.json ================================================ { "itemCount": 4 } ================================================ FILE: testdata/modeldata/playlists/playlist_info.json ================================================ { "kind": "youtube#playlist", "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/XooPPPPffp2qIyK-PJIIwE8GJuM\"", "id": "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp", "snippet": { "publishedAt": "2019-05-16T18:46:20.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Assistant on Air", "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!", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Assistant on Air", "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!" } }, "status": { "privacyStatus": "public" }, "contentDetails": { "itemCount": 4 } } ================================================ FILE: testdata/modeldata/playlists/playlist_snippet.json ================================================ { "publishedAt": "2019-05-16T18:46:20.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Assistant on Air", "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!", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "localized": { "title": "Assistant on Air", "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!" } } ================================================ FILE: testdata/modeldata/playlists/playlist_status.json ================================================ { "privacyStatus": "public" } ================================================ FILE: testdata/modeldata/search_result/search_result.json ================================================ { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/vbYWvy5RlqHHhMVjeHUTwJcQQWg\"", "id": { "kind": "youtube#video", "videoId": "fq4N0hgOWzU" }, "snippet": { "publishedAt": "2018-02-23T15:00:09.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Introducing Flutter", "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 ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/fq4N0hgOWzU/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/fq4N0hgOWzU/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/fq4N0hgOWzU/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } } ================================================ FILE: testdata/modeldata/search_result/search_result_api_response.json ================================================ { "kind": "youtube#searchListResponse", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/aluR_NlUCSvgLE_pAjGxhfmcHoY\"", "nextPageToken": "CAUQAA", "regionCode": "US", "pageInfo": { "totalResults": 489126, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/vbYWvy5RlqHHhMVjeHUTwJcQQWg\"", "id": { "kind": "youtube#video", "videoId": "fq4N0hgOWzU" }, "snippet": { "publishedAt": "2018-02-23T15:00:09.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Introducing Flutter", "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 ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/fq4N0hgOWzU/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/fq4N0hgOWzU/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/fq4N0hgOWzU/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/NPdUn-g5a_FcBJsdRmVLX4FUjtw\"", "id": { "kind": "youtube#video", "videoId": "cKxRvEZd3Mw" }, "snippet": { "publishedAt": "2016-03-30T16:59:12.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Hello World - Machine Learning Recipes #1", "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 ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/cKxRvEZd3Mw/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/cKxRvEZd3Mw/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/cKxRvEZd3Mw/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/ZvP7ILdxl9Kzl8PrkCWA71Dh_GY\"", "id": { "kind": "youtube#playlist", "playlistId": "PLOU2XLYxmsIJ7imRl4jU7623pHNjZqw3t" }, "snippet": { "publishedAt": "2018-08-31T16:03:59.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Mobile Ads Garage : Season 2", "description": "Welcome to the Mobile Ads Garage, a YouTube series where we'll show you the nuts and bolts of the Google Mobile Ads SDK and the best ways to monetize ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/vfsgBBky4bg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/vfsgBBky4bg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/vfsgBBky4bg/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/WaD7AfMNykBwM2y31b43CKk7WKs\"", "id": { "kind": "youtube#playlist", "playlistId": "PLOU2XLYxmsIKX0pUJV3uqp6N3NeHwHh0c" }, "snippet": { "publishedAt": "2016-04-05T22:45:35.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Mobile Ads Garage", "description": "Welcome to the Mobile Ads Garage, a YouTube series where we'll show you the nuts and bolts of the Google Mobile Ads SDK and the best ways to monetize ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/OLLLRUPICcc/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/OLLLRUPICcc/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/OLLLRUPICcc/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } }, { "kind": "youtube#searchResult", "etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/Stj9CP60v4-ZD7b7FBgp9h9OffY\"", "id": { "kind": "youtube#video", "videoId": "mWl45NkFBOc" }, "snippet": { "publishedAt": "2017-02-15T18:01:59.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "TensorFlow: Machine Learning for Everyone", "description": "The TensorFlow community is thriving. We're thrilled to see the adoption and the pace of machine learning development by people all around the world.", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/mWl45NkFBOc/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/mWl45NkFBOc/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/mWl45NkFBOc/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Google Developers", "liveBroadcastContent": "none" } } ] } ================================================ FILE: testdata/modeldata/search_result/search_result_id.json ================================================ { "kind": "youtube#playlist", "playlistId": "PLOU2XLYxmsIKX0pUJV3uqp6N3NeHwHh0c" } ================================================ FILE: testdata/modeldata/search_result/search_result_snippet.json ================================================ { "publishedAt": "2016-03-30T16:59:12.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "Hello World - Machine Learning Recipes #1", "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 ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/cKxRvEZd3Mw/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/cKxRvEZd3Mw/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/cKxRvEZd3Mw/hqdefault.jpg", "width": 480, "height": 360 } } } ================================================ FILE: testdata/modeldata/subscriptions/contentDetails.json ================================================ { "totalItemCount": 2, "newItemCount": 0, "activityType": "all" } ================================================ FILE: testdata/modeldata/subscriptions/resp.json ================================================ { "kind": "youtube#subscriptionListResponse", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/tcw1POI4O_SxXM12fDiwN47t82I\"", "nextPageToken": "CAUQAA", "pageInfo": { "totalResults": 16, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/R40G3IlR9sgjUldVPi90sGvUQTE\"", "id": "zqShTXi-2-Tx7TtwQqhCBwtJ-Aho6DZeutqZiP4Q79Q", "snippet": { "publishedAt": "2018-12-25T09:12:18.265Z", "title": "Next Day Video", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UCQ7dFBzZGlBvtU2hCecsBBg" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } }, "contentDetails": { "totalItemCount": 1562, "newItemCount": 0, "activityType": "all" }, "subscriberSnippet": { "title": "kun liu", "description": "", "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/vqJozAaZqELuPPv4HHNhUWLLY20\"", "id": "zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo", "snippet": { "publishedAt": "2018-09-11T11:35:04.568Z", "title": "PyCon 2015", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UCgxzjK6GuOHVKR_08TT4hJQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } }, "contentDetails": { "totalItemCount": 134, "newItemCount": 0, "activityType": "all" }, "subscriberSnippet": { "title": "kun liu", "description": "", "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/7ArnKe3x-zHWrA235OPSLkFtcGg\"", "id": "zqShTXi-2-S50Nc0aJJ6zdHSZbM7XNml9y9B3V6WQ9A", "snippet": { "publishedAt": "2018-06-11T01:42:48.406Z", "title": "李永乐老师", "description": "欢迎关注我的微信公众号“李永乐老师”,上面有超多文字版科普内容和中学视频课程哦", "resourceId": { "kind": "youtube#channel", "channelId": "UCSs4A6HYKmHA2MG_0z-F0xw" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } }, "contentDetails": { "totalItemCount": 278, "newItemCount": 1, "activityType": "all" }, "subscriberSnippet": { "title": "kun liu", "description": "", "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/hZAeF0AETpmxML6TuUZfYWXtNzQ\"", "id": "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo", "snippet": { "publishedAt": "2019-11-29T03:00:56.380Z", "title": "ikaros-life", "description": "This is a test channel.", "resourceId": { "kind": "youtube#channel", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } }, "contentDetails": { "totalItemCount": 2, "newItemCount": 0, "activityType": "all" }, "subscriberSnippet": { "title": "kun liu", "description": "", "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } }, { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/YH8OpiSknjSnSE5lK4iP84c-RMg\"", "id": "zqShTXi-2-Rya5uUxEp3ZpfEZoPHGpH2MBMMdN1Yl9Y", "snippet": { "publishedAt": "2019-05-28T01:19:42.921Z", "title": "PyCon 2019", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UCxs2IIVXaEHHA4BtTiWZ2mQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } }, "contentDetails": { "totalItemCount": 148, "newItemCount": 0, "activityType": "all" }, "subscriberSnippet": { "title": "kun liu", "description": "", "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } } ] } ================================================ FILE: testdata/modeldata/subscriptions/snippet.json ================================================ { "publishedAt": "2018-12-25T09:12:18.265Z", "title": "Next Day Video", "description": "", "resourceId": { "kind": "youtube#channel", "channelId": "UCQ7dFBzZGlBvtU2hCecsBBg" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } ================================================ FILE: testdata/modeldata/subscriptions/subscriberSnippet.json ================================================ { "title": "kun liu", "description": "", "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } ================================================ FILE: testdata/modeldata/subscriptions/subscription.json ================================================ { "kind": "youtube#subscription", "etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/hZAeF0AETpmxML6TuUZfYWXtNzQ\"", "id": "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo", "snippet": { "publishedAt": "2019-11-29T03:00:56.380Z", "title": "ikaros-life", "description": "This is a test channel.", "resourceId": { "kind": "youtube#channel", "channelId": "UCa-vrCLQHviTOVnEKDOdetQ" }, "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } }, "contentDetails": { "totalItemCount": 2, "newItemCount": 0, "activityType": "all" }, "subscriberSnippet": { "title": "kun liu", "description": "", "channelId": "UCNvMBmCASzTNNX8lW3JRMbw", "thumbnails": { "default": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "medium": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg" }, "high": { "url": "https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg" } } } } ================================================ FILE: testdata/modeldata/users/access_token.json ================================================ { "access_token": "access_token", "id_token": "id_token", "expires_in": 3600, "token_type": "Bearer", "scope": [ "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/youtube" ], "refresh_token": "refresh_token" } ================================================ FILE: testdata/modeldata/users/user_profile.json ================================================ { "family_name": "liu", "name": "kun liu", "picture": "https://lh3.googleusercontent.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAACY/1E9uN31I7cE/photo.jpg", "locale": "zh-CN", "given_name": "kun", "id": "12345678910" } ================================================ FILE: testdata/modeldata/videos/video_api_response.json ================================================ { "kind": "youtube#videoListResponse", "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/iv3lyeXWs7VXBQLwxMdaHn-GgXM\"", "pageInfo": { "totalResults": 1, "resultsPerPage": 1 }, "items": [ { "kind": "youtube#video", "etag": "\"nlUZBA6NbTS7q9G8D1GljyfTIWI/tLwQL5gV5utoTrC4nEayyWXynfY\"", "id": "D-lhorsDlUQ", "snippet": { "publishedAt": "2019-03-21T20:37:49.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "What are Actions on Google (Assistant on Air)", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "tags": [ "Google", "developers", "aog", "Actions on Google", "Assistant", "Google Assistant", "actions", "google home", "actions on google", "google assistant developers", "google assistant sdk", "Actions on google developers", "smarthome developers", "common terminology", "custom action on google", "google assistant in your app", "add google assistant", "assistant on air", "how to use google assistant on air", "Actions on Google how to" ], "categoryId": "28", "liveBroadcastContent": "none", "defaultLanguage": "en", "localized": { "title": "What are Actions on Google (Assistant on Air)", "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" }, "defaultAudioLanguage": "en" }, "contentDetails": { "duration": "PT21M7S", "dimension": "2d", "definition": "hd", "caption": "true", "licensedContent": false, "projection": "rectangular" }, "status": { "uploadStatus": "processed", "privacyStatus": "public", "license": "youtube", "embeddable": true, "publicStatsViewable": true }, "statistics": { "viewCount": "7920", "likeCount": "190", "dislikeCount": "23", "favoriteCount": "0", "commentCount": "32" } } ] } ================================================ FILE: testdata/modeldata/videos/video_category_info.json ================================================ { "kind": "youtube#videoCategory", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/9GQMSRjrZdHeb1OEM1XVQ9zbGec\"", "id": "17", "snippet": { "channelId": "UCBR8-60-B28hp2BmDPdntcQ", "title": "Sports", "assignable": true } } ================================================ FILE: testdata/modeldata/videos/video_content_details.json ================================================ { "duration": "PT21M7S", "dimension": "2d", "definition": "hd", "caption": "true", "licensedContent": false, "projection": "rectangular" } ================================================ FILE: testdata/modeldata/videos/video_info.json ================================================ { "kind": "youtube#video", "etag": "\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/ywRH8hhCtBBffWyVbxSDNA08vr0\"", "id": "D-lhorsDlUQ", "snippet": { "publishedAt": "2019-03-21T20:37:49.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "What are Actions on Google (Assistant on Air)", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "tags": [ "Google", "developers", "aog", "Actions on Google", "Assistant", "Google Assistant", "actions", "google home", "actions on google", "google assistant developers", "google assistant sdk", "Actions on google developers", "smarthome developers", "common terminology", "custom action on google", "google assistant in your app", "add google assistant", "assistant on air", "how to use google assistant on air", "Actions on Google how to" ], "categoryId": "28", "liveBroadcastContent": "none", "defaultLanguage": "en", "localized": { "title": "What are Actions on Google (Assistant on Air)", "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" }, "defaultAudioLanguage": "en" }, "contentDetails": { "duration": "PT21M7S", "dimension": "2d", "definition": "hd", "caption": "true", "licensedContent": false, "projection": "rectangular" }, "status": { "uploadStatus": "processed", "privacyStatus": "public", "license": "youtube", "embeddable": true, "publicStatsViewable": true }, "statistics": { "viewCount": "8087", "likeCount": "190", "dislikeCount": "23", "favoriteCount": "0", "commentCount": "32" }, "topicDetails": { "topicIds": [ "/m/02jjt" ], "relevantTopicIds": [ "/m/02jjt" ], "topicCategories": [ "https://en.wikipedia.org/wiki/Entertainment" ] } } ================================================ FILE: testdata/modeldata/videos/video_paid_product_placement_details.json ================================================ { "hasPaidProductPlacement": true } ================================================ FILE: testdata/modeldata/videos/video_recording_details.json ================================================ { "recordingDate": "2024-07-03T00:00:00Z" } ================================================ FILE: testdata/modeldata/videos/video_snippet.json ================================================ { "publishedAt": "2019-03-21T20:37:49.000Z", "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", "title": "What are Actions on Google (Assistant on Air)", "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", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg", "width": 480, "height": 360 }, "standard": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg", "width": 640, "height": 480 }, "maxres": { "url": "https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg", "width": 1280, "height": 720 } }, "channelTitle": "Google Developers", "tags": [ "Google", "developers", "aog", "Actions on Google", "Assistant", "Google Assistant", "actions", "google home", "actions on google", "google assistant developers", "google assistant sdk", "Actions on google developers", "smarthome developers", "common terminology", "custom action on google", "google assistant in your app", "add google assistant", "assistant on air", "how to use google assistant on air", "Actions on Google how to" ], "categoryId": "28", "liveBroadcastContent": "none", "defaultLanguage": "en", "localized": { "title": "What are Actions on Google (Assistant on Air)", "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" }, "defaultAudioLanguage": "en" } ================================================ FILE: testdata/modeldata/videos/video_statistics.json ================================================ { "viewCount": 8087, "likeCount": "190", "dislikeCount": "23", "favoriteCount": "0", "commentCount": "32" } ================================================ FILE: testdata/modeldata/videos/video_status.json ================================================ { "uploadStatus": "processed", "privacyStatus": "public", "license": "youtube", "embeddable": true, "publicStatsViewable": true, "publishAt": "2019-03-21T20:37:49.000Z", "madeForKids": false } ================================================ FILE: testdata/modeldata/videos/video_topic_details.json ================================================ { "relevantTopicIds": [ "/m/02jjt" ], "topicCategories": [ "https://en.wikipedia.org/wiki/Entertainment" ] } ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/apis/__init__.py ================================================ ================================================ FILE: tests/apis/test_activities.py ================================================ import json import unittest import responses import pyyoutube class ApiActivitiesTest(unittest.TestCase): BASE_PATH = "testdata/apidata/activities/" BASE_URL = "https://www.googleapis.com/youtube/v3/activities" with open(BASE_PATH + "activities_by_channel_p1.json", "rb") as f: ACTIVITIES_CHANNEL_P1 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "activities_by_channel_p2.json", "rb") as f: ACTIVITIES_CHANNEL_P2 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "activities_by_mine_p1.json", "rb") as f: ACTIVITIES_MINE_P1 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "activities_by_mine_p2.json", "rb") as f: ACTIVITIES_MINE_P2 = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") self.api_with_access_token = pyyoutube.Api(access_token="token") def testGetChannelActivities(self) -> None: # test parts with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_activities_by_channel(channel_id="id", parts="id,not_part") # test get all activities with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.ACTIVITIES_CHANNEL_P1) m.add("GET", self.BASE_URL, json=self.ACTIVITIES_CHANNEL_P2) res = self.api.get_activities_by_channel( channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", parts="id,snippet", before="2019-11-1T00:00:00.000Z", after="2019-10-1T00:00:00.000Z", region_code="US", count=None, ) self.assertEqual(len(res.items), 13) # test get by page token with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.ACTIVITIES_CHANNEL_P2) res = self.api.get_activities_by_channel( channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", parts="id,snippet", count=None, page_token="CAoQAA", return_json=True, ) self.assertEqual(len(res["items"]), 3) def testGetMineActivities(self) -> None: # test parts with self.assertRaises(pyyoutube.PyYouTubeException): self.api_with_access_token.get_activities_by_me(parts="id,not_part") # test get all activities with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.ACTIVITIES_MINE_P1) m.add("GET", self.BASE_URL, json=self.ACTIVITIES_MINE_P2) res = self.api_with_access_token.get_activities_by_me( parts="id,snippet", before="2019-11-1T00:00:00.000Z", after="2019-12-1T00:00:00.000Z", region_code="US", count=None, ) self.assertEqual(len(res.items), 2) # test page token with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.ACTIVITIES_MINE_P2) res = self.api_with_access_token.get_activities_by_me( parts="id,snippet", before="2019-11-1T00:00:00.000Z", after="2019-12-1T00:00:00.000Z", region_code="US", page_token="CAEQAA", count=None, return_json=True, ) self.assertEqual(len(res["items"]), 1) ================================================ FILE: tests/apis/test_auth.py ================================================ import json import unittest import responses from requests import HTTPError import pyyoutube class TestOAuthApi(unittest.TestCase): BASE_PATH = "testdata/apidata/" with open(BASE_PATH + "access_token.json", "rb") as f: ACCESS_TOKEN_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "user_profile.json", "rb") as f: USER_PROFILE_INFO = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(client_id="xx", client_secret="xx") def testInitApi(self) -> None: with self.assertRaises(pyyoutube.PyYouTubeException): pyyoutube.Api() def testOAuth(self) -> None: url, statue = self.api.get_authorization_url() self.assertEqual(statue, "PyYouTube") redirect_response = ( "https://localhost/?state=PyYouTube&code=code" "&scope=profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile#" ) with self.assertRaises(pyyoutube.PyYouTubeException): self.api.refresh_token() with responses.RequestsMock() as m: m.add( "POST", self.api.EXCHANGE_ACCESS_TOKEN_URL, json=self.ACCESS_TOKEN_INFO ) token = self.api.generate_access_token( authorization_response=redirect_response, ) self.assertEqual(token.access_token, "access_token") token_origin = self.api.generate_access_token( authorization_response=redirect_response, return_json=True ) self.assertEqual(token_origin["access_token"], "access_token") refresh_token = self.api.refresh_token() self.assertEqual(refresh_token.access_token, "access_token") refresh_token_origin = self.api.refresh_token(return_json=True) self.assertEqual(refresh_token_origin["refresh_token"], "refresh_token") api = pyyoutube.Api(client_id="xx", client_secret="xx") refresh_token = api.refresh_token(refresh_token="refresh_token") self.assertEqual(refresh_token.refresh_token, "refresh_token") def testGetProfile(self) -> None: with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_profile() self.api._access_token = "access_token" with responses.RequestsMock() as m: m.add("GET", self.api.USER_INFO_URL, json=self.USER_PROFILE_INFO) profile = self.api.get_profile() self.assertEqual(profile.given_name, "kun") profile_origin = self.api.get_profile(return_json=True) self.assertEqual(profile_origin["given_name"], "kun") with responses.RequestsMock() as m: m.add("GET", self.api.USER_INFO_URL, body=HTTPError("Exception")) with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_profile() ================================================ FILE: tests/apis/test_captions.py ================================================ import json import unittest import responses import pyyoutube class ApiCaptionsTest(unittest.TestCase): BASE_PATH = "testdata/apidata/captions/" BASE_URL = "https://www.googleapis.com/youtube/v3/captions" with open(BASE_PATH + "captions_by_video.json", "rb") as f: CAPTIONS_BY_VIDEO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "captions_filter_by_id.json", "rb") as f: CAPTIONS_FILTER_ID = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api_with_access_token = pyyoutube.Api(access_token="token") def testGetCaptionByVideo(self) -> None: video_id = "oHR3wURdJ94" # test parts with self.assertRaises(pyyoutube.PyYouTubeException): self.api_with_access_token.get_captions_by_video( video_id=video_id, parts="id,not_part", ) # test by video with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.CAPTIONS_BY_VIDEO) res = self.api_with_access_token.get_captions_by_video( video_id=video_id, parts="id,snippet", return_json=True, ) self.assertEqual(len(res["items"]), 2) self.assertEqual(res["items"][0]["snippet"]["videoId"], video_id) # test filter id with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.CAPTIONS_FILTER_ID) res = self.api_with_access_token.get_captions_by_video( video_id=video_id, parts=["id", "snippet"], caption_id="SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I", ) self.assertEqual(len(res.items), 1) self.assertEqual(res.items[0].snippet.videoId, video_id) ================================================ FILE: tests/apis/test_categories.py ================================================ import json import unittest import responses import pyyoutube class ApiVideoCategoryTest(unittest.TestCase): BASE_PATH = "testdata/apidata/categories/" BASE_URL = "https://www.googleapis.com/youtube/v3/videoCategories" with open(BASE_PATH + "video_category_single.json", "rb") as f: VIDEO_CATEGORY_SINGLE = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_category_multi.json", "rb") as f: VIDEO_CATEGORY_MULTI = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_category_by_region.json", "rb") as f: VIDEO_CATEGORY_BY_REGION = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") def testGetVideoCategories(self) -> None: # test params with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_video_categories() # test parts with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_video_categories(category_id="id", parts="id,not_part") with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.VIDEO_CATEGORY_SINGLE) m.add("GET", self.BASE_URL, json=self.VIDEO_CATEGORY_MULTI) m.add("GET", self.BASE_URL, json=self.VIDEO_CATEGORY_BY_REGION) res_by_single = self.api.get_video_categories( category_id="17", parts=["snippet"], return_json=True, ) self.assertEqual(res_by_single["kind"], "youtube#videoCategoryListResponse") self.assertEqual(len(res_by_single["items"]), 1) self.assertEqual(res_by_single["items"][0]["id"], "17") res_by_multi = self.api.get_video_categories( category_id=["17", "18"], parts="snippet", ) self.assertEqual(len(res_by_multi.items), 2) self.assertEqual(res_by_multi.items[1].id, "18") res_by_region = self.api.get_video_categories( region_code="US", parts="snippet", ) self.assertEqual(len(res_by_region.items), 32) self.assertEqual(res_by_region.items[0].id, "1") ================================================ FILE: tests/apis/test_channel_sections.py ================================================ import json import unittest import pyyoutube import responses class ApiChannelSectionTest(unittest.TestCase): BASE_PATH = "testdata/apidata/channel_sections/" BASE_URL = "https://www.googleapis.com/youtube/v3/channelSections" with open(BASE_PATH + "channel_sections_by_id.json", "rb") as f: CHANNEL_SECTIONS_BY_ID = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_sections_by_ids.json", "rb") as f: CHANNEL_SECTIONS_BY_IDS = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_sections_by_channel.json", "rb") as f: CHANNEL_SECTIONS_BY_CHANNEL = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") def testGetChannelSectionsById(self) -> None: section_id = "UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY" section_ids = [ "UC_x5XG1OV2P6uZZ5FSM9Ttw.npYvuMz0_es", "UC_x5XG1OV2P6uZZ5FSM9Ttw.9_wU0qhEPR8", ] with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.CHANNEL_SECTIONS_BY_ID) m.add("GET", self.BASE_URL, json=self.CHANNEL_SECTIONS_BY_IDS) section_res = self.api.get_channel_sections_by_id( section_id=section_id, ) self.assertEqual(section_res.kind, "youtube#channelSectionListResponse") self.assertEqual(len(section_res.items), 1) self.assertEqual(section_res.items[0].id, section_id) section_multi_res = self.api.get_channel_sections_by_id( section_id=section_ids, parts=["id", "snippet"], return_json=True ) self.assertEqual(len(section_multi_res["items"]), 2) self.assertIn(section_multi_res["items"][1]["id"], section_ids) def testGetChannelSectionsByChannel(self) -> None: channel_id = "UCa-vrCLQHviTOVnEKDOdetQ" with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.CHANNEL_SECTIONS_BY_CHANNEL) section_by_channel = self.api.get_channel_sections_by_channel( channel_id=channel_id, ) self.assertEqual(len(section_by_channel.items), 3) self.assertEqual( section_by_channel.items[0].id, "UCa-vrCLQHviTOVnEKDOdetQ.jNQXAC9IVRw" ) section_by_me = self.api.get_channel_sections_by_channel( mine=True, return_json=True, ) self.assertEqual( section_by_me["items"][2]["id"], "UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY" ) ================================================ FILE: tests/apis/test_channels.py ================================================ import json import unittest from requests import HTTPError import pyyoutube import responses class ApiChannelTest(unittest.TestCase): BASE_PATH = "testdata/apidata/" BASE_URL = "https://www.googleapis.com/youtube/v3/channels" with open(BASE_PATH + "channel_info_single.json", "rb") as f: CHANNELS_INFO_SINGLE = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_info_multi.json", "rb") as f: CHANNELS_INFO_MULTI = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") def testSendRequest(self) -> None: with self.assertRaises(pyyoutube.PyYouTubeException): api = pyyoutube.Api(client_id="id", client_secret="secret") api._request("channels", post_args={"a": "a"}) with responses.RequestsMock() as m: m.add("POST", self.BASE_URL, json={}) api = pyyoutube.Api(access_token="access token") res = api._request("channels", post_args={"a": "a"}) self.assertTrue(res) with self.assertRaises(pyyoutube.PyYouTubeException): with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, body=HTTPError("Exception")) self.api.get_channel_info(channel_id="channel_id", parts="id,snippet") # TODO need to separate. def testParseResponse(self) -> None: with open("testdata/error_response.json", "rb") as f: error_response = json.loads(f.read().decode("utf-8")) with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=error_response, status=400) with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_channel_info( channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", parts="id,snippet,statistics" ) def testGetChannelInfo(self) -> None: # test params checker with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_channel_info(parts="id,invideoPromotion") with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_channel_info() with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.CHANNELS_INFO_SINGLE) res_by_channel_id = self.api.get_channel_info( channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", parts="id,snippet,statistics" ) self.assertEqual(res_by_channel_id.items[0].id, "UC_x5XG1OV2P6uZZ5FSM9Ttw") res_by_channel_handle = self.api.get_channel_info( for_handle="googledevelopers", return_json=True ) self.assertEqual( res_by_channel_handle["items"][0]["snippet"]["customUrl"], "@googledevelopers", ) res_by_channel_name = self.api.get_channel_info( for_username="GoogleDevelopers", return_json=True ) self.assertEqual( res_by_channel_name["items"][0]["id"], "UC_x5XG1OV2P6uZZ5FSM9Ttw" ) res_by_mine = self.api.get_channel_info(mine=True) self.assertEqual(res_by_mine.items[0].id, "UC_x5XG1OV2P6uZZ5FSM9Ttw") with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.CHANNELS_INFO_MULTI) res_by_channel_id_list = self.api.get_channel_info( channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw,UCK8sQmJBp8GCxrOtXWBpyEA", parts="id,snippet", ) self.assertEqual(len(res_by_channel_id_list.items), 2) self.assertEqual( res_by_channel_id_list.items[1].id, "UCK8sQmJBp8GCxrOtXWBpyEA" ) ================================================ FILE: tests/apis/test_comment_threads.py ================================================ import json import unittest import responses import pyyoutube class ApiCommentThreadTest(unittest.TestCase): BASE_PATH = "testdata/apidata/comment_threads/" BASE_URL = "https://www.googleapis.com/youtube/v3/commentThreads" with open(BASE_PATH + "comment_thread_single.json", "rb") as f: COMMENT_THREAD_INFO_SINGLE = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_threads_multi.json", "rb") as f: COMMENT_THREAD_INFO_MULTI = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_threads_all_to_me.json", "rb") as f: COMMENT_THREAD_ALL_TO_ME = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_threads_by_channel.json", "rb") as f: COMMENT_THREAD_BY_CHANNEL = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_threads_with_search.json", "rb") as f: COMMENT_THREAD_BY_SEARCH = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_threads_by_video_paged_1.json", "rb") as f: COMMENT_THREAD_BY_VIDEO_P_1 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_threads_by_video_paged_2.json", "rb") as f: COMMENT_THREAD_BY_VIDEO_P_2 = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") self.api_with_token = pyyoutube.Api(access_token="access token") def testGetCommentThreadById(self) -> None: # test parts with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_comment_thread_by_id( comment_thread_id="id", parts="id,not_part" ) with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENT_THREAD_INFO_SINGLE) m.add("GET", self.BASE_URL, json=self.COMMENT_THREAD_INFO_MULTI) res_by_single_id = self.api.get_comment_thread_by_id( comment_thread_id="UgxKREWxIgDrw8w2e_Z4AaABAg", parts="id,snippet", text_format="plain_text", return_json=True, ) self.assertEqual( res_by_single_id["kind"], "youtube#commentThreadListResponse" ) self.assertEqual(len(res_by_single_id["items"]), 1) self.assertEqual( res_by_single_id["items"][0]["id"], "UgxKREWxIgDrw8w2e_Z4AaABAg" ) res_by_multi_id = self.api.get_comment_thread_by_id( comment_thread_id=[ "UgxKREWxIgDrw8w2e_Z4AaABAg", "UgyrVQaFfEdvaSzstj14AaABAg", ], parts=["id", "snippet"], ) self.assertEqual(res_by_multi_id.pageInfo.totalResults, 2) self.assertEqual(res_by_multi_id.items[1].id, "UgyrVQaFfEdvaSzstj14AaABAg") def testGetCommentThreads(self) -> None: # test no params with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_comment_threads() # test parts with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_comment_threads(all_to_channel_id="id", parts="id,not_part") # test with all to channel. with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENT_THREAD_ALL_TO_ME) res_by_all = self.api_with_token.get_comment_threads( all_to_channel_id="UCa-vrCLQHviTOVnEKDOdetQ", parts="id,snippet", moderation_status="published", order="time", return_json=True, ) self.assertEqual(res_by_all["kind"], "youtube#commentThreadListResponse") self.assertEqual(res_by_all["pageInfo"]["totalResults"], 4) self.assertEqual(len(res_by_all["items"]), 4) self.assertEqual(res_by_all["items"][0]["id"], "UgyWeTdgc4sc1xgmbld4AaABAg") # test with channel with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENT_THREAD_BY_CHANNEL) res_by_channel = self.api.get_comment_threads( channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", ) self.assertEqual(res_by_channel.pageInfo.totalResults, 2) self.assertEqual( res_by_channel.items[0].snippet.channelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw" ) # test with search with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENT_THREAD_BY_SEARCH) res_by_search = self.api.get_comment_threads( channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", search_terms="Hello", ) self.assertEqual(res_by_search.pageInfo.totalResults, 1) self.assertEqual( res_by_channel.items[0].snippet.channelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw" ) self.assertIn( "Hello", res_by_channel.items[0].snippet.topLevelComment.snippet.textDisplay, ) # test with video with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENT_THREAD_BY_VIDEO_P_1) m.add("GET", self.BASE_URL, json=self.COMMENT_THREAD_BY_VIDEO_P_2) res_by_video = self.api.get_comment_threads( video_id="F1UP7wRCPH8", count=8, limit=5, ) self.assertEqual(len(res_by_video.items), 8) self.assertEqual(res_by_video.items[0].snippet.videoId, "F1UP7wRCPH8") # test get all items with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENT_THREAD_BY_VIDEO_P_1) m.add("GET", self.BASE_URL, json=self.COMMENT_THREAD_BY_VIDEO_P_2) res_by_video = self.api.get_comment_threads( video_id="F1UP7wRCPH8", count=None, ) self.assertEqual(len(res_by_video.items), 10) # test use page token with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENT_THREAD_BY_VIDEO_P_2) res_by_video = self.api.get_comment_threads( video_id="F1UP7wRCPH8", count=None, page_token="QURTSl9pMzdZOUVzMkI0czlmRmNjSVBPcTBTdzVzajUydDVnbE5SNElWS0l5WU12amYweVotdzF5c1hTNmxzUmVIcEZXbmVEVFMzNVJmWk82TVVwUlB2LWh5aUpOQlA5TGQzTWZEcHlTeTd2dlNGRUFZaVF0cmtJd01BTHlnOG0=", ) self.assertEqual(len(res_by_video.items), 5) ================================================ FILE: tests/apis/test_comments.py ================================================ import json import unittest import responses import pyyoutube class ApiCommentTest(unittest.TestCase): BASE_PATH = "testdata/apidata/comments/" BASE_URL = "https://www.googleapis.com/youtube/v3/comments" with open(BASE_PATH + "comments_single.json", "rb") as f: COMMENTS_INFO_SINGLE = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comments_multi.json", "rb") as f: COMMENTS_INFO_MULTI = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comments_by_parent_paged_1.json", "rb") as f: COMMENTS_PAGED_1 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comments_by_parent_paged_2.json", "rb") as f: COMMENTS_PAGED_2 = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") def testGetCommentById(self) -> None: # test parts with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_comment_by_id(comment_id="id", parts="id,not_part") with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENTS_INFO_SINGLE) m.add("GET", self.BASE_URL, json=self.COMMENTS_INFO_MULTI) res_by_single = self.api.get_comment_by_id( comment_id="UgyUBI0HsgL9emxcZpR4AaABAg", parts=["id", "snippet"], return_json=True, ) self.assertEqual(res_by_single["kind"], "youtube#commentListResponse") self.assertEqual(len(res_by_single["items"]), 1) self.assertEqual( res_by_single["items"][0]["id"], "UgyUBI0HsgL9emxcZpR4AaABAg" ) res_by_multi = self.api.get_comment_by_id( comment_id=["UgyUBI0HsgL9emxcZpR4AaABAg", "Ugzi3lkqDPfIOirGFLh4AaABAg"], parts=("id", "snippet"), ) self.assertEqual(len(res_by_multi.items), 2) self.assertEqual(res_by_multi.items[1].id, "Ugzi3lkqDPfIOirGFLh4AaABAg") def testGetCommentsByParentId(self) -> None: # test parts with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_comments(parent_id="id", parts="id,not_part") # test paged with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENTS_PAGED_1) m.add("GET", self.BASE_URL, json=self.COMMENTS_PAGED_2) res_by_parent = self.api.get_comments( parent_id="Ugw5zYU6n9pmIgAZWvN4AaABAg", parts="id,snippet", limit=2, ) self.assertEqual(res_by_parent.kind, "youtube#commentListResponse") self.assertEqual(len(res_by_parent.items), 3) self.assertEqual( res_by_parent.items[0].id, "Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za6voUoRh", ) # test count with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENTS_PAGED_1) res_by_parent = self.api.get_comments( parent_id="Ugw5zYU6n9pmIgAZWvN4AaABAg", parts="id,snippet", count=2, limit=2, return_json=True, ) self.assertEqual(len(res_by_parent["items"]), 2) self.assertEqual( res_by_parent["items"][0]["id"], "Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za6voUoRh", ) # test get all comments with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENTS_PAGED_1) m.add("GET", self.BASE_URL, json=self.COMMENTS_PAGED_2) res_by_parent = self.api.get_comments( parent_id="Ugw5zYU6n9pmIgAZWvN4AaABAg", parts="id,snippet", count=None ) self.assertEqual(len(res_by_parent.items), 3) # test use page token with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.COMMENTS_PAGED_2) res_by_parent = self.api.get_comments( parent_id="Ugw5zYU6n9pmIgAZWvN4AaABAg", parts="id,snippet", count=None, page_token="R0FJeVZnbzBJTl9zNXRxNXlPWUNNaWtRQUJpQ3RNeW4wcFBtQWlBQktBTXdDam9XT1RGNlZETmpXV0kxUWpJNU1YcGhOV1ZLZUhwek1SSWVDQVVTR2xWbmR6VjZXVlUyYmpsd2JVbG5RVnBYZGs0MFFXRkJRa0ZuT2lBSUFSSWNOVHBWWjNjMWVsbFZObTQ1Y0cxSlowRmFWM1pPTkVGaFFVSkJadw==", ) self.assertEqual(len(res_by_parent.items), 1) ================================================ FILE: tests/apis/test_i18ns.py ================================================ import json import unittest import responses import pyyoutube class ApiI18nTest(unittest.TestCase): BASE_PATH = "testdata/apidata/i18ns/" REGION_URL = "https://www.googleapis.com/youtube/v3/i18nRegions" LANGUAGE_URL = "https://www.googleapis.com/youtube/v3/i18nLanguages" with open(BASE_PATH + "regions_res.json", "rb") as f: REGIONS_RES = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "language_res.json", "rb") as f: LANGUAGE_RES = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") def testGetI18nRegions(self) -> None: with responses.RequestsMock() as m: m.add("GET", self.REGION_URL, json=self.REGIONS_RES) regions = self.api.get_i18n_regions(parts=["snippet"]) self.assertEqual(regions.kind, "youtube#i18nRegionListResponse") self.assertEqual(len(regions.items), 4) self.assertEqual(regions.items[0].id, "VE") regions_json = self.api.get_i18n_regions(return_json=True) self.assertEqual(len(regions_json["items"]), 4) def testGetI18nLanguages(self) -> None: with responses.RequestsMock() as m: m.add("GET", self.LANGUAGE_URL, json=self.LANGUAGE_RES) languages = self.api.get_i18n_languages(parts=["snippet"]) self.assertEqual(len(languages.items), 5) self.assertEqual(languages.items[0].id, "zh-CN") languages_json = self.api.get_i18n_languages(return_json=True) self.assertEqual(len(languages_json["items"]), 5) ================================================ FILE: tests/apis/test_members.py ================================================ import json import unittest import responses import pyyoutube class ApiMembersTest(unittest.TestCase): BASE_PATH = "testdata/apidata/members/" MEMBERS_URL = "https://www.googleapis.com/youtube/v3/members" MEMBERSHIP_LEVEL_URL = "https://www.googleapis.com/youtube/v3/membershipsLevels" with open(BASE_PATH + "members_data.json", "rb") as f: MEMBERS_RES = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "membership_levels.json", "rb") as f: MEMBERSHIP_LEVEL_RES = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(access_token="Authorize token") def testGetMembers(self) -> None: with responses.RequestsMock() as m: m.add("GET", self.MEMBERS_URL, json=self.MEMBERS_RES) members = self.api.get_members(parts=["snippet"]) self.assertEqual(members.kind, "youtube#memberListResponse") self.assertEqual(len(members.items), 2) members_json = self.api.get_members( page_token="token", count=None, has_access_to_level="high", filter_by_member_channel_id="id", return_json=True, ) self.assertEqual(len(members_json["items"]), 2) def testGetMembershipLevels(self) -> None: with responses.RequestsMock() as m: m.add("GET", self.MEMBERSHIP_LEVEL_URL, json=self.MEMBERSHIP_LEVEL_RES) membership_levels = self.api.get_membership_levels(parts=["id", "snippet"]) self.assertEqual( membership_levels.kind, "youtube#membershipsLevelListResponse" ) self.assertEqual(len(membership_levels.items), 2) membership_levels_json = self.api.get_membership_levels(return_json=True) self.assertEqual(len(membership_levels_json["items"]), 2) ================================================ FILE: tests/apis/test_playlist_items.py ================================================ import json import unittest import responses import pyyoutube class ApiPlaylistItemTest(unittest.TestCase): BASE_PATH = "testdata/apidata/playlist_items/" BASE_URL = "https://www.googleapis.com/youtube/v3/playlistItems" with open(BASE_PATH + "playlist_items_single.json", "rb") as f: PLAYLIST_ITEM_INFO_SINGLE = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_items_multi.json", "rb") as f: PLAYLIST_ITEM_INFO_MULTI = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_items_paged_1.json", "rb") as f: PLAYLIST_ITEM_PAGED_1 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_items_paged_2.json", "rb") as f: PLAYLIST_ITEM_PAGED_2 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_items_filter_video.json", "rb") as f: PLAYLIST_ITEM_FILTER = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") def testGetPlaylistItemById(self) -> None: with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.PLAYLIST_ITEM_INFO_SINGLE) m.add("GET", self.BASE_URL, json=self.PLAYLIST_ITEM_INFO_MULTI) res_by_single_id = self.api.get_playlist_item_by_id( playlist_item_id="UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2", parts="id,snippet", return_json=True, ) self.assertEqual( res_by_single_id["kind"], "youtube#playlistItemListResponse" ) self.assertEqual( res_by_single_id["items"][0]["id"], "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2", ) res_by_multi_id = self.api.get_playlist_item_by_id( playlist_item_id=[ "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2", "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4yODlGNEE0NkRGMEEzMEQy", ], parts=["id", "snippet"], ) self.assertEqual(res_by_multi_id.pageInfo.totalResults, 2) self.assertEqual(len(res_by_multi_id.items), 2) self.assertEqual( res_by_multi_id.items[1].id, "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4yODlGNEE0NkRGMEEzMEQy", ) def testGetPlaylistItems(self) -> None: with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_playlist_items(playlist_id="id", parts="id,not_part") # test paged with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_1) m.add("GET", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_2) res_by_playlist = self.api.get_playlist_items( playlist_id="PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", parts="id,snippet", limit=10, count=20, ) self.assertEqual(res_by_playlist.kind, "youtube#playlistItemListResponse") self.assertEqual(res_by_playlist.pageInfo.totalResults, 13) self.assertEqual(len(res_by_playlist.items), 13) self.assertEqual( res_by_playlist.items[0].id, "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2", ) # test count with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_1) res_by_playlist = self.api.get_playlist_items( playlist_id="PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", parts="id,snippet", limit=10, count=5, ) self.assertEqual(res_by_playlist.kind, "youtube#playlistItemListResponse") self.assertEqual(res_by_playlist.pageInfo.totalResults, 13) self.assertEqual(len(res_by_playlist.items), 5) self.assertEqual( res_by_playlist.items[0].id, "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2", ) # test get all items with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_1) m.add("GET", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_2) res_by_playlist = self.api.get_playlist_items( playlist_id="PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", parts="id,snippet", count=None, ) self.assertEqual(res_by_playlist.pageInfo.totalResults, 13) self.assertEqual(len(res_by_playlist.items), 13) # test filter with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.PLAYLIST_ITEM_FILTER) res_by_filter = self.api.get_playlist_items( playlist_id="PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", parts=("id", "snippet"), video_id="VCv-KKIkLns", return_json=True, ) self.assertEqual(res_by_filter["pageInfo"]["totalResults"], 1) self.assertEqual( res_by_filter["items"][0]["snippet"]["resourceId"]["videoId"], "VCv-KKIkLns", ) # test use page token with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_2) res_by_playlist = self.api.get_playlist_items( playlist_id="PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", parts="id,snippet", page_token="CAoQAA", count=3, ) self.assertEqual(len(res_by_playlist.items), 3) ================================================ FILE: tests/apis/test_playlists.py ================================================ import json import unittest import responses import pyyoutube class ApiPlaylistTest(unittest.TestCase): BASE_PATH = "testdata/apidata/playlists/" BASE_URL = "https://www.googleapis.com/youtube/v3/playlists" with open(BASE_PATH + "playlists_single.json", "rb") as f: PLAYLISTS_INFO_SINGLE = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlists_multi.json", "rb") as f: PLAYLISTS_INFO_MULTI = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlists_paged_1.json", "rb") as f: PLAYLISTS_PAGED_1 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlists_paged_2.json", "rb") as f: PLAYLISTS_PAGED_2 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlists_mine.json", "rb") as f: PLAYLISTS_MINE = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") self.api_with_access_token = pyyoutube.Api(access_token="access token") def testGetPlaylistById(self) -> None: with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.PLAYLISTS_INFO_SINGLE) m.add("GET", self.BASE_URL, json=self.PLAYLISTS_INFO_MULTI) res_by_playlist_id = self.api.get_playlist_by_id( playlist_id="PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", parts="id,snippet", return_json=True, ) self.assertEqual(res_by_playlist_id["kind"], "youtube#playlistListResponse") self.assertEqual( res_by_playlist_id["items"][0]["id"], "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", ) res_by_playlist_multi_id = self.api.get_playlist_by_id( playlist_id=[ "PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i", "PLOU2XLYxmsIJJVnHWmd1qfr0Caq4VZCu4", ], parts=["id", "snippet"], ) self.assertEqual(len(res_by_playlist_multi_id.items), 2) self.assertEqual( res_by_playlist_multi_id.items[1].id, "PLOU2XLYxmsIJJVnHWmd1qfr0Caq4VZCu4", ) def testGetPlaylists(self) -> None: # test params checker with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_playlists(parts="id,not_part") with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_playlists() with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.PLAYLISTS_PAGED_1) m.add("GET", self.BASE_URL, json=self.PLAYLISTS_PAGED_2) m.add("GET", self.BASE_URL, json=self.PLAYLISTS_MINE) res_by_channel_id = self.api.get_playlists( channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", limit=10, count=13, ) self.assertEqual(res_by_channel_id.pageInfo.totalResults, 422) self.assertEqual(len(res_by_channel_id.items), 13) self.assertEqual( res_by_channel_id.items[0].snippet.channelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw" ) res_by_mine = self.api_with_access_token.get_playlists( mine=True, limit=10, count=10, return_json=True ) self.assertEqual(len(res_by_mine["items"]), 2) self.assertEqual( res_by_mine["items"][0]["id"], "PLOU2XLYxmsIIOSO0eWuj-6yQmdakarUzN" ) # test for all items with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.PLAYLISTS_PAGED_1) m.add("GET", self.BASE_URL, json=self.PLAYLISTS_PAGED_2) res_by_channel_id = self.api.get_playlists( channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", count=None, ) self.assertEqual(len(res_by_channel_id.items), 20) # test for page token with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.PLAYLISTS_PAGED_2) res_by_channel_id = self.api.get_playlists( channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", count=None, page_token="CAoQAA" ) self.assertEqual(len(res_by_channel_id.items), 10) ================================================ FILE: tests/apis/test_search.py ================================================ import json import unittest import responses import pyyoutube class ApiSearchTest(unittest.TestCase): BASE_PATH = "testdata/apidata/search/" BASE_URL = "https://www.googleapis.com/youtube/v3/search" def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") def testSearch(self) -> None: with open(self.BASE_PATH + "search_videos_by_channel.json", "rb") as f: search_videos_by_channel = json.loads(f.read().decode("utf-8")) with open(self.BASE_PATH + "search_by_location.json", "rb") as f: search_by_location = json.loads(f.read().decode("utf-8")) with open(self.BASE_PATH + "search_by_event.json", "rb") as f: search_by_event = json.loads(f.read().decode("utf-8")) with open(self.BASE_PATH + "search_channels.json", "rb") as f: search_channels = json.loads(f.read().decode("utf-8")) # test search videos with channel with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=search_videos_by_channel) res = self.api.search( parts="snippet", channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", q="news", count=5, ) self.assertEqual(res.items[0].id.videoId, "LrQWzOkC0XQ") self.assertEqual(res.items[0].snippet.channelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw") # test search locations with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=search_by_location) res = self.api.search( location="21.5922529, -158.1147114", location_radius="10mi", q="surfing", parts=["snippet"], count=5, published_after="2020-02-01T00:00:00Z", published_before="2020-03-01T00:00:00Z", safe_search="moderate", search_type="video", ) self.assertEqual(res.pageInfo.resultsPerPage, 5) self.assertEqual(len(res.items), 5) self.assertEqual(res.items[0].snippet.channelId, "UCo_q6aOlvPH7M-j_XGWVgXg") # test search event with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=search_by_event) res = self.api.search( event_type="live", q="news", count=25, limit=25, parts=["snippet"], search_type="video", topic_id="/m/09s1f", order="viewCount", ) self.assertEqual(res.pageInfo.resultsPerPage, 25) self.assertEqual(len(res.items), 25) self.assertEqual(res.items[0].snippet.channelId, "UCDGiCfCZIV5phsoGiPwIcyQ") # test search channel with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=search_channels) res_channels = self.api.search( parts=["snippet"], channel_type="any", count=5, search_type="channel", ) self.assertEqual(res_channels.pageInfo.resultsPerPage, 5) self.assertEqual( res_channels.items[0].snippet.channelId, "UCxRULEz6kS0PMxCzOY25GhQ" ) def testSearchByKeywords(self) -> None: with open(self.BASE_PATH + "search_by_keywords_p1.json", "rb") as f: res_p1 = json.loads(f.read().decode("utf-8")) with open(self.BASE_PATH + "search_by_keywords_p2.json", "rb") as f: res_p2 = json.loads(f.read().decode("utf-8")) # test parts with self.assertRaises(pyyoutube.PyYouTubeException): self.api.search_by_keywords(q="x", parts="id,not_part") # test response with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=res_p1) m.add("GET", self.BASE_URL, json=res_p2) res_json = self.api.search_by_keywords( q="surfing", count=30, limit=25, return_json=True ) self.assertEqual(res_json["kind"], "youtube#searchListResponse") self.assertEqual(res_json["regionCode"], "JP") self.assertEqual(res_json["pageInfo"]["totalResults"], 1000000) self.assertEqual(len(res_json["items"]), 30) res = self.api.search_by_keywords( q="surfing", parts=["snippet"], count=25, ) self.assertEqual(res.pageInfo.resultsPerPage, 25) self.assertEqual(res.items[0].id.videoId, "-2IlD-x8wvY") self.assertEqual(res.items[0].snippet.channelId, "UCeYue9Nbodzg3T1Nt88E3fg") def testSearchByRelatedToVideoId(self) -> None: with open(self.BASE_PATH + "search_by_related_video.json", "rb") as f: search_by_related_video = json.loads(f.read().decode("utf-8")) with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=search_by_related_video) res = self.api.search_by_related_video( related_to_video_id="Ks-_Mh1QhMc", region_code="US", relevance_language="en", safe_search="moderate", count=5, ) self.assertEqual(res.pageInfo.resultsPerPage, 5) self.assertEqual(len(res.items), 5) self.assertEqual(res.regionCode, "US") self.assertEqual(res.items[0].snippet.channelId, "UCAuUUnT6oDeKwE6v1NGQxug") def testSearchByDeveloper(self) -> None: with open(self.BASE_PATH + "search_by_developer.json", "rb") as f: search_by_developer = json.loads(f.read().decode("utf-8")) with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=search_by_developer) res_dev = self.api.search_by_developer( parts=["snippet"], q="news", count=5, page_token="CAUQAA", video_category_id="17", video_caption="any", video_definition="any", video_dimension="any", video_duration="any", video_embeddable="any", video_license="any", video_paid_product_placement="any", video_syndicated="any", video_type="any", ) self.assertEqual(res_dev.pageInfo.resultsPerPage, 5) self.assertEqual( res_dev.items[0].snippet.channelId, "UCeY0bbntWzzVIaj2z3QigXg" ) def testSearchByMine(self) -> None: with open(self.BASE_PATH + "search_by_mine.json", "rb") as f: search_by_mine = json.loads(f.read().decode("utf-8")) with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=search_by_mine) res_mine = self.api.search_by_mine( parts=["snippet"], ) self.assertEqual(res_mine.pageInfo.totalResults, 2) self.assertEqual( res_mine.items[0].snippet.channelId, "UCa-vrCLQHviTOVnEKDOdetQ" ) ================================================ FILE: tests/apis/test_subscriptions.py ================================================ import json import unittest import responses import pyyoutube class ApiPlaylistTest(unittest.TestCase): BASE_PATH = "testdata/apidata/subscriptions/" BASE_URL = "https://www.googleapis.com/youtube/v3/subscriptions" with open(BASE_PATH + "subscription_zero.json", "rb") as f: SUBSCRIPTIONS_ZERO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "subscriptions_by_id.json", "rb") as f: SUBSCRIPTIONS_BY_ID = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "subscriptions_by_channel_p1.json", "rb") as f: SUBSCRIPTIONS_BY_CHANNEL_P1 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "subscriptions_by_channel_p2.json", "rb") as f: SUBSCRIPTIONS_BY_CHANNEL_P2 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "subscriptions_by_channel_with_filter.json", "rb") as f: SUBSCRIPTIONS_BY_CHANNEL_FILTER = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "subscriptions_by_mine_p1.json", "rb") as f: SUBSCRIPTIONS_BY_MINE_P1 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "subscriptions_by_mine_p2.json", "rb") as f: SUBSCRIPTIONS_BY_MINE_P2 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "subscriptions_by_mine_filter.json", "rb") as f: SUBSCRIPTIONS_BY_MINE_FILTER = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") self.api_with_access_token = pyyoutube.Api(access_token="access token") def testGetSubscriptionById(self) -> None: # test params checker with self.assertRaises(pyyoutube.PyYouTubeException): self.api_with_access_token.get_subscription_by_id( subscription_id="id", parts="id,not_part" ) with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_ZERO) m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_ID) res_zero = self.api.get_subscription_by_id( subscription_id=( "zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo," "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo" ), parts="id,snippet", return_json=True, ) self.assertEqual(len(res_zero["items"]), 0) self.assertEqual(res_zero["pageInfo"]["totalResults"], 0) res_by_id = self.api_with_access_token.get_subscription_by_id( subscription_id=[ "zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo", "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo", ], parts="id,snippet", ) self.assertEqual(len(res_by_id.items), 2) self.assertEqual(res_by_id.pageInfo.totalResults, 2) self.assertEqual( res_by_id.items[0].id, "zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo" ) def testGetSubscriptionByChannel(self) -> None: # test params checker with self.assertRaises(pyyoutube.PyYouTubeException): self.api_with_access_token.get_subscription_by_channel( channel_id="id", parts="id,not_part" ) # test count is None with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_CHANNEL_P1) m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_CHANNEL_P2) res = self.api.get_subscription_by_channel( channel_id="UCAuUUnT6oDeKwE6v1NGQxug", count=None, limit=5, ) self.assertEqual(len(res.items), 7) # test count with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_CHANNEL_P1) res = self.api.get_subscription_by_channel( channel_id="UCAuUUnT6oDeKwE6v1NGQxug", count=5, limit=5, ) self.assertEqual(len(res.items), 5) # test filter with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_CHANNEL_P1) res = self.api.get_subscription_by_channel( channel_id="UCAuUUnT6oDeKwE6v1NGQxug", for_channel_id=["UCsT0YIqwnpJCM-mx7-gSA4Q", "UCtC8aQzdEHAmuw8YvtH1CcQ"], count=2, return_json=True, ) self.assertEqual(len(res["items"]), 2) # test use page token with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_CHANNEL_P2) res = self.api.get_subscription_by_channel( channel_id="UCAuUUnT6oDeKwE6v1NGQxug", count=None, limit=5, page_token="CAUQAA", ) self.assertEqual(len(res.items), 2) def testGetSubscriptionByMe(self) -> None: # test not have required parameters with self.assertRaises(pyyoutube.PyYouTubeException): self.api_with_access_token.get_subscription_by_me() # test get all data with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_MINE_P1) m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_MINE_P2) sub = self.api_with_access_token.get_subscription_by_me( mine=True, parts=["id", "snippet"], order="alphabetically", count=None, limit=10, ) self.assertEqual(len(sub.items), 15) self.assertEqual(sub.pageInfo.totalResults, 16) # totalResults is only an approximation/estimate. # Refer: https://stackoverflow.com/questions/43507281/totalresults-count-doesnt-match-with-the-actual-results-returned-in-youtube-v3 # test count with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_MINE_P1) sub = self.api_with_access_token.get_subscription_by_me( mine=True, parts="id,snippet", order="alphabetically", count=5, limit=10, return_json=True, ) self.assertEqual(len(sub["items"]), 5) self.assertEqual(sub["pageInfo"]["totalResults"], 16) # test filter channel id with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_MINE_FILTER) sub = self.api_with_access_token.get_subscription_by_me( mine=True, parts="id,snippet", for_channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw,UCa-vrCLQHviTOVnEKDOdetQ", count=None, ) self.assertEqual(len(sub.items), 2) self.assertEqual(sub.pageInfo.totalResults, 2) # test remain with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_ZERO) recent = self.api_with_access_token.get_subscription_by_me( recent_subscriber=True ) self.assertEqual(len(recent.items), 0) subscriber = self.api_with_access_token.get_subscription_by_me( subscriber=True ) self.assertEqual(len(subscriber.items), 0) # test get all data with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_MINE_P2) sub = self.api_with_access_token.get_subscription_by_me( mine=True, parts=["id", "snippet"], order="alphabetically", count=None, limit=10, page_token="CAoQAA", ) self.assertEqual(len(sub.items), 6) ================================================ FILE: tests/apis/test_video_abuse_reason.py ================================================ import json import unittest import responses import pyyoutube class ApiVideoAbuseReason(unittest.TestCase): BASE_PATH = "testdata/apidata/abuse_reasons/" BASE_URL = "https://www.googleapis.com/youtube/v3/videoAbuseReportReasons" with open(BASE_PATH + "abuse_reason.json", "rb") as f: ABUSE_REASON_RES = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api_with_token = pyyoutube.Api(access_token="access token") def testGetVideoAbuseReportReason(self) -> None: with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.ABUSE_REASON_RES) abuse_res = self.api_with_token.get_video_abuse_report_reason( parts=["id", "snippet"], ) self.assertEqual( abuse_res.kind, "youtube#videoAbuseReportReasonListResponse" ) self.assertEqual(len(abuse_res.items), 3) abuse_res_json = self.api_with_token.get_video_abuse_report_reason( return_json=True ) self.assertEqual(len(abuse_res_json["items"]), 3) ================================================ FILE: tests/apis/test_videos.py ================================================ import json import unittest import responses import pyyoutube class ApiVideoTest(unittest.TestCase): BASE_PATH = "testdata/apidata/videos/" BASE_URL = "https://www.googleapis.com/youtube/v3/videos" with open(BASE_PATH + "videos_info_single.json", "rb") as f: VIDEOS_INFO_SINGLE = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "videos_info_multi.json", "rb") as f: VIDEOS_INFO_MULTI = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "videos_chart_paged_1.json", "rb") as f: VIDEOS_CHART_PAGED_1 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "videos_chart_paged_2.json", "rb") as f: VIDEOS_CHART_PAGED_2 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "videos_myrating_paged_1.json", "rb") as f: VIDEOS_MYRATING_PAGED_1 = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "videos_myrating_paged_2.json", "rb") as f: VIDEOS_MYRATING_PAGED_2 = json.loads(f.read().decode("utf-8")) def setUp(self) -> None: self.api = pyyoutube.Api(api_key="api key") self.api_with_token = pyyoutube.Api(access_token="token") def testGetVideoById(self) -> None: with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_video_by_id(video_id="id", parts="id,not_part") with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.VIDEOS_INFO_SINGLE) m.add("GET", self.BASE_URL, json=self.VIDEOS_INFO_MULTI) res_by_single_id = self.api.get_video_by_id( video_id="D-lhorsDlUQ", parts="id,snippet,player", max_height=480, max_width=270, return_json=True, ) self.assertEqual(res_by_single_id["kind"], "youtube#videoListResponse") self.assertEqual(res_by_single_id["pageInfo"]["totalResults"], 1) video = res_by_single_id["items"][0] self.assertEqual(video["id"], "D-lhorsDlUQ") self.assertEqual( video["player"]["embedHtml"], ( '\u003ciframe width="480" height="270" src="//www.youtube.com/embed/D-lhorsDlUQ" ' 'frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; ' 'picture-in-picture" allowfullscreen\u003e\u003c/iframe\u003e' ), ) res_by_multi_id = self.api.get_video_by_id( video_id=["D-lhorsDlUQ", "ovdbrdCIP7U"] ) self.assertEqual(res_by_multi_id.pageInfo.totalResults, 2) self.assertEqual(len(res_by_multi_id.items), 2) self.assertEqual(res_by_multi_id.items[0].id, "D-lhorsDlUQ") def testGetVideoByChart(self) -> None: with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_videos_by_chart(chart="mostPopular", parts="id,not_part") # test paged with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_1) m.add("GET", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_2) res_by_chart = self.api.get_videos_by_chart( chart="mostPopular", region_code="US", category_id="0", max_height=480, max_width=270, count=20, limit=5, return_json=True, ) self.assertEqual(res_by_chart["kind"], "youtube#videoListResponse") self.assertEqual(res_by_chart["pageInfo"]["totalResults"], 8) self.assertEqual(len(res_by_chart["items"]), 8) self.assertEqual(res_by_chart["items"][0]["id"], "hDeuSfo_Ys0") # test count with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_1) res_by_chart = self.api.get_videos_by_chart(chart="mostPopular", count=3) self.assertEqual(res_by_chart.pageInfo.totalResults, 8) self.assertEqual(len(res_by_chart.items), 3) self.assertEqual(res_by_chart.items[0].id, "hDeuSfo_Ys0") # test get all items with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_1) m.add("GET", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_2) res_by_chart = self.api.get_videos_by_chart(chart="mostPopular", count=None) self.assertEqual(res_by_chart.pageInfo.totalResults, 8) # test use page token with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_2) res_by_chart = self.api.get_videos_by_chart( chart="mostPopular", count=None, page_token="CAUQAA" ) self.assertEqual(len(res_by_chart.items), 3) def testGetVideoByMyRating(self) -> None: with self.assertRaises(pyyoutube.PyYouTubeException): self.api_with_token.get_videos_by_myrating( rating="like", parts="id,not_part" ) # test need authorization with self.assertRaises(pyyoutube.PyYouTubeException): self.api.get_videos_by_myrating(rating="like", parts="id,not_part") # test paged with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_1) m.add("GET", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_2) res_by_my_rating = self.api_with_token.get_videos_by_myrating( rating="like", parts=("id", "snippet", "player"), max_height=480, max_width=270, count=10, limit=2, return_json=True, ) self.assertEqual(res_by_my_rating["kind"], "youtube#videoListResponse") self.assertEqual(res_by_my_rating["pageInfo"]["totalResults"], 3) self.assertEqual(len(res_by_my_rating["items"]), 3) self.assertEqual(res_by_my_rating["items"][0]["id"], "P4IfFLAX9hY") # test count with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_1) res_by_my_rating = self.api_with_token.get_videos_by_myrating( rating="like", parts=("id", "snippet", "player"), count=1, limit=2, ) self.assertEqual(res_by_my_rating.pageInfo.totalResults, 3) self.assertEqual(len(res_by_my_rating.items), 1) self.assertEqual(res_by_my_rating.items[0].id, "P4IfFLAX9hY") # test get all items with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_1) m.add("GET", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_2) res_by_my_rating = self.api_with_token.get_videos_by_myrating( rating="like", parts=("id", "snippet", "player"), count=None, ) self.assertEqual(res_by_my_rating.pageInfo.totalResults, 3) # test use page token with responses.RequestsMock() as m: m.add("GET", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_2) res_by_my_rating = self.api_with_token.get_videos_by_myrating( rating="like", parts=("id", "snippet", "player"), count=None, page_token="CAIQAA", ) self.assertEqual(len(res_by_my_rating.items), 1) ================================================ FILE: tests/clients/__init__.py ================================================ ================================================ FILE: tests/clients/base.py ================================================ """ Base class """ class BaseTestCase: BASE_PATH = "testdata/apidata" BASE_URL = "https://www.googleapis.com/youtube/v3" RESOURCE = "CHANNELS" @property def url(self): return f"{self.BASE_URL}/{self.RESOURCE}" def load_json(self, filename, helpers): return helpers.load_json(f"{self.BASE_PATH}/{filename}") ================================================ FILE: tests/clients/test_activities.py ================================================ import pytest import responses from .base import BaseTestCase from pyyoutube.error import PyYouTubeException class TestActivitiesResource(BaseTestCase): RESOURCE = "activities" def test_list(self, helpers, authed_cli): with pytest.raises(PyYouTubeException): authed_cli.activities.list() with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json( "activities/activities_by_channel_p1.json", helpers ), ) res = authed_cli.activities.list( parts=["id", "snippet"], channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", max_results=10, ) assert len(res.items) == 10 assert authed_cli.activities.access_token == "access token" res = authed_cli.activities.list( parts=["id", "snippet"], mine=True, max_results=10 ) assert res.items[0].snippet.type == "upload" ================================================ FILE: tests/clients/test_captions.py ================================================ """ Tests for captions resources. """ import io import pytest import responses import pyyoutube.models as mds from .base import BaseTestCase from pyyoutube.error import PyYouTubeException from pyyoutube.media import Media class TestCaptionsResource(BaseTestCase): RESOURCE = "captions" def test_list(self, helpers, key_cli): with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("captions/captions_by_video.json", helpers), ) res = key_cli.captions.list(parts=["snippet"], video_id="oHR3wURdJ94") assert res.items[0].id == "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I" def test_insert(self, helpers, authed_cli): video_id = "zxTVeyG1600" body = mds.Caption( snippet=mds.CaptionSnippet( name="日文字幕", language="ja", videoId=video_id, isDraft=True ) ) media = Media(io.StringIO(""" 1 00:00:00,036 --> 00:00:00,703 ジメジメした天気 """)) upload = authed_cli.captions.insert( body=body, media=media, ) assert upload.resumable_progress == 0 def test_update(self, helpers, authed_cli): caption_id = "AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA" new_body = mds.Caption( id=caption_id, snippet=mds.CaptionSnippet(videoId="zxTVeyG1600", isDraft=False), ) media = Media( io.StringIO(""" 1 00:00:00,036 --> 00:00:00,703 ジメジメした天気 """), ) upload = authed_cli.captions.update( body=new_body, media=media, ) assert upload.resumable_progress == 0 with responses.RequestsMock() as m: m.add( method="PUT", url=self.url, json=self.load_json("captions/update_response.json", helpers), ) caption = authed_cli.captions.update(body=new_body) assert not caption.snippet.isDraft def test_download(self, authed_cli): caption_id = "AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA" with responses.RequestsMock() as m: m.add( method="GET", url=f"{self.url}/{caption_id}", ) res = authed_cli.captions.download(caption_id=caption_id) assert res.status_code == 200 def test_delete(self, helpers, authed_cli): caption_id = "AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA" with responses.RequestsMock() as m: m.add(method="DELETE", url=self.url) assert authed_cli.captions.delete(caption_id=caption_id) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="DELETE", url=self.url, status=403, json=self.load_json("error_permission_resp.json", helpers), ) authed_cli.captions.delete(caption_id=caption_id) ================================================ FILE: tests/clients/test_channel_banners.py ================================================ """ Tests for channel banners """ import io from .base import BaseTestCase from pyyoutube.media import Media class TestChannelBanners(BaseTestCase): def test_insert(self, helpers, authed_cli): media = Media(fd=io.StringIO("jpg content"), mimetype="image/jpeg") upload = authed_cli.channelBanners.insert(media=media) assert upload.resumable_progress == 0 ================================================ FILE: tests/clients/test_channel_sections.py ================================================ import pytest import responses import pyyoutube.models as mds from .base import BaseTestCase from pyyoutube.error import PyYouTubeException class TestChannelBannersResource(BaseTestCase): RESOURCE = "channelSections" def test_list(self, helpers, authed_cli, key_cli): with pytest.raises(PyYouTubeException): key_cli.channelSections.list() with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json( "channel_sections/channel_sections_by_channel.json", helpers ), ) res = key_cli.channelSections.list( parts=["id", "snippet"], channel_id="UCa-vrCLQHviTOVnEKDOdetQ", ) assert res.items[0].snippet.type == "recentUploads" res = authed_cli.channelSections.list( mine=True, parts=["id", "snippet"], ) assert res.items[0].snippet.channelId == "UCa-vrCLQHviTOVnEKDOdetQ" with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json( "channel_sections/channel_sections_by_id.json", helpers ), ) res = key_cli.channelSections.list( parts=["id", "snippet"], section_id="UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY", ) assert res.items[0].snippet.type == "multiplePlaylists" def test_insert(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="POST", url=self.url, json=self.load_json("channel_sections/insert_resp.json", helpers), ) section = authed_cli.channelSections.insert( parts="id,snippet,contentDetails", body=mds.ChannelSection( snippet=mds.ChannelSectionSnippet( type="multiplePlaylists", position=4, ), contentDetails=mds.ChannelSectionContentDetails( playlists=["PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g"] ), ), ) assert section.id == "UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM" def test_update(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="PUT", url=self.url, json=self.load_json("channel_sections/insert_resp.json", helpers), ) section = authed_cli.channelSections.update( parts="id,snippet,contentDetails", body=mds.ChannelSection( id="UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM", snippet=mds.ChannelSectionSnippet( type="multiplePlaylists", position=4, ), ), ) assert section.id == "UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM" def test_delete(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="DELETE", url=self.url, ) assert authed_cli.channelSections.delete( section_id="UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM" ) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="DELETE", url=self.url, json=self.load_json("error_permission_resp.json", helpers), status=403, ) authed_cli.channelSections.delete( section_id="UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM" ) ================================================ FILE: tests/clients/test_channels.py ================================================ import pytest import responses from .base import BaseTestCase from pyyoutube.error import PyYouTubeException import pyyoutube.models as mds class TestChannelsResource(BaseTestCase): RESOURCE = "channels" channel_id = "UC_x5XG1OV2P6uZZ5FSM9Ttw" def test_list(self, helpers, authed_cli, key_cli): with pytest.raises(PyYouTubeException): key_cli.channels.list() with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("channels/info.json", helpers), ) res = key_cli.channels.list( parts="id,snippet", channel_id=self.channel_id, ) assert res.items[0].id == self.channel_id assert key_cli.channels.api_key == "api key" res = key_cli.channels.list( parts="id,snippet", for_handle="@googledevelopers", ) assert res.items[0].snippet.customUrl == "@googledevelopers" res = key_cli.channels.list( parts=["id", "snippet"], for_username="googledevelopers" ) assert res.items[0].snippet.title == "Google Developers" res = authed_cli.channels.list( parts=("id", "snippet"), managed_by_me=True, ) assert res.items[0].snippet.title == "Google Developers" res = authed_cli.channels.list( parts={"id", "snippet"}, mine=True, ) assert res.items[0].snippet.title == "Google Developers" with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("channels/info_multiple.json", helpers), ) res = authed_cli.channels.list( parts="id,snippet,statistics,contentDetails,brandingSettings", channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw,UCK8sQmJBp8GCxrOtXWBpyEA", ) assert len(res.items) == 2 def test_update(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="PUT", url=self.url, json=self.load_json("channels/update_resp.json", helpers), ) updated_channel = authed_cli.channels.update( part="brandingSettings", body=mds.Channel( brandingSettings=mds.ChannelBrandingSetting( channel=mds.ChannelBrandingSettingChannel( title="ikaros data", description="This is a test channel.", keywords="life 学习 测试", country="CN", defaultLanguage="en", ) ) ), ) assert updated_channel.brandingSettings.channel.defaultLanguage == "en" ================================================ FILE: tests/clients/test_client.py ================================================ """ Tests for client. """ import pytest import responses from requests import Response, HTTPError from .base import BaseTestCase from pyyoutube import Client, PyYouTubeException class TestClient(BaseTestCase): BASE_PATH = "testdata" RESOURCE = "channels" def test_initial(self): with pytest.raises(PyYouTubeException): Client() cli = Client(api_key="key", headers={"HA": "P"}) assert cli.session.headers["HA"] == "P" def test_client_secret_web(self): filename = "apidata/client_secrets/client_secret_web.json" client_secret_path = f"{self.BASE_PATH}/{filename}" cli = Client(client_secret_path=client_secret_path) assert cli.client_id == "client_id" assert cli.client_secret == "client_secret" assert cli.DEFAULT_REDIRECT_URI == "http://localhost:5000/oauth2callback" def test_client_secret_installed(self): filename_good = "apidata/client_secrets/client_secret_installed_good.json" client_secret_good_path = f"{self.BASE_PATH}/{filename_good}" cli = Client(client_secret_path=client_secret_good_path) assert cli.client_id == "client_id" assert cli.client_secret == "client_secret" def test_client_secret_bad(self): filename_bad = "apidata/client_secrets/client_secret_installed_bad.json" filename_unsupported = "apidata/client_secrets/client_secret_unsupported.json" client_secret_bad_path = f"{self.BASE_PATH}/{filename_bad}" client_secret_unsupported_path = f"{self.BASE_PATH}/{filename_unsupported}" with pytest.raises(PyYouTubeException): Client(client_secret_path=client_secret_bad_path) with pytest.raises(PyYouTubeException): Client(client_secret_path=client_secret_unsupported_path) def test_request(self, key_cli): with pytest.raises(PyYouTubeException): cli = Client(client_id="id", client_secret="secret") cli.request(path="path", enforce_auth=True) with responses.RequestsMock() as m: m.add(method="GET", url="https://example.com", body="") key_cli.request(path="https://example.com") with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add(method="GET", url=self.url, body=HTTPError("Exception")) key_cli.channels.list(channel_id="xxxxx") def test_parse_response(self, key_cli, helpers): with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("error_response.json", helpers), status=400, ) key_cli.channels.list(id="xxxx") def test_oauth(self, helpers): cli = Client(client_id="id", client_secret="secret") url, state = cli.get_authorize_url() assert state == "Python-YouTube" # test oauth flow with responses.RequestsMock() as m: m.add( method="POST", url=cli.EXCHANGE_ACCESS_TOKEN_URL, json=self.load_json("apidata/access_token.json", helpers), ) token = cli.generate_access_token(code="code") assert token.access_token == "access_token" refresh_token = cli.refresh_access_token(refresh_token="token") assert refresh_token.access_token == "access_token" # test revoke access token with responses.RequestsMock() as m: m.add( method="POST", url=cli.REVOKE_TOKEN_URL, ) assert cli.revoke_access_token(token="token") with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="POST", url=cli.REVOKE_TOKEN_URL, json={"error": {"code": 400, "message": "error"}}, status=400, ) cli.revoke_access_token(token="token") def test_subscribe_push_notification(self): HUB_URL = "https://pubsubhubbub.appspot.com/subscribe" cli = Client(client_id="id", client_secret="secret") # subscribe returns True on 202 Accepted with responses.RequestsMock() as m: m.add(method="POST", url=HUB_URL, status=202) result = cli.subscribe_push_notification( channel_id="UCxxxxxx", callback_url="https://example.com/webhook", ) assert result is True # verify hub.mode and hub.topic were sent correctly assert m.calls[0].request.body is not None assert "hub.mode=subscribe" in m.calls[0].request.body assert "UCxxxxxx" in m.calls[0].request.body # unsubscribe returns True on 202 Accepted with responses.RequestsMock() as m: m.add(method="POST", url=HUB_URL, status=202) result = cli.subscribe_push_notification( channel_id="UCxxxxxx", callback_url="https://example.com/webhook", mode="unsubscribe", ) assert result is True assert "hub.mode=unsubscribe" in m.calls[0].request.body # sync verify returns True on 204 No Content with responses.RequestsMock() as m: m.add(method="POST", url=HUB_URL, status=204) result = cli.subscribe_push_notification( channel_id="UCxxxxxx", callback_url="https://example.com/webhook", verify="sync", ) assert result is True # optional params: lease_seconds and secret are included in request body with responses.RequestsMock() as m: m.add(method="POST", url=HUB_URL, status=202) cli.subscribe_push_notification( channel_id="UCxxxxxx", callback_url="https://example.com/webhook", lease_seconds=432000, secret="mysecret", ) body = m.calls[0].request.body assert "hub.lease_seconds=432000" in body assert "hub.secret=mysecret" in body # hub error raises PyYouTubeException with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="POST", url=HUB_URL, json={"error": {"code": 400, "message": "bad request"}}, status=400, ) cli.subscribe_push_notification( channel_id="UCxxxxxx", callback_url="https://example.com/webhook", ) ================================================ FILE: tests/clients/test_comment_threads.py ================================================ import pytest import responses import pyyoutube.models as mds from .base import BaseTestCase from pyyoutube.error import PyYouTubeException class TestCommentThreadsResource(BaseTestCase): RESOURCE = "commentThreads" def test_list(self, helpers, key_cli): with pytest.raises(PyYouTubeException): key_cli.commentThreads.list() with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json( "comment_threads/comment_threads_by_video_paged_1.json", helpers ), ) res = key_cli.commentThreads.list( parts=["id", "snippet"], all_threads_related_to_channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", ) assert res.items[0].snippet.totalReplyCount == 0 res = key_cli.commentThreads.list( parts=["id", "snippet"], channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", ) assert res.items[0].snippet.totalReplyCount == 0 res = key_cli.commentThreads.list( parts=["id", "snippet"], video_id="F1UP7wRCPH8", ) assert res.items[0].snippet.videoId == "F1UP7wRCPH8" res = key_cli.commentThreads.list( parts=["id", "snippet"], thread_id="UgyZ1jqkHKYvi1-ruOZ4AaABAg,Ugy4OzAuz5uJuFt3FH54AaABAg", ) assert res.items[0].id == "UgyZ1jqkHKYvi1-ruOZ4AaABAg" def test_insert(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="POST", url=self.url, json=self.load_json("comment_threads/insert_response.json", helpers), ) thread = authed_cli.commentThreads.insert( body=mds.CommentThread( snippet=mds.CommentThreadSnippet( videoId="JE8xdDp5B8Q", topLevelComment=mds.Comment( snippet=mds.CommentSnippet( textOriginal="Sun from the api", ) ), ) ), parts=["id", "snippet"], ) assert thread.snippet.videoId == "JE8xdDp5B8Q" ================================================ FILE: tests/clients/test_comments.py ================================================ import pytest import responses import pyyoutube.models as mds from .base import BaseTestCase from pyyoutube.error import PyYouTubeException class TestCommentsResource(BaseTestCase): RESOURCE = "comments" def test_list(self, helpers, key_cli): with pytest.raises(PyYouTubeException): key_cli.comments.list() with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json( "comments/comments_by_parent_paged_1.json", helpers ), ) res = key_cli.comments.list( parts=["id", "snippet"], parent_id="Ugw5zYU6n9pmIgAZWvN4AaABAg", ) assert ( res.items[0].id == "Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za6voUoRh" ) assert res.items[0].snippet.parentId == "Ugw5zYU6n9pmIgAZWvN4AaABAg" res = key_cli.comments.list( parts=["id", "snippet"], comment_id="UgyUBI0HsgL9emxcZpR4AaABAg,Ugzi3lkqDPfIOirGFLh4AaABAg", ) assert len(res.items) == 2 def test_insert(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="POST", url=self.url, json=self.load_json("comments/insert_response.json", helpers), ) comment = authed_cli.comments.insert( body=mds.Comment( snippet=mds.CommentSnippet( parentId="Ugy_CAftKrIUCyPr9GR4AaABAg", textOriginal="wow", ) ), parts=["id", "snippet"], ) assert comment.snippet.parentId == "Ugy_CAftKrIUCyPr9GR4AaABAg" def test_update(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="PUT", url=self.url, json=self.load_json("comments/insert_response.json", helpers), ) comment = authed_cli.comments.update( body=mds.Comment( id="Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt", snippet=mds.CommentSnippet( textOriginal="wow", ), ), parts=["id", "snippet"], ) assert comment.snippet.parentId == "Ugy_CAftKrIUCyPr9GR4AaABAg" def test_mark_as_spam(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add(method="POST", url=f"{self.url}/markAsSpam", status=204) assert authed_cli.comments.mark_as_spam( comment_id="Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt", ) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="POST", url=f"{self.url}/markAsSpam", json=self.load_json("error_permission_resp.json", helpers), status=403, ) authed_cli.comments.mark_as_spam( comment_id="Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt", ) def test_set_moderation_status(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add(method="POST", url=f"{self.url}/setModerationStatus", status=204) assert authed_cli.comments.set_moderation_status( comment_id="Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt", moderation_status="rejected", ) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="POST", url=f"{self.url}/setModerationStatus", json=self.load_json("error_permission_resp.json", helpers), status=403, ) authed_cli.comments.set_moderation_status( comment_id="Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt", moderation_status="published", ban_author=True, ) def test_delete(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add(method="DELETE", url=f"{self.url}", status=204) assert authed_cli.comments.delete( comment_id="Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt", ) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="DELETE", url=f"{self.url}", json=self.load_json("error_permission_resp.json", helpers), status=403, ) authed_cli.comments.delete( comment_id="Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt", ) ================================================ FILE: tests/clients/test_i18n.py ================================================ import responses from .base import BaseTestCase class TestI18nLanguagesResource(BaseTestCase): RESOURCE = "i18nLanguages" def test_list(self, helpers, key_cli): with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("i18ns/language_res.json", helpers), ) res = key_cli.i18nLanguages.list( parts=["snippet"], ) assert res.items[0].snippet.name == "Chinese" class TestI18nRegionsResource(BaseTestCase): RESOURCE = "i18nRegions" def test_list(self, helpers, key_cli): with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("i18ns/regions_res.json", helpers), ) res = key_cli.i18nRegions.list( parts=["snippet"], ) assert res.items[0].snippet.name == "Venezuela" ================================================ FILE: tests/clients/test_media.py ================================================ """ Tests for media upload. """ import io import pytest import responses from requests import Response from pyyoutube.error import PyYouTubeException from pyyoutube.media import Media, MediaUpload, MediaUploadProgress class TestMedia: def test_initial(self, tmp_path): with pytest.raises(PyYouTubeException): Media() d = tmp_path / "sub" d.mkdir() f = d / "simple.vvv" f.write_bytes(b"asd") m = Media(filename=str(f)) assert m.mimetype == "application/octet-stream" f1 = d / "video.mp4" f1.write_text("video") m = Media(fd=f1.open("rb")) assert m.size == 5 assert m.get_bytes(0, 2) class TestMediaUploadProgress: def test_progress(self): pg = MediaUploadProgress(10, 20) assert pg.progress() == 0.5 assert str(pg) pg = MediaUploadProgress(10, 0) assert pg.progress() == 0.0 class TestMediaUpload: def test_upload(self, helpers, authed_cli): location = "https://youtube.googleapis.com/upload/youtube/v3/videos?part=snippet&alt=json&uploadType=resumable&upload_id=upload_id" media = Media(fd=io.StringIO("1234567890"), mimetype="video/mp4", chunk_size=5) upload = MediaUpload( client=authed_cli, resource="videos", media=media, params={"part": "snippet"}, body={"body": '{"snippet": {dasd}}'}, ) with responses.RequestsMock() as m: m.add( method="POST", url="https://www.googleapis.com/upload/youtube/v3/videos", status=200, adding_headers={"location": location}, ) m.add( method="PUT", url=location, status=308, adding_headers={ "range": "0-4", }, ) m.add( method="PUT", url=location, json=helpers.load_json("testdata/apidata/videos/insert_response.json"), ) pg, body = upload.next_chunk() assert pg.progress() == 0.5 assert body is None pg, body = upload.next_chunk() assert pg is None assert body["id"] == "D-lhorsDlUQ" def test_upload_response(self, authed_cli, helpers): location = "https://youtube.googleapis.com/upload/youtube/v3/videos?part=snippet&alt=json&uploadType=resumable&upload_id=upload_id" media = Media( fd=io.StringIO("1234567890"), mimetype="video/mp4", ) upload = MediaUpload( client=authed_cli, resource="videos", media=media, params={"part": "snippet"}, body={"body": '{"snippet": {dasd}}'}, ) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="POST", url="https://www.googleapis.com/upload/youtube/v3/videos", status=400, json=helpers.load_json("testdata/error_response.json"), ) upload.next_chunk() with responses.RequestsMock() as m: m.add( method="PUT", url=location, status=308, ) upload.resumable_uri = location upload.next_chunk() with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="PUT", url=location, status=400, json=helpers.load_json("testdata/error_response.json"), ) upload.resumable_uri = location upload.next_chunk() resp = Response() resp.status_code = 308 resp.headers = {"location": location} upload.process_response(resp=resp) ================================================ FILE: tests/clients/test_members.py ================================================ import responses from .base import BaseTestCase class TestMembersResource(BaseTestCase): RESOURCE = "members" def test_list(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("members/members_data.json", helpers), ) res = authed_cli.members.list( parts=["snippet"], mode="all_current", max_results=5, ) assert len(res.items) == 2 ================================================ FILE: tests/clients/test_membership_levels.py ================================================ import responses from .base import BaseTestCase class TestMembershipLevelsResource(BaseTestCase): RESOURCE = "membershipsLevels" def test_list(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("members/membership_levels.json", helpers), ) res = authed_cli.membershipsLevels.list( parts=["id", "snippet"], ) assert len(res.items) == 2 ================================================ FILE: tests/clients/test_playlist_items.py ================================================ import pytest import responses import pyyoutube.models as mds from .base import BaseTestCase from pyyoutube.error import PyYouTubeException class TestPlaylistItemsResource(BaseTestCase): RESOURCE = "playlistItems" def test_list(self, helpers, key_cli): with pytest.raises(PyYouTubeException): key_cli.playlistItems.list() with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json( "playlist_items/playlist_items_paged_1.json", helpers ), ) res = key_cli.playlistItems.list( parts=["id", "snippet"], playlist_id="PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", max_results=10, ) assert len(res.items) == 10 res = key_cli.playlistItems.list( playlist_item_id="UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2", parts=["id", "snippet"], ) assert ( res.items[0].id == "UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2" ) def test_insert(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="POST", url=self.url, json=self.load_json("playlist_items/insert_response.json", helpers), ) item = authed_cli.playlistItems.insert( body=mds.PlaylistItem( snippet=mds.PlaylistItemSnippet( playlistId="PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS", position=0, resourceId=mds.ResourceId( kind="youtube#video", videoId="2sjqTHE0zok" ), ) ), parts=["id", "snippet"], ) assert item.snippet.playlistId == "PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS" def test_update(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="PUT", url=self.url, json=self.load_json("playlist_items/insert_response.json", helpers), ) item = authed_cli.playlistItems.update( body=mds.PlaylistItem( snippet=mds.PlaylistItemSnippet( playlistId="PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS", position=1, resourceId=mds.ResourceId( kind="youtube#video", videoId="2sjqTHE0zok" ), ) ), parts=["id", "snippet"], ) assert item.snippet.playlistId == "PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS" def test_delete(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add(method="DELETE", url=self.url, status=204) assert authed_cli.playlistItems.delete( playlist_item_id="PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvSxxxxx" ) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="DELETE", url=self.url, json=self.load_json("error_permission_resp.json", helpers), status=403, ) authed_cli.playlistItems.delete(playlist_item_id="xxxxxx") ================================================ FILE: tests/clients/test_playlists.py ================================================ import pytest import responses import pyyoutube.models as mds from .base import BaseTestCase from pyyoutube.error import PyYouTubeException class TestPlaylistsResource(BaseTestCase): RESOURCE = "playlists" def test_list(self, helpers, authed_cli, key_cli): with pytest.raises(PyYouTubeException): authed_cli.playlists.list() with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("playlists/playlists_paged_1.json", helpers), ) res = key_cli.playlists.list( parts=["id", "snippet"], channel_id="UC_x5XG1OV2P6uZZ5FSM9Ttw", max_results=10, ) assert len(res.items) == 10 res = key_cli.playlists.list( parts=["id", "snippet"], playlist_id=[ "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw", "PLOU2XLYxmsIJO83u2UmyC8ud41AvUnhgj", ], ) assert res.items[0].id == "PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw" res = authed_cli.playlists.list( parts=["id", "snippet"], mine=True, max_results=10 ) assert len(res.items) == 10 def test_insert(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="POST", url=self.url, json=self.load_json("playlists/insert_response.json", helpers), ) playlist = authed_cli.playlists.insert( body=mds.Playlist( snippet=mds.PlaylistSnippet( title="Test playlist", ) ), ) assert playlist.id == "PLBaidt0ilCMZN8XPVB5iXY6FlSYGeyn2n" def test_update(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="PUT", url=self.url, json=self.load_json("playlists/insert_response.json", helpers), ) playlist = authed_cli.playlists.update( body=mds.Playlist( snippet=mds.PlaylistSnippet( title="Test playlist", defaultLanguage="", ) ) ) assert playlist.snippet.description == "" def test_delete(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add(method="DELETE", url=self.url, status=204) assert authed_cli.playlists.delete( playlist_id="PLBaidt0ilCMZN8XPVB5iXY6FlSYGeyn2n" ) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="DELETE", url=self.url, json=self.load_json("error_permission_resp.json", helpers), status=403, ) authed_cli.playlists.delete( playlist_id="PLBaidt0ilCMZN8XPVB5iXY6FlSYGeyn2n" ) ================================================ FILE: tests/clients/test_search.py ================================================ import responses from .base import BaseTestCase class TestSearchResource(BaseTestCase): RESOURCE = "search" def test_list(self, helpers, authed_cli, key_cli): with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("search/search_by_developer.json", helpers), ) res = authed_cli.search.list( parts=["snippet"], for_content_owner=True, ) assert res.items[0].id.videoId == "WuyFniRMrxY" res = authed_cli.search.list( for_developer=True, max_results=5, ) assert len(res.items) == 5 with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("search/search_by_mine.json", helpers), ) res = authed_cli.search.list(for_mine=True, max_results=5) assert res.items[0].snippet.channelId == "UCa-vrCLQHviTOVnEKDOdetQ" with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("search/search_by_related_video.json", helpers), ) res = authed_cli.search.list( related_to_video_id="Ks-_Mh1QhMc", region_code="US", relevance_language="en", safe_search="moderate", max_results=5, ) assert res.items[0].id.videoId == "eIho2S0ZahI" with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("search/search_by_keywords_p1.json", helpers), ) res = key_cli.search.list( q="surfing", parts=["snippet"], count=25, ) assert len(res.items) == 25 ================================================ FILE: tests/clients/test_subscriptions.py ================================================ import pytest import responses import pyyoutube.models as mds from .base import BaseTestCase from pyyoutube.error import PyYouTubeException class TestSubscriptionsResource(BaseTestCase): RESOURCE = "subscriptions" def test_list(self, helpers, key_cli, authed_cli): with pytest.raises(PyYouTubeException): key_cli.subscriptions.list() with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json( "subscriptions/subscriptions_by_mine_p1.json", helpers ), ) res = key_cli.subscriptions.list( parts=["id", "snippet"], channel_id="UCa-vrCLQHviTOVnEKDOdetQ", max_results=10, ) assert res.items[0].id == "zqShTXi-2-Tx7TtwQqhCBzrqBvZj94YvFZOGA9x6NuY" res = authed_cli.subscriptions.list(mine=True, max_results=10) assert res.items[0].snippet.channelId == "UCNvMBmCASzTNNX8lW3JRMbw" res = authed_cli.subscriptions.list( my_recent_subscribers=True, max_results=10 ) assert res.items[0].snippet.channelId == "UCNvMBmCASzTNNX8lW3JRMbw" res = authed_cli.subscriptions.list(my_subscribers=True, max_results=10) assert res.items[0].snippet.channelId == "UCNvMBmCASzTNNX8lW3JRMbw" with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("subscriptions/subscriptions_by_id.json", helpers), ) res = key_cli.subscriptions.list( parts=["id", "snippet"], subscription_id=[ "zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo", "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo", ], ) assert res.items[0].id == "zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo" def test_inset(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="POST", url=self.url, json=self.load_json("subscriptions/insert_response.json", helpers), ) subscription = authed_cli.subscriptions.insert( body=mds.Subscription( snippet=mds.SubscriptionSnippet( resourceId=mds.ResourceId( kind="youtube#channel", channelId="UCQ6ptCagG3W0Bf4lexvnBEg", ) ) ) ) assert subscription.id == "POsnRIYsMcp1Cghr_Fsh-6uFZRcIHmTKzzByiv9ZAro" def test_delete(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="DELETE", url=self.url, status=204, ) assert authed_cli.subscriptions.delete( subscription_id="POsnRIYsMcp1Cghr_Fsh-6uFZRcIHmTKzzByiv9ZAro" ) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="DELETE", url=self.url, json=self.load_json("error_permission_resp.json", helpers), status=403, ) authed_cli.subscriptions.delete( subscription_id="POsnRIYsMcp1Cghr_Fsh-6uFZRcIHmTKzzByiv9ZAro" ) ================================================ FILE: tests/clients/test_thumbnails.py ================================================ """ Tests for thumbnails. """ import io from .base import BaseTestCase from pyyoutube.media import Media class TestThumbnailsResource(BaseTestCase): RESOURCE = "thumbnails" def test_set(self, authed_cli): video_id = "zxTVeyG1600" media = Media(fd=io.StringIO("jpeg content"), mimetype="image/jpeg") upload = authed_cli.thumbnails.set( video_id=video_id, media=media, ) assert upload.resumable_progress == 0 ================================================ FILE: tests/clients/test_video_abuse_report_reasons.py ================================================ import responses from .base import BaseTestCase class TestVideoAbuseReportReasonsResource(BaseTestCase): RESOURCE = "videoAbuseReportReasons" def test_list(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("abuse_reasons/abuse_reason.json", helpers), ) res = authed_cli.videoAbuseReportReasons.list( parts=["id", "snippet"], ) assert res.items[0].id == "N" ================================================ FILE: tests/clients/test_video_categories.py ================================================ import pytest import responses from .base import BaseTestCase from pyyoutube.error import PyYouTubeException class TestVideoCategoriesResource(BaseTestCase): RESOURCE = "videoCategories" def test_list(self, helpers, key_cli): with pytest.raises(PyYouTubeException): key_cli.videoCategories.list() with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json( "categories/video_category_by_region.json", helpers ), ) res = key_cli.videoCategories.list( parts=["snippet"], region_code="US", ) assert res.items[0].snippet.title == "Film & Animation" with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("categories/video_category_multi.json", helpers), ) res = key_cli.videoCategories.list( parts=["snippet"], category_id=["17", "18"], ) assert len(res.items) == 2 ================================================ FILE: tests/clients/test_videos.py ================================================ import io import pytest import responses import pyyoutube.models as mds from .base import BaseTestCase from pyyoutube.error import PyYouTubeException from pyyoutube.media import Media class TestVideosResource(BaseTestCase): RESOURCE = "videos" def test_list(self, helpers, authed_cli, key_cli): with pytest.raises(PyYouTubeException): key_cli.videos.list() with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("videos/videos_info_multi.json", helpers), ) res = key_cli.videos.list( video_id=["D-lhorsDlUQ", "ovdbrdCIP7U"], parts=["snippet"] ) assert len(res.items) == 2 with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("videos/videos_chart_paged_1.json", helpers), ) res = key_cli.videos.list(chart="mostPopular", parts=["snippet"]) assert len(res.items) == 5 with responses.RequestsMock() as m: m.add( method="GET", url=self.url, json=self.load_json("videos/videos_myrating_paged_1.json", helpers), ) res = key_cli.videos.list(my_rating="like", parts=["snippet"]) assert len(res.items) == 2 def test_insert(self, helpers, authed_cli): body = mds.Video( snippet=mds.VideoSnippet( title="video title", description="video description", ) ) media = Media(fd=io.StringIO("video content"), mimetype="video/mp4") upload = authed_cli.videos.insert( body=body, media=media, notify_subscribers=True, ) assert upload.resumable_progress == 0 def test_update(self, helpers, authed_cli): body = mds.Video( snippet=mds.VideoSnippet( title="updated video title", ) ) with responses.RequestsMock() as m: m.add( method="PUT", url=self.url, json=self.load_json("videos/insert_response.json", helpers), ) video = authed_cli.videos.update(body=body, parts=["snippet"]) assert video.id == "D-lhorsDlUQ" def test_rate(self, helpers, authed_cli): video_id = "D-lhorsDlUQ" with responses.RequestsMock() as m: m.add(method="POST", url=f"{self.url}/rate", status=204) assert authed_cli.videos.rate( video_id=video_id, rating="like", ) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="POST", url=f"{self.url}/rate", status=403, json=self.load_json("error_permission_resp.json", helpers), ) authed_cli.videos.rate( video_id=video_id, rating="like", ) def test_get_rating(self, helpers, authed_cli): video_id = "D-lhorsDlUQ" with responses.RequestsMock() as m: m.add( method="GET", url=f"{self.url}/getRating", json=self.load_json("videos/get_rating_response.json", helpers), ) res = authed_cli.videos.get_rating( video_id=video_id, ) assert res.items[0].rating == "none" def test_report_abuse(self, helpers, authed_cli): body = mds.VideoReportAbuse( videoId="D-lhorsDlUQ", reasonId="xxxxxx", ) with responses.RequestsMock() as m: m.add(method="POST", url=f"{self.url}/reportAbuse", status=204) assert authed_cli.videos.report_abuse(body=body) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="POST", url=f"{self.url}/reportAbuse", status=403, json=self.load_json("error_permission_resp.json", helpers), ) authed_cli.videos.report_abuse(body=body) def test_delete(self, helpers, authed_cli): video_id = "D-lhorsDlUQ" with responses.RequestsMock() as m: m.add(method="DELETE", url=self.url, status=204) assert authed_cli.videos.delete(video_id=video_id) with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="DELETE", url=self.url, status=403, json=self.load_json("error_permission_resp.json", helpers), ) authed_cli.videos.delete(video_id=video_id) ================================================ FILE: tests/clients/test_watermarks.py ================================================ """ Tests for watermarks. """ import io import pytest import responses import pyyoutube.models as mds from .base import BaseTestCase from pyyoutube.error import PyYouTubeException from pyyoutube.media import Media class TestWatermarksResource(BaseTestCase): RESOURCE = "watermarks" def test_set(self, authed_cli): body = mds.Watermark( timing=mds.WatermarkTiming( type="offsetFromStart", offsetMs=1000, durationMs=3000, ), position=mds.WatermarkPosition( type="corner", cornerPosition="topRight", ), ) media = Media(fd=io.StringIO("image content"), mimetype="image/jpeg") upload = authed_cli.watermarks.set( channel_id="id", body=body, media=media, ) assert upload.resumable_progress == 0 def test_unset(self, helpers, authed_cli): with responses.RequestsMock() as m: m.add(method="POST", url=f"{self.url}/unset", status=204) assert authed_cli.watermarks.unset(channel_id="id") with pytest.raises(PyYouTubeException): with responses.RequestsMock() as m: m.add( method="POST", url=f"{self.url}/unset", status=403, json=self.load_json("error_permission_resp.json", helpers), ) assert authed_cli.watermarks.unset(channel_id="id") ================================================ FILE: tests/conftest.py ================================================ import json import pytest from pyyoutube import Client class Helpers: @staticmethod def load_json(filename): with open(filename, "rb") as f: return json.loads(f.read().decode("utf-8")) @staticmethod def load_file_binary(filename): with open(filename, "rb") as f: return f.read() @pytest.fixture def helpers(): return Helpers() @pytest.fixture(scope="class") def authed_cli(): return Client(access_token="access token") @pytest.fixture(scope="class") def key_cli(): return Client(api_key="api key") ================================================ FILE: tests/models/__init__.py ================================================ ================================================ FILE: tests/models/test_abuse_reason.py ================================================ import json import unittest import pyyoutube.models as models class AbuseReasonModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/abuse_report_reason/" with open(BASE_PATH + "abuse_reason.json", "rb") as f: ABUSE_REASON = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "abuse_reason_res.json", "rb") as f: ABUSE_REASON_RES = json.loads(f.read().decode("utf-8")) def testAbuseReason(self) -> None: m = models.VideoAbuseReportReason.from_dict(self.ABUSE_REASON) self.assertEqual(m.id, "N") self.assertEqual(m.snippet.label, "Sex or nudity") self.assertEqual(len(m.snippet.secondaryReasons), 3) def testAbuseReasonResponse(self) -> None: m = models.VideoAbuseReportReasonListResponse.from_dict(self.ABUSE_REASON_RES) self.assertEqual(m.kind, "youtube#videoAbuseReportReasonListResponse") self.assertEqual(len(m.items), 3) ================================================ FILE: tests/models/test_activities.py ================================================ import json import unittest import pyyoutube.models as models class ActivityModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/activities/" with open(BASE_PATH + "activity_contentDetails.json", "rb") as f: ACTIVITY_CONTENT_DETAILS = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "activity_snippet.json", "rb") as f: ACTIVITY_SNIPPET = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "activity.json", "rb") as f: ACTIVITY = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "activity_response.json", "rb") as f: ACTIVITY_RESPONSE = json.loads(f.read().decode("utf-8")) def testActivityContentDetails(self) -> None: m = models.ActivityContentDetails.from_dict(self.ACTIVITY_CONTENT_DETAILS) self.assertEqual(m.upload.videoId, "LDXYRzerjzU") def testActivitySnippet(self) -> None: m = models.ActivitySnippet.from_dict(self.ACTIVITY_SNIPPET) self.assertEqual(m.channelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw") self.assertEqual( m.thumbnails.default.url, "https://i.ytimg.com/vi/DQGSZTxLVrI/default.jpg" ) def testActivity(self) -> None: m = models.Activity.from_dict(self.ACTIVITY) self.assertEqual(m.snippet.channelId, "UCa-vrCLQHviTOVnEKDOdetQ") self.assertEqual(m.contentDetails.upload.videoId, "JE8xdDp5B8Q") def testActivityListResponse(self) -> None: m = models.ActivityListResponse.from_dict(self.ACTIVITY_RESPONSE) self.assertEqual(m.kind, "youtube#activityListResponse") self.assertEqual(m.pageInfo.totalResults, 2) self.assertEqual(len(m.items), 2) ================================================ FILE: tests/models/test_auth_models.py ================================================ import json import unittest import pyyoutube.models as models class AuthModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/users/" with open(BASE_PATH + "access_token.json", "rb") as f: ACCESS_TOKEN_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "user_profile.json", "rb") as f: USER_PROFILE_INFO = json.loads(f.read().decode("utf-8")) def testAccessToken(self) -> None: m = models.AccessToken.from_dict(self.ACCESS_TOKEN_INFO) self.assertEqual(m.access_token, "access_token") def testUserProfile(self) -> None: m = models.UserProfile.from_dict(self.USER_PROFILE_INFO) self.assertEqual(m.id, "12345678910") origin_data = json.dumps(self.USER_PROFILE_INFO, sort_keys=True) d = m.to_json(sort_keys=True, allow_nan=False) self.assertEqual(origin_data, d) ================================================ FILE: tests/models/test_captions.py ================================================ import json import unittest import pyyoutube.models as models class CaptionModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/captions/" with open(BASE_PATH + "caption_snippet.json", "rb") as f: CAPTION_SNIPPET = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "caption.json", "rb") as f: CAPTION_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "caption_response.json", "rb") as f: CAPTION_RESPONSE = json.loads(f.read().decode("utf-8")) def testCaptionSnippet(self): m = models.CaptionSnippet.from_dict(self.CAPTION_SNIPPET) self.assertEqual(m.videoId, "oHR3wURdJ94") self.assertEqual( m.string_to_datetime(m.lastUpdated).isoformat(), "2020-01-14T09:40:49.981000+00:00", ) def testCaption(self): m = models.Caption.from_dict(self.CAPTION_INFO) self.assertEqual(m.id, "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I") self.assertEqual(m.snippet.videoId, "oHR3wURdJ94") def testCaptionListResponse(self): m = models.CaptionListResponse.from_dict(self.CAPTION_RESPONSE) self.assertEqual(m.kind, "youtube#captionListResponse") self.assertEqual(len(m.items), 2) self.assertEqual(m.items[0].id, "SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I") ================================================ FILE: tests/models/test_category.py ================================================ import json import unittest import pyyoutube.models as models class CategoryModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/categories/" with open(BASE_PATH + "video_category_info.json", "rb") as f: VIDEO_CATEGORY_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_category_response.json", "rb") as f: VIDEO_CATEGORY_RESPONSE = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "guide_category_info.json", "rb") as f: GUIDE_CATEGORY_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "guide_category_response.json", "rb") as f: GUIDE_CATEGORY_RESPONSE = json.loads(f.read().decode("utf-8")) def testVideoCategory(self) -> None: m = models.VideoCategory.from_dict(self.VIDEO_CATEGORY_INFO) self.assertEqual(m.id, "17") self.assertEqual(m.snippet.title, "Sports") def testVideoCategoryListResponse(self) -> None: m = models.VideoCategoryListResponse.from_dict(self.VIDEO_CATEGORY_RESPONSE) self.assertEqual(m.kind, "youtube#videoCategoryListResponse") self.assertEqual(len(m.items), 1) self.assertEqual(m.items[0].id, "17") ================================================ FILE: tests/models/test_channel.py ================================================ import json import unittest import pyyoutube.models as models class ChannelModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/channels/" with open(BASE_PATH + "channel_branding_settings.json", "rb") as f: BRANDING_SETTINGS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_content_details.json", "rb") as f: CONTENT_DETAILS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_topic_details.json", "rb") as f: TOPIC_DETAILS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_snippet.json", "rb") as f: SNIPPET_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_statistics.json", "rb") as f: STATISTICS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_status.json", "rb") as f: STATUS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_info.json", "rb") as f: CHANNEL_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_api_response.json", "rb") as f: CHANNEL_API_RESPONSE = json.loads(f.read().decode("utf-8")) def testChannelBrandingSettings(self) -> None: m = models.ChannelBrandingSetting.from_dict(self.BRANDING_SETTINGS_INFO) self.assertEqual(m.channel.title, "Google Developers") def testChannelContentDetails(self) -> None: m = models.ChannelContentDetails.from_dict(self.CONTENT_DETAILS_INFO) self.assertEqual(m.relatedPlaylists.uploads, "UU_x5XG1OV2P6uZZ5FSM9Ttw") def testChannelTopicDetails(self) -> None: m = models.ChannelTopicDetails.from_dict(self.TOPIC_DETAILS_INFO) self.assertEqual(m.topicIds[0], "/m/019_rr") self.assertEqual(len(m.topicCategories), 3) full_topics = m.get_full_topics() self.assertEqual(full_topics[0].id, "/m/019_rr") self.assertEqual(full_topics[0].description, "Lifestyle (parent topic)") def testChannelSnippet(self) -> None: m = models.ChannelSnippet.from_dict(self.SNIPPET_INFO) self.assertEqual(m.title, "Google Developers") self.assertEqual(m.localized.title, "Google Developers") self.assertEqual( m.thumbnails.default.url, "https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo", ) published_at = m.string_to_datetime(m.publishedAt) self.assertEqual(published_at.isoformat(), "2007-08-23T00:34:43+00:00") def testChannelStatistics(self) -> None: m = models.ChannelStatistics.from_dict(self.STATISTICS_INFO) self.assertEqual(m.viewCount, 160361638) def testChannelStatus(self) -> None: m = models.ChannelStatus.from_dict(self.STATUS_INFO) self.assertEqual(m.privacyStatus, "public") def testChannel(self) -> None: m = models.Channel.from_dict(self.CHANNEL_INFO) self.assertEqual(m.id, "UC_x5XG1OV2P6uZZ5FSM9Ttw") def testChannelListResponse(self) -> None: m = models.ChannelListResponse.from_dict(self.CHANNEL_API_RESPONSE) self.assertEqual(m.kind, "youtube#channelListResponse") self.assertEqual(m.pageInfo.totalResults, 1) self.assertEqual(m.items[0].id, "UC_x5XG1OV2P6uZZ5FSM9Ttw") ================================================ FILE: tests/models/test_channel_sections.py ================================================ import json import unittest import pyyoutube.models as models class ChannelSectionModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/channel_sections/" with open(BASE_PATH + "channel_section_info.json", "rb") as f: CHANNEL_SECTION_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "channel_section_response.json", "rb") as f: CHANNEL_SECTION_RESPONSE = json.loads(f.read().decode("utf-8")) def testChannelSection(self) -> None: m = models.ChannelSection.from_dict(self.CHANNEL_SECTION_INFO) self.assertEqual(m.id, "UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE") self.assertEqual(m.snippet.type, "multipleChannels") self.assertEqual(len(m.contentDetails.channels), 16) def testChannelSectionResponse(self) -> None: m = models.ChannelSectionResponse.from_dict(self.CHANNEL_SECTION_RESPONSE) self.assertEqual(m.kind, "youtube#channelSectionListResponse") self.assertEqual(len(m.items), 10) ================================================ FILE: tests/models/test_comments.py ================================================ import json import unittest import pyyoutube.models as models class CommentModelModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/comments/" with open(BASE_PATH + "comment_snippet.json", "rb") as f: SNIPPET_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_info.json", "rb") as f: COMMENT_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_api_response.json", "rb") as f: COMMENT_API_INFO = json.loads(f.read().decode("utf-8")) def testCommentSnippet(self) -> None: m = models.CommentSnippet.from_dict(self.SNIPPET_INFO) self.assertEqual(m.videoId, "wtLJPvx7-ys") self.assertTrue(m.canRate) self.assertEqual(m.authorChannelId.value, "UCqPku3cxM-ED3poX8YtGqeg") self.assertEqual( m.string_to_datetime(m.publishedAt).isoformat(), "2019-03-28T11:33:46+00:00" ) def testComment(self) -> None: m = models.Comment.from_dict(self.COMMENT_INFO) self.assertEqual(m.id, "UgwxApqcfzZzF_C5Zqx4AaABAg") self.assertEqual(m.snippet.authorDisplayName, "Oeurn Ravuth") self.assertEqual( m.snippet.string_to_datetime(m.snippet.updatedAt).isoformat(), "2019-03-28T11:33:46+00:00", ) def testCommentListResponse(self) -> None: m = models.CommentListResponse.from_dict(self.COMMENT_API_INFO) self.assertEqual(m.kind, "youtube#commentListResponse") self.assertEqual(m.items[0].id, "UgxKREWxIgDrw8w2e_Z4AaABAg") class CommentThreadModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/comments/" with open(BASE_PATH + "comment_thread_snippet.json", "rb") as f: SNIPPET_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_thread_replies.json", "rb") as f: REPLIES_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_thread_info.json", "rb") as f: COMMENT_THREAD_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "comment_thread_api_response.json", "rb") as f: COMMENT_THREAD_API_INFO = json.loads(f.read().decode("utf-8")) def testCommentThreadSnippet(self) -> None: m = models.CommentThreadSnippet.from_dict(self.SNIPPET_INFO) self.assertEqual(m.videoId, "D-lhorsDlUQ") self.assertEqual(m.topLevelComment.id, "UgydxWWoeA7F1OdqypJ4AaABAg") self.assertEqual(m.topLevelComment.snippet.videoId, "D-lhorsDlUQ") def testCommentThreadReplies(self) -> None: m = models.CommentThreadReplies.from_dict(self.REPLIES_INFO) self.assertEqual(len(m.comments), 1) self.assertEqual( m.comments[0].id, "UgydxWWoeA7F1OdqypJ4AaABAg.8wWQ3tdHcFx8xcDheui-qb" ) self.assertEqual(m.comments[0].snippet.videoId, "D-lhorsDlUQ") def testCommentThread(self) -> None: m = models.CommentThread.from_dict(self.COMMENT_THREAD_INFO) self.assertEqual(m.id, "UgydxWWoeA7F1OdqypJ4AaABAg") self.assertEqual(m.snippet.videoId, "D-lhorsDlUQ") self.assertEqual( m.replies.comments[0].id, "UgydxWWoeA7F1OdqypJ4AaABAg.8wWQ3tdHcFx8xcDheui-qb", ) def testCommentThreadListResponse(self) -> None: m = models.CommentThreadListResponse.from_dict(self.COMMENT_THREAD_API_INFO) self.assertEqual(m.kind, "youtube#commentThreadListResponse") self.assertEqual(m.items[0].id, "Ugz097FRhsQy5CVhAjp4AaABAg") ================================================ FILE: tests/models/test_i18n_models.py ================================================ import json import unittest import pyyoutube.models as models class I18nModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/i18ns/" with open(BASE_PATH + "region_info.json", "rb") as f: REGION_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "region_res.json", "rb") as f: REGION_RES = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "language_info.json", "rb") as f: LANGUAGE_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "language_res.json", "rb") as f: LANGUAGE_RES = json.loads(f.read().decode("utf-8")) def testI18nRegion(self) -> None: m = models.I18nRegion.from_dict(self.REGION_INFO) self.assertEqual(m.id, "DZ") self.assertEqual(m.snippet.gl, "DZ") def testI18nRegionResponse(self) -> None: m = models.I18nRegionListResponse.from_dict(self.REGION_RES) self.assertEqual(m.kind, "youtube#i18nRegionListResponse") self.assertEqual(len(m.items), 2) def testI18nLanguage(self) -> None: m = models.I18nLanguage.from_dict(self.LANGUAGE_INFO) self.assertEqual(m.id, "af") self.assertEqual(m.snippet.hl, "af") def testI18nLanguageResponse(self) -> None: m = models.I18nRegionListResponse.from_dict(self.LANGUAGE_RES) self.assertEqual(m.kind, "youtube#i18nLanguageListResponse") self.assertEqual(len(m.items), 2) ================================================ FILE: tests/models/test_members.py ================================================ import json import unittest import pyyoutube.models as models class MemberModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/members/" with open(BASE_PATH + "member_info.json", "rb") as f: MEMBER_INFO = json.loads(f.read().decode("utf-8")) def testMember(self) -> None: m = models.Member.from_dict(self.MEMBER_INFO) self.assertEqual(m.kind, "youtube#member") self.assertEqual(m.snippet.memberDetails.channelId, "UCa-vrCLQHviTOVnEKDOdetQ") self.assertEqual(m.snippet.membershipsDetails.highestAccessibleLevel, "string") class MembershipLevelModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/members/" with open(BASE_PATH + "membership_level.json", "rb") as f: MEMBERSHIP_LEVEL_INFO = json.loads(f.read().decode("utf-8")) def testMembershipLevel(self) -> None: m = models.MembershipsLevel.from_dict(self.MEMBERSHIP_LEVEL_INFO) self.assertEqual(m.kind, "youtube#membershipsLevel") self.assertEqual(m.snippet.levelDetails.displayName, "high") ================================================ FILE: tests/models/test_playlist.py ================================================ import json import unittest import pyyoutube.models as models class PlaylistModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/playlists/" with open(BASE_PATH + "playlist_content_details.json", "rb") as f: CONTENT_DETAILS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_snippet.json", "rb") as f: SNIPPET_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_status.json", "rb") as f: STATUS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_info.json", "rb") as f: PLAYLIST_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_api_response.json", "rb") as f: PLAYLIST_RESPONSE_INFO = json.loads(f.read().decode("utf-8")) def testPlayListContentDetails(self) -> None: m = models.PlaylistContentDetails.from_dict(self.CONTENT_DETAILS_INFO) self.assertEqual(m.itemCount, 4) def testPlayListSnippet(self) -> None: m = models.PlaylistSnippet.from_dict(self.SNIPPET_INFO) self.assertEqual( m.string_to_datetime(m.publishedAt).isoformat(), "2019-05-16T18:46:20+00:00" ) self.assertEqual(m.title, "Assistant on Air") self.assertEqual( m.thumbnails.default.url, "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg" ) self.assertEqual(m.localized.title, "Assistant on Air") def testPlayListStatus(self) -> None: m = models.PlaylistStatus.from_dict(self.STATUS_INFO) self.assertEqual(m.privacyStatus, "public") def testPlayList(self) -> None: m = models.Playlist.from_dict(self.PLAYLIST_INFO) self.assertEqual(m.id, "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp") self.assertEqual(m.player, None) self.assertEqual(m.snippet.title, "Assistant on Air") def testPlaylistListResponse(self) -> None: m = models.PlaylistListResponse.from_dict(self.PLAYLIST_RESPONSE_INFO) self.assertEqual(m.kind, "youtube#playlistListResponse") self.assertEqual(m.pageInfo.totalResults, 416) self.assertEqual(m.items[0].id, "PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp") ================================================ FILE: tests/models/test_playlist_item.py ================================================ import json import unittest import pyyoutube.models as models class PlaylistItemModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/playlist_items/" with open(BASE_PATH + "playlist_item_content_details.json", "rb") as f: CONTENT_DETAILS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_item_snippet.json", "rb") as f: SNIPPET_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_item_status.json", "rb") as f: STATUS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_item_info.json", "rb") as f: PLAYLIST_ITEM_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "playlist_item_api_response.json", "rb") as f: PLAYLIST_LIST_RESPONSE = json.loads(f.read().decode("utf-8")) def testPlaylistItemContentDetails(self) -> None: m = models.PlaylistItemContentDetails.from_dict(self.CONTENT_DETAILS_INFO) self.assertEqual(m.videoId, "D-lhorsDlUQ") self.assertEqual( m.string_to_datetime(m.videoPublishedAt).isoformat(), "2019-03-21T20:37:49+00:00", ) def testPlaylistItemSnippet(self) -> None: m = models.PlaylistItemSnippet.from_dict(self.SNIPPET_INFO) self.assertEqual( m.string_to_datetime(m.publishedAt).isoformat(), "2019-05-16T18:46:20+00:00" ) self.assertEqual(m.title, "What are Actions on Google (Assistant on Air)") self.assertEqual( m.thumbnails.default.url, "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg" ) self.assertEqual(m.resourceId.videoId, "D-lhorsDlUQ") def testPlaylistItemStatus(self) -> None: m = models.PlaylistItemStatus.from_dict(self.STATUS_INFO) self.assertEqual(m.privacyStatus, "public") def testPlaylistItem(self) -> None: m = models.PlaylistItem.from_dict(self.PLAYLIST_ITEM_INFO) self.assertEqual( m.id, "UExPVTJYTFl4bXNJSnB1ZmVNSG5jblF2Rk9lMEszTWhWcC41NkI0NEY2RDEwNTU3Q0M2" ) self.assertEqual(m.snippet.channelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw") self.assertEqual(m.snippet.resourceId.videoId, "D-lhorsDlUQ") self.assertEqual(m.contentDetails.videoId, "D-lhorsDlUQ") self.assertEqual(m.status.privacyStatus, "public") self.assertEqual(m.snippet.videoOwnerChannelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw") def testPlaylistItemListResponse(self) -> None: m = models.PlaylistItemListResponse.from_dict(self.PLAYLIST_LIST_RESPONSE) self.assertEqual(m.kind, "youtube#playlistItemListResponse") self.assertEqual(m.pageInfo.totalResults, 3) self.assertEqual(len(m.items), 3) self.assertEqual( m.items[0].id, "UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2", ) ================================================ FILE: tests/models/test_search_result.py ================================================ import json import unittest import pyyoutube.models as models class SearchResultModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/search_result/" with open(BASE_PATH + "search_result_id.json", "rb") as f: SEARCH_RES_ID_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "search_result_snippet.json", "rb") as f: SEARCH_RES_SNIPPET_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "search_result.json", "rb") as f: SEARCH_RES_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "search_result_api_response.json", "rb") as f: SEARCH_RES_API_INFO = json.loads(f.read().decode("utf-8")) def testSearchResultId(self): m = models.SearchResultId.from_dict(self.SEARCH_RES_ID_INFO) self.assertEqual(m.kind, "youtube#playlist") def testSearchResultSnippet(self): m = models.SearchResultSnippet.from_dict(self.SEARCH_RES_SNIPPET_INFO) self.assertEqual(m.channelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw") self.assertEqual( m.string_to_datetime(m.publishedAt).isoformat(), "2016-03-30T16:59:12+00:00", ) self.assertEqual( m.thumbnails.default.url, "https://i.ytimg.com/vi/cKxRvEZd3Mw/default.jpg" ) def testSearchResult(self): m = models.SearchResult.from_dict(self.SEARCH_RES_INFO) self.assertEqual(m.kind, "youtube#searchResult") self.assertEqual(m.id.videoId, "fq4N0hgOWzU") def testSearchListResponse(self): m = models.SearchListResponse.from_dict(self.SEARCH_RES_API_INFO) self.assertEqual(m.kind, "youtube#searchListResponse") self.assertEqual(m.regionCode, "US") self.assertEqual(m.pageInfo.totalResults, 489126) self.assertEqual(len(m.items), 5) ================================================ FILE: tests/models/test_subscriptions.py ================================================ import json import unittest import pyyoutube.models as models class SubscriptionModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/subscriptions/" with open(BASE_PATH + "snippet.json", "rb") as f: SNIPPETS = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "contentDetails.json", "rb") as f: CONTENT_DETAILS = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "subscriberSnippet.json", "rb") as f: SUBSCRIBER_SNIPPET = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "subscription.json", "rb") as f: SUBSCRIPTION_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "resp.json", "rb") as f: SUBSCRIPTION_RESPONSE = json.loads(f.read().decode("utf-8")) def testSubscriptionSnippet(self) -> None: m = models.SubscriptionSnippet.from_dict(self.SNIPPETS) self.assertEqual(m.channelId, "UCNvMBmCASzTNNX8lW3JRMbw") self.assertEqual(m.resourceId.channelId, "UCQ7dFBzZGlBvtU2hCecsBBg") self.assertEqual( m.thumbnails.default.url, "https://yt3.ggpht.com/s88-c-k-no-mo-rj-c0xffffff/photo.jpg", ) def testSubscriptionContentDetails(self) -> None: m = models.SubscriptionContentDetails.from_dict(self.CONTENT_DETAILS) self.assertEqual(m.totalItemCount, 2) self.assertEqual(m.activityType, "all") def testSubscriptionSubscriberSnippet(self) -> None: m = models.SubscriptionSubscriberSnippet.from_dict(self.SUBSCRIBER_SNIPPET) self.assertEqual(m.title, "kun liu") self.assertEqual( m.thumbnails.default.url, "https://yt3.ggpht.com/s88-c-k-no-mo-rj-c0xffffff/photo.jpg", ) def testSubscription(self) -> None: m = models.Subscription.from_dict(self.SUBSCRIPTION_INFO) self.assertEqual(m.id, "zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo") self.assertEqual(m.snippet.title, "ikaros-life") self.assertEqual(m.contentDetails.totalItemCount, 2) self.assertEqual(m.subscriberSnippet.title, "kun liu") def testSubscriptionResponse(self) -> None: m = models.SubscriptionListResponse.from_dict(self.SUBSCRIPTION_RESPONSE) self.assertEqual(m.nextPageToken, "CAUQAA") self.assertEqual(m.pageInfo.totalResults, 16) self.assertEqual(len(m.items), 5) ================================================ FILE: tests/models/test_videos.py ================================================ import json import unittest import pyyoutube import pyyoutube.models as models class VideoModelTest(unittest.TestCase): BASE_PATH = "testdata/modeldata/videos/" with open(BASE_PATH + "video_content_details.json", "rb") as f: CONTENT_DETAILS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_topic_details.json", "rb") as f: TOPIC_DETAILS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_snippet.json", "rb") as f: SNIPPET_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_statistics.json", "rb") as f: STATISTICS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_status.json", "rb") as f: STATUS_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_info.json", "rb") as f: VIDEO_INFO = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_api_response.json", "rb") as f: VIDEO_API_RESPONSE = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_recording_details.json", "rb") as f: RECORDING_DETAILS = json.loads(f.read().decode("utf-8")) with open(BASE_PATH + "video_paid_product_placement_details.json", "rb") as f: PAID_PRODUCT_PLACEMENT_DETAILS = json.loads(f.read().decode("utf-8")) def testVideoContentDetails(self) -> None: m = models.VideoContentDetails.from_dict(self.CONTENT_DETAILS_INFO) self.assertEqual(m.duration, "PT21M7S") seconds = m.get_video_seconds_duration() self.assertEqual(seconds, 1267) m.duration = None self.assertEqual(m.get_video_seconds_duration(), None) with self.assertRaises(pyyoutube.PyYouTubeException): m.duration = "error datetime" m.get_video_seconds_duration() def testVideoTopicDetails(self) -> None: m = models.VideoTopicDetails.from_dict(self.TOPIC_DETAILS_INFO) self.assertEqual(m.topicIds[0], "/m/02jjt") self.assertEqual(len(m.topicCategories), 1) full_topics = m.get_full_topics() self.assertEqual(full_topics[0].id, "/m/02jjt") self.assertEqual(full_topics[0].description, "Entertainment (parent topic)") def testVideoSnippet(self) -> None: m = models.VideoSnippet.from_dict(self.SNIPPET_INFO) self.assertEqual( m.string_to_datetime(m.publishedAt).isoformat(), "2019-03-21T20:37:49+00:00" ) m.publishedAt = None self.assertEqual(m.string_to_datetime(m.publishedAt), None) with self.assertRaises(pyyoutube.PyYouTubeException): m.string_to_datetime("error datetime string") self.assertEqual(m.channelId, "UC_x5XG1OV2P6uZZ5FSM9Ttw") self.assertEqual( m.thumbnails.default.url, "https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg" ) self.assertEqual(m.tags[0], "Google") self.assertEqual( m.localized.title, "What are Actions on Google (Assistant on Air)" ) def testVideoStatistics(self) -> None: m = models.VideoStatistics.from_dict(self.STATISTICS_INFO) self.assertEqual(m.viewCount, 8087) def testVideoStatus(self) -> None: m = models.VideoStatus.from_dict(self.STATUS_INFO) self.assertEqual(m.uploadStatus, "processed") self.assertEqual( m.string_to_datetime(m.publishAt).isoformat(), "2019-03-21T20:37:49+00:00" ) def testVideo(self) -> None: m = models.Video.from_dict(self.VIDEO_INFO) self.assertEqual(m.id, "D-lhorsDlUQ") self.assertEqual( m.snippet.title, "What are Actions on Google (Assistant on Air)" ) def testVideoListResponse(self) -> None: m = models.VideoListResponse.from_dict(self.VIDEO_API_RESPONSE) self.assertEqual(m.kind, "youtube#videoListResponse") self.assertEqual(m.pageInfo.totalResults, 1) self.assertEqual(m.items[0].id, "D-lhorsDlUQ") def testVideoRecordingDetails(self) -> None: m = models.VideoRecordingDetails.from_dict(self.RECORDING_DETAILS) self.assertEqual( m.string_to_datetime(m.recordingDate).isoformat(), "2024-07-03T00:00:00+00:00", ) def testPaidProductPlacementDetail(self) -> None: m = models.PaidProductPlacementDetail.from_dict( self.PAID_PRODUCT_PLACEMENT_DETAILS ) self.assertTrue(m.hasPaidProductPlacement) ================================================ FILE: tests/test_error_handling.py ================================================ import unittest from requests import Response from pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException class ErrorTest(unittest.TestCase): BASE_PATH = "testdata/" with open(BASE_PATH + "error_response.json", "rb") as f: ERROR_DATA = f.read() with open(BASE_PATH + "error_response_simple.json", "rb") as f: ERROR_DATA_SIMPLE = f.read() def testResponseError(self) -> None: response = Response() response.status_code = 400 response._content = self.ERROR_DATA ex = PyYouTubeException(response=response) self.assertEqual(ex.status_code, 400) self.assertEqual(ex.message, "Bad Request") self.assertEqual(ex.error_type, "YouTubeException") error_msg = "YouTubeException(status_code=400,message=Bad Request)" self.assertEqual(repr(ex), error_msg) self.assertTrue(str(ex), error_msg) def testResponseErrorSimple(self) -> None: response = Response() response.status_code = 400 response._content = self.ERROR_DATA_SIMPLE ex = PyYouTubeException(response=response) self.assertEqual(ex.status_code, 400) def testErrorMessage(self): response = ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message="error") ex = PyYouTubeException(response=response) self.assertEqual(ex.status_code, 10000) self.assertEqual(ex.message, "error") self.assertEqual(ex.error_type, "PyYouTubeException") ================================================ FILE: tests/test_youtube_utils.py ================================================ import unittest from pyyoutube import youtube_utils as utils from pyyoutube.error import PyYouTubeException class UtilsTest(unittest.TestCase): def testDurationConvert(self): duration = "PT14H23M42S" self.assertEqual(utils.get_video_duration(duration), 51822) duration = "PT14H23M42" with self.assertRaises(PyYouTubeException): utils.get_video_duration(duration) ================================================ FILE: tests/utils/__init__.py ================================================ ================================================ FILE: tests/utils/test_params_checker.py ================================================ import unittest import pyyoutube from pyyoutube.utils.params_checker import enf_comma_separated, enf_parts class ParamCheckerTest(unittest.TestCase): def testEnfCommaSeparated(self) -> None: self.assertIsNone(enf_comma_separated("id", None)) self.assertEqual(enf_comma_separated("id", "my_id"), "my_id") self.assertEqual(enf_comma_separated("id", "id1,id2"), "id1,id2") self.assertEqual(enf_comma_separated("id", ["id1", "id2"]), "id1,id2") self.assertEqual(enf_comma_separated("id", ("id1", "id2")), "id1,id2") self.assertTrue(enf_comma_separated("id", {"id1", "id2"})) with self.assertRaises(pyyoutube.PyYouTubeException): enf_comma_separated("id", 1) with self.assertRaises(pyyoutube.PyYouTubeException): enf_comma_separated("id", [None, None]) def testEnfParts(self) -> None: self.assertTrue(enf_parts(resource="channels", value=None)) self.assertTrue(enf_parts(resource="channels", value="id"), "id") self.assertTrue(enf_parts(resource="channels", value="id,snippet")) self.assertTrue(enf_parts(resource="channels", value=["id", "snippet"])) self.assertTrue(enf_parts(resource="channels", value=("id", "snippet"))) self.assertTrue(enf_parts(resource="channels", value={"id", "snippet"})) with self.assertRaises(pyyoutube.PyYouTubeException): enf_parts(resource="channels", value=1) with self.assertRaises(pyyoutube.PyYouTubeException): enf_parts(resource="channels", value="not_part")