[
  {
    "path": ".bumpversion.cfg",
    "content": "[bumpversion]\ncurrent_version = 0.9.9\ncommit = True\ntag = True\n\n[bumpversion:file:pyyoutube/__version__.py]\n\n[bumpversion:file:pyproject.toml]\n"
  },
  {
    "path": ".coveragerc",
    "content": "[run]\nomit = pyyoutube/__version__.py"
  },
  {
    "path": ".github/hack/changelog.sh",
    "content": "#!/bin/sh\n\nMARKER_PREFIX=\"## Version\"\nVERSION=$(echo \"$1\" | sed 's/^v//g')\n\nIFS=''\nfound=0\n\nwhile read -r \"line\"; do\n  # If not found and matching heading\n  if [ $found -eq 0 ] && echo \"$line\" | grep -q \"$MARKER_PREFIX $VERSION\"; then\n    echo \"$line\"\n    found=1\n    continue\n  fi\n\n  # If needed version if found, and reaching next delimter - stop\n  if [ $found -eq 1 ] && echo \"$line\" | grep -q -E \"$MARKER_PREFIX [[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]\"; then\n    found=0\n    break\n  fi\n\n  # Keep printing out lines as no other version delimiter found\n  if [ $found -eq 1 ]; then\n    echo \"$line\"\n  fi\ndone < CHANGELOG.md"
  },
  {
    "path": ".github/hack/version.sh",
    "content": "#!/bin/sh\n\nLATEST_TAG_REV=$(git rev-list --tags --max-count=1)\nLATEST_COMMIT_REV=$(git rev-list HEAD --max-count=1)\n\nif [ -n \"$LATEST_TAG_REV\" ]; then\n    LATEST_TAG=$(git describe --tags \"$(git rev-list --tags --max-count=1)\")\nelse\n    LATEST_TAG=\"v0.0.0\"\nfi\n\nif [ \"$LATEST_TAG_REV\" != \"$LATEST_COMMIT_REV\" ]; then\n    echo \"$LATEST_TAG+$(git rev-list HEAD --max-count=1 --abbrev-commit)\"\nelse\n    echo \"$LATEST_TAG\"\nfi"
  },
  {
    "path": ".github/workflows/docs.yaml",
    "content": "name: Publish docs via GitHub Pages\non:\n  push:\n    branches:\n      - master\njobs:\n  build:\n    name: Deploy docs\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Deploy docs\n        uses: mhausenblas/mkdocs-deploy-gh-pages@master\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          CONFIG_FILE: docs/mkdocs.yml\n          EXTRA_PACKAGES: build-base\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Publish Pypi\non:\n  push:\n    tags:\n      - 'v*.*.*'\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - name: Build and publish to pypi\n      uses: JRubics/poetry-publish@v1.17\n      with:\n        pypi_token: ${{ secrets.PYPI_TOKEN }}\n\n    - name: Generate Changelog\n      run: |\n        VERSION=$(.github/hack/version.sh)\n        .github/hack/changelog.sh $VERSION > NEW-VERSION-CHANGELOG.md\n    - name: Publish\n      uses: softprops/action-gh-release@v1\n      with:\n        body_path: NEW-VERSION-CHANGELOG.md\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "name: Test\n\non:\n  pull_request:\n  push:\n    branches:\n      - master\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']\n        include:\n          - python-version: '3.12'\n            update-coverage: true\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Cache pip\n        uses: actions/cache@v4\n        with:\n          path: ~/.cache/pip\n          key: ${{ matrix.python-version }}-poetry-${{ hashFiles('pyproject.toml') }}\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip poetry\n          poetry install\n      - name: Test with pytest\n        run: |\n          poetry run pytest\n      - name: Upload coverage to Codecov\n        if: ${{ matrix.update-coverage }}\n        uses: codecov/codecov-action@v5\n        with:\n          file: ./coverage.xml\n          fail_ci_if_error: true\n          token: ${{ secrets.CODECOV_TOKEN }}\n\n  lint:\n    name: black\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n      - name: Cache pip\n        uses: actions/cache@v4\n        with:\n          path: ~/.cache/pip\n          key: lintenv-v2\n      - name: Install dependencies\n        run: python -m pip install --upgrade pip black\n      - name: Black test\n        run: make lint-check\n"
  },
  {
    "path": ".gitignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n### Python template\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\npoetry.lock\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n\n# PyCharm\n.idea/\n\n# for git commitizen\nnode_modules/\npackage-lock.json\npackage.json\n\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## Version 0.9.9 (2026-04-17)\n\n### What's New\n\n- Add new part field `paidProductPlacementDetail` for video resource. Thanks for [@sarod](https://github.com/sarod)\n- Add new method to subscribe or unsubscribe to a YouTube channel's push notifications via PubSubHubbub.\n- Upgrade to python 3.9\n\n## Version 0.9.8 (2025-08-22)\n\n### What's New\n\n- Fix dependencies and update docs. Thanks for [@sagarvora](https://github.com/sagarvora)\n\n## Version 0.9.7 (2024-10-28)\n\n### What's New\n\n- Fix dependencies.\n\n## Version 0.9.6 (2024-09-09)\n\n### What's New\n\n-Add new part field `recordingDetails` for video resource. Thanks for [@vmx](https://github.com/vmx)\n\n## Version 0.9.5 (2024-08-09)\n\n### What's New\n\n- Make video regionRestriction fields to Optional. Thanks for [@pidi3000](https://github.com/pidi3000)\n- Modify some examples. Thanks for [@pidi3000](https://github.com/pidi3000)\n- fix enf_parts for part with whitespaces. Thanks for [@pidi3000](https://github.com/pidi3000)\n\n## Version 0.9.4 (2024-02-18)\n\n### What's New\n\n- Add new parameter `for_handle` to get channel by handle.  \n- fix some wrong error message.\n\n## Version 0.9.3 (2023-11-22)\n\n### What's New\n\n- Add initial client with client_secret file. Thanks for [@pidi3000](https://github.com/pidi3000)\n\n## Version 0.9.2 (2023-09-26)\n\n### What's New\n\n- Add new parameter for search method\n- Mark some parameter or method to be deprecated.\n\n## Version 0.9.1 (2023-07-19)\n\n### What's New\n\n- upgrade poetry. Thanks for [@blaggacao](https://github.com/blaggacao)\n\n## Version 0.9.0 (2022-12-26)\n\n### What's New\n\n- Introduce new `Client` to operate YouTube DATA API. [#120](https://github.com/sns-sdks/python-youtube/issues/120).\n- More example to show library usage.\n\n## Version 0.8.3 (2022-10-17)\n\n### What's New\n\n- Add parts for video, thanks for [@Omer](https://github.com/dusking)\n\n## Version 0.8.2 (2022-03-16)\n\n### What's New\n\n- Update OAuthorize functions.\n- Update for examples.\n\n## Version 0.8.1 (2021-05-14)\n\n### Deprecation\n\nDetail at: https://developers.google.com/youtube/v3/revision_history#may-12,-2021\n\n- Remove channel resource in brandingSettings for channel.\n- Remove localizations,targeting resource and some snippet resource for channelSection.\n- Remove tags in snippet for playlist. \n\n### Broken Change\n\nMethods `get_channel_sections_by_channel`, `get_channel_section_by_id` has remove parameter `hl`.\n\n\n## Version 0.8.0\n\n### Broken Change\n\nModify the auth flow methods.\n\n### What's New\n\n1. add python3.9 tests\n2. New docs\n\n\n## Version 0.7.0\n\n### What's New\n\n1. Add api methods for members and membership levels\n2. Add more examples for api\n3. Add fields for playlist item api\n4. fix some.\n\n\n## Version 0.6.1\n\n### What's New\n\nRemove deprecated api.\n\n\n## Version 0.6.0\n\n### What's New\n\nProvide remain get apis. like activities, captions, channel_sections, i18n, video_abuse_report_reason, search resource and so on.\n\nYou can see the `README`_ to get more detail for those api.\n\n\n## Version 0.5.3\n\n### What's New\n\nProvide the page token parameter to skip data have retrieved.\n\nThis for follow api methods\n\n```python\napi.get_playlists()\napi.get_playlist_items()\napi.get_videos_by_chart()\napi.get_videos_by_myrating()\napi.get_comment_threads()\napi.get_comments()\napi.get_subscription_by_channel()\napi.get_subscription_by_me()\n```\n\nexample\n\n```\nIn[1]: r = api.get_subscription_by_channel(channel_id=\"UCAuUUnT6oDeKwE6v1NGQxug\", limit=5, count=None, page_token=\"CAUQAA\")\nIn[2]:r.prevPageToken\nOut[2]: 'CAUQAQ'\n```\n\n\n## Version 0.5.2\n\n### What's New\n\nNow you can use authorized access token to get your subscriptions.\nYou 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.\nOr you can see the [subscriptions usage](https://github.com/sns-sdks/python-youtube/blob/master/README.rst#subscriptions) docs.\n\n    #43 add api for get my subscriptions\n\n    #41 add api for channel subscriptions\n\n\n\n## Version 0.5.1\n\n### What's New\n\nNow some apis can get all target items just by one method call.\n\nFor example, you can get playlist's all items by follow call\n\n```\nIn [1]: r = api.get_playlist_items(playlist_id=\"PLWz5rJ2EKKc_xXXubDti2eRnIKU0p7wHd\", parts=[\"id\", \"snippet\"], count=None)\nIn [2]: r.pageInfo\nOut[2]: PageInfo(totalResults=73, resultsPerPage=50)\nIn [3]: len(r.items)\nOut[4]: 73\n```\n\nYou can see the [README](https://github.com/sns-sdks/python-youtube/blob/master/README.rst) to find which methods support this.\n\n## Version 0.5.0\n\n### **Broken Change**\n\nNow introduce new model ApiResponse representing the response from youtube, so previous usage has been invalidated.\n\nYou need to read the docs [README](https://github.com/sns-sdks/python-youtube/blob/master/README.rst) to get the simple new usage.\n\n### What's New\n\nSplit some method into multiple usage, for example get video has been split three methods:\n\n* api.get_video_by_id()\n* api.get_videos_by_chart()\n* api.get_videos_by_myrating()\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 sns-sdks\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "all: help clean lint test\n\n.PHONY: all\n\nhelp:\n\t@echo \"  env         install all dependencies\"\n\t@echo \"  clean       remove unwanted stuff\"\n\t@echo \"  docs        build documentation\"\n\t@echo \"  lint        check style with black\"\n\t@echo \"  test        run tests with cov\"\n\nenv:\n\tpip install --upgrade pip\n\tpip install poetry\n\tpoetry install\n\nclean: clean-build clean-pyc clean-test\n\nclean-build:\n\trm -fr build/\n\trm -fr dist/\n\trm -fr .eggs/\n\tfind . -name '*.egg-info' -exec rm -fr {} +\n\tfind . -name '*.egg' -exec rm -f {} +\n\nclean-pyc:\n\tfind . -name '*.pyc' -exec rm -f {} +\n\tfind . -name '*.pyo' -exec rm -f {} +\n\tfind . -name '*~' -exec rm -f {} +\n\tfind . -name '__pycache__' -exec rm -fr {} +\n\nclean-test:\n\trm -fr .pytest_cache\n\trm -f .coverage\n\trm -fr htmlcov/\n\ndocs:\n\t$(MAKE) -C docs html\n\nlint:\n\tblack .\n\nlint-check:\n\tblack --check .\n\ntest:\n\tpytest -s\n\ntests-html:\n\tpytest -s --cov-report term --cov-report html\n\n# v0.1.0 -> v0.2.0\nbump-minor:\n\tbump2version minor\n\n# v0.1.0 -> v0.1.1\nbump-patch:\n\tbump2version patch\n"
  },
  {
    "path": "README.rst",
    "content": "Python YouTube\n\nA Python wrapper for the YouTube Data API V3.\n\n.. image:: https://github.com/sns-sdks/python-youtube/workflows/Test/badge.svg\n    :target: https://github.com/sns-sdks/python-youtube/actions\n\n.. image:: https://img.shields.io/badge/Docs-passing-brightgreen\n    :target: https://sns-sdks.github.io/python-youtube/\n    :alt: Documentation Status\n\n.. image:: https://codecov.io/gh/sns-sdks/python-youtube/branch/master/graph/badge.svg\n    :target: https://codecov.io/gh/sns-sdks/python-youtube\n\n.. image:: https://img.shields.io/pypi/v/python-youtube.svg\n    :target: https://img.shields.io/pypi/v/python-youtube\n\n======\nTHANKS\n======\n\nInspired by `Python-Twitter <https://github.com/bear/python-twitter>`_.\n\nThanks a lot to Python-Twitter Developers.\n\n============\nIntroduction\n============\n\nThis library provides an easy way to use the YouTube Data API V3.\n\n.. \n\n    We have recently been working on the new structure for the library. `Read docs <docs/docs/introduce-new-structure.md>`_ to get more detail.\n\n=============\nDocumentation\n=============\n\nYou can view the latest ``python-youtube`` documentation at: https://sns-sdks.github.io/python-youtube/.\n\nAlso view the full ``YouTube DATA API`` docs at: https://developers.google.com/youtube/v3/docs/.\n\n==========\nInstalling\n==========\n\nYou can install this lib from PyPI:\n\n.. code:: shell\n\n    pip install --upgrade python-youtube\n    # ✨🍰✨\n\n=====\nUsing\n=====\n\nThe library covers all resource methods, including ``insert``,``update``, and so on.\n\nWe recommend using the ``pyyoutube.Client`` to operate DATA API. It is more modern and feature rich than ``pyyoutube.Api``.\n\nWork with Client\n----------------\n\nYou can initialize with an api key:\n\n.. code-block:: python\n\n    >>> from pyyoutube import Client\n    >>> client = Client(api_key=\"your api key\")\n\nTo access additional data that requires authorization, you need to initialize with an access token:\n\n.. code-block:: python\n\n    >>> from pyyoutube import Client\n    >>> client = Client(access_token='your access token')\n\nYou can read the docs to see how to get an access token.\n\nOr you can ask for user to do OAuth:\n\n.. code-block:: python\n\n    >>> from pyyoutube import Client\n    >>> client = Client(client_id=\"client key\", client_secret=\"client secret\")\n\n    >>> client.get_authorize_url()\n    ('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')\n\n    >>> client.generate_access_token(authorization_response=\"link for response\")\n    AccessToken(access_token='token', expires_in=3599, token_type='Bearer')\n\nNow you can use the instance to get data from YouTube.\n\nGet channel detail:\n\n    >>> cli.channels.list(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n    ChannelListResponse(kind='youtube#channelListResponse')\n    >>> cli.channels.list(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\", return_json=True)\n    {'kind': 'youtube#channelListResponse',\n     'etag': 'eHSYpB_FqHX8vJiGi_sLCu0jkmE',\n    ...\n    }\n\nSee the `client docs <docs/docs/usage/work-with-client.md>`_, or `client examples <examples/clients>`_, for additional usage\n\nWork with API\n----------------\n\n..\n\n    For compatibility with older code, we continue to support the old way.\n\nYou can just initialize with an api key:\n\n.. code-block:: python\n\n    >>> from pyyoutube import Api\n    >>> api = Api(api_key=\"your api key\")\n\nTo access additional data that requires authorization, you need to initialize with an access token:\n\n.. code-block:: python\n\n    >>> from pyyoutube import Api\n    >>> api = Api(access_token='your access token')\n\nYou can read the docs to see how to get an access token.\n\nOr you can ask for user to do OAuth flow:\n\n.. code-block:: python\n\n    >>> from pyyoutube import Api\n    >>> api = Api(client_id=\"client key\", client_secret=\"client secret\")\n    # Get authorization url\n    >>> api.get_authorization_url()\n    ('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')\n    # user to do\n    # copy the response url\n    >>> api.generate_access_token(authorization_response=\"link for response\")\n    AccessToken(access_token='token', expires_in=3599, token_type='Bearer')\n\nNow you can use the instance to get data from YouTube.\n\nGet channel detail:\n\n.. code-block:: python\n\n    >>> channel_by_id = api.get_channel_info(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n    >>> channel_by_id.items\n    [Channel(kind='youtube#channel', id='UC_x5XG1OV2P6uZZ5FSM9Ttw')]\n    >>> channel_by_id.items[0].to_dict()\n    {'kind': 'youtube#channel',\n     'etag': '\"j6xRRd8dTPVVptg711_CSPADRfg/AW8QEqbNRoIJv9KuzCIg0CG6aJA\"',\n     'id': 'UC_x5XG1OV2P6uZZ5FSM9Ttw',\n     'snippet': {'title': 'Google Developers',\n      'description': 'The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.',\n      'customUrl': 'googlecode',\n      'publishedAt': '2007-08-23T00:34:43.000Z',\n      'thumbnails': {'default': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo',\n        'width': 88,\n        'height': 88},\n       'medium': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo',\n        'width': 240,\n        'height': 240},\n       'high': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo',\n        'width': 800,\n        'height': 800},\n       'standard': None,\n       'maxres': None},\n      'defaultLanguage': None,\n      'localized': {'title': 'Google Developers',\n       'description': 'The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.'},\n      'country': 'US'},\n      ...\n      }\n      # Get json response from youtube\n      >>> api.get_channel_info(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\", return_json=True)\n      {'kind': 'youtube#channelListResponse',\n        'etag': '17FOkdjp-_FPTiIJXdawBS4jWtc',\n        ...\n       }\n\nSee the `api docs <docs/docs/usage/work-with-api.md>`_, or `api examples <examples/apis>`_, for additional usage.\n"
  },
  {
    "path": "docs/docs/authorization.md",
    "content": "If you want to get more data for your channel, You need provide the authorization.\n\nThis doc shows how to authorize a client.\n\n## Prerequisite\n\nTo begin with, you must know what authorization is.\n\nYou can see some information at the [Official Documentation](https://developers.google.com/youtube/v3/guides/authentication).\n\nYou 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.\n\nOnce complete, you will be able to do a simple authorize with `Python-Youtube` library.\n\n## Get authorization url\n\nSuppose now we want to get user's permission to manage their YouTube account.\n\nFor the `Python-YouTube` library, the default scopes are:\n\n- https://www.googleapis.com/auth/youtube\n- https://www.googleapis.com/auth/userinfo.profile\n\nYou can get more scope information at [Access scopes](https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#identify-access-scopes).\n\n(The defailt redirect URI used in PyYoutube is `https://localhost/`)\n\nWe can now perform the following steps:\n\nInitialize the api instance with your app credentials\n\n```\nIn [1]: from pyyoutube import Client\n\nIn [2]: cli = Client(client_id=\"you client id\", client_secret=\"you client secret\")\n\nIn [3]: cli.get_authorize_url()\nOut[3]:\n('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',\n 'PyYouTube')\n```\n\nOpen your broswer of choice and copy the link returned by `get_authorize_url()` into the searchbar.\n\n## Do authorization\n\nOn entering the URL, you will see the following:\n\n![auth-1-chose-account](images/auth-1-chose-account.png)\n\nSelect the account to authorize your app to read data from.\n\nIf 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.\nOtherwise, you will see the following:\n\n![auth-2-not-approval](images/auth-2-not-approval.png)\n\nYou will need to click ``Advanced``, then click the ``Go to Python-YouTube (unsafe)``.\n\n![auth-3-advanced](images/auth-3-advanced.png)\n\nYou should now see a window to select permissions granted to the application.\n\n![auth-4-allow-permission](images/auth-4-allow-permission.png)\n\nClick `allow` to give the permission.\n\nYou 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!\n\n## Retrieve access token\n\nCopy the full redicted URL from the browser address bar, and return to your original console.\n\n```\nIn [4]: token = cli.generate_access_token(authorization_response=\"$redirect_url\")\n\nIn [5]: token\nOut[5]: AccessToken(access_token='access token', expires_in=3600, token_type='Bearer')\n```\n    \n(Replace `$redirect_url` with the URL you copied)\n\nYou now have an access token to view your account data.\n\n\n## Get your data\n\nFor example, you can get your playlists.\n\n```\nIn [6]: playlists = cli.playlists.list(mine=True)\n\nIn [7]: playlists.items\nOut[7]:\n[Playlist(kind='youtube#playlist', id='PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS'),\n Playlist(kind='youtube#playlist', id='PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g')]\n```\n\n!!! note \"Tips\"\n\n    If you are confused, it is beneficial to read the [Authorize Requests](https://developers.google.com/youtube/v3/guides/authentication) guide first.\n"
  },
  {
    "path": "docs/docs/getting_started.md",
    "content": "This document is a simple tutorial to show how to use this library to get data from YouTube data API.\n\nYou can get the whole description for the YouTube API at [YouTube API Reference](https://developers.google.com/youtube/v3/docs/).\n\n## Prerequisite\n\nTo begin, you need to create a [Google Project](https://console.cloud.google.com) with your google account.\n\nEvery new account has a free quota of 12 projects.\n\n## Create your project\n\nClick `Select a project-> NEW PROJECT` to create a new project to use our library.\n\nFill in the basic info and create the project.\n\n![gt-create-app-1](images/gt-create-app-1.png)\n\n## Enable YouTube DATA API service\n\nOnce the project created, the browser will redirect you to the project home page.\n\nClick the `≡≡` symbol on the top left and select the `APIs & Services` tab.\n\nYou will see following info:\n\n![gt-create-app-2](images/gt-create-app-2.png)\n\nClick the `+ ENABLE APIS AND SERVICES` symbol, and input `YouTube DATA API` to search.\n\n![gt-create-app-3](images/gt-create-app-3.png)\n\nChose the ``YouTube DATA API`` item.\n\n![gt-create-app-4](images/gt-create-app-4.png)\n\nThen click the `ENABLE` blue button. After a short period where the API is added to your project, the service will be activated.\n\n## Create credentials\n\nTo use this API, you need credentials. Click `Create credentials` to get started.\n\n![gt-create-app-5](images/gt-create-app-5.png)\n\nYou need to fill in some information to create credentials.\n\nJust chose `YouTube DATA API v3`, `Other non-UI (e.g. cron job, daemon)` and `Public data`.\n\nThen click the blue button `What credentials do I need?` to create.\n\n![gt-create-app-6](images/gt-create-app-6.png)\n\nYou have now generated an api key.\n\nUsing this key, you can retrieve public YouTube data with our library\n\n```python\nfrom pyyoutube import Client\n\ncli = Client(api_key=\"your api key\")\n```\n\nCheck out the [examples](https://github.com/sns-sdks/python-youtube/tree/master/examples) directory for some examples of using the library.\n\nIf you have an open source application using python-youtube, send me a link. I am very happy to add a link to it here.\n\nIf you want to get user data by OAuth. You need create the credential for ``OAuth client ID``.\n\nYou will find more information on OAth at the [Authorization](authorization.md) page.\n"
  },
  {
    "path": "docs/docs/index.md",
    "content": "# Welcome to Python-Youtube's documentation!\n\n**A Python wrapper around for YouTube Data API.**\n\nAuthor: IkarosKun <merle.liukun@gmail.com>\n\n## Introduction\n\n\nWith the YouTube Data API, you can add a variety of YouTube features to your application. \n\nUse the API to upload videos, manage playlists and subscriptions, update channel settings, and more.\n\nThis library provides a Python interface for the [YouTube DATA API](https://developers.google.com/youtube/v3).\n\nThis library has works on all Python versions 3.6 and newer.\n\n!!! tip \"Tips\"\n\n    This library only supports `DATA API`, It does not support `Analytics and Reporting APIs` and `Live Streaming API`.\n"
  },
  {
    "path": "docs/docs/installation.md",
    "content": "This library supports Python 3.6 and newer.\n\n## Dependencies\n\nThese following distributions will be installed automatically when installing Python-Youtube.\n\n- [requests](https://2.python-requests.org/en/master/): is an elegant and simple HTTP library for Python, built for human beings.\n- [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.\n- [isodate](https://pypi.org/project/isodate/): implements ISO 8601 date, time and duration parsing.\n\n## Installation\n\nYou can install this library from **PyPI**\n\n```shell\n$ pip install --upgrade python-youtube\n```\n\n\nYou can also build this library from source\n\n```shell\n$ git clone https://github.com/sns-sdks/python-youtube.git\n$ cd python-youtube\n$ make env\n$ make build\n```\n\n## Testing\n\nRun `make env` after you have installed the project requirements.\nOnce completed, you can run code tests with\n\n```shell\n$ make tests-html\n```\n"
  },
  {
    "path": "docs/docs/introduce-new-structure.md",
    "content": "This doc will show you the new api structure for this library.\n\n## Brief\n\nTo 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.\n\n![structure-uml](images/structure-uml.png)\n\n\nIn this structure, every resource has a self class.\n\n## Simple usage\n\n\n### Initial Client\n\n```python\nfrom pyyoutube import Client\n\nclient = Client(api_key=\"your api key\")\n```\n\n### Get data.\n\nfor example to get channel data.\n\n```python\nresp = client.channels.list(\n    parts=[\"id\", \"snippet\"],\n    channel_id=\"UCa-vrCLQHviTOVnEKDOdetQ\"    \n)\n# resp output\n# ChannelListResponse(kind='youtube#channelListResponse')\n# resp.items[0].id  output\n# UCa-vrCLQHviTOVnEKDOdetQ\n```\n"
  },
  {
    "path": "docs/docs/usage/work-with-api.md",
    "content": "# Work with Api\n\n!!! note \"Tips\"\n\n    This is the previous version to operate YouTube DATA API.\n\n    We recommend using the latest version of methods to operate YouTube DATA API.\n\nThe API is exposed via the ``pyyoutube.Api`` class.\n\n## INSTANTIATE\n\nWe provide two method to create instances of the ``pyyoutube.Api``.\n\nYou can just initialize with an api key.\n\n```\n>>> from pyyoutube import Api\n\n>>> api = Api(api_key=\"your api key\")\n```\n\nIf you want to get authorization data, you will need to initialize with an access token.\n\n```\n>>> from pyyoutube import Api\n\n>>> api = Api(access_token='your api key')\n```\n\nYou can read the docs to see how to get an access token.\n\nOr you can ask for the user to do oauth flow:\n\n```\n>>> from pyyoutube import Api\n\n>>> api = Api(client_id=\"client key\", client_secret=\"client secret\")\n# Get authorization url\n>>> api.get_authorization_url()\n# ('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')\n# user to do\n# copy the response url\n>>> api.generate_access_token(authorization_response=\"link for response\")\n# AccessToken(access_token='token', expires_in=3599, token_type='Bearer')\n```\n\n## Usage\n\nNow you can use the instance to get data from YouTube.\n\n### CHANNEL DATA\n\nThe library provides several ways to get a channels data.\n\nIf a channel is not found, the property ``items`` will return an empty list.\n\nYou can use channel id:\n\n```\n>>> channel_by_id = api.get_channel_info(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n>>> channel_by_id.items\n[Channel(kind='youtube#channel', id='UC_x5XG1OV2P6uZZ5FSM9Ttw')]\n>>> channel_by_id.items[0].to_dict()\n{'kind': 'youtube#channel',\n 'etag': '\"j6xRRd8dTPVVptg711_CSPADRfg/AW8QEqbNRoIJv9KuzCIg0CG6aJA\"',\n 'id': 'UC_x5XG1OV2P6uZZ5FSM9Ttw',\n 'snippet': {'title': 'Google Developers',\n  'description': 'The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.',\n  'customUrl': 'googlecode',\n  'publishedAt': '2007-08-23T00:34:43.000Z',\n  'thumbnails': {'default': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo',\n    'width': 88,\n    'height': 88},\n   'medium': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo',\n    'width': 240,\n    'height': 240},\n   'high': {'url': 'https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo',\n    'width': 800,\n    'height': 800},\n   'standard': None,\n   'maxres': None},\n  'defaultLanguage': None,\n  'localized': {'title': 'Google Developers',\n   'description': 'The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.'},\n  'country': 'US'},\n  ...\n  }\n```\n\nTo get multiple channels, you can pass any of: a string containing comma-seperated ids; or an enumarable (list, tuple, or set) of ids\n\nMany other methods also provide this functionality.\n\nwith ids:\n\n```\n>>> channel_by_ids = api.get_channel_info(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw,UCa-vrCLQHviTOVnEKDOdetQ\")\n>>> channel_by_ids.items\n[Channel(kind='youtube#channel', id='UC_x5XG1OV2P6uZZ5FSM9Ttw'),\n Channel(kind='youtube#channel', id='UCa-vrCLQHviTOVnEKDOdetQ')]\n```\n\nYou can also use a channel name:\n\n```\n>>> channel_by_username = api.get_channel_info(for_username=\"GoogleDevelopers\")\n>>> channel_by_username.items[0]\nChannel(kind='youtube#channel', id='UC_x5XG1OV2P6uZZ5FSM9Ttw')\n```\n\nIf you have authorized your client, you can get your channels directly:\n\n```\n>>> channel_by_mine = api_with_authorization.get_channel_info(mine=True)\n>>> channel_by_mine.items[0]\nChannel(kind='youtube#channel', id='UCa-vrCLQHviTOVnEKDOdetQ')\n```\n\n!!! note \"Tips\"\n\n    To get your channel, you must do authorization first, otherwise you will get an error.\n\n### PLAYLIST\n\nThere are methods to get playlists by playlist id, channel id, or get your own playlists.\n\nGet playlists by id:\n\n```\n>>> playlists_by_id = api.get_playlist_by_id(playlist_id=\"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\")\n>>> playlists_by_id.items\n[Playlist(kind='youtube#playlist', id='PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw')]\n```\n\nGet playlists by channel (If you want to get all playlists for the target channels, provide the\nparameter `count=None`):\n\n```\n>>> playlists_by_channel = api.get_playlists(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n>>> playlists_by_channel.items\n[Playlist(kind='youtube#playlist', id='PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw'),\n Playlist(kind='youtube#playlist', id='PLOU2XLYxmsIJO83u2UmyC8ud41AvUnhgj'),\n Playlist(kind='youtube#playlist', id='PLOU2XLYxmsILfV1LiUhDjbh1jkFjQWrYB'),\n Playlist(kind='youtube#playlist', id='PLOU2XLYxmsIKNr3Wfhm8o0TSojW7hEPPY'),\n Playlist(kind='youtube#playlist', id='PLOU2XLYxmsIJ8ItHmK4bRlY4GCzMgXLAJ')]\n```\n\nGet your playlists (this requires authorization):\n\n```\n>>> playlists_by_mine = api.get_playlists(mine=True)\n```\n\n### PLAYLIST ITEM\n\nSimilarly, you can get playlist items by playlist item id or playlist id.\n\nGet playlist items by id:\n\n```\n>>> playlist_item_by_id = api.get_playlist_item_by_id(playlist_item_id=\"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA\"\n...     \"1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2\")\n\n>>> playlist_item_by_id.items\n[PlaylistItem(kind='youtube#playlistItem', id='UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2')]\n```\n\nGet playlist items by playlist id (If you want to get return all items in a playlist, provide the\nparameter `count=None`):\n\n```\n>>> playlist_item_by_playlist = api.get_playlist_items(playlist_id=\"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\", count=2)\n\n>>> playlist_item_by_playlist.items\n[PlaylistItem(kind='youtube#playlistItem', id='UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2'),\n PlaylistItem(kind='youtube#playlistItem', id='UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy4yODlGNEE0NkRGMEEzMEQy')]\n>>> playlist_item_by_id.items[0].snippet.resourceId\nResourceId(kind='youtube#video', videoId='CvTApw9X8aA')\n```\n\n### VIDEO\n\nYou can get a video's information by several methods.\n\nGet videos by video id(s):\n\n```\n>>> video_by_id = api.get_video_by_id(video_id=\"CvTApw9X8aA\")\n\n>>> video_by_id\nVideoListResponse(kind='youtube#videoListResponse')\n\n>>> video_by_id.items\n[Video(kind='youtube#video', id='CvTApw9X8aA')]\n```\n\nGet videos by chart (If you want to get all videos, just provide the parameter `count=None`):\n\n```\n>>> video_by_chart = api.get_videos_by_chart(chart=\"mostPopular\", region_code=\"US\", count=2)\n\n>>> video_by_chart.items\n[Video(kind='youtube#video', id='RwnN2FVaHmw'),\n Video(kind='youtube#video', id='hDeuSfo_Ys0')]\n```\n\nGet videos by your rating (this requires authorization. If you also want to get all videos, provide the\nparameter `count=None`):\n\n```\n>>> videos_by_rating = api.get_videos_by_myrating(rating=\"like\", count=2)\n```\n\n### COMMENT THREAD\n\nYou can get comment thread information by id or by a filter.\n\nGet comment thread by id(s):\n\n```\n>>> ct_by_id = api.get_comment_thread_by_id(comment_thread_id='Ugz097FRhsQy5CVhAjp4AaABAg,UgzhytyP79_Pwa\n... Dd4UB4AaABAg')\n\n>>> ct_by_id.items\n[CommentThread(kind='youtube#commentThread', id='Ugz097FRhsQy5CVhAjp4AaABAg'),\n CommentThread(kind='youtube#commentThread', id='UgzhytyP79_PwaDd4UB4AaABAg')]\n```\n\nGet all comment threads related to a channel (including comment threads for the channel's video. If you want to get\nall comment threads, provide the parameter `count=None`):\n\n```\n>>> ct_by_all = api.get_comment_threads(all_to_channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\", count=2)\n\n>>> ct_by_all.items\n[CommentThread(kind='youtube#commentThread', id='UgwlB_Cza9WtzUWahYN4AaABAg'),\n CommentThread(kind='youtube#commentThread', id='UgyvoQJ2LsxCBwGEpMB4AaABAg')]\n```\n\nGet comment threads only for the channel (If you want to get all comment threads, provide the\nparameter `count=None`):\n\n```\n>>> ct_by_channel = api.get_comment_threads(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\", count=2)\n\n>>> ct_by_channel.items\n[CommentThread(kind='youtube#commentThread', id='UgyUBI0HsgL9emxcZpR4AaABAg'),\n CommentThread(kind='youtube#commentThread', id='Ugzi3lkqDPfIOirGFLh4AaABAg')]\n```\n\nGet comment threads only for the video (If you want to get all comment threads, provide the\nparameter `count=None`):\n\n```\n>>> ct_by_video = api.get_comment_threads(video_id=\"D-lhorsDlUQ\", count=2)\n\n>>> ct_by_video.items\n[CommentThread(kind='youtube#commentThread', id='UgydxWWoeA7F1OdqypJ4AaABAg'),\n CommentThread(kind='youtube#commentThread', id='UgxKREWxIgDrw8w2e_Z4AaABAg')]\n```\n\n### COMMENT\n\nYou can get comment information by id or use the top-level comment id to get replies.\n\n!!! note \"Tips\"\n\n    The reply has the same structure as a comment.\n\nGet comments by id(s):\n\n```\n>>> comment_by_id = api.get_comment_by_id(comment_id='UgxKREWxIgDrw8w2e_Z4AaABAg,UgyrVQaFfEdvaSzstj14AaABAg')\n\n>>> comment_by_id.items\n[Comment(kind='youtube#comment', id='UgxKREWxIgDrw8w2e_Z4AaABAg', snippet=CommentSnippet(authorDisplayName='Hieu Nguyen', likeCount=0)),\n Comment(kind='youtube#comment', id='UgyrVQaFfEdvaSzstj14AaABAg', snippet=CommentSnippet(authorDisplayName='Mani Kanta', likeCount=0))]\n```\n\nGet replies by comment id (If you want to get all comments, just provide the parameter `count=None`):\n\n```\n>>> comment_by_parent = api.get_comments(parent_id=\"UgwYjZXfNCUTKPq9CZp4AaABAg\")\n>>> comment_by_parent.items\n[Comment(kind='youtube#comment', id='UgwYjZXfNCUTKPq9CZp4AaABAg.8yxhlQJogG18yz_cXK9Kcj', snippet=CommentSnippet(authorDisplayName='Marlon López', likeCount=0))]\n```\n\n### VIDEO CATEGORY\n\nYou can get video category with id or region.\n\nGet video categories with id(s):\n\n```\n>>> video_category_by_id = api.get_video_categories(category_id=\"17,18\")\n\n>>> video_category_by_id.items\n[VideoCategory(kind='youtube#videoCategory', id='17'),\n VideoCategory(kind='youtube#videoCategory', id='18')]\n```\n\nGet video categories with region code:\n\n```\n>>> video_categories_by_region = api.get_video_categories(region_code=\"US\")\n\n>>> video_categories_by_region.items\n[VideoCategory(kind='youtube#videoCategory', id='1'),\n VideoCategory(kind='youtube#videoCategory', id='2'),\n VideoCategory(kind='youtube#videoCategory', id='10'),\n VideoCategory(kind='youtube#videoCategory', id='15'),\n ...]\n```\n\n### SUBSCRIPTIONS\n\nYou can get subscription information by id, by point channel, or your own.\n\n!!! note \"Tips\"\n\n    If you want to get the non-public subscriptions, you need to authorize and obtain the access token first.\n    See the demo [A demo for get my subscription](examples/subscription.py).\n\nTo get subscription info by id(s), your token needs to have the permission for the subscriptions belonging to a\nchannel or user:\n\n```\n>>> r = api.get_subscription_by_id(\n...         subscription_id=[\n...             \"zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo\",\n...             \"zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo\"])\n>>> r\nSubscriptionListResponse(kind='youtube#subscriptionListResponse')\n>>> r.items\n[Subscription(kind='youtube#subscription', id='zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo', snippet=SubscriptionSnippet(title='PyCon 2015', description='')),\n Subscription(kind='youtube#subscription', id='zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo', snippet=SubscriptionSnippet(title='ikaros-life', description='This is a test channel.'))]\n```\n\nGet your own subscriptions, you need to authorize first, and supply the token:\n\n```\n>>> r = api.get_subscription_by_me(\n...         mine=True,\n...         parts=[\"id\", \"snippet\"],\n...         count=2\n... )\n>>> r\nSubscriptionListResponse(kind='youtube#subscriptionListResponse')\n>>> r.items\n[Subscription(kind='youtube#subscription', id='zqShTXi-2-Tx7TtwQqhCBwtJ-Aho6DZeutqZiP4Q79Q', snippet=SubscriptionSnippet(title='Next Day Video', description='')),\n Subscription(kind='youtube#subscription', id='zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo', snippet=SubscriptionSnippet(title='PyCon 2015', description=''))]\n```\n\nGet public channel subscriptions:\n\n```\n>>> r = api.get_subscription_by_channel(\n...      channel_id=\"UCAuUUnT6oDeKwE6v1NGQxug\",\n...      parts=\"id,snippet\",\n...      count=2\n... )\n>>> r\nSubscriptionListResponse(kind='youtube#subscriptionListResponse')\n>>> r.items\n[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.\")),\n 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.'))]\n```\n\n### ACTIVITIES\n\nYou can get activities by channel id. You can also get your own activities after you have completed authorization.\n\nGet public channel activities:\n\n```\n>>> r = api.get_activities_by_channel(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\", count=2)\n>>> r\nActivityListResponse(kind='youtube#activityListResponse')\n>>> r.items\n[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')),\n 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'))]\n```\n\nGet your activities:\n\n```\n>>> r = api_with_token.get_activities_by_me()\n>>> r.items\n[Activity(kind='youtube#activity', id='MTUxNTc0OTk2MjI3NDE0MjYwMDY1NjAwODA=', snippet=ActivitySnippet(title='华山日出', description='冷冷的山头')),\n Activity(kind='youtube#activity', id='MTUxNTc0OTk1OTAyNDE0MjYwMDY1NTc2NDg=', snippet=ActivitySnippet(title='海上日出', description='美美美'))]\n```\n\nGet your video captions:\n\n```\n>>> r = api.get_captions_by_video(video_id=\"oHR3wURdJ94\", parts=[\"id\", \"snippet\"])\n>>> r\nCaptionListResponse(kind='youtube#captionListResponse')\n>>> r.items\n[Caption(kind='youtube#caption', id='SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I', snippet=CaptionSnippet(videoId='oHR3wURdJ94', lastUpdated='2020-01-14T09:40:49.981Z')),\n Caption(kind='youtube#caption', id='fPMuDm722CIRcUAT3NTPQHQZJZJxt39kU7JvrHk8Kzs=', snippet=CaptionSnippet(videoId='oHR3wURdJ94', lastUpdated='2020-01-14T09:39:46.991Z'))]\n```\n\nIf you already have caption id(s), you can get video caption by id(s):\n\n```\n>>> r = api.get_captions_by_video(video_id=\"oHR3wURdJ94\", parts=[\"id\", \"snippet\"], caption_id=\"SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I\")\n>>> r\nCaptionListResponse(kind='youtube#captionListResponse')\n>>> r.items\n[Caption(kind='youtube#caption', id='SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I', snippet=CaptionSnippet(videoId='oHR3wURdJ94', lastUpdated='2020-01-14T09:40:49.981Z'))]\n```\n\n### CHANNEL SECTIONS\n\nYou can get channel sections by channel id, section id, or your own channel.\n\nGet channel sections by channel id:\n\n```\n>>> r = api.get_channel_sections_by_channel(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n>>>> r\nChannelSectionResponse(kind='youtube#channelSectionListResponse')\n>>> r.items\n[ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE'),\n ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.B8DTd9ZXJqM'),\n ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.MfvRjkWLxgk'),\n ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.fEjJOXRoWwg'),\n ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.PvTmxDBxtLs'),\n ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.pmcIOsL7s98'),\n ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.c3r3vYf9uD0'),\n ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.ZJpkBl-mXfM'),\n ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.9_wU0qhEPR8'),\n ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.npYvuMz0_es')]\n```\n\nGet authorized user's channel sections:\n\n```\n>>> r = api.get_channel_sections_by_channel(mine=True)\n>>> r.items\n[ChannelSection(kind='youtube#channelSection', id='UCa-vrCLQHviTOVnEKDOdetQ.jNQXAC9IVRw'),\n ChannelSection(kind='youtube#channelSection', id='UCa-vrCLQHviTOVnEKDOdetQ.LeAltgu_pbM'),\n ChannelSection(kind='youtube#channelSection', id='UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY')]\n```\n\nGet channel section detail info by id:\n\n```\n>>> r = api.get_channel_section_by_id(section_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE\")\n>>> r\nChannelSectionResponse(kind='youtube#channelSectionListResponse')\n>>> r1.items\n[ChannelSection(kind='youtube#channelSection', id='UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE')]\n```\n\n### I18N RESOURCE\n\nYou can get a list of content regions that the YouTube website supports:\n\n```\n>>> r = api.get_i18n_regions(parts=[\"snippet\"])\n>>> r.items\n[I18nRegion(kind='youtube#i18nRegion', id='DZ', snippet=I18nRegionSnippet(gl='DZ', name='Algeria')),\n I18nRegion(kind='youtube#i18nRegion', id='AR', snippet=I18nRegionSnippet(gl='AR', name='Argentina')),\n I18nRegion(kind='youtube#i18nRegion', id='AU', snippet=I18nRegionSnippet(gl='AU', name='Australia'))\n ...]\n```\n\nYou can get a list of application languages that the YouTube website supports:\n\n```\n>>> r = api.get_i18n_languages(parts=[\"snippet\"])\n>>> r.items\n[I18nLanguage(kind='youtube#i18nLanguage', id='af', snippet=I18nLanguageSnippet(hl='af', name='Afrikaans')),\n I18nLanguage(kind='youtube#i18nLanguage', id='az', snippet=I18nLanguageSnippet(hl='az', name='Azerbaijani')),\n I18nLanguage(kind='youtube#i18nLanguage', id='id', snippet=I18nLanguageSnippet(hl='id', name='Indonesian')),\n ...]\n```\n\n### MEMBER\n\nThe API request must be authorized by the channel owner.\n\nYou can retrieve a list of members (formerly known as \"sponsors\") for a channel:\n\n```\n>>> r = api_with_token.get_members(parts=[\"snippet\"])\n>>> r.items\n[MemberListResponse(kind='youtube#memberListResponse'),\n MemberListResponse(kind='youtube#memberListResponse')]\n```\n\n### MEMBERSHIP LEVEL\n\nThe API request must be authorized by the channel owner.\n\nYou can retrieve a list membership levels for a channel:\n\n```\n>>> r = api_with_token.get_membership_levels(parts=[\"snippet\"])\n>>> r.items\n[MembershipsLevelListResponse(kind='youtube#membershipsLevelListResponse'),\n MembershipsLevelListResponse(kind='youtube#membershipsLevelListResponse')]\n```\n\n### VIDEO ABUSE REPORT REASON\n\nYou can retrieve a list of reasons that can be used to report abusive videos:\n\n```\n>>> r = api_with_token.get_video_abuse_report_reason(parts=[\"snippet\"])\n>>> r.items\n[VideoAbuseReportReason(kind='youtube#videoAbuseReportReason'),\n VideoAbuseReportReason(kind='youtube#videoAbuseReportReason')]\n```\n\n### SEARCH\n\nYou can use those methods to search the video, playlist, or channel data. For more info, you can see\nthe [Search Request Docs](https://developers.google.com/youtube/v3/docs/search/list).\n\nYou can search different type of resource with keywords:\n\n```\n>>> r = api.search_by_keywords(q=\"surfing\", search_type=[\"channel\",\"video\", \"playlist\"], count=5, limit=5)\n>>> r.items\n[SearchResult(kind='youtube#searchResult'),\n SearchResult(kind='youtube#searchResult'),\n SearchResult(kind='youtube#searchResult'),\n SearchResult(kind='youtube#searchResult'),\n SearchResult(kind='youtube#searchResult')]\n```\n\nYou can search your app send videos:\n\n```\n>>> r = api_with_token.search_by_developer(q=\"news\", count=1)\n>>> r.items\n[SearchResult(kind='youtube#searchResult')]\n```\n\nYou can search your videos:\n\n```\n>>> r = api_with_token.search_by_mine(q=\"news\", count=1)\n>>> r.items\n[SearchResult(kind='youtube#searchResult')]\n```\n\nOr you can build your request using the `search` method:\n\n```\n>>> r = api.search(\n...     location=\"21.5922529, -158.1147114\",\n...     location_radius=\"10mi\",\n...     q=\"surfing\",\n...     parts=[\"snippet\"],\n...     count=5,\n...     published_after=\"2020-02-01T00:00:00Z\",\n...     published_before=\"2020-03-01T00:00:00Z\",\n...     safe_search=\"moderate\",\n...     search_type=\"video\")\n>>> r.items\n[SearchResult(kind='youtube#searchResult'),\n SearchResult(kind='youtube#searchResult'),\n SearchResult(kind='youtube#searchResult'),\n SearchResult(kind='youtube#searchResult'),\n SearchResult(kind='youtube#searchResult')]\n\n>>> r = api.search(\n...     event_type=\"live\",\n...     q=\"news\",\n...     count=3,\n...     parts=[\"snippet\"],\n...     search_type=\"video\",\n...     topic_id=\"/m/09s1f\",\n...     order=\"viewCount\")\n>>> r.items\n[SearchResult(kind='youtube#searchResult'),\n SearchResult(kind='youtube#searchResult'),\n SearchResult(kind='youtube#searchResult')]\n```\n"
  },
  {
    "path": "docs/docs/usage/work-with-client.md",
    "content": "# Work with Client\n\nWe have refactored the project code to support more methods and improve code usability.\n\nAnd new structure like follows.\n\n![structure-uml](../images/structure-uml.png)\n\nIn this structure, we identify each entity as a class of resources and perform operations on the resources.\n\n## INSTANTIATE\n\nClient is exposed via the ``pyyoutube.Client`` class.\n\nYou can initialize it with `api key`, to get public data.\n\n```python\nfrom pyyoutube import Client\n\ncli = Client(api_key=\"your api key\")\n```\n\nIf you want to update your channel data. or upload video. You need to initialize with `access token`, or do the auth flow.\n\n```python\nfrom pyyoutube import Client\n\ncli = Client(access_token=\"Access Token with permissions\")\n```\n\n```python\nfrom pyyoutube import Client\n\ncli = Client(client_id=\"ID for app\", client_secret=\"Secret for app\")\n# Get authorization url\ncli.get_authorize_url()\n# ('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')\n# Click url and give permissions.\n# Copy the redirected url.\ncli.generate_access_token(authorization_response=\"redirected url\")\n# AccessToken(access_token='token', expires_in=3599, token_type='Bearer')\n```\n\n### from client_secret\n\nOnly `web` and some `installed` type client_secrets are supported.\n\nThe fields `client_id` and `client_secret` must be set.\n\n`Client.DEFAULT_REDIRECT_URI` will be set the first entry of the field `redirect_uris`.\n\n```python\nfrom pyyoutube import Client\n\nfile_path = \"path/to/client_secret.json\"\ncli = Client(client_secret_path=file_path)\n\n# Then go through auth flow descriped above\n```\n\nOnce initialize to the client, you can operate the API to get data.\n\n## Usage\n\n### Channel Resource\n\nThe API supports the following methods for the `channels` resources:\n\n- list: Returns a collection of zero or more channel resources that match the request criteria.\n- update: Updates a channel's metadata. Note that this method currently only supports updates to the channel resource's\n  brandingSettings and invideoPromotion objects and their child properties\n\n#### List channel data\n\n```python\nresp = cli.channels.list(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n# ChannelListResponse(kind='youtube#channelListResponse')\nprint(resp.items)\n# [Channel(kind='youtube#channel', id='UC_x5XG1OV2P6uZZ5FSM9Ttw')]\n```\n\n#### update channel metadata\n\n```python\nimport pyyoutube.models as mds\n\nbody = mds.Channel(\n    id=\"channel id\",\n    brandingSettings=mds.ChannelBrandingSetting(\n        image=mds.ChannelBrandingSettingImage(\n            bannerExternalUrl=\"new banner url\"\n        )\n    )\n)\n\nchannel = cli.channels.update(\n    part=\"brandingSettings\",\n    body=body\n)\nprint(channel.brandingSettings.image.bannerExternalUrl)\n# 'https://yt3.googleusercontent.com/AegVxoIusdXEmsJ9j3bcJR3zuImOd6TngNw58iJAP0AOAXCnb1xHPcuEDOQC8J85SCZvt5i8A_g'\n```\n\n### Video Resource\n\nThe API supports the following methods for `videos` resources.\n\n#### getRating\n\nRetrieves the ratings that the authorized user gave to a list of specified videos.\n\n```python\nresp = cli.videos.get_rating(video_id=\"Z56Jmr9Z34Q\")\n\nprint(resp.items)\n# [VideoRatingItem(videoId='Z56Jmr9Z34Q', rating='none')]\n```\n\n#### list\n\nReturns a list of videos that match the API request parameters.\n\n```python\nresp = cli.videos.list(video_id=\"Z56Jmr9Z34Q\")\n\nprint(resp.items)\n# [Video(kind='youtube#video', id='Z56Jmr9Z34Q')]\n```\n\n#### insert\n\nUploads a video to YouTube and optionally sets the video's metadata.\n\n```python\nimport pyyoutube.models as mds\nfrom pyyoutube.media import Media\n\nbody = mds.Video(\n    snippet=mds.VideoSnippet(\n        title=\"video title\",\n        description=\"video description\"\n    )\n)\n\nmedia = Media(filename=\"video.mp4\")\n\nupload = cli.videos.insert(\n    body=body,\n    media=media,\n    parts=[\"snippet\"],\n    notify_subscribers=True\n)\n\nvideo_body = None\n\nwhile video_body is None:\n    status, video_body = upload.next_chunk()\n    if status:\n        print(f\"Upload progress: {status.progress()}\")\n\nprint(video_body)\n# {\"kind\": \"youtube#video\", \"etag\": \"17W46NjVxoxtaoh1E6GmbQ2hv5c\",....}\n```\n\n#### update\n\nUpdates a video's metadata.\n\n```python\nimport pyyoutube.models as mds\n\nbody = mds.Video(\n    id=\"fTK1Jj6QlDw\",\n    snippet=mds.VideoSnippet(\n        title=\"What a nice day\",\n        description=\"Blue sky with cloud. updated.\",\n        categoryId=\"1\",\n    )\n)\n\nresp = cli.videos.update(\n    parts=[\"snippet\"],\n    body=body,\n    return_json=True,\n)\nprint(resp)\n# {\"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\"}}\n```\n\n#### delete\n\nDeletes a YouTube video.\n\n```python\ncli.videos.delete(video_id=\"fTK1Jj6QlDw\")\n# True\n```\n\n#### rate\n\nAdd a like or dislike rating to a video or remove a rating from a video.\n\n```python\ncli.videos.rate(video_id=\"fTK1Jj6QlDw\", rating=\"like\")\n# True\n```\n\n#### reportAbuse\n\nReport a video for containing abusive content.\n\n```python\nimport pyyoutube.models as mds\n\nbody = mds.VideoReportAbuse(\n    videoId=\"fTK1Jj6QlDw\",\n    reasonId=\"32\"\n)\ncli.videos.report_abuse(body=body)\n# True\n```\n"
  },
  {
    "path": "docs/mkdocs.yml",
    "content": "site_name: Python-Youtube Docs\nsite_description: Docs for python-youtube library\nsite_url: https://sns-sdks.github.io/python-youtube/\nrepo_url: https://github.com/sns-sdks/python-youtube\ncopyright: Copyright &copy; 2019 - 2021 Ikaros kun\n\n\ntheme:\n  name: material\n  features:\n    - navigation.tabs\n  palette:\n    # Light mode\n    - media: \"(prefers-color-scheme: light)\"\n      scheme: default\n      primary: indigo\n      accent: indigo\n      toggle:\n        icon: material/toggle-switch-off-outline\n        name: Switch to dark mode\n\n    # Dark mode\n    - media: \"(prefers-color-scheme: dark)\"\n      scheme: slate\n      primary: blue\n      accent: blue\n      toggle:\n        icon: material/toggle-switch\n        name: Switch to light mode\n\nnav:\n  - Introduction: index.md\n  - Introduce Structure: introduce-new-structure.md\n  - Usage:\n      - Work With `Api`: usage/work-with-api.md\n      - Work With `Client`: usage/work-with-client.md\n  - Installation: installation.md\n  - Getting Started: getting_started.md\n  - Authorization: authorization.md\n  - Changelog: CHANGELOG.md\n\nextra:\n  social:\n    - icon: fontawesome/brands/twitter\n      link: https://twitter.com/realllkk520\n    - icon: fontawesome/brands/github\n      link: https://github.com/sns-sdks/python-youtube\n\n\nmarkdown_extensions:\n  - codehilite\n  - admonition\n  - pymdownx.superfences\n  - pymdownx.emoji\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Examples\n\nWe provide two entry points to operate the YouTube DATA API.\n\n- Api `from pyyoutube import Api`: This is an old implementation used to be compatible with older versions of code.\n- Client `from pyyoutube import Client`: This is a new implementation for operating the API and provides additional\n  capabilities.\n\n# Basic Usage\n\n## API\n\n```python\nfrom pyyoutube import Api\n\napi = Api(api_key=\"your key\")\napi.get_channel_info(channel_id=\"id for channel\")\n# ChannelListResponse(kind='youtube#channelListResponse')\n```\n\nYou can get more examples at [api examples](/examples/apis/).\n\n## Client\n\n```python\nfrom pyyoutube import Client\n\ncli = Client(api_key=\"your key\")\ncli.channels.list(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n# ChannelListResponse(kind='youtube#channelListResponse')\n```\n\nYou can get more examples at [client examples](/examples/clients/).\n"
  },
  {
    "path": "examples/__init__.py",
    "content": ""
  },
  {
    "path": "examples/apis/__init__.py",
    "content": ""
  },
  {
    "path": "examples/apis/channel_videos.py",
    "content": "\"\"\"\nRetrieve some videos info from given channel.\n\nUse pyyoutube.api.get_channel_info to get channel video uploads playlist id.\nThen use pyyoutube.api.get_playlist_items to get playlist's videos id.\nLast use get_video_by_id to get videos data.\n\"\"\"\n\nimport pyyoutube\n\nAPI_KEY = \"xxx\"  # replace this with your api key.\n\n\ndef get_videos(channel_id):\n    api = pyyoutube.Api(api_key=API_KEY)\n    channel_info = api.get_channel_info(channel_id=channel_id)\n\n    playlist_id = channel_info.items[0].contentDetails.relatedPlaylists.uploads\n\n    uploads_playlist_items = api.get_playlist_items(\n        playlist_id=playlist_id, count=10, limit=6\n    )\n\n    videos = []\n    for item in uploads_playlist_items.items:\n        video_id = item.contentDetails.videoId\n        video = api.get_video_by_id(video_id=video_id)\n        videos.extend(video.items)\n    return videos\n\n\ndef processor():\n    channel_id = \"UC_x5XG1OV2P6uZZ5FSM9Ttw\"\n    videos = get_videos(channel_id)\n\n    with open(\"videos.json\", \"w+\") as f:\n        for video in videos:\n            f.write(video.to_json())\n            f.write(\"\\n\")\n\n\nif __name__ == \"__main__\":\n    processor()\n"
  },
  {
    "path": "examples/apis/get_all_videos_id_with_channel_by_search.py",
    "content": "\"\"\"\nRetrieve channel's videos by search api.\n\nNote Quota impact: A call to this method has a quota cost of 100 units.\n\"\"\"\n\nimport pyyoutube\n\nAPI_KEY = \"xxx\"  # replace this with your api key.\n\n\ndef get_all_videos_id_by_channel(channel_id, limit=50, count=50):\n    api = pyyoutube.Api(api_key=API_KEY)\n\n    videos = []\n    next_page = None\n\n    while True:\n        res = api.search(\n            channel_id=channel_id,\n            limit=limit,\n            count=count,\n            page_token=next_page,\n        )\n\n        next_page = res.nextPageToken\n\n        for item in res.items:\n            if item.id.videoId:\n                videos.append(item.id.videoId)\n\n        if not next_page:\n            break\n\n    return videos\n"
  },
  {
    "path": "examples/apis/get_subscription_with_oauth.py",
    "content": "\"\"\"\nThis demo show how to use this library to do authorization and get your subscription.\n\"\"\"\n\nimport pyyoutube\nimport webbrowser\n\nCLIENT_ID = \"your app id\"\nCLIENT_SECRET = \"your app secret\"\n\n\ndef get_subscriptions():\n    api = pyyoutube.Api(client_id=CLIENT_ID, client_secret=CLIENT_SECRET)\n\n    # need follows scope\n    scope = [\"https://www.googleapis.com/auth/youtube.readonly\"]\n\n    url, _ = api.get_authorization_url(scope=scope)\n\n    print(\n        \"Try to start a browser to visit the authorization page. If not opened. you can copy and visit by hand:\\n\"\n        f\"{url}\"\n    )\n    webbrowser.open(url)\n\n    auth_response = input(\n        \"\\nCopy the whole url if you finished the step to authorize:\\n\"\n    )\n\n    api.generate_access_token(authorization_response=auth_response, scope=scope)\n\n    sub_res = api.get_subscription_by_me(mine=True, parts=\"id,snippet\", count=None)\n\n    with open(\"subscriptions.json\", \"w+\") as f:\n        f.write(sub_res.to_json())\n\n    print(\"Finished.\")\n\n\nif __name__ == \"__main__\":\n    get_subscriptions()\n"
  },
  {
    "path": "examples/apis/oauth_flow.py",
    "content": "\"\"\"\nThis example demonstrates how to perform authorization.\n\"\"\"\n\nfrom pyyoutube import Api\n\nCLIENT_ID = \"xxx\"  # Your app id\nCLIENT_SECRET = \"xxx\"  # Your app secret\nSCOPE = [\n    \"https://www.googleapis.com/auth/youtube\",\n    \"https://www.googleapis.com/auth/youtube.force-ssl\",\n    \"https://www.googleapis.com/auth/userinfo.profile\",\n]\n\n\ndef do_authorize():\n    api = Api(client_id=CLIENT_ID, client_secret=CLIENT_SECRET)\n\n    authorize_url, state = api.get_authorization_url(scope=SCOPE)\n    print(f\"Click url to do authorize: {authorize_url}\")\n\n    response_uri = input(\"Input youtube redirect uri:\\n\")\n\n    token = api.generate_access_token(authorization_response=response_uri, scope=SCOPE)\n    print(f\"Your token: {token}\")\n\n    # get data\n    profile = api.get_profile()\n    print(f\"Your channel id: {profile.id}\")\n\n\nif __name__ == \"__main__\":\n    do_authorize()\n"
  },
  {
    "path": "examples/clients/__init__.py",
    "content": ""
  },
  {
    "path": "examples/clients/channel_info.py",
    "content": "\"\"\"\nThis example demonstrates how to retrieve information for a channel.\n\"\"\"\n\nfrom pyyoutube import Client\n\nAPI_KEY = \"Your key\"  # replace this with your api key.\n\n\ndef get_channel_info():\n    cli = Client(api_key=API_KEY)\n\n    channel_id = \"UC_x5XG1OV2P6uZZ5FSM9Ttw\"\n\n    resp = cli.channels.list(\n        channel_id=channel_id, parts=[\"id\", \"snippet\", \"statistics\"], return_json=True\n    )\n    print(f\"Channel info: {resp['items'][0]}\")\n\n\nif __name__ == \"__main__\":\n    get_channel_info()\n"
  },
  {
    "path": "examples/clients/oauth_flow.py",
    "content": "\"\"\"\nThis example demonstrates how to perform authorization.\n\"\"\"\n\nfrom pyyoutube import Client\n\nCLIENT_ID = \"xxx\"  # Your app id\nCLIENT_SECRET = \"xxx\"  # Your app secret\nCLIENT_SECRET_PATH = None  # or your path/to/client_secret_web.json\n\nSCOPE = [\n    \"https://www.googleapis.com/auth/youtube\",\n    \"https://www.googleapis.com/auth/youtube.force-ssl\",\n    \"https://www.googleapis.com/auth/userinfo.profile\",\n]\n\n\ndef do_authorize():\n    cli = Client(client_id=CLIENT_ID, client_secret=CLIENT_SECRET)\n    # or if you want to use a web type client_secret.json\n    # cli = Client(client_secret_path=CLIENT_SECRET_PATH)\n\n    authorize_url, state = cli.get_authorize_url(scope=SCOPE)\n    print(f\"Click url to do authorize: {authorize_url}\")\n\n    response_uri = input(\"Input youtube redirect uri:\\n\")\n\n    token = cli.generate_access_token(authorization_response=response_uri, scope=SCOPE)\n    print(f\"Your token: {token}\")\n\n    # get data\n    resp = cli.channels.list(mine=True)\n    print(f\"Your channel id: {resp.items[0].id}\")\n\n\nif __name__ == \"__main__\":\n    do_authorize()\n"
  },
  {
    "path": "examples/clients/oauth_refreshing.py",
    "content": "\"\"\"\nThis example demonstrates how to automatically (re)generate tokens for continuous OAuth.\nWe store the Access Token in a seperate .env file to be used later.\n\"\"\"\n\nfrom pyyoutube import Client\nfrom json import loads, dumps\nfrom pathlib import Path\n\nCLIENT_ID = \"xxx\"  # Your app id\nCLIENT_SECRET = \"xxx\"  # Your app secret\nCLIENT_SECRET_PATH = None  # or your path/to/client_secret_web.json\n\nTOKEN_PERSISTENT_PATH = None  # path/to/persistent_token_storage_location\n\nSCOPE = [\n    \"https://www.googleapis.com/auth/youtube\",\n    \"https://www.googleapis.com/auth/youtube.force-ssl\",\n    \"https://www.googleapis.com/auth/userinfo.profile\",\n]\n\n\ndef do_refresh():\n    token_location = Path(TOKEN_PERSISTENT_PATH)\n\n    # Read the persistent token data if it exists\n    token_data = {}\n    if token_location.exists():\n        token_data = loads(token_location.read_text())\n\n    cli = Client(\n        client_id=CLIENT_ID,\n        client_secret=CLIENT_SECRET,\n        access_token=token_data.get(\"access_token\"),\n        refresh_token=token_data.get(\"refresh_token\"),\n    )\n    # or if you want to use a web type client_secret.json\n    # cli = Client(\n    #     client_secret_path=CLIENT_SECRET_PATH,\n    #     access_token=token_data.get(\"access_token\"),\n    #     refresh_token=token_data.get(\"refresh_token\")\n    # )\n\n    # If no access token is provided, this is the same as oauth_flow.py\n    if not cli._has_auth_credentials():\n        authorize_url, state = cli.get_authorize_url(scope=SCOPE)\n        print(f\"Click url to do authorize: {authorize_url}\")\n\n        response_uri = input(\"Input youtube redirect uri:\\n\")\n\n        token = cli.generate_access_token(\n            authorization_response=response_uri, scope=SCOPE\n        )\n        print(f\"Your token: {token}\")\n\n    # Otherwise, refresh the access token if it has expired\n    else:\n        token = cli.refresh_access_token(cli.refresh_token)\n\n        # we add the token data to the client and token objects so that they are complete\n        token.refresh_token = cli.refresh_token\n        cli.access_token = token.access_token\n        print(f\"Your token: {token}\")\n\n    # Write the token data to the persistent location to be used again, ensuring the file exists\n    token_location.mkdir(parents=True, exist_ok=True)\n    token_location.write_text(\n        dumps(\n            {\"access_token\": token.access_token, \"refresh_token\": token.refresh_token}\n        )\n    )\n\n    # Now you can do things with the client\n    resp = cli.channels.list(mine=True)\n    print(f\"Your channel id: {resp.items[0].id}\")\n\n\nif __name__ == \"__main__\":\n    do_refresh()\n"
  },
  {
    "path": "examples/clients/upload_video.py",
    "content": "\"\"\"\nThis example demonstrates how to upload a video.\n\"\"\"\n\nimport pyyoutube.models as mds\nfrom pyyoutube import Client\nfrom pyyoutube.media import Media\n\n# Access token with scope:\n# https://www.googleapis.com/auth/youtube.upload\n# https://www.googleapis.com/auth/youtube\n# https://www.googleapis.com/auth/youtube.force-ssl\nACCESS_TOKEN = \"xxx\"\n\n\ndef upload_video():\n    cli = Client(access_token=ACCESS_TOKEN)\n\n    body = mds.Video(\n        snippet=mds.VideoSnippet(title=\"video title\", description=\"video description\")\n    )\n\n    media = Media(filename=\"target_video.mp4\")\n\n    upload = cli.videos.insert(\n        body=body, media=media, parts=[\"snippet\"], notify_subscribers=True\n    )\n\n    response = None\n    while response is None:\n        print(f\"Uploading video...\")\n        status, response = upload.next_chunk()\n        if status is not None:\n            print(f\"Uploading video progress: {status.progress()}...\")\n\n    # Use video class to representing the video resource.\n    video = mds.Video.from_dict(response)\n    print(f\"Video id {video.id} was successfully uploaded.\")\n\n\nif __name__ == \"__main__\":\n    upload_video()\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"python-youtube\"\nversion = \"0.9.9\"\ndescription = \"A Python wrapper around for YouTube Data API.\"\nauthors = [\"ikaroskun <merle.liukun@gmail.com>\"]\nlicense = \"MIT\"\nkeywords = [\"youtube-api\", \"youtube-v3-api\", \"youtube-data-api\", \"youtube-sdk\"]\nreadme = \"README.rst\"\nhomepage = \"https://github.com/sns-sdks/python-youtube\"\nrepository = \"https://github.com/sns-sdks/python-youtube\"\nclassifiers = [\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: Apache Software License\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Programming Language :: Python :: Implementation :: CPython\",\n    \"Programming Language :: Python :: Implementation :: PyPy\",\n]\n\npackages = [\n    { include = \"pyyoutube\" },\n    { include = \"tests\", format = \"sdist\" },\n]\n\n[tool.poetry.dependencies]\npython = \"^3.9\"\nrequests = \">=2.28.0,<3.0.0\"\nrequests-oauthlib = \">=1.3.0,<3.0.0\"\nisodate = \">=0.6.1,<1.0.0\"\ndataclasses-json = \">=0.6.0,<1.0.0\"\n\n[tool.poetry.group.dev.dependencies]\nresponses = \"^0.25.0\"\npytest = \"^8.4.0\"\npytest-cov = \"^6.2.0\"\n\n[build-system]\nrequires = [\"poetry-core>=2.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\naddopts = --cov=pyyoutube --cov-report xml"
  },
  {
    "path": "pyyoutube/__init__.py",
    "content": "from .api import Api  # noqa\nfrom .client import Client  # noqa\nfrom .error import *  # noqa\nfrom .models import *  # noqa\nfrom .utils.constants import TOPICS  # noqa\n"
  },
  {
    "path": "pyyoutube/__version__.py",
    "content": "# d8888b. db    db d888888b db   db  .d88b.  d8b   db db    db  .d88b.  db    db d888888b db    db d8888b. d88888b\n# 88  `8D `8b  d8' `~~88~~' 88   88 .8P  Y8. 888o  88 `8b  d8' .8P  Y8. 88    88 `~~88~~' 88    88 88  `8D 88'\n# 88oodD'  `8bd8'     88    88ooo88 88    88 88V8o 88  `8bd8'  88    88 88    88    88    88    88 88oooY' 88ooooo\n# 88~~~      88       88    88~~~88 88    88 88 V8o88    88    88    88 88    88    88    88    88 88~~~b. 88~~~~~\n# 88         88       88    88   88 `8b  d8' 88  V888    88    `8b  d8' 88b  d88    88    88b  d88 88   8D 88.\n# 88         YP       YP    YP   YP  `Y88P'  VP   V8P    YP     `Y88P'  ~Y8888P'    YP    ~Y8888P' Y8888P' Y88888P\n\n__version__ = \"0.9.9\"\n"
  },
  {
    "path": "pyyoutube/api.py",
    "content": "\"\"\"\nMain Api implementation.\n\"\"\"\n\nfrom typing import Optional, List, Union\n\nimport requests\nfrom requests.auth import HTTPBasicAuth\nfrom requests.models import Response\nfrom requests_oauthlib.oauth2_session import OAuth2Session\n\nfrom pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException\nfrom pyyoutube.models import (\n    AccessToken,\n    UserProfile,\n    ActivityListResponse,\n    CaptionListResponse,\n    ChannelListResponse,\n    ChannelSectionResponse,\n    PlaylistListResponse,\n    PlaylistItemListResponse,\n    VideoListResponse,\n    CommentThreadListResponse,\n    CommentListResponse,\n    VideoCategoryListResponse,\n    SearchListResponse,\n    SubscriptionListResponse,\n    I18nRegionListResponse,\n    I18nLanguageListResponse,\n    MemberListResponse,\n    MembershipsLevelListResponse,\n    VideoAbuseReportReasonListResponse,\n)\nfrom pyyoutube.utils.params_checker import enf_comma_separated, enf_parts\n\n\nclass Api(object):\n    \"\"\"\n    Example usage:\n        To create an instance of pyyoutube.Api class:\n\n            >>> import pyyoutube\n            >>> api = pyyoutube.Api(api_key=\"your api key\")\n\n        To get one channel info:\n\n            >>> res = api.get_channel_info(channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n            >>> print(res.items[0])\n\n        Now this api provide methods as follows:\n            >>> api.get_authorization_url()\n            >>> api.generate_access_token()\n            >>> api.refresh_token()\n            >>> api.get_channel_info()\n            >>> api.get_playlist_by_id()\n            >>> api.get_playlists()\n            >>> api.get_playlist_item_by_id()\n            >>> api.get_playlist_items()\n            >>> api.get_video_by_id()\n            >>> api.get_videos_by_chart()\n            >>> api.get_videos_by_myrating()\n            >>> api.get_comment_thread_by_id()\n            >>> api.get_comment_threads()\n            >>> api.get_comment_by_id()\n            >>> api.get_comments()\n            >>> api.get_video_categories()\n            >>> api.get_subscription_by_id()\n            >>> api.get_subscription_by_channel()\n            >>> api.get_subscription_by_me()\n            >>> api.get_activities_by_channel()\n            >>> api.get_activities_by_me()\n            >>> api.get_captions_by_video()\n            >>> api.get_channel_sections_by_id()\n            >>> api.get_channel_sections_by_channel()\n            >>> api.get_i18n_regions()\n            >>> api.get_i18n_languages()\n            >>> api.get_video_abuse_report_reason()\n            >>> api.search()\n            >>> api.search_by_keywords()\n            >>> api.search_by_developer()\n            >>> api.search_by_mine()\n            >>> api.search_by_related_video()\n    \"\"\"\n\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/\"\n    AUTHORIZATION_URL = \"https://accounts.google.com/o/oauth2/v2/auth\"\n    EXCHANGE_ACCESS_TOKEN_URL = \"https://oauth2.googleapis.com/token\"\n    USER_INFO_URL = \"https://www.googleapis.com/oauth2/v1/userinfo\"\n\n    DEFAULT_REDIRECT_URI = \"https://localhost/\"\n\n    DEFAULT_SCOPE = [\n        \"https://www.googleapis.com/auth/youtube\",\n        \"https://www.googleapis.com/auth/userinfo.profile\",\n    ]\n\n    DEFAULT_STATE = \"PyYouTube\"\n    DEFAULT_TIMEOUT = 10\n    DEFAULT_QUOTA = 10000  # this quota reset at 00:00:00(GMT-7) every day.\n\n    def __init__(\n        self,\n        client_id: Optional[str] = None,\n        client_secret: Optional[str] = None,\n        api_key: Optional[str] = None,\n        access_token: Optional[str] = None,\n        timeout: Optional[int] = None,\n        proxies: Optional[dict] = None,\n    ) -> None:\n        \"\"\"\n        This Api provide two method to work. Use api key or use access token.\n\n        Args:\n            client_id(str, optional):\n                Your google app's ID.\n            client_secret (str, optional):\n                Your google app's secret.\n            api_key(str, optional):\n                The api key which you create from google api console.\n            access_token(str, optional):\n                If you not provide api key, you can do authorization to get an access token.\n                If all api key and access token provided. Use access token first.\n            timeout(int, optional):\n                The request timeout.\n            proxies(dict, optional):\n                If you want use proxy, need point this param.\n                param style like requests lib style.\n                Refer https://2.python-requests.org//en/latest/user/advanced/#proxies\n\n        Returns:\n            YouTube Api instance.\n        \"\"\"\n        self._client_id = client_id\n        self._client_secret = client_secret\n        self._api_key = api_key\n        self._access_token = access_token\n        self._refresh_token = None  # This keep current user's refresh token.\n        self._timeout = timeout\n        self.session = requests.Session()\n        self.proxies = proxies\n\n        if not (\n            (self._client_id and self._client_secret)\n            or self._api_key\n            or self._access_token\n        ):\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Must specify either client key info or api key.\",\n                )\n            )\n\n        if self._timeout is None:\n            self._timeout = self.DEFAULT_TIMEOUT\n\n    def _get_oauth_session(\n        self,\n        redirect_uri: Optional[str] = None,\n        scope: Optional[List[str]] = None,\n        **kwargs,\n    ) -> OAuth2Session:\n        \"\"\"\n        Build a request session for OAuth.\n\n        Args:\n            redirect_uri(str, optional)\n                Determines how Google's authorization server sends a response to your app.\n                If not provide will use default https://localhost/\n            scope (list, optional)\n                The scope you want give permission.\n                If you not provide, will use default scope.\n            kwargs(dict, optional)\n                Some other params you want provide.\n\n        Returns:\n            OAuth2 Session\n        \"\"\"\n        if redirect_uri is None:\n            redirect_uri = self.DEFAULT_REDIRECT_URI\n\n        if scope is None:\n            scope = self.DEFAULT_SCOPE\n\n        return OAuth2Session(\n            client_id=self._client_id,\n            scope=scope,\n            redirect_uri=redirect_uri,\n            state=self.DEFAULT_STATE,\n            **kwargs,\n        )\n\n    def get_authorization_url(\n        self,\n        redirect_uri: Optional[str] = None,\n        scope: Optional[List[str]] = None,\n        **kwargs,\n    ) -> (str, str):\n        \"\"\"\n        Build authorization url to do authorize.\n\n        Args:\n            redirect_uri(str, optional)\n                Determines how Google's authorization server sends a response to your app.\n                If not provide will use default https://localhost/\n            scope (list, optional)\n                The scope you want give permission.\n                If you not provide, will use default scope.\n            kwargs(dict, optional)\n                Some other params you want provide.\n\n        Returns:\n            The uri you can open on browser to do authorize.\n        \"\"\"\n        oauth_session = self._get_oauth_session(\n            redirect_uri=redirect_uri,\n            scope=scope,\n            **kwargs,\n        )\n        authorization_url, state = oauth_session.authorization_url(\n            self.AUTHORIZATION_URL,\n            access_type=\"offline\",\n            prompt=\"select_account\",\n            **kwargs,\n        )\n\n        return authorization_url, state\n\n    def generate_access_token(\n        self,\n        authorization_response: str,\n        redirect_uri: Optional[str] = None,\n        scope: Optional[List[str]] = None,\n        return_json: bool = False,\n        **kwargs,\n    ) -> Union[dict, AccessToken]:\n        \"\"\"\n        Use the google auth response to get access token\n\n        Args:\n            authorization_response (str)\n                The response url which google redirect.\n            redirect_uri(str, optional)\n                Determines how Google's authorization server sends a response to your app.\n                If not provide will use default https://localhost/\n            scope (list, optional)\n                The scope you want give permission.\n                If you not provide, will use default scope.\n            return_json(bool, optional)\n                The return data type. If you set True JSON data will be returned.\n                False will return pyyoutube.AccessToken\n            kwargs(dict, optional)\n                Some other params you want provide.\n        Return:\n            Retrieved access token's info, pyyoutube.AccessToken instance.\n        \"\"\"\n\n        oauth_session = self._get_oauth_session(\n            redirect_uri=redirect_uri,\n            scope=scope,\n            **kwargs,\n        )\n        token = oauth_session.fetch_token(\n            self.EXCHANGE_ACCESS_TOKEN_URL,\n            client_secret=self._client_secret,\n            authorization_response=authorization_response,\n            proxies=self.proxies,\n        )\n        self._access_token = oauth_session.access_token\n        self._refresh_token = oauth_session.token[\"refresh_token\"]\n        if return_json:\n            return token\n        else:\n            return AccessToken.from_dict(token)\n\n    def refresh_token(\n        self, refresh_token: Optional[str] = None, return_json: bool = False\n    ) -> Union[dict, AccessToken]:\n        \"\"\"\n        Refresh token by api return refresh token.\n\n        Args:\n            refresh_token (str)\n                The refresh token which the api returns.\n            return_json (bool, optional):\n                If True JSON data will be returned, instead of pyyoutube.AccessToken\n        Return:\n            Retrieved new access token's info,  pyyoutube.AccessToken instance.\n        \"\"\"\n\n        refresh_token = refresh_token if refresh_token else self._refresh_token\n\n        if refresh_token is None:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=f\"Must provide the refresh token or api has been authorized.\",\n                )\n            )\n\n        oauth_session = OAuth2Session(client_id=self._client_id)\n        auth = HTTPBasicAuth(self._client_id, self._client_secret)\n        new_token = oauth_session.refresh_token(\n            self.EXCHANGE_ACCESS_TOKEN_URL,\n            refresh_token=refresh_token,\n            auth=auth,\n        )\n        self._access_token = oauth_session.access_token\n        if return_json:\n            return new_token\n        else:\n            return AccessToken.from_dict(new_token)\n\n    @staticmethod\n    def _parse_response(response: Response) -> dict:\n        \"\"\"\n        Parse response data and check whether errors exists.\n\n        Args:\n            response (Response)\n                The response which the request return.\n        Return:\n             response's data\n        \"\"\"\n        data = response.json()\n        if \"error\" in data:\n            raise PyYouTubeException(response)\n        return data\n\n    @staticmethod\n    def _parse_data(data: Optional[dict]) -> Union[dict, list]:\n        \"\"\"\n        Parse resp data.\n\n        Args:\n            data (dict)\n                The response data by response.json()\n        Return:\n             response's items\n        \"\"\"\n        items = data[\"items\"]\n        return items\n\n    def _request(\n        self, resource, method=None, args=None, post_args=None, enforce_auth=True\n    ) -> Response:\n        \"\"\"\n        Main request sender.\n\n        Args:\n            resource(str)\n                Resource field is which type data you want to retrieve.\n                Such as channels，videos and so on.\n            method(str, optional)\n                The method this request to send request.\n                Default is 'GET'\n            args(dict, optional)\n                The url params for this request.\n            post_args(dict, optional)\n                The Post params for this request.\n            enforce_auth(bool, optional)\n                Whether use google credentials\n        Returns:\n            response\n        \"\"\"\n        if method is None:\n            method = \"GET\"\n\n        if args is None:\n            args = dict()\n\n        if post_args is not None:\n            method = \"POST\"\n\n        key = None\n        access_token = None\n        if self._api_key is not None:\n            key = \"key\"\n            access_token = self._api_key\n        if self._access_token is not None:\n            key = \"access_token\"\n            access_token = self._access_token\n        if access_token is None and enforce_auth:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"You must provide your credentials.\",\n                )\n            )\n\n        if enforce_auth:\n            if method == \"POST\" and key not in post_args:\n                post_args[key] = access_token\n            elif method == \"GET\" and key not in args:\n                args[key] = access_token\n\n        try:\n            response = self.session.request(\n                method=method,\n                url=self.BASE_URL + resource,\n                timeout=self._timeout,\n                params=args,\n                data=post_args,\n                proxies=self.proxies,\n            )\n        except requests.HTTPError as e:\n            raise PyYouTubeException(\n                ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message=e.args[0])\n            )\n        else:\n            return response\n\n    def get_profile(\n        self, access_token: Optional[str] = None, return_json: Optional[bool] = False\n    ) -> Union[dict, UserProfile]:\n        \"\"\"\n        Get token user info.\n\n        Args:\n            access_token(str, optional)\n                user access token. If not provide, use api instance access token\n            return_json(bool, optional)\n                The return data type. If you set True JSON data will be returned.\n                False will return pyyoutube.UserProfile\n\n        Returns:\n            The data for you given access token's user info.\n        \"\"\"\n        if access_token is None:\n            access_token = self._access_token\n        if access_token is None:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=f\"Must provide the access token or api has been authorized.\",\n                )\n            )\n        try:\n            response = self.session.get(\n                self.USER_INFO_URL,\n                params={\"access_token\": access_token},\n                timeout=self._timeout,\n                proxies=self.proxies,\n            )\n        except requests.HTTPError as e:\n            raise PyYouTubeException(\n                ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message=e.args[0])\n            )\n        data = self._parse_response(response)\n        if return_json:\n            return data\n        else:\n            return UserProfile.from_dict(data)\n\n    def paged_by_page_token(\n        self,\n        resource: str,\n        args: dict,\n        count: Optional[int] = None,\n    ):\n        \"\"\"\n        Response paged by response's page token. If not provide response token\n\n        Args:\n            resource (str):\n                The resource string need to retrieve data.\n            args (dict)\n                The args for api.\n            count (int, optional):\n                The count for result items you want to get.\n                If provide this with None, will retrieve all items.\n                Note:\n                    The all items maybe too much. Notice your app's cost.\n        Returns:\n            Data api origin response.\n        \"\"\"\n        res_data: Optional[dict] = None\n        current_items: List[dict] = []\n        page_token: Optional[str] = None\n        now_items_count: int = 0\n\n        while True:\n            if page_token is not None:\n                args[\"pageToken\"] = page_token\n\n            resp = self._request(resource=resource, method=\"GET\", args=args)\n            data = self._parse_response(resp)  # origin response\n            # set page token\n            page_token = data.get(\"nextPageToken\")\n            prev_page_token = data.get(\"prevPageToken\")\n\n            # parse results.\n            items = self._parse_data(data)\n            current_items.extend(items)\n            now_items_count += len(items)\n            if res_data is None:\n                res_data = data\n            # first check the count if satisfies.\n            if count is not None:\n                if now_items_count >= count:\n                    current_items = current_items[:count]\n                    break\n            # if have no page token, mean no more data.\n            if page_token is None:\n                break\n        res_data[\"items\"] = current_items\n\n        # use last request page token\n        res_data[\"nextPageToken\"] = page_token\n        res_data[\"prevPageToken\"] = prev_page_token\n        return res_data\n\n    def get_activities_by_channel(\n        self,\n        *,\n        channel_id: str,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        before: Optional[str] = None,\n        after: Optional[str] = None,\n        region_code: Optional[str] = None,\n        count: Optional[int] = 20,\n        limit: int = 20,\n        page_token: Optional[str] = None,\n        return_json: bool = False,\n    ):\n        \"\"\"\n        Retrieve given channel's activities data.\n\n        Args:\n            channel_id (str):\n                The id for channel which you want to get activities data.\n            parts ((str,list,tuple,set) optional):\n                The resource parts for activities you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            before (str, optional):\n                Set this will only return the activities occurred before this timestamp.\n                This need specified in ISO 8601 (YYYY-MM-DDThh:mm:ss.sZ) format.\n            after (str, optional):\n                Set this will only return the activities occurred after this timestamp.\n                This need specified in ISO 8601 (YYYY-MM-DDThh:mm:ss.sZ) format.\n            region_code (str, optional):\n                Set this will only return the activities for the specified country.\n                This need specified with an ISO 3166-1 alpha-2 country code.\n            count (int, optional):\n                The count will retrieve activities data.\n                Default is 20.\n                If provide this with None, will retrieve all activities.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For activities, this should not be more than 50.\n                Default is 20.\n            page_token (str, optional):\n                The token of the page of activities result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the page result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.ActivityListResponse instance.\n\n        Returns:\n            ActivityListResponse or original data.\n        \"\"\"\n\n        if count is None:\n            limit = 50  # for activities the max limit for per request is 50\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"channelId\": channel_id,\n            \"part\": enf_parts(resource=\"activities\", value=parts),\n            \"maxResults\": limit,\n        }\n\n        if before:\n            args[\"publishedBefore\"] = before\n        if after:\n            args[\"publishedAfter\"] = after\n        if region_code:\n            args[\"regionCode\"] = region_code\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(\n            resource=\"activities\", args=args, count=count\n        )\n\n        if return_json:\n            return res_data\n        else:\n            return ActivityListResponse.from_dict(res_data)\n\n    def get_activities_by_me(\n        self,\n        *,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        before: Optional[str] = None,\n        after: Optional[str] = None,\n        region_code: Optional[str] = None,\n        count: Optional[int] = 20,\n        limit: int = 20,\n        page_token: Optional[str] = None,\n        return_json: bool = False,\n    ):\n        \"\"\"\n        Retrieve authorized user's activities.\n\n        Note:\n            This need you do authorize first.\n\n        Args:\n            parts ((str,list,tuple,set) optional):\n                The resource parts for activities you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            before (str, optional):\n                Set this will only return the activities occurred before this timestamp.\n                This need specified in ISO 8601 (YYYY-MM-DDThh:mm:ss.sZ) format.\n            after (str, optional):\n                Set this will only return the activities occurred after this timestamp.\n                This need specified in ISO 8601 (YYYY-MM-DDThh:mm:ss.sZ) format.\n            region_code (str, optional):\n                Set this will only return the activities for the specified country.\n                This need specified with an ISO 3166-1 alpha-2 country code.\n            count (int, optional):\n                The count will retrieve activities data.\n                Default is 20.\n                If provide this with None, will retrieve all activities.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For activities, this should not be more than 50.\n                Default is 20.\n            page_token (str, optional):\n                The token of the page of activities result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the page result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.ActivityListResponse instance.\n\n        Returns:\n            ActivityListResponse or original data.\n        \"\"\"\n\n        if count is None:\n            limit = 50  # for activities the max limit for per request is 50\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"mine\": True,\n            \"part\": enf_parts(resource=\"activities\", value=parts),\n            \"maxResults\": limit,\n        }\n\n        if before:\n            args[\"publishedBefore\"] = before\n        if after:\n            args[\"publishedAfter\"] = after\n        if region_code:\n            args[\"regionCode\"] = region_code\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(\n            resource=\"activities\", args=args, count=count\n        )\n\n        if return_json:\n            return res_data\n        else:\n            return ActivityListResponse.from_dict(res_data)\n\n    def get_captions_by_video(\n        self,\n        *,\n        video_id: str,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        caption_id: Optional[Union[str, list, tuple, set]] = None,\n        return_json: bool = False,\n    ):\n        \"\"\"\n        Retrieve authorized user's video's caption data.\n\n        Note:\n            This need you do authorize first.\n\n        Args:\n            video_id (str):\n                The id for video which you want to get caption.\n            parts ((str,list,tuple,set) optional):\n                The resource parts for caption you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            caption_id ((str,list,tuple,set)):\n                The id for caption that you want to get data.\n                You can pass this with single id str,comma-separated id str, or list, tuple, set of id str.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.CaptionListResponse instance.\n        Returns:\n            CaptionListResponse or original data.\n        \"\"\"\n\n        args = {\n            \"videoId\": video_id,\n            \"part\": enf_parts(\"captions\", parts),\n        }\n\n        if caption_id is not None:\n            args[\"id\"] = enf_comma_separated(\"caption_id\", caption_id)\n\n        resp = self._request(resource=\"captions\", method=\"GET\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return CaptionListResponse.from_dict(data)\n\n    def get_channel_info(\n        self,\n        *,\n        channel_id: Optional[Union[str, list, tuple, set]] = None,\n        for_handle: Optional[str] = None,\n        for_username: Optional[str] = None,\n        mine: Optional[bool] = None,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: str = \"en_US\",\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve channel data from YouTube Data API.\n\n        Note:\n            1. Don't know why, but now you couldn't get channel list by given an guide category.\n               You can only get list by parameters mine,forUsername,id.\n               Refer: https://developers.google.com/youtube/v3/guides/implementation/channels\n            2. The origin maxResult param not work for these filter method.\n\n        Args:\n            channel_id ((str,list,tuple,set), optional):\n                The id or comma-separated id string for youtube channel which you want to get.\n                You can also pass this with an id list, tuple, set.\n            for_handle (str, optional):\n                The parameter specifies a YouTube handle, thereby requesting the channel associated with that handle.\n                The parameter value can be prepended with an @ symbol. For example, to retrieve the resource for\n                the \"Google for Developers\" channel, set the forHandle parameter value to\n                either GoogleDevelopers or @GoogleDevelopers.\n            for_username (str, optional):\n                The name for YouTube username which you want to get.\n                Note: This name may the old youtube version's channel's user's username, Not the the channel name.\n                Refer: https://developers.google.com/youtube/v3/guides/working_with_channel_ids\n            mine (bool, optional):\n                If you have give the authorization. Will return your channels.\n                Must provide the access token.\n            parts (str, optional):\n                Comma-separated list of one or more channel resource properties.\n                If not provided. will use default public properties.\n            hl (str, optional):\n                If provide this. Will return channel's language localized info.\n                This value need https://developers.google.com/youtube/v3/docs/i18nLanguages.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.ChannelListResponse instance.\n        Returns:\n            ChannelListResponse instance or original data.\n        \"\"\"\n\n        args = {\n            \"part\": enf_parts(resource=\"channels\", value=parts),\n            \"hl\": hl,\n        }\n        if for_handle is not None:\n            args[\"forHandle\"] = for_handle\n        elif for_username is not None:\n            args[\"forUsername\"] = for_username\n        elif channel_id is not None:\n            args[\"id\"] = enf_comma_separated(\"channel_id\", channel_id)\n        elif mine is not None:\n            args[\"mine\"] = mine\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=f\"Specify at least one of channel_id,channel_name or mine\",\n                )\n            )\n\n        resp = self._request(resource=\"channels\", method=\"GET\", args=args)\n\n        data = self._parse_response(resp)\n        if return_json:\n            return data\n        else:\n            return ChannelListResponse.from_dict(data)\n\n    def get_channel_sections_by_id(\n        self,\n        *,\n        section_id: Union[str, list, tuple, set],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        return_json: Optional[bool] = False,\n    ) -> Union[ChannelSectionResponse, dict]:\n        \"\"\"\n        Retrieve channel section info by his ids(s).\n\n        Args:\n            section_id:\n                The id(s) for channel sections.\n                You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids.\n            parts:\n                The resource parts for channel section you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            return_json:\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.ChannelSectionResponse instance.\n        Returns:\n            ChannelSectionResponse or original data.\n        \"\"\"\n\n        args = {\n            \"id\": enf_comma_separated(field=\"section_id\", value=section_id),\n            \"part\": enf_parts(resource=\"channelSections\", value=parts),\n        }\n\n        resp = self._request(resource=\"channelSections\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return ChannelSectionResponse.from_dict(data)\n\n    def get_channel_sections_by_channel(\n        self,\n        *,\n        channel_id: Optional[str] = None,\n        mine: bool = False,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        return_json: Optional[bool] = False,\n    ) -> Union[ChannelSectionResponse, dict]:\n        \"\"\"\n        Retrieve channel sections by channel id.\n\n        Args:\n            channel_id:\n                The id for channel which you want to get channel sections.\n            mine:\n                If you want to get your channel's sections, set this with True.\n                And this need your authorization.\n            parts:\n                The resource parts for channel section you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            return_json:\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.ChannelSectionResponse instance.\n        Returns:\n            ChannelSectionResponse or original data.\n        \"\"\"\n\n        args = {\n            \"part\": enf_parts(resource=\"channelSections\", value=parts),\n        }\n\n        if mine:\n            args[\"mine\"] = mine\n        else:\n            args[\"channelId\"] = channel_id\n\n        resp = self._request(resource=\"channelSections\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return ChannelSectionResponse.from_dict(data)\n\n    def get_comment_by_id(\n        self,\n        *,\n        comment_id: Union[str, list, tuple, set],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        text_format: Optional[str] = \"html\",\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve comment info by given comment id str.\n\n        Args:\n            comment_id (str, optional):\n                The id for comment that you want to retrieve data.\n                You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids.\n            parts ((str,list,tuple,set), optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            text_format (str, optional):\n                Comments left by users format style.\n                Acceptable values are: html, plainText.\n                Default is html.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.CommentListResponse instance.\n\n        Returns:\n            CommentListResponse or original data\n        \"\"\"\n\n        args = {\n            \"id\": enf_comma_separated(field=\"comment_id\", value=comment_id),\n            \"part\": enf_parts(resource=\"comments\", value=parts),\n            \"textFormat\": text_format,\n        }\n\n        resp = self._request(resource=\"comments\", method=\"GET\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return CommentListResponse.from_dict(data)\n\n    def get_comments(\n        self,\n        *,\n        parent_id: str,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        text_format: Optional[str] = \"html\",\n        count: Optional[int] = 20,\n        limit: Optional[int] = 20,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve comments info by given parent id.\n        Note: YouTube currently supports replies only for top-level comments.\n        However, replies to replies may be supported in the future.\n\n        Args:\n            parent_id (str):\n                Provide the ID of the comment for which replies should be retrieved.\n            parts ((str,list,tuple,set), optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            text_format (str, optional):\n                Comments left by users format style.\n                Acceptable values are: html, plainText.\n                Default is html.\n            count (int, optional):\n                The count will retrieve videos data.\n                Default is 20.\n                If provide this with None, will retrieve all comments.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For comments, this should not be more than 100.\n                Default is 20.\n            page_token(str, optional):\n                The token of the page of comments result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.CommentListResponse instance.\n        Returns:\n            CommentListResponse or original data\n        \"\"\"\n\n        if count is None:\n            limit = 100  # for comments the max limit for per request is 100\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"parentId\": parent_id,\n            \"part\": enf_parts(resource=\"comments\", value=parts),\n            \"textFormat\": text_format,\n            \"maxResults\": limit,\n        }\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(resource=\"comments\", args=args, count=count)\n        if return_json:\n            return res_data\n        else:\n            return CommentListResponse.from_dict(res_data)\n\n    def get_comment_thread_by_id(\n        self,\n        *,\n        comment_thread_id: Union[str, list, tuple, set],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        text_format: Optional[str] = \"html\",\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve the comment thread info by given id.\n\n        Args:\n            comment_thread_id ((str,list,tuple,set)):\n                The id for comment thread that you want to retrieve data.\n                You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids.\n            parts ((str,list,tuple,set), optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            text_format (str, optional):\n                Comments left by users format style.\n                Acceptable values are: html, plainText.\n                Default is html.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.CommentThreadListResponse instance.\n        Returns:\n            CommentThreadListResponse or original data\n        \"\"\"\n\n        args = {\n            \"id\": enf_comma_separated(\"comment_thread_id\", comment_thread_id),\n            \"part\": enf_parts(resource=\"commentThreads\", value=parts),\n            \"textFormat\": text_format,\n        }\n\n        resp = self._request(resource=\"commentThreads\", method=\"GET\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return CommentThreadListResponse.from_dict(data)\n\n    def get_comment_threads(\n        self,\n        *,\n        all_to_channel_id: Optional[str] = None,\n        channel_id: Optional[str] = None,\n        video_id: Optional[str] = None,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        moderation_status: Optional[str] = None,\n        order: Optional[str] = None,\n        search_terms: Optional[str] = None,\n        text_format: Optional[str] = \"html\",\n        count: Optional[int] = 20,\n        limit: Optional[int] = 20,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve the comment threads info by given filter condition.\n\n        Args:\n            all_to_channel_id (str, optional):\n                If you provide this with a channel id, will return all comment threads associated with the channel.\n                The response can include comments about the channel or about the channel's videos.\n            channel_id (str, optional):\n                If you provide this with a channel id, will return the comment threads associated with the channel.\n                But the response not include comments about the channel's videos.\n            video_id  (str, optional):\n                If you provide this with a video id, will return the comment threads associated with the video.\n            parts ((str,list,tuple,set), optional)\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            moderation_status (str, optional):\n                This parameter must used with authorization.\n                If you provide this. the response will return comment threads match this filter only.\n                Acceptable values are:\n                    - heldForReview: Retrieve comment threads that are awaiting review by a moderator.\n                    - likelySpam: Retrieve comment threads classified as likely to be spam.\n                    - published: Retrieve threads of published comments. this is default for all.\n                See more: https://developers.google.com/youtube/v3/docs/commentThreads/list#parameters\n            order (str, optional):\n                Order parameter specifies the order in which the API response should list comment threads.\n                Acceptable values are:\n                    - time: Comment threads are ordered by time. This is the default behavior.\n                    - relevance: Comment threads are ordered by relevance.\n            search_terms (str, optional):\n                The searchTerms parameter instructs the API to limit the API response to only contain comments\n                that contain the specified search terms.\n            text_format (str, optional):\n                Comments left by users format style.\n                Acceptable values are: html, plainText.\n                Default is html.\n            count (int, optional):\n                The count will retrieve comment threads data.\n                Default is 20.\n                If provide this with None, will retrieve all comment threads.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For comment threads, this should not be more than 100.\n                Default is 20.\n            page_token(str, optional):\n                The token of the page of commentThreads result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.CommentThreadListResponse instance.\n\n        Returns:\n            CommentThreadListResponse or original data\n        \"\"\"\n\n        if count is None:\n            limit = 100  # for commentThreads the max limit for per request is 100\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"part\": enf_parts(resource=\"commentThreads\", value=parts),\n            \"maxResults\": limit,\n            \"textFormat\": text_format,\n        }\n\n        if all_to_channel_id:\n            args[\"allThreadsRelatedToChannelId\"] = (all_to_channel_id,)\n        elif channel_id:\n            args[\"channelId\"] = channel_id\n        elif video_id:\n            args[\"videoId\"] = video_id\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=f\"Specify at least one of all_to_channel_id, channel_id or video_id\",\n                )\n            )\n\n        if moderation_status:\n            args[\"moderationStatus\"] = moderation_status\n        if order:\n            args[\"order\"] = order\n        if search_terms:\n            args[\"searchTerms\"] = search_terms\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(\n            resource=\"commentThreads\", args=args, count=count\n        )\n        if return_json:\n            return res_data\n        else:\n            return CommentThreadListResponse.from_dict(res_data)\n\n    def get_i18n_languages(\n        self,\n        *,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = \"en_US\",\n        return_json: Optional[bool] = False,\n    ) -> Union[I18nLanguageListResponse, dict]:\n        \"\"\"\n        Returns a list of application languages that the YouTube website supports.\n\n        Args:\n            parts:\n                The resource parts for i18n language you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            hl:\n                If provide this. Will return i18n language's language localized info.\n                This value need https://developers.google.com/youtube/v3/docs/i18nLanguages.\n            return_json:\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.I18nLanguageListResponse instance.\n\n        Returns:\n            I18nLanguageListResponse or original data.\n        \"\"\"\n\n        args = {\"hl\": hl, \"part\": enf_parts(resource=\"i18nLanguages\", value=parts)}\n\n        resp = self._request(resource=\"i18nLanguages\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return I18nLanguageListResponse.from_dict(data)\n\n    def get_i18n_regions(\n        self,\n        *,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = \"en_US\",\n        return_json: Optional[bool] = False,\n    ) -> Union[I18nRegionListResponse, dict]:\n        \"\"\"\n        Retrieve all available regions.\n\n        Args:\n            parts:\n                The resource parts for i18n region you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            hl:\n                If provide this. Will return i18n region's language localized info.\n                This value need https://developers.google.com/youtube/v3/docs/i18nLanguages.\n            return_json:\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.I18nRegionListResponse instance.\n        Returns:\n            I18nRegionListResponse or origin data\n        \"\"\"\n\n        args = {\"hl\": hl, \"part\": enf_parts(resource=\"i18nRegions\", value=parts)}\n\n        resp = self._request(resource=\"i18nRegions\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return I18nRegionListResponse.from_dict(data)\n\n    def get_members(\n        self,\n        *,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        mode: Optional[str] = \"all_current\",\n        count: Optional[int] = 5,\n        limit: Optional[int] = 5,\n        page_token: Optional[str] = None,\n        has_access_to_level: Optional[str] = None,\n        filter_by_member_channel_id: Optional[Union[str, list, tuple, set]] = None,\n        return_json: Optional[bool] = False,\n    ) -> Union[MemberListResponse, dict]:\n        \"\"\"\n        Retrieve a list of members for a channel.\n\n        Args:\n            parts ((str,list,tuple,set) optional):\n                The resource parts for member you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            mode:\n                The mode parameter indicates which members will be included in the API response.\n                Set the parameter value to one of the following values:\n                    - all_current (default): List current members, from newest to oldest. When this value is used,\n                        the end of the list is reached when the API response does not contain a nextPageToken.\n                    - updates : List only members that joined or upgraded since the previous API call.\n                        Note: The first call starts a new stream of updates but does not actually return any members.\n                        To start retrieving the membership updates, you need to poll the endpoint using the\n                        nextPageToken at your desired frequency.\n                        Note that when this value is used, the API response always contains a nextPageToken.\n            count (int, optional):\n                The count will retrieve videos data.\n                Default is 5.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For members, this should not be more than 1000.\n                Default is 5.\n            page_token (str, optional):\n                The token of the page of search result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            has_access_to_level (str, optional):\n                The hasAccessToLevel parameter value is a level ID that specifies the minimum level\n                that members in the result set should have.\n            filter_by_member_channel_id ((str,list,tuple,set) optional):\n                A list of channel IDs that can be used to check the membership status of specific users.\n                A maximum of 100 channels can be specified per call.\n            return_json (bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.MemberListResponse instance.\n        Returns:\n            MemberListResponse or original data\n        \"\"\"\n\n        if count is None:\n            limit = 1000\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"part\": enf_parts(resource=\"members\", value=parts),\n            \"maxResults\": limit,\n        }\n\n        if mode:\n            args[\"mode\"] = mode\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        if has_access_to_level:\n            args[\"hasAccessToLevel\"] = has_access_to_level\n\n        if filter_by_member_channel_id:\n            args[\"filterByMemberChannelId\"] = enf_parts(\n                resource=\"filterByMemberChannelId\",\n                value=filter_by_member_channel_id,\n                check=False,\n            )\n\n        res_data = self.paged_by_page_token(\n            resource=\"members\",\n            args=args,\n            count=count,\n        )\n        if return_json:\n            return res_data\n        else:\n            return MemberListResponse.from_dict(res_data)\n\n    def get_membership_levels(\n        self,\n        *,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        return_json: Optional[bool] = False,\n    ) -> Union[MembershipsLevelListResponse, dict]:\n        \"\"\"\n        Retrieve membership levels for a channel\n\n        Notes:\n            This requires your authorization.\n\n        Args:\n            parts ((str,list,tuple,set) optional):\n                The resource parts for membership level you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            return_json (bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.MembershipsLevelListResponse instance.\n\n        Returns:\n            MembershipsLevelListResponse or original data\n        \"\"\"\n\n        args = {\n            \"part\": enf_parts(resource=\"membershipsLevels\", value=parts),\n        }\n\n        resp = self._request(resource=\"membershipsLevels\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return MembershipsLevelListResponse.from_dict(data)\n\n    def get_playlist_item_by_id(\n        self,\n        *,\n        playlist_item_id: Union[str, list, tuple, set],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve playlist Items info by your given id\n\n        Args:\n            playlist_item_id ((str,list,tuple,set)):\n                The id for playlist item that you want to retrieve info.\n                You can pass this with single id str, comma-separated id str.\n                Or a list,tuple,set of ids.\n            parts ((str,list,tuple,set) optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.PlayListItemApiResponse instance.\n        Returns:\n            PlaylistItemListResponse or original data\n        \"\"\"\n\n        args = {\n            \"id\": enf_comma_separated(\"playlist_item_id\", playlist_item_id),\n            \"part\": enf_parts(resource=\"playlistItems\", value=parts),\n        }\n\n        resp = self._request(resource=\"playlistItems\", method=\"GET\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return PlaylistItemListResponse.from_dict(data)\n\n    def get_playlist_items(\n        self,\n        *,\n        playlist_id: str,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        video_id: Optional[str] = None,\n        count: Optional[int] = 5,\n        limit: Optional[int] = 5,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve playlist Items info by your given playlist id\n\n        Args:\n            playlist_id (str):\n                The id for playlist that you want to retrieve items data.\n            parts ((str,list,tuple,set) optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            video_id (str, Optional):\n                Specifies that the request should return only the playlist items that contain the specified video.\n            count (int, optional):\n                The count will retrieve playlist items data.\n                Default is 5.\n                If provide this with None, will retrieve all playlist items.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For playlistItem, this should not be more than 50.\n                Default is 5\n            page_token(str, optional):\n                The token of the page of playlist items result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.PlayListItemApiResponse instance.\n        Returns:\n            PlaylistItemListResponse or original data\n        \"\"\"\n\n        if count is None:\n            limit = 50  # for playlistItems the max limit for per request is 50\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"playlistId\": playlist_id,\n            \"part\": enf_parts(resource=\"playlistItems\", value=parts),\n            \"maxResults\": limit,\n        }\n        if video_id is not None:\n            args[\"videoId\"] = video_id\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(\n            resource=\"playlistItems\", args=args, count=count\n        )\n        if return_json:\n            return res_data\n        else:\n            return PlaylistItemListResponse.from_dict(res_data)\n\n    def get_playlist_by_id(\n        self,\n        *,\n        playlist_id: Union[str, list, tuple, set],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = \"en_US\",\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve playlist data by given playlist id.\n\n        Args:\n            playlist_id ((str,list,tuple,set)):\n                The id for playlist that you want to retrieve data.\n                You can pass this with single id str,comma-separated id str, or list, tuple, set of id str.\n            parts (str, optional):\n                Comma-separated list of one or more playlist resource properties.\n                You can also pass this with list, tuple, set of part str.\n                If not provided. will use default public properties.\n            hl (str, optional):\n                If provide this. Will return playlist's language localized info.\n                This value need https://developers.google.com/youtube/v3/docs/i18nLanguages.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.PlaylistListResponse instance\n        Returns:\n            PlaylistListResponse or original data\n        \"\"\"\n        args = {\n            \"id\": enf_comma_separated(\"playlist_id\", playlist_id),\n            \"part\": enf_parts(resource=\"playlists\", value=parts),\n            \"hl\": hl,\n        }\n\n        resp = self._request(resource=\"playlists\", method=\"GET\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return PlaylistListResponse.from_dict(data)\n\n    def get_playlists(\n        self,\n        *,\n        channel_id: Optional[str] = None,\n        mine: Optional[bool] = None,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        count: Optional[int] = 5,\n        limit: Optional[int] = 5,\n        hl: Optional[str] = \"en_US\",\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve channel playlists info from youtube data api.\n\n        Args:\n            channel_id (str, optional):\n                If provide channel id, this will return pointed channel's playlist info.\n            mine (bool, optional):\n                If you have given the authorization. Will return your playlists.\n                Must provide the access token.\n            parts (str, optional):\n                Comma-separated list of one or more playlist resource properties.\n                You can also pass this with list, tuple, set of part str.\n                If not provided. will use default public properties.\n            count (int, optional):\n                The count will retrieve playlist data.\n                Default is 5.\n                If provide this with None, will retrieve all playlists.\n            limit (int, optional):\n                The maximum number of items each request to retrieve.\n                For playlist, this should not be more than 50.\n                Default is 5\n            hl (str, optional):\n                If provide this. Will return playlist's language localized info.\n                This value need https://developers.google.com/youtube/v3/docs/i18nLanguages.\n            page_token(str, optional):\n                The token of the page of playlists result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.PlaylistListResponse instance.\n        Returns:\n            PlaylistListResponse or original data\n        \"\"\"\n\n        if count is None:\n            limit = 50  # for playlists the max limit for per request is 50\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"part\": enf_parts(resource=\"playlists\", value=parts),\n            \"hl\": hl,\n            \"maxResults\": limit,\n        }\n\n        if channel_id is not None:\n            args[\"channelId\"] = channel_id\n        elif mine is not None:\n            args[\"mine\"] = mine\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=f\"Specify at least one of channel_id,playlist_id or mine\",\n                )\n            )\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(\n            resource=\"playlists\", args=args, count=count\n        )\n        if return_json:\n            return res_data\n        else:\n            return PlaylistListResponse.from_dict(res_data)\n\n    def search(\n        self,\n        *,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        for_developer: Optional[bool] = None,\n        for_mine: Optional[bool] = None,\n        related_to_video_id: Optional[str] = None,\n        channel_id: Optional[str] = None,\n        channel_type: Optional[str] = None,\n        event_type: Optional[str] = None,\n        location: Optional[str] = None,\n        location_radius: Optional[str] = None,\n        count: Optional[int] = 10,\n        limit: Optional[int] = 10,\n        order: Optional[str] = None,\n        published_after: Optional[str] = None,\n        published_before: Optional[str] = None,\n        q: Optional[str] = None,\n        region_code: Optional[str] = None,\n        relevance_language: Optional[str] = None,\n        safe_search: Optional[str] = None,\n        topic_id: Optional[str] = None,\n        search_type: Optional[Union[str, list, tuple, set]] = None,\n        video_caption: Optional[str] = None,\n        video_category_id: Optional[str] = None,\n        video_definition: Optional[str] = None,\n        video_dimension: Optional[str] = None,\n        video_duration: Optional[str] = None,\n        video_embeddable: Optional[str] = None,\n        video_license: Optional[str] = None,\n        video_paid_product_placement: Optional[str] = None,\n        video_syndicated: Optional[str] = None,\n        video_type: Optional[str] = None,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n    ) -> Union[SearchListResponse, dict]:\n        \"\"\"\n        Main search api implementation.\n        You can find all parameters description at https://developers.google.com/youtube/v3/docs/search/list#parameters\n\n        Returns:\n            SearchListResponse or original data\n        \"\"\"\n        parts = enf_parts(resource=\"search\", value=parts)\n        if search_type is None:\n            search_type = \"video,channel,playlist\"\n        else:\n            search_type = enf_comma_separated(field=\"search_type\", value=search_type)\n\n        args = {\n            \"part\": parts,\n            \"maxResults\": min(limit, count),\n        }\n        if for_developer:\n            args[\"forDeveloper\"] = for_developer\n        if for_mine:\n            args[\"forMine\"] = for_mine\n        if related_to_video_id:\n            args[\"relatedToVideoId\"] = related_to_video_id\n        if channel_id:\n            args[\"channelId\"] = channel_id\n        if channel_type:\n            args[\"channelType\"] = channel_type\n        if event_type:\n            args[\"eventType\"] = event_type\n        if location:\n            args[\"location\"] = location\n        if location_radius:\n            args[\"locationRadius\"] = location_radius\n        if order:\n            args[\"order\"] = order\n        if published_after:\n            args[\"publishedAfter\"] = published_after\n        if published_before:\n            args[\"publishedBefore\"] = published_before\n        if q:\n            args[\"q\"] = q\n        if region_code:\n            args[\"regionCode\"] = region_code\n        if relevance_language:\n            args[\"relevanceLanguage\"] = relevance_language\n        if safe_search:\n            args[\"safeSearch\"] = safe_search\n        if topic_id:\n            args[\"topicId\"] = topic_id\n        if search_type:\n            args[\"type\"] = search_type\n        if video_caption:\n            args[\"videoCaption\"] = video_caption\n        if video_category_id:\n            args[\"videoCategoryId\"] = video_category_id\n        if video_definition:\n            args[\"videoDefinition\"] = video_definition\n        if video_dimension:\n            args[\"videoDimension\"] = video_dimension\n        if video_duration:\n            args[\"videoDuration\"] = video_duration\n        if video_embeddable:\n            args[\"videoEmbeddable\"] = video_embeddable\n        if video_license:\n            args[\"videoLicense\"] = video_license\n        if video_paid_product_placement:\n            args[\"videoPaidProductPlacement\"] = video_paid_product_placement\n        if video_syndicated:\n            args[\"videoSyndicated\"] = video_syndicated\n        if video_type:\n            args[\"videoType\"] = video_type\n        if page_token:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(resource=\"search\", args=args, count=count)\n\n        if return_json:\n            return res_data\n        else:\n            return SearchListResponse.from_dict(res_data)\n\n    def search_by_keywords(\n        self,\n        *,\n        q: Optional[str],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        search_type: Optional[Union[str, list, tuple, set]] = None,\n        count: Optional[int] = 25,\n        limit: Optional[int] = 25,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n        **kwargs: Optional[dict],\n    ) -> Union[SearchListResponse, dict]:\n        \"\"\"\n        This is simplest usage for search api. You can only passed the keywords to retrieve data from YouTube.\n        And the result will include videos,playlists and channels.\n\n        Note: A call to this method has a quota cost of 100 units.\n\n        Args:\n            q (str):\n                Your keywords can also use the Boolean NOT (-) and OR (|) operators to exclude videos or\n                to find videos that are associated with one of several search terms. For example,\n                to search for videos matching either \"boating\" or \"sailing\",\n                set the q parameter value to boating|sailing. Similarly,\n                to search for videos matching either \"boating\" or \"sailing\" but not \"fishing\",\n                set the q parameter value to boating|sailing -fishing.\n                Note that the pipe character must be URL-escaped when it is sent in your API request.\n                The URL-escaped value for the pipe character is %7C.\n            parts ((str,list,tuple,set) optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            search_type ((str,list,tuple,set), optional):\n                Parameter restricts a search query to only retrieve a particular type of resource.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n                The default value is video,channel,playlist\n                Acceptable values are:\n                    - channel\n                    - playlist\n                    - video\n            count (int, optional):\n                The count will retrieve videos data.\n                Default is 25.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For search, this should not be more than 50.\n                Default is 25.\n            page_token (str, optional):\n                The token of the page of search result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.SearchListResponse instance.\n            kwargs:\n                If you want use this pass more args. You can use this.\n\n        Returns:\n            SearchListResponse or original data\n        \"\"\"\n        return self.search(\n            parts=parts,\n            q=q,\n            search_type=search_type,\n            count=count,\n            limit=limit,\n            page_token=page_token,\n            return_json=return_json,\n            **kwargs,\n        )\n\n    def search_by_developer(\n        self,\n        *,\n        parts: Optional[Union[str, list, tuple, set]],\n        q: Optional[str] = None,\n        count: Optional[int] = 25,\n        limit: Optional[int] = 25,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n        **kwargs,\n    ) -> Union[SearchListResponse, dict]:\n        \"\"\"\n        Parameter restricts the search to only retrieve videos uploaded via the developer's application or website.\n\n        Args:\n            parts:\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            q:\n                Your keywords can also use the Boolean NOT (-) and OR (|) operators to exclude videos or\n                to find videos that are associated with one of several search terms. For example,\n                to search for videos matching either \"boating\" or \"sailing\",\n                set the q parameter value to boating|sailing. Similarly,\n                to search for videos matching either \"boating\" or \"sailing\" but not \"fishing\",\n                set the q parameter value to boating|sailing -fishing.\n                Note that the pipe character must be URL-escaped when it is sent in your API request.\n                The URL-escaped value for the pipe character is %7C.\n            count:\n                The count will retrieve videos data.\n                Default is 25.\n            limit:\n                The maximum number of items each request retrieve.\n                For search, this should not be more than 50.\n                Default is 25.\n            page_token:\n                The token of the page of search result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json:\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.SearchListResponse instance.\n            kwargs:\n                If you want use this pass more args. You can use this.\n\n        Returns:\n            SearchListResponse or original data\n        \"\"\"\n        return self.search(\n            for_developer=True,\n            search_type=\"video\",\n            parts=parts,\n            q=q,\n            count=count,\n            limit=limit,\n            page_token=page_token,\n            return_json=return_json,\n            **kwargs,\n        )\n\n    def search_by_mine(\n        self,\n        *,\n        parts: Optional[Union[str, list, tuple, set]],\n        q: Optional[str] = None,\n        count: Optional[int] = 25,\n        limit: Optional[int] = 25,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n        **kwargs,\n    ) -> Union[SearchListResponse, dict]:\n        \"\"\"\n        Parameter restricts the search to only retrieve videos owned by the authenticated user.\n\n        Note:\n            This methods can not use following parameters:\n            video_definition, video_dimension, video_duration, video_license,\n            video_embeddable, video_syndicated, video_type.\n        Args:\n            q:\n                Your keywords can also use the Boolean NOT (-) and OR (|) operators to exclude videos or\n                to find videos that are associated with one of several search terms. For example,\n                to search for videos matching either \"boating\" or \"sailing\",\n                set the q parameter value to boating|sailing. Similarly,\n                to search for videos matching either \"boating\" or \"sailing\" but not \"fishing\",\n                set the q parameter value to boating|sailing -fishing.\n                Note that the pipe character must be URL-escaped when it is sent in your API request.\n                The URL-escaped value for the pipe character is %7C.\n            parts ((str,list,tuple,set) optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            count (int, optional):\n                The count will retrieve videos data.\n                Default is 25.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For search, this should not be more than 50.\n                Default is 25.\n            page_token (str, optional):\n                The token of the page of search result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.SearchListResponse instance.\n            kwargs:\n                If you want use this pass more args. You can use this.\n\n        Returns:\n            SearchListResponse or original data\n        \"\"\"\n        return self.search(\n            for_mine=True,\n            search_type=\"video\",\n            parts=parts,\n            q=q,\n            count=count,\n            limit=limit,\n            page_token=page_token,\n            return_json=return_json,\n            **kwargs,\n        )\n\n    def search_by_related_video(\n        self,\n        *,\n        related_to_video_id: str,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        region_code: Optional[str] = None,\n        relevance_language: Optional[str] = None,\n        safe_search: Optional[str] = None,\n        count: Optional[int] = 25,\n        limit: Optional[int] = 25,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n    ) -> Union[SearchListResponse, dict]:\n        \"\"\"\n        Retrieve a list of videos related to that video.\n\n        Args:\n            related_to_video_id:\n                 A YouTube video ID which result associated with.\n            parts:\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            region_code:\n                Parameter instructs the API to return search results for videos\n                that can be viewed in the specified country.\n            relevance_language:\n                Parameter instructs the API to return search results that are most relevant to the specified language.\n            safe_search:\n                Parameter indicates whether the search results should include restricted content\n                as well as standard content.\n                Acceptable values are:\n                    - moderate – YouTube will filter some content from search results and, at the least,\n                      will filter content that is restricted in your locale. Based on their content,\n                      search results could be removed from search results or demoted in search results.\n                      This is the default parameter value.\n                    - none – YouTube will not filter the search result set.\n                    - strict – YouTube will try to exclude all restricted content from the search result set.\n                      Based on their content, search results could be removed from search results or\n                      demoted in search results.\n            count:\n                The count will retrieve videos data.\n                Default is 25.\n            limit:\n                The maximum number of items each request retrieve.\n                For search, this should not be more than 50.\n                Default is 25.\n            page_token:\n                The token of the page of search result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json:\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.SearchListResponse instance.\n        Returns:\n            If you want use this pass more args. You can use this.\n        \"\"\"\n\n        return self.search(\n            parts=parts,\n            related_to_video_id=related_to_video_id,\n            search_type=\"video\",\n            region_code=region_code,\n            relevance_language=relevance_language,\n            safe_search=safe_search,\n            count=count,\n            limit=limit,\n            page_token=page_token,\n            return_json=return_json,\n        )\n\n    def get_subscription_by_id(\n        self,\n        *,\n        subscription_id: Union[str, list, tuple, set],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve subscriptions by given subscription id(s).\n\n        Note:\n            This need authorized access token. or you will get no data.\n\n        Args:\n            subscription_id ((str,list,tuple,set)):\n                The id for subscription that you want to retrieve data.\n                You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids.\n            parts ((str,list,tuple,set), optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.SubscriptionListResponse instance.\n        Returns:\n            SubscriptionListResponse or original data.\n        \"\"\"\n\n        args = {\n            \"id\": enf_comma_separated(field=\"subscription_id\", value=subscription_id),\n            \"part\": enf_parts(resource=\"subscriptions\", value=parts),\n        }\n\n        resp = self._request(resource=\"subscriptions\", method=\"GET\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return SubscriptionListResponse.from_dict(data)\n\n    def get_subscription_by_channel(\n        self,\n        *,\n        channel_id: str,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        for_channel_id: Optional[Union[str, list, tuple, set]] = None,\n        order: Optional[str] = \"relevance\",\n        count: Optional[int] = 20,\n        limit: Optional[int] = 20,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve the specified channel's subscriptions.\n\n        Note:\n             The API returns a 403 (Forbidden) HTTP response code if the specified channel\n             does not publicly expose its subscriptions and the request is not authorized\n             by the channel's owner.\n\n        Args:\n            channel_id (str):\n                The id for channel which you want to get subscriptions.\n            parts ((str,list,tuple,set) optional):\n                The resource parts for subscription you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            for_channel_id ((str,list,tuple,set) optional):\n                The parameter specifies a comma-separated list of channel IDs.\n                and will then only contain subscriptions matching those channels.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of channel ids.\n            order (str, optional):\n                The parameter specifies the method that will be used to sort resources in the API response.\n                Acceptable values are:\n                    alphabetical – Sort alphabetically.\n                    relevance – Sort by relevance.\n                    unread – Sort by order of activity.\n                Default is relevance\n            count (int, optional):\n                The count will retrieve subscriptions data.\n                Default is 20.\n                If provide this with None, will retrieve all subscriptions.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For comment threads, this should not be more than 50.\n                Default is 20.\n            page_token(str, optional):\n                The token of the page of subscriptions result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.SubscriptionListResponse instance.\n        Returns:\n            SubscriptionListResponse or original data.\n        \"\"\"\n\n        if count is None:\n            limit = 50  # for subscriptions the max limit for per request is 50\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"channelId\": channel_id,\n            \"part\": enf_parts(resource=\"subscriptions\", value=parts),\n            \"order\": order,\n            \"maxResults\": limit,\n        }\n\n        if for_channel_id is not None:\n            args[\"forChannelId\"] = enf_comma_separated(\n                field=\"for_channel_id\", value=for_channel_id\n            )\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(\n            resource=\"subscriptions\", args=args, count=count\n        )\n        if return_json:\n            return res_data\n        else:\n            return SubscriptionListResponse.from_dict(res_data)\n\n    def get_subscription_by_me(\n        self,\n        *,\n        mine: Optional[bool] = None,\n        recent_subscriber: Optional[bool] = None,\n        subscriber: Optional[bool] = None,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        for_channel_id: Optional[Union[str, list, tuple, set]] = None,\n        order: Optional[str] = \"relevance\",\n        count: Optional[int] = 20,\n        limit: Optional[int] = 20,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve your subscriptions.\n\n        Note:\n            This can only used in a properly authorized request.\n            And for me test the parameter `recent_subscriber` and `subscriber` maybe not working.\n            Use the `mine` first.\n\n        Args:\n            mine (bool, optional):\n                Set this parameter's value to True to retrieve a feed of the authenticated user's subscriptions.\n            recent_subscriber (bool, optional):\n                Set this parameter's value to true to retrieve a feed of the subscribers of the authenticated user\n                in reverse chronological order (newest first).\n                And this can only get most recent 1000 subscribers.\n            subscriber (bool, optional):\n                Set this parameter's value to true to retrieve a feed of the subscribers of\n                the authenticated user in no particular order.\n            parts ((str,list,tuple,set) optional):\n                The resource parts for subscription you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            for_channel_id ((str,list,tuple,set) optional):\n                The parameter specifies a comma-separated list of channel IDs.\n                and will then only contain subscriptions matching those channels.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of channel ids.\n            order (str, optional):\n                The parameter specifies the method that will be used to sort resources in the API response.\n                Acceptable values are:\n                    alphabetical – Sort alphabetically.\n                    relevance – Sort by relevance.\n                    unread – Sort by order of activity.\n                Default is relevance\n            count (int, optional):\n                The count will retrieve subscriptions data.\n                Default is 20.\n                If provide this with None, will retrieve all subscriptions.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For subscriptions, this should not be more than 50.\n                Default is 20.\n            page_token(str, optional):\n                The token of the page of subscriptions result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.SubscriptionListResponse instance.\n\n        Returns:\n            SubscriptionListResponse or original data.\n        \"\"\"\n\n        if count is None:\n            limit = 50  # for subscriptions the max limit for per request is 50\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"part\": enf_parts(resource=\"subscriptions\", value=parts),\n            \"order\": order,\n            \"maxResults\": limit,\n        }\n\n        if mine is not None:\n            args[\"mine\"] = mine\n        elif recent_subscriber is not None:\n            args[\"myRecentSubscribers\"] = recent_subscriber\n        elif subscriber is not None:\n            args[\"mySubscribers\"] = subscriber\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=f\"Must specify at least one of mine,recent_subscriber,subscriber.\",\n                )\n            )\n\n        if for_channel_id is not None:\n            args[\"forChannelId\"] = enf_comma_separated(\n                field=\"for_channel_id\", value=for_channel_id\n            )\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(\n            resource=\"subscriptions\", args=args, count=count\n        )\n        if return_json:\n            return res_data\n        else:\n            return SubscriptionListResponse.from_dict(res_data)\n\n    def get_video_abuse_report_reason(\n        self,\n        *,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = \"en_US\",\n        return_json: Optional[bool] = False,\n    ) -> Union[VideoAbuseReportReasonListResponse, dict]:\n        \"\"\"\n        Retrieve a list of reasons that can be used to report abusive videos.\n\n        Notes:\n            This requires your authorization.\n\n        Args:\n            parts:\n                The resource parts for abuse reason you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            hl:\n                If provide this. Will return report reason's language localized info.\n                This value need https://developers.google.com/youtube/v3/docs/i18nLanguages.\n            return_json:\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.VideoAbuseReportReasonListResponse instance.\n        Returns:\n            VideoAbuseReportReasonListResponse or original data.\n        \"\"\"\n\n        args = {\n            \"part\": enf_parts(resource=\"videoAbuseReportReasons\", value=parts),\n            \"hl\": hl,\n        }\n\n        resp = self._request(resource=\"videoAbuseReportReasons\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return VideoAbuseReportReasonListResponse.from_dict(data)\n\n    def get_video_categories(\n        self,\n        *,\n        category_id: Optional[Union[str, list, tuple, set]] = None,\n        region_code: Optional[str] = None,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = \"en_US\",\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve video categories by category id or region code.\n\n        Args:\n            category_id ((str,list,tuple,set), optional):\n                The id for video category thread that you want to retrieve data.\n                You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids.\n            region_code (str, optional):\n                The region code that you want to retrieve guide categories.\n                The parameter value is an ISO 3166-1 alpha-2 country code.\n                Refer: https://www.iso.org/iso-3166-country-codes.html\n            parts ((str,list,tuple,set) optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            hl (str, optional):\n                If provide this. Will return video category's language localized info.\n                This value need https://developers.google.com/youtube/v3/docs/i18nLanguages.\n                Default is en_US.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.VideoCategoryListResponse instance.\n        Returns:\n            VideoCategoryListResponse or original data\n        \"\"\"\n        args = {\n            \"part\": enf_parts(resource=\"videoCategories\", value=parts),\n            \"hl\": hl,\n        }\n\n        if category_id is not None:\n            args[\"id\"] = enf_comma_separated(field=\"category_id\", value=category_id)\n        elif region_code is not None:\n            args[\"regionCode\"] = region_code\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Specify at least one of category_id or region_code\",\n                )\n            )\n\n        resp = self._request(resource=\"videoCategories\", method=\"GET\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return VideoCategoryListResponse.from_dict(data)\n\n    def get_video_by_id(\n        self,\n        *,\n        video_id: Union[str, list, tuple, set],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = \"en_US\",\n        max_height: Optional[int] = None,\n        max_width: Optional[int] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve video data by given video id.\n\n        Args:\n            video_id ((str,list,tuple,set)):\n                The id for video that you want to retrieve data.\n                You can pass this with single id str, comma-separated id str, or a list,tuple,set of ids.\n            parts ((str,list,tuple,set), optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            hl (str, optional):\n                If provide this. Will return video's language localized info.\n                This value need https://developers.google.com/youtube/v3/docs/i18nLanguages.\n            max_height (int, optional):\n                Specifies the maximum height of the embedded player returned in the player.embedHtml property.\n                Acceptable values are 72 to 8192, inclusive.\n            max_width (int, optional):\n                Specifies the maximum width of the embedded player returned in the player.embedHtml property.\n                Acceptable values are 72 to 8192, inclusive.\n                If provide max_height at the same time. This will may be shorter than max_height.\n                For more https://developers.google.com/youtube/v3/docs/videos/list#parameters.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.VideoListResponse instance.\n\n        Returns:\n            VideoListResponse or original data\n        \"\"\"\n\n        args = {\n            \"id\": enf_comma_separated(field=\"video_id\", value=video_id),\n            \"part\": enf_parts(resource=\"videos\", value=parts),\n            \"hl\": hl,\n        }\n        if max_height is not None:\n            args[\"maxHeight\"] = max_height\n        if max_width is not None:\n            args[\"maxWidth\"] = max_width\n\n        resp = self._request(resource=\"videos\", method=\"GET\", args=args)\n        data = self._parse_response(resp)\n\n        if return_json:\n            return data\n        else:\n            return VideoListResponse.from_dict(data)\n\n    def get_videos_by_chart(\n        self,\n        *,\n        chart: str,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = \"en_US\",\n        max_height: Optional[int] = None,\n        max_width: Optional[int] = None,\n        region_code: Optional[str] = None,\n        category_id: Optional[str] = \"0\",\n        count: Optional[int] = 5,\n        limit: Optional[int] = 5,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve a list of YouTube's most popular videos.\n\n        Args:\n            chart (str):\n                The chart string for you want to retrieve data.\n                Acceptable values are: mostPopular\n            parts ((str,list,tuple,set), optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            hl (str, optional):\n                If provide this. Will return playlist's language localized info.\n                This value need https://developers.google.com/youtube/v3/docs/i18nLanguages.\n            max_height (int, optional):\n                Specifies the maximum height of the embedded player returned in the player.embedHtml property.\n                Acceptable values are 72 to 8192, inclusive.\n            max_width (int, optional):\n                Specifies the maximum width of the embedded player returned in the player.embedHtml property.\n                Acceptable values are 72 to 8192, inclusive.\n                If provide max_height at the same time. This will may be shorter than max_height.\n                For more https://developers.google.com/youtube/v3/docs/videos/list#parameters.\n            region_code (str, optional):\n                This parameter instructs the API to select a video chart available in the specified region.\n                Value is an ISO 3166-1 alpha-2 country code.\n            category_id (str, optional):\n                The id for video category that you want to filter.\n                Default is 0.\n            count (int, optional):\n                The count will retrieve videos data.\n                Default is 5.\n                If provide this with None, will retrieve all videos.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For videos, this should not be more than 50.\n                Default is 5.\n            page_token(str, optional):\n                The token of the page of videos result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.PlaylistListResponse instance.\n\n        Returns:\n            VideoListResponse or original data\n        \"\"\"\n\n        if count is None:\n            limit = 50  # for videos the max limit for per request is 50\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"chart\": chart,\n            \"part\": enf_parts(resource=\"videos\", value=parts),\n            \"hl\": hl,\n            \"maxResults\": limit,\n            \"videoCategoryId\": category_id,\n        }\n        if max_height is not None:\n            args[\"maxHeight\"] = max_height\n        if max_width is not None:\n            args[\"maxWidth\"] = max_width\n        if region_code:\n            args[\"regionCode\"] = region_code\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(resource=\"videos\", args=args, count=count)\n        if return_json:\n            return res_data\n        else:\n            return VideoListResponse.from_dict(res_data)\n\n    def get_videos_by_myrating(\n        self,\n        *,\n        rating: str,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = \"en_US\",\n        max_height: Optional[int] = None,\n        max_width: Optional[int] = None,\n        count: Optional[int] = 5,\n        limit: Optional[int] = 5,\n        page_token: Optional[str] = None,\n        return_json: Optional[bool] = False,\n    ):\n        \"\"\"\n        Retrieve video data by my ration.\n\n        Args:\n            rating (str):\n                The rating string for you to retrieve data.\n                Acceptable values are: dislike, like\n            parts ((str,list,tuple,set), optional):\n                The resource parts for you want to retrieve.\n                If not provide, use default public parts.\n                You can pass this with single part str, comma-separated parts str or a list,tuple,set of parts.\n            hl (str, optional):\n                If provide this. Will return video's language localized info.\n                This value need https://developers.google.com/youtube/v3/docs/i18nLanguages.\n            max_height (int, optional):\n                Specifies the maximum height of the embedded player returned in the player.embedHtml property.\n                Acceptable values are 72 to 8192, inclusive.\n            max_width (int, optional):\n                Specifies the maximum width of the embedded player returned in the player.embedHtml property.\n                Acceptable values are 72 to 8192, inclusive.\n                If provide max_height at the same time. This will may be shorter than max_height.\n                For more https://developers.google.com/youtube/v3/docs/videos/list#parameters.\n            count (int, optional):\n                The count will retrieve videos data.\n                Default is 5.\n                If provide this with None, will retrieve all videos.\n            limit (int, optional):\n                The maximum number of items each request retrieve.\n                For videos, this should not be more than 50.\n                Default is 5.\n            page_token(str, optional):\n                The token of the page of videos result to retrieve.\n                You can use this retrieve point result page directly.\n                And you should know about the the result set for YouTube.\n            return_json(bool, optional):\n                The return data type. If you set True JSON data will be returned.\n                False will return a pyyoutube.VideoListResponse instance.\n        Returns:\n            VideoListResponse or original data\n        \"\"\"\n\n        if self._access_token is None:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.NEED_AUTHORIZATION,\n                    message=\"This method can only used with authorization\",\n                )\n            )\n\n        if count is None:\n            limit = 50  # for videos the max limit for per request is 50\n        else:\n            limit = min(count, limit)\n\n        args = {\n            \"myRating\": rating,\n            \"part\": enf_parts(resource=\"videos\", value=parts),\n            \"hl\": hl,\n            \"maxResults\": limit,\n        }\n\n        if max_height is not None:\n            args[\"maxHeight\"] = max_height\n        if max_width is not None:\n            args[\"maxWidth\"] = max_width\n\n        if page_token is not None:\n            args[\"pageToken\"] = page_token\n\n        res_data = self.paged_by_page_token(resource=\"videos\", args=args, count=count)\n        if return_json:\n            return res_data\n        else:\n            return VideoListResponse.from_dict(res_data)\n"
  },
  {
    "path": "pyyoutube/client.py",
    "content": "\"\"\"\nNew Client for YouTube API\n\"\"\"\n\nimport inspect\nimport json\nfrom typing import List, Optional, Tuple, Union\n\nimport requests\nfrom requests import Response\nfrom requests.sessions import merge_setting\nfrom requests.structures import CaseInsensitiveDict\nfrom requests_oauthlib.oauth2_session import OAuth2Session\n\nimport pyyoutube.resources as resources\nfrom pyyoutube.models.base import BaseModel\nfrom pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException\nfrom pyyoutube.models import (\n    AccessToken,\n)\nfrom pyyoutube.resources.base_resource import Resource\n\n\ndef _is_resource_endpoint(obj):\n    return isinstance(obj, Resource)\n\n\nclass Client:\n    \"\"\"Client for YouTube resource\"\"\"\n\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/\"\n    BASE_UPLOAD_URL = \"https://www.googleapis.com/upload/youtube/v3/\"\n    AUTHORIZATION_URL = \"https://accounts.google.com/o/oauth2/v2/auth\"\n    EXCHANGE_ACCESS_TOKEN_URL = \"https://oauth2.googleapis.com/token\"\n    REVOKE_TOKEN_URL = \"https://oauth2.googleapis.com/revoke\"\n    HUB_URL = \"https://pubsubhubbub.appspot.com/subscribe\"\n\n    DEFAULT_REDIRECT_URI = \"https://localhost/\"\n    DEFAULT_SCOPE = [\n        \"https://www.googleapis.com/auth/youtube\",\n        \"https://www.googleapis.com/auth/userinfo.profile\",\n    ]\n    DEFAULT_STATE = \"Python-YouTube\"\n\n    activities = resources.ActivitiesResource()\n    captions = resources.CaptionsResource()\n    channels = resources.ChannelsResource()\n    channelBanners = resources.ChannelBannersResource()\n    channelSections = resources.ChannelSectionsResource()\n    comments = resources.CommentsResource()\n    commentThreads = resources.CommentThreadsResource()\n    i18nLanguages = resources.I18nLanguagesResource()\n    i18nRegions = resources.I18nRegionsResource()\n    members = resources.MembersResource()\n    membershipsLevels = resources.MembershipLevelsResource()\n    playlistItems = resources.PlaylistItemsResource()\n    playlists = resources.PlaylistsResource()\n    search = resources.SearchResource()\n    subscriptions = resources.SubscriptionsResource()\n    thumbnails = resources.ThumbnailsResource()\n    videoAbuseReportReasons = resources.VideoAbuseReportReasonsResource()\n    videoCategories = resources.VideoCategoriesResource()\n    videos = resources.VideosResource()\n    watermarks = resources.WatermarksResource()\n\n    def __new__(cls, *args, **kwargs):\n        self = super().__new__(cls)\n        sub_resources = inspect.getmembers(self, _is_resource_endpoint)\n        for name, resource in sub_resources:\n            resource_cls = type(resource)\n            resource = resource_cls(self)\n            setattr(self, name, resource)\n\n        return self\n\n    def __init__(\n        self,\n        client_id: Optional[str] = None,\n        client_secret: Optional[str] = None,\n        access_token: Optional[str] = None,\n        refresh_token: Optional[str] = None,\n        api_key: Optional[str] = None,\n        client_secret_path: Optional[str] = None,\n        timeout: Optional[int] = None,\n        proxies: Optional[dict] = None,\n        headers: Optional[dict] = None,\n    ) -> None:\n        \"\"\"Class initial\n\n        Args:\n            client_id:\n                ID for your app.\n            client_secret:\n                Secret for your app.\n            access_token:\n                Access token for user authorized with your app.\n            refresh_token:\n                Refresh Token for user.\n            api_key:\n                API key for your app which generated from api console.\n            client_secret_path:\n                path to the client_secret.json file provided by google console\n            timeout:\n                Timeout for every request.\n            proxies:\n                Proxies for every request.\n            headers:\n                Headers for every request.\n\n        Raises:\n            PyYouTubeException: Missing either credentials.\n        \"\"\"\n        self.client_id = client_id\n        self.client_secret = client_secret\n        self.access_token = access_token\n        self.refresh_token = refresh_token\n        self.api_key = api_key\n        self.timeout = timeout\n        self.proxies = proxies\n        self.headers = headers\n\n        self.session = requests.Session()\n        self.merge_headers()\n\n        if not self._has_client_data() and client_secret_path is not None:\n            # try to use client_secret file\n            self._from_client_secrets_file(client_secret_path)\n\n        # Auth settings\n        if not (self._has_auth_credentials() or self._has_client_data()):\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Must specify either client key info or api key.\",\n                )\n            )\n\n    def _from_client_secrets_file(self, client_secret_path: str):\n        \"\"\"Set credentials from client_sectet file\n\n        Args:\n            client_secret_path:\n                path to the client_secret.json file, provided by google console\n\n        Raises:\n            PyYouTubeException: missing required key, client_secret file not in 'web' format.\n        \"\"\"\n\n        with open(client_secret_path, \"r\") as f:\n            secrets_data = json.load(f)\n\n        credentials = None\n        for secrets_type in [\"web\", \"installed\"]:\n            if secrets_type in secrets_data:\n                credentials = secrets_data[secrets_type]\n\n        if not credentials:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.INVALID_PARAMS,\n                    message=\"Only 'web' and 'installed' type client_secret files are supported.\",\n                )\n            )\n\n        # check for reqiered fields\n        for field in [\"client_secret\", \"client_id\"]:\n            if field not in credentials:\n                raise PyYouTubeException(\n                    ErrorMessage(\n                        status_code=ErrorCode.MISSING_PARAMS,\n                        message=f\"missing required field '{field}'.\",\n                    )\n                )\n\n        self.client_id = credentials[\"client_id\"]\n        self.client_secret = credentials[\"client_secret\"]\n\n        # Set default redirect to first defined in client_secrets file if any\n        if \"redirect_uris\" in credentials and len(credentials[\"redirect_uris\"]) > 0:\n            self.DEFAULT_REDIRECT_URI = credentials[\"redirect_uris\"][0]\n\n    def _has_auth_credentials(self) -> bool:\n        return self.api_key or self.access_token\n\n    def _has_client_data(self) -> bool:\n        return self.client_id and self.client_secret\n\n    def merge_headers(self):\n        \"\"\"Merge custom headers to session.\"\"\"\n        if self.headers:\n            self.session.headers = merge_setting(\n                request_setting=self.session.headers,\n                session_setting=self.headers,\n                dict_class=CaseInsensitiveDict,\n            )\n\n    @staticmethod\n    def parse_response(response: Response) -> dict:\n        \"\"\"Response parser\n\n        Args:\n            response:\n                Response from the Response.\n\n        Returns:\n            Response dict data.\n\n        Raises:\n            PyYouTubeException: If response has errors.\n        \"\"\"\n        data = response.json()\n        if \"error\" in data:\n            raise PyYouTubeException(response)\n        return data\n\n    def request(\n        self,\n        path: str,\n        method: str = \"GET\",\n        params: Optional[dict] = None,\n        data: Optional[dict] = None,\n        json: Optional[dict] = None,\n        enforce_auth: bool = True,\n        is_upload: bool = False,\n        **kwargs,\n    ):\n        \"\"\"Send request to YouTube.\n\n        Args:\n            path:\n                Resource or url for YouTube data. such as channels,videos and so on.\n            method:\n                Method for the request.\n            params:\n                Object to send in the query string of the request.\n            data:\n                Object to send in the body of the request.\n            json:\n                Object json to send in the body of the request.\n            enforce_auth:\n                Whether to use user credentials.\n            is_upload:\n                Whether it is an upload job.\n            kwargs:\n                Additional parameters for request.\n\n        Returns:\n            Response for request.\n\n        Raises:\n            PyYouTubeException: Missing credentials when need credentials.\n                                Request http error.\n        \"\"\"\n        if not path.startswith(\"http\"):\n            base_url = self.BASE_UPLOAD_URL if is_upload else self.BASE_URL\n            path = base_url + path\n\n        # Add credentials to request\n        if enforce_auth:\n            if self.api_key is None and self.access_token is None:\n                raise PyYouTubeException(\n                    ErrorMessage(\n                        status_code=ErrorCode.MISSING_PARAMS,\n                        message=\"You must provide your credentials.\",\n                    )\n                )\n            else:\n                self.add_token_to_headers()\n                params = self.add_api_key_to_params(params=params)\n\n        # If json is dataclass convert to dict\n        if isinstance(json, BaseModel):\n            json = json.to_dict_ignore_none()\n\n        try:\n            response = self.session.request(\n                method=method,\n                url=path,\n                params=params,\n                data=data,\n                json=json,\n                proxies=self.proxies,\n                timeout=self.timeout,\n                **kwargs,\n            )\n        except requests.HTTPError as e:\n            raise PyYouTubeException(\n                ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message=e.args[0])\n            )\n        else:\n            return response\n\n    def add_token_to_headers(self):\n        if self.access_token:\n            self.session.headers.update(\n                {\"Authorization\": f\"Bearer {self.access_token}\"}\n            )\n\n    def add_api_key_to_params(self, params: Optional[dict] = None):\n        if not self.api_key:\n            return params\n        if params is None:\n            params = {\"key\": self.api_key}\n        else:\n            params[\"key\"] = self.api_key\n        return params\n\n    def _get_oauth_session(\n        self,\n        redirect_uri: Optional[str] = None,\n        scope: Optional[List[str]] = None,\n        state: Optional[str] = None,\n        **kwargs,\n    ) -> OAuth2Session:\n        \"\"\"Build request session for authorization\n\n        Args:\n            redirect_uri:\n                Determines how Google's authorization server sends a response to your app.\n                If not provide will use default https://localhost/\n            scope:\n                Permission scope for authorization.\n                see more: https://developers.google.com/identity/protocols/oauth2/scopes#youtube\n            state:\n                State sting for authorization.\n            **kwargs:\n                Additional parameters for session.\n\n        Returns:\n            OAuth2.0 Session\n        \"\"\"\n        redirect_uri = (\n            redirect_uri if redirect_uri is not None else self.DEFAULT_REDIRECT_URI\n        )\n        scope = scope if scope is not None else self.DEFAULT_SCOPE\n        state = state if state is not None else self.DEFAULT_STATE\n\n        return OAuth2Session(\n            client_id=self.client_id,\n            scope=scope,\n            redirect_uri=redirect_uri,\n            state=state,\n            **kwargs,\n        )\n\n    def get_authorize_url(\n        self,\n        redirect_uri: Optional[str] = None,\n        scope: Optional[List[str]] = None,\n        access_type: str = \"offline\",\n        state: Optional[str] = None,\n        include_granted_scopes: Optional[bool] = None,\n        login_hint: Optional[str] = None,\n        prompt: Optional[str] = None,\n        **kwargs,\n    ) -> Tuple[str, str]:\n        \"\"\"Get authorize url for user.\n\n        Args:\n            redirect_uri:\n                Determines how Google's authorization server sends a response to your app.\n                If not provide will use default https://localhost/\n            scope:\n                The scope you want user to grant permission.\n            access_type:\n                Indicates whether your application can refresh access tokens when the user\n                is not present at the browser.\n                Valid parameter are `online` and `offline`.\n            state:\n                State string between your authorization request and the authorization server's response.\n            include_granted_scopes:\n                Enables applications to use incremental authorization to request\n                access to additional scopes in context.\n                Set true to enable.\n            login_hint:\n                Set the parameter value to an email address or sub identifier, which is\n                equivalent to the user's Google ID.\n            prompt:\n                A space-delimited, case-sensitive list of prompts to present the user.\n                Possible values are:\n                - none:\n                    Do not display any authentication or consent screens.\n                    Must not be specified with other values.\n                - consent:\n                    Prompt the user for consent.\n                - select_account:\n                    Prompt the user to select an account.\n            **kwargs:\n                Additional parameters for authorize session.\n\n        Returns:\n            A tuple of (url, state)\n\n            url: Authorize url for user.\n            state: State string for authorization.\n\n        References:\n            https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps\n        \"\"\"\n        session = self._get_oauth_session(\n            redirect_uri=redirect_uri,\n            scope=scope,\n            state=state,\n            **kwargs,\n        )\n        authorize_url, state = session.authorization_url(\n            url=self.AUTHORIZATION_URL,\n            access_type=access_type,\n            include_granted_scopes=include_granted_scopes,\n            login_hint=login_hint,\n            prompt=prompt,\n        )\n        return authorize_url, state\n\n    def generate_access_token(\n        self,\n        authorization_response: Optional[str] = None,\n        code: Optional[str] = None,\n        redirect_uri: Optional[str] = None,\n        scope: Optional[List[str]] = None,\n        state: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs,\n    ) -> Union[dict, AccessToken]:\n        \"\"\"Exchange the authorization code or authorization response for an access token.\n\n        Args:\n            authorization_response:\n                Response url for YouTune redirected to.\n            code:\n                Authorization code from authorization_response.\n            redirect_uri:\n                Determines how Google's authorization server sends a response to your app.\n                If not provide will use default https://localhost/\n            scope:\n                The scope you want user to grant permission.\n            state:\n                State string between your authorization request and the authorization server's response.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for authorize session.\n\n        Returns:\n            Access token data.\n        \"\"\"\n        session = self._get_oauth_session(\n            redirect_uri=redirect_uri,\n            scope=scope,\n            state=state,\n            **kwargs,\n        )\n        token = session.fetch_token(\n            token_url=self.EXCHANGE_ACCESS_TOKEN_URL,\n            client_secret=self.client_secret,\n            authorization_response=authorization_response,\n            code=code,\n            proxies=self.proxies,\n        )\n        self.access_token = token[\"access_token\"]\n        self.refresh_token = token.get(\"refresh_token\")\n        return token if return_json else AccessToken.from_dict(token)\n\n    def refresh_access_token(\n        self, refresh_token: str, return_json: bool = False, **kwargs\n    ) -> Union[dict, AccessToken]:\n        \"\"\"Refresh new access token.\n\n        Args:\n            refresh_token:\n                The refresh token returned from the authorization code exchange.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for request.\n\n        Returns:\n            Access token data.\n        \"\"\"\n        response = self.request(\n            method=\"POST\",\n            path=self.EXCHANGE_ACCESS_TOKEN_URL,\n            data={\n                \"client_id\": self.client_id,\n                \"client_secret\": self.client_secret,\n                \"refresh_token\": refresh_token,\n                \"grant_type\": \"refresh_token\",\n            },\n            enforce_auth=False,\n            **kwargs,\n        )\n        data = self.parse_response(response)\n        return data if return_json else AccessToken.from_dict(data)\n\n    def revoke_access_token(\n        self,\n        token: str,\n    ) -> bool:\n        \"\"\"Revoke token.\n\n        Notes:\n            If the token is an access token which has a corresponding refresh token,\n            the refresh token will also be revoked.\n\n        Args:\n            token:\n                Can be an access token or a refresh token.\n\n        Returns:\n            Revoked status\n\n        Raises:\n            PyYouTubeException: When occur errors.\n        \"\"\"\n        response = self.request(\n            method=\"POST\",\n            path=self.REVOKE_TOKEN_URL,\n            params={\"token\": token},\n            enforce_auth=False,\n        )\n        if response.ok:\n            return True\n        self.parse_response(response)\n\n    def subscribe_push_notification(\n        self,\n        channel_id: str,\n        callback_url: str,\n        mode: str = \"subscribe\",\n        lease_seconds: Optional[int] = None,\n        secret: Optional[str] = None,\n        verify: str = \"async\",\n    ) -> bool:\n        \"\"\"Subscribe or unsubscribe to a YouTube channel's push notifications via PubSubHubbub.\n\n        When a subscribed channel publishes a new video or updates an existing one,\n        Google will send a notification to the callback_url.\n\n        Args:\n            channel_id:\n                The YouTube channel ID to subscribe to.\n            callback_url:\n                The URL that will receive push notifications from the hub.\n                Must be publicly accessible.\n            mode:\n                Either \"subscribe\" or \"unsubscribe\".\n            lease_seconds:\n                How long (in seconds) the subscription should remain active.\n                If omitted, the hub uses its own default (typically ~432000, i.e. 5 days).\n            secret:\n                A secret string used to compute an HMAC-SHA1 signature on each notification,\n                allowing you to verify the payload came from the hub.\n            verify:\n                Verification mode. Either \"async\" (default) or \"sync\".\n\n        Returns:\n            True if the hub accepted the request (HTTP 202 Accepted).\n\n        Raises:\n            PyYouTubeException: If the hub returns an error response.\n\n        References:\n            https://developers.google.com/youtube/v3/guides/push_notifications\n            https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html\n        \"\"\"\n        topic_url = (\n            f\"https://www.youtube.com/xml/feeds/videos.xml?channel_id={channel_id}\"\n        )\n\n        data = {\n            \"hub.callback\": callback_url,\n            \"hub.mode\": mode,\n            \"hub.topic\": topic_url,\n            \"hub.verify\": verify,\n        }\n        if lease_seconds is not None:\n            data[\"hub.lease_seconds\"] = str(lease_seconds)\n        if secret is not None:\n            data[\"hub.secret\"] = secret\n\n        response = self.request(\n            method=\"POST\",\n            path=self.HUB_URL,\n            data=data,\n            enforce_auth=False,\n        )\n\n        # Hub returns 202 Accepted on success (async) or 204 No Content (sync)\n        if response.status_code in (202, 204):\n            return True\n        self.parse_response(response)\n"
  },
  {
    "path": "pyyoutube/error.py",
    "content": "from dataclasses import dataclass\nfrom typing import Optional, Union\n\nfrom requests import Response\n\n__all__ = [\"ErrorCode\", \"ErrorMessage\", \"PyYouTubeException\"]\n\n\nclass ErrorCode:\n    HTTP_ERROR = 10000\n    MISSING_PARAMS = 10001\n    INVALID_PARAMS = 10002\n    NEED_AUTHORIZATION = 10003\n    AUTHORIZE_URL_FIRST = 10004\n\n\n@dataclass\nclass ErrorMessage:\n    status_code: Optional[int] = None\n    message: Optional[str] = None\n\n\nclass PyYouTubeException(Exception):\n    \"\"\"\n    This is a return demo:\n    {'error': {'errors': [{'domain': 'youtube.parameter',\n    'reason': 'missingRequiredParameter',\n    'message': 'No filter selected. Expected one of: forUsername, managedByMe, categoryId, mine, mySubscribers, id, idParam',\n    'locationType': 'parameter',\n    'location': ''}],\n    'code': 400,\n    'message': 'No filter selected. Expected one of: forUsername, managedByMe, categoryId, mine, mySubscribers, id, idParam'}}\n    \"\"\"\n\n    def __init__(self, response: Optional[Union[ErrorMessage, Response]]):\n        self.status_code: Optional[int] = None\n        self.error_type: Optional[str] = None\n        self.message: Optional[str] = None\n        self.response: Optional[Union[ErrorMessage, Response]] = response\n        self.error_handler()\n\n    def error_handler(self):\n        \"\"\"\n        Error has two big type(but not the error type.): This module's error, Api return error.\n        So This will change two error to one format\n        \"\"\"\n        if isinstance(self.response, ErrorMessage):\n            self.status_code = self.response.status_code\n            self.message = self.response.message\n            self.error_type = \"PyYouTubeException\"\n        elif isinstance(self.response, Response):\n            res_data = self.response.json()\n            if \"error\" in res_data:\n                error = res_data[\"error\"]\n                if isinstance(error, dict):\n                    self.status_code = res_data[\"error\"][\"code\"]\n                    self.message = res_data[\"error\"][\"message\"]\n                else:\n                    self.status_code = self.response.status_code\n                    self.message = error\n                self.error_type = \"YouTubeException\"\n\n    def __repr__(self):\n        return (\n            f\"{self.error_type}(status_code={self.status_code},message={self.message})\"\n        )\n\n    def __str__(self):\n        return self.__repr__()\n"
  },
  {
    "path": "pyyoutube/media.py",
    "content": "\"\"\"\nMedia object to upload.\n\"\"\"\n\nimport mimetypes\nimport os\nfrom typing import IO, Optional, Tuple\n\nfrom requests import Response\n\nfrom pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode\n\nDEFAULT_CHUNK_SIZE = 20 * 1024 * 1024\n\n\nclass Media:\n    def __init__(\n        self,\n        fd: Optional[IO] = None,\n        mimetype: Optional[str] = None,\n        filename: Optional[str] = None,\n        chunk_size: int = DEFAULT_CHUNK_SIZE,\n    ) -> None:\n        \"\"\"Media representing a file to upload with metadata.\n\n        Args:\n            fd:\n                The source of the bytes to upload.\n            mimetype:\n                Mime-type of the file.\n            filename:\n                Name of the file.\n                At least one of the `fd` or `filename`.\n            chunk_size:\n                File will be uploaded in chunks of this many bytes. Only\n                used if resumable=True.\n        \"\"\"\n\n        if fd is not None:\n            self.fd = fd\n        elif filename is not None:\n            self._filename = filename\n            self.fd = open(self._filename, \"rb\")\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Specify at least one of fd or filename\",\n                )\n            )\n\n        if mimetype is None and filename is not None:\n            mimetype, _ = mimetypes.guess_type(filename)\n            if mimetype is None:\n                # Guess failed, use octet-stream.\n                mimetype = \"application/octet-stream\"\n        self.mimetype = mimetype\n        self.chunk_size = chunk_size\n\n        self.fd.seek(0, os.SEEK_END)\n        self.size = self.fd.tell()\n\n    def get_bytes(self, begin: int, length: int) -> bytes:\n        \"\"\"Get bytes from the media.\n\n        Args:\n          begin:\n            Offset from beginning of file.\n          length:\n            Number of bytes to read, starting at begin.\n\n        Returns:\n          A string of bytes read. May be shorted than length if EOF was reached\n          first.\n        \"\"\"\n        self.fd.seek(begin)\n        return self.fd.read(length)\n\n\nclass MediaUploadProgress:\n    def __init__(self, progressed_seize: int, total_size: int):\n        \"\"\"\n        Args:\n            progressed_seize: Bytes sent so far.\n            total_size: Total bytes in complete upload, or None if the total\n            upload size isn't known ahead of time.\n        \"\"\"\n        self.progressed_seize = progressed_seize\n        self.total_size = total_size\n\n    def progress(self) -> float:\n        \"\"\"Percent of upload completed, as a float.\n\n        Returns:\n          the percentage complete as a float, returning 0.0 if the total size of\n          the upload is unknown.\n        \"\"\"\n        if self.total_size is not None and self.total_size != 0:\n            return float(self.progressed_seize) / float(self.total_size)\n        else:\n            return 0.0\n\n    def __repr__(self) -> str:\n        return f\"Media upload {int(self.progress() * 100)} complete.\"\n\n\nclass MediaUpload:\n    def __init__(\n        self,\n        client,\n        resource: str,\n        media: Media,\n        params: Optional[dict] = None,\n        body: Optional[dict] = None,\n    ) -> None:\n        \"\"\"Constructor for upload a file.\n\n        Args:\n            client:\n                Client instance.\n            resource:\n                Resource like videos,captions and so on.\n            media:\n                Media instance.\n            params:\n                Parameters for the request.\n            body:\n                Body for the request.\n        \"\"\"\n        self.client = client\n        self.media = media\n        self.params = params\n        self.body = body\n        self.resource = resource\n\n        if self.params is not None:\n            self.params[\"uploadType\"] = \"resumable\"\n\n        self.resumable_uri = None  # Real uri to upload media.\n        self.resumable_progress = 0  # The bytes that have been uploaded.\n\n    def next_chunk(self) -> Tuple[Optional[MediaUploadProgress], Optional[dict]]:\n        \"\"\"Execute the next step of a resumable upload.\n\n        Returns:\n            The body will be None until the resumable media is fully uploaded.\n        \"\"\"\n        size = str(self.media.size)\n\n        if self.resumable_uri is None:\n            start_headers = {\n                \"X-Upload-Content-Type\": self.media.mimetype,\n                \"X-Upload-Content-Length\": size,\n                \"content-length\": str(len(str(self.body or \"\"))),\n            }\n            resp = self.client.request(\n                method=\"POST\",\n                path=self.resource,\n                params=self.params,\n                json=self.body,\n                is_upload=True,\n                headers=start_headers,\n            )\n            if resp.status_code == 200 and \"location\" in resp.headers:\n                self.resumable_uri = resp.headers[\"location\"]\n            else:\n                raise PyYouTubeException(resp)\n\n        data = self.media.get_bytes(self.resumable_progress, self.media.chunk_size)\n\n        # A short read implies that we are at EOF, so finish the upload.\n        if len(data) < self.media.chunk_size:\n            size = str(self.resumable_progress + len(data))\n\n        chunk_end = self.resumable_progress + len(data) - 1\n\n        headers = {\n            \"Content-Length\": str(chunk_end - self.resumable_progress + 1),\n        }\n        # An empty file results in chunk_end = -1 and size = 0\n        # sending \"bytes 0--1/0\" results in an invalid request\n        # Only add header \"Content-Range\" if chunk_end != -1\n        if chunk_end != -1:\n            headers[\"Content-Range\"] = (\n                f\"bytes {self.resumable_progress}-{chunk_end}/{size}\"\n            )\n\n        resp = self.client.request(\n            path=self.resumable_uri,\n            method=\"PUT\",\n            data=data,\n            headers=headers,\n        )\n        return self.process_response(resp)\n\n    def process_response(\n        self, resp: Response\n    ) -> Tuple[Optional[MediaUploadProgress], Optional[dict]]:\n        \"\"\"Process the response from chunk upload.\n\n        Args:\n            resp: Response for request.\n\n        Returns:\n            The body will be None until the resumable media is fully uploaded.\n        \"\"\"\n        if resp.status_code in [200, 201]:\n            return None, self.client.parse_response(response=resp)\n        elif resp.status_code == 308:\n            try:\n                self.resumable_progress = int(resp.headers[\"range\"].split(\"-\")[1]) + 1\n            except KeyError:\n                # If resp doesn't contain range header, resumable progress is 0\n                self.resumable_progress = 0\n            if \"location\" in resp.headers:\n                self.resumable_uri = resp.headers[\"location\"]\n        else:\n            raise PyYouTubeException(resp)\n\n        return (\n            MediaUploadProgress(self.resumable_progress, self.media.size),\n            None,\n        )\n"
  },
  {
    "path": "pyyoutube/models/__init__.py",
    "content": "from .activity import *  # noqa\nfrom .auth import AccessToken, UserProfile\nfrom .caption import *  # noqa\nfrom .category import *  # noqa\nfrom .channel import *  # noqa\nfrom .channel_banner import *  # noqa\nfrom .channel_section import *  # noqa\nfrom .comment import *  # noqa\nfrom .comment_thread import *  # noqa\nfrom .common import *  # noqa\nfrom .i18n import *  # noqa\nfrom .member import *  # noqa\nfrom .memberships_level import *  # noqa\nfrom .playlist_item import *  # noqa\nfrom .playlist import *  # noqa\nfrom .search_result import *  # noqa\nfrom .subscription import *  # noqa\nfrom .video_abuse_report_reason import *  # noqa\nfrom .video import *  # noqa\nfrom .watermark import *  # noqa\n"
  },
  {
    "path": "pyyoutube/models/activity.py",
    "content": "\"\"\"\nThese are activity related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .common import BaseApiResponse, BaseResource, ResourceId, Thumbnails\nfrom .mixins import DatetimeTimeMixin\n\n\n@dataclass\nclass ActivityContentDetailsUpload(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails upload resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.upload\n    \"\"\"\n\n    videoId: Optional[str] = field(default=None)\n\n\n@dataclass\nclass ActivityContentDetailsLike(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails like resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.like\n    \"\"\"\n\n    resourceId: Optional[ResourceId] = field(default=None)\n\n\n@dataclass\nclass ActivityContentDetailsFavorite(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails favorite resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.favorite\n    \"\"\"\n\n    resourceId: Optional[ResourceId] = field(default=None)\n\n\n@dataclass\nclass ActivityContentDetailsComment(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails comment resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.comment\n    \"\"\"\n\n    resourceId: Optional[ResourceId] = field(default=None)\n\n\n@dataclass\nclass ActivityContentDetailsSubscription(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails subscription resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.subscription\n    \"\"\"\n\n    resourceId: Optional[ResourceId] = field(default=None)\n\n\n@dataclass\nclass ActivityContentDetailsPlaylistItem(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails playlistItem resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.playlistItem\n    \"\"\"\n\n    resourceId: Optional[ResourceId] = field(default=None)\n    playlistId: Optional[str] = field(default=None)\n    playlistItemId: Optional[str] = field(default=None)\n\n\n@dataclass\nclass ActivityContentDetailsRecommendation(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails recommendation resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.recommendation\n    \"\"\"\n\n    resourceId: Optional[ResourceId] = field(default=None)\n    reason: Optional[str] = field(default=None)\n    seedResourceId: Optional[ResourceId] = field(default=None)\n\n\n@dataclass\nclass ActivityContentDetailsBulletin(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails bulletin resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.bulletin\n    \"\"\"\n\n    resourceId: Optional[ResourceId] = field(default=None)\n\n\n@dataclass\nclass ActivityContentDetailsSocial(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails social resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.social\n    \"\"\"\n\n    type: Optional[str] = field(default=None)\n    resourceId: Optional[ResourceId] = field(default=None)\n    author: Optional[str] = field(default=None)\n    referenceUrl: Optional[str] = field(default=None)\n    imageUrl: Optional[str] = field(default=None)\n\n\n@dataclass\nclass ActivityContentDetailsChannelItem(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails channelItem resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails.channelItem\n    \"\"\"\n\n    resourceId: Optional[ResourceId] = field(default=None)\n\n\n@dataclass\nclass ActivitySnippet(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the activity snippet resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#snippet\n    \"\"\"\n\n    publishedAt: Optional[str] = field(default=None, repr=False)\n    channelId: Optional[str] = field(default=None, repr=False)\n    title: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None)\n    thumbnails: Optional[Thumbnails] = field(default=None, repr=False)\n    channelTitle: Optional[str] = field(default=None, repr=False)\n    type: Optional[str] = field(default=None, repr=False)\n    groupId: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass ActivityContentDetails(BaseModel):\n    \"\"\"\n    A class representing the activity contentDetails resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities#contentDetails\n    \"\"\"\n\n    upload: Optional[ActivityContentDetailsUpload] = field(default=None)\n    like: Optional[ActivityContentDetailsLike] = field(default=None, repr=False)\n    favorite: Optional[ActivityContentDetailsFavorite] = field(default=None, repr=False)\n    comment: Optional[ActivityContentDetailsComment] = field(default=None, repr=False)\n    subscription: Optional[ActivityContentDetailsSubscription] = field(\n        default=None, repr=False\n    )\n    playlistItem: Optional[ActivityContentDetailsPlaylistItem] = field(\n        default=None, repr=False\n    )\n    recommendation: Optional[ActivityContentDetailsRecommendation] = field(\n        default=None, repr=False\n    )\n    bulletin: Optional[ActivityContentDetailsBulletin] = field(default=None, repr=False)\n    social: Optional[ActivityContentDetailsSocial] = field(default=None, repr=False)\n    channelItem: Optional[ActivityContentDetailsChannelItem] = field(\n        default=None, repr=False\n    )\n\n\n@dataclass\nclass Activity(BaseResource):\n    \"\"\"\n    A class representing the activity resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities\n    \"\"\"\n\n    snippet: Optional[ActivitySnippet] = field(default=None)\n    contentDetails: Optional[ActivityContentDetails] = field(default=None, repr=False)\n\n\n@dataclass\nclass ActivityListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the activity response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/activities/list#response_1\n    \"\"\"\n\n    items: Optional[List[Activity]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/auth.py",
    "content": "from dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\n\n\n@dataclass\nclass AccessToken(BaseModel):\n    \"\"\"\n    A class representing for access token.\n    Refer: https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#exchange-authorization-code\n    \"\"\"\n\n    access_token: Optional[str] = field(default=None)\n    expires_in: Optional[int] = field(default=None)\n    refresh_token: Optional[str] = field(default=None, repr=False)\n    scope: Optional[List[str]] = field(default=None, repr=False)\n    token_type: Optional[str] = field(default=None)\n    expires_at: Optional[float] = field(default=None, repr=False)\n\n\n@dataclass\nclass UserProfile(BaseModel):\n    \"\"\"\n    A class representing for user profile.\n    Refer: https://any-api.com/googleapis_com/oauth2/docs/userinfo/oauth2_userinfo_v2_me_get\n    \"\"\"\n\n    id: Optional[str] = field(default=None)\n    name: Optional[str] = field(default=None)\n    given_name: Optional[str] = field(default=None, repr=False)\n    family_name: Optional[str] = field(default=None, repr=False)\n    picture: Optional[str] = field(default=None, repr=False)\n    locale: Optional[str] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/base.py",
    "content": "from dataclasses import dataclass, asdict\nfrom typing import Type, TypeVar\n\nfrom dataclasses_json import DataClassJsonMixin\nfrom dataclasses_json.core import Json, _decode_dataclass\n\nA = TypeVar(\"A\", bound=\"DataClassJsonMixin\")\n\n\n@dataclass\nclass BaseModel(DataClassJsonMixin):\n    \"\"\"Base model class for instance use.\"\"\"\n\n    @classmethod\n    def from_dict(cls: Type[A], kvs: Json, *, infer_missing=False) -> A:\n        # save original data for lookup\n        cls._json = kvs\n        return _decode_dataclass(cls, kvs, infer_missing)\n\n    def to_dict_ignore_none(self):\n        return asdict(\n            obj=self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None}\n        )\n"
  },
  {
    "path": "pyyoutube/models/caption.py",
    "content": "\"\"\"\nThese are caption related models\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .mixins import DatetimeTimeMixin\nfrom .common import BaseResource, BaseApiResponse\n\n\n@dataclass\nclass CaptionSnippet(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the caption snippet resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/captions#snippet\n    \"\"\"\n\n    videoId: Optional[str] = field(default=None)\n    lastUpdated: Optional[str] = field(default=None)\n    trackKind: Optional[str] = field(default=None, repr=False)\n    language: Optional[str] = field(default=None, repr=False)\n    name: Optional[str] = field(default=None, repr=False)\n    audioTrackType: Optional[str] = field(default=None, repr=False)\n    isCC: Optional[bool] = field(default=None, repr=False)\n    isLarge: Optional[bool] = field(default=None, repr=False)\n    isEasyReader: Optional[bool] = field(default=None, repr=False)\n    isDraft: Optional[bool] = field(default=None, repr=False)\n    isAutoSynced: Optional[bool] = field(default=None, repr=False)\n    status: Optional[str] = field(default=None, repr=False)\n    failureReason: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass Caption(BaseResource):\n    \"\"\"\n    A class representing the caption resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/captions\n    \"\"\"\n\n    snippet: Optional[CaptionSnippet] = field(default=None)\n\n\n@dataclass\nclass CaptionListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the activity response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/captions/list?#response_1\n    \"\"\"\n\n    items: Optional[List[Caption]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/category.py",
    "content": "\"\"\"\nThese are category related models.\nInclude VideoCategory\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .common import BaseApiResponse, BaseResource\n\n\n@dataclass\nclass CategorySnippet(BaseModel):\n    \"\"\"\n    This is base category snippet for video and guide.\n    \"\"\"\n\n    channelId: Optional[str] = field(default=None)\n    title: Optional[str] = field(default=None)\n\n\n@dataclass\nclass VideoCategorySnippet(CategorySnippet):\n    \"\"\"\n    A class representing video category snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videoCategories#snippet\n    \"\"\"\n\n    assignable: Optional[bool] = field(default=None, repr=False)\n\n\n@dataclass\nclass VideoCategory(BaseResource):\n    \"\"\"\n    A class representing video category info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videoCategories\n    \"\"\"\n\n    snippet: Optional[VideoCategorySnippet] = field(default=None, repr=False)\n\n\n@dataclass\nclass VideoCategoryListResponse(BaseApiResponse):\n    \"\"\"\n     A class representing the video category's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videoCategories/list#response_1\n    \"\"\"\n\n    items: Optional[List[VideoCategory]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/channel.py",
    "content": "\"\"\"\nThese are channel related models.\n\nReferences: https://developers.google.com/youtube/v3/docs/channels#properties\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .common import (\n    BaseResource,\n    BaseTopicDetails,\n    Thumbnails,\n    BaseApiResponse,\n    Localized,\n)\nfrom .mixins import DatetimeTimeMixin\n\n\n@dataclass\nclass RelatedPlaylists(BaseModel):\n    \"\"\"\n    A class representing the channel's related playlists info\n\n    References: https://developers.google.com/youtube/v3/docs/channels#contentDetails.relatedPlaylists\n    \"\"\"\n\n    likes: Optional[str] = field(default=None, repr=False)\n    uploads: Optional[str] = field(default=None)\n\n\n@dataclass\nclass ChannelBrandingSettingChannel(BaseModel):\n    \"\"\"\n    A class representing the channel branding setting's channel info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels#brandingSettings.channel\n    \"\"\"\n\n    title: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None)\n    keywords: Optional[str] = field(default=None, repr=False)\n    trackingAnalyticsAccountId: Optional[str] = field(default=None, repr=False)\n    # Important:\n    # moderateComments has been deprecated at March 7, 2024.\n    moderateComments: Optional[bool] = field(default=None, repr=False)\n    unsubscribedTrailer: Optional[str] = field(default=None, repr=False)\n    defaultLanguage: Optional[str] = field(default=None, repr=False)\n    country: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass ChannelBrandingSettingImage(BaseModel):\n    \"\"\"\n    A class representing the channel branding setting's image info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels#brandingSettings.image\n    \"\"\"\n\n    bannerExternalUrl: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass ChannelSnippet(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the channel snippet info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels#snippet\n    \"\"\"\n\n    title: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None)\n    customUrl: Optional[str] = field(default=None, repr=False)\n    publishedAt: Optional[str] = field(default=None, repr=False)\n    thumbnails: Optional[Thumbnails] = field(default=None, repr=False)\n    defaultLanguage: Optional[str] = field(default=None, repr=False)\n    localized: Optional[Localized] = field(default=None, repr=False)\n    country: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass ChannelContentDetails(BaseModel):\n    \"\"\"\n    A class representing the channel's content info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels#contentDetails\n    \"\"\"\n\n    relatedPlaylists: Optional[RelatedPlaylists] = field(default=None)\n\n\n@dataclass\nclass ChannelStatistics(BaseModel):\n    \"\"\"\n    A class representing the Channel's statistics info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels#statistics\n    \"\"\"\n\n    viewCount: Optional[int] = field(default=None)\n    subscriberCount: Optional[int] = field(default=None)\n    hiddenSubscriberCount: Optional[bool] = field(default=None, repr=False)\n    videoCount: Optional[int] = field(default=None, repr=False)\n\n\n@dataclass\nclass ChannelTopicDetails(BaseTopicDetails):\n    \"\"\"\n    A class representing the channel's topic detail info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels#topicDetails\n    \"\"\"\n\n    # Important:\n    # topicIds maybe has deprecated.\n    # see more: https://developers.google.com/youtube/v3/revision_history#november-10-2016\n    topicIds: Optional[List[str]] = field(default=None, repr=False)\n    topicCategories: Optional[List[str]] = field(default=None)\n\n\n@dataclass\nclass ChannelStatus(BaseModel):\n    \"\"\"\n    A class representing the channel's status info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels#status\n    \"\"\"\n\n    privacyStatus: Optional[str] = field(default=None)\n    isLinked: Optional[bool] = field(default=None, repr=False)\n    longUploadsStatus: Optional[str] = field(default=None, repr=False)\n    madeForKids: Optional[bool] = field(default=None, repr=False)\n    selfDeclaredMadeForKids: Optional[bool] = field(default=None, repr=False)\n\n\n@dataclass\nclass ChannelBrandingSetting(BaseModel):\n    \"\"\"\n    A class representing the channel branding settings info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels#brandingSettings\n    \"\"\"\n\n    channel: Optional[ChannelBrandingSettingChannel] = field(default=None)\n    image: Optional[ChannelBrandingSettingImage] = field(default=None)\n\n\n@dataclass\nclass ChannelAuditDetails(BaseModel):\n    \"\"\"A class representing the channel audit details info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels#auditDetails\n    \"\"\"\n\n    overallGoodStanding: Optional[bool] = field(default=None)\n    communityGuidelinesGoodStanding: Optional[bool] = field(default=None, repr=True)\n    copyrightStrikesGoodStanding: Optional[bool] = field(default=None, repr=True)\n    contentIdClaimsGoodStanding: Optional[bool] = field(default=None, repr=True)\n\n\n@dataclass\nclass ChannelContentOwnerDetails(BaseModel):\n    \"\"\"A class representing the channel data relevant for YouTube Partners.\n\n    References: https://developers.google.com/youtube/v3/docs/channels#contentOwnerDetails\n    \"\"\"\n\n    contentOwner: Optional[str] = field(default=None)\n    timeLinked: Optional[str] = field(default=None)\n\n\n@dataclass\nclass Channel(BaseResource):\n    \"\"\"\n    A class representing the channel's info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels\n    \"\"\"\n\n    snippet: Optional[ChannelSnippet] = field(default=None, repr=False)\n    contentDetails: Optional[ChannelContentDetails] = field(default=None, repr=False)\n    statistics: Optional[ChannelStatistics] = field(default=None, repr=False)\n    topicDetails: Optional[ChannelTopicDetails] = field(default=None, repr=False)\n    status: Optional[ChannelStatus] = field(default=None, repr=False)\n    brandingSettings: Optional[ChannelBrandingSetting] = field(default=None, repr=False)\n    auditDetails: Optional[ChannelAuditDetails] = field(default=None, repr=False)\n    contentOwnerDetails: Optional[ChannelContentOwnerDetails] = field(\n        default=None, repr=False\n    )\n    localizations: Optional[dict] = field(default=None, repr=False)\n\n\n@dataclass\nclass ChannelListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the channel's retrieve response info.\n\n    References: https://developers.google.com/youtube/v3/docs/channels/list#response\n    \"\"\"\n\n    items: Optional[List[Channel]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/channel_banner.py",
    "content": "\"\"\"\nThere are channel banner related models\n\nReferences: https://developers.google.com/youtube/v3/docs/channelBanners#properties\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\n\n\n@dataclass\nclass ChannelBanner(BaseModel):\n    \"\"\"\n    A class representing the channel banner's info.\n\n    References: https://developers.google.com/youtube/v3/docs/channelBanners#resource\n    \"\"\"\n\n    kind: Optional[str] = field(default=None)\n    etag: Optional[str] = field(default=None, repr=False)\n    url: Optional[str] = field(default=None)\n"
  },
  {
    "path": "pyyoutube/models/channel_section.py",
    "content": "\"\"\"\nThose are models related to channel sections.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .common import BaseResource, BaseApiResponse\n\n\n@dataclass\nclass ChannelSectionSnippet(BaseModel):\n    \"\"\"\n    A class representing the channel section snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/channelSections#snippet\n    \"\"\"\n\n    type: Optional[str] = field(default=None)\n    channelId: Optional[str] = field(default=None, repr=False)\n    title: Optional[str] = field(default=None, repr=False)\n    position: Optional[int] = field(default=None)\n\n\n@dataclass\nclass ChannelSectionContentDetails(BaseModel):\n    \"\"\"\n    A class representing the channel section content details info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/channelSections#contentDetails\n    \"\"\"\n\n    playlists: Optional[List[str]] = field(default=None, repr=False)\n    channels: Optional[List[str]] = field(default=None)\n\n\n@dataclass\nclass ChannelSection(BaseResource):\n    \"\"\"\n    A class representing the channel section info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/channelSections#properties\n    \"\"\"\n\n    snippet: Optional[ChannelSectionSnippet] = field(default=None, repr=False)\n    contentDetails: Optional[ChannelSectionContentDetails] = field(\n        default=None, repr=False\n    )\n\n\n@dataclass\nclass ChannelSectionResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the channel section's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/channelSections/list?#properties_1\n    \"\"\"\n\n    items: Optional[List[ChannelSection]] = field(default=None, repr=False)\n\n\n@dataclass\nclass ChannelSectionListResponse(ChannelSectionResponse): ...\n"
  },
  {
    "path": "pyyoutube/models/comment.py",
    "content": "\"\"\"\nThese are comment related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .mixins import DatetimeTimeMixin\nfrom .common import BaseApiResponse, BaseResource\n\n\n@dataclass\nclass CommentSnippetAuthorChannelId(BaseModel):\n    \"\"\"\n    A class representing comment's snippet authorChannelId info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/comments#snippet.authorChannelId\n    \"\"\"\n\n    value: Optional[str] = field(default=None)\n\n\n@dataclass\nclass CommentSnippet(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing comment's snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/comments#snippet\n    \"\"\"\n\n    authorDisplayName: Optional[str] = field(default=None)\n    authorProfileImageUrl: Optional[str] = field(default=None, repr=False)\n    authorChannelUrl: Optional[str] = field(default=None, repr=False)\n    authorChannelId: Optional[CommentSnippetAuthorChannelId] = field(\n        default=None, repr=False\n    )\n    channelId: Optional[str] = field(default=None, repr=False)\n    # videoId has deprecated, see https://developers.google.com/youtube/v3/revision_history#november-09,-2023\n    videoId: Optional[str] = field(default=None, repr=False)\n    textDisplay: Optional[str] = field(default=None, repr=False)\n    textOriginal: Optional[str] = field(default=None, repr=False)\n    parentId: Optional[str] = field(default=None, repr=False)\n    canRate: Optional[bool] = field(default=None, repr=False)\n    viewerRating: Optional[str] = field(default=None, repr=False)\n    likeCount: Optional[int] = field(default=None)\n    moderationStatus: Optional[str] = field(default=None, repr=False)\n    publishedAt: Optional[str] = field(default=None, repr=False)\n    updatedAt: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass Comment(BaseResource):\n    \"\"\"\n    A class representing comment info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/comments\n    \"\"\"\n\n    snippet: Optional[CommentSnippet] = field(default=None)\n\n\n@dataclass\nclass CommentListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the comment's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/comments/list#response_1\n    \"\"\"\n\n    items: Optional[List[Comment]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/comment_thread.py",
    "content": "\"\"\"\nThese are comment threads related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Optional, List\n\nfrom .base import BaseModel\nfrom .common import BaseResource, BaseApiResponse\nfrom .comment import Comment\n\n\n@dataclass\nclass CommentThreadSnippet(BaseModel):\n    \"\"\"A class representing comment tread snippet info.\n\n    References: https://developers.google.com/youtube/v3/docs/commentThreads#snippet\n    \"\"\"\n\n    channelId: Optional[str] = field(default=None)\n    videoId: Optional[str] = field(default=None)\n    topLevelComment: Optional[Comment] = field(default=None, repr=False)\n    canReply: Optional[bool] = field(default=None, repr=False)\n    totalReplyCount: Optional[int] = field(default=None, repr=False)\n    isPublic: Optional[bool] = field(default=None, repr=False)\n\n\n@dataclass\nclass CommentThreadReplies(BaseModel):\n    \"\"\"\n    A class representing comment tread replies info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/commentThreads#replies\n    \"\"\"\n\n    comments: Optional[List[Comment]] = field(default=None, repr=False)\n\n\n@dataclass\nclass CommentThread(BaseResource):\n    \"\"\"\n    A class representing comment thread info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/commentThreads\n    \"\"\"\n\n    snippet: Optional[CommentThreadSnippet] = field(default=None, repr=False)\n    replies: Optional[CommentThreadReplies] = field(default=None, repr=False)\n\n\n@dataclass\nclass CommentThreadListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the comment thread's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/commentThreads/list#response_1\n    \"\"\"\n\n    items: Optional[List[CommentThread]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/common.py",
    "content": "\"\"\"\nThese are common models for multi resource.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Optional, List\n\nfrom .base import BaseModel\n\n\n@dataclass\nclass Thumbnail(BaseModel):\n    \"\"\"\n    A class representing the thumbnail resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/channels#snippet.thumbnails.(key).url\n    \"\"\"\n\n    url: Optional[str] = field(default=None)\n    width: Optional[int] = field(default=None, repr=False)\n    height: Optional[int] = field(default=None, repr=False)\n\n\n@dataclass\nclass Thumbnails(BaseModel):\n    \"\"\"\n    A class representing the multi thumbnail resource info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/channels#snippet.thumbnails\n    \"\"\"\n\n    default: Optional[Thumbnail] = field(default=None)\n    medium: Optional[Thumbnail] = field(default=None, repr=False)\n    high: Optional[Thumbnail] = field(default=None, repr=False)\n    standard: Optional[Thumbnail] = field(default=None, repr=False)\n    maxres: Optional[Thumbnail] = field(default=None, repr=False)\n\n\n@dataclass\nclass Topic(BaseModel):\n    \"\"\"\n    A class representing the channel topic info. this model also suitable for video.\n\n    Refer:\n        https://developers.google.com/youtube/v3/docs/channels#topicDetails.topicIds[]\n        https://developers.google.com/youtube/v3/docs/videos#topicDetails.topicIds[]\n\n    This model is customized for parsing topic id. YouTube Data Api not return this.\n    \"\"\"\n\n    id: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None)\n\n\n@dataclass\nclass BaseTopicDetails(BaseModel):\n    \"\"\"\n    This is the base model for channel or video topic details.\n    \"\"\"\n\n    topicIds: List[str] = field(default=None, repr=False)\n\n    def get_full_topics(self):\n        \"\"\"\n        Convert topicIds list to Topic model list\n        :return: List[Topic]\n        \"\"\"\n        from pyyoutube import TOPICS\n\n        r: List[Topic] = []\n        if self.topicIds:\n            for topic_id in self.topicIds:\n                topic = Topic.from_dict(\n                    {\"id\": topic_id, \"description\": TOPICS.get(topic_id)}\n                )\n                r.append(topic)\n        return r\n\n\n@dataclass\nclass Localized(BaseModel):\n    \"\"\"\n    A class representing the channel or video snippet localized info.\n\n    Refer:\n        https://developers.google.com/youtube/v3/docs/channels#snippet.localized\n        https://developers.google.com/youtube/v3/docs/videos#snippet.localized\n    \"\"\"\n\n    title: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass PageInfo(BaseModel):\n    \"\"\"\n    This is data model for save paging data.\n    Note:\n        totalResults is only an approximation/estimate.\n        Refer:\n            https://stackoverflow.com/questions/43507281/totalresults-count-doesnt-match-with-the-actual-results-returned-in-youtube-v3\n    \"\"\"\n\n    totalResults: Optional[int] = field(default=None)\n    resultsPerPage: Optional[int] = field(default=None)\n\n\n@dataclass\nclass BaseApiResponse(BaseModel):\n    \"\"\"\n    This is Data Api response structure when retrieve data.\n    They both have same response structure, but items.\n\n    Refer:\n        https://developers.google.com/youtube/v3/docs/channels/list#response_1\n        https://developers.google.com/youtube/v3/docs/playlistItems/list#response_1\n    \"\"\"\n\n    kind: Optional[str] = field(default=None)\n    etag: Optional[str] = field(default=None, repr=False)\n    nextPageToken: Optional[str] = field(default=None, repr=False)\n    prevPageToken: Optional[str] = field(default=None, repr=False)\n    pageInfo: Optional[PageInfo] = field(default=None, repr=False)\n\n\n@dataclass\nclass BaseResource(BaseModel):\n    \"\"\"\n    This is a base model for different resource type.\n\n    Refer: https://developers.google.com/youtube/v3/docs#resource-types\n    \"\"\"\n\n    kind: Optional[str] = field(default=None)\n    etag: Optional[str] = field(default=None, repr=False)\n    id: Optional[str] = field(default=None)\n\n\n@dataclass\nclass ResourceId(BaseModel):\n    \"\"\"\n    A class representing the subscription snippet resource info.\n    Refer:\n        1. https://developers.google.com/youtube/v3/docs/playlistItems#snippet.resourceId\n        2. https://developers.google.com/youtube/v3/docs/subscriptions#snippet.resourceId\n        3. https://developers.google.com/youtube/v3/docs/activities#contentDetails.social.resourceId\n    \"\"\"\n\n    kind: Optional[str] = field(default=None)\n    videoId: Optional[str] = field(default=None)\n    channelId: Optional[str] = field(default=None)\n    playlistId: Optional[str] = field(default=None)\n\n\n@dataclass\nclass Player(BaseModel):\n    \"\"\"\n    A class representing the video,playlist player info.\n\n    Refer:\n        https://developers.google.com/youtube/v3/docs/videos#player\n\n    \"\"\"\n\n    embedHtml: Optional[str] = field(default=None)\n    # Important:\n    # follows attributions maybe not exists.\n    embedHeight: Optional[int] = field(default=None, repr=False)\n    embedWidth: Optional[int] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/i18n.py",
    "content": "\"\"\"\nThese are i18n language and region related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .common import BaseResource, BaseApiResponse\n\n\n@dataclass\nclass I18nRegionSnippet(BaseModel):\n    \"\"\"\n    A class representing the I18n region snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/i18nRegions#snippet\n    \"\"\"\n\n    gl: Optional[str] = field(default=None)\n    name: Optional[str] = field(default=None)\n\n\n@dataclass\nclass I18nRegion(BaseResource):\n    \"\"\"\n    A class representing the I18n region info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/i18nRegions#resource-representation\n    \"\"\"\n\n    snippet: Optional[I18nRegionSnippet] = field(default=None)\n\n\n@dataclass\nclass I18nRegionListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the I18n region list response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages/list#response_1\n    \"\"\"\n\n    items: Optional[List[I18nRegion]] = field(default=None, repr=False)\n\n\n@dataclass\nclass I18nLanguageSnippet(BaseModel):\n    \"\"\"\n    A class representing the I18n language snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages#snippet\n    \"\"\"\n\n    hl: Optional[str] = field(default=None)\n    name: Optional[str] = field(default=None)\n\n\n@dataclass\nclass I18nLanguage(BaseResource):\n    \"\"\"\n    A class representing the I18n language info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages#resource-representation\n    \"\"\"\n\n    snippet: Optional[I18nLanguageSnippet] = field(default=None)\n\n\n@dataclass\nclass I18nLanguageListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the I18n language list response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/i18nLanguages/list#response_1\n    \"\"\"\n\n    items: Optional[List[I18nLanguage]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/member.py",
    "content": "\"\"\"\nThese are member related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .common import BaseApiResponse\nfrom .mixins import DatetimeTimeMixin\n\n\n@dataclass\nclass MemberSnippetMemberDetails(BaseModel):\n    \"\"\"\n    A class representing the member snippet member detail.\n\n    Refer: https://developers.google.com/youtube/v3/docs/members#snippet.memberDetails\n    \"\"\"\n\n    channelId: Optional[str] = field(default=None)\n    channelUrl: Optional[str] = field(default=None, repr=False)\n    displayName: Optional[str] = field(default=None, repr=False)\n    profileImageUrl: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass MemberSnippetMembershipsDuration(BaseModel, DatetimeTimeMixin):\n    memberSince: Optional[str] = field(default=None)\n    memberTotalDurationMonths: Optional[int] = field(default=None, repr=False)\n\n\n@dataclass\nclass MemberSnippetMembershipsDurationAtLevel(BaseModel):\n    level: Optional[str] = field(default=None)\n    memberSince: Optional[str] = field(default=None, repr=False)\n    memberTotalDurationMonths: Optional[int] = field(default=None, repr=False)\n\n\n@dataclass\nclass MemberSnippetMembershipsDetails(BaseModel):\n    \"\"\"\n    A class representing the member snippet membership detail.\n\n    Refer: https://developers.google.com/youtube/v3/docs/members#snippet.membershipsDetails\n    \"\"\"\n\n    highestAccessibleLevel: Optional[str] = field(default=None)\n    highestAccessibleLevelDisplayName: Optional[str] = field(default=None)\n    accessibleLevels: Optional[List[str]] = field(default=None, repr=False)\n    membershipsDuration: Optional[MemberSnippetMembershipsDuration] = field(\n        default=None, repr=False\n    )\n    membershipsDurationAtLevel: Optional[\n        List[MemberSnippetMembershipsDurationAtLevel]\n    ] = field(default=None, repr=False)\n\n\n@dataclass\nclass MemberSnippet(BaseModel):\n    \"\"\"\n    A class representing the member snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/members#snippet\n    \"\"\"\n\n    creatorChannelId: Optional[str] = field(default=None)\n    memberDetails: Optional[MemberSnippetMemberDetails] = field(\n        default=None, repr=False\n    )\n    membershipsDetails: Optional[MemberSnippetMembershipsDetails] = field(\n        default=None, repr=False\n    )\n\n\n@dataclass\nclass Member(BaseModel):\n    \"\"\"\n    A class representing the member info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/members\n    \"\"\"\n\n    kind: Optional[str] = field(default=None)\n    etag: Optional[str] = field(default=None, repr=False)\n    snippet: Optional[MemberSnippet] = field(default=None, repr=False)\n\n\n@dataclass\nclass MemberListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the member's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/members/list#response\n    \"\"\"\n\n    items: Optional[List[Member]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/memberships_level.py",
    "content": "\"\"\"\nThese are membership level related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .common import BaseResource, BaseApiResponse\n\n\n@dataclass\nclass MembershipLevelSnippetLevelDetails(BaseModel):\n    displayName: Optional[str] = field(default=None)\n\n\n@dataclass\nclass MembershipsLevelSnippet(BaseModel):\n    \"\"\"\n    A class representing the membership level snippet.\n\n    Refer: https://developers.google.com/youtube/v3/docs/membershipsLevels#snippet\n    \"\"\"\n\n    creatorChannelId: Optional[str] = field(default=None)\n    levelDetails: Optional[MembershipLevelSnippetLevelDetails] = field(\n        default=None, repr=False\n    )\n\n\n@dataclass\nclass MembershipsLevel(BaseResource):\n    \"\"\"\n    A class representing the membership level.\n\n    Refer: https://developers.google.com/youtube/v3/docs/membershipsLevels\n    \"\"\"\n\n    snippet: Optional[MembershipsLevelSnippet] = field(default=None, repr=False)\n\n\n@dataclass\nclass MembershipsLevelListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the memberships level's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/membershipsLevels/list#response\n    \"\"\"\n\n    items: Optional[List[MembershipsLevel]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/mixins.py",
    "content": "\"\"\"\nThese are some mixin for models\n\"\"\"\n\nimport datetime\nfrom typing import Optional\n\nimport isodate\nfrom isodate.isoerror import ISO8601Error\n\nfrom pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException\n\n\nclass DatetimeTimeMixin:\n    @staticmethod\n    def string_to_datetime(dt_str: Optional[str]) -> Optional[datetime.datetime]:\n        \"\"\"\n        Convert datetime string to datetime instance.\n        original string format is YYYY-MM-DDThh:mm:ss.sZ.\n        :return:\n        \"\"\"\n        if not dt_str:\n            return None\n        try:\n            r = isodate.parse_datetime(dt_str)\n        except ISO8601Error as e:\n            raise PyYouTubeException(\n                ErrorMessage(status_code=ErrorCode.INVALID_PARAMS, message=e.args[0])\n            )\n        else:\n            return r\n"
  },
  {
    "path": "pyyoutube/models/playlist.py",
    "content": "\"\"\"\nThese are playlist related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Optional, List\n\nfrom .base import BaseModel\nfrom .common import BaseApiResponse, BaseResource, Localized, Player, Thumbnails\nfrom .mixins import DatetimeTimeMixin\n\n\n@dataclass\nclass PlaylistContentDetails(BaseModel):\n    \"\"\"\n    A class representing playlist's content details info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/playlists#contentDetails\n    \"\"\"\n\n    itemCount: Optional[int] = field(default=None)\n\n\n@dataclass\nclass PlaylistSnippet(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the playlist snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/playlists#snippet\n    \"\"\"\n\n    publishedAt: Optional[str] = field(default=None, repr=False)\n    channelId: Optional[str] = field(default=None, repr=False)\n    title: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None)\n    thumbnails: Optional[Thumbnails] = field(default=None, repr=False)\n    channelTitle: Optional[str] = field(default=None, repr=False)\n    defaultLanguage: Optional[str] = field(default=None, repr=False)\n    localized: Optional[Localized] = field(default=None, repr=False)\n\n\n@dataclass\nclass PlaylistStatus(BaseModel):\n    \"\"\"\n    A class representing the playlist status info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/playlists#status\n    \"\"\"\n\n    privacyStatus: Optional[str] = field(default=None)\n\n\n@dataclass\nclass Playlist(BaseResource):\n    \"\"\"\n    A class representing the playlist info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/playlists\n    \"\"\"\n\n    snippet: Optional[PlaylistSnippet] = field(default=None, repr=False)\n    status: Optional[PlaylistStatus] = field(default=None, repr=False)\n    contentDetails: Optional[PlaylistContentDetails] = field(default=None, repr=False)\n    player: Optional[Player] = field(default=None, repr=False)\n\n\n@dataclass\nclass PlaylistListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the playlist's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/playlists/list#response_1\n    \"\"\"\n\n    items: Optional[List[Playlist]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/playlist_item.py",
    "content": "\"\"\"\nThese are playlistItem related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .mixins import DatetimeTimeMixin\nfrom .common import BaseApiResponse, BaseResource, ResourceId, Thumbnails\n\n\n@dataclass\nclass PlaylistItemContentDetails(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the playlist item's content details info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/playlistItems#contentDetails\n    \"\"\"\n\n    videoId: Optional[str] = field(default=None)\n    note: Optional[str] = field(default=None, repr=False)\n    videoPublishedAt: Optional[str] = field(default=None)\n    startAt: Optional[str] = field(default=None, repr=False)\n    endAt: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass PlaylistItemSnippet(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the playlist item's snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/playlistItems#snippet\n    \"\"\"\n\n    publishedAt: Optional[str] = field(default=None, repr=False)\n    channelId: Optional[str] = field(default=None, repr=False)\n    title: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None)\n    thumbnails: Optional[Thumbnails] = field(default=None, repr=False)\n    channelTitle: Optional[str] = field(default=None, repr=False)\n    videoOwnerChannelTitle: Optional[str] = field(default=None, repr=False)\n    videoOwnerChannelId: Optional[str] = field(default=None, repr=False)\n    playlistId: Optional[str] = field(default=None, repr=False)\n    position: Optional[int] = field(default=None, repr=False)\n    resourceId: Optional[ResourceId] = field(default=None, repr=False)\n\n\n@dataclass\nclass PlaylistItemStatus(BaseModel):\n    \"\"\"\n    A class representing the playlist item's status info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/playlistItems#status\n    \"\"\"\n\n    privacyStatus: Optional[str] = field(default=None)\n\n\n@dataclass\nclass PlaylistItem(BaseResource):\n    \"\"\"\n    A class representing the playlist item's info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/playlistItems\n    \"\"\"\n\n    snippet: Optional[PlaylistItemSnippet] = field(default=None, repr=False)\n    contentDetails: Optional[PlaylistItemContentDetails] = field(\n        default=None, repr=False\n    )\n    status: Optional[PlaylistItemStatus] = field(default=None, repr=False)\n\n\n@dataclass\nclass PlaylistItemListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the playlist item's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/playlistItems/list#response_1\n    \"\"\"\n\n    items: Optional[List[PlaylistItem]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/search_result.py",
    "content": "\"\"\"\nThese are search result related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Optional, List\n\nfrom .base import BaseModel\nfrom .common import BaseApiResponse, BaseResource, Thumbnails\nfrom .mixins import DatetimeTimeMixin\n\n\n@dataclass\nclass SearchResultSnippet(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the search result snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/search#snippet\n    \"\"\"\n\n    publishedAt: Optional[str] = field(default=None, repr=False)\n    channelId: Optional[str] = field(default=None)\n    title: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None, repr=False)\n    thumbnails: Optional[Thumbnails] = field(default=None, repr=False)\n    channelTitle: Optional[str] = field(default=None, repr=False)\n    liveBroadcastContent: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass SearchResultId(BaseModel):\n    \"\"\"\n    A class representing the search result id info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/search#id\n    \"\"\"\n\n    kind: Optional[str] = field(default=None)\n    videoId: Optional[str] = field(default=None, repr=False)\n    channelId: Optional[str] = field(default=None, repr=False)\n    playlistId: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass SearchResult(BaseResource):\n    \"\"\"\n    A class representing the search result's info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/search\n    \"\"\"\n\n    id: Optional[SearchResultId] = field(default=None, repr=False)\n    snippet: Optional[SearchResultSnippet] = field(default=None, repr=False)\n\n\n@dataclass\nclass SearchListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the channel's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/channels/list#response_1\n    \"\"\"\n\n    regionCode: Optional[str] = field(default=None, repr=False)\n    items: Optional[List[SearchResult]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/subscription.py",
    "content": "\"\"\"\nThese are subscription related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Optional\n\nfrom .base import BaseModel\nfrom .common import BaseApiResponse, BaseResource, ResourceId, Thumbnails\nfrom .mixins import DatetimeTimeMixin\n\n\n@dataclass\nclass SubscriptionSnippet(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the subscription snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/subscriptions#snippet\n    \"\"\"\n\n    publishedAt: Optional[str] = field(default=None, repr=False)\n    channelTitle: Optional[str] = field(default=None, repr=False)\n    title: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None)\n    resourceId: Optional[ResourceId] = field(default=None, repr=False)\n    channelId: Optional[str] = field(default=None, repr=False)\n    thumbnails: Optional[Thumbnails] = field(default=None, repr=False)\n\n\n@dataclass\nclass SubscriptionContentDetails(BaseModel):\n    \"\"\"\n    A class representing the subscription contentDetails info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/subscriptions#contentDetails\n    \"\"\"\n\n    totalItemCount: Optional[int] = field(default=None)\n    newItemCount: Optional[int] = field(default=None)\n    activityType: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass SubscriptionSubscriberSnippet(BaseModel):\n    \"\"\"\n    A class representing the subscription subscriberSnippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/subscriptions#subscriberSnippet\n    \"\"\"\n\n    title: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None)\n    channelId: Optional[str] = field(default=None, repr=False)\n    thumbnails: Optional[Thumbnails] = field(default=None, repr=False)\n\n\n@dataclass\nclass Subscription(BaseResource):\n    \"\"\"\n    A class representing the subscription info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/subscriptions\n    \"\"\"\n\n    snippet: Optional[SubscriptionSnippet] = field(default=None)\n    contentDetails: Optional[SubscriptionContentDetails] = field(\n        default=None, repr=False\n    )\n    subscriberSnippet: Optional[SubscriptionSubscriberSnippet] = field(\n        default=None, repr=False\n    )\n\n\n@dataclass\nclass SubscriptionListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the subscription's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/subscriptions/list#response_1\n    \"\"\"\n\n    items: Optional[List[Subscription]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/video.py",
    "content": "\"\"\"\nThese are video related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Optional, List\n\nimport isodate\nfrom isodate import ISO8601Error\n\nfrom pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException\nfrom .base import BaseModel\nfrom .common import (\n    BaseApiResponse,\n    BaseTopicDetails,\n    BaseResource,\n    Localized,\n    Player,\n    Thumbnails,\n)\nfrom .mixins import DatetimeTimeMixin\n\n\n@dataclass\nclass RegionRestriction(BaseModel):\n    \"\"\"\n    A class representing the video content details region restriction info\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos#contentDetails.regionRestriction\n    \"\"\"\n\n    allowed: Optional[List[str]] = field(default=None)\n    blocked: Optional[List[str]] = field(default=None, repr=False)\n\n\n# TODO get detail rating description\nclass ContentRating(BaseModel):\n    \"\"\"\n    A class representing the video content rating info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos#contentDetails.contentRating\n    \"\"\"\n\n    acbRating: Optional[str] = field(default=None, repr=False)\n    agcomRating: Optional[str] = field(default=None, repr=False)\n    anatelRating: Optional[str] = field(default=None, repr=False)\n    bbfcRating: Optional[str] = field(default=None, repr=False)\n    bfvcRating: Optional[str] = field(default=None, repr=False)\n    bmukkRating: Optional[str] = field(default=None, repr=False)\n    catvRating: Optional[str] = field(default=None, repr=False)\n    catvfrRating: Optional[str] = field(default=None, repr=False)\n    cbfcRating: Optional[str] = field(default=None, repr=False)\n    cccRating: Optional[str] = field(default=None, repr=False)\n    cceRating: Optional[str] = field(default=None, repr=False)\n    chfilmRating: Optional[str] = field(default=None, repr=False)\n    chvrsRating: Optional[str] = field(default=None, repr=False)\n    cicfRating: Optional[str] = field(default=None, repr=False)\n    cnaRating: Optional[str] = field(default=None, repr=False)\n    cncRating: Optional[str] = field(default=None, repr=False)\n    csaRating: Optional[str] = field(default=None, repr=False)\n    cscfRating: Optional[str] = field(default=None, repr=False)\n    czfilmRating: Optional[str] = field(default=None, repr=False)\n    djctqRating: Optional[str] = field(default=None, repr=False)\n    djctqRatingReasons: List[str] = field(default=None, repr=False)\n    ecbmctRating: Optional[str] = field(default=None, repr=False)\n    eefilmRating: Optional[str] = field(default=None, repr=False)\n    egfilmRating: Optional[str] = field(default=None, repr=False)\n    eirinRating: Optional[str] = field(default=None, repr=False)\n    fcbmRating: Optional[str] = field(default=None, repr=False)\n    fcoRating: Optional[str] = field(default=None, repr=False)\n    fpbRating: Optional[str] = field(default=None, repr=False)\n    fpbRatingReasons: List[str] = field(default=None, repr=False)\n    fskRating: Optional[str] = field(default=None, repr=False)\n    grfilmRating: Optional[str] = field(default=None, repr=False)\n    icaaRating: Optional[str] = field(default=None, repr=False)\n    ifcoRating: Optional[str] = field(default=None, repr=False)\n    ilfilmRating: Optional[str] = field(default=None, repr=False)\n    incaaRating: Optional[str] = field(default=None, repr=False)\n    kfcbRating: Optional[str] = field(default=None, repr=False)\n    kijkwijzerRating: Optional[str] = field(default=None, repr=False)\n    kmrbRating: Optional[str] = field(default=None, repr=False)\n    lsfRating: Optional[str] = field(default=None, repr=False)\n    mccaaRating: Optional[str] = field(default=None, repr=False)\n    mccypRating: Optional[str] = field(default=None, repr=False)\n    mcstRating: Optional[str] = field(default=None, repr=False)\n    mdaRating: Optional[str] = field(default=None, repr=False)\n    medietilsynetRating: Optional[str] = field(default=None, repr=False)\n    mekuRating: Optional[str] = field(default=None, repr=False)\n    mibacRating: Optional[str] = field(default=None, repr=False)\n    mocRating: Optional[str] = field(default=None, repr=False)\n    moctwRating: Optional[str] = field(default=None, repr=False)\n    mpaaRating: Optional[str] = field(default=None, repr=False)\n    mpaatRating: Optional[str] = field(default=None, repr=False)\n    mtrcbRating: Optional[str] = field(default=None, repr=False)\n    nbcRating: Optional[str] = field(default=None, repr=False)\n    nfrcRating: Optional[str] = field(default=None, repr=False)\n    nfvcbRating: Optional[str] = field(default=None, repr=False)\n    nkclvRating: Optional[str] = field(default=None, repr=False)\n    oflcRating: Optional[str] = field(default=None, repr=False)\n    pefilmRating: Optional[str] = field(default=None, repr=False)\n    resorteviolenciaRating: Optional[str] = field(default=None, repr=False)\n    rtcRating: Optional[str] = field(default=None, repr=False)\n    rteRating: Optional[str] = field(default=None, repr=False)\n    russiaRating: Optional[str] = field(default=None, repr=False)\n    skfilmRating: Optional[str] = field(default=None, repr=False)\n    smaisRating: Optional[str] = field(default=None, repr=False)\n    smsaRating: Optional[str] = field(default=None, repr=False)\n    tvpgRating: Optional[str] = field(default=None, repr=False)\n    ytRating: Optional[str] = field(default=None)\n\n\n@dataclass\nclass VideoContentDetails(BaseModel):\n    \"\"\"\n    A class representing the video content details info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos#contentDetails\n    \"\"\"\n\n    duration: Optional[str] = field(default=None)\n    dimension: Optional[str] = field(default=None)\n    definition: Optional[str] = field(default=None, repr=False)\n    caption: Optional[str] = field(default=None, repr=False)\n    licensedContent: Optional[bool] = field(default=None, repr=False)\n    regionRestriction: Optional[RegionRestriction] = field(default=None, repr=False)\n    contentRating: Optional[ContentRating] = field(default=None, repr=False)\n    projection: Optional[str] = field(default=None, repr=False)\n    hasCustomThumbnail: Optional[bool] = field(default=None, repr=False)\n\n    def get_video_seconds_duration(self):\n        if not self.duration:\n            return None\n        try:\n            seconds = isodate.parse_duration(self.duration).total_seconds()\n        except ISO8601Error as e:\n            raise PyYouTubeException(\n                ErrorMessage(status_code=ErrorCode.INVALID_PARAMS, message=e.args[0])\n            )\n        else:\n            return int(seconds)\n\n\n@dataclass\nclass VideoTopicDetails(BaseTopicDetails):\n    \"\"\"\n    A class representing video's topic detail info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos#topicDetails\n    \"\"\"\n\n    # Important:\n    # This property has been deprecated as of November 10, 2016.\n    # Any topics associated with a video are now returned by the topicDetails.relevantTopicIds[] property value.\n    topicIds: Optional[List[str]] = field(default=None, repr=False)\n    relevantTopicIds: Optional[List[str]] = field(default=None, repr=False)\n    topicCategories: Optional[List[str]] = field(default=None)\n\n    def __post_init__(self):\n        \"\"\"\n        If topicIds is not return and relevantTopicIds has return. let relevantTopicIds for topicIds.\n        This is for the get_full_topics method.\n        :return:\n        \"\"\"\n        if self.topicIds is None and self.relevantTopicIds is not None:\n            self.topicIds = self.relevantTopicIds\n\n\n@dataclass\nclass VideoSnippet(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the video snippet info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos#snippet\n    \"\"\"\n\n    publishedAt: Optional[str] = field(default=None, repr=False)\n    channelId: Optional[str] = field(default=None, repr=False)\n    title: Optional[str] = field(default=None)\n    description: Optional[str] = field(default=None)\n    thumbnails: Optional[Thumbnails] = field(default=None, repr=False)\n    channelTitle: Optional[str] = field(default=None, repr=False)\n    tags: Optional[List[str]] = field(default=None, repr=False)\n    categoryId: Optional[str] = field(default=None, repr=False)\n    liveBroadcastContent: Optional[str] = field(default=None, repr=False)\n    defaultLanguage: Optional[str] = field(default=None, repr=False)\n    localized: Optional[Localized] = field(default=None, repr=False)\n    defaultAudioLanguage: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass VideoStatistics(BaseModel):\n    \"\"\"\n    A class representing the video statistics info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos#statistics\n    \"\"\"\n\n    viewCount: Optional[int] = field(default=None)\n    likeCount: Optional[int] = field(default=None)\n    dislikeCount: Optional[int] = field(default=None, repr=False)\n    commentCount: Optional[int] = field(default=None, repr=False)\n\n\n@dataclass\nclass VideoStatus(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the video status info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos#status\n    \"\"\"\n\n    uploadStatus: Optional[str] = field(default=None)\n    failureReason: Optional[str] = field(default=None, repr=False)\n    rejectionReason: Optional[str] = field(default=None, repr=False)\n    privacyStatus: Optional[str] = field(default=None)\n    publishAt: Optional[str] = field(default=None, repr=False)\n    license: Optional[str] = field(default=None, repr=False)\n    embeddable: Optional[bool] = field(default=None, repr=False)\n    publicStatsViewable: Optional[bool] = field(default=None, repr=False)\n    madeForKids: Optional[bool] = field(default=None, repr=False)\n    selfDeclaredMadeForKids: Optional[bool] = field(default=None, repr=False)\n\n\n@dataclass\nclass VideoRecordingDetails(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the video recording details.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos#recordingDetails\n    \"\"\"\n\n    recordingDate: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass VideoLiveStreamingDetails(BaseModel, DatetimeTimeMixin):\n    \"\"\"\n    A class representing the video live streaming details.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos#liveStreamingDetails\n    \"\"\"\n\n    actualStartTime: Optional[str] = field(default=None, repr=False)\n    actualEndTime: Optional[str] = field(default=None, repr=False)\n    scheduledStartTime: Optional[str] = field(default=None, repr=False)\n    scheduledEndTime: Optional[str] = field(default=None, repr=False)\n    concurrentViewers: Optional[int] = field(default=None)\n    activeLiveChatId: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass PaidProductPlacementDetail(BaseModel):\n    hasPaidProductPlacement: Optional[dataclass] = field(default=None, repr=False)\n\n\n@dataclass\nclass Video(BaseResource):\n    \"\"\"\n    A class representing the video info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos\n    \"\"\"\n\n    snippet: Optional[VideoSnippet] = field(default=None, repr=False)\n    contentDetails: Optional[VideoContentDetails] = field(default=None, repr=False)\n    status: Optional[VideoStatus] = field(default=None, repr=False)\n    statistics: Optional[VideoStatistics] = field(default=None, repr=False)\n    topicDetails: Optional[VideoTopicDetails] = field(default=None, repr=False)\n    player: Optional[Player] = field(default=None, repr=False)\n    recordingDetails: Optional[VideoRecordingDetails] = field(default=None, repr=False)\n    liveStreamingDetails: Optional[VideoLiveStreamingDetails] = field(\n        default=None, repr=False\n    )\n    paidProductPlacementDetail: Optional[PaidProductPlacementDetail] = field(\n        default=None, repr=False\n    )\n\n\n@dataclass\nclass VideoListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the video's retrieve response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videos/list#response_1\n    \"\"\"\n\n    items: Optional[List[Video]] = field(default=None, repr=False)\n\n\n@dataclass\nclass VideoReportAbuse(BaseModel):\n    \"\"\"\n    A class representing the video report abuse body.\n    \"\"\"\n\n    videoId: Optional[str] = field(default=None)\n    reasonId: Optional[str] = field(default=None)\n    secondaryReasonId: Optional[str] = field(default=None)\n    comments: Optional[str] = field(default=None)\n    language: Optional[str] = field(default=None)\n\n\n@dataclass\nclass VideoRatingItem(BaseModel):\n    \"\"\"\n    A class representing the video rating item info.\n    \"\"\"\n\n    videoId: Optional[str] = field(default=None)\n    rating: Optional[str] = field(default=None)\n\n\n@dataclass\nclass VideoGetRatingResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the video rating response.\n\n    References: https://developers.google.com/youtube/v3/docs/videos/getRating#properties\n    \"\"\"\n\n    items: Optional[List[VideoRatingItem]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/video_abuse_report_reason.py",
    "content": "\"\"\"\nThese are video abuse report reason related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Optional, List\n\nfrom .base import BaseModel\nfrom .common import BaseResource, BaseApiResponse\n\n\n@dataclass\nclass SecondaryReason(BaseModel):\n    \"\"\"\n    A class representing the video abuse report reason info\n\n    Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons#snippet.secondaryReasons\n    \"\"\"\n\n    id: Optional[str] = field(default=None)\n    label: Optional[str] = field(default=None, repr=True)\n\n\n@dataclass\nclass VideoAbuseReportReasonSnippet(BaseModel):\n    \"\"\"\n    A class representing the video abuse report snippet info\n\n    Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons#snippet\n    \"\"\"\n\n    label: Optional[str] = field(default=None)\n    secondaryReasons: Optional[List[SecondaryReason]] = field(default=None, repr=True)\n\n\n@dataclass\nclass VideoAbuseReportReason(BaseResource):\n    \"\"\"\n    A class representing the video abuse report info\n\n    Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons\n    \"\"\"\n\n    snippet: Optional[VideoAbuseReportReasonSnippet] = field(default=None)\n\n\n@dataclass\nclass VideoAbuseReportReasonListResponse(BaseApiResponse):\n    \"\"\"\n    A class representing the I18n language list response info.\n\n    Refer: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons/list#response_1\n    \"\"\"\n\n    items: Optional[List[VideoAbuseReportReason]] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/models/watermark.py",
    "content": "\"\"\"\nThese are watermark related models.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Optional\n\nfrom .base import BaseModel\n\n\n@dataclass\nclass WatermarkTiming(BaseModel):\n    type: Optional[str] = field(default=None)\n    offsetMs: Optional[int] = field(default=None, repr=False)\n    durationMs: Optional[int] = field(default=None, repr=False)\n\n\n@dataclass\nclass WatermarkPosition(BaseModel):\n    type: Optional[str] = field(default=None)\n    cornerPosition: Optional[str] = field(default=None, repr=False)\n\n\n@dataclass\nclass Watermark(BaseModel):\n    \"\"\"\n    A class representing the watermark info.\n\n    References: https://developers.google.com/youtube/v3/docs/watermarks#resource-representation\n    \"\"\"\n\n    timing: Optional[WatermarkTiming] = field(default=None, repr=False)\n    position: Optional[WatermarkPosition] = field(default=None, repr=False)\n    imageUrl: Optional[str] = field(default=None)\n    imageBytes: Optional[bytes] = field(default=None, repr=False)\n    targetChannelId: Optional[str] = field(default=None, repr=False)\n"
  },
  {
    "path": "pyyoutube/resources/__init__.py",
    "content": "from .activities import ActivitiesResource  # noqa\nfrom .captions import CaptionsResource  # noqa\nfrom .channel_banners import ChannelBannersResource  # noqa\nfrom .channels import ChannelsResource  # noqa\nfrom .channel_sections import ChannelSectionsResource  # noqa\nfrom .comments import CommentsResource  # noqa\nfrom .comment_threads import CommentThreadsResource  # noqa\nfrom .i18n_languages import I18nLanguagesResource  # noqa\nfrom .i18n_regions import I18nRegionsResource  # noqa\nfrom .members import MembersResource  # noqa\nfrom .membership_levels import MembershipLevelsResource  # noqa\nfrom .playlist_items import PlaylistItemsResource  # noqa\nfrom .playlists import PlaylistsResource  # noqa\nfrom .search import SearchResource  # noqa\nfrom .subscriptions import SubscriptionsResource  # noqa\nfrom .thumbnails import ThumbnailsResource  # noqa\nfrom .video_abuse_report_reasons import VideoAbuseReportReasonsResource  # noqa\nfrom .video_categories import VideoCategoriesResource  # noqa\nfrom .videos import VideosResource  # noqa\nfrom .watermarks import WatermarksResource  # noqa\n"
  },
  {
    "path": "pyyoutube/resources/activities.py",
    "content": "\"\"\"\nActivities resource implementation\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import ActivityListResponse\nfrom pyyoutube.utils.params_checker import enf_parts\n\n\nclass ActivitiesResource(Resource):\n    \"\"\"An activity resource contains information about an action that a particular channel,\n    or user, has taken on YouTube.\n\n    References: https://developers.google.com/youtube/v3/docs/activities\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        channel_id: Optional[str] = None,\n        mine: Optional[bool] = None,\n        max_results: Optional[int] = None,\n        page_token: Optional[str] = None,\n        published_after: Optional[str] = None,\n        published_before: Optional[str] = None,\n        region_code: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs,\n    ) -> Union[dict, ActivityListResponse]:\n        \"\"\"Returns a list of channel activity events that match the request criteria.\n\n        Args:\n            parts:\n                Comma-separated list of one or more activity resource properties.\n            channel_id:\n                The channelId parameter specifies a unique YouTube channel ID.\n            mine:\n                This parameter can only be used in a properly authorized request. Set this parameter's value\n                to true to retrieve a feed of the authenticated user's activities.\n            max_results:\n                The parameter specifies the maximum number of items that should be returned\n                the result set.\n                Acceptable values are 0 to 50, inclusive. The default value is 5.\n            page_token:\n                The parameter identifies a specific page in the result set that should be returned.\n            published_after:\n                The parameter specifies the earliest date and time that an activity could\n                have occurred for that activity to be included in the API response.\n            published_before:\n                The parameter specifies the date and time before which an activity must\n                have occurred for that activity to be included in the API response.\n            region_code:\n                The parameter instructs the API to return results for the specified country.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Activities data\n\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"activities\", value=parts),\n            \"maxResults\": max_results,\n            \"pageToken\": page_token,\n            \"publishedAfter\": published_after,\n            \"publishedBefore\": published_before,\n            \"regionCode\": region_code,\n            **kwargs,\n        }\n        if channel_id is not None:\n            params[\"channelId\"] = channel_id\n        elif mine is not None:\n            params[\"mine\"] = mine\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Specify at least one of channel_id or mine\",\n                )\n            )\n\n        response = self._client.request(path=\"activities\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else ActivityListResponse.from_dict(data)\n"
  },
  {
    "path": "pyyoutube/resources/base_resource.py",
    "content": "\"\"\"\nBase resource class.\n\"\"\"\n\nfrom typing import Optional, TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from pyyoutube import Client  # pragma: no cover\n\n\nclass Resource:\n    \"\"\"Resource base class\"\"\"\n\n    def __init__(self, client: Optional[\"Client\"] = None):\n        self._client = client\n\n    @property\n    def access_token(self):\n        return self._client.access_token\n\n    @property\n    def api_key(self):\n        return self._client.api_key\n"
  },
  {
    "path": "pyyoutube/resources/captions.py",
    "content": "\"\"\"\nCaptions resource implementation\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom requests import Response\n\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.media import Media, MediaUpload\nfrom pyyoutube.models import Caption, CaptionListResponse\nfrom pyyoutube.utils.params_checker import enf_comma_separated, enf_parts\n\n\nclass CaptionsResource(Resource):\n    \"\"\"A caption resource represents a YouTube caption track\n\n    References: https://developers.google.com/youtube/v3/docs/captions\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        video_id: Optional[str] = None,\n        caption_id: Optional[Union[str, list, tuple, set]] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, CaptionListResponse]:\n        \"\"\"Returns a list of caption tracks that are associated with a specified video.\n\n        Args:\n            parts:\n                Comma-separated list of one or more caption resource properties.\n            video_id:\n                The parameter specifies the YouTube video ID of the video for which the API\n                should return caption tracks.\n            caption_id:\n                The id parameter specifies a comma-separated list of IDs that identify the\n                caption resources that should be retrieved.\n            on_behalf_of_content_owner:\n                This parameter can only be used in a properly authorized request.\n                Note: This parameter is intended exclusively for YouTube content partners.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Caption data\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"captions\", value=parts),\n            \"videoId\": video_id,\n            \"id\": enf_comma_separated(field=\"caption_id\", value=caption_id),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(path=\"captions\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else CaptionListResponse.from_dict(data)\n\n    def insert(\n        self,\n        body: Union[dict, Caption],\n        media: Media,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        sync: Optional[bool] = None,\n        **kwargs,\n    ) -> MediaUpload:\n        \"\"\"Uploads a caption track.\n\n        Args:\n            body:\n                Provide caption data in the request body. You can give dataclass or just a dict with data.\n            media:\n                Caption media data to upload.\n            parts:\n                The part parameter specifies the caption resource parts that\n                the API response will include. Set the parameter value to snippet.\n            on_behalf_of_content_owner:\n                This parameter can only be used in a properly authorized request.\n                Note: This parameter is intended exclusively for YouTube content partners.\n            sync:\n                The sync parameter indicates whether YouTube should automatically synchronize the caption\n                file with the audio track of the video.\n                If you set the value to true, YouTube will disregard any time codes that are in the uploaded\n                caption file and generate new time codes for the captions.\n                Important:\n                    This parameter will be deprecated at April 12, 2024.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Caption data.\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"captions\", value=parts),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"sync\": sync,\n            **kwargs,\n        }\n        # Build a media upload instance.\n        media_upload = MediaUpload(\n            client=self._client,\n            resource=\"captions\",\n            media=media,\n            params=params,\n            body=body.to_dict_ignore_none(),\n        )\n        return media_upload\n\n    def update(\n        self,\n        body: Union[dict, Caption],\n        media: Optional[Media] = None,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        sync: Optional[bool] = None,\n        return_json: bool = False,\n        **kwargs,\n    ) -> Union[dict, Caption, MediaUpload]:\n        \"\"\"Updates a caption track.\n\n        Args:\n            body:\n                Provide caption data in the request body. You can give dataclass or just a dict with data.\n            media:\n                New caption media.\n            parts:\n                The part parameter specifies the caption resource parts that\n                the API response will include. Set the parameter value to snippet.\n            on_behalf_of_content_owner:\n                This parameter can only be used in a properly authorized request.\n                Note: This parameter is intended exclusively for YouTube content partners.\n            sync:\n                The sync parameter indicates whether YouTube should automatically synchronize the caption\n                file with the audio track of the video.\n                If you set the value to true, YouTube will disregard any time codes that are in the uploaded\n                caption file and generate new time codes for the captions.\n                Important:\n                    This parameter will be deprecated at April 12, 2024.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n        Returns:\n            Caption data.\n\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"captions\", value=parts),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"sync\": sync,\n            **kwargs,\n        }\n        if media is not None:\n            # Build a media upload instance.\n            media_upload = MediaUpload(\n                client=self._client,\n                resource=\"captions\",\n                media=media,\n                params=params,\n                body=body.to_dict_ignore_none(),\n            )\n            return media_upload\n\n        response = self._client.request(\n            method=\"PUT\",\n            path=\"captions\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else Caption.from_dict(data)\n\n    def download(\n        self,\n        caption_id: str,\n        on_behalf_of_content_owner: Optional[str] = None,\n        tfmt: Optional[str] = None,\n        tlang: Optional[str] = None,\n        **kwargs,\n    ) -> Response:\n        \"\"\"Downloads a caption track.\n\n        Args:\n            caption_id:\n                ID for the caption track that is being deleted.\n            on_behalf_of_content_owner:\n                This parameter can only be used in a properly authorized request.\n                Note: This parameter is intended exclusively for YouTube content partners.\n            tfmt:\n                Specifies that the caption track should be returned in a specific format.\n                Supported values are:\n                    sbv – SubViewer subtitle\n                    scc – Scenarist Closed Caption format\n                    srt – SubRip subtitle\n                    ttml – Timed Text Markup Language caption\n                    vtt – Web Video Text Tracks caption\n            tlang:\n                Specifies that the API response should return a translation of the specified caption track.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Response form YouTube.\n        \"\"\"\n        params = {\n            \"id\": caption_id,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"tfmt\": tfmt,\n            \"tlang\": tlang,\n            **kwargs,\n        }\n        response = self._client.request(\n            path=f\"captions/{caption_id}\",\n            params=params,\n        )\n        return response\n\n    def delete(\n        self,\n        caption_id: str,\n        on_behalf_of_content_owner: Optional[str] = None,\n        **kwargs,\n    ) -> bool:\n        \"\"\"Deletes a specified caption track.\n\n        Args:\n            caption_id:\n                ID for the caption track that is being deleted.\n            on_behalf_of_content_owner:\n                This parameter can only be used in a properly authorized request.\n                Note: This parameter is intended exclusively for YouTube content partners.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Delete status\n\n        Raises:\n            PyYouTubeException: Request not success.\n        \"\"\"\n        params = {\n            \"id\": caption_id,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n\n        response = self._client.request(path=\"captions\", method=\"DELETE\", params=params)\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n"
  },
  {
    "path": "pyyoutube/resources/channel_banners.py",
    "content": "\"\"\"\nChannel banners resource implementation.\n\"\"\"\n\nfrom typing import Optional\n\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.media import Media, MediaUpload\n\n\nclass ChannelBannersResource(Resource):\n    \"\"\"A channelBanner resource contains the URL that you would use to set a newly uploaded image as\n    the banner image for a channel.\n\n    References: https://developers.google.com/youtube/v3/docs/channelBanners\n    \"\"\"\n\n    def insert(\n        self,\n        media: Media,\n        on_behalf_of_content_owner: Optional[str] = None,\n        **kwargs: Optional[dict],\n    ) -> MediaUpload:\n        \"\"\"Uploads a channel banner image to YouTube.\n\n        Args:\n            media:\n                Banner media data.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many different YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Channel banner data.\n        \"\"\"\n        params = {\"onBehalfOfContentOwner\": on_behalf_of_content_owner, **kwargs}\n        # Build a media upload instance.\n        media_upload = MediaUpload(\n            client=self._client,\n            resource=\"channelBanners/insert\",\n            media=media,\n            params=params,\n        )\n        return media_upload\n"
  },
  {
    "path": "pyyoutube/resources/channel_sections.py",
    "content": "\"\"\"\nChannel Section resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import ChannelSection, ChannelSectionListResponse\nfrom pyyoutube.utils.params_checker import enf_comma_separated, enf_parts\n\n\nclass ChannelSectionsResource(Resource):\n    \"\"\"A channelSection resource contains information about a set of videos that a channel has chosen to feature.\n\n    References: https://developers.google.com/youtube/v3/docs/channelSections\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        channel_id: Optional[str] = None,\n        section_id: Optional[Union[str, list, tuple, set]] = None,\n        mine: Optional[bool] = None,\n        hl: Optional[str] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, ChannelSectionListResponse]:\n        \"\"\"Returns a list of channelSection resources that match the API request criteria.\n\n        Args:\n            parts:\n                Comma-separated list of one or more channel resource properties.\n            channel_id:\n                ID for the channel which you want to retrieve sections.\n            section_id:\n                Specifies a comma-separated list of IDs that uniquely identify the channelSection\n                resources that are being retrieved.\n            mine:\n                Set this parameter's value to true to retrieve a feed of the channel sections\n                associated with the authenticated user's YouTube channel.\n            hl:\n                The hl parameter provided support for retrieving localized metadata for a channel section.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many different YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Channel section data.\n\n        Raises:\n            PyYouTubeException: Missing filter parameter.\n\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"channelSections\", value=parts),\n            \"hl\": hl,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        if channel_id is not None:\n            params[\"channelId\"] = channel_id\n        elif section_id is not None:\n            params[\"id\"] = enf_comma_separated(field=\"section_id\", value=section_id)\n        elif mine is not None:\n            params[\"mine\"] = mine\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Specify at least one of channel_id, section_id or mine\",\n                )\n            )\n        response = self._client.request(path=\"channelSections\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else ChannelSectionListResponse.from_dict(data)\n\n    def insert(\n        self,\n        body: Union[dict, ChannelSection],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        on_behalf_of_content_owner_channel: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs,\n    ) -> Union[dict, ChannelSection]:\n        \"\"\"Adds a channel section to the authenticated user's channel.\n        A channel can create a maximum of 10 shelves.\n\n        Args:\n            parts:\n                The part parameter serves two purposes in this operation. It identifies the properties\n                that the write operation will set as well as the properties that the API response will include.\n                Accept values:\n                    - id\n                    - contentDetails\n                    - snippet\n            body:\n                Provide a channelSection resource in the request body. You can give dataclass or just a dict with data.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many different YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            on_behalf_of_content_owner_channel:\n                The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the\n                channel to which a video is being added. This parameter is required when a request\n                specifies a value for the onBehalfOfContentOwner parameter, and it can only be used\n                in conjunction with that parameter. In addition, the request must be authorized\n                using a CMS account that is linked to the content owner that the onBehalfOfContentOwner\n                parameter specifies. Finally, the channel that the onBehalfOfContentOwnerChannel parameter\n                value specifies must be linked to the content owner that the onBehalfOfContentOwner parameter specifies.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Channel section data.\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"channelSections\", value=parts),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"onBehalfOfContentOwnerChannel\": on_behalf_of_content_owner_channel,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"POST\",\n            path=\"channelSections\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else ChannelSection.from_dict(data)\n\n    def update(\n        self,\n        body: Union[dict, ChannelSection],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs,\n    ) -> Union[dict, ChannelSection]:\n        \"\"\"Updates a channel section.\n\n        Args:\n            parts:\n                The part parameter serves two purposes in this operation. It identifies the properties\n                that the write operation will set as well as the properties that the API response will include.\n                Accept values:\n                    - id\n                    - contentDetails\n                    - snippet\n            body:\n                Provide a channelSection resource in the request body. You can give dataclass or just a dict with data.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many different YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Channel section data.\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"channelSections\", value=parts),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"PUT\",\n            path=\"channelSections\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else ChannelSection.from_dict(data)\n\n    def delete(\n        self,\n        section_id: str,\n        on_behalf_of_content_owner: Optional[str] = None,\n        **kwargs,\n    ) -> bool:\n        \"\"\"Deletes a channel section.\n\n        Args:\n            section_id:\n                ID for the target channel section.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many different YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Channel section delete status\n        \"\"\"\n        params = {\n            \"id\": section_id,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"DELETE\",\n            path=\"channelSections\",\n            params=params,\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n"
  },
  {
    "path": "pyyoutube/resources/channels.py",
    "content": "\"\"\"\nChannel resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import Channel, ChannelListResponse\nfrom pyyoutube.utils.params_checker import enf_comma_separated, enf_parts\n\n\nclass ChannelsResource(Resource):\n    \"\"\"A channel resource contains information about a YouTube channel.\n\n    References: https://developers.google.com/youtube/v3/docs/channels\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        for_handle: Optional[str] = None,\n        for_username: Optional[str] = None,\n        channel_id: Optional[Union[str, list, tuple, set]] = None,\n        managed_by_me: Optional[bool] = None,\n        mine: Optional[bool] = None,\n        hl: Optional[str] = None,\n        max_results: Optional[int] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        page_token: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, ChannelListResponse]:\n        \"\"\"Returns a collection of zero or more channel resources that match the request criteria.\n\n        Args:\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,auditDetails,brandingSettings,contentDetails,contentOwnerDetails,\n                localizations,snippet,statistics,status,topicDetails\n            for_handle:\n                The parameter specifies a YouTube handle, thereby requesting the channel associated with that handle.\n                The parameter value can be prepended with an @ symbol. For example, to retrieve the resource for\n                the \"Google for Developers\" channel, set the forHandle parameter value to\n                either GoogleDevelopers or @GoogleDevelopers.\n            for_username:\n                The parameter specifies a YouTube username, thereby requesting\n                the channel associated with that username.\n            channel_id:\n                The parameter specifies a comma-separated list of the YouTube channel ID(s)\n                for the resource(s) that are being retrieved.\n            managed_by_me:\n                Set this parameter's value to true to instruct the API to only return channels\n                managed by the content owner that the onBehalfOfContentOwner parameter specifies.\n                The user must be authenticated as a CMS account linked to the specified content\n                owner and onBehalfOfContentOwner must be provided.\n            mine:\n                Set this parameter's value to true to instruct the API to only return channels\n                owned by the authenticated user.\n            hl:\n                The hl parameter instructs the API to retrieve localized resource metadata for\n                a specific application language that the YouTube website supports.\n                The parameter value must be a language code included in the list returned by the\n                i18nLanguages.list method.\n            max_results:\n                The parameter specifies the maximum number of items that should be returned\n                the result set.\n                Acceptable values are 0 to 50, inclusive. The default value is 5.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            page_token:\n                The parameter identifies a specific page in the result set that should be returned.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Channel data\n        Raises:\n            PyYouTubeException: Missing filter parameter.\n                                Request not success.\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"channels\", value=parts),\n            \"hl\": hl,\n            \"maxResults\": max_results,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"pageToken\": page_token,\n            **kwargs,\n        }\n        if for_handle is not None:\n            params[\"forHandle\"] = for_handle\n        elif for_username is not None:\n            params[\"forUsername\"] = for_username\n        elif channel_id is not None:\n            params[\"id\"] = enf_comma_separated(field=\"channel_id\", value=channel_id)\n        elif managed_by_me is not None:\n            params[\"managedByMe\"] = managed_by_me\n        elif mine is not None:\n            params[\"mine\"] = mine\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Specify at least one of for_handle,for_username,channel_id,managedByMe or mine\",\n                )\n            )\n\n        response = self._client.request(path=\"channels\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else ChannelListResponse.from_dict(data)\n\n    def update(\n        self,\n        part: str,\n        body: Union[dict, Channel],\n        on_behalf_of_content_owner: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs,\n    ) -> Union[dict, Channel]:\n        \"\"\"Updates a channel's metadata.\n\n        Note that this method currently only supports updates to the channel resource's brandingSettings,\n        invideoPromotion, and localizations objects and their child properties.\n\n        Args:\n            part:\n                The part parameter serves two purposes in this operation. It identifies the properties\n                that the write operation will set as well as the properties that the API response will include.\n            body:\n                Provide channel data in the request body. You can give dataclass or just a dict with data.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many different YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Channel updated data.\n        \"\"\"\n\n        params = {\n            \"part\": part,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"PUT\",\n            path=\"channels\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else Channel.from_dict(data)\n"
  },
  {
    "path": "pyyoutube/resources/comment_threads.py",
    "content": "\"\"\"\nComment threads resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import CommentThread, CommentThreadListResponse\nfrom pyyoutube.utils.params_checker import enf_parts\n\n\nclass CommentThreadsResource(Resource):\n    \"\"\"A commentThread resource contains information about a YouTube comment thread, which comprises a\n    top-level comment and replies, if any exist, to that comment\n\n    References: https://developers.google.com/youtube/v3/docs/commentThreads\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        all_threads_related_to_channel_id: Optional[str] = None,\n        channel_id: Optional[str] = None,\n        thread_id: Optional[Union[str, list, tuple, set]] = None,\n        video_id: Optional[str] = None,\n        max_results: Optional[int] = None,\n        moderation_status: Optional[str] = None,\n        order: Optional[str] = None,\n        page_token: Optional[str] = None,\n        search_terms: Optional[str] = None,\n        text_format: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, CommentThreadListResponse]:\n        \"\"\"Returns a list of comment threads that match the API request parameters.\n\n        Args:\n            parts:\n                Comma-separated list of one or more comment thread resource properties.\n            all_threads_related_to_channel_id:\n                Instructs the API to return all comment threads associated with the specified channel.\n            channel_id:\n                Instructs the API to return comment threads containing comments about the specified channel\n            thread_id:\n                Specifies a comma-separated list of comment thread IDs for the resources that should be retrieved.\n            video_id:\n                Instructs the API to return comment threads associated with the specified video ID.\n            max_results:\n                The parameter specifies the maximum number of items that should be returned\n                the result set.\n                Acceptable values are 1 to 100, inclusive. The default value is 20.\n            moderation_status:\n                Set this parameter to limit the returned comment threads to a particular moderation state.\n                The default value is published.\n                Note: This parameter is not supported for use in conjunction with the id parameter.\n            order:\n                Specifies the order in which the API response should list comment threads.\n                Valid values are:\n                    - time: Comment threads are ordered by time. This is the default behavior.\n                    - relevance: Comment threads are ordered by relevance.\n                Notes: This parameter is not supported for use in conjunction with the `id` parameter.\n            page_token:\n                 Identifies a specific page in the result set that should be returned.\n                 Notes: This parameter is not supported for use in conjunction with the `id` parameter.\n            search_terms:\n                 Instructs the API to limit the API response to only contain comments that contain\n                 the specified search terms.\n                 Notes: This parameter is not supported for use in conjunction with the `id` parameter.\n            text_format:\n                Set this parameter's value to html or plainText to instruct the API to return the comments\n                left by users in html formatted or in plain text. The default value is html.\n                Acceptable values are:\n                    – html: Returns the comments in HTML format. This is the default value.\n                    – plainText: Returns the comments in plain text format.\n                Notes: This parameter is not supported for use in conjunction with the `id` parameter.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Comment threads data.\n        Raises:\n            PyYouTubeException: Missing filter parameter.\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"commentThreads\", value=parts),\n            \"maxResults\": max_results,\n            \"moderationStatus\": moderation_status,\n            \"order\": order,\n            \"pageToken\": page_token,\n            \"searchTerms\": search_terms,\n            \"textFormat\": text_format,\n            **kwargs,\n        }\n        if all_threads_related_to_channel_id is not None:\n            params[\"allThreadsRelatedToChannelId\"] = all_threads_related_to_channel_id\n        elif channel_id:\n            params[\"channelId\"] = channel_id\n        elif thread_id:\n            params[\"id\"] = thread_id\n        elif video_id:\n            params[\"videoId\"] = video_id\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Specify at least one of all_threads_related_to_channel_id,channel_id,thread_id or video_id\",\n                )\n            )\n        response = self._client.request(path=\"commentThreads\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else CommentThreadListResponse.from_dict(data)\n\n    def insert(\n        self,\n        body: Union[dict, CommentThread],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        return_json: bool = False,\n        **kwargs,\n    ) -> Union[dict, CommentThread]:\n        \"\"\"Creates a new top-level comment.\n\n        Notes: To add a reply to an existing comment, use the comments.insert method instead.\n\n        Args:\n            body:\n                Provide a commentThread resource in the request body. You can give dataclass or just a dict with data.\n            parts:\n                Comma-separated list of one or more comment thread resource properties.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Channel thread data.\n\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"commentThreads\", value=parts),\n            **kwargs,\n        }\n\n        response = self._client.request(\n            method=\"POST\",\n            path=\"commentThreads\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else CommentThread.from_dict(data)\n"
  },
  {
    "path": "pyyoutube/resources/comments.py",
    "content": "\"\"\"\nComment resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import Comment, CommentListResponse\nfrom pyyoutube.utils.params_checker import enf_comma_separated, enf_parts\n\n\nclass CommentsResource(Resource):\n    \"\"\"A comment resource contains information about a single YouTube comment.\n\n    References: https://developers.google.com/youtube/v3/docs/comments\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        comment_id: Optional[Union[str, list, tuple, set]] = None,\n        parent_id: Optional[str] = None,\n        max_results: Optional[int] = None,\n        text_format: Optional[str] = None,\n        page_token: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, CommentListResponse]:\n        \"\"\"Returns a list of comments that match the API request parameters.\n\n        Args:\n            parts:\n                Comma-separated list of one or more comment resource properties.\n            comment_id:\n                Specifies a comma-separated list of comment IDs for the resources that are being retrieved.\n            parent_id:\n                Specifies the ID of the comment for which replies should be retrieved.\n            max_results:\n                The parameter specifies the maximum number of items that should be returned\n                the result set.\n                This parameter is not supported for use in conjunction with the comment_id parameter.\n                Acceptable values are 1 to 100, inclusive. The default value is 20.\n            text_format:\n                Whether the API should return comments formatted as HTML or as plain text.\n                The default value is html.\n                Acceptable values are:\n                    - html: Returns the comments in HTML format.\n                    - plainText: Returns the comments in plain text format.\n            page_token:\n                The parameter identifies a specific page in the result set that should be returned.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Comments data\n        Raises:\n            PyYouTubeException: Missing filter parameter.\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"comments\", value=parts),\n            \"maxResults\": max_results,\n            \"textFormat\": text_format,\n            \"pageToken\": page_token,\n            **kwargs,\n        }\n        if comment_id is not None:\n            params[\"id\"] = enf_comma_separated(field=\"comment_id\", value=comment_id)\n        elif parent_id is not None:\n            params[\"parentId\"] = parent_id\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Specify at least one of comment_id, or parent_id\",\n                )\n            )\n\n        response = self._client.request(path=\"comments\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else CommentListResponse.from_dict(data)\n\n    def insert(\n        self,\n        body: Union[dict, Comment],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        return_json: bool = False,\n        **kwargs,\n    ) -> Union[dict, Comment]:\n        \"\"\"Creates a reply to an existing comment.\n\n        Notes:\n            To create a top-level comment, use the commentThreads.insert method.\n\n        Args:\n            body:\n                Provide a comment resource in the request body. You can give dataclass or just a dict with data.\n            parts:\n                Comma-separated list of one or more comment resource properties.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Comment data.\n        \"\"\"\n\n        params = {\"part\": enf_parts(resource=\"comments\", value=parts), **kwargs}\n        response = self._client.request(\n            method=\"POST\",\n            path=\"comments\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else Comment.from_dict(data)\n\n    def update(\n        self,\n        body: Union[dict, Comment],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        return_json: bool = False,\n        **kwargs,\n    ) -> Union[dict, Comment]:\n        \"\"\"Modifies a comment.\n\n        Args:\n            body:\n                Provide a comment resource in the request body. You can give dataclass or just a dict with data.\n            parts:\n                Comma-separated list of one or more comment resource properties.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Comment updated data.\n\n        \"\"\"\n        params = {\"part\": enf_parts(resource=\"comments\", value=parts), **kwargs}\n        response = self._client.request(\n            method=\"PUT\",\n            path=\"comments\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else Comment.from_dict(data)\n\n    def mark_as_spam(\n        self,\n        comment_id: str,\n        **kwargs,\n    ) -> bool:\n        \"\"\"Expresses the caller's opinion that one or more comments should be flagged as spam.\n\n        Deprecated at [2023.09.12](https://developers.google.com/youtube/v3/revision_history#september-12,-2023)\n\n        Args:\n            comment_id:\n                ID for the target comment.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Mark as spam status.\n\n        \"\"\"\n        params = {\"id\": comment_id, **kwargs}\n        response = self._client.request(\n            method=\"POST\",\n            path=\"comments/markAsSpam\",\n            params=params,\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n\n    def set_moderation_status(\n        self,\n        comment_id: str,\n        moderation_status: str,\n        ban_author: Optional[bool] = None,\n        **kwargs,\n    ) -> bool:\n        \"\"\"Sets the moderation status of one or more comments.\n\n        Args:\n            comment_id:\n                ID for the target comment.\n            moderation_status:\n                Identifies the new moderation status of the specified comments.\n                Acceptable values:\n                    - heldForReview: Marks a comment as awaiting review by a moderator.\n                    - published: Clears a comment for public display.\n                    - rejected:  Rejects a comment as being unfit for display.\n                        This action also effectively hides all replies to the rejected comment.\n            ban_author:\n                Set the parameter value to true to ban the author.\n                 This parameter is only valid if the moderationStatus parameter is also set to rejected.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Moderation status set status.\n        \"\"\"\n        params = {\n            \"id\": comment_id,\n            \"moderationStatus\": moderation_status,\n            \"banAuthor\": ban_author,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"POST\",\n            path=\"comments/setModerationStatus\",\n            params=params,\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n\n    def delete(\n        self,\n        comment_id: str,\n        **kwargs,\n    ) -> bool:\n        \"\"\"Deletes a comment.\n\n        Args:\n            comment_id:\n                ID for the target comment.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Comment delete status.\n\n        \"\"\"\n        params = {\"id\": comment_id, **kwargs}\n        response = self._client.request(\n            method=\"DELETE\",\n            path=\"comments\",\n            params=params,\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n"
  },
  {
    "path": "pyyoutube/resources/i18n_languages.py",
    "content": "\"\"\"\ni18n language resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import I18nLanguageListResponse\nfrom pyyoutube.utils.params_checker import enf_parts\n\n\nclass I18nLanguagesResource(Resource):\n    \"\"\"An i18nLanguage resource identifies an application language that the YouTube website supports.\n    The application language can also be referred to as a UI language\n\n    References: https://developers.google.com/youtube/v3/docs/i18nLanguages\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, I18nLanguageListResponse]:\n        \"\"\"Returns a list of application languages that the YouTube website supports.\n\n        Args:\n            parts:\n                Comma-separated list of one or more i18n languages resource properties.\n                Accepted values: snippet.\n            hl:\n                Specifies the language that should be used for text values in the API response.\n                The default value is en_US.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            i18n language data\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"i18nLanguages\", value=parts),\n            \"hl\": hl,\n            **kwargs,\n        }\n        response = self._client.request(path=\"i18nLanguages\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else I18nLanguageListResponse.from_dict(data)\n"
  },
  {
    "path": "pyyoutube/resources/i18n_regions.py",
    "content": "\"\"\"\ni18n regions resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import I18nRegionListResponse\nfrom pyyoutube.utils.params_checker import enf_parts\n\n\nclass I18nRegionsResource(Resource):\n    \"\"\"An i18nRegion resource identifies a geographic area that a YouTube user can select as\n    the preferred content region.\n\n    References: https://developers.google.com/youtube/v3/docs/i18nRegions\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, I18nRegionListResponse]:\n        \"\"\"Returns a list of content regions that the YouTube website supports.\n\n        Args:\n            parts:\n                Comma-separated list of one or more i18n regions resource properties.\n                Accepted values: snippet.\n            hl:\n                Specifies the language that should be used for text values in the API response.\n                The default value is en_US.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            i18n regions data.\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"i18nRegions\", value=parts),\n            \"hl\": hl,\n            **kwargs,\n        }\n        response = self._client.request(path=\"i18nRegions\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else I18nRegionListResponse.from_dict(data)\n"
  },
  {
    "path": "pyyoutube/resources/members.py",
    "content": "\"\"\"\nMembers resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import MemberListResponse\nfrom pyyoutube.utils.params_checker import enf_parts, enf_comma_separated\n\n\nclass MembersResource(Resource):\n    \"\"\"A member resource represents a channel member for a YouTube channel.\n\n    References: https://developers.google.com/youtube/v3/docs/members\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        mode: Optional[str] = None,\n        max_results: Optional[int] = None,\n        page_token: Optional[str] = None,\n        has_access_to_level: Optional[str] = None,\n        filter_by_member_channel_id: Optional[Union[str, list, tuple, set]] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, MemberListResponse]:\n        \"\"\"Lists members (formerly known as \"sponsors\") for a channel.\n\n        Args:\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: snippet\n            mode:\n                Indicates which members will be included in the API response.\n                Accepted values:\n                    - all_current: List current members, from newest to oldest.\n                    - updates: List only members that joined or upgraded since the previous API call.\n            max_results:\n                The parameter specifies the maximum number of items that should be returned\n                the result set.\n                Acceptable values are 0 to 1000, inclusive. The default value is 5.\n            page_token:\n                The parameter identifies a specific page in the result set that should be returned.\n            has_access_to_level:\n                A level ID that specifies the minimum level that members in the result set should have.\n            filter_by_member_channel_id:\n                specifies a comma-separated list of channel IDs that can be used to check the membership\n                status of specific users.\n                Maximum of 100 channels can be specified per call.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Members data.\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"members\", value=parts),\n            \"mode\": mode,\n            \"maxResults\": max_results,\n            \"pageToken\": page_token,\n            \"hasAccessToLevel\": has_access_to_level,\n            \"filterByMemberChannelId\": enf_comma_separated(\n                field=\"filter_by_member_channel_id\", value=filter_by_member_channel_id\n            ),\n            **kwargs,\n        }\n        response = self._client.request(path=\"members\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else MemberListResponse.from_dict(data)\n"
  },
  {
    "path": "pyyoutube/resources/membership_levels.py",
    "content": "\"\"\"\nMembership levels resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.models import MembershipsLevelListResponse\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.utils.params_checker import enf_parts\n\n\nclass MembershipLevelsResource(Resource):\n    \"\"\"A membershipsLevel resource identifies a pricing level managed by the creator that authorized the API request.\n\n    References: https://developers.google.com/youtube/v3/docs/membershipsLevels\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, MembershipsLevelListResponse]:\n        \"\"\"Lists membership levels for the channel that authorized the request.\n\n        Args:\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,snippet\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Membership levels data.\n\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"membershipsLevels\", value=parts),\n            **kwargs,\n        }\n        response = self._client.request(path=\"membershipsLevels\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else MembershipsLevelListResponse.from_dict(data)\n"
  },
  {
    "path": "pyyoutube/resources/playlist_items.py",
    "content": "\"\"\"\nPlaylist items resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import PyYouTubeException, ErrorCode, ErrorMessage\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import PlaylistItem, PlaylistItemListResponse\nfrom pyyoutube.utils.params_checker import enf_parts, enf_comma_separated\n\n\nclass PlaylistItemsResource(Resource):\n    \"\"\"A playlistItem resource identifies another resource, such as a video, that is included\n    in a playlist. In addition, the playlistItem resource contains details about the included\n    resource that pertain specifically to how that resource is used in that playlist.\n\n    References: https://developers.google.com/youtube/v3/docs/playlistItems\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        playlist_item_id: Optional[Union[str, list, tuple, set]] = None,\n        playlist_id: Optional[str] = None,\n        max_results: Optional[int] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        page_token: Optional[str] = None,\n        video_id: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, PlaylistItemListResponse]:\n        \"\"\"Returns a collection of playlist items that match the API request parameters.\n\n        Args:\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,contentDetails,snippet,snippet\n            playlist_item_id:\n                Specifies a comma-separated list of one or more unique playlist item IDs.\n            playlist_id:\n                Specifies the unique ID of the playlist for which you want to retrieve playlist items.\n            max_results:\n                The parameter specifies the maximum number of items that should be returned\n                the result set.\n                Acceptable values are 0 to 50, inclusive. The default value is 5.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            page_token:\n                The parameter identifies a specific page in the result set that should be returned.\n            video_id:\n                Specifies that the request should return only the playlist items that contain the specified video.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Playlist items data.\n\n        Raises:\n            PyYouTubeException: Missing filter parameter.\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"playlistItems\", value=parts),\n            \"maxResults\": max_results,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"videoId\": video_id,\n            \"pageToken\": page_token,\n            **kwargs,\n        }\n        if playlist_item_id is not None:\n            params[\"id\"] = enf_comma_separated(\n                field=\"playlist_item_id\", value=playlist_item_id\n            )\n        elif playlist_id is not None:\n            params[\"playlistId\"] = playlist_id\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=f\"Specify at least one of playlist_item_id or playlist_id\",\n                )\n            )\n\n        response = self._client.request(path=\"playlistItems\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else PlaylistItemListResponse.from_dict(data)\n\n    def insert(\n        self,\n        body: Union[dict, PlaylistItem],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, PlaylistItem]:\n        \"\"\"Adds a resource to a playlist.\n\n        Args:\n            body:\n                Provide playlist item data in the request body. You can give dataclass or just a dict with data.\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,contentDetails,snippet,snippet\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Playlist item data.\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"playlistItems\", value=parts),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"POST\",\n            path=\"playlistItems\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else PlaylistItem.from_dict(data)\n\n    def update(\n        self,\n        body: Union[dict, PlaylistItem],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, PlaylistItem]:\n        \"\"\"Modifies a playlist item. For example, you could update the item's position in the playlist.\n\n        Args:\n            body:\n                Provide playlist item data in the request body. You can give dataclass or just a dict with data.\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,contentDetails,snippet,snippet\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Playlist item update data.\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"playlistItems\", value=parts),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"PUT\",\n            path=\"playlistItems\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else PlaylistItem.from_dict(data)\n\n    def delete(\n        self,\n        playlist_item_id: str,\n        on_behalf_of_content_owner: Optional[str] = None,\n        **kwargs: Optional[dict],\n    ) -> bool:\n        \"\"\"Deletes a playlist item.\n\n        Args:\n            playlist_item_id:\n                Specifies the YouTube playlist item ID for the playlist item that is being deleted.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Playlist item delete status.\n\n        \"\"\"\n        params = {\n            \"id\": playlist_item_id,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"DELETE\",\n            path=\"playlistItems\",\n            params=params,\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n"
  },
  {
    "path": "pyyoutube/resources/playlists.py",
    "content": "\"\"\"\nPlaylist resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import PyYouTubeException, ErrorCode, ErrorMessage\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import Playlist, PlaylistListResponse\nfrom pyyoutube.utils.params_checker import enf_comma_separated, enf_parts\n\n\nclass PlaylistsResource(Resource):\n    \"\"\"A playlist resource represents a YouTube playlist.\n\n    References: https://developers.google.com/youtube/v3/docs/playlists\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        channel_id: Optional[str] = None,\n        playlist_id: Optional[Union[str, list, tuple, set]] = None,\n        mine: Optional[bool] = None,\n        hl: Optional[str] = None,\n        max_results: Optional[int] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        on_behalf_of_content_owner_channel: Optional[str] = None,\n        page_token: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, PlaylistListResponse]:\n        \"\"\"Returns a collection of playlists that match the API request parameters.\n\n        Args:\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,contentDetails,localizations,player,snippet,status\n            channel_id:\n                Indicates that the API should only return the specified channel's playlists.\n            playlist_id:\n                Specifies a comma-separated list of the YouTube playlist ID(s) for the resource(s)\n                that are being retrieved.\n            mine:\n                Set this parameter's value to true to instruct the API to only return playlists\n                owned by the authenticated user.\n            hl:\n                The hl parameter instructs the API to retrieve localized resource metadata for\n                a specific application language that the YouTube website supports.\n                The parameter value must be a language code included in the list returned by the\n                i18nLanguages.list method.\n            max_results:\n                The parameter specifies the maximum number of items that should be returned\n                the result set.\n                Acceptable values are 0 to 50, inclusive. The default value is 5.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            on_behalf_of_content_owner_channel:\n                The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel\n                to which a video is being added. This parameter is required when a request specifies a value\n                for the onBehalfOfContentOwner parameter, and it can only be used in conjunction with that\n                parameter. In addition, the request must be authorized using a CMS account that is linked to\n                the content owner that the onBehalfOfContentOwner parameter specifies. Finally, the channel\n                that the onBehalfOfContentOwnerChannel parameter value specifies must be linked to the content\n                owner that the onBehalfOfContentOwner parameter specifies.\n            page_token:\n                The parameter identifies a specific page in the result set that should be returned.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Playlist data.\n        Raises:\n            PyYouTubeException: Missing filter parameter.\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"playlists\", value=parts),\n            \"hl\": hl,\n            \"maxResults\": max_results,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"onBehalfOfContentOwnerChannel\": on_behalf_of_content_owner_channel,\n            \"pageToken\": page_token,\n            **kwargs,\n        }\n        if channel_id is not None:\n            params[\"channelId\"] = channel_id\n        elif playlist_id is not None:\n            params[\"id\"] = enf_comma_separated(field=\"playlist_id\", value=playlist_id)\n        elif mine is not None:\n            params[\"mine\"] = mine\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Specify at least one of channel_id, playlist_id or mine\",\n                )\n            )\n        response = self._client.request(path=\"playlists\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else PlaylistListResponse.from_dict(data)\n\n    def insert(\n        self,\n        body: Union[dict, Playlist],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        on_behalf_of_content_owner_channel: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, Playlist]:\n        \"\"\"Creates a playlist.\n\n        Args:\n            body:\n                Provide playlist data in the request body. You can give dataclass or just a dict with data.\n            parts:\n                The part parameter serves two purposes in this operation. It identifies the properties\n                that the write operation will set as well as the properties that the API response will include.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            on_behalf_of_content_owner_channel:\n                The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel\n                to which a video is being added. This parameter is required when a request specifies a value\n                for the onBehalfOfContentOwner parameter, and it can only be used in conjunction with that\n                parameter. In addition, the request must be authorized using a CMS account that is linked to\n                the content owner that the onBehalfOfContentOwner parameter specifies. Finally, the channel\n                that the onBehalfOfContentOwnerChannel parameter value specifies must be linked to the content\n                owner that the onBehalfOfContentOwner parameter specifies.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n        Returns:\n            playlist data.\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"playlists\", value=parts),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"onBehalfOfContentOwnerChannel\": on_behalf_of_content_owner_channel,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"POST\", path=\"playlists\", params=params, json=body\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else Playlist.from_dict(data)\n\n    def update(\n        self,\n        body: Union[dict, Playlist],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, Playlist]:\n        \"\"\"Modifies a playlist.\n\n        Args:\n            body:\n                Provide playlist data in the request body. You can give dataclass or just a dict with data.\n            parts:\n                The part parameter serves two purposes in this operation. It identifies the properties\n                that the write operation will set as well as the properties that the API response will include.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Playlist updated data.\n\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"playlists\", value=parts),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"PUT\", path=\"playlists\", params=params, json=body\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else Playlist.from_dict(data)\n\n    def delete(\n        self,\n        playlist_id: str,\n        on_behalf_of_content_owner: Optional[str] = None,\n        **kwargs: Optional[dict],\n    ) -> bool:\n        \"\"\"Deletes a playlist.\n\n        Args:\n            playlist_id:\n                Specifies the YouTube playlist ID for the playlist that is being deleted.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            playlist delete status\n\n        \"\"\"\n        params = {\n            \"id\": playlist_id,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"DELETE\",\n            path=\"playlists\",\n            params=params,\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n"
  },
  {
    "path": "pyyoutube/resources/search.py",
    "content": "\"\"\"\nSearch resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import SearchListResponse\nfrom pyyoutube.utils.params_checker import enf_parts\n\n\nclass SearchResource(Resource):\n    \"\"\"A search result contains information about a YouTube video, channel, or playlist\n    that matches the search parameters specified in an API request\n\n    References: https://developers.google.com/youtube/v3/docs/search\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        for_content_owner: Optional[bool] = None,\n        for_developer: Optional[bool] = None,\n        for_mine: Optional[bool] = None,\n        related_to_video_id: Optional[str] = None,\n        channel_id: Optional[str] = None,\n        channel_type: Optional[str] = None,\n        event_type: Optional[str] = None,\n        location: Optional[str] = None,\n        location_radius: Optional[str] = None,\n        max_results: Optional[int] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        order: Optional[str] = None,\n        page_token: Optional[str] = None,\n        published_after: Optional[str] = None,\n        published_before: Optional[str] = None,\n        q: Optional[str] = None,\n        region_code: Optional[str] = None,\n        relevance_language: Optional[str] = None,\n        safe_search: Optional[str] = None,\n        topic_id: Optional[str] = None,\n        type: Optional[Union[str, list, tuple, set]] = None,\n        video_caption: Optional[str] = None,\n        video_category_id: Optional[str] = None,\n        video_definition: Optional[str] = None,\n        video_dimension: Optional[str] = None,\n        video_duration: Optional[str] = None,\n        video_embeddable: Optional[str] = None,\n        video_license: Optional[str] = None,\n        video_paid_product_placement: Optional[str] = None,\n        video_syndicated: Optional[str] = None,\n        video_type: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, SearchListResponse]:\n        \"\"\"Returns a collection of search results that match the query parameters specified in the API request.\n\n        Notes:\n            Search API is very complex. If you want to search, You may need to read the parameter description\n            with the docs: https://developers.google.com/youtube/v3/docs/search/list#parameters\n\n        Args:\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: snippet\n            for_content_owner:\n                Parameter restricts the search to only retrieve videos owned by the content owner\n                identified by the onBehalfOfContentOwner parameter.\n            for_developer:\n                Parameter restricts the search to only retrieve videos uploaded via the developer's\n                application or website.\n            for_mine:\n                Parameter restricts the search to only retrieve videos owned by the authenticated user.\n            related_to_video_id:\n                Parameter retrieves a list of videos that are related to the video that the parameter value identifies.\n                Deprecated at [2023.08.07](https://developers.google.com/youtube/v3/revision_history#august-7,-2023)\n            channel_id:\n                Indicates that the API response should only contain resources created by the channel.\n            channel_type:\n                Parameter lets you restrict a search to a particular type of channel.\n                Acceptable values are:\n                    - any: Return all channels.\n                    - show: Only retrieve shows.\n            event_type:\n                Parameter restricts a search to broadcast events.\n                Acceptable values are:\n                    - completed: Only include completed broadcasts.\n                    - live: Only include active broadcasts.\n                    - upcoming: Only include upcoming broadcasts.\n            location:\n                Parameter value identifies the point at the center of the area.\n            location_radius:\n                Specifies the maximum distance that the location associated with a video can be from\n                that point for the video to still be included in the search results.\n            max_results:\n                The parameter specifies the maximum number of items that should be returned\n                the result set.\n                Acceptable values are 0 to 50, inclusive. The default value is 5.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            order:\n                Specifies the method that will be used to order resources in the API response.\n                The default value is relevance.\n                Acceptable values are:\n                    - date: Resources are sorted in reverse chronological order based on the date they were created.\n                    - rating: Resources are sorted from highest to lowest rating.\n                    - relevance: Resources are sorted based on their relevance to the search query.\n                    - title: Resources are sorted alphabetically by title.\n                    - videoCount: Channels are sorted in descending order of their number of uploaded videos.\n                    - viewCount: Resources are sorted from highest to lowest number of views.\n                    For live broadcasts, videos are sorted by number of concurrent viewers while the broadcasts\n                    are ongoing.\n            page_token:\n                The parameter identifies a specific page in the result set that should be returned.\n            published_after:\n                Indicates that the API response should only contain resources created at or after the specified time.\n            published_before:\n                Indicates that the API response should only contain resources created before or at the specified time.\n            q:\n                Specifies the query term to search for.\n            region_code:\n                Instructs the API to return search results for videos that can be viewed in the specified country.\n            relevance_language:\n                Instructs the API to return search results that are most relevant to the specified language.\n            safe_search:\n                Indicates whether the search results should include restricted content as well as standard content.\n                Acceptable values are:\n                    - moderate: YouTube will filter some content from search results and, at the least,\n                        will filter content that is restricted in your locale. Based on their content, search\n                        results could be removed from search results or demoted in search results.\n                        This is the default parameter value.\n                    - none: YouTube will not filter the search result set.\n                    - strict: YouTube will try to exclude all restricted content from the search result set.\n                        Based on their content, search results could be removed from search results or\n                        demoted in search results.\n            topic_id:\n                Indicates that the API response should only contain resources associated with the specified topic.\n            type:\n                Parameter restricts a search query to only retrieve a particular type of resource.\n                The value is a comma-separated list of resource types.\n                Acceptable values are: channel,playlist,video\n            video_caption:\n                Indicates whether the API should filter video search results based on whether they have captions.\n                Acceptable values are:\n                    - any: Do not filter results based on caption availability.\n                    - closedCaption: Only include videos that have captions.\n                    - none: Only include videos that do not have captions.\n            video_category_id:\n                Parameter filters video search results based on their category.\n            video_definition:\n                Parameter lets you restrict a search to only include either high definition (HD) or\n                standard definition (SD) videos.\n                Acceptable values are:\n                    - any: Return all videos, regardless of their resolution.\n                    - high: Only retrieve HD videos.\n                    - standard: Only retrieve videos in standard definition.\n            video_dimension:\n                Parameter lets you restrict a search to only retrieve 2D or 3D videos.\n                Acceptable values are:\n                    - 2d: Restrict search results to exclude 3D videos.\n                    - 3d: Restrict search results to only include 3D videos.\n                    - any: Include both 3D and non-3D videos in returned results. This is the default value.\n            video_duration:\n                Parameter filters video search results based on their duration.\n                Acceptable values are:\n                    - any: Do not filter video search results based on their duration. This is the default value.\n                    - long: Only include videos longer than 20 minutes.\n                    - medium: Only include videos that are between four and 20 minutes long (inclusive).\n                    - short: Only include videos that are less than four minutes long.\n            video_embeddable:\n                Parameter lets you to restrict a search to only videos that can be embedded into a webpage.\n                Acceptable values are:\n                    - any: Return all videos, embeddable or not.\n                    - true: Only retrieve embeddable videos.\n            video_license:\n                Parameter filters search results to only include videos with a particular license.\n                Acceptable values are:\n                    - any – Return all videos, regardless of which license they have, that match the query parameters.\n                    - creativeCommon – Only return videos that have a Creative Commons license.\n                        Users can reuse videos with this license in other videos that they create. Learn more.\n                    - youtube – Only return videos that have the standard YouTube license.\n            video_paid_product_placement:\n                Parameter filters search results to only include videos that the creator has denoted as\n                having a paid promotion.\n                Acceptable values are:\n                    - any – Return all videos, regardless of whether they contain paid promotions.\n                    - true – Only retrieve videos with paid promotions.\n            video_syndicated:\n                Parameter lets you to restrict a search to only videos that can be played outside youtube.com.\n                Acceptable values are:\n                    - any: Return all videos, syndicated or not.\n                    - true: Only retrieve syndicated videos.\n            video_type:\n                Parameter lets you restrict a search to a particular type of videos.\n                Acceptable values are:\n                    - any: Return all videos.\n                    - episode: Only retrieve episodes of shows.\n                    - movie: Only retrieve movies.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n        Returns:\n            Search result data\n\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"search\", value=parts),\n            \"channelId\": channel_id,\n            \"channelType\": channel_type,\n            \"eventType\": event_type,\n            \"location\": location,\n            \"locationRadius\": location_radius,\n            \"maxResults\": max_results,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"order\": order,\n            \"pageToken\": page_token,\n            \"publishedAfter\": published_after,\n            \"publishedBefore\": published_before,\n            \"q\": q,\n            \"regionCode\": region_code,\n            \"relevanceLanguage\": relevance_language,\n            \"safeSearch\": safe_search,\n            \"topicId\": topic_id,\n            \"type\": type,\n            \"videoCaption\": video_caption,\n            \"videoCategoryId\": video_category_id,\n            \"videoDefinition\": video_definition,\n            \"videoDimension\": video_dimension,\n            \"videoDuration\": video_duration,\n            \"videoEmbeddable\": video_embeddable,\n            \"videoLicense\": video_license,\n            \"videoPaidProductPlacement\": video_paid_product_placement,\n            \"videoSyndicated\": video_syndicated,\n            \"videoType\": video_type,\n            **kwargs,\n        }\n\n        if for_content_owner is not None:\n            params[\"forContentOwner\"] = for_content_owner\n        elif for_developer is not None:\n            params[\"forDeveloper\"] = for_developer\n        elif for_mine is not None:\n            params[\"forMine\"] = for_mine\n        elif related_to_video_id is not None:\n            params[\"relatedToVideoId\"] = related_to_video_id\n\n        response = self._client.request(path=\"search\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else SearchListResponse.from_dict(data)\n"
  },
  {
    "path": "pyyoutube/resources/subscriptions.py",
    "content": "\"\"\"\nSubscription resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import PyYouTubeException, ErrorCode, ErrorMessage\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import Subscription, SubscriptionListResponse\nfrom pyyoutube.utils.params_checker import enf_parts, enf_comma_separated\n\n\nclass SubscriptionsResource(Resource):\n    \"\"\"A subscription resource contains information about a YouTube user subscription.\n\n    References: https://developers.google.com/youtube/v3/docs/subscriptions\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        channel_id: Optional[str] = None,\n        subscription_id: Optional[Union[str, list, tuple, set]] = None,\n        mine: Optional[bool] = None,\n        my_recent_subscribers: Optional[bool] = None,\n        my_subscribers: Optional[bool] = None,\n        for_channel_id: Optional[Union[str, list, tuple, set]] = None,\n        max_results: Optional[int] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        on_behalf_of_content_owner_channel: Optional[str] = None,\n        order: Optional[str] = None,\n        page_token: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, SubscriptionListResponse]:\n        \"\"\"Returns subscription resources that match the API request criteria.\n\n        Args:\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,contentDetails,snippet,subscriberSnippet\n            channel_id:\n                Specifies a YouTube channel ID. The API will only return that channel's subscriptions.\n            subscription_id:\n                Specifies a comma-separated list of the YouTube subscription ID(s) for the resource(s)\n                that are being retrieved.\n            mine:\n                Set this parameter's value to true to retrieve a feed of the authenticated user's subscriptions.\n            my_recent_subscribers:\n                Set this parameter's value to true to retrieve a feed of the subscribers of the authenticated user\n                in reverse chronological order (the newest first).\n            my_subscribers:\n                Set this parameter's value to true to retrieve a feed of the subscribers of the authenticated user\n                in no particular order.\n            for_channel_id:\n                Specifies a comma-separated list of channel IDs.\n                The API response will then only contain subscriptions matching those channels.\n            max_results:\n                The parameter specifies the maximum number of items that should be returned\n                the result set.\n                Acceptable values are 0 to 50, inclusive. The default value is 5.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            on_behalf_of_content_owner_channel:\n                The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of\n                the channel to which a video is being added. This parameter is required when a request\n                specifies a value for the onBehalfOfContentOwner parameter, and it can only be used in\n                conjunction with that parameter. In addition, the request must be authorized using a\n                CMS account that is linked to the content owner that the onBehalfOfContentOwner parameter\n                specifies. Finally, the channel that the onBehalfOfContentOwnerChannel parameter value\n                specifies must be linked to the content owner that the onBehalfOfContentOwner parameter specifies.\n            order:\n                Specifies the method that will be used to sort resources in the API response.\n                Acceptable values are:\n                    - alphabetical: Sort alphabetically.\n                    - relevance: Sort by relevance. Default.\n                    - unread: Sort by order of activity.\n            page_token:\n                The parameter identifies a specific page in the result set that should be returned.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Subscriptions data.\n        Raises:\n            PyYouTubeException: Missing filter parameter.\n        \"\"\"\n\n        params = {\n            \"part\": enf_parts(resource=\"subscriptions\", value=parts),\n            \"forChannelId\": enf_comma_separated(\n                field=\"for_channel_id\", value=for_channel_id\n            ),\n            \"maxResults\": max_results,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"onBehalfOfContentOwnerChannel\": on_behalf_of_content_owner_channel,\n            \"order\": order,\n            \"pageToken\": page_token,\n            **kwargs,\n        }\n\n        if channel_id is not None:\n            params[\"channelId\"] = channel_id\n        elif subscription_id is not None:\n            params[\"id\"] = enf_comma_separated(\n                field=\"subscription_id\", value=subscription_id\n            )\n        elif mine is not None:\n            params[\"mine\"] = mine\n        elif my_recent_subscribers is not None:\n            params[\"myRecentSubscribers\"] = my_recent_subscribers\n        elif my_subscribers is not None:\n            params[\"mySubscribers\"] = my_subscribers\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=f\"Specify at least one of channel_id,subscription_id,mine,my_recent_subscribers or mySubscribers\",\n                )\n            )\n\n        response = self._client.request(path=\"subscriptions\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else SubscriptionListResponse.from_dict(data)\n\n    def insert(\n        self,\n        body: Union[dict, Subscription],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, Subscription]:\n        \"\"\"Adds a subscription for the authenticated user's channel.\n\n        Args:\n            body:\n                Provide subscription data in the request body. You can give dataclass or just a dict with data.\n            parts:\n                The part parameter serves two purposes in this operation. It identifies the properties\n                that the write operation will set as well as the properties that the API response will include.\n                Accepted values: id,contentDetails,snippet,subscriberSnippet\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Subscription data\n\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"subscriptions\", value=parts),\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"POST\",\n            path=\"subscriptions\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else Subscription.from_dict(data)\n\n    def delete(\n        self,\n        subscription_id: str,\n        **kwargs: Optional[dict],\n    ) -> bool:\n        \"\"\"Deletes a subscription.\n\n        Args:\n            subscription_id:\n                Specifies the YouTube subscription ID for the resource that is being deleted.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Subscription delete status.\n        \"\"\"\n        params = {\n            \"id\": subscription_id,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"DELETE\", path=\"subscriptions\", params=params\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n"
  },
  {
    "path": "pyyoutube/resources/thumbnails.py",
    "content": "\"\"\"\nThumbnails resources implementation.\n\"\"\"\n\nfrom typing import Optional\n\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.media import Media, MediaUpload\n\n\nclass ThumbnailsResource(Resource):\n    \"\"\"A thumbnail resource identifies different thumbnail image sizes associated with a resource.\n\n    References: https://developers.google.com/youtube/v3/docs/thumbnails\n    \"\"\"\n\n    def set(\n        self,\n        video_id: str,\n        media: Media,\n        on_behalf_of_content_owner: Optional[str] = None,\n        **kwargs: Optional[dict],\n    ) -> MediaUpload:\n        params = {\n            \"videoId\": video_id,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        # Build a media upload instance.\n        media_upload = MediaUpload(\n            client=self._client,\n            resource=\"thumbnails/set\",\n            media=media,\n            params=params,\n        )\n        return media_upload\n"
  },
  {
    "path": "pyyoutube/resources/video_abuse_report_reasons.py",
    "content": "\"\"\"\nVideo abuse report reasons resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import VideoAbuseReportReasonListResponse\nfrom pyyoutube.utils.params_checker import enf_parts\n\n\nclass VideoAbuseReportReasonsResource(Resource):\n    \"\"\"A videoAbuseReportReason resource contains information about a reason that a video would be flagged\n    for containing abusive content.\n\n    References: https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        hl: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, VideoAbuseReportReasonListResponse]:\n        \"\"\"Retrieve a list of reasons that can be used to report abusive videos.\n\n        Args:\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,snippet\n            hl:\n                Specifies the language that should be used for text values in the API response.\n                The default value is en_US.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            reasons data.\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"videoAbuseReportReasons\", value=parts),\n            \"hl\": hl,\n            **kwargs,\n        }\n        response = self._client.request(path=\"videoAbuseReportReasons\", params=params)\n        data = self._client.parse_response(response=response)\n        return (\n            data if return_json else VideoAbuseReportReasonListResponse.from_dict(data)\n        )\n"
  },
  {
    "path": "pyyoutube/resources/video_categories.py",
    "content": "\"\"\"\nVideo categories resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.models import VideoCategoryListResponse\nfrom pyyoutube.utils.params_checker import enf_comma_separated, enf_parts\n\n\nclass VideoCategoriesResource(Resource):\n    \"\"\"A videoCategory resource identifies a category that has been or could be associated with uploaded videos.\n\n    References: https://developers.google.com/youtube/v3/docs/videoCategories\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        category_id: Optional[Union[str, list, tuple, set]] = None,\n        region_code: Optional[str] = None,\n        hl: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, VideoCategoryListResponse]:\n        \"\"\"Returns a list of categories that can be associated with YouTube videos.\n\n        Args:\n            parts:\n                Comma-separated list of one or more video category resource properties.\n                Accepted values: snippet\n            category_id:\n                Specifies a comma-separated list of video category IDs for the resources that you are retrieving.\n            region_code:\n                Instructs the API to return the list of video categories available in the specified country.\n                The parameter value is an ISO 3166-1 alpha-2 country code.\n            hl:\n                Specifies the language that should be used for text values in the API response.\n                The default value is en_US.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Video category data.\n        Raises:\n            PyYouTubeException: Missing filter parameter.\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"videoCategories\", value=parts),\n            \"hl\": hl,\n            **kwargs,\n        }\n\n        if category_id is not None:\n            params[\"id\"] = enf_comma_separated(field=\"category_id\", value=category_id)\n        elif region_code is not None:\n            params[\"regionCode\"] = region_code\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=f\"Specify at least one of category_id or region_code\",\n                )\n            )\n        response = self._client.request(path=\"videoCategories\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else VideoCategoryListResponse.from_dict(data)\n"
  },
  {
    "path": "pyyoutube/resources/videos.py",
    "content": "\"\"\"\nVideos resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import PyYouTubeException, ErrorCode, ErrorMessage\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.media import Media, MediaUpload\nfrom pyyoutube.models import (\n    Video,\n    VideoListResponse,\n    VideoGetRatingResponse,\n    VideoReportAbuse,\n)\nfrom pyyoutube.utils.params_checker import enf_comma_separated, enf_parts\n\n\nclass VideosResource(Resource):\n    \"\"\"A video resource represents a YouTube video.\n\n    References: https://developers.google.com/youtube/v3/docs/videos\n    \"\"\"\n\n    def list(\n        self,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        chart: Optional[str] = None,\n        video_id: Optional[Union[str, list, tuple, set]] = None,\n        my_rating: Optional[str] = None,\n        hl: Optional[str] = None,\n        max_height: Optional[int] = None,\n        max_results: Optional[int] = None,\n        max_width: Optional[int] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        page_token: Optional[str] = None,\n        region_code: Optional[str] = None,\n        video_category_id: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, VideoListResponse]:\n        \"\"\"Returns a list of videos that match the API request parameters.\n\n        Args:\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,contentDetails,fileDetails,liveStreamingDetails,\n                localizations,paidProductPlacementDetails,player,processingDetails,recordingDetails,snippet,statistics,\n                status,suggestions,topicDetails\n            chart:\n                Identifies the chart that you want to retrieve.\n                Acceptable values are:\n                    - mostPopular:  Return the most popular videos for the specified content region and video category.\n            video_id:\n                Specifies a comma-separated list of the YouTube video ID(s) for the resource(s) that are being retrieved.\n            my_rating:\n                Set this parameter's value to like or dislike to instruct the API to only return videos liked\n                or disliked by the authenticated user.\n                Acceptable values are:\n                    - dislike: Returns only videos disliked by the authenticated user.\n                    - like: Returns only video liked by the authenticated user.\n            hl:\n                Instructs the API to retrieve localized resource metadata for a specific application language\n                that the YouTube website supports.\n            max_height:\n                Specifies the maximum height of the embedded player returned the player.embedHtml property.\n            max_results:\n                Specifies the maximum number of items that should be returned the result set.\n            max_width:\n                Specifies the maximum width of the embedded player returned the player.embedHtml property.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            page_token:\n                The parameter identifies a specific page in the result set that should be returned.\n            region_code:\n                Instructs the API to select a video chart available in the specified region.\n            video_category_id:\n                Identifies the video category for which the chart should be retrieved.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Videos data.\n        Raises:\n            PyYouTubeException: Missing filter parameter.\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"videos\", value=parts),\n            \"hl\": hl,\n            \"maxHeight\": max_height,\n            \"maxResults\": max_results,\n            \"maxWidth\": max_width,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"pageToken\": page_token,\n            \"regionCode\": region_code,\n            \"videoCategoryId\": video_category_id,\n            **kwargs,\n        }\n        if chart is not None:\n            params[\"chart\"] = chart\n        elif video_id is not None:\n            params[\"id\"] = enf_comma_separated(field=\"video_id\", value=video_id)\n        elif my_rating is not None:\n            params[\"myRating\"] = my_rating\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.MISSING_PARAMS,\n                    message=\"Specify at least one of chart,video_id or my_rating\",\n                )\n            )\n        response = self._client.request(path=\"videos\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else VideoListResponse.from_dict(data)\n\n    def insert(\n        self,\n        body: Union[dict, Video],\n        media: Media,\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        notify_subscribers: Optional[bool] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        on_behalf_of_content_owner_channel: Optional[str] = None,\n        **kwargs,\n    ) -> MediaUpload:\n        \"\"\"Uploads a video to YouTube and optionally sets the video's metadata.\n\n        Example:\n\n            import pyyoutube.models as mds\n            from pyyoutube.media import Media\n\n            body = mds.Video(\n                snippet=mds.VideoSnippet(\n                    title=\"video title\",\n                    description=\"video description\"\n                )\n            )\n\n            media = Media(filename=\"video.mp4\")\n\n            upload = client.videos.insert(\n                body=body,\n                media=media,\n                parts=[\"snippet\"],\n            )\n\n            response = None\n            while response is None:\n                status, response = upload.next_chunk()\n                if status:\n                    print(f\"Upload {int(status.progress() * 100)} complete.\")\n\n            print(f\"Response body: {response}\")\n\n        Args:\n            body:\n                Provide video data in the request body. You can give dataclass or just a dict with data.\n            media:\n                Media data to upload.\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,contentDetails,fileDetails,liveStreamingDetails,\n                localizations,player,processingDetails,recordingDetails,snippet,statistics,\n                status,suggestions,topicDetails\n            notify_subscribers:\n                Indicates whether YouTube should send a notification about the new video to users who\n                subscribe to the video's channel\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many different YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            on_behalf_of_content_owner_channel:\n                The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel\n                to which a video is being added. This parameter is required when a request specifies a value\n                for the onBehalfOfContentOwner parameter, and it can only be used in conjunction with that\n                parameter. In addition, the request must be authorized using a CMS account that is linked to\n                the content owner that the onBehalfOfContentOwner parameter specifies. Finally, the channel\n                that the onBehalfOfContentOwnerChannel parameter value specifies must be linked to the content\n                owner that the onBehalfOfContentOwner parameter specifies.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n        Returns:\n            Video data.\n\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"videos\", value=parts),\n            \"notifySubscribers\": notify_subscribers,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            \"onBehalfOfContentOwnerChannel\": on_behalf_of_content_owner_channel,\n            **kwargs,\n        }\n\n        # Build a media upload instance.\n        media_upload = MediaUpload(\n            client=self._client,\n            resource=\"videos\",\n            media=media,\n            params=params,\n            body=body.to_dict_ignore_none(),\n        )\n        return media_upload\n\n    def update(\n        self,\n        body: Union[dict, Video],\n        parts: Optional[Union[str, list, tuple, set]] = None,\n        on_behalf_of_content_owner: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, Video]:\n        \"\"\"Updates a video's metadata.\n\n        Args:\n            body:\n                Provide video data in the request body. You can give dataclass or just a dict with data.\n            parts:\n                Comma-separated list of one or more channel resource properties.\n                Accepted values: id,contentDetails,fileDetails,liveStreamingDetails,\n                localizations,player,processingDetails,recordingDetails,snippet,statistics,\n                status,suggestions,topicDetails\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Video updated data.\n        \"\"\"\n        params = {\n            \"part\": enf_parts(resource=\"videos\", value=parts),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"PUT\",\n            path=\"videos\",\n            params=params,\n            json=body,\n        )\n        data = self._client.parse_response(response=response)\n        return data if return_json else Video.from_dict(data)\n\n    def rate(\n        self,\n        video_id: str,\n        rating: Optional[str] = None,\n        **kwargs: Optional[dict],\n    ) -> bool:\n        \"\"\"Add a like or dislike rating to a video or remove a rating from a video.\n\n        Args:\n            video_id:\n                Specifies the YouTube video ID of the video that is being rated or having its rating removed.\n            rating:\n                Specifies the rating to record.\n                Acceptable values are:\n                    - dislike: Records that the authenticated user disliked the video.\n                    - like: Records that the authenticated user liked the video.\n                    - none: Removes any rating that the authenticated user had previously set for the video.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Video rating status\n        \"\"\"\n        params = {\n            \"id\": video_id,\n            \"rating\": rating,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"POST\",\n            path=\"videos/rate\",\n            params=params,\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n\n    def get_rating(\n        self,\n        video_id: Union[str, list, tuple, set],\n        on_behalf_of_content_owner: Optional[str] = None,\n        return_json: bool = False,\n        **kwargs: Optional[dict],\n    ) -> Union[dict, VideoGetRatingResponse]:\n        \"\"\"Retrieves the ratings that the authorized user gave to a list of specified videos.\n\n        Args:\n            video_id:\n                Specifies a comma-separated list of the YouTube video ID(s).\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many different YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            return_json:\n                Type for returned data. If you set True JSON data will be returned.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            Video rating data.\n        \"\"\"\n\n        params = {\n            \"id\": enf_comma_separated(field=\"video_id\", value=video_id),\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(path=\"videos/getRating\", params=params)\n        data = self._client.parse_response(response=response)\n        return data if return_json else VideoGetRatingResponse.from_dict(data)\n\n    def report_abuse(\n        self,\n        body: Optional[Union[dict, VideoReportAbuse]],\n        on_behalf_of_content_owner: Optional[str] = None,\n        **kwargs: Optional[dict],\n    ) -> bool:\n        \"\"\"Reports a video for containing abusive content.\n\n        Args:\n            body:\n                Provide report abuse data in the request body. You can give dataclass or just a dict with data.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many different YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            report status.\n        \"\"\"\n        params = {\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"POST\",\n            path=\"videos/reportAbuse\",\n            params=params,\n            json=body,\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n\n    def delete(\n        self,\n        video_id: str,\n        on_behalf_of_content_owner: Optional[str] = None,\n        **kwargs: Optional[dict],\n    ) -> bool:\n        \"\"\"Deletes a YouTube video.\n\n        Args:\n            video_id:\n                Specifies the YouTube video ID for the resource that is being deleted.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many different YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            video delete status.\n        \"\"\"\n        params = {\n            \"id\": video_id,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"DELETE\",\n            path=\"videos\",\n            params=params,\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n"
  },
  {
    "path": "pyyoutube/resources/watermarks.py",
    "content": "\"\"\"\nWatermarks resource implementation.\n\"\"\"\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.resources.base_resource import Resource\nfrom pyyoutube.media import Media, MediaUpload\nfrom pyyoutube.models import Watermark\n\n\nclass WatermarksResource(Resource):\n    def set(\n        self,\n        channel_id: str,\n        body: Union[dict, Watermark],\n        media: Media,\n        on_behalf_of_content_owner: Optional[str] = None,\n        **kwargs: Optional[dict],\n    ) -> MediaUpload:\n        \"\"\"\n\n        Args:\n            channel_id:\n                Specifies the YouTube channel ID for which the watermark is being provided.\n            body:\n                Provide watermark data in the request body. You can give dataclass or just a dict with data.\n            media:\n                Media for watermark image.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n        Returns:\n            Watermark set status.\n        \"\"\"\n        params = {\n            \"channel_id\": channel_id,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n\n        # Build a media upload instance.\n        media_upload = MediaUpload(\n            client=self._client,\n            resource=\"watermarks/set\",\n            media=media,\n            params=params,\n            body=body.to_dict_ignore_none(),\n        )\n        return media_upload\n\n    def unset(\n        self,\n        channel_id: str,\n        on_behalf_of_content_owner: Optional[str] = None,\n        **kwargs: Optional[dict],\n    ) -> bool:\n        \"\"\"Deletes a channel's watermark image.\n\n        Args:\n            channel_id:\n                Specifies the YouTube channel ID for which the watermark is being unset.\n            on_behalf_of_content_owner:\n                The onBehalfOfContentOwner parameter indicates that the request's authorization\n                credentials identify a YouTube CMS user who is acting on behalf of the content\n                owner specified in the parameter value. This parameter is intended for YouTube\n                content partners that own and manage many difference YouTube channels. It allows\n                content owners to authenticate once and get access to all their video and channel\n                data, without having to provide authentication credentials for each individual channel.\n                The CMS account that the user authenticates with must be linked to the specified YouTube content owner.\n            **kwargs:\n                Additional parameters for system parameters.\n                Refer: https://cloud.google.com/apis/docs/system-parameters.\n\n        Returns:\n            watermark unset status.\n        \"\"\"\n        params = {\n            \"channelId\": channel_id,\n            \"onBehalfOfContentOwner\": on_behalf_of_content_owner,\n            **kwargs,\n        }\n        response = self._client.request(\n            method=\"POST\",\n            path=\"watermarks/unset\",\n            params=params,\n        )\n        if response.ok:\n            return True\n        self._client.parse_response(response=response)\n"
  },
  {
    "path": "pyyoutube/utils/__init__.py",
    "content": ""
  },
  {
    "path": "pyyoutube/utils/constants.py",
    "content": "\"\"\"\nsome constants for YouTube\n\"\"\"\n\nACTIVITIES_RESOURCE_PROPERTIES = {\"id\", \"snippet\", \"contentDetails\"}\n\nCAPTIONS_RESOURCE_PROPERTIES = {\"id\", \"snippet\"}\n\nCHANNEL_RESOURCE_PROPERTIES = {\n    \"id\",\n    \"brandingSettings\",\n    \"contentDetails\",\n    \"localizations\",\n    \"snippet\",\n    \"statistics\",\n    \"status\",\n    \"topicDetails\",\n}\n\nCHANNEL_SECTIONS_PROPERTIES = {\"id\", \"contentDetails\", \"snippet\"}\n\nCOMMENT_RESOURCE_PROPERTIES = {\"id\", \"snippet\"}\n\nCOMMENT_THREAD_RESOURCE_PROPERTIES = {\"id\", \"replies\", \"snippet\"}\n\nI18N_LANGUAGE_PROPERTIES = {\"snippet\"}\n\nI18N_REGION_PROPERTIES = {\"snippet\"}\n\nMEMBER_PROPERTIES = {\"snippet\"}\n\nMEMBERSHIP_LEVEL_PROPERTIES = {\"id\", \"snippet\"}\n\nPLAYLIST_ITEM_RESOURCE_PROPERTIES = {\"id\", \"contentDetails\", \"snippet\", \"status\"}\n\nPLAYLIST_RESOURCE_PROPERTIES = {\n    \"id\",\n    \"contentDetails\",\n    \"localizations\",\n    \"player\",\n    \"snippet\",\n    \"status\",\n}\n\nSEARCH_RESOURCE_PROPERTIES = {\"snippet\"}\n\nSUBSCRIPTION_RESOURCE_PROPERTIES = {\n    \"id\",\n    \"snippet\",\n    \"contentDetails\",\n    \"subscriberSnippet\",\n}\n\nVIDEO_ABUSE_REPORT_REASON_PROPERTIES = {\"id\", \"snippet\"}\n\nVIDEO_CATEGORY_RESOURCE_PROPERTIES = {\"snippet\"}\n\nVIDEO_RESOURCE_PROPERTIES = {\n    \"id\",\n    \"contentDetails\",\n    \"player\",\n    \"snippet\",\n    \"statistics\",\n    \"status\",\n    \"topicDetails\",\n    \"recordingDetails\",\n    \"liveStreamingDetails\",\n    \"paidProductPlacementDetails\",\n}\n\nGUIDE_CATEGORY_RESOURCE_PROPERTIES = {\"id\", \"snippet\"}\n\nRESOURCE_PARTS_MAPPING = {\n    \"activities\": ACTIVITIES_RESOURCE_PROPERTIES,\n    \"captions\": CAPTIONS_RESOURCE_PROPERTIES,\n    \"channels\": CHANNEL_RESOURCE_PROPERTIES,\n    \"channelSections\": CHANNEL_SECTIONS_PROPERTIES,\n    \"comments\": COMMENT_RESOURCE_PROPERTIES,\n    \"commentThreads\": COMMENT_THREAD_RESOURCE_PROPERTIES,\n    \"i18nLanguages\": I18N_LANGUAGE_PROPERTIES,\n    \"i18nRegions\": I18N_REGION_PROPERTIES,\n    \"members\": MEMBER_PROPERTIES,\n    \"membershipsLevels\": MEMBERSHIP_LEVEL_PROPERTIES,\n    \"playlistItems\": PLAYLIST_ITEM_RESOURCE_PROPERTIES,\n    \"playlists\": PLAYLIST_RESOURCE_PROPERTIES,\n    \"search\": SEARCH_RESOURCE_PROPERTIES,\n    \"subscriptions\": SUBSCRIPTION_RESOURCE_PROPERTIES,\n    \"videoAbuseReportReasons\": VIDEO_ABUSE_REPORT_REASON_PROPERTIES,\n    \"videoCategories\": VIDEO_CATEGORY_RESOURCE_PROPERTIES,\n    \"videos\": VIDEO_RESOURCE_PROPERTIES,\n    \"guideCategories\": GUIDE_CATEGORY_RESOURCE_PROPERTIES,\n}\n\nTOPICS = {\n    # Music topics\n    \"/m/04rlf\": \"Music (parent topic)\",\n    \"/m/02mscn\": \"Christian music\",\n    \"/m/0ggq0m\": \"Classical music\",\n    \"/m/01lyv\": \"Country\",\n    \"/m/02lkt\": \"Electronic music\",\n    \"/m/0glt670\": \"Hip hop music\",\n    \"/m/05rwpb\": \"Independent music\",\n    \"/m/03_d0\": \"Jazz\",\n    \"/m/028sqc\": \"Music of Asia\",\n    \"/m/0g293\": \"Music of Latin America\",\n    \"/m/064t9\": \"Pop music\",\n    \"/m/06cqb\": \"Reggae\",\n    \"/m/06j6l\": \"Rhythm and blues\",\n    \"/m/06by7\": \"Rock music\",\n    \"/m/0gywn\": \"Soul music\",\n    # Gaming topics\n    \"/m/0bzvm2\": \"Gaming (parent topic)\",\n    \"/m/025zzc\": \"Action game\",\n    \"/m/02ntfj\": \"Action-adventure game\",\n    \"/m/0b1vjn\": \"Casual game\",\n    \"/m/02hygl\": \"Music video game\",\n    \"/m/04q1x3q\": \"Puzzle video game\",\n    \"/m/01sjng\": \"Racing video game\",\n    \"/m/0403l3g\": \"Role-playing video game\",\n    \"/m/021bp2\": \"Simulation video game\",\n    \"/m/022dc6\": \"Sports game\",\n    \"/m/03hf_rm\": \"Strategy video game\",\n    # Sports topics\n    \"/m/06ntj\": \"Sports (parent topic)\",\n    \"/m/0jm_\": \"American football\",\n    \"/m/018jz\": \"Baseball\",\n    \"/m/018w8\": \"Basketball\",\n    \"/m/01cgz\": \"Boxing\",\n    \"/m/09xp_\": \"Cricket\",\n    \"/m/02vx4\": \"Football\",\n    \"/m/037hz\": \"Golf\",\n    \"/m/03tmr\": \"Ice hockey\",\n    \"/m/01h7lh\": \"Mixed martial arts\",\n    \"/m/0410tth\": \"Motorsport\",\n    \"/m/07bs0\": \"Tennis\",\n    \"/m/07_53\": \"Volleyball\",\n    # Entertainment topics\n    \"/m/02jjt\": \"Entertainment (parent topic)\",\n    \"/m/09kqc\": \"Humor\",\n    \"/m/02vxn\": \"Movies\",\n    \"/m/05qjc\": \"Performing arts\",\n    \"/m/066wd\": \"Professional wrestling\",\n    \"/m/0f2f9\": \"TV shows\",\n    # Lifestyle topics\n    \"/m/019_rr\": \"Lifestyle (parent topic)\",\n    \"/m/032tl\": \"Fashion\",\n    \"/m/027x7n\": \"Fitness\",\n    \"/m/02wbm\": \"Food\",\n    \"/m/03glg\": \"Hobby\",\n    \"/m/068hy\": \"Pets\",\n    \"/m/041xxh\": \"Physical attractiveness [Beauty]\",\n    \"/m/07c1v\": \"Technology\",\n    \"/m/07bxq\": \"Tourism\",\n    \"/m/07yv9\": \"Vehicles\",\n    # Society topics\n    \"/m/098wr\": \"Society (parent topic)\",\n    \"/m/09s1f\": \"Business\",\n    \"/m/0kt51\": \"Health\",\n    \"/m/01h6rj\": \"Military\",\n    \"/m/05qt0\": \"Politics\",\n    \"/m/06bvp\": \"Religion\",\n    # Other topics\n    \"/m/01k8wb\": \"Knowledge\",\n}\n"
  },
  {
    "path": "pyyoutube/utils/params_checker.py",
    "content": "\"\"\"\nfunction's params checker.\n\"\"\"\n\nimport logging\n\nfrom typing import Optional, Union\n\nfrom pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException\nfrom pyyoutube.utils.constants import RESOURCE_PARTS_MAPPING\n\nlogger = logging.getLogger(__name__)\n\n\ndef enf_comma_separated(\n    field: str,\n    value: Optional[Union[str, list, tuple, set]],\n):\n    \"\"\"\n    Check to see if field's value type belong to correct type.\n    If it is, return api need value, otherwise, raise a PyYouTubeException.\n\n    Args:\n        field (str):\n            Name of the field you want to do check.\n        value (str, list, tuple, set, Optional)\n            Value for the field.\n\n    Returns:\n        Api needed string\n    \"\"\"\n    if value is None:\n        return None\n    try:\n        if isinstance(value, str):\n            return value\n        elif isinstance(value, (list, tuple, set)):\n            if isinstance(value, set):\n                logging.warning(f\"Note: The order of the set is unreliable.\")\n            return \",\".join(value)\n        else:\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.INVALID_PARAMS,\n                    message=f\"Parameter ({field}) must be single str,comma-separated str,list,tuple or set\",\n                )\n            )\n    except (TypeError, ValueError):\n        raise PyYouTubeException(\n            ErrorMessage(\n                status_code=ErrorCode.INVALID_PARAMS,\n                message=f\"Parameter ({field}) must be single str,comma-separated str,list,tuple or set\",\n            )\n        )\n\n\ndef enf_parts(resource: str, value: Optional[Union[str, list, tuple, set]], check=True):\n    \"\"\"\n    Check to see if value type belong to correct type, and if resource support the given part.\n    If it is, return api need value, otherwise, raise a PyYouTubeException.\n\n    Args:\n        resource (str):\n            Name of the resource you want to retrieve.\n        value (str, list, tuple, set, Optional):\n            Value for the part.\n        check (bool, optional):\n            Whether check the resource properties.\n\n    Returns:\n        Api needed part string\n    \"\"\"\n    if value is None:\n        parts = RESOURCE_PARTS_MAPPING[resource]\n    elif isinstance(value, str):\n        parts = set(value.split(\",\"))\n    elif isinstance(value, (list, tuple, set)):\n        parts = set(value)\n    else:\n        raise PyYouTubeException(\n            ErrorMessage(\n                status_code=ErrorCode.INVALID_PARAMS,\n                message=f\"Parameter (parts) must be single str,comma-separated str,list,tuple or set\",\n            )\n        )\n\n    # Remove leading/trailing whitespaces\n    parts = set({part.strip() for part in parts})\n\n    # check parts whether support.\n    if check:\n        support_parts = RESOURCE_PARTS_MAPPING[resource]\n        if not support_parts.issuperset(parts):\n            not_support_parts = \",\".join(parts.difference(support_parts))\n            raise PyYouTubeException(\n                ErrorMessage(\n                    status_code=ErrorCode.INVALID_PARAMS,\n                    message=f\"Parts {not_support_parts} for resource {resource} not support\",\n                )\n            )\n    return \",\".join(parts)\n"
  },
  {
    "path": "pyyoutube/youtube_utils.py",
    "content": "\"\"\"\nThis provide some common utils methods for YouTube resource.\n\"\"\"\n\nimport isodate\nfrom isodate.isoerror import ISO8601Error\n\nfrom pyyoutube.error import ErrorMessage, PyYouTubeException\n\n\ndef get_video_duration(duration: str) -> int:\n    \"\"\"\n    Parse video ISO 8601 duration to seconds.\n    Refer: https://developers.google.com/youtube/v3/docs/videos#contentDetails.duration\n\n    Args:\n        duration(str)\n            Videos ISO 8601 duration. Like: PT14H23M42S\n    Returns:\n        integer for seconds.\n    \"\"\"\n    try:\n        seconds = isodate.parse_duration(duration).total_seconds()\n        return int(seconds)\n    except ISO8601Error as e:\n        raise PyYouTubeException(\n            ErrorMessage(\n                status_code=10001,\n                message=f\"Exception in convert video duration: {duration}. errors: {e}\",\n            )\n        )\n"
  },
  {
    "path": "testdata/apidata/abuse_reasons/abuse_reason.json",
    "content": "{\n  \"kind\": \"youtube#videoAbuseReportReasonListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/YH398HlGf_qbYlJQUZVMRoL4RTE\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#videoAbuseReportReason\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/_WIvuNJwlISvQNQt_ukh2m0kt2Y\\\"\",\n      \"id\": \"N\",\n      \"snippet\": {\n        \"label\": \"Sex or nudity\",\n        \"secondaryReasons\": [\n          {\n            \"id\": \"32\",\n            \"label\": \"Graphic sex or nudity\"\n          },\n          {\n            \"id\": \"33\",\n            \"label\": \"Content involving minors\"\n          },\n          {\n            \"id\": \"34\",\n            \"label\": \"Other sexual content\"\n          }\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#videoAbuseReportReason\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/9uBFtSRN_-W5oQDz_AhiTSN5sTE\\\"\",\n      \"id\": \"S\",\n      \"snippet\": {\n        \"label\": \"Spam or misleading\",\n        \"secondaryReasons\": [\n          {\n            \"id\": \"27\",\n            \"label\": \"Spam or mass advertising\"\n          },\n          {\n            \"id\": \"28\",\n            \"label\": \"Misleading thumbnail\"\n          },\n          {\n            \"id\": \"29\",\n            \"label\": \"Malware or phishing\"\n          },\n          {\n            \"id\": \"30\",\n            \"label\": \"Pharmaceutical drugs for sale\"\n          },\n          {\n            \"id\": \"31\",\n            \"label\": \"Other misleading info\"\n          }\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#videoAbuseReportReason\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/e6pyrZ9LzezCkkpfXAc0gkDdQ0Q\\\"\",\n      \"id\": \"V\",\n      \"snippet\": {\n        \"label\": \"Violent, hateful, or dangerous\",\n        \"secondaryReasons\": [\n          {\n            \"id\": \"35\",\n            \"label\": \"Promotes violence or hatred\"\n          },\n          {\n            \"id\": \"36\",\n            \"label\": \"Promotes terrorism\"\n          },\n          {\n            \"id\": \"37\",\n            \"label\": \"Bullying or abusing vulnerable individuals\"\n          },\n          {\n            \"id\": \"38\",\n            \"label\": \"Suicide or self-injury\"\n          },\n          {\n            \"id\": \"39\",\n            \"label\": \"Pharmaceutical or drug abuse\"\n          },\n          {\n            \"id\": \"40\",\n            \"label\": \"Other violent, hateful, or dangerous acts\"\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/access_token.json",
    "content": "{\"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}"
  },
  {
    "path": "testdata/apidata/activities/activities_by_channel_p1.json",
    "content": "{\n  \"kind\": \"youtube#activityListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/wgXTq3L7ZY7nAhRFHl3U-cfbtIM\\\"\",\n  \"nextPageToken\": \"CAoQAA\",\n  \"pageInfo\": {\n    \"totalResults\": 13,\n    \"resultsPerPage\": 10\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/Nx8UQRPToT_leC4ECgznp5ynzlw\\\"\",\n      \"id\": \"MTUxNTcyNTU1NjEyNjk5ODAxNzY0MzM4MDg=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-31T21:00:12.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Android Developer Challenge, Google Maps Platform, New in Chrome 78\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Du7E0okmNlk/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Du7E0okmNlk/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Du7E0okmNlk/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/Du7E0okmNlk/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/Du7E0okmNlk/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/E5fFu6HZXDfgKmndxIQtR6b9U48\\\"\",\n      \"id\": \"MTUxNTcyNDUyMjczNjk5ODAxNzY0MzY3NTI=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-30T16:17:53.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Developer Student Clubs 2019 Paris Leads Summit\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Yg7woDxIeBY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Yg7woDxIeBY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Yg7woDxIeBY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/Yg7woDxIeBY/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/Yg7woDxIeBY/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/dq9w5npCGyzsmwPILW3ywIxJEQk\\\"\",\n      \"id\": \"MTUxNTcyMzAyNDEwNjk5ODAxNzY0MzY2MjQ=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-28T22:40:10.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Dev Show Top 5 from the Android Dev Summit 2019\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/6jUbPkgADMk/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/6jUbPkgADMk/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/6jUbPkgADMk/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/6jUbPkgADMk/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/6jUbPkgADMk/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/wqKEFFnFDHpoAbNWO-MhiM5jPuc\\\"\",\n      \"id\": \"MTUxNTcyMDE5MjA3Njk5ODAxNzY0MzQxMjg=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-25T16:00:07.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Announcing the Google Maps Platform YouTube Channel!\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/UcGP8xTXSoA/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/UcGP8xTXSoA/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/UcGP8xTXSoA/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/UcGP8xTXSoA/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/UcGP8xTXSoA/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/Km3fXXOj6Vh4NgpqCy9l73ALmbs\\\"\",\n      \"id\": \"MTUxNTcxOTUzMzIyNjk5ODAxNzY0MzY4MTY=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-24T21:42:02.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Android NDK r21, Security Health Analytics for GCP, Bazel 1.0\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/9wU8ML2pUBs/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/9wU8ML2pUBs/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/9wU8ML2pUBs/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/9wU8ML2pUBs/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/9wU8ML2pUBs/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/4XzMakgiRPmTS2ZhbAu1JiUFSqE\\\"\",\n      \"id\": \"MTUxNTcxNjk4OTA0Njk5ODAxNzY0MzY5NDQ=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-21T23:01:44.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Amey learns to create a low-cost hearing aid with Android\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/-tzNjiwhdfo/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/-tzNjiwhdfo/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/-tzNjiwhdfo/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/-tzNjiwhdfo/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/-tzNjiwhdfo/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/0U50OgvF2amEQZNR1ha9S1rkwws\\\"\",\n      \"id\": \"MTUxNTcxNjczODQ2Njk5ODAxNzY0MzQxOTI=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-21T16:04:06.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Developing Accessible Routes for Google Maps\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/xFJ4Q6MB8A8/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/xFJ4Q6MB8A8/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/xFJ4Q6MB8A8/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/xFJ4Q6MB8A8/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/xFJ4Q6MB8A8/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/dynLVmlq3kVelMwmuEA4oEVtMpA\\\"\",\n      \"id\": \"MTUxNTcxMzUwMTA2Njk5ODAxNjIwNDA0MDA=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-17T22:08:26.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"#AndroidDevSummit, architecting on Google Cloud, Google Code-in 2019 Org, & more!\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/SBFfJeJGQIM/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/SBFfJeJGQIM/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/SBFfJeJGQIM/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/SBFfJeJGQIM/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/SBFfJeJGQIM/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/HCZb7JyJ2UdJwxeCtHEB6qiYSt4\\\"\",\n      \"id\": \"MTUxNTcxMTYwMjgxNjk5ODAxNjIwNDI3MDQ=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-15T17:24:41.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Developer Student Clubs 2019 South East Asia Leads Summit\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/lBMvXIsTVDQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/lBMvXIsTVDQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/lBMvXIsTVDQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/lBMvXIsTVDQ/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/2oVIgF4Bamzs2ZNX_Q8sHInq0kQ\\\"\",\n      \"id\": \"MTUxNTcwNzQzMzU3Njk5ODAxNjIwNDMzNDQ=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-10T21:35:57.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Android Emulator tools, updates to AutoML Vision Edge, AutoML Video, & Video Intelligence API\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/FOtdgiw2Emo/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/FOtdgiw2Emo/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/FOtdgiw2Emo/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/FOtdgiw2Emo/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/FOtdgiw2Emo/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/activities/activities_by_channel_p2.json",
    "content": "{\n  \"kind\": \"youtube#activityListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/ivly37DC9305MS_NVi_P4LZl_5I\\\"\",\n  \"prevPageToken\": \"CAoQAQ\",\n  \"pageInfo\": {\n    \"totalResults\": 13,\n    \"resultsPerPage\": 10\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/kZCGI2QUa5ta2c3L6EQevDFgRmo\\\"\",\n      \"id\": \"MTUxNTcwMTQxNzc1MjMwMTQ0NDI5ODcyMTY=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-03T22:29:35.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google Play Pass, DevFest, Firebase Summit ‘19, & more!\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/3QroLKeXjzU/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/3QroLKeXjzU/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/3QroLKeXjzU/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/3QroLKeXjzU/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/3QroLKeXjzU/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/NM37ICD3UdOFmxzZT7Cxz8BLxK4\\\"\",\n      \"id\": \"MTUxNTY5OTcxNzAyMjMwMTQ0NDI5ODQ0NjQ=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-01T23:15:02.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Thinking in 5G (Google I/O'19)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/fn4SaN-lUns/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/fn4SaN-lUns/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/fn4SaN-lUns/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/fn4SaN-lUns/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/fn4SaN-lUns/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/EwpknHmWhnkbN7WZy_xnvbfcbWg\\\"\",\n      \"id\": \"MTUxNTY5OTU2NDg1MjMwMTQ0NDI5ODUyMzI=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-01T19:01:25.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Dev Show Top 5 from the Firebase Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/fNVT0G3ttTM/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/fNVT0G3ttTM/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/fNVT0G3ttTM/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/fNVT0G3ttTM/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/fNVT0G3ttTM/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"type\": \"upload\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/activities/activities_by_mine_p1.json",
    "content": "{\n  \"kind\": \"youtube#activityListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/le7hKns0ey3G45tVxUz8WPZskcQ\\\"\",\n  \"nextPageToken\": \"CAEQAA\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 1\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZVGvURQNCh-0EGNyS2UTdedzrhM\\\"\",\n      \"id\": \"MTUxNTc0OTk2MjI3NzE4NDU5NjEyODA4MA==\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T02:57:07.000Z\",\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"title\": \"华山日出\",\n        \"description\": \"冷冷的山头\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"ikaros-life\",\n        \"type\": \"upload\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/activities/activities_by_mine_p2.json",
    "content": "{\n  \"kind\": \"youtube#activityListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/6yncT242auTddLxSe4dfDTC-4xE\\\"\",\n  \"prevPageToken\": \"CAEQAQ\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 1\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZshL2QThX_bnTHhpJy5emGCWHcE\\\"\",\n      \"id\": \"MTUxNTc0OTk1OTAyNDkwNjA3MjU5NzQ1Ng==\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T02:51:42.000Z\",\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"title\": \"海上日出\",\n        \"description\": \"美美美\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"ikaros-life\",\n        \"type\": \"upload\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/captions/captions_by_video.json",
    "content": "{\n  \"kind\": \"youtube#captionListResponse\",\n  \"etag\": \"\\\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/bB4ewYNN7bQHonV-K7efrgBqh8M\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#caption\",\n      \"etag\": \"\\\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\\\"\",\n      \"id\": \"SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I\",\n      \"snippet\": {\n        \"videoId\": \"oHR3wURdJ94\",\n        \"lastUpdated\": \"2020-01-14T09:40:49.981Z\",\n        \"trackKind\": \"standard\",\n        \"language\": \"en\",\n        \"name\": \"\",\n        \"audioTrackType\": \"unknown\",\n        \"isCC\": false,\n        \"isLarge\": false,\n        \"isEasyReader\": false,\n        \"isDraft\": false,\n        \"isAutoSynced\": false,\n        \"status\": \"serving\"\n      }\n    },\n    {\n      \"kind\": \"youtube#caption\",\n      \"etag\": \"\\\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/iRxIplZcCiX0oujr5gSVMXkij8M\\\"\",\n      \"id\": \"fPMuDm722CIRcUAT3NTPQHQZJZJxt39kU7JvrHk8Kzs=\",\n      \"snippet\": {\n        \"videoId\": \"oHR3wURdJ94\",\n        \"lastUpdated\": \"2020-01-14T09:39:46.991Z\",\n        \"trackKind\": \"standard\",\n        \"language\": \"zh-Hans\",\n        \"name\": \"\",\n        \"audioTrackType\": \"unknown\",\n        \"isCC\": false,\n        \"isLarge\": false,\n        \"isEasyReader\": false,\n        \"isDraft\": false,\n        \"isAutoSynced\": false,\n        \"status\": \"serving\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/captions/captions_filter_by_id.json",
    "content": "{\n  \"kind\": \"youtube#captionListResponse\",\n  \"etag\": \"\\\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/4OU1z5mciyh4emins-W6FGneNdM\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#caption\",\n      \"etag\": \"\\\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\\\"\",\n      \"id\": \"SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I\",\n      \"snippet\": {\n        \"videoId\": \"oHR3wURdJ94\",\n        \"lastUpdated\": \"2020-01-14T09:40:49.981Z\",\n        \"trackKind\": \"standard\",\n        \"language\": \"en\",\n        \"name\": \"\",\n        \"audioTrackType\": \"unknown\",\n        \"isCC\": false,\n        \"isLarge\": false,\n        \"isEasyReader\": false,\n        \"isDraft\": false,\n        \"isAutoSynced\": false,\n        \"status\": \"serving\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/captions/insert_response.json",
    "content": "{\n  \"kind\": \"youtube#caption\",\n  \"etag\": \"R7KYT4aJbHp2wxlTmtFuKJ4pmF8\",\n  \"id\": \"AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA\",\n  \"snippet\": {\n    \"videoId\": \"zxTVeyG1600\",\n    \"lastUpdated\": \"2022-12-13T08:20:45.636548Z\",\n    \"trackKind\": \"standard\",\n    \"language\": \"ja\",\n    \"name\": \"\\\\u65e5\\\\u6587\\\\u5b57\\\\u5e55\",\n    \"audioTrackType\": \"unknown\",\n    \"isCC\": false,\n    \"isLarge\": false,\n    \"isEasyReader\": false,\n    \"isDraft\": true,\n    \"isAutoSynced\": false,\n    \"status\": \"serving\"\n  }\n}"
  },
  {
    "path": "testdata/apidata/captions/update_response.json",
    "content": "{\n  \"kind\": \"youtube#caption\",\n  \"etag\": \"R7KYT4aJbHp2wxlTmtFuKJ4pmF8\",\n  \"id\": \"AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA\",\n  \"snippet\": {\n    \"videoId\": \"zxTVeyG1600\",\n    \"lastUpdated\": \"2022-12-13T08:20:45.636548Z\",\n    \"trackKind\": \"standard\",\n    \"language\": \"ja\",\n    \"name\": \"\\\\u65e5\\\\u6587\\\\u5b57\\\\u5e55\",\n    \"audioTrackType\": \"unknown\",\n    \"isCC\": false,\n    \"isLarge\": false,\n    \"isEasyReader\": false,\n    \"isDraft\": false,\n    \"isAutoSynced\": false,\n    \"status\": \"serving\"\n  }\n}"
  },
  {
    "path": "testdata/apidata/categories/guide_categories_by_region.json",
    "content": "{\n  \"kind\": \"youtube#guideCategoryListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/cKe5cQFDAFacJgSsRH_6x7oGHZU\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\\\"\",\n      \"id\": \"GCQmVzdCBvZiBZb3VUdWJl\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Best of YouTube\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ImrTevQ0UvryyOmvjPFFu85AKCU\\\"\",\n      \"id\": \"GCQ3JlYXRvciBvbiB0aGUgUmlzZQ\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Creator on the Rise\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/mbUaBJAZzQQXrUy_F02c8idvkek\\\"\",\n      \"id\": \"GCTXVzaWM\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Music\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/dhUpBnIOaCoCDniv0ZybQIGWa8k\\\"\",\n      \"id\": \"GCQ29tZWR5\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Comedy\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/nPXKSW3gvbE_IGO9kQ0pVWWIX3E\\\"\",\n      \"id\": \"GCRmlsbSAmIEVudGVydGFpbm1lbnQ\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Film & Entertainment\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/81fHLyWLDYMN1EQqlz89HXIdMs4\\\"\",\n      \"id\": \"GCR2FtaW5n\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Gaming\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/H1ihcHpjpFnqgfFZnS7Y95-ELJE\\\"\",\n      \"id\": \"GCQmVhdXR5ICYgRmFzaGlvbg\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Beauty & Fashion\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Pv5088ySWfRzEy9SLDhygXIXhag\\\"\",\n      \"id\": \"GCU3BvcnRz\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Sports\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/gRB8nQk5gRYsCtZYQAPUufs9ElM\\\"\",\n      \"id\": \"GCVGVjaA\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Tech\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/NECk71Pm-g9olSeb8f65ze2ElOc\\\"\",\n      \"id\": \"GCQ29va2luZyAmIEhlYWx0aA\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Cooking & Health\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/AVwXb7tzi2-JuEetcsorNLk6tZg\\\"\",\n      \"id\": \"GCTmV3cyAmIFBvbGl0aWNz\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"News & Politics\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/categories/guide_category_multi.json",
    "content": "{\n  \"kind\": \"youtube#guideCategoryListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/JvKaPPmX316HuCUpJddmxaDPomo\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\\\"\",\n      \"id\": \"GCQmVzdCBvZiBZb3VUdWJl\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Best of YouTube\"\n      }\n    },\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ImrTevQ0UvryyOmvjPFFu85AKCU\\\"\",\n      \"id\": \"GCQ3JlYXRvciBvbiB0aGUgUmlzZQ\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Creator on the Rise\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/categories/guide_category_single.json",
    "content": "{\n  \"kind\": \"youtube#guideCategoryListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/KIJAFi2jsRHVBmAk3XYhyRKynjw\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\\\"\",\n      \"id\": \"GCQmVzdCBvZiBZb3VUdWJl\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Best of YouTube\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/categories/video_category_by_region.json",
    "content": "{\n  \"kind\": \"youtube#videoCategoryListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/S730Ilt-Fi-emsQJvJAAShlR6hM\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Xy1mB4_yLrHy_BmKmPBggty2mZQ\\\"\",\n      \"id\": \"1\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Film & Animation\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/UZ1oLIIz2dxIhO45ZTFR3a3NyTA\\\"\",\n      \"id\": \"2\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Autos & Vehicles\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/nqRIq97-xe5XRZTxbknKFVe5Lmg\\\"\",\n      \"id\": \"10\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Music\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/HwXKamM1Q20q9BN-oBJavSGkfDI\\\"\",\n      \"id\": \"15\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Pets & Animals\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\\\"\",\n      \"id\": \"17\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Sports\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/FJwVpGCVZ1yiJrqZbpqe68Sy_OE\\\"\",\n      \"id\": \"18\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Short Movies\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/M-3iD9dwK7YJCafRf_DkLN8CouA\\\"\",\n      \"id\": \"19\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Travel & Events\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/WmA0qYEfjWsAoyJFSw2zinhn2wM\\\"\",\n      \"id\": \"20\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Gaming\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/EapFaGYG7K0StIXVf8aba249tdM\\\"\",\n      \"id\": \"21\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Videoblogging\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/xId8RX7vRN8rqkbYZbNIytUQDRo\\\"\",\n      \"id\": \"22\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"People & Blogs\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/G9LHzQmx44rX2S5yaga_Aqtwz8M\\\"\",\n      \"id\": \"23\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Comedy\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/UVB9oxX2Bvqa_w_y3vXSLVK5E_s\\\"\",\n      \"id\": \"24\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Entertainment\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/QiLK0ZIrFoORdk_g2l_XR_ECjDc\\\"\",\n      \"id\": \"25\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"News & Politics\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/r6Ck6Z0_L0rG37VJQR200SGNA_w\\\"\",\n      \"id\": \"26\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Howto & Style\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/EoYkczo9I3RCf96RveKTOgOPkUM\\\"\",\n      \"id\": \"27\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Education\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/w5HjcTD82G_XA3xBctS30zS-JpQ\\\"\",\n      \"id\": \"28\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Science & Technology\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/SalkJoBWq_smSEqiAx_qyri6Wa8\\\"\",\n      \"id\": \"29\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Nonprofits & Activism\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/lL7uWDr_071CHxifjYG1tJrp4Uo\\\"\",\n      \"id\": \"30\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Movies\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/WnuVfjO-PyFLO7NTRQIbrGE62nk\\\"\",\n      \"id\": \"31\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Anime/Animation\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ctpH2hGA_UZ3volJT_FTlOg9M00\\\"\",\n      \"id\": \"32\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Action/Adventure\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/L0kR3-g1BAo5UD1PLVbQ7LkkDtQ\\\"\",\n      \"id\": \"33\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Classics\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/pUZOAC_s9sfiwar639qr_wAB-aI\\\"\",\n      \"id\": \"34\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Comedy\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Xb5JLhtyNRN3AQq021Ds-OV50Jk\\\"\",\n      \"id\": \"35\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Documentary\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/u8WXzF4HIhtEi805__sqjuA4lEk\\\"\",\n      \"id\": \"36\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Drama\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/D04PP4Gr7wc4IV_O9G66Z4A8KWQ\\\"\",\n      \"id\": \"37\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Family\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/i5-_AceGXQCEEMWU0V8CcQm_vLQ\\\"\",\n      \"id\": \"38\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Foreign\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/rtlxd0zOixA9QHdIZB26-St5qgQ\\\"\",\n      \"id\": \"39\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Horror\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/N1TrDFLRppxZgBowCJfJCvh0Dpg\\\"\",\n      \"id\": \"40\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Sci-Fi/Fantasy\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/7UMGi6zRySqXopr_rv4sZq6Za2E\\\"\",\n      \"id\": \"41\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Thriller\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/RScXhi324h8usyIetreAVb-uKeM\\\"\",\n      \"id\": \"42\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Shorts\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/0n9MJVCDLpA8q7aiGVrFsuFsd0A\\\"\",\n      \"id\": \"43\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Shows\",\n        \"assignable\": false\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/x5NxSf5fz8hn4loSN4rvhwzD_pY\\\"\",\n      \"id\": \"44\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Trailers\",\n        \"assignable\": false\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/categories/video_category_multi.json",
    "content": "{\n  \"kind\": \"youtube#videoCategoryListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/QhsRsql8vvkcmFdomppeHDbsV0Q\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\\\"\",\n      \"id\": \"17\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Sports\",\n        \"assignable\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/FJwVpGCVZ1yiJrqZbpqe68Sy_OE\\\"\",\n      \"id\": \"18\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Short Movies\",\n        \"assignable\": false\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/categories/video_category_single.json",
    "content": "{\n  \"kind\": \"youtube#videoCategoryListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/0_wT9Ta0iZu7ETYC3E6Xi_B4mtA\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\\\"\",\n      \"id\": \"17\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Sports\",\n        \"assignable\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/channel_banners/insert_response.json",
    "content": "{\n  \"kind\": \"youtube#channelBannerResource\",\n  \"etag\": \"ezPZq6gkoCbM-5C4P-ved0Irol0\",\n  \"url\": \"https://yt3.googleusercontent.com/1mrHHBsTG4JhGAQg_dmFf3ByELNVnXu7qCvmuhC81TFemB8XpaDgYuMgh5w220bh4APAj-xDeA\"\n}"
  },
  {
    "path": "testdata/apidata/channel_info_multi.json",
    "content": "{\n  \"kind\": \"youtube#channelListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/GaZ2FEuxLAqXsTHh13eEnkvWngM\\\"\",\n  \"prevPageToken\": \"CAUQAQ\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 2\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#channel\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/MegdiM4iUe5XzO4555ucXsyo7aQ\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n      \"snippet\": {\n        \"title\": \"Google Developers\",\n        \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\",\n        \"customUrl\": \"googlecode\",\n        \"publishedAt\": \"2007-08-23T00:34:43.000Z\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 88,\n            \"height\": 88\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 240,\n            \"height\": 240\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 800,\n            \"height\": 800\n          }\n        },\n        \"localized\": {\n          \"title\": \"Google Developers\",\n          \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\"\n        },\n        \"country\": \"US\"\n      }\n    },\n    {\n      \"kind\": \"youtube#channel\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/YDtNMQVe1s66E1iSBpnrlXzvJgE\\\"\",\n      \"id\": \"UCK8sQmJBp8GCxrOtXWBpyEA\",\n      \"snippet\": {\n        \"title\": \"Google\",\n        \"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.\",\n        \"customUrl\": \"google\",\n        \"publishedAt\": \"2005-09-18T22:37:10.000Z\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s88-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 88,\n            \"height\": 88\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s240-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 240,\n            \"height\": 240\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s800-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 800,\n            \"height\": 800\n          }\n        },\n        \"localized\": {\n          \"title\": \"Google\",\n          \"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.\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/channel_info_single.json",
    "content": "{\n  \"kind\": \"youtube#channelListResponse\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/0lqbdkIcLGXAPiLsJ3FTHo96TDg\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 1,\n    \"resultsPerPage\": 1\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#channel\",\n      \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/HUbWoTqNN1LPZKmbyCzPgvjVuR4\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n      \"snippet\": {\n        \"title\": \"Google Developers\",\n        \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\",\n        \"customUrl\": \"@googledevelopers\",\n        \"publishedAt\": \"2007-08-23T00:34:43.000Z\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 88,\n            \"height\": 88\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 240,\n            \"height\": 240\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 800,\n            \"height\": 800\n          }\n        },\n        \"localized\": {\n          \"title\": \"Google Developers\",\n          \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\"\n        },\n        \"country\": \"US\"\n      },\n      \"statistics\": {\n        \"viewCount\": \"160361638\",\n        \"commentCount\": \"0\",\n        \"subscriberCount\": \"1927873\",\n        \"hiddenSubscriberCount\": false,\n        \"videoCount\": \"5026\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/channel_sections/channel_sections_by_channel.json",
    "content": "{\n  \"kind\": \"youtube#channelSectionListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/IG4AAhdP913_ibNr3xxa2XjZhAU\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/JNSONRhMV8b1OaalB42ZUtVBZ44\\\"\",\n      \"id\": \"UCa-vrCLQHviTOVnEKDOdetQ.jNQXAC9IVRw\",\n      \"snippet\": {\n        \"type\": \"recentUploads\",\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"position\": 0\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/bcTK2_pxKS22pZizMGDGgnCcdeQ\\\"\",\n      \"id\": \"UCa-vrCLQHviTOVnEKDOdetQ.LeAltgu_pbM\",\n      \"snippet\": {\n        \"type\": \"allPlaylists\",\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"position\": 1\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/lkkZaRpGqH1OLyeS4UMzEQkz5IU\\\"\",\n      \"id\": \"UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY\",\n      \"snippet\": {\n        \"type\": \"multiplePlaylists\",\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"title\": \"我的操作诶\",\n        \"position\": 2\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS\",\n          \"PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g\"\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/channel_sections/channel_sections_by_id.json",
    "content": "{\n  \"kind\": \"youtube#channelSectionListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Oysqp4SfBtVFI8-0LVzUEHn8LN4\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Z3z8l_2oWLi9cWlfGTNMxsVwOTw\\\"\",\n      \"id\": \"UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY\",\n      \"snippet\": {\n        \"type\": \"multiplePlaylists\",\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"title\": \"我的操作诶\",\n        \"position\": 2\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS\",\n          \"PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g\"\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/channel_sections/channel_sections_by_ids.json",
    "content": "{\n  \"kind\": \"youtube#channelSectionListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Nvmls-WhS6tunMyp9v6ZIEFrgRI\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/RSxEQQPXGQo3MTN75toyRTUTEmY\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.npYvuMz0_es\",\n      \"snippet\": {\n        \"type\": \"recentUploads\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 9\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/zaHbYWO-Q1zjW4IYjza-bTrqeIc\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.9_wU0qhEPR8\",\n      \"snippet\": {\n        \"type\": \"singlePlaylist\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 8\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLOU2XLYxmsIKKMtrYD-IfPdlVunyPl9GM\"\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/channel_sections/insert_resp.json",
    "content": "{\n  \"kind\": \"youtube#channelSection\",\n  \"etag\": \"VNVb0NhdJ8VHoZaVCqGVqfaRrVU\",\n  \"id\": \"UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM\",\n  \"snippet\": {\n    \"type\": \"multipleplaylists\",\n    \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n    \"position\": 4\n  },\n  \"contentDetails\": {\n    \"playlists\": [\n      \"PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g\"\n    ]\n  }\n}"
  },
  {
    "path": "testdata/apidata/channels/info.json",
    "content": "{\"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\"}}]}"
  },
  {
    "path": "testdata/apidata/channels/info_multiple.json",
    "content": "{\"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\"}}}]}"
  },
  {
    "path": "testdata/apidata/channels/update_resp.json",
    "content": "{\"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\"}}}"
  },
  {
    "path": "testdata/apidata/client_secrets/client_secret_installed_bad.json",
    "content": "{\n    \"installed\": {\n        \"client_id\": \"client_id\",\n        \"project_id\": \"project_id\",\n        \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n        \"token_uri\": \"https://oauth2.googleapis.com/token\",\n        \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\"\n    }\n}"
  },
  {
    "path": "testdata/apidata/client_secrets/client_secret_installed_good.json",
    "content": "{\n    \"installed\": {\n        \"client_id\": \"client_id\",\n        \"project_id\": \"project_id\",\n        \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n        \"token_uri\": \"https://oauth2.googleapis.com/token\",\n        \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n        \"client_secret\": \"client_secret\"\n    }\n}"
  },
  {
    "path": "testdata/apidata/client_secrets/client_secret_unsupported.json",
    "content": "{\n    \"unsupported\": {\n        \"client_id\": \"client_id\",\n        \"project_id\": \"project_id\",\n        \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n        \"token_uri\": \"https://oauth2.googleapis.com/token\",\n        \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\"\n    }\n}"
  },
  {
    "path": "testdata/apidata/client_secrets/client_secret_web.json",
    "content": "{\n    \"web\": {\n        \"client_id\": \"client_id\",\n        \"project_id\": \"project_id\",\n        \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n        \"token_uri\": \"https://oauth2.googleapis.com/token\",\n        \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n        \"client_secret\": \"client_secret\",\n        \"redirect_uris\": [\n            \"http://localhost:5000/oauth2callback\"\n        ]\n    }\n}"
  },
  {
    "path": "testdata/apidata/comment_threads/comment_thread_single.json",
    "content": "{\n  \"kind\": \"youtube#commentThreadListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/VQfUfBFenzO3S8AzxaX0A2cOK_w\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 1,\n    \"resultsPerPage\": 20\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Bov8ITX91R0QmVGZN70wbJ5_hOs\\\"\",\n      \"id\": \"UgxKREWxIgDrw8w2e_Z4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"D-lhorsDlUQ\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/W05nAf1QR8i2AYeULDR019ku3Lg\\\"\",\n          \"id\": \"UgxKREWxIgDrw8w2e_Z4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"Hieu Nguyen\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7-oQeqHcOEyt0l2rBBZH1qAiBNKNn1UmmGk5Q=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UClfzT4CU_yaZjJaI4pKqSjQ\",\n            \"authorChannelId\": {\n              \"value\": \"UClfzT4CU_yaZjJaI4pKqSjQ\"\n            },\n            \"videoId\": \"D-lhorsDlUQ\",\n            \"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 ...\",\n            \"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 ...\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-04-20T01:03:39.000Z\",\n            \"updatedAt\": \"2019-04-20T01:03:39.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comment_threads/comment_threads_all_to_me.json",
    "content": "{\n  \"kind\": \"youtube#commentThreadListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/9f4Mz6CLxsfPgItKiTYYywpU5pY\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 4,\n    \"resultsPerPage\": 20\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/W4DPat_BSfoIcTpeSjXLpOq8mRw\\\"\",\n      \"id\": \"UgyWeTdgc4sc1xgmbld4AaABAg\",\n      \"snippet\": {\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"videoId\": \"JE8xdDp5B8Q\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/dQGpRBbLeR7VSsV4-nNtA7qKCcI\\\"\",\n          \"id\": \"UgyWeTdgc4sc1xgmbld4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"kun liu\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw\",\n            \"authorChannelId\": {\n              \"value\": \"UCNvMBmCASzTNNX8lW3JRMbw\"\n            },\n            \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n            \"videoId\": \"JE8xdDp5B8Q\",\n            \"textDisplay\": \"Hope to go next time. yah!\",\n            \"textOriginal\": \"Hope to go next time. yah!\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-12-01T03:36:55.000Z\",\n            \"updatedAt\": \"2019-12-01T03:36:55.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Lu2JczYrJaDkKhajTXWW8SIdxJQ\\\"\",\n      \"id\": \"Ugw5zYU6n9pmIgAZWvN4AaABAg\",\n      \"snippet\": {\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"videoId\": \"JE8xdDp5B8Q\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/dlWLs_1koF5w5gat2bc4UAvWn_U\\\"\",\n          \"id\": \"Ugw5zYU6n9pmIgAZWvN4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"kun liu\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw\",\n            \"authorChannelId\": {\n              \"value\": \"UCNvMBmCASzTNNX8lW3JRMbw\"\n            },\n            \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n            \"videoId\": \"JE8xdDp5B8Q\",\n            \"textDisplay\": \"This is so beautiful !\",\n            \"textOriginal\": \"This is so beautiful !\",\n            \"canRate\": true,\n            \"viewerRating\": \"like\",\n            \"likeCount\": 1,\n            \"publishedAt\": \"2019-12-01T03:36:10.000Z\",\n            \"updatedAt\": \"2019-12-01T03:36:10.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/s8Hj1OPYiSO7BiFeS0Pc66OAw8E\\\"\",\n      \"id\": \"Ugy0FhSzKMNMT9qs6zt4AaABAg\",\n      \"snippet\": {\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/FA35BA_7xSgoiY5DpoybVoEFtEc\\\"\",\n          \"id\": \"Ugy0FhSzKMNMT9qs6zt4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"杨佳名\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l78ya-P2zYdCbefXtEDb1eMRGncYTsgAgBS9xQ=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCupGECpPBlTnanhGHLe6GpA\",\n            \"authorChannelId\": {\n              \"value\": \"UCupGECpPBlTnanhGHLe6GpA\"\n            },\n            \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n            \"textDisplay\": \"大满贯\",\n            \"textOriginal\": \"大满贯\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-11-29T02:58:25.000Z\",\n            \"updatedAt\": \"2019-11-29T02:58:25.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/qkxnREu6fFm4Z803KhpgDZQJEeU\\\"\",\n      \"id\": \"UgwSRpgkjCe6OHHe3IJ4AaABAg\",\n      \"snippet\": {\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/0zRJJZKVzaO9-IfxGda3Sbke03o\\\"\",\n          \"id\": \"UgwSRpgkjCe6OHHe3IJ4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"杨佳名\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l78ya-P2zYdCbefXtEDb1eMRGncYTsgAgBS9xQ=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCupGECpPBlTnanhGHLe6GpA\",\n            \"authorChannelId\": {\n              \"value\": \"UCupGECpPBlTnanhGHLe6GpA\"\n            },\n            \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n            \"textDisplay\": \"中午吃啥\",\n            \"textOriginal\": \"中午吃啥\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-11-29T02:57:53.000Z\",\n            \"updatedAt\": \"2019-11-29T02:57:53.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 1,\n        \"isPublic\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comment_threads/comment_threads_by_channel.json",
    "content": "{\n  \"kind\": \"youtube#commentThreadListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Wm2vP6ZY_91XACR5c9Vk8QFfpWY\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 20\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/o0Du7va0vUQliAR76OFrtcgOjOc\\\"\",\n      \"id\": \"UgyUBI0HsgL9emxcZpR4AaABAg\",\n      \"snippet\": {\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/CD_Xk4X_gxANaxestqfTSanwWrk\\\"\",\n          \"id\": \"UgyUBI0HsgL9emxcZpR4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"DevRagz\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw\",\n            \"authorChannelId\": {\n              \"value\": \"UCTWlvHQQXUs-4IVfdnGOUbw\"\n            },\n            \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n            \"textDisplay\": \"Hello\",\n            \"textOriginal\": \"Hello\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-06-23T02:49:00.000Z\",\n            \"updatedAt\": \"2019-06-23T02:49:00.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/yHddGp8i_wDOcek2w4RkfBpgS7s\\\"\",\n      \"id\": \"Ugzi3lkqDPfIOirGFLh4AaABAg\",\n      \"snippet\": {\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/jYGGL-8Rw6c0xU3CT-WWU1uHZyw\\\"\",\n          \"id\": \"Ugzi3lkqDPfIOirGFLh4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"EclipZe Muzik\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7-JV7pt6X2WsxOdaD6nzK_rAj_2FVAjLyNR1Q=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCCuQ3wts9ASkk0qZJyEjYbw\",\n            \"authorChannelId\": {\n              \"value\": \"UCCuQ3wts9ASkk0qZJyEjYbw\"\n            },\n            \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n            \"textDisplay\": \"exceptional content!\",\n            \"textOriginal\": \"exceptional content!\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2018-05-26T20:11:30.000Z\",\n            \"updatedAt\": \"2018-05-26T20:11:30.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comment_threads/comment_threads_by_video_paged_1.json",
    "content": "{\n  \"kind\": \"youtube#commentThreadListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/mj64hlcyhnGGcJWAccdWisvf8MY\\\"\",\n  \"nextPageToken\": \"QURTSl9pMzdZOUVzMkI0czlmRmNjSVBPcTBTdzVzajUydDVnbE5SNElWS0l5WU12amYweVotdzF5c1hTNmxzUmVIcEZXbmVEVFMzNVJmWk82TVVwUlB2LWh5aUpOQlA5TGQzTWZEcHlTeTd2dlNGRUFZaVF0cmtJd01BTHlnOG0=\",\n  \"pageInfo\": {\n    \"totalResults\": 5,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/-kLX-6-yrqngKqYMS46dOtyqEUE\\\"\",\n      \"id\": \"UgyZ1jqkHKYvi1-ruOZ4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"F1UP7wRCPH8\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/qVtEMtZJzm2E2_ThbD7t3KLAsWc\\\"\",\n          \"id\": \"UgyZ1jqkHKYvi1-ruOZ4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"himani agarwal\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l78sAaVkRzpOeLtaisG-MYI3gOvnvNL9iXZo3g=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCzj-CjKYmGlZ7vTL-vOe57A\",\n            \"authorChannelId\": {\n              \"value\": \"UCzj-CjKYmGlZ7vTL-vOe57A\"\n            },\n            \"videoId\": \"F1UP7wRCPH8\",\n            \"textDisplay\": \"\\u003ca href=\\\"http://google.com/\\\"\\u003eGoogle.com\\u003c/a\\u003e\",\n            \"textOriginal\": \"Google.com\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-11-26T15:51:44.000Z\",\n            \"updatedAt\": \"2019-11-26T15:51:44.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Iw6Eo0WB_HfKsyEl2jXWyOCWyKA\\\"\",\n      \"id\": \"Ugy4OzAuz5uJuFt3FH54AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"F1UP7wRCPH8\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/zqJ_eR8wdFdwY96FNFPKAVgyBKM\\\"\",\n          \"id\": \"Ugy4OzAuz5uJuFt3FH54AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"el bojo loco\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l79o1pzBIfl2qWbKEyhg__UhsQZ4V9Dsg-lRSg=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCGBUkig1Ala-Z2gF73WSTdw\",\n            \"authorChannelId\": {\n              \"value\": \"UCGBUkig1Ala-Z2gF73WSTdw\"\n            },\n            \"videoId\": \"F1UP7wRCPH8\",\n            \"textDisplay\": \"Please use CLS on your own search results page..\",\n            \"textOriginal\": \"Please use CLS on your own search results page..\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 1,\n            \"publishedAt\": \"2019-11-21T19:18:39.000Z\",\n            \"updatedAt\": \"2019-11-21T19:18:39.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/vBjDNfPUz4zXiio50mRS5MC19bg\\\"\",\n      \"id\": \"UgysQP-vp089eFP0Stl4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"F1UP7wRCPH8\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ebOBApCraUZPu-TA4vuCJbwNjz4\\\"\",\n          \"id\": \"UgysQP-vp089eFP0Stl4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"Andres Jorquera\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7_71xCslORfWq9-j9dIPbw1Sa3M0hw_G7gCNA=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCzZ87J8wYVpOADFviqyFKhw\",\n            \"authorChannelId\": {\n              \"value\": \"UCzZ87J8wYVpOADFviqyFKhw\"\n            },\n            \"videoId\": \"F1UP7wRCPH8\",\n            \"textDisplay\": \"Funny that they use heroku to show a demo instead of google cloud\",\n            \"textOriginal\": \"Funny that they use heroku to show a demo instead of google cloud\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 4,\n            \"publishedAt\": \"2019-11-17T11:34:48.000Z\",\n            \"updatedAt\": \"2019-11-17T11:34:48.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/TqNnjkgG07M6i2ikeu8X4rDTxZ0\\\"\",\n      \"id\": \"UgyKv8ziPYFJDGytM_J4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"F1UP7wRCPH8\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/I5hbgLodwC0nmD3iOcByof5O7rk\\\"\",\n          \"id\": \"UgyKv8ziPYFJDGytM_J4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"Masum Khan\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l79ngmGDoFG7XuL9cAzTmh6U8moqME8OHdh7pQ=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCHJQ8PHPGprvhKdLVHLwRPA\",\n            \"authorChannelId\": {\n              \"value\": \"UCHJQ8PHPGprvhKdLVHLwRPA\"\n            },\n            \"videoId\": \"F1UP7wRCPH8\",\n            \"textDisplay\": \"All account on Android store for data show the Google and YouTube video Android computer system fordata show\\u003cbr /\\u003e        settings displayed\",\n            \"textOriginal\": \"All account on Android store for data show the Google and YouTube video Android computer system fordata show\\n        settings displayed\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 1,\n            \"publishedAt\": \"2019-11-16T20:05:39.000Z\",\n            \"updatedAt\": \"2019-11-16T20:05:39.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/5gYLe0DJitJqoq12oqE8gIeLiUw\\\"\",\n      \"id\": \"UgwI_ylXVnzPS8Q-oAV4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"F1UP7wRCPH8\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/RETGsc5ySPyJ5IIAKVndyEC4R10\\\"\",\n          \"id\": \"UgwI_ylXVnzPS8Q-oAV4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"matrix uduma\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7_xsrGS2s0whxHHHAMbfAsHerh3HxH1VHZ3fg=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCACHW4qVHoE4zqhHxcVE3Kw\",\n            \"authorChannelId\": {\n              \"value\": \"UCACHW4qVHoE4zqhHxcVE3Kw\"\n            },\n            \"videoId\": \"F1UP7wRCPH8\",\n            \"textDisplay\": \"Lot&#39;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.\",\n            \"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.\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-11-14T23:31:33.000Z\",\n            \"updatedAt\": \"2019-11-14T23:31:33.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 2,\n        \"isPublic\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comment_threads/comment_threads_by_video_paged_2.json",
    "content": "{\n  \"kind\": \"youtube#commentThreadListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/mj64hlcyhnGGcJWAccdWisvf8MY\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 5,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/-kLX-6-yrqngKqYMS46dOtyqEUE\\\"\",\n      \"id\": \"UgyZ1jqkHKYvi1-ruOZ4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"F1UP7wRCPH8\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/qVtEMtZJzm2E2_ThbD7t3KLAsWc\\\"\",\n          \"id\": \"UgyZ1jqkHKYvi1-ruOZ4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"himani agarwal\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l78sAaVkRzpOeLtaisG-MYI3gOvnvNL9iXZo3g=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCzj-CjKYmGlZ7vTL-vOe57A\",\n            \"authorChannelId\": {\n              \"value\": \"UCzj-CjKYmGlZ7vTL-vOe57A\"\n            },\n            \"videoId\": \"F1UP7wRCPH8\",\n            \"textDisplay\": \"\\u003ca href=\\\"http://google.com/\\\"\\u003eGoogle.com\\u003c/a\\u003e\",\n            \"textOriginal\": \"Google.com\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-11-26T15:51:44.000Z\",\n            \"updatedAt\": \"2019-11-26T15:51:44.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Iw6Eo0WB_HfKsyEl2jXWyOCWyKA\\\"\",\n      \"id\": \"Ugy4OzAuz5uJuFt3FH54AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"F1UP7wRCPH8\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/zqJ_eR8wdFdwY96FNFPKAVgyBKM\\\"\",\n          \"id\": \"Ugy4OzAuz5uJuFt3FH54AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"el bojo loco\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l79o1pzBIfl2qWbKEyhg__UhsQZ4V9Dsg-lRSg=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCGBUkig1Ala-Z2gF73WSTdw\",\n            \"authorChannelId\": {\n              \"value\": \"UCGBUkig1Ala-Z2gF73WSTdw\"\n            },\n            \"videoId\": \"F1UP7wRCPH8\",\n            \"textDisplay\": \"Please use CLS on your own search results page..\",\n            \"textOriginal\": \"Please use CLS on your own search results page..\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 1,\n            \"publishedAt\": \"2019-11-21T19:18:39.000Z\",\n            \"updatedAt\": \"2019-11-21T19:18:39.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/vBjDNfPUz4zXiio50mRS5MC19bg\\\"\",\n      \"id\": \"UgysQP-vp089eFP0Stl4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"F1UP7wRCPH8\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ebOBApCraUZPu-TA4vuCJbwNjz4\\\"\",\n          \"id\": \"UgysQP-vp089eFP0Stl4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"Andres Jorquera\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7_71xCslORfWq9-j9dIPbw1Sa3M0hw_G7gCNA=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCzZ87J8wYVpOADFviqyFKhw\",\n            \"authorChannelId\": {\n              \"value\": \"UCzZ87J8wYVpOADFviqyFKhw\"\n            },\n            \"videoId\": \"F1UP7wRCPH8\",\n            \"textDisplay\": \"Funny that they use heroku to show a demo instead of google cloud\",\n            \"textOriginal\": \"Funny that they use heroku to show a demo instead of google cloud\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 4,\n            \"publishedAt\": \"2019-11-17T11:34:48.000Z\",\n            \"updatedAt\": \"2019-11-17T11:34:48.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/TqNnjkgG07M6i2ikeu8X4rDTxZ0\\\"\",\n      \"id\": \"UgyKv8ziPYFJDGytM_J4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"F1UP7wRCPH8\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/I5hbgLodwC0nmD3iOcByof5O7rk\\\"\",\n          \"id\": \"UgyKv8ziPYFJDGytM_J4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"Masum Khan\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l79ngmGDoFG7XuL9cAzTmh6U8moqME8OHdh7pQ=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCHJQ8PHPGprvhKdLVHLwRPA\",\n            \"authorChannelId\": {\n              \"value\": \"UCHJQ8PHPGprvhKdLVHLwRPA\"\n            },\n            \"videoId\": \"F1UP7wRCPH8\",\n            \"textDisplay\": \"All account on Android store for data show the Google and YouTube video Android computer system fordata show\\u003cbr /\\u003e        settings displayed\",\n            \"textOriginal\": \"All account on Android store for data show the Google and YouTube video Android computer system fordata show\\n        settings displayed\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 1,\n            \"publishedAt\": \"2019-11-16T20:05:39.000Z\",\n            \"updatedAt\": \"2019-11-16T20:05:39.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/5gYLe0DJitJqoq12oqE8gIeLiUw\\\"\",\n      \"id\": \"UgwI_ylXVnzPS8Q-oAV4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"F1UP7wRCPH8\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/RETGsc5ySPyJ5IIAKVndyEC4R10\\\"\",\n          \"id\": \"UgwI_ylXVnzPS8Q-oAV4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"matrix uduma\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7_xsrGS2s0whxHHHAMbfAsHerh3HxH1VHZ3fg=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCACHW4qVHoE4zqhHxcVE3Kw\",\n            \"authorChannelId\": {\n              \"value\": \"UCACHW4qVHoE4zqhHxcVE3Kw\"\n            },\n            \"videoId\": \"F1UP7wRCPH8\",\n            \"textDisplay\": \"Lot&#39;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.\",\n            \"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.\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-11-14T23:31:33.000Z\",\n            \"updatedAt\": \"2019-11-14T23:31:33.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 2,\n        \"isPublic\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comment_threads/comment_threads_multi.json",
    "content": "{\n  \"kind\": \"youtube#commentThreadListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/LKHkXHQZoSC-BXdpnHc829mN3QM\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 20\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/hSvCiIVcQzGAvouQ8s14qvX4vak\\\"\",\n      \"id\": \"UgxKREWxIgDrw8w2e_Z4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"D-lhorsDlUQ\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/fcXJ4Y0FWzHufPruLqAlCMnzQ4A\\\"\",\n          \"id\": \"UgxKREWxIgDrw8w2e_Z4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"Hieu Nguyen\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7-oQeqHcOEyt0l2rBBZH1qAiBNKNn1UmmGk5Q=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UClfzT4CU_yaZjJaI4pKqSjQ\",\n            \"authorChannelId\": {\n              \"value\": \"UClfzT4CU_yaZjJaI4pKqSjQ\"\n            },\n            \"videoId\": \"D-lhorsDlUQ\",\n            \"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 ...\",\n            \"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 ...\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-04-20T01:03:39.000Z\",\n            \"updatedAt\": \"2019-04-20T01:03:39.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/hK5O5Tf4dzAUW8LQFP4yX6Wmsd8\\\"\",\n      \"id\": \"UgyrVQaFfEdvaSzstj14AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"D-lhorsDlUQ\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/bVR2Q3iGDhKQCFKVdH7yK0p4ogI\\\"\",\n          \"id\": \"UgyrVQaFfEdvaSzstj14AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"Mani Kanta\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7_EFNV6q-NNu9BGJ58f-2Da9utFG8ISB8uKYg=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCJBxRADq6jctX-YdhjkB6PA\",\n            \"authorChannelId\": {\n              \"value\": \"UCJBxRADq6jctX-YdhjkB6PA\"\n            },\n            \"videoId\": \"D-lhorsDlUQ\",\n            \"textDisplay\": \"super\",\n            \"textOriginal\": \"super\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-04-04T04:14:44.000Z\",\n            \"updatedAt\": \"2019-04-04T04:14:44.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comment_threads/comment_threads_with_search.json",
    "content": "{\n  \"kind\": \"youtube#commentThreadListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Sb1s9ORYlw5LpXhXM1osg1jeYgM\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 1,\n    \"resultsPerPage\": 20\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/o0Du7va0vUQliAR76OFrtcgOjOc\\\"\",\n      \"id\": \"UgyUBI0HsgL9emxcZpR4AaABAg\",\n      \"snippet\": {\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/CD_Xk4X_gxANaxestqfTSanwWrk\\\"\",\n          \"id\": \"UgyUBI0HsgL9emxcZpR4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"DevRagz\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw\",\n            \"authorChannelId\": {\n              \"value\": \"UCTWlvHQQXUs-4IVfdnGOUbw\"\n            },\n            \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n            \"textDisplay\": \"Hello\",\n            \"textOriginal\": \"Hello\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-06-23T02:49:00.000Z\",\n            \"updatedAt\": \"2019-06-23T02:49:00.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comment_threads/insert_response.json",
    "content": "{\n  \"kind\": \"youtube#commentThread\",\n  \"etag\": \"AMgl2io48I4z6Ulu9kv4C43sVvk\",\n  \"id\": \"Ugx_5P8rmn4vKbN6wwt4AaABAg\",\n  \"snippet\": {\n    \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n    \"videoId\": \"JE8xdDp5B8Q\",\n    \"topLevelComment\": {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"I_E2on6NOdGkpW0WodB74OVCU_E\",\n      \"id\": \"Ugx_5P8rmn4vKbN6wwt4AaABAg\",\n      \"snippet\": {\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"videoId\": \"JE8xdDp5B8Q\",\n        \"textDisplay\": \"Sun from the api\",\n        \"textOriginal\": \"Sun from the api\",\n        \"authorDisplayName\": \"ikaros data\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s48-c-k-c0x00ffffff-no-rj\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"authorChannelId\": {\n          \"value\": \"UCa-vrCLQHviTOVnEKDOdetQ\"\n        },\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 0,\n        \"publishedAt\": \"2022-11-15T02:20:01Z\",\n        \"updatedAt\": \"2022-11-15T02:20:01Z\"\n      }\n    },\n    \"canReply\": true,\n    \"totalReplyCount\": 0,\n    \"isPublic\": true\n  }\n}\n"
  },
  {
    "path": "testdata/apidata/comments/comments_by_parent_paged_1.json",
    "content": "{\n  \"kind\": \"youtube#commentListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/WG_t4MaQHNzJKOhVkfr0prByYcE\\\"\",\n  \"nextPageToken\": \"R0FJeVZnbzBJTl9zNXRxNXlPWUNNaWtRQUJpQ3RNeW4wcFBtQWlBQktBTXdDam9XT1RGNlZETmpXV0kxUWpJNU1YcGhOV1ZLZUhwek1SSWVDQVVTR2xWbmR6VjZXVlUyYmpsd2JVbG5RVnBYZGs0MFFXRkJRa0ZuT2lBSUFSSWNOVHBWWjNjMWVsbFZObTQ1Y0cxSlowRmFWM1pPTkVGaFFVSkJadw==\",\n  \"pageInfo\": {\n    \"resultsPerPage\": 2\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/MKybcC4eKVsCy4dNSdJpsCB2f9I\\\"\",\n      \"id\": \"Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za6voUoRh\",\n      \"snippet\": {\n        \"authorDisplayName\": \"kun liu\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"authorChannelId\": {\n          \"value\": \"UCNvMBmCASzTNNX8lW3JRMbw\"\n        },\n        \"textDisplay\": \"this is the third reply!\",\n        \"textOriginal\": \"this is the third reply!\",\n        \"parentId\": \"Ugw5zYU6n9pmIgAZWvN4AaABAg\",\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 0,\n        \"publishedAt\": \"2019-12-01T04:46:31.000Z\",\n        \"updatedAt\": \"2019-12-01T04:46:31.000Z\"\n      }\n    },\n    {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Hnby-TPwSCvcPfoqxAwocl44Ijw\\\"\",\n      \"id\": \"Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za5eJxzs1\",\n      \"snippet\": {\n        \"authorDisplayName\": \"kun liu\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"authorChannelId\": {\n          \"value\": \"UCNvMBmCASzTNNX8lW3JRMbw\"\n        },\n        \"textDisplay\": \"this is the second reply!\",\n        \"textOriginal\": \"this is the second reply!\",\n        \"parentId\": \"Ugw5zYU6n9pmIgAZWvN4AaABAg\",\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 0,\n        \"publishedAt\": \"2019-12-01T04:46:20.000Z\",\n        \"updatedAt\": \"2019-12-01T04:46:20.000Z\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comments/comments_by_parent_paged_2.json",
    "content": "{\n  \"kind\": \"youtube#commentListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/pCAXgQBtRsyN8PLsTBsg6O6diuY\\\"\",\n  \"pageInfo\": {\n    \"resultsPerPage\": 2\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/0CMWJ6jK5tTSWIBuH6KQLYVM9xI\\\"\",\n      \"id\": \"Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za3E-eR-l\",\n      \"snippet\": {\n        \"authorDisplayName\": \"kun liu\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7845DW6v_u-F6i94KsqacXl7fYAiGl3roTghQ=s48-c-k-c0xffffffff-no-rj-mo\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"authorChannelId\": {\n          \"value\": \"UCNvMBmCASzTNNX8lW3JRMbw\"\n        },\n        \"textDisplay\": \"hey, this is the replay\",\n        \"textOriginal\": \"hey, this is the replay\",\n        \"parentId\": \"Ugw5zYU6n9pmIgAZWvN4AaABAg\",\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 0,\n        \"publishedAt\": \"2019-12-01T04:46:00.000Z\",\n        \"updatedAt\": \"2019-12-01T04:46:00.000Z\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comments/comments_multi.json",
    "content": "{\n  \"kind\": \"youtube#commentListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/KGqYDVMiabGgL8yNEbUJOwtrsqY\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/N8dTotfuHXvcw1YZ3SZNWPHYxvM\\\"\",\n      \"id\": \"UgyUBI0HsgL9emxcZpR4AaABAg\",\n      \"snippet\": {\n        \"authorDisplayName\": \"DevRagz\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw\",\n        \"authorChannelId\": {\n          \"value\": \"UCTWlvHQQXUs-4IVfdnGOUbw\"\n        },\n        \"textDisplay\": \"Hello\",\n        \"textOriginal\": \"Hello\",\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 0,\n        \"publishedAt\": \"2019-06-23T02:49:00.000Z\",\n        \"updatedAt\": \"2019-06-23T02:49:00.000Z\"\n      }\n    },\n    {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/U1u_QcNKFwxVznA5x7CNZ9nXECc\\\"\",\n      \"id\": \"Ugzi3lkqDPfIOirGFLh4AaABAg\",\n      \"snippet\": {\n        \"authorDisplayName\": \"EclipZe Muzik\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7-JV7pt6X2WsxOdaD6nzK_rAj_2FVAjLyNR1Q=s48-c-k-c0xffffffff-no-rj-mo\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UCCuQ3wts9ASkk0qZJyEjYbw\",\n        \"authorChannelId\": {\n          \"value\": \"UCCuQ3wts9ASkk0qZJyEjYbw\"\n        },\n        \"textDisplay\": \"exceptional content!\",\n        \"textOriginal\": \"exceptional content!\",\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 0,\n        \"publishedAt\": \"2018-05-26T20:11:30.000Z\",\n        \"updatedAt\": \"2018-05-26T20:11:30.000Z\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comments/comments_single.json",
    "content": "{\n  \"kind\": \"youtube#commentListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/A2nkVdtP_3bQZi4INK9lZ7XTYXs\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/N8dTotfuHXvcw1YZ3SZNWPHYxvM\\\"\",\n      \"id\": \"UgyUBI0HsgL9emxcZpR4AaABAg\",\n      \"snippet\": {\n        \"authorDisplayName\": \"DevRagz\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/a/AGF-l7_28iRYJ_LkQSV8Ed7Rvq_R7VSvdX3smbp3vw=s48-c-k-c0xffffffff-no-rj-mo\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UCTWlvHQQXUs-4IVfdnGOUbw\",\n        \"authorChannelId\": {\n          \"value\": \"UCTWlvHQQXUs-4IVfdnGOUbw\"\n        },\n        \"textDisplay\": \"Hello\",\n        \"textOriginal\": \"Hello\",\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 0,\n        \"publishedAt\": \"2019-06-23T02:49:00.000Z\",\n        \"updatedAt\": \"2019-06-23T02:49:00.000Z\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/comments/insert_response.json",
    "content": "{\n  \"kind\": \"youtube#comment\",\n  \"etag\": \"lTl2Wjqipb6KqrmPU04DLigrzrg\",\n  \"id\": \"Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt\",\n  \"snippet\": {\n    \"textDisplay\": \"wow\",\n    \"textOriginal\": \"wow\",\n    \"parentId\": \"Ugy_CAftKrIUCyPr9GR4AaABAg\",\n    \"authorDisplayName\": \"ikaros data\",\n    \"authorProfileImageUrl\": \"https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s48-c-k-c0x00ffffff-no-rj\",\n    \"authorChannelUrl\": \"http://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ\",\n    \"authorChannelId\": {\n      \"value\": \"UCa-vrCLQHviTOVnEKDOdetQ\"\n    },\n    \"canRate\": true,\n    \"viewerRating\": \"none\",\n    \"likeCount\": 0,\n    \"publishedAt\": \"2022-11-14T10:23:02Z\",\n    \"updatedAt\": \"2022-11-14T10:23:02Z\"\n  }\n}\n"
  },
  {
    "path": "testdata/apidata/error_permission_resp.json",
    "content": "{\n  \"error\": {\n    \"code\": 403,\n    \"message\": \"The caller does not have permission\",\n    \"errors\": [\n      {\n        \"message\": \"Permission denied.\",\n        \"domain\": \"youtube.CoreErrorDomain\",\n        \"reason\": \"SERVICE_UNAVAILABLE\"\n      }\n    ],\n    \"status\": \"PERMISSION_DENIED\"\n  }\n}"
  },
  {
    "path": "testdata/apidata/i18ns/language_res.json",
    "content": "{\n  \"kind\": \"youtube#i18nLanguageListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/qgFy24yvs-L_dNjr2d-Rd_Xcfw4\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#i18nLanguage\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/wD2SRT6G7gbFH07ePlumHAynSRo\\\"\",\n      \"id\": \"zh-CN\",\n      \"snippet\": {\n        \"hl\": \"zh-CN\",\n        \"name\": \"Chinese\"\n      }\n    },\n    {\n      \"kind\": \"youtube#i18nLanguage\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/6fRre896XUzNQjc89q329PaFKjE\\\"\",\n      \"id\": \"zh-TW\",\n      \"snippet\": {\n        \"hl\": \"zh-TW\",\n        \"name\": \"Chinese (Taiwan)\"\n      }\n    },\n    {\n      \"kind\": \"youtube#i18nLanguage\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/MbipoDEFiRRUlYr5UzjpCwXXRMc\\\"\",\n      \"id\": \"zh-HK\",\n      \"snippet\": {\n        \"hl\": \"zh-HK\",\n        \"name\": \"Chinese (Hong Kong)\"\n      }\n    },\n    {\n      \"kind\": \"youtube#i18nLanguage\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/XBmnabKsnR1WSWcZvNxC2NvrLYo\\\"\",\n      \"id\": \"ja\",\n      \"snippet\": {\n        \"hl\": \"ja\",\n        \"name\": \"Japanese\"\n      }\n    },\n    {\n      \"kind\": \"youtube#i18nLanguage\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/l06bH0pLscIVm87oyBnJ3aZR4Ts\\\"\",\n      \"id\": \"ko\",\n      \"snippet\": {\n        \"hl\": \"ko\",\n        \"name\": \"Korean\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/i18ns/regions_res.json",
    "content": "{\n  \"kind\": \"youtube#i18nRegionListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/q85_wZeDyKDzYtt-LhNaozyi_sk\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#i18nRegion\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/VLBm14P6cRurVqVIS2Z9SfyDJdU\\\"\",\n      \"id\": \"VE\",\n      \"snippet\": {\n        \"gl\": \"VE\",\n        \"name\": \"Venezuela\"\n      }\n    },\n    {\n      \"kind\": \"youtube#i18nRegion\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/PecEyDpgDPYWfvmuJVxuVgwxQwU\\\"\",\n      \"id\": \"VN\",\n      \"snippet\": {\n        \"gl\": \"VN\",\n        \"name\": \"Vietnam\"\n      }\n    },\n    {\n      \"kind\": \"youtube#i18nRegion\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Pzd0Bx5oG8rW9CkFMKHP0X12ywM\\\"\",\n      \"id\": \"YE\",\n      \"snippet\": {\n        \"gl\": \"YE\",\n        \"name\": \"Yemen\"\n      }\n    },\n    {\n      \"kind\": \"youtube#i18nRegion\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/u5oySmRytTPqfZBg8zIE7G7jvRs\\\"\",\n      \"id\": \"ZW\",\n      \"snippet\": {\n        \"gl\": \"ZW\",\n        \"name\": \"Zimbabwe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/members/members_data.json",
    "content": "{\n  \"kind\": \"youtube#memberListResponse\",\n  \"etag\": \"etag\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#member\",\n      \"etag\": \"etag\",\n      \"snippet\": {\n        \"creatorChannelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"memberDetails\": {\n          \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n          \"channelUrl\": \"https://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ\",\n          \"displayName\": \"ikaros-life\",\n          \"profileImageUrl\": \"https://yt3.ggpht.com/a-/AOh14Gg1_gYcI03VLDd3FMLUY5cb5O9RC9sElj26-1SR=s288-c-k-c0xffffffff-no-rj-mo\"\n        },\n        \"membershipsDetails\": {\n          \"highestAccessibleLevel\": \"string\",\n          \"highestAccessibleLevelDisplayName\": \"string\",\n          \"accessibleLevels\": [\n            \"string\"\n          ],\n          \"membershipsDuration\": {\n            \"memberSince\": \"2007-08-23T00:34:43Z\",\n            \"memberTotalDurationMonths\": 5\n          },\n          \"membershipsDurationAtLevel\": [\n            {\n              \"level\": \"string\",\n              \"memberSince\": \"2007-08-23T00:34:43Z\",\n              \"memberTotalDurationMonths\": 6\n            }\n          ]\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#member\",\n      \"etag\": \"etag\",\n      \"snippet\": {\n        \"creatorChannelId\": \"UCa-vrCLQHviTOVnEKDOdet1\",\n        \"memberDetails\": {\n          \"channelId\": \"UCa-vrCLQHviTOVnEKDOdet1\",\n          \"channelUrl\": \"https://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdet1\",\n          \"displayName\": \"ikaros-life--1\",\n          \"profileImageUrl\": \"https://yt3.ggpht.com/a-/AOh14Gg1_gYcI03VLDd3FMLUY5cb5O9RC9sElj26-1SR=s288-c-k-c0xffffffff-no-rj-mo\"\n        },\n        \"membershipsDetails\": {\n          \"highestAccessibleLevel\": \"string\",\n          \"highestAccessibleLevelDisplayName\": \"string\",\n          \"accessibleLevels\": [\n            \"string\"\n          ],\n          \"membershipsDuration\": {\n            \"memberSince\": \"2007-08-23T00:34:43Z\",\n            \"memberTotalDurationMonths\": 5\n          },\n          \"membershipsDurationAtLevel\": [\n            {\n              \"level\": \"string\",\n              \"memberSince\": \"2007-08-23T00:34:43Z\",\n              \"memberTotalDurationMonths\": 6\n            }\n          ]\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/members/membership_levels.json",
    "content": "{\n  \"kind\": \"youtube#membershipsLevelListResponse\",\n  \"etag\": \"etag\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#membershipsLevel\",\n      \"etag\": \"etag\",\n      \"id\": \"id\",\n      \"snippet\": {\n        \"creatorChannelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"levelDetails\": {\n          \"displayName\": \"high\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#membershipsLevel\",\n      \"etag\": \"etag\",\n      \"id\": \"id\",\n      \"snippet\": {\n        \"creatorChannelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"levelDetails\": {\n          \"displayName\": \"low\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/playlist_items/insert_response.json",
    "content": "{\n  \"kind\": \"youtube#playlistItem\",\n  \"etag\": \"4Bl2u6s8N1Jkkz1AHN4E-tw4OQQ\",\n  \"id\": \"UExCYWlkdDBpbENNYW5HRElLcjhVVkJGWndOX1V2TUt2Uy4wMTcyMDhGQUE4NTIzM0Y5\",\n  \"snippet\": {\n    \"publishedAt\": \"2022-11-15T13:38:09Z\",\n    \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n    \"title\": \"Lecture 6: Version Control (git) (2020)\",\n    \"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/\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://i.ytimg.com/vi/2sjqTHE0zok/default.jpg\",\n        \"width\": 120,\n        \"height\": 90\n      },\n      \"medium\": {\n        \"url\": \"https://i.ytimg.com/vi/2sjqTHE0zok/mqdefault.jpg\",\n        \"width\": 320,\n        \"height\": 180\n      },\n      \"high\": {\n        \"url\": \"https://i.ytimg.com/vi/2sjqTHE0zok/hqdefault.jpg\",\n        \"width\": 480,\n        \"height\": 360\n      },\n      \"standard\": {\n        \"url\": \"https://i.ytimg.com/vi/2sjqTHE0zok/sddefault.jpg\",\n        \"width\": 640,\n        \"height\": 480\n      },\n      \"maxres\": {\n        \"url\": \"https://i.ytimg.com/vi/2sjqTHE0zok/maxresdefault.jpg\",\n        \"width\": 1280,\n        \"height\": 720\n      }\n    },\n    \"channelTitle\": \"ikaros data\",\n    \"playlistId\": \"PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS\",\n    \"position\": 0,\n    \"resourceId\": {\n      \"kind\": \"youtube#video\",\n      \"videoId\": \"2sjqTHE0zok\"\n    },\n    \"videoOwnerChannelTitle\": \"Missing Semester\",\n    \"videoOwnerChannelId\": \"UCuXy5tCgEninup9cGplbiFw\"\n  }\n}\n"
  },
  {
    "path": "testdata/apidata/playlist_items/playlist_items_filter_video.json",
    "content": "{\n  \"kind\": \"youtube#playlistItemListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/L2uYevt91mZBw1hKeHio9_Aamz8\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 1,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Kib3kvf3c_Bq79UyVpa2pHYzV_U\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4wMTcyMDhGQUE4NTIzM0Y5\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-11T00:55:44.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google I/O'19 - I/O Live (Day 3 Composite)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/VCv-KKIkLns/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/VCv-KKIkLns/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/VCv-KKIkLns/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/VCv-KKIkLns/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/VCv-KKIkLns/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n        \"position\": 2,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"VCv-KKIkLns\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/playlist_items/playlist_items_multi.json",
    "content": "{\n  \"kind\": \"youtube#playlistItemListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Rwbdfm4jH2tQT1_c-Jxyy8qh2Xs\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/E5rTjxNaKfzDc-GFs2Cb9jkKlGM\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-11T00:27:38.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google I/O'19 - I/O Live (Day 1 Composite)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n        \"position\": 0,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"H1HZyvc0QnI\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/3JqJ3Bv7ZIVEu4ZoeH6ZGsUe7js\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4yODlGNEE0NkRGMEEzMEQy\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-11T00:52:10.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google I/O'19 - I/O Live (Day 2  Composite)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n        \"position\": 1,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"5NgsfxIWNls\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/playlist_items/playlist_items_paged_1.json",
    "content": "{\n  \"kind\": \"youtube#playlistItemListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/P4kQiZH0w8ID3LMajA0LjTQAypg\\\"\",\n  \"nextPageToken\": \"CAoQAA\",\n  \"pageInfo\": {\n    \"totalResults\": 13,\n    \"resultsPerPage\": 10\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Qq9nexxDsu-O8I8Z-OYt-i7VhGw\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-21T22:17:43.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Welcome from Google Developers - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/CvTApw9X8aA/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/CvTApw9X8aA/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/CvTApw9X8aA/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/CvTApw9X8aA/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/CvTApw9X8aA/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 0,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"CvTApw9X8aA\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/szvAfCSYgJYm3LDxkSSrYhC0WrM\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy4yODlGNEE0NkRGMEEzMEQy\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:14:16.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Conversation AI - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Yh4EKaUY3gg/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Yh4EKaUY3gg/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Yh4EKaUY3gg/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/Yh4EKaUY3gg/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/Yh4EKaUY3gg/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 1,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"Yh4EKaUY3gg\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/SoE9Vp7LPueb-lVLD4JEnLvQyxY\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy4wMTcyMDhGQUE4NTIzM0Y5\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:20:06.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Reinforcement Learning with TensorFlow and Unity - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/S-MbpQiwfls/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/S-MbpQiwfls/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/S-MbpQiwfls/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/S-MbpQiwfls/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/S-MbpQiwfls/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 2,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"S-MbpQiwfls\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/mTmJckP7mIexrzn524dGpHfwpPA\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41MjE1MkI0OTQ2QzJGNzNG\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:25:29.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Art + AI - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/BBjVl1EETb0/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/BBjVl1EETb0/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/BBjVl1EETb0/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/BBjVl1EETb0/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/BBjVl1EETb0/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 3,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"BBjVl1EETb0\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/QJ09nQXlszEGNjJA4zMFr6KKXWE\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy4wOTA3OTZBNzVEMTUzOTMy\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:28:46.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Code-Free probing of Machine Learning models - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/h8CnO-oqqSs/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/h8CnO-oqqSs/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/h8CnO-oqqSs/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/h8CnO-oqqSs/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/h8CnO-oqqSs/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 4,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"h8CnO-oqqSs\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/oKKyBUNjbGZqwxRylpicJtxpL1w\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy4xMkVGQjNCMUM1N0RFNEUx\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:32:55.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Neural Query Language - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/2cHddIA4EvY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/2cHddIA4EvY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/2cHddIA4EvY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/2cHddIA4EvY/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/2cHddIA4EvY/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 5,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"2cHddIA4EvY\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/B3KKp2wby9fWhYeq0Ho4GaF5hKE\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41MzJCQjBCNDIyRkJDN0VD\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:35:29.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"The future of Mobile Learning - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/kTJUt9CmTXg/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/kTJUt9CmTXg/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/kTJUt9CmTXg/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/kTJUt9CmTXg/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/kTJUt9CmTXg/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 6,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"kTJUt9CmTXg\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/DhloJevNp-LgUKvIK-6UyahOQVQ\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy5DQUNERDQ2NkIzRUQxNTY1\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:37:49.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Inclusive AI - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/o7_oJYZw2Hg/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/o7_oJYZw2Hg/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/o7_oJYZw2Hg/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/o7_oJYZw2Hg/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/o7_oJYZw2Hg/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 7,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"o7_oJYZw2Hg\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/pG62VWLcupRiuy-bO1NuaVKby3I\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy45NDk1REZENzhEMzU5MDQz\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:40:19.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Serverless & AI/ML - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/vN6uK5Qm23c/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/vN6uK5Qm23c/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/vN6uK5Qm23c/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/vN6uK5Qm23c/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/vN6uK5Qm23c/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 8,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"vN6uK5Qm23c\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/7QBFXcO5f-kiWNCzM-byiACxvzw\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy5GNjNDRDREMDQxOThCMDQ2\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:42:56.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Convolutional neural networks with Swift - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/o5LP2xzKkpg/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/o5LP2xzKkpg/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/o5LP2xzKkpg/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/o5LP2xzKkpg/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/o5LP2xzKkpg/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 9,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"o5LP2xzKkpg\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/playlist_items/playlist_items_paged_2.json",
    "content": "{\n  \"kind\": \"youtube#playlistItemListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Mdch507Btu8hjUyr5r5qJbQgL1Y\\\"\",\n  \"prevPageToken\": \"CAoQAQ\",\n  \"pageInfo\": {\n    \"totalResults\": 13,\n    \"resultsPerPage\": 10\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/SQwmGD19cBQb5iwOedC6-LApUEo\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy40NzZCMERDMjVEN0RFRThB\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:45:05.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Vizier: Black-box optimization and AutoML - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/6aSG8SdvkoU/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/6aSG8SdvkoU/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/6aSG8SdvkoU/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/6aSG8SdvkoU/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/6aSG8SdvkoU/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 10,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"6aSG8SdvkoU\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/vDhnNoD_Q3xyw2m9CBl4hkab4cI\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy5EMEEwRUY5M0RDRTU3NDJC\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:48:01.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Panel discussion - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/ECGpZCFUL8s/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/ECGpZCFUL8s/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/ECGpZCFUL8s/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/ECGpZCFUL8s/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/ECGpZCFUL8s/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 11,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"ECGpZCFUL8s\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/gtg7bcS_SDwOLHtZwPMU8OBxAh4\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy45ODRDNTg0QjA4NkFBNkQy\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-23T00:49:59.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Closing remarks - Pittsburgh ML Summit ‘19\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Fu2bldybpkA/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Fu2bldybpkA/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Fu2bldybpkA/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/Fu2bldybpkA/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/Fu2bldybpkA/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n        \"position\": 12,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"Fu2bldybpkA\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/playlist_items/playlist_items_single.json",
    "content": "{\n  \"kind\": \"youtube#playlistItemListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/YVsjtCTnqysTcNJy7jZglowaNYM\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 1,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/E5rTjxNaKfzDc-GFs2Cb9jkKlGM\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-11T00:27:38.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google I/O'19 - I/O Live (Day 1 Composite)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n        \"position\": 0,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"H1HZyvc0QnI\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/playlists/insert_response.json",
    "content": "{\n  \"kind\": \"youtube#playlist\",\n  \"etag\": \"Gw0SW_V3Hy1XNqjAJB1v1Q0ZmB4\",\n  \"id\": \"PLBaidt0ilCMZN8XPVB5iXY6FlSYGeyn2n\",\n  \"snippet\": {\n    \"publishedAt\": \"2022-11-16T04:12:59Z\",\n    \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n    \"title\": \"Test playlist\",\n    \"description\": \"\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://i.ytimg.com/img/no_thumbnail.jpg\",\n        \"width\": 120,\n        \"height\": 90\n      },\n      \"medium\": {\n        \"url\": \"https://i.ytimg.com/img/no_thumbnail.jpg\",\n        \"width\": 320,\n        \"height\": 180\n      },\n      \"high\": {\n        \"url\": \"https://i.ytimg.com/img/no_thumbnail.jpg\",\n        \"width\": 480,\n        \"height\": 360\n      }\n    },\n    \"channelTitle\": \"ikaros data\",\n    \"localized\": {\n      \"title\": \"Test playlist\",\n      \"description\": \"\"\n    }\n  }\n}\n"
  },
  {
    "path": "testdata/apidata/playlists/playlists_mine.json",
    "content": "{\n  \"kind\": \"youtube#playlistListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/LjFPI2nwRBxtabydhze2pwRj5LI\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/9CjYT6fC317YBYj1iRh_oZ3v27U\\\"\",\n      \"id\": \"PLOU2XLYxmsIIOSO0eWuj-6yQmdakarUzN\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-02T23:38:49.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Accessibility at Google I/O 2019\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Accessibility at Google I/O 2019\",\n          \"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/\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/V5b6wvRkyEXDBtrgywP5vk-SnuA\\\"\",\n      \"id\": \"PLOU2XLYxmsIJ5Bl3HmuxKY5WE555cu9Uc\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-02T23:38:06.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Inspiration at Google I/O 2019\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/qL4U9Ygtxh8/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/qL4U9Ygtxh8/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/qL4U9Ygtxh8/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/qL4U9Ygtxh8/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/qL4U9Ygtxh8/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Inspiration at Google I/O 2019\",\n          \"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/\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/playlists/playlists_multi.json",
    "content": "{\n  \"kind\": \"youtube#playlistListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/tANZNtv_p2OkP0sqKDLjKwJQ4Cs\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/gdl_m86JfWCd37Wtcc2dte9hrEg\\\"\",\n      \"id\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-10T00:18:56.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"I/O Live - Show Composite\",\n        \"description\": \"\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"I/O Live - Show Composite\",\n          \"description\": \"\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/FChLS9OtNbF2YcyhMyWgTHL7VLg\\\"\",\n      \"id\": \"PLOU2XLYxmsIJJVnHWmd1qfr0Caq4VZCu4\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-10T00:18:07.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"I/O Live\",\n        \"description\": \"Relive moments from Google I/O 2019\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/o-2RMd6WOg8/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/o-2RMd6WOg8/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/o-2RMd6WOg8/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/o-2RMd6WOg8/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/o-2RMd6WOg8/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"I/O Live\",\n          \"description\": \"Relive moments from Google I/O 2019\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/playlists/playlists_paged_1.json",
    "content": "{\n  \"kind\": \"youtube#playlistListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/jGXEiDmTtAs7vTBHF90lfHs9yOA\\\"\",\n  \"nextPageToken\": \"CAoQAA\",\n  \"pageInfo\": {\n    \"totalResults\": 422,\n    \"resultsPerPage\": 10\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/z8ateNaRCjA2yCsvti1OTzDIvHo\\\"\",\n      \"id\": \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-21T22:17:38.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Pittsburgh ML Summit ‘19\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/CvTApw9X8aA/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/CvTApw9X8aA/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/CvTApw9X8aA/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/CvTApw9X8aA/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/CvTApw9X8aA/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Pittsburgh ML Summit ‘19\",\n          \"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.\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/8kvRpRGgdJloFkUOu_S12Vy00E4\\\"\",\n      \"id\": \"PLOU2XLYxmsIJO83u2UmyC8ud41AvUnhgj\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-18T20:00:47.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Kirkland ML Summit '19\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/c2gJxZ1Qa4k/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/c2gJxZ1Qa4k/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/c2gJxZ1Qa4k/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/c2gJxZ1Qa4k/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/c2gJxZ1Qa4k/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Kirkland ML Summit '19\",\n          \"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.\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ewmspkP9LrOg8_2nd1e71xcr0u8\\\"\",\n      \"id\": \"PLOU2XLYxmsILfV1LiUhDjbh1jkFjQWrYB\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-07T22:55:11.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google Developers Experts\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/kFjxIgIgW80/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/kFjxIgIgW80/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/kFjxIgIgW80/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/kFjxIgIgW80/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/kFjxIgIgW80/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Google Developers Experts\",\n          \"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.\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/c9OQR20t2PJhtUIJw79bb7CgGrU\\\"\",\n      \"id\": \"PLOU2XLYxmsIKNr3Wfhm8o0TSojW7hEPPY\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-01T20:57:43.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Cambridge ML Summit '19\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/KWefSoJDja8/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/KWefSoJDja8/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/KWefSoJDja8/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/KWefSoJDja8/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/KWefSoJDja8/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Cambridge ML Summit '19\",\n          \"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.\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/3Q4DeS3RXGlkoRoaDjRTzgP8hl0\\\"\",\n      \"id\": \"PLOU2XLYxmsIJ8ItHmK4bRlY4GCzMgXLAJ\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-09-27T22:16:57.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"#MyDomain\",\n        \"description\": \"\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/RiqYfiKU1Zo/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/RiqYfiKU1Zo/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/RiqYfiKU1Zo/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/RiqYfiKU1Zo/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"#MyDomain\",\n          \"description\": \"\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/wFrhKcRFkwQd7ocSg8Hy1VXnBnQ\\\"\",\n      \"id\": \"PLOU2XLYxmsIIra6qDjAHl63jlWbq5me8i\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-09-23T22:52:15.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"North America DSC Summit 2019\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/aiMb0sNmpdY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/aiMb0sNmpdY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/aiMb0sNmpdY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/aiMb0sNmpdY/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/aiMb0sNmpdY/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"North America DSC Summit 2019\",\n          \"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.\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ytuOSx4rjsuY7yHeez5VCA7dGw4\\\"\",\n      \"id\": \"PLOU2XLYxmsILHvpAkROp2dXz-jQi4S4_y\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-09-11T22:29:53.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Introduction to ARCore Augmented Faces\",\n        \"description\": \"\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/8ih7eHwPoxM/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/8ih7eHwPoxM/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/8ih7eHwPoxM/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/8ih7eHwPoxM/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/8ih7eHwPoxM/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Introduction to ARCore Augmented Faces\",\n          \"description\": \"\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/NvwWi2jAzPFc884j9G1IPW2_YIA\\\"\",\n      \"id\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-10T00:18:56.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"I/O Live - Show Composite\",\n        \"description\": \"\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"I/O Live - Show Composite\",\n          \"description\": \"\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/WdUKh80tT13sUtVr5ZIFMGc8M5I\\\"\",\n      \"id\": \"PLOU2XLYxmsIJJVnHWmd1qfr0Caq4VZCu4\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-10T00:18:07.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"I/O Live\",\n        \"description\": \"Relive moments from Google I/O 2019\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/UVOhgly2VEc/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/UVOhgly2VEc/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/UVOhgly2VEc/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/UVOhgly2VEc/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/UVOhgly2VEc/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"I/O Live\",\n          \"description\": \"Relive moments from Google I/O 2019\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/s7ZgGwVG5R5qa-Sxi9I4xeeVI3k\\\"\",\n      \"id\": \"PLOU2XLYxmsIKW-llcbcFdpR9RjCfYHZaV\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-02T23:39:42.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Machine Learning at Google I/O 2019\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/pM9u9xcM_cs/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/pM9u9xcM_cs/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/pM9u9xcM_cs/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/pM9u9xcM_cs/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/pM9u9xcM_cs/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Machine Learning at Google I/O 2019\",\n          \"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/\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/playlists/playlists_paged_2.json",
    "content": "{\n  \"kind\": \"youtube#playlistListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/LjFPI2nwRBxtabydhze2pwRj5LI\\\"\",\n  \"prevPageToken\": \"CAoQAQ\",\n  \"pageInfo\": {\n    \"totalResults\": 422,\n    \"resultsPerPage\": 10\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/9CjYT6fC317YBYj1iRh_oZ3v27U\\\"\",\n      \"id\": \"PLOU2XLYxmsIIOSO0eWuj-6yQmdakarUzN\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-02T23:38:49.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Accessibility at Google I/O 2019\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Accessibility at Google I/O 2019\",\n          \"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/\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/V5b6wvRkyEXDBtrgywP5vk-SnuA\\\"\",\n      \"id\": \"PLOU2XLYxmsIJ5Bl3HmuxKY5WE555cu9Uc\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-02T23:38:06.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Inspiration at Google I/O 2019\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/qL4U9Ygtxh8/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/qL4U9Ygtxh8/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/qL4U9Ygtxh8/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/qL4U9Ygtxh8/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/qL4U9Ygtxh8/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Inspiration at Google I/O 2019\",\n          \"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/\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/7N7BKt6QAueeE4_8csdQyUXvhdY\\\"\",\n      \"id\": \"PLOU2XLYxmsIK1qyYq0gzScqXT8JmHQLs4\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-02T23:37:27.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Gaming at Google I/O 2019\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/RY7wXC_b0R8/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/RY7wXC_b0R8/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/RY7wXC_b0R8/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/RY7wXC_b0R8/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/RY7wXC_b0R8/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Gaming at Google I/O 2019\",\n          \"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/\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/jcbtyLPKyuXpapzBMBE5rr0VfEU\\\"\",\n      \"id\": \"PLOU2XLYxmsIIICsWVglOZDtXpK7HLYswc\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-02T23:35:47.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"IoT at Google I/O 2019\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/QRzvINzJTyQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/QRzvINzJTyQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/QRzvINzJTyQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/QRzvINzJTyQ/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/QRzvINzJTyQ/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"IoT at Google I/O 2019\",\n          \"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/\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ykZ3hvrD6AmkKbNvXwpeTV_hYYI\\\"\",\n      \"id\": \"PLOU2XLYxmsIK5JcIMUhrPH7T0lHBrKbER\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-02T23:33:43.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Assistant at Google I/O 2019\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/dpNrq_wiqGs/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/dpNrq_wiqGs/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/dpNrq_wiqGs/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/dpNrq_wiqGs/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/dpNrq_wiqGs/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Assistant at Google I/O 2019\",\n          \"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/\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/xzIksSFchIg10f12PMD5bo5mmkk\\\"\",\n      \"id\": \"PLOU2XLYxmsILVTiOlMJdo7RQS55jYhsMi\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-01T00:43:22.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google I/O 2019 - All Sessions\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/lyRPyRKHO8M/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/lyRPyRKHO8M/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/lyRPyRKHO8M/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/lyRPyRKHO8M/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/lyRPyRKHO8M/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Google I/O 2019 - All Sessions\",\n          \"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/\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/46hFFhvAepPrQMRR8dWjWyLB5zE\\\"\",\n      \"id\": \"PLOU2XLYxmsIKP4Hh9gQO54naZ8V7mDEQi\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-04-30T21:37:13.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Behind the Actions\",\n        \"description\": \"In this Behind the Actions series, you will learn how to build, test, and publish Actions on Google.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/plr65MD-FBY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/plr65MD-FBY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/plr65MD-FBY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/plr65MD-FBY/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/plr65MD-FBY/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Behind the Actions\",\n          \"description\": \"In this Behind the Actions series, you will learn how to build, test, and publish Actions on Google.\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ZLqa7_rX9Czyvo5-8kj9d1QgARE\\\"\",\n      \"id\": \"PLOU2XLYxmsIKORZ6mM3fQ02gKjqvSa2yd\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-04-09T18:18:59.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"How I Code\",\n        \"description\": \"\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/6GMs_S3XIys/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/6GMs_S3XIys/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/6GMs_S3XIys/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/6GMs_S3XIys/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/6GMs_S3XIys/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"How I Code\",\n          \"description\": \"\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/BXfcSFdRnnKPVwAAcTZOX9ffEZU\\\"\",\n      \"id\": \"PLOU2XLYxmsILU6mHf5ERbUBpvKX6GL4rn\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-03-22T00:05:51.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Assistant on Air\",\n        \"description\": \"\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Assistant on Air\",\n          \"description\": \"\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/OqmLB5ZBwHPYbJHQZa8ImdBv9RU\\\"\",\n      \"id\": \"PLOU2XLYxmsIIfaDH4-GcZzWIPFyD7Lpt3\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-03-18T16:48:03.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google @ Game Developers Conference 2019\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/nUih5C5rOrA/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/nUih5C5rOrA/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/nUih5C5rOrA/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Google @ Game Developers Conference 2019\",\n          \"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.\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/playlists/playlists_single.json",
    "content": "{\n  \"kind\": \"youtube#playlistListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/fk7i9mk8dlQehbz-BVBy0SzccWY\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 1,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/gdl_m86JfWCd37Wtcc2dte9hrEg\\\"\",\n      \"id\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-10T00:18:56.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"I/O Live - Show Composite\",\n        \"description\": \"\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"I/O Live - Show Composite\",\n          \"description\": \"\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/search/search_by_developer.json",
    "content": "{\n  \"kind\": \"youtube#searchListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/-_uuZ-sNfL41nlDe7y-H3-hTVR8\\\"\",\n  \"nextPageToken\": \"CAoQAA\",\n  \"prevPageToken\": \"CAUQAQ\",\n  \"regionCode\": \"US\",\n  \"pageInfo\": {\n    \"totalResults\": 1000000,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/0Qdy2kYX0gwN6vW9FaXr5bKR1e8\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"WuyFniRMrxY\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-14T01:59:46.000Z\",\n        \"channelId\": \"UCeY0bbntWzzVIaj2z3QigXg\",\n        \"title\": \"NBC Nightly News Broadcast (Full) - March 13th, 2020 | NBC Nightly News\",\n        \"description\": \"Coronavirus pandemic: President Trump declares national emergency to combat coronavirus, already strained hospitals worry about what's to come, and ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/WuyFniRMrxY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/WuyFniRMrxY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/WuyFniRMrxY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"NBC News\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/VrmRau0XKBBIRmWgTtLIcifdHnQ\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"VQIZkYkIynE\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-15T12:43:32.000Z\",\n        \"channelId\": \"UCknLrEdhRCp1aegoMqRaCZg\",\n        \"title\": \"Coronavirus precautions sweep around the world | DW News\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/VQIZkYkIynE/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/VQIZkYkIynE/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/VQIZkYkIynE/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"DW News\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/ZQL6qzIJxT1j-e6wqOssMFgBC7g\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"q_WM_pMp0Hg\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-13T21:12:36.000Z\",\n        \"channelId\": \"UCeY0bbntWzzVIaj2z3QigXg\",\n        \"title\": \"Trump Holds News Conference On Coronavirus Pandemic | NBC News (Live Stream Recording)\",\n        \"description\": \"President Donald Trump holds a news conference on the coronavirus pandemic which is spreading across the United States. The president is expected to ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/q_WM_pMp0Hg/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/q_WM_pMp0Hg/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/q_WM_pMp0Hg/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"NBC News\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/VKr-2JYTn80JU9wfted23CwUrJU\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"wDbyThHkctA\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-14T00:06:34.000Z\",\n        \"channelId\": \"UCBi2mrWuNuyYy4gbM6fU18Q\",\n        \"title\": \"ABC News Prime: Coronavirus national emergency, life under quarantine, stock market\",\n        \"description\": \"coronavirus #quarantine #nationalemergency #nyse SUBSCRIBE to ABC NEWS: https://bit.ly/2vZb6yP Watch More on http://abcnews.go.com/ LIKE ABC News ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/wDbyThHkctA/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/wDbyThHkctA/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/wDbyThHkctA/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"ABC News\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/kubYi5AYaeyfTjRY02WZFb1xipE\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"IQdYDkwtvoo\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-13T22:41:09.000Z\",\n        \"channelId\": \"UC16niRr50-MSBwiO3YDb3RA\",\n        \"title\": \"Coronavirus: Europe at the epicentre of the pandemic - BBC News\",\n        \"description\": \"The World Health Organisation says Europe is now the epicentre, of the global coronavirus pandemic. The news emerged as organised sport, both professional ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/IQdYDkwtvoo/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/IQdYDkwtvoo/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/IQdYDkwtvoo/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"BBC News\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/search/search_by_event.json",
    "content": "{\n  \"kind\": \"youtube#searchListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/SE083xj7h4uR9Tn6xMg-LwbsFw8\\\"\",\n  \"nextPageToken\": \"CBkQAA\",\n  \"regionCode\": \"US\",\n  \"pageInfo\": {\n    \"totalResults\": 2342,\n    \"resultsPerPage\": 25\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/5aX5VRI4Ow4wngR-oJGG0m5ghLY\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"qgylp3Td1Bw\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-01-30T01:54:33.000Z\",\n        \"channelId\": \"UCDGiCfCZIV5phsoGiPwIcyQ\",\n        \"title\": \"[LIVE] Coronavirus Pandemic: Real Time Counter, World Map, News\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/qgylp3Td1Bw/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/qgylp3Td1Bw/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/qgylp3Td1Bw/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Roylab Stats\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/KZp_3SD0kS28nlJqpCt7CKUKDC8\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"9Auq9mYxFEE\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-02T09:49:18.000Z\",\n        \"channelId\": \"UCoMdktPbSTixAyNGwb-UYkQ\",\n        \"title\": \"Watch Sky News live\",\n        \"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\\\".\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/9Auq9mYxFEE/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/9Auq9mYxFEE/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/9Auq9mYxFEE/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Sky News\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/7a4VJgDJUaloBgxdFIsUXOj4HIE\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"fy8iq-tib-U\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-13T06:39:48.000Z\",\n        \"channelId\": \"UCRWFSbif-RFENbBrSiez1DA\",\n        \"title\": \"ABP News LIVE TV: Watch Top News Of The Day 24*7 on ABP News LIVE TV\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/fy8iq-tib-U/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/fy8iq-tib-U/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/fy8iq-tib-U/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"ABP NEWS\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Qzvc-nqa6GICabLjplsOCUk2GCI\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"fjxHTtZOAgQ\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-15T09:42:02.000Z\",\n        \"channelId\": \"UCttspZesZIDEwwpVIgoZtWQ\",\n        \"title\": \"IndiaTV LIVE | Hindi News LIVE 24X7 | इंडिया टीवी LIVE\",\n        \"description\": \"IndiaTV LIVE | Hindi News LIVE | इंडिया टीवी LIVE Subscribe to IndiaTV and don't forget to press \\\"THE BELL ICON\\\" to never miss any updates- ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/fjxHTtZOAgQ/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/fjxHTtZOAgQ/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/fjxHTtZOAgQ/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"IndiaTV\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/2rT6MY29RMrS3vcglPt5QxyLlmg\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"Q6QR4979KIQ\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-08-21T11:09:13.000Z\",\n        \"channelId\": \"UCPXTXMecYqnRKNdqdVOGSFg\",\n        \"title\": \"TV9 Telugu LIVE\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Q6QR4979KIQ/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Q6QR4979KIQ/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Q6QR4979KIQ/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"TV9 Telugu Live\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/edP7aU3v1BL_vvpd84thza2b3z0\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"E9mb9fwL1Pk\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-10T22:19:59.000Z\",\n        \"channelId\": \"UCH7nv1A9xIrAifZJNvt7cgA\",\n        \"title\": \"ABP Majha LIVE | ABP Majha Latest Updates | एबीपी माझा | Marathi LIVE News\",\n        \"description\": \"For latest breaking news ( #MahaPoliticsNews #MarathiNews #DelhiRiots2020 ) log on to: https://marathi.abplive.com/ Social Media Handles: Facebook: ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/E9mb9fwL1Pk/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/E9mb9fwL1Pk/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/E9mb9fwL1Pk/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"ABP Majha\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/bzzK40_g2ivEub0g_ttaakdT2E8\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"w_Ma8oQLmSM\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-02-12T00:32:37.000Z\",\n        \"channelId\": \"UCBi2mrWuNuyYy4gbM6fU18Q\",\n        \"title\": \"Watch the Latest News Headlines and Live Events l ABC News Live\",\n        \"description\": \"News #LiveNews #StreamingNews #ABCNewsLive LATEST NEWS: https://abcnews.go.com/ SUBSCRIBE to ABC News on YouTube: https://bit.ly/2vZb6yP ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/w_Ma8oQLmSM/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/w_Ma8oQLmSM/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/w_Ma8oQLmSM/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"ABC News\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/rqvpQP9DNC1PWSy1xx_upyzfazo\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"jdJoOhqCipA\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-01-08T06:59:28.000Z\",\n        \"channelId\": \"UC8dnBi4WUErqYQHZ4PfsLTg\",\n        \"title\": \"TV9 KANNADA NEWS LIVE | ಟಿವಿ9 ಕನ್ನಡ ನ್ಯೂಸ್ ಲೈವ್\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/jdJoOhqCipA/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/jdJoOhqCipA/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/jdJoOhqCipA/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Tv9 Kannada\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/FHVxe0z-mMnJfh4WVeSC05g0qIo\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"mcHRJAxmWhA\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-15T02:19:37.000Z\",\n        \"channelId\": \"UCE2606prvXQc_noEqKxVJXA\",\n        \"title\": \"Metro Manila isinailalim sa quarantine kontra pagkalat ng COVID-19 | DZMM\",\n        \"description\": \"ABSCBNNews #ABSCBNNewsLivestream Subscribe to the ABS-CBN News channel! - http://bit.ly/TheABSCBNNews Visit our website at ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/mcHRJAxmWhA/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/mcHRJAxmWhA/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/mcHRJAxmWhA/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"ABS-CBN News\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/jG__YPSLyRPg0Nanctder5zWPGc\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"iL53Y28Rp84\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-15T01:45:33.000Z\",\n        \"channelId\": \"UCf8w5m0YsRa8MHQ5bwSGmbw\",\n        \"title\": \"Asianet News Live TV  | Malayalam News Live | ഏഷ്യാനെറ്റ് ന്യൂസ് ലൈവ്  | Kerala News Live\",\n        \"description\": \"MalaylamLiveTV #MalayalamLiveNews #AsianetnewsLiveTV #KeralaLiveNews #AsianetNewsLive Click Here to Subscribe! ▻ http://goo.gl/Y4yRZG Asianet ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/iL53Y28Rp84/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/iL53Y28Rp84/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/iL53Y28Rp84/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"asianetnews\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/AryGyghLx28-1KGCz8jCTkZr8jc\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"l9ViEIip9q4\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-02T09:47:22.000Z\",\n        \"channelId\": \"UC9CYT9gSNLevX5ey2_6CK0Q\",\n        \"title\": \"NDTV India LIVE TV - Watch Latest News in Hindi | हिंदी समाचार\",\n        \"description\": \"देखें NDTV इंडिया लाइव, फ़्री डिश पर चैनल नं 45. NDTV India live stream is also available on https://khabar.ndtv.com/videos/live/channe...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/l9ViEIip9q4/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/l9ViEIip9q4/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/l9ViEIip9q4/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"NDTV India\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/ntEW0YTHOoO856jDvagg8Bl9BCU\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"fraxH3rvU84\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-01-29T12:06:30.000Z\",\n        \"channelId\": \"UCdOSeEq9Cs2Pco7OCn2_i5w\",\n        \"title\": \"TV9 Marathi Live | टीव्ही9 मराठी LIVE | Marathi News Updates\",\n        \"description\": \"TV9 Marathi Live | टीव्ही9 मराठी LIVE | Marathi News Updates कोरोनाचं प्रश्नचक्र | Corona Special Show | कोरोनावर...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/fraxH3rvU84/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/fraxH3rvU84/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/fraxH3rvU84/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"TV9 Marathi\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/KHFdy6b3DLBhKnkc1JoLafO6rGc\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"jjH6v95z3Nw\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-01-27T05:05:51.000Z\",\n        \"channelId\": \"UCP0uG-mcMImgKnJz-VjJZmQ\",\n        \"title\": \"Manorama News LIVE TV | മനോരമ ന്യൂസ് ലൈവ് | Latest Kerala News Updates | Malayalam News LIVE Channel\",\n        \"description\": \"Watch Manorama News Malayalam Channel Live Stream for Kerala Budget Updates, Latest Malayalam News Updates, Breaking News, Political News and ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/jjH6v95z3Nw/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/jjH6v95z3Nw/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/jjH6v95z3Nw/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Manorama News\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/0b4I4C7PgvHQe5koF1Vq9qotwG4\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"lhI934dror4\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-13T19:54:16.000Z\",\n        \"channelId\": \"UCeJWZgSMlzqYEDytDnvzHnw\",\n        \"title\": \"Top News Stories From Gujarat, India and International | Tv9 Gujarati LIVE\",\n        \"description\": \"Top News Stories From Gujarat, India and International | Tv9 Gujarati LIVE #TV9GujaratiLive #GujaratiNews #TV9News Tv9 ગુજરાતીની Youtube ચેનલને ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/lhI934dror4/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/lhI934dror4/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/lhI934dror4/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"TV9 Gujarati\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/cQAyqrmM5zSwQ8XaXdzC43IXXfA\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"pc6kNspzvqI\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-12T00:07:00.000Z\",\n        \"channelId\": \"UCBwc2cbPpvxNCNEI2-8YrqQ\",\n        \"title\": \"Madhya Pradesh Political Crisis LIVE Updates | News18 MP Chattisgarh Live | Chhattisgarh News\",\n        \"description\": \"MPNewsLive #मध्यप्रदेशसमाचार #ChattisgarhNews #News18MadhyaPradesh #ChhattisgarhLiveTV #HindiNews News18 MP-Chhattisgarh में ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/pc6kNspzvqI/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/pc6kNspzvqI/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/pc6kNspzvqI/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"News18 MP Chhattisgarh\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/pJgY5U7TjUN1FipOXWJQxZsGbDw\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"41e4fSihWG0\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-01-25T03:49:13.000Z\",\n        \"channelId\": \"UC-f7r46JhYv78q5pGrO6ivA\",\n        \"title\": \"MediaOneTV Live | Latest Malayalam News &amp; Live Updates | മീഡിയവണ്\\u200d ടി.വി ലൈവ്\",\n        \"description\": \"MediaOneTVLive #MalayalamNews #KeralaNews #MediaOneTVLive #MalayalamLiveNews MediaOneTVLive MalayalamLiveNews KeralaLiveNews ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/41e4fSihWG0/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/41e4fSihWG0/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/41e4fSihWG0/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"MediaoneTV Live\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/6diKV3o5THkuBEUSKUjjPkVsV3k\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"H3AZDSGwqMM\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-02-15T03:15:48.000Z\",\n        \"channelId\": \"UC8Z-VjXBtDJTvq6aqkIskPg\",\n        \"title\": \"🔴LIVE: Polimer News Live | Tamil News | Coronavirus|WHO|Rajini|Rajini Press Meet|Rajya Sabha MP\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/H3AZDSGwqMM/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/H3AZDSGwqMM/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/H3AZDSGwqMM/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Polimer News\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/nFzCQm1JkyTW1-4NiDpjfSG--6I\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"XxJKnDLYZz4\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2015-01-16T02:28:27.000Z\",\n        \"channelId\": \"UClIfopQZlkkSpM1VgCFLRJA\",\n        \"title\": \"民視新聞直播 | Taiwan Formosa live news HD | 台湾のニュース放送HD | 대만 뉴스 방송HD\",\n        \"description\": \"民視新聞#即時新聞#新聞直播四季線上影視https://4gtv.tv 民視新聞網影音https://bit.ly/2Jwxt2D 民視新聞網官網https://www.ftvnews.com.tw/ 每日新聞熱點...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/XxJKnDLYZz4/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/XxJKnDLYZz4/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/XxJKnDLYZz4/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"民視直播 FTVN Live 53\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/1nnCgS9I3pBRGbDod7prHOaUBZM\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"coYw-eVU0Ks\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-01-22T07:21:31.000Z\",\n        \"channelId\": \"UCGCZAYq5Xxojl_tSXcVJhiQ\",\n        \"title\": \"JapaNews24 ～日本のニュースを24時間配信\",\n        \"description\": \"事件や政治、自然災害など時事問題から街のトレンドまで…。 日本国内の注目のニュースを中心にまとめた番組を30分ごとにお送りしています。...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/coYw-eVU0Ks/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/coYw-eVU0Ks/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/coYw-eVU0Ks/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"ANNnewsCH\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/EoBPwJQcSjFjwFDK4Glv5vltXLw\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"r7r8zrbOOlk\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-02-17T10:22:35.000Z\",\n        \"channelId\": \"UCPP3etACgdUWvizcES1dJ8Q\",\n        \"title\": \"News 18 India LIVE | Watch Latest News In India | हिंदी समाचार LIVE\",\n        \"description\": \"News18India देखिये ताज़ातरीन खबर सिर्फ News 18 पर Watch all the current, latest and breaking Hindi news only on NEWS18 India Live TV.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/r7r8zrbOOlk/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/r7r8zrbOOlk/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/r7r8zrbOOlk/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"News18 India\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/tBnFfKm0QOcVSKMnY0tAeFgczwE\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"ktNOFI35pwk\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-15T01:22:34.000Z\",\n        \"channelId\": \"UCGyZswzm4G-wEfRQHgMSAuw\",\n        \"title\": \"🔴 News7 Tamil LIVE | Tamil News Live | News Live | நியூஸ்7 தமிழ் | Corona Virus | Rajini Press Meet\",\n        \"description\": \"News7 Tamil LIVE | Tamil News Live | News Live | நியூஸ்7 தமிழ் | Corona Virus | Rajini Press Meet நியூஸ்7 தமிழ் நேரலை ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/ktNOFI35pwk/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/ktNOFI35pwk/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/ktNOFI35pwk/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"News7 Tamil\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/8Z5uE84SwcjOr1DlBT_WtsrfKBo\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"1r2w-b5laYo\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-01-14T09:05:15.000Z\",\n        \"channelId\": \"UCv3rFzn-GHGtqzXiaq3sWNg\",\n        \"title\": \"ABP Ananda News Live | দিনের সেরা খবর সরাসরি | Live Bangla New 24X7 | Latest Bengali News\",\n        \"description\": \"Watch Live Bangla news on ABP Ananda Live #ABPAnanda #Bangla News #LiveNews Subscribe to our YouTube channel here: ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/1r2w-b5laYo/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/1r2w-b5laYo/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/1r2w-b5laYo/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"ABP ANANDA\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/3RwJf3LxGymy8-5v3YeqILn975Y\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"zcrUCvBD16k\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-12-08T14:22:54.000Z\",\n        \"channelId\": \"UCup3etEdjyF1L3sRbU-rKLw\",\n        \"title\": \"24 News Live TV  24/7| HD Live Streaming\",\n        \"description\": \"24 News Live TV 24/7 | Live | HD Live Streaming ഏറ്റവും പുതിയ വാർത്തകൾക്കായി സന്ദർശിക്കുക == http://www.twentyfourne...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/zcrUCvBD16k/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/zcrUCvBD16k/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/zcrUCvBD16k/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"24 News\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/zGlB4yIoO_pevH6VjhhiuPlDnxs\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"5s7AZZKobxo\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-14T13:31:24.000Z\",\n        \"channelId\": \"UCsgC5cbz3DE2Shh34gNKiog\",\n        \"title\": \"92 News HD Live\",\n        \"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, ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/5s7AZZKobxo/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/5s7AZZKobxo/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/5s7AZZKobxo/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"92 News HD\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/sIoVFfBZD87MYz0G0e2TqvCXRdk\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"HOgwUdcMgeY\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-03-13T09:07:39.000Z\",\n        \"channelId\": \"UC7wXt18f2iA3EDXeqAVuKng\",\n        \"title\": \"Coronavirus Outbreak: Republic Bharat Live TV | Latest News in Hindi | रिपब्लिक भारत। लाइव टीवी\",\n        \"description\": \"RepublicBharat #RepublicBharatLive #Coronavirus #HindiNews #IndiaNews #LatestNewsToday #LiveNews #HindiNews #Coronavirus #LiveTV ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/HOgwUdcMgeY/default_live.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/HOgwUdcMgeY/mqdefault_live.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/HOgwUdcMgeY/hqdefault_live.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Republic Bharat\",\n        \"liveBroadcastContent\": \"live\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/search/search_by_keywords_p1.json",
    "content": "{\n  \"kind\": \"youtube#searchListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/HlaDCPqWxO8HJ_tV51UiHmzJdAs\\\"\",\n  \"nextPageToken\": \"CBkQAA\",\n  \"regionCode\": \"JP\",\n  \"pageInfo\": {\n    \"totalResults\": 1000000,\n    \"resultsPerPage\": 25\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/IUPje0AHo3kVWZPuaOELZDozfa0\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"gQ_1zX-F_No\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-12-05T19:00:10.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"SURFING HAWAII’S BEST WAVE WITH MY GIRLFRIEND (PIPELINE)\",\n        \"description\": \"SOME FUN BARRELS IN THE MORNING TO SOME LONG BOARDING, RAFTING, AND BOARD TRANSFERS IN THE AFTERNOON! NEW STAY PSYCHED ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/gQ_1zX-F_No/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/gQ_1zX-F_No/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/gQ_1zX-F_No/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/yT8vWPNKatpC5KUNNeCaAUnhg78\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"i2hewkFijuY\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-12-04T16:00:08.000Z\",\n        \"channelId\": \"UC_F4Iy5korq2mEWZDQhG07w\",\n        \"title\": \"SURFING PERFECT PIPE &amp; BACKDOOR || SUNRISE SHACK!\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/i2hewkFijuY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/i2hewkFijuY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/i2hewkFijuY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Koa Rothman\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/exxw2qujoMBCF6s6v8geVAlQMgo\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"_Z56nIAQj7Q\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-12-06T07:54:12.000Z\",\n        \"channelId\": \"UChuLeaTGRcfzo0UjL-2qSbQ\",\n        \"title\": \"Taiwan Open of Surfing World Longboard Championships\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/_Z56nIAQj7Q/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/_Z56nIAQj7Q/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/_Z56nIAQj7Q/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"World Surf League\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/yfVam76dXHEx_BigZW-xV7NBxqE\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"W7h-Yho8EB0\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-06-15T15:21:45.000Z\",\n        \"channelId\": \"UCqhnX4jA0A5paNd1v-zEysw\",\n        \"title\": \"GoPro: Top 10 Surf Moments\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/W7h-Yho8EB0/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/W7h-Yho8EB0/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/W7h-Yho8EB0/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"GoPro\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/XWyPkbiV-j7gjEJev9Cknl4DDQ0\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"caj9z17kuyw\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-07T19:00:04.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"SURFING GIANT WAVES IN HAWAII (PIPELINE)\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/caj9z17kuyw/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/caj9z17kuyw/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/caj9z17kuyw/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/_mOBjnA5mb03qReqVQdkZxSdaUQ\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"oci0CqUU5KI\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-21T19:00:04.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"ENDLESS SURF ON THE NORTH SHORE (HAWAII)\",\n        \"description\": \"SURFER AWARDS: HELP US PROVE THAT THE STAY PSYCHED COMMUNITY IS THE BEST VLOG FANS IN THE WORLD! VOTE HERE: ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/oci0CqUU5KI/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/oci0CqUU5KI/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/oci0CqUU5KI/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/OOD6SlB-NumCjkurCLUxN68r25E\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"nkhpGC10OVw\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2017-04-09T17:18:44.000Z\",\n        \"channelId\": \"UCHeaHzQFLElUw__yG3SSzMg\",\n        \"title\": \"World&#39;s best surfing 2017\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/nkhpGC10OVw/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/nkhpGC10OVw/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/nkhpGC10OVw/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Monthly Winners\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/WO5q6dSUt9m8pHRzGlR3fq9xvbA\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"c6yOxWf3A6g\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2017-08-20T07:00:02.000Z\",\n        \"channelId\": \"UC1Ho5YvHCtyReazatbhBowA\",\n        \"title\": \"HOW TO SURF:  7 BEGINNER MISTAKES AND HOW TO FIX THEM\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/c6yOxWf3A6g/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/c6yOxWf3A6g/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/c6yOxWf3A6g/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Stomp It Tutorials\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/akBNz6Ly7vzE5-E5M5tZV6bn5BQ\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"8Bha766qpNw\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-12-03T03:51:46.000Z\",\n        \"channelId\": \"UCnJ0mt5Cgx4ER_LhTijG_4A\",\n        \"title\": \"2019 Vans World Cup of Surfing - Final Day Highlights | Triple Crown of Surfing | VANS\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/8Bha766qpNw/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/8Bha766qpNw/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/8Bha766qpNw/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Vans\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/zaiyqHbBJdyoBqfoY2zz481clSU\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"67QNw2xQlsk\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-01-12T14:00:15.000Z\",\n        \"channelId\": \"UCuZSTHZf3vd7eVehhnotcsg\",\n        \"title\": \"Learn How To Surf In 10 Minutes\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/67QNw2xQlsk/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/67QNw2xQlsk/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/67QNw2xQlsk/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"How to Rip\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/E8GZG_CZfJeaVF75eZYmJHnGe0c\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"rj7xMBxd5iY\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2017-11-12T11:09:52.000Z\",\n        \"channelId\": \"UCiiFGfvlKvX3uzMovO3unaw\",\n        \"title\": \"BIG WAVE SURFING COMPILATION 2017\",\n        \"description\": \"BIG WAVE SURFING COMPILATION 2017 ** REVISED **AMAZING FOOTAGE ** WITH 60-100FT- HUGE SURF Please Subscribe if You Would like to see More ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/rj7xMBxd5iY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/rj7xMBxd5iY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/rj7xMBxd5iY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Absolutely Flawless\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/jd-wJK0NPMZT7aGsDpq-edAZRWk\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"FeNPd168uqg\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-06-22T01:41:06.000Z\",\n        \"channelId\": \"UCJBWZDkunrjWh50Etxdys2g\",\n        \"title\": \"Surfing San Diego Winter 2018-2019 Season (Short Film)\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/FeNPd168uqg/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/FeNPd168uqg/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/FeNPd168uqg/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"SoCal Surfer\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/WznjU0SpaKoMkx-E7Jf-iI4VIgs\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"xU3iCjnlqx8\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-01-16T00:05:33.000Z\",\n        \"channelId\": \"UCKo-NbWOxnxBnU41b-AoKeA\",\n        \"title\": \"The Best Surf Clips of 2018 | SURFER Magazine\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/xU3iCjnlqx8/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/xU3iCjnlqx8/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/xU3iCjnlqx8/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"SURFER\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/QRCpeAtifFokjELuUekSWYVIbtI\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"PBEnhMlnRg0\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-12-01T11:00:04.000Z\",\n        \"channelId\": \"UCzcQOTuXYGuCvTekySb_CeQ\",\n        \"title\": \"Bali Surf Journal - November 2019\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/PBEnhMlnRg0/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/PBEnhMlnRg0/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/PBEnhMlnRg0/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Surfers of Bali\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/VH1h1gqPQ5j194_Te6FEuy-mwwY\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"ZjbJkdQV1XA\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-25T19:00:05.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"RIVER SURFING IN HAWAII (WAIMEA BAY)\",\n        \"description\": \"LET'S PROVE THAT JOBVLOGS HAS THE MOST PSYCHED VIEWERS BY VOTING!!! https://www.surfer.com/surfer-awards/ CHECK OUT MY MERCH: WWW.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/ZjbJkdQV1XA/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/ZjbJkdQV1XA/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/ZjbJkdQV1XA/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Qpy1ZO77GTQNi1QMa1iqS6NgODE\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"vAryEy9QvgU\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-07-08T01:11:29.000Z\",\n        \"channelId\": \"UCJBWZDkunrjWh50Etxdys2g\",\n        \"title\": \"Surfing ENDLESS Waves in Malibu!\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/vAryEy9QvgU/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/vAryEy9QvgU/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/vAryEy9QvgU/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"SoCal Surfer\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/HBBY8t7e5zs0MgO37mJyflDMunA\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"ddYARvl4PgE\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-12-06T08:25:39.000Z\",\n        \"channelId\": \"UCKo-NbWOxnxBnU41b-AoKeA\",\n        \"title\": \"THE 2019 SURFER AWARDS\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/ddYARvl4PgE/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/ddYARvl4PgE/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/ddYARvl4PgE/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"SURFER\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/KLKwu7HjRM7EC2G2mPqropCIm6E\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"2nX5Sjsd5To\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2018-07-09T19:30:02.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"DANGEROUS SEWER DRAIN SURFING! | Jamie O&#39;Brien\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/2nX5Sjsd5To/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/2nX5Sjsd5To/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/2nX5Sjsd5To/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/wfwhWGYsHfq_0CKXxYsmHmvVK6M\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"6GUtd7f1_Xo\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-01-01T20:01:55.000Z\",\n        \"channelId\": \"UCVk7uRN7g-q_uHMaAjHxk8A\",\n        \"title\": \"THE GIRLS OF SURFING XIX\",\n        \"description\": \"The Girls of Surfing 2018 edition Join the page here : https://www.facebook.com/thegirlsofsurfing/ Instagram @jolieoligny Twitter @jolieoligny Inspired by ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/6GUtd7f1_Xo/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/6GUtd7f1_Xo/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/6GUtd7f1_Xo/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"thegirlsofsurfing\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/k1xVqEDOCAGEo5gJVqHfGHm_xaY\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"e0t9RpSje7w\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-08-20T19:00:08.000Z\",\n        \"channelId\": \"UCuwdplPbuTFZj_64d03tSBA\",\n        \"title\": \"Young Thug - Surf ft. Gunna [Official Video]\",\n        \"description\": \"Stream So Much Fun Now! https://youngthug.ffm.to/somuchfun Listen to So Much Fun on Youtube Music: ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/e0t9RpSje7w/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/e0t9RpSje7w/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/e0t9RpSje7w/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Young Thug\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Y3HcmuZG0Dx-64_AoDIlD6UArS0\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"EaZ-uNoMRHw\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-06-03T23:55:59.000Z\",\n        \"channelId\": \"UCJBWZDkunrjWh50Etxdys2g\",\n        \"title\": \"Surfers Catch INSANE Party waves in San Diego!\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/EaZ-uNoMRHw/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/EaZ-uNoMRHw/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/EaZ-uNoMRHw/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"SoCal Surfer\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Y5PFmyzA0s9UXLDwLVRHeeoGCcY\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"JJJtW8tQvB0\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-20T05:18:41.000Z\",\n        \"channelId\": \"UCq650FWunrqAhM6pV5sncCg\",\n        \"title\": \"FINGER SURFING!\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/JJJtW8tQvB0/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/JJJtW8tQvB0/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/JJJtW8tQvB0/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"davidsjones\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/By2tddm9SOnD8bAg7OoeX2niVJU\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"kD7QBPONnlY\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-15T20:45:21.000Z\",\n        \"channelId\": \"UCf5CA0OsvhhU-6AcSjT1oKQ\",\n        \"title\": \"Surfing PERFECT river wave and GNARLY skim wedge !!!\",\n        \"description\": \"Blair Conklin surfs a glassy river wave and the crew scores some gnarly skim wedges in Southern California ! Featured shredders include Johnny Redmond, ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/kD7QBPONnlY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/kD7QBPONnlY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/kD7QBPONnlY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"BEEFS T.V.\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/dNwYM_CRPhW0nFjpEGUcAXSYfPw\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"X9tU8ybzcFs\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2017-07-15T07:03:37.000Z\",\n        \"channelId\": \"UCsG5dkqFUHZO6eY9uOzQqow\",\n        \"title\": \"The Dock\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/X9tU8ybzcFs/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/X9tU8ybzcFs/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/X9tU8ybzcFs/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Stab Magazine\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/yXQOdwn7hCIqAtPOI4sUHBbaNDA\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"-2IlD-x8wvY\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-09-01T17:16:15.000Z\",\n        \"channelId\": \"UCeYue9Nbodzg3T1Nt88E3fg\",\n        \"title\": \"Hawaiian Summer Surfing\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/-2IlD-x8wvY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/-2IlD-x8wvY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/-2IlD-x8wvY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Ho & Pringle Productions\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/search/search_by_keywords_p2.json",
    "content": "{\n  \"kind\": \"youtube#searchListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ALJWUURzLUpxp1uFSnJBAmUIj8g\\\"\",\n  \"nextPageToken\": \"CDIQAA\",\n  \"prevPageToken\": \"CBkQAQ\",\n  \"regionCode\": \"JP\",\n  \"pageInfo\": {\n    \"totalResults\": 1000000,\n    \"resultsPerPage\": 25\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/yXQOdwn7hCIqAtPOI4sUHBbaNDA\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"-2IlD-x8wvY\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-09-01T17:16:15.000Z\",\n        \"channelId\": \"UCeYue9Nbodzg3T1Nt88E3fg\",\n        \"title\": \"Hawaiian Summer Surfing\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/-2IlD-x8wvY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/-2IlD-x8wvY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/-2IlD-x8wvY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Ho & Pringle Productions\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/KG1OvjtEBf5STAnjMIthCoYNgwE\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"q0yVuNANAkY\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-07T19:00:03.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"SCORING FUN SURF IN CALIFORNIA\",\n        \"description\": \"SANTA CRUZ BOMB DROPS, NOVELTY WEDGES, AND OFF TO LOS ANGELES FOR THE WEDGE! WE ARE HAVING TOO MUCH FUN IN CALIFORNIA!\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/q0yVuNANAkY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/q0yVuNANAkY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/q0yVuNANAkY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/i_OOiFOlnCtf_ALcsaAw7Ufo4bg\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"SVDcJK8rSlQ\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-10-24T19:00:02.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"PUMPING SURF IN OCTOBER\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/SVDcJK8rSlQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/SVDcJK8rSlQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/SVDcJK8rSlQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/7LwrprFd6InKtKBmdjzvZahcxSc\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"_azIoZ50zuM\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2013-04-07T23:38:06.000Z\",\n        \"channelId\": \"UCSZy7dboa_o9X8itlpQx7yw\",\n        \"title\": \"Local Style - Best Surf Breaks in Bali Indonesia, Episode 9\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/_azIoZ50zuM/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/_azIoZ50zuM/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/_azIoZ50zuM/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Surf Channel Television Network\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/IuYgBGy2WstW-sQM6YfVO11Tmvc\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"GO1ZC_997MM\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-09-01T22:16:28.000Z\",\n        \"channelId\": \"UCuZSTHZf3vd7eVehhnotcsg\",\n        \"title\": \"The TOP 5 Surfing Mistakes | Learning How To Surf\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/GO1ZC_997MM/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/GO1ZC_997MM/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/GO1ZC_997MM/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"How to Rip\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/3kofrY51480WPJdJyTJxLSbE_bs\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"vk0F8dHo3wU\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2015-10-14T13:45:47.000Z\",\n        \"channelId\": \"UC-Zt7GPzlrPPQexkG9-shPg\",\n        \"title\": \"&quot;Pacific Dreams&quot; A California Surfing Film\",\n        \"description\": \"\\\"Pacific Dreams\\\" is a surfing movie featuring my 2015 footage shot around the beautiful state of California. Buy Merch: ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/vk0F8dHo3wU/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/vk0F8dHo3wU/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/vk0F8dHo3wU/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Surf Rinse Repeat\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/qYSQyC-A4aYQyT_K8sa1LPdT5Ew\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"GHzvMcYqJqk\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-07-18T08:59:33.000Z\",\n        \"channelId\": \"UCuZSTHZf3vd7eVehhnotcsg\",\n        \"title\": \"The MOST Important SURFING Technique Used By The Pro’s\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/GHzvMcYqJqk/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/GHzvMcYqJqk/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/GHzvMcYqJqk/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"How to Rip\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/zc8AwWji78nEFu-q-NDXkNgNKTw\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"w7DoXnRYMUg\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2014-05-14T13:00:04.000Z\",\n        \"channelId\": \"UCblfuW_4rakIf2h6aqANefA\",\n        \"title\": \"Subzero Surfing in Nova Scotia - Sally Stories Season 2 - Ep 5\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/w7DoXnRYMUg/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/w7DoXnRYMUg/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/w7DoXnRYMUg/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Red Bull\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/K9rACPHz4VTQAV-0WRwPs5sOBo0\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"hc0lYWSJ0-0\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2018-11-24T05:48:39.000Z\",\n        \"channelId\": \"UCuZSTHZf3vd7eVehhnotcsg\",\n        \"title\": \"Small Wave Surfing Secrets\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/hc0lYWSJ0-0/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/hc0lYWSJ0-0/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/hc0lYWSJ0-0/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"How to Rip\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/puaW26DfNs0BwdGhQ_kUAQr4Vb0\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"dVr3m9S0cEk\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-08-19T16:55:56.000Z\",\n        \"channelId\": \"UCphTF9wHwhCt-BzIq-s4V-g\",\n        \"title\": \"What If You Tried to Surf a Tsunami?\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/dVr3m9S0cEk/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/dVr3m9S0cEk/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/dVr3m9S0cEk/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"What If\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/j9grghmhodulrxd04LMAYUHGG8g\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"A_0tgAVjQPw\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2018-12-18T03:46:00.000Z\",\n        \"channelId\": \"UCnJ0mt5Cgx4ER_LhTijG_4A\",\n        \"title\": \"2018 Billabong Pipe Masters - Final Day Highlights | Triple Crown of Surfing | VANS\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/A_0tgAVjQPw/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/A_0tgAVjQPw/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/A_0tgAVjQPw/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Vans\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/f4B92xUeptcNLBwHd4cnYEjNyZE\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"eSwisMEtkBg\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2018-09-02T01:56:19.000Z\",\n        \"channelId\": \"UCfn_qdZ1XMLRKIfMhexjooA\",\n        \"title\": \"What Surfing Is Actually Like\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/eSwisMEtkBg/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/eSwisMEtkBg/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/eSwisMEtkBg/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Atua Mo'e\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/dZ7qWwmgtXL4M38OuQz6PyTYomI\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"koidZdKL2HY\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-08-09T19:00:04.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"SURFING IN BALI WITH MY GIRLFRIEND\",\n        \"description\": \"TINA WON THE FIRST HEAT AND IM BACK FOR ANOTHER SHOT BEFORE WE ABSOLUTELY SCORE PERFECT KERAMAS CHECK OUT MY MERCH: ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/koidZdKL2HY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/koidZdKL2HY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/koidZdKL2HY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/PWh9CBJQJTtiT1eLEFdXuTraxUc\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"4sqcVvWBK08\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T18:30:02.000Z\",\n        \"channelId\": \"UCsG5dkqFUHZO6eY9uOzQqow\",\n        \"title\": \"&quot;Go Easy On The Zambezi&quot; River Surfing In Africa With Harry Bryant, Mikey Feb and Dylan Graves\",\n        \"description\": \"\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/4sqcVvWBK08/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/4sqcVvWBK08/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/4sqcVvWBK08/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Stab Magazine\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Xd0Upa2qCNmlJi3cBxzDUa8N_CY\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"At4T3Ujv4xk\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-04-02T21:00:00.000Z\",\n        \"channelId\": \"UCZFhj_r-MjoPCFVUo3E1ZRg\",\n        \"title\": \"10-Year-Old PRO Skater &amp; Surfing PRODIGY | Sky Brown\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/At4T3Ujv4xk/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/At4T3Ujv4xk/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/At4T3Ujv4xk/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Whistle\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/AD7sXFmNIsziect608vZjRHozU8\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"wxBtwCZtDAg\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2018-11-21T01:00:11.000Z\",\n        \"channelId\": \"UCZFhj_r-MjoPCFVUo3E1ZRg\",\n        \"title\": \"13-Year-Old FEARLESS Surfing Prodigy\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/wxBtwCZtDAg/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/wxBtwCZtDAg/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/wxBtwCZtDAg/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Whistle\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/f78GSdxpM_beAxdRfJCNqKNd3ho\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"oSR8irQRdWs\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-03-06T12:23:21.000Z\",\n        \"channelId\": \"UCLtWzMJSuuaTjBYe7IYVpCw\",\n        \"title\": \"Surfing&#39;s most insane 10&#39;s\",\n        \"description\": \"Check out Surfing's greatest airs ever here: https://youtu.be/Y3iogCFIUOo Subscribe for more frequent surf compilations and content. Instagram: slaptv Footage ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/oSR8irQRdWs/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/oSR8irQRdWs/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/oSR8irQRdWs/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"slaptv\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/31svwC9OwSMqzQiED2GkYCUOVa4\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"i2xVrRrxzVE\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-08-19T19:00:04.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"SURFING THE AUSTRALIAN WEDGE\",\n        \"description\": \"CHASING SWELLS STRAIGHT FROM BALI TO AUSTRALIA, SURFING WEDGES WITH THE BOYS, STAYING REALLY PSYCHED, AND TALKING TO SHANE ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/i2xVrRrxzVE/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/i2xVrRrxzVE/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/i2xVrRrxzVE/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/-W0fYvWJUixRPkPGp05-2EWJNUQ\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"IurVzNwneYo\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2017-08-01T00:41:25.000Z\",\n        \"channelId\": \"UCIGO64hbbGBs2o2wa8xXjwA\",\n        \"title\": \"[SURF JAPAN] TOP 6 BEST SURFS SPOTS IN JAPAN\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/IurVzNwneYo/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/IurVzNwneYo/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/IurVzNwneYo/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Yunako TV\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/AHSycI2tynUVfUL7B0qODZCTxCM\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"zQVpzCzQa1U\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2018-09-13T21:38:34.000Z\",\n        \"channelId\": \"UCf5CA0OsvhhU-6AcSjT1oKQ\",\n        \"title\": \"Surfing PERFECT waves at TEXAS Wavepool\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/zQVpzCzQa1U/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/zQVpzCzQa1U/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/zQVpzCzQa1U/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"BEEFS T.V.\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/qPLoN7ozjPU6_u8uY29bI1821ZA\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"wSsjla8BBHA\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-08-22T19:00:08.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"SURFING AUSTRALIAN PIPELINE?\",\n        \"description\": \"CHASING SWELLS STRAIGHT FROM BALI TO AUSTRALIA, SURFING WEDGES WITH THE BOYS, STAYING REALLY PSYCHED, AND TALKING TO SHANE ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/wSsjla8BBHA/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/wSsjla8BBHA/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/wSsjla8BBHA/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/hAvlnbuBu7SPUmCCf9BkV4uG97c\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"BUhJwdet8Rs\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-08-27T18:22:52.000Z\",\n        \"channelId\": \"UCKo-NbWOxnxBnU41b-AoKeA\",\n        \"title\": \"Why Alaska Might Be Surfing’s Greatest Frontier | WITHIN REACH (4K EDITION) | SURFER\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/BUhJwdet8Rs/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/BUhJwdet8Rs/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/BUhJwdet8Rs/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"SURFER\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/U1_e-wKYZrl5wfKA7Z31Jqo3i7s\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"bbMCFIfskd4\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-11T19:00:07.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"ALMOST BROKE MY LEG SURFING! (PIPELINE)\",\n        \"description\": \"SURFING MASSIVE WAVES AT BANZAI PIPELINE TO SURFING LONG BOARD WAVES WITH TINA AND FRIENDS. CHECK OUT MY MERCH: WWW.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/bbMCFIfskd4/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/bbMCFIfskd4/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/bbMCFIfskd4/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/V3raa3z1l54O4nPc_yUcNXonaYk\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"gm7eT0MGt2Y\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2014-04-25T16:00:07.000Z\",\n        \"channelId\": \"UCqhnX4jA0A5paNd1v-zEysw\",\n        \"title\": \"GoPro: Endless Barrels - GoPro of the Winter 2013-14 powered by Surfline\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/gm7eT0MGt2Y/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/gm7eT0MGt2Y/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/gm7eT0MGt2Y/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"GoPro\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/pYFbBUoCwrwiRlGwtYHHdQYunSs\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"y92SwP4odAk\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-01-16T18:57:38.000Z\",\n        \"channelId\": \"UCtinbF-Q-fVthA0qrFQTgXQ\",\n        \"title\": \"Surfing with GREAT WHITE SHARKS at DUNGEONS SOUTH AFRICA\",\n        \"description\": \"Animal Oceans - https://www.animalocean.co.za Dan - https://www.youtube.com/user/DanTheDirector1 Music - HUSBANDS: ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/y92SwP4odAk/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/y92SwP4odAk/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/y92SwP4odAk/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"CaseyNeistat\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/search/search_by_location.json",
    "content": "{\n  \"kind\": \"youtube#searchListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/KRABNKy-TvvaFRB9poJwJHu7_CA\\\"\",\n  \"nextPageToken\": \"CAUQAA\",\n  \"regionCode\": \"US\",\n  \"pageInfo\": {\n    \"totalResults\": 83,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/z-4-8ZS7WBjxdAkQsCYIrkc2Yi4\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"OU4d3O_VZCk\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-02-06T19:00:04.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"SURFING WITH KELLY SLATER (HAWAII)\",\n        \"description\": \"FUN BARRELS, BEATER SPINS, AND GOOD TIMES WITH SOME OF THE REDBULL SKATEBOARD TEAM! STAY PSYCHED MERCH! HTTP://WWW.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/OU4d3O_VZCk/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/OU4d3O_VZCk/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/OU4d3O_VZCk/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/ehUlorWN-KIwVssROIKJNyKAlts\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"R5n6TgvOLgA\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-02-17T19:00:03.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"DREAMY SURF AT BANZAI PIPELINE\",\n        \"description\": \"CRISTAL CLEAR WATERS, NEW SURFBOARD, AND NO CROWD WITH SOME FUN SIZED WAVES... BEST DAY EVER!! CHECK OUT MY MERCH: ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/R5n6TgvOLgA/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/R5n6TgvOLgA/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/R5n6TgvOLgA/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/PYZ4gleO499ovnEt8_WXFyD2ZlA\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"R86-eGa96bQ\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-02-13T19:00:04.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"SURFING WITH 11x WORLD CHAMPION SURFER!! Scored Pipeline, Waimea River &amp; Cleaned Up 300 lbs Of Trash\",\n        \"description\": \"GOOD WEATHER OR BAD WEATHER, WE MAKE SURE WE HAVE THE MOST FUN EVERY SINGLE DAY. FUN SURF AT PIPELINE WITH KELLY SLATER ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/R86-eGa96bQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/R86-eGa96bQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/R86-eGa96bQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/yxmO5YZFScEXiQHvIJyAJc7Ophc\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"dnFxU2mik74\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-02-09T19:00:05.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"JAMIE O&#39;BRIEN NEAR DEATH SURFING PIPELINE (OAHU, HAWAII)\",\n        \"description\": \"SUBSCRIBE: https://www.youtube.com/jamieobrienjob FIND JAMIE ON INSTAGRAM: https://www.instagram.com/WHOISJOB/ EDITOR: ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/dnFxU2mik74/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/dnFxU2mik74/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/dnFxU2mik74/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Q4uSEXp12CwRiMVDqNa7TmsGeHw\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"drQjl1rsj28\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2020-02-20T19:00:04.000Z\",\n        \"channelId\": \"UCo_q6aOlvPH7M-j_XGWVgXg\",\n        \"title\": \"PERFECT BARRELS AT BANZAI PIPELINE  (POINT OF VIEW)\",\n        \"description\": \"WHAT IT REALLY LOOKS LIKE OUT AT BANZAI PIPELINE. BIG, STEEP, INTENSE WAVES, AND YOU GET THE FRONT ROW SEAT! CHECK OUT MY ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/drQjl1rsj28/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/drQjl1rsj28/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/drQjl1rsj28/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Jamie O'Brien\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/search/search_by_mine.json",
    "content": "{\n  \"kind\": \"youtube#searchListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/2d1a6UDYt8tuTka8VkEd_2F0uNM\\\"\",\n  \"nextPageToken\": \"Cib3_fGxJP____9YZnJjZmlob194TQD_Af_-WGZyY2ZpaG9feE0AARACIQVwDsDzTq1QOQAAAADbTg4CSAFQAloLCZ9XfdhCQCTNEANguYajdA==\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/NajmzO_gHVKkR5q0QtF_ia9PnT0\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"JE8xdDp5B8Q\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T02:56:49.000Z\",\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"title\": \"华山日出\",\n        \"description\": \"冷冷的山头\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"ikaros-life\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/AMxXvT-JEdxPv-H_aJmZX28aLQM\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"Xfrcfiho_xM\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T02:51:17.000Z\",\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"title\": \"海上日出\",\n        \"description\": \"美美美\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"ikaros-life\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/search/search_by_related_video.json",
    "content": "{\n  \"kind\": \"youtube#searchListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/Jt-dx_Bx1OzzEBghaPZjhe1AeIc\\\"\",\n  \"nextPageToken\": \"CAUQAA\",\n  \"regionCode\": \"US\",\n  \"pageInfo\": {\n    \"totalResults\": 119,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/egH1J4nEGcCF7cEL3aIwdZWZRvs\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"eIho2S0ZahI\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2014-06-27T14:18:00.000Z\",\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"title\": \"How to speak so that people want to listen | Julian Treasure\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/eIho2S0ZahI/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/eIho2S0ZahI/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/eIho2S0ZahI/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"TED\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/OsttkLOQ3t6OgMhtZA2_80MOA30\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"RcGyVTAoXEU\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2013-09-03T21:19:54.000Z\",\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"title\": \"How to make stress your friend | Kelly McGonigal\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/RcGyVTAoXEU/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/RcGyVTAoXEU/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/RcGyVTAoXEU/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"TED\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/KmhbLy_Yw-hcmuSJ5wF0d3eoufQ\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"VQRjouwKDlU\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2016-10-07T16:20:19.000Z\",\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"title\": \"4 reasons to learn a new language | John McWhorter\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/VQRjouwKDlU/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/VQRjouwKDlU/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/VQRjouwKDlU/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"TED\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/S-MWeTCziM6r5Zr1EynjTodYOhY\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"_v36Vt9GmH8\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2016-05-02T17:33:45.000Z\",\n        \"channelId\": \"UCsT0YIqwnpJCM-mx7-gSA4Q\",\n        \"title\": \"Body Language: The Key to Your Subconscious | Ann Washburn | TEDxIdahoFalls\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/_v36Vt9GmH8/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/_v36Vt9GmH8/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/_v36Vt9GmH8/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"TEDx Talks\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/tr49eHnX6J7gk65hhZfaGoP6kYM\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"9kxL9Cf46VM\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2012-12-12T17:34:13.000Z\",\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"title\": \"A Saudi, an Indian and an Iranian walk into a Qatari bar ... | Maz Jobrani\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/9kxL9Cf46VM/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/9kxL9Cf46VM/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/9kxL9Cf46VM/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"TED\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/search/search_channels.json",
    "content": "{\n  \"kind\": \"youtube#searchListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/CvjFcA434WC9odlJ9qApNTqQE3Y\\\"\",\n  \"nextPageToken\": \"CAUQAA\",\n  \"regionCode\": \"US\",\n  \"pageInfo\": {\n    \"totalResults\": 1000000,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/HjEU7hR_OXoK_1u0-g1CqVS1RH0\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#channel\",\n        \"channelId\": \"UCxRULEz6kS0PMxCzOY25GhQ\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2009-09-02T23:35:38.000Z\",\n        \"channelId\": \"UCxRULEz6kS0PMxCzOY25GhQ\",\n        \"title\": \"NLaFourcadeVEVO\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-yUgxca4sWt0/AAAAAAAAAAI/AAAAAAAAAAA/IE61lBuRSd8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-yUgxca4sWt0/AAAAAAAAAAI/AAAAAAAAAAA/IE61lBuRSd8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-yUgxca4sWt0/AAAAAAAAAAI/AAAAAAAAAAA/IE61lBuRSd8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        },\n        \"channelTitle\": \"NLaFourcadeVEVO\",\n        \"liveBroadcastContent\": \"upcoming\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/1pKCiT2tOMI9S7prcPE4MY-7nq4\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#channel\",\n        \"channelId\": \"UCBGVFobD9nXtTxBdMbDZfnQ\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2007-11-15T15:12:06.000Z\",\n        \"channelId\": \"UCBGVFobD9nXtTxBdMbDZfnQ\",\n        \"title\": \"fritz5139\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-40EYesesZNE/AAAAAAAAAAI/AAAAAAAAAAA/AumRpR60plo/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-40EYesesZNE/AAAAAAAAAAI/AAAAAAAAAAA/AumRpR60plo/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-40EYesesZNE/AAAAAAAAAAI/AAAAAAAAAAA/AumRpR60plo/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        },\n        \"channelTitle\": \"fritz5139\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/6NB-kVFErMEmn8dD_FAEK_O7JBQ\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#channel\",\n        \"channelId\": \"UCHFz3F_wTBHhOMqTTlAeNTA\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2015-12-30T20:42:55.000Z\",\n        \"channelId\": \"UCHFz3F_wTBHhOMqTTlAeNTA\",\n        \"title\": \"The Big Jackpot\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-3G0oiIWgcIc/AAAAAAAAAAI/AAAAAAAAAAA/Y-QZ2wRDjus/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-3G0oiIWgcIc/AAAAAAAAAAI/AAAAAAAAAAA/Y-QZ2wRDjus/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-3G0oiIWgcIc/AAAAAAAAAAI/AAAAAAAAAAA/Y-QZ2wRDjus/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        },\n        \"channelTitle\": \"The Big Jackpot\",\n        \"liveBroadcastContent\": \"upcoming\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/phIwxWP55wQ6k2kQyoysNwCx694\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#channel\",\n        \"channelId\": \"UCiMhD4jzUqG-IgPzUmmytRQ\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2008-07-07T13:35:27.000Z\",\n        \"channelId\": \"UCiMhD4jzUqG-IgPzUmmytRQ\",\n        \"title\": \"Queen Official\",\n        \"description\": \"Welcome to the official Queen channel. Subscribe today for exclusive Queen videos, including live performances, interviews, official videos, behind-the-scenes ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-S1HWgr--7So/AAAAAAAAAAI/AAAAAAAAAAA/lqWNX9pJni8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-S1HWgr--7So/AAAAAAAAAAI/AAAAAAAAAAA/lqWNX9pJni8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-S1HWgr--7So/AAAAAAAAAAI/AAAAAAAAAAA/lqWNX9pJni8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        },\n        \"channelTitle\": \"Queen Official\",\n        \"liveBroadcastContent\": \"upcoming\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/LYJC4LDYx_LfXe6eJLgSPmrOCKc\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#channel\",\n        \"channelId\": \"UCEqbPy-ZcK2gR8X1KEtJwpQ\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2006-09-10T17:13:54.000Z\",\n        \"channelId\": \"UCEqbPy-ZcK2gR8X1KEtJwpQ\",\n        \"title\": \"Robert Morecook\",\n        \"description\": \"Military Music from around the World Musica Militar de todo del mundo Martial Music.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-uv59_RwyuUo/AAAAAAAAAAI/AAAAAAAAAAA/srnldUUxBtk/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-uv59_RwyuUo/AAAAAAAAAAI/AAAAAAAAAAA/srnldUUxBtk/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-uv59_RwyuUo/AAAAAAAAAAI/AAAAAAAAAAA/srnldUUxBtk/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        },\n        \"channelTitle\": \"Robert Morecook\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/search/search_videos_by_channel.json",
    "content": "{\n  \"kind\": \"youtube#searchListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/qDAZEJAlbMKs4-6fpruc8u6toc4\\\"\",\n  \"nextPageToken\": \"CAUQAA\",\n  \"regionCode\": \"US\",\n  \"pageInfo\": {\n    \"totalResults\": 81297,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/N6Cmq8y3KhHdFk-mVd7aFwW_U34\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"LrQWzOkC0XQ\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2018-10-08T22:51:23.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Code Cleanup and Fixing Pub Versioning in Hacker News App (The Boring Flutter Dev Show, Ep. 8.2)\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/LrQWzOkC0XQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/LrQWzOkC0XQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/LrQWzOkC0XQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/R5xfc7-ZMB5vjNr4inKwmNeRuWM\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"lVQ1EKR1v1I\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2011-09-12T20:55:45.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"YouTube&#39;s API and The News\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/lVQ1EKR1v1I/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/lVQ1EKR1v1I/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/lVQ1EKR1v1I/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/hJOHKkFuHJmaRSOr6eukMOQ6QNY\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"Bud7XR8crWw\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2018-10-08T22:47:38.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Adding Caching to the Hacker News App (The Boring Flutter Development Show, Ep. 8.3)\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Bud7XR8crWw/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Bud7XR8crWw/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Bud7XR8crWw/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/9OAtGs7Xc-48ZJrJ20f7sgxOnp4\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"k_NtkiMAC-o\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2013-08-14T18:01:59.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"YouTube Developers Live: Storyful, The News Agency of The Social Media Age\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/k_NtkiMAC-o/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/k_NtkiMAC-o/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/k_NtkiMAC-o/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/LS7osmF_lfU6MCsh0sOh-Cetex4\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"dfweWyVScaI\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2014-09-03T00:53:44.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Breaking News at 1000ms with Patrick Hamann\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/dfweWyVScaI/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/dfweWyVScaI/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/dfweWyVScaI/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/subscriptions/insert_response.json",
    "content": "{\n  \"kind\": \"youtube#subscription\",\n  \"etag\": \"BBbHqFIch0N1EhR1bwn0s3MofFg\",\n  \"id\": \"POsnRIYsMcp1Cghr_Fsh-6uFZRcIHmTKzzByiv9ZAro\",\n  \"snippet\": {\n    \"publishedAt\": \"2022-11-16T11:02:09.19802Z\",\n    \"title\": \"iQIYI 综艺精选\",\n    \"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\",\n    \"resourceId\": {\n      \"kind\": \"youtube#channel\",\n      \"channelId\": \"UCQ6ptCagG3W0Bf4lexvnBEg\"\n    },\n    \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://yt3.ggpht.com/kszyCtmb0Bsjo-Pwp6lhZnAA6wMP8gzqNU9qZOj-b9p2GvwF4yygtPBzuRpmGUPtymByYol9Oj8=s88-c-k-c0x00ffffff-no-rj\"\n      },\n      \"medium\": {\n        \"url\": \"https://yt3.ggpht.com/kszyCtmb0Bsjo-Pwp6lhZnAA6wMP8gzqNU9qZOj-b9p2GvwF4yygtPBzuRpmGUPtymByYol9Oj8=s240-c-k-c0x00ffffff-no-rj\"\n      },\n      \"high\": {\n        \"url\": \"https://yt3.ggpht.com/kszyCtmb0Bsjo-Pwp6lhZnAA6wMP8gzqNU9qZOj-b9p2GvwF4yygtPBzuRpmGUPtymByYol9Oj8=s800-c-k-c0x00ffffff-no-rj\"\n      }\n    }\n  },\n  \"contentDetails\": {\n    \"totalItemCount\": 6986,\n    \"newItemCount\": 0,\n    \"activityType\": \"all\"\n  },\n  \"subscriberSnippet\": {\n    \"title\": \"ikaros data\",\n    \"description\": \"This is a test channel.\",\n    \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s88-c-k-c0x00ffffff-no-rj\"\n      },\n      \"medium\": {\n        \"url\": \"https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s240-c-k-c0x00ffffff-no-rj\"\n      },\n      \"high\": {\n        \"url\": \"https://yt3.ggpht.com/ytc/AMLnZu-fT-p35OYe0OaSzOmrQY8KEjXVHMgPxyj-hV2r=s800-c-k-c0x00ffffff-no-rj\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "testdata/apidata/subscriptions/subscription_zero.json",
    "content": "{\n  \"kind\": \"youtube#subscriptionListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/ewwRz0VbTYpp2EGbOkvZ5M_1mbo\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 0,\n    \"resultsPerPage\": 5\n  },\n  \"items\": []\n}"
  },
  {
    "path": "testdata/apidata/subscriptions/subscriptions_by_channel_p1.json",
    "content": "{\n  \"kind\": \"youtube#subscriptionListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/NtDoQfEOjOm9UE8PH2xuzaVGuko\\\"\",\n  \"nextPageToken\": \"CAUQAA\",\n  \"pageInfo\": {\n    \"totalResults\": 7,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/ah58pA0LZUrj8p8Ay6v9PqipM1E\\\"\",\n      \"id\": \"FMP3Mleijt-52zZDGkHtR5KhwkvCcdQKWWWIA1j5eGc\",\n      \"snippet\": {\n        \"publishedAt\": \"2011-11-11T14:00:19.000Z\",\n        \"title\": \"TEDx Talks\",\n        \"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.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCsT0YIqwnpJCM-mx7-gSA4Q\"\n        },\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/5Fb7lQjSSevEAy86E2VxNZ43Kzg\\\"\",\n      \"id\": \"FMP3Mleijt_ZKvy5M-HhRlsqI4wXY7VmP5g8lvmRhVU\",\n      \"snippet\": {\n        \"publishedAt\": \"2016-11-17T16:24:18.000Z\",\n        \"title\": \"TED Residency\",\n        \"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.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCtC8aQzdEHAmuw8YvtH1CcQ\"\n        },\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/sr4uYsDNIOJdZ3aURvsOrmYGrCA\\\"\",\n      \"id\": \"FMP3Mleijt-52zZDGkHtRypBo_RsBLYwvw2DbDoGUl8\",\n      \"snippet\": {\n        \"publishedAt\": \"2013-03-04T18:00:00.000Z\",\n        \"title\": \"TED-Ed\",\n        \"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/\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCsooa4yRKGN_zEE8iknghZA\"\n        },\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-mnEpZE0uuus/AAAAAAAAAAI/AAAAAAAAAAA/SM5q4mSZgq4/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-mnEpZE0uuus/AAAAAAAAAAI/AAAAAAAAAAA/SM5q4mSZgq4/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-mnEpZE0uuus/AAAAAAAAAAI/AAAAAAAAAAA/SM5q4mSZgq4/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/UFZvm6H-6qrPRC_qJSp4vk5LWFc\\\"\",\n      \"id\": \"FMP3Mleijt-52zZDGkHtR9rgHNN_tQoAYW8hkbgy-r4\",\n      \"snippet\": {\n        \"publishedAt\": \"2011-11-16T22:33:47.000Z\",\n        \"title\": \"TEDxYouth\",\n        \"description\": \"\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UC-yTB2bUcin9mmah36sXiYA\"\n        },\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-yJAJEG_PrHk/AAAAAAAAAAI/AAAAAAAAAAA/nHtSZ9elZkM/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-yJAJEG_PrHk/AAAAAAAAAAI/AAAAAAAAAAA/nHtSZ9elZkM/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-yJAJEG_PrHk/AAAAAAAAAAI/AAAAAAAAAAA/nHtSZ9elZkM/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZAGz9909mfKsIPY3jbzi6Mw9rO4\\\"\",\n      \"id\": \"FMP3Mleijt__etnGmWHe6HXi2qefpnvN-pKf2M_P728\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-11-04T19:27:35.615Z\",\n        \"title\": \"TED-Ed Educator Talks\",\n        \"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.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UC9k9wQAp0SrWYZVORZnPIlg\"\n        },\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-Tn5RxuSSd5w/AAAAAAAAAAI/AAAAAAAAAAA/jwPzKVK1pcA/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-Tn5RxuSSd5w/AAAAAAAAAAI/AAAAAAAAAAA/jwPzKVK1pcA/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-Tn5RxuSSd5w/AAAAAAAAAAI/AAAAAAAAAAA/jwPzKVK1pcA/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/subscriptions/subscriptions_by_channel_p2.json",
    "content": "{\n  \"kind\": \"youtube#subscriptionListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/XhiVa6ArhD2_-Lydooagc6Tq7oc\\\"\",\n  \"prevPageToken\": \"CAUQAQ\",\n  \"pageInfo\": {\n    \"totalResults\": 7,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/tiNsWzC4jgYHHwZWw2qeoOgWpvY\\\"\",\n      \"id\": \"FMP3Mleijt-52zZDGkHtRx2qxyD4FDSpFDSSTUlt8Hg\",\n      \"snippet\": {\n        \"publishedAt\": \"2011-11-16T22:34:15.000Z\",\n        \"title\": \"TED Fellow\",\n        \"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\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCBjBZmguQzn6WCYR7DQykLw\"\n        },\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-aL55wuJJvNw/AAAAAAAAAAI/AAAAAAAAAAA/B2mD3TD72q8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-aL55wuJJvNw/AAAAAAAAAAI/AAAAAAAAAAA/B2mD3TD72q8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-aL55wuJJvNw/AAAAAAAAAAI/AAAAAAAAAAA/B2mD3TD72q8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/1P2EiA0I1WFh6jFbN-sYeZoa_rc\\\"\",\n      \"id\": \"FMP3Mleijt-52zZDGkHtR907Ony4G2X5nk8s-zEHODE\",\n      \"snippet\": {\n        \"publishedAt\": \"2013-03-20T15:40:41.000Z\",\n        \"title\": \"TEDPartners\",\n        \"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/\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCDAdYdnCDt9zx3p3e_78lEQ\"\n        },\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-c16Ofh8MU2U/AAAAAAAAAAI/AAAAAAAAAAA/8PIr4YGwkrQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-c16Ofh8MU2U/AAAAAAAAAAI/AAAAAAAAAAA/8PIr4YGwkrQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-c16Ofh8MU2U/AAAAAAAAAAI/AAAAAAAAAAA/8PIr4YGwkrQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/subscriptions/subscriptions_by_channel_with_filter.json",
    "content": "{\n  \"kind\": \"youtube#subscriptionListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/Bpz_bu12-YiX5_pAavOTWX7GtDQ\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/ah58pA0LZUrj8p8Ay6v9PqipM1E\\\"\",\n      \"id\": \"FMP3Mleijt-52zZDGkHtR5KhwkvCcdQKWWWIA1j5eGc\",\n      \"snippet\": {\n        \"publishedAt\": \"2011-11-11T14:00:19.000Z\",\n        \"title\": \"TEDx Talks\",\n        \"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.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCsT0YIqwnpJCM-mx7-gSA4Q\"\n        },\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-yjj95JJQEm8/AAAAAAAAAAI/AAAAAAAAAAA/qkyW_xNpwSo/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/5Fb7lQjSSevEAy86E2VxNZ43Kzg\\\"\",\n      \"id\": \"FMP3Mleijt_ZKvy5M-HhRlsqI4wXY7VmP5g8lvmRhVU\",\n      \"snippet\": {\n        \"publishedAt\": \"2016-11-17T16:24:18.000Z\",\n        \"title\": \"TED Residency\",\n        \"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.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCtC8aQzdEHAmuw8YvtH1CcQ\"\n        },\n        \"channelId\": \"UCAuUUnT6oDeKwE6v1NGQxug\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-5Ccn68kZqtE/AAAAAAAAAAI/AAAAAAAAAAA/FcE2mH3Fwww/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/subscriptions/subscriptions_by_id.json",
    "content": "{\n  \"kind\": \"youtube#subscriptionListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/USyhytrL1qAH8AxBqW22EUor8kw\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/VIabsyP8MBhapi7K0fjjRX5bM2U\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-09-11T11:35:04.568Z\",\n        \"title\": \"PyCon 2015\",\n        \"description\": \"\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCgxzjK6GuOHVKR_08TT4hJQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/pYbP9RYZzTnefaJtv-B2uQwsR4A\\\"\",\n      \"id\": \"zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T03:00:56.380Z\",\n        \"title\": \"ikaros-life\",\n        \"description\": \"This is a test channel.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/subscriptions/subscriptions_by_mine_filter.json",
    "content": "{\n  \"kind\": \"youtube#subscriptionListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/VWi2sv7EuSXCkMveaGb_XUpimn4\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 10\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/UvJJpKjvHCalu8cZNUg40ji9hO4\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCBzrqBvZj94YvFZOGA9x6NuY\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-08-23T08:49:39.958Z\",\n        \"title\": \"Google Developers\",\n        \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/pYbP9RYZzTnefaJtv-B2uQwsR4A\\\"\",\n      \"id\": \"zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T03:00:56.380Z\",\n        \"title\": \"ikaros-life\",\n        \"description\": \"This is a test channel.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/subscriptions/subscriptions_by_mine_p1.json",
    "content": "{\n  \"kind\": \"youtube#subscriptionListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/U5b9PTEHoZSQJmnmfpQ8uv2uVUY\\\"\",\n  \"nextPageToken\": \"CAoQAA\",\n  \"pageInfo\": {\n    \"totalResults\": 16,\n    \"resultsPerPage\": 10\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/UvJJpKjvHCalu8cZNUg40ji9hO4\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCBzrqBvZj94YvFZOGA9x6NuY\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-08-23T08:49:39.958Z\",\n        \"title\": \"Google Developers\",\n        \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-Fgp8KFpgQqE/AAAAAAAAAAI/AAAAAAAAAAA/Wyh1vV5Up0I/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/3trn0EfS-9Z6JA1nFmKUAurRyvs\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCB-o3EGr8v1XwT0dqravfvQQ\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-03-14T08:12:41.903Z\",\n        \"title\": \"Hua Hua\",\n        \"description\": \"huahua leetcode\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UC5xDNEcvb1vgw3lE21Ack2Q\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-X3Qmupq3CEM/AAAAAAAAAAI/AAAAAAAAAAA/NL2uBxl6-Rs/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-X3Qmupq3CEM/AAAAAAAAAAI/AAAAAAAAAAA/NL2uBxl6-Rs/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-X3Qmupq3CEM/AAAAAAAAAAI/AAAAAAAAAAA/NL2uBxl6-Rs/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/pYbP9RYZzTnefaJtv-B2uQwsR4A\\\"\",\n      \"id\": \"zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T03:00:56.380Z\",\n        \"title\": \"ikaros-life\",\n        \"description\": \"This is a test channel.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/haI133x3uuikRTFb40awqRK8EFk\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCB6lHuD0nnzgzuZ6TNDS9yAg\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-05-29T16:24:05.203Z\",\n        \"title\": \"JC狗魚\",\n        \"description\": \"My name is Dofi.\\nWelcome to my channel :D\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UC3X3vJLnMH1SaH2qFqFySVQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-Js7rJmRlfcY/AAAAAAAAAAI/AAAAAAAAAAA/pUSyWgcgKHg/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-Js7rJmRlfcY/AAAAAAAAAAI/AAAAAAAAAAA/pUSyWgcgKHg/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-Js7rJmRlfcY/AAAAAAAAAAI/AAAAAAAAAAA/pUSyWgcgKHg/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/d_cRIQsfQRumM5xTJFPl4oW9cgc\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCB1GDJbXiW2g3mMAB51HqwRo\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-01-19T09:27:40.656Z\",\n        \"title\": \"JP傑劈\",\n        \"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 ---\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UChsUVRskM42-wS1nLKd79RQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-EIrymuDbRaA/AAAAAAAAAAI/AAAAAAAAAAA/CSN-M6REJAQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-EIrymuDbRaA/AAAAAAAAAAI/AAAAAAAAAAA/CSN-M6REJAQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-EIrymuDbRaA/AAAAAAAAAAI/AAAAAAAAAAA/CSN-M6REJAQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/0SoEv7zYSOxgLjbYKlwMg57XI5w\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCB5anKv2QRa1fCDUiUZU8xbc\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-05-18T09:39:14.951Z\",\n        \"title\": \"moco\",\n        \"description\": \"ヽ(✿ﾟ▽ﾟ)ノ \\n謝謝你點進來我的頻道,我是Moco.\\n用閒暇之餘偶爾拍拍影片,現實生活中是一枚、一隻、一個努力苦命的上班族?\\n歡迎逛逛我的影片,如果你肯留下足跡那我會更高興ww\\n真的非常歡迎你在影片下方留下意見或是閒聊也行的唷 ♥(´∀` )人\\n\\n(๑• . •๑) 如果你覺得我還不錯,歡迎留下你的訂閱唷\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCLW-5M0C7ELByxDc0R_buNA\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-Pgr0igo62-A/AAAAAAAAAAI/AAAAAAAAAAA/YVClvG-F96U/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-Pgr0igo62-A/AAAAAAAAAAI/AAAAAAAAAAA/YVClvG-F96U/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-Pgr0igo62-A/AAAAAAAAAAI/AAAAAAAAAAA/YVClvG-F96U/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZN8TnM_SWw9N7ydHnSPYYPvaziQ\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCBwtJ-Aho6DZeutqZiP4Q79Q\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-12-25T09:12:18.265Z\",\n        \"title\": \"Next Day Video\",\n        \"description\": \"\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCQ7dFBzZGlBvtU2hCecsBBg\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/VIabsyP8MBhapi7K0fjjRX5bM2U\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-09-11T11:35:04.568Z\",\n        \"title\": \"PyCon 2015\",\n        \"description\": \"\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCgxzjK6GuOHVKR_08TT4hJQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/f-6302NaKYDgbG-CAYLW8arJosQ\\\"\",\n      \"id\": \"zqShTXi-2-S50Nc0aJJ6zfe6vF6lHl-Vpk5bYUpybWc\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-09-11T11:35:08.083Z\",\n        \"title\": \"PyCon 2016\",\n        \"description\": \"\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCwTD5zJbsQGJN75MwbykYNw\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-OpJADPm8uMw/AAAAAAAAAAI/AAAAAAAAAAA/OB26ecvpcC8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-OpJADPm8uMw/AAAAAAAAAAI/AAAAAAAAAAA/OB26ecvpcC8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-OpJADPm8uMw/AAAAAAAAAAI/AAAAAAAAAAA/OB26ecvpcC8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/subscriptions/subscriptions_by_mine_p2.json",
    "content": "{\n  \"kind\": \"youtube#subscriptionListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/VTrpIDMd19kB85U38VXsGy5SZ4Q\\\"\",\n  \"prevPageToken\": \"CAoQAQ\",\n  \"pageInfo\": {\n    \"totalResults\": 16,\n    \"resultsPerPage\": 10\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/Y5O3JqndJlfaSyZhUujH5416cis\\\"\",\n      \"id\": \"zqShTXi-2-S50Nc0aJJ6zUmRLF6uD9ENBVmwRGs4Gms\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-08-03T05:42:47.580Z\",\n        \"title\": \"PyCon 2017\",\n        \"description\": \"PyCon 2017 Conference tutorials and talks (https://us.pycon.org/2017/).\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCrJhliKNQ8g0qoE_zvL8eVg\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-_nxlsgdzrYk/AAAAAAAAAAI/AAAAAAAAAAA/QK6pKl5EMIU/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-_nxlsgdzrYk/AAAAAAAAAAI/AAAAAAAAAAA/QK6pKl5EMIU/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-_nxlsgdzrYk/AAAAAAAAAAI/AAAAAAAAAAA/QK6pKl5EMIU/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/fkjXXxpircl7g4cT13YB6rICLpM\\\"\",\n      \"id\": \"zqShTXi-2-S50Nc0aJJ6zcF_SE2B9QIeS7jGEhCPHtA\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-09-11T11:35:09.217Z\",\n        \"title\": \"PyCon 2018\",\n        \"description\": \"\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCsX05-2sVSH7Nx3zuk3NYuQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-p7EsXAUc0No/AAAAAAAAAAI/AAAAAAAAAAA/DYVdKW67VVg/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-p7EsXAUc0No/AAAAAAAAAAI/AAAAAAAAAAA/DYVdKW67VVg/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-p7EsXAUc0No/AAAAAAAAAAI/AAAAAAAAAAA/DYVdKW67VVg/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/f_Y7vGxhlCLazdbPfhCUaJnO2Js\\\"\",\n      \"id\": \"zqShTXi-2-Rya5uUxEp3ZpfEZoPHGpH2MBMMdN1Yl9Y\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-28T01:19:42.921Z\",\n        \"title\": \"PyCon 2019\",\n        \"description\": \"\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCxs2IIVXaEHHA4BtTiWZ2mQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/wa29z1XTmr6G-xu3deE6-ncNstU\\\"\",\n      \"id\": \"zqShTXi-2-S50Nc0aJJ6zWOFTjfx4UQx5_WL63MoVzo\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-06-20T18:17:55.464Z\",\n        \"title\": \"媛媛\",\n        \"description\": \"哈囉大家好我是媛媛\\n我很喜歡玩遊戲和分享遊戲過程\\n希望大家會喜歡我的影片唷\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UC9A1upZ6Rk7Un-iHfGsW5xA\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-0Rp5ka2uQuI/AAAAAAAAAAI/AAAAAAAAAAA/0Mh8KYtyuOE/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-0Rp5ka2uQuI/AAAAAAAAAAI/AAAAAAAAAAA/0Mh8KYtyuOE/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-0Rp5ka2uQuI/AAAAAAAAAAI/AAAAAAAAAAA/0Mh8KYtyuOE/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/r61YOaAgy9R8JJbtbhyS6CC6_4E\\\"\",\n      \"id\": \"zqShTXi-2-S50Nc0aJJ6zdHSZbM7XNml9y9B3V6WQ9A\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-06-11T01:42:48.406Z\",\n        \"title\": \"李永乐老师\",\n        \"description\": \"欢迎关注我的微信公众号“李永乐老师”，上面有超多文字版科普内容和中学视频课程哦\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCSs4A6HYKmHA2MG_0z-F0xw\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/rOQa6yHDQP-fdGrilGJGCnKHzVA\\\"\",\n      \"id\": \"zqShTXi-2-S50Nc0aJJ6zVxXxmD5w9LtQOiqnv5ZnvI\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-05-18T04:06:37.795Z\",\n        \"title\": \"歐拉\",\n        \"description\": \"各位觀眾大家好，我是歐拉~~高音\\n我超級用心的去錄製以及剪輯每一集\\n如果你們喜歡我的影片，一定要記得訂閱我和繼續支持我哟\\n斗內歐拉拍更優質的影片：https://goo.gl/cLEy9f\\n工商合作請洽詢:olaolaola1014@gmail.com\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCPRuZhEnHRXt8_aHcm-Hl3Q\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-usnp0F0jA1o/AAAAAAAAAAI/AAAAAAAAAAA/pDEGoy1EEvc/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-usnp0F0jA1o/AAAAAAAAAAI/AAAAAAAAAAA/pDEGoy1EEvc/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-usnp0F0jA1o/AAAAAAAAAAI/AAAAAAAAAAA/pDEGoy1EEvc/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/apidata/user_profile.json",
    "content": "{\n  \"family_name\": \"liu\",\n  \"name\": \"kun liu\",\n  \"picture\": \"https://lh3.googleusercontent.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAACY/1E9uN31I7cE/photo.jpg\",\n  \"locale\": \"zh-CN\",\n  \"given_name\": \"kun\",\n  \"id\": \"12345678910\"\n}"
  },
  {
    "path": "testdata/apidata/videos/get_rating_response.json",
    "content": "{\n  \"kind\": \"youtube#videoGetRatingResponse\",\n  \"etag\": \"jHmA6WPghQxwUKfIGg5LVYotT3Y\",\n  \"items\": [\n    {\n      \"videoId\": \"D-lhorsDlUQ\",\n      \"rating\": \"none\"\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/videos/insert_response.json",
    "content": "{\n  \"kind\": \"youtube#video\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/dbCtFPFQrd6OMTnWAYrcpZDPai0\\\"\",\n  \"id\": \"D-lhorsDlUQ\",\n  \"snippet\": {\n    \"publishedAt\": \"2019-03-21T20:37:49.000Z\",\n    \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n    \"title\": \"What are Actions on Google (Assistant on Air)\",\n    \"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\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n        \"width\": 120,\n        \"height\": 90\n      },\n      \"medium\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n        \"width\": 320,\n        \"height\": 180\n      },\n      \"high\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n        \"width\": 480,\n        \"height\": 360\n      },\n      \"standard\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n        \"width\": 640,\n        \"height\": 480\n      },\n      \"maxres\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n        \"width\": 1280,\n        \"height\": 720\n      }\n    },\n    \"channelTitle\": \"Google Developers\",\n    \"tags\": [\n      \"Google\",\n      \"developers\",\n      \"aog\",\n      \"Actions on Google\",\n      \"Assistant\",\n      \"Google Assistant\",\n      \"actions\",\n      \"google home\",\n      \"actions on google\",\n      \"google assistant developers\",\n      \"google assistant sdk\",\n      \"Actions on google developers\",\n      \"smarthome developers\",\n      \"common terminology\",\n      \"custom action on google\",\n      \"google assistant in your app\",\n      \"add google assistant\",\n      \"assistant on air\",\n      \"how to use google assistant on air\",\n      \"Actions on Google how to\"\n    ],\n    \"categoryId\": \"28\",\n    \"liveBroadcastContent\": \"none\",\n    \"defaultLanguage\": \"en\",\n    \"localized\": {\n      \"title\": \"What are Actions on Google (Assistant on Air)\",\n      \"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\"\n    },\n    \"defaultAudioLanguage\": \"en\"\n  },\n  \"player\": {\n    \"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\"\n  }\n}"
  },
  {
    "path": "testdata/apidata/videos/videos_chart_paged_1.json",
    "content": "{\n  \"kind\": \"youtube#videoListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/rzuXDNleIBUJYgD8CQtVBDeS0mU\\\"\",\n  \"nextPageToken\": \"CAUQAA\",\n  \"pageInfo\": {\n    \"totalResults\": 8,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/lPIldyZ8_QoOFUPkRMujFc1zoFA\\\"\",\n      \"id\": \"hDeuSfo_Ys0\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T20:00:11.000Z\",\n        \"channelId\": \"UCWwWOFsW68TqXE-HZLC3WIA\",\n        \"title\": \"GIDDY UP ( OFFICIAL MUSIC VIDEO )\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/hDeuSfo_Ys0/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/hDeuSfo_Ys0/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/hDeuSfo_Ys0/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/hDeuSfo_Ys0/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/hDeuSfo_Ys0/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"The ACE Family\",\n        \"tags\": [\n          \"the ace family giddy up\",\n          \"ace family giddy up\",\n          \"ace family music\",\n          \"ace family song\",\n          \"giddy up\",\n          \"giddy up song\",\n          \"giddy up ace family\",\n          \"new song giddy up\",\n          \"giddy up challenge\",\n          \"giddy up music video\",\n          \"the ace family\",\n          \"ace family\"\n        ],\n        \"categoryId\": \"22\",\n        \"liveBroadcastContent\": \"none\",\n        \"localized\": {\n          \"title\": \"GIDDY UP ( OFFICIAL MUSIC VIDEO )\",\n          \"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\"\n        }\n      },\n      \"player\": {\n        \"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\"\n      }\n    },\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/3jRkViNzm-GC4pj_ViD7TIB1ggQ\\\"\",\n      \"id\": \"Ukd8dJNBvOA\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T18:20:46.000Z\",\n        \"channelId\": \"UC5LGPvoUOfwcLi4Ck8LiR4A\",\n        \"title\": \"TRAPPED WITH THE PRINCE FAMILY REALITY SHOW TRAILER\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Ukd8dJNBvOA/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Ukd8dJNBvOA/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Ukd8dJNBvOA/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/Ukd8dJNBvOA/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/Ukd8dJNBvOA/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"THE PRINCE FAMILY\",\n        \"tags\": [\n          \"The Prince Family\",\n          \"The Prince Family Vlogs\",\n          \"The Prince Family Pranks\",\n          \"Damien & Biannca\",\n          \"Damien Prince\",\n          \"Biannca Prince\",\n          \"Biannca\",\n          \"Damien\",\n          \"the prince family reality show\",\n          \"the prince family reality show 2019\",\n          \"reality\",\n          \"reality house\",\n          \"reality tv\",\n          \"youtube originals\",\n          \"youtube original series\",\n          \"d&b nation reality show\",\n          \"d&b nation\",\n          \"the ace family\",\n          \"faze rug\",\n          \"cj so cool\",\n          \"the labrant family\",\n          \"trapped\",\n          \"trapped with the prince family\",\n          \"trapped with the prince family reality show trailer\",\n          \"trailer\"\n        ],\n        \"categoryId\": \"22\",\n        \"liveBroadcastContent\": \"none\",\n        \"defaultLanguage\": \"en\",\n        \"localized\": {\n          \"title\": \"TRAPPED WITH THE PRINCE FAMILY REALITY SHOW TRAILER\",\n          \"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\"\n        },\n        \"defaultAudioLanguage\": \"en-US\"\n      },\n      \"player\": {\n        \"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\"\n      }\n    },\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/7ztVZ5Pnpj4PAv2sa37S_OQfckE\\\"\",\n      \"id\": \"Pdgk3ERKdug\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-28T14:46:03.000Z\",\n        \"channelId\": \"UCkd0_tpDvgnBqfhUzM7Q0og\",\n        \"title\": \"A Holiday Reunion – Xfinity 2019\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Pdgk3ERKdug/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Pdgk3ERKdug/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Pdgk3ERKdug/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/Pdgk3ERKdug/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/Pdgk3ERKdug/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Xfinity\",\n        \"tags\": [\n          \"E.T.\",\n          \"holiday\",\n          \"Christmas\",\n          \"Spielberg\",\n          \"Xfinity\",\n          \"extraterrestrial\",\n          \"Xfinity X1\",\n          \"Xfinity Internet\",\n          \"Xfinity Stream\",\n          \"Xfinity Home\",\n          \"Xfinity Voice\",\n          \"Xfinity Commercials\",\n          \"Commercials\",\n          \"Henry Thomas\",\n          \"E.T. Phone Home\",\n          \"Xfinity Voice Remote\",\n          \"Holiday Movies\",\n          \"Voice Command\",\n          \"snowman\",\n          \"TV\",\n          \"Comcast\",\n          \"Elliot\",\n          \"Lance Acord\"\n        ],\n        \"categoryId\": \"24\",\n        \"liveBroadcastContent\": \"none\",\n        \"localized\": {\n          \"title\": \"A Holiday Reunion – Xfinity 2019\",\n          \"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\"\n        },\n        \"defaultAudioLanguage\": \"en\"\n      },\n      \"player\": {\n        \"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\"\n      }\n    },\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/MJoV_9qVK_qh6ZsU1O3tkxrwEM4\\\"\",\n      \"id\": \"QmYNmNHrtCQ\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T22:15:38.000Z\",\n        \"channelId\": \"UCuVHOs0H5hvAHGr8O4yIBNQ\",\n        \"title\": \"Black Friday Haul 2019! Niki and Gabi\",\n        \"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!\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/QmYNmNHrtCQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/QmYNmNHrtCQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/QmYNmNHrtCQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/QmYNmNHrtCQ/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/QmYNmNHrtCQ/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Niki and Gabi\",\n        \"tags\": [\n          \"niki and gabi\",\n          \"black friday haul\",\n          \"black friday haul 2019\",\n          \"black friday shopping\",\n          \"black friday\",\n          \"shopping\",\n          \"haul\",\n          \"giveaway\",\n          \"twinter giveaway\",\n          \"niki and gabi haul\",\n          \"aerie\",\n          \"bath and body works\",\n          \"victorias secret\",\n          \"lush\",\n          \"ulta\"\n        ],\n        \"categoryId\": \"26\",\n        \"liveBroadcastContent\": \"none\",\n        \"localized\": {\n          \"title\": \"Black Friday Haul 2019! Niki and Gabi\",\n          \"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!\"\n        },\n        \"defaultAudioLanguage\": \"en\"\n      },\n      \"player\": {\n        \"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\"\n      }\n    },\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/wNjkzpdYbrOsNIN9HtcKYlV-9ZA\\\"\",\n      \"id\": \"X1jMMFOqxEw\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-28T21:00:01.000Z\",\n        \"channelId\": \"UCX6OQ3DkcsbYNE6H8uQQuVA\",\n        \"title\": \"$50,000 Game Of Extreme Hide And Seek - Challenge\",\n        \"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--------------------------------------------------------------------\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/X1jMMFOqxEw/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/X1jMMFOqxEw/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/X1jMMFOqxEw/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/X1jMMFOqxEw/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/X1jMMFOqxEw/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"MrBeast\",\n        \"categoryId\": \"24\",\n        \"liveBroadcastContent\": \"none\",\n        \"localized\": {\n          \"title\": \"$50,000 Game Of Extreme Hide And Seek - Challenge\",\n          \"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--------------------------------------------------------------------\"\n        },\n        \"defaultAudioLanguage\": \"en\"\n      },\n      \"player\": {\n        \"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\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/videos/videos_chart_paged_2.json",
    "content": "{\n  \"kind\": \"youtube#videoListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Ef75aANPGrASduOASo4wDAnehrs\\\"\",\n  \"prevPageToken\": \"CAUQAQ\",\n  \"pageInfo\": {\n    \"totalResults\": 8,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/VXWgiTAYvxnCZnyGoY0XGNQSb0Q\\\"\",\n      \"id\": \"MKM90u7pf3U\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-28T17:00:11.000Z\",\n        \"channelId\": \"UCs6eXM7s8Vl5WcECcRHc2qQ\",\n        \"title\": \"Kanye West - Closed On Sunday\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/MKM90u7pf3U/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/MKM90u7pf3U/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/MKM90u7pf3U/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/MKM90u7pf3U/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/MKM90u7pf3U/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Kanye West\",\n        \"tags\": [\n          \"kanye\",\n          \"kanyewest\",\n          \"kanye west\",\n          \"jesus is king\",\n          \"jesusisking\",\n          \"JIK\",\n          \"jesus\",\n          \"god\",\n          \"gospel\",\n          \"christ\",\n          \"church\",\n          \"closed on sunday\",\n          \"closedonsunday\",\n          \"chickfila\",\n          \"Chick Fil A\",\n          \"Chic Fil A\",\n          \"kim kardashian\",\n          \"kim k\",\n          \"kris jenner\",\n          \"kourtney kardashian\",\n          \"north west\",\n          \"chicago\",\n          \"saint\",\n          \"the wests\",\n          \"thanksgiving\"\n        ],\n        \"categoryId\": \"22\",\n        \"liveBroadcastContent\": \"none\",\n        \"localized\": {\n          \"title\": \"Kanye West - Closed On Sunday\",\n          \"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\"\n        },\n        \"defaultAudioLanguage\": \"zh-Hans\"\n      },\n      \"player\": {\n        \"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\"\n      }\n    },\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Ob11Wh0zRIrMStSut6dFK6iC9ds\\\"\",\n      \"id\": \"4MK8usgnvfo\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T18:30:01.000Z\",\n        \"channelId\": \"UCi9cDo6239RAzPpBZO9y5SA\",\n        \"title\": \"I'm Dating a Celebrity?! | Lele Pons & Juanpa Zurita\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/4MK8usgnvfo/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/4MK8usgnvfo/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/4MK8usgnvfo/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/4MK8usgnvfo/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/4MK8usgnvfo/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Lele Pons\",\n        \"tags\": [\n          \"I'm Dating a Celebrity?! | Lele Pons & Juanpa Zurita\",\n          \"dating a celeb\",\n          \"dating\",\n          \"lele pons\",\n          \"lelepons\",\n          \"lele\",\n          \"pons\",\n          \"juanpa\",\n          \"zurita\",\n          \"juanpazurita\",\n          \"lele boyfriend\",\n          \"eljuanpazurita\",\n          \"shots studios\",\n          \"mindie\",\n          \"tarte\",\n          \"check out my new collaboration\",\n          \"meet our kids\",\n          \"how to make a friend in 10 hours\",\n          \"hannah stocking\",\n          \"hannahstocking\",\n          \"rudy mancuso\",\n          \"anwar\",\n          \"jibawi\",\n          \"anwarjibawi\",\n          \"rudy\",\n          \"mancuso\",\n          \"los puti\"\n        ],\n        \"categoryId\": \"23\",\n        \"liveBroadcastContent\": \"none\",\n        \"localized\": {\n          \"title\": \"I'm Dating a Celebrity?! | Lele Pons & Juanpa Zurita\",\n          \"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\"\n        },\n        \"defaultAudioLanguage\": \"en\"\n      },\n      \"player\": {\n        \"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\"\n      }\n    },\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/6p6atbY5fVUJcaxj9N8Pwa47fFc\\\"\",\n      \"id\": \"h107pApTY84\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T20:00:34.000Z\",\n        \"channelId\": \"UCBlbxksRa-KRSEKLi6foxjQ\",\n        \"title\": \"Customizing 8 Apple Watches⌚️💦Then Giving Them Away!!\",\n        \"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_\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/h107pApTY84/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/h107pApTY84/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/h107pApTY84/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/h107pApTY84/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/h107pApTY84/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"MARKO\",\n        \"tags\": [\n          \"MARKO\",\n          \"MatTV\",\n          \"Custom\",\n          \"Shoes\",\n          \"Sneakers\",\n          \"Vans\",\n          \"Air Force 1's\",\n          \"marko\",\n          \"custom vans\",\n          \"copic\",\n          \"pewdiepie\",\n          \"PewdiePie\",\n          \"custom shoes\",\n          \"drawing\",\n          \"ZHC\",\n          \"custom sharpie\",\n          \"sharpie shoes\",\n          \"Hydro Dip\",\n          \"HYDRO\",\n          \"VANS\",\n          \"Spray paint\",\n          \"hydro dipping vans\",\n          \"hydro dipping shoes\",\n          \"hydro dip shoes\",\n          \"hydro flask\",\n          \"custom hydro flask\",\n          \"painting hydro flask\",\n          \"iphone\",\n          \"iphone 11\",\n          \"iphone 11 pro max\",\n          \"custom iphone 11\",\n          \"custom phone\",\n          \"custom iphone\",\n          \"markoterzo\"\n        ],\n        \"categoryId\": \"24\",\n        \"liveBroadcastContent\": \"none\",\n        \"defaultLanguage\": \"en\",\n        \"localized\": {\n          \"title\": \"Customizing 8 Apple Watches⌚️💦Then Giving Them Away!!\",\n          \"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_\"\n        },\n        \"defaultAudioLanguage\": \"en\"\n      },\n      \"player\": {\n        \"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\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/videos/videos_info_multi.json",
    "content": "{\n  \"kind\": \"youtube#videoListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/gdL-V0hs_jHqrqpeSTPV4WGOc9Y\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 2\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/dbCtFPFQrd6OMTnWAYrcpZDPai0\\\"\",\n      \"id\": \"D-lhorsDlUQ\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-03-21T20:37:49.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"What are Actions on Google (Assistant on Air)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"tags\": [\n          \"Google\",\n          \"developers\",\n          \"aog\",\n          \"Actions on Google\",\n          \"Assistant\",\n          \"Google Assistant\",\n          \"actions\",\n          \"google home\",\n          \"actions on google\",\n          \"google assistant developers\",\n          \"google assistant sdk\",\n          \"Actions on google developers\",\n          \"smarthome developers\",\n          \"common terminology\",\n          \"custom action on google\",\n          \"google assistant in your app\",\n          \"add google assistant\",\n          \"assistant on air\",\n          \"how to use google assistant on air\",\n          \"Actions on Google how to\"\n        ],\n        \"categoryId\": \"28\",\n        \"liveBroadcastContent\": \"none\",\n        \"defaultLanguage\": \"en\",\n        \"localized\": {\n          \"title\": \"What are Actions on Google (Assistant on Air)\",\n          \"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\"\n        },\n        \"defaultAudioLanguage\": \"en\"\n      },\n      \"player\": {\n        \"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\"\n      }\n    },\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/reY3Wnf12Q5myxn3EOXDguKzvns\\\"\",\n      \"id\": \"ovdbrdCIP7U\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-09-11T23:00:05.000Z\",\n        \"channelId\": \"UCJdl3Paao2f3ha5JXMYUCIA\",\n        \"title\": \"How EVERY Team Got Its Name & Identity!\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/ovdbrdCIP7U/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/ovdbrdCIP7U/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/ovdbrdCIP7U/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/ovdbrdCIP7U/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          }\n        },\n        \"channelTitle\": \"NFL Throwback\",\n        \"tags\": [\n          \"nfl\",\n          \"american football\",\n          \"nfl history\",\n          \"nfl highlights\",\n          \"nfl vault\",\n          \"nfl throwback\",\n          \"How EVERY NFL Team Got Its Name & Identity\",\n          \"how every team got its name\",\n          \"how every nfl team got its name\",\n          \"evolution of the nfl\",\n          \"nfl explained\",\n          \"infograhic\",\n          \"history of every tean\",\n          \"history of nfl\",\n          \"history of logos\",\n          \"nfl 100\",\n          \"nfl timeline\",\n          \"dallas cowboys\",\n          \"pittsburgh steelers\",\n          \"cleveland browns\",\n          \"green bay packers\",\n          \"chicago bear\",\n          \"new england patriots\",\n          \"oakland raiders\",\n          \"philadelphia eagles\"\n        ],\n        \"categoryId\": \"17\",\n        \"liveBroadcastContent\": \"none\",\n        \"localized\": {\n          \"title\": \"How EVERY Team Got Its Name & Identity!\",\n          \"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\"\n        },\n        \"defaultAudioLanguage\": \"en\"\n      },\n      \"player\": {\n        \"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\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/videos/videos_info_single.json",
    "content": "{\n  \"kind\": \"youtube#videoListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/tCG7DWpALbkZUGKS9l2aPYQwYRo\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 1,\n    \"resultsPerPage\": 1\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/dbCtFPFQrd6OMTnWAYrcpZDPai0\\\"\",\n      \"id\": \"D-lhorsDlUQ\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-03-21T20:37:49.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"What are Actions on Google (Assistant on Air)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"tags\": [\n          \"Google\",\n          \"developers\",\n          \"aog\",\n          \"Actions on Google\",\n          \"Assistant\",\n          \"Google Assistant\",\n          \"actions\",\n          \"google home\",\n          \"actions on google\",\n          \"google assistant developers\",\n          \"google assistant sdk\",\n          \"Actions on google developers\",\n          \"smarthome developers\",\n          \"common terminology\",\n          \"custom action on google\",\n          \"google assistant in your app\",\n          \"add google assistant\",\n          \"assistant on air\",\n          \"how to use google assistant on air\",\n          \"Actions on Google how to\"\n        ],\n        \"categoryId\": \"28\",\n        \"liveBroadcastContent\": \"none\",\n        \"defaultLanguage\": \"en\",\n        \"localized\": {\n          \"title\": \"What are Actions on Google (Assistant on Air)\",\n          \"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\"\n        },\n        \"defaultAudioLanguage\": \"en\"\n      },\n      \"player\": {\n        \"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\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/videos/videos_myrating_paged_1.json",
    "content": "{\n  \"kind\": \"youtube#videoListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/b3UF68xF07lN4fYS18DcwcBg3mE\\\"\",\n  \"nextPageToken\": \"CAIQAA\",\n  \"pageInfo\": {\n    \"totalResults\": 3,\n    \"resultsPerPage\": 2\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/cTOUTPk5T0BpAE5u9yFR_Dd3UbI\\\"\",\n      \"id\": \"P4IfFLAX9hY\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-07T06:50:29.000Z\",\n        \"channelId\": \"UCxs2IIVXaEHHA4BtTiWZ2mQ\",\n        \"title\": \"Python Software Foundation Community Report and Community Service Awards - PyCon 2019\",\n        \"description\": \"\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/P4IfFLAX9hY/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/P4IfFLAX9hY/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/P4IfFLAX9hY/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/P4IfFLAX9hY/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/P4IfFLAX9hY/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"PyCon 2019\",\n        \"categoryId\": \"22\",\n        \"liveBroadcastContent\": \"none\",\n        \"localized\": {\n          \"title\": \"Python Software Foundation Community Report and Community Service Awards - PyCon 2019\",\n          \"description\": \"\"\n        }\n      },\n      \"player\": {\n        \"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\"\n      }\n    },\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/FsRNzz1dKoCU6ct60MCxJtR-wpc\\\"\",\n      \"id\": \"kNke39OZ2k0\",\n      \"snippet\": {\n        \"publishedAt\": \"2014-05-24T21:35:51.000Z\",\n        \"channelId\": \"UC-mexo-76-J1MlQM8NkWCYw\",\n        \"title\": \"Building Command Line Applications with Click\",\n        \"description\": \"Quick introduction to building command line applications with click.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/kNke39OZ2k0/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/kNke39OZ2k0/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/kNke39OZ2k0/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/kNke39OZ2k0/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/kNke39OZ2k0/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Armin Ronacher\",\n        \"tags\": [\n          \"Command-line Interface (Computing Platform)\",\n          \"Click\",\n          \"Python (Software)\",\n          \"Software (Industry)\"\n        ],\n        \"categoryId\": \"28\",\n        \"liveBroadcastContent\": \"none\",\n        \"localized\": {\n          \"title\": \"Building Command Line Applications with Click\",\n          \"description\": \"Quick introduction to building command line applications with click.\"\n        }\n      },\n      \"player\": {\n        \"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\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/apidata/videos/videos_myrating_paged_2.json",
    "content": "{\n  \"kind\": \"youtube#videoListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Uj_O0S-h8FpI0EG1DLVrKzjMiqM\\\"\",\n  \"prevPageToken\": \"CAIQAQ\",\n  \"pageInfo\": {\n    \"totalResults\": 3,\n    \"resultsPerPage\": 2\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/97veyDdoO33-JR5D_HUtdm_rAP0\\\"\",\n      \"id\": \"7mIDiKK4eyo\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-08-28T09:29:43.000Z\",\n        \"channelId\": \"UC-mexo-76-J1MlQM8NkWCYw\",\n        \"title\": \"Insta Snapshot Tests Introduction\",\n        \"description\": \"Shows how snapshot testing in the rust insta library works.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/7mIDiKK4eyo/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/7mIDiKK4eyo/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/7mIDiKK4eyo/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/7mIDiKK4eyo/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/7mIDiKK4eyo/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Armin Ronacher\",\n        \"tags\": [\n          \"rust\",\n          \"insta\",\n          \"snapshot testing\"\n        ],\n        \"categoryId\": \"28\",\n        \"liveBroadcastContent\": \"none\",\n        \"localized\": {\n          \"title\": \"Insta Snapshot Tests Introduction\",\n          \"description\": \"Shows how snapshot testing in the rust insta library works.\"\n        }\n      },\n      \"player\": {\n        \"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\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/error_response.json",
    "content": "{\n  \"error\": {\n    \"errors\": [\n      {\n        \"domain\": \"usageLimits\",\n        \"reason\": \"keyInvalid\",\n        \"message\": \"Bad Request\"\n      }\n    ],\n    \"code\": 400,\n    \"message\": \"Bad Request\"\n  }\n}"
  },
  {
    "path": "testdata/error_response_simple.json",
    "content": "{\n  \"error\": \"error message\"\n}"
  },
  {
    "path": "testdata/modeldata/abuse_report_reason/abuse_reason.json",
    "content": "{\n  \"kind\": \"youtube#videoAbuseReportReason\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/_WIvuNJwlISvQNQt_ukh2m0kt2Y\\\"\",\n  \"id\": \"N\",\n  \"snippet\": {\n    \"label\": \"Sex or nudity\",\n    \"secondaryReasons\": [\n      {\n        \"id\": \"32\",\n        \"label\": \"Graphic sex or nudity\"\n      },\n      {\n        \"id\": \"33\",\n        \"label\": \"Content involving minors\"\n      },\n      {\n        \"id\": \"34\",\n        \"label\": \"Other sexual content\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "testdata/modeldata/abuse_report_reason/abuse_reason_res.json",
    "content": "{\n  \"kind\": \"youtube#videoAbuseReportReasonListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/YH398HlGf_qbYlJQUZVMRoL4RTE\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#videoAbuseReportReason\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/_WIvuNJwlISvQNQt_ukh2m0kt2Y\\\"\",\n      \"id\": \"N\",\n      \"snippet\": {\n        \"label\": \"Sex or nudity\",\n        \"secondaryReasons\": [\n          {\n            \"id\": \"32\",\n            \"label\": \"Graphic sex or nudity\"\n          },\n          {\n            \"id\": \"33\",\n            \"label\": \"Content involving minors\"\n          },\n          {\n            \"id\": \"34\",\n            \"label\": \"Other sexual content\"\n          }\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#videoAbuseReportReason\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/9uBFtSRN_-W5oQDz_AhiTSN5sTE\\\"\",\n      \"id\": \"S\",\n      \"snippet\": {\n        \"label\": \"Spam or misleading\",\n        \"secondaryReasons\": [\n          {\n            \"id\": \"27\",\n            \"label\": \"Spam or mass advertising\"\n          },\n          {\n            \"id\": \"28\",\n            \"label\": \"Misleading thumbnail\"\n          },\n          {\n            \"id\": \"29\",\n            \"label\": \"Malware or phishing\"\n          },\n          {\n            \"id\": \"30\",\n            \"label\": \"Pharmaceutical drugs for sale\"\n          },\n          {\n            \"id\": \"31\",\n            \"label\": \"Other misleading info\"\n          }\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#videoAbuseReportReason\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/e6pyrZ9LzezCkkpfXAc0gkDdQ0Q\\\"\",\n      \"id\": \"V\",\n      \"snippet\": {\n        \"label\": \"Violent, hateful, or dangerous\",\n        \"secondaryReasons\": [\n          {\n            \"id\": \"35\",\n            \"label\": \"Promotes violence or hatred\"\n          },\n          {\n            \"id\": \"36\",\n            \"label\": \"Promotes terrorism\"\n          },\n          {\n            \"id\": \"37\",\n            \"label\": \"Bullying or abusing vulnerable individuals\"\n          },\n          {\n            \"id\": \"38\",\n            \"label\": \"Suicide or self-injury\"\n          },\n          {\n            \"id\": \"39\",\n            \"label\": \"Pharmaceutical or drug abuse\"\n          },\n          {\n            \"id\": \"40\",\n            \"label\": \"Other violent, hateful, or dangerous acts\"\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/modeldata/activities/activity.json",
    "content": "{\n  \"kind\": \"youtube#activity\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/Jy79IfTqdSUQSMOkAA9ynak3zOI\\\"\",\n  \"id\": \"MTUxNTc0OTk2MjI3Mjg1OTU3Nzk0MzQzODQ=\",\n  \"snippet\": {\n    \"publishedAt\": \"2019-11-29T02:57:07.000Z\",\n    \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n    \"title\": \"华山日出\",\n    \"description\": \"冷冷的山头\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/default.jpg\",\n        \"width\": 120,\n        \"height\": 90\n      },\n      \"medium\": {\n        \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/mqdefault.jpg\",\n        \"width\": 320,\n        \"height\": 180\n      },\n      \"high\": {\n        \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/hqdefault.jpg\",\n        \"width\": 480,\n        \"height\": 360\n      },\n      \"standard\": {\n        \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/sddefault.jpg\",\n        \"width\": 640,\n        \"height\": 480\n      },\n      \"maxres\": {\n        \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/maxresdefault.jpg\",\n        \"width\": 1280,\n        \"height\": 720\n      }\n    },\n    \"channelTitle\": \"ikaros-life\",\n    \"type\": \"upload\"\n  },\n  \"contentDetails\": {\n    \"upload\": {\n      \"videoId\": \"JE8xdDp5B8Q\"\n    }\n  }\n}"
  },
  {
    "path": "testdata/modeldata/activities/activity_contentDetails.json",
    "content": "{\n  \"upload\": {\n    \"videoId\": \"LDXYRzerjzU\"\n  }\n}"
  },
  {
    "path": "testdata/modeldata/activities/activity_response.json",
    "content": "{\n  \"kind\": \"youtube#activityListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/JK3LLzdEV9zduJqxK4pgzb55QfI\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 50\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/Jy79IfTqdSUQSMOkAA9ynak3zOI\\\"\",\n      \"id\": \"MTUxNTc0OTk2MjI3Mjg1OTU3Nzk0MzQzODQ=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T02:57:07.000Z\",\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"title\": \"华山日出\",\n        \"description\": \"冷冷的山头\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/JE8xdDp5B8Q/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"ikaros-life\",\n        \"type\": \"upload\"\n      },\n      \"contentDetails\": {\n        \"upload\": {\n          \"videoId\": \"JE8xdDp5B8Q\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#activity\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/NT6mauHnBU6ymoM2jbKT7bfHlss\\\"\",\n      \"id\": \"MTUxNTc0OTk1OTAyMjg1OTU3Nzk0MzY0MzI=\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T02:51:42.000Z\",\n        \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n        \"title\": \"海上日出\",\n        \"description\": \"美美美\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/Xfrcfiho_xM/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"ikaros-life\",\n        \"type\": \"upload\"\n      },\n      \"contentDetails\": {\n        \"upload\": {\n          \"videoId\": \"Xfrcfiho_xM\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/activities/activity_snippet.json",
    "content": "{\n  \"publishedAt\": \"2019-12-30T20:00:02.000Z\",\n  \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n  \"title\": \"2019 Year in Review - The Developer Show\",\n  \"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\",\n  \"thumbnails\": {\n    \"default\": {\n      \"url\": \"https://i.ytimg.com/vi/DQGSZTxLVrI/default.jpg\",\n      \"width\": 120,\n      \"height\": 90\n    },\n    \"medium\": {\n      \"url\": \"https://i.ytimg.com/vi/DQGSZTxLVrI/mqdefault.jpg\",\n      \"width\": 320,\n      \"height\": 180\n    },\n    \"high\": {\n      \"url\": \"https://i.ytimg.com/vi/DQGSZTxLVrI/hqdefault.jpg\",\n      \"width\": 480,\n      \"height\": 360\n    },\n    \"standard\": {\n      \"url\": \"https://i.ytimg.com/vi/DQGSZTxLVrI/sddefault.jpg\",\n      \"width\": 640,\n      \"height\": 480\n    },\n    \"maxres\": {\n      \"url\": \"https://i.ytimg.com/vi/DQGSZTxLVrI/maxresdefault.jpg\",\n      \"width\": 1280,\n      \"height\": 720\n    }\n  },\n  \"channelTitle\": \"Google Developers\",\n  \"type\": \"upload\"\n}\n"
  },
  {
    "path": "testdata/modeldata/captions/caption.json",
    "content": "{\n  \"kind\": \"youtube#caption\",\n  \"etag\": \"\\\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\\\"\",\n  \"id\": \"SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I\",\n  \"snippet\": {\n    \"videoId\": \"oHR3wURdJ94\",\n    \"lastUpdated\": \"2020-01-14T09:40:49.981Z\",\n    \"trackKind\": \"standard\",\n    \"language\": \"en\",\n    \"name\": \"\",\n    \"audioTrackType\": \"unknown\",\n    \"isCC\": false,\n    \"isLarge\": false,\n    \"isEasyReader\": false,\n    \"isDraft\": false,\n    \"isAutoSynced\": false,\n    \"status\": \"serving\"\n  }\n}"
  },
  {
    "path": "testdata/modeldata/captions/caption_response.json",
    "content": "{\n  \"kind\": \"youtube#captionListResponse\",\n  \"etag\": \"\\\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/bB4ewYNN7bQHonV-K7efrgBqh8M\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#caption\",\n      \"etag\": \"\\\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/X6ucQ8rZVjhog8RtdYb8rQYLErE\\\"\",\n      \"id\": \"SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I\",\n      \"snippet\": {\n        \"videoId\": \"oHR3wURdJ94\",\n        \"lastUpdated\": \"2020-01-14T09:40:49.981Z\",\n        \"trackKind\": \"standard\",\n        \"language\": \"en\",\n        \"name\": \"\",\n        \"audioTrackType\": \"unknown\",\n        \"isCC\": false,\n        \"isLarge\": false,\n        \"isEasyReader\": false,\n        \"isDraft\": false,\n        \"isAutoSynced\": false,\n        \"status\": \"serving\"\n      }\n    },\n    {\n      \"kind\": \"youtube#caption\",\n      \"etag\": \"\\\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/iRxIplZcCiX0oujr5gSVMXkij8M\\\"\",\n      \"id\": \"fPMuDm722CIRcUAT3NTPQHQZJZJxt39kU7JvrHk8Kzs=\",\n      \"snippet\": {\n        \"videoId\": \"oHR3wURdJ94\",\n        \"lastUpdated\": \"2020-01-14T09:39:46.991Z\",\n        \"trackKind\": \"standard\",\n        \"language\": \"zh-Hans\",\n        \"name\": \"\",\n        \"audioTrackType\": \"unknown\",\n        \"isCC\": false,\n        \"isLarge\": false,\n        \"isEasyReader\": false,\n        \"isDraft\": false,\n        \"isAutoSynced\": false,\n        \"status\": \"serving\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/captions/caption_snippet.json",
    "content": "{\n  \"videoId\": \"oHR3wURdJ94\",\n  \"lastUpdated\": \"2020-01-14T09:40:49.981Z\",\n  \"trackKind\": \"standard\",\n  \"language\": \"en\",\n  \"name\": \"\",\n  \"audioTrackType\": \"unknown\",\n  \"isCC\": false,\n  \"isLarge\": false,\n  \"isEasyReader\": false,\n  \"isDraft\": false,\n  \"isAutoSynced\": false,\n  \"status\": \"serving\"\n}\n"
  },
  {
    "path": "testdata/modeldata/categories/guide_category_info.json",
    "content": "{\n  \"kind\": \"youtube#guideCategory\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/fnL4T7wf3HKS8VCeb2Mui5q9zeM\\\"\",\n  \"id\": \"GCQmVzdCBvZiBZb3VUdWJl\",\n  \"snippet\": {\n    \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n    \"title\": \"Best of YouTube\"\n  }\n}"
  },
  {
    "path": "testdata/modeldata/categories/guide_category_response.json",
    "content": "{\n  \"kind\": \"youtube#guideCategoryListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/KIJAFi2jsRHVBmAk3XYhyRKynjw\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#guideCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/fnL4T7wf3HKS8VCeb2Mui5q9zeM\\\"\",\n      \"id\": \"GCQmVzdCBvZiBZb3VUdWJl\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Best of YouTube\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/modeldata/categories/video_category_info.json",
    "content": "{\n  \"kind\": \"youtube#videoCategory\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/9GQMSRjrZdHeb1OEM1XVQ9zbGec\\\"\",\n  \"id\": \"17\",\n  \"snippet\": {\n    \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n    \"title\": \"Sports\",\n    \"assignable\": true\n  }\n}"
  },
  {
    "path": "testdata/modeldata/categories/video_category_response.json",
    "content": "{\n  \"kind\": \"youtube#videoCategoryListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/0_wT9Ta0iZu7ETYC3E6Xi_B4mtA\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#videoCategory\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/9GQMSRjrZdHeb1OEM1XVQ9zbGec\\\"\",\n      \"id\": \"17\",\n      \"snippet\": {\n        \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n        \"title\": \"Sports\",\n        \"assignable\": true\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/channel_sections/channel_section_info.json",
    "content": "{\n  \"kind\": \"youtube#channelSection\",\n  \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/5bNXeieMoiNVa4NokOortBf50ZA\\\"\",\n  \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE\",\n  \"snippet\": {\n    \"type\": \"multipleChannels\",\n    \"style\": \"horizontalRow\",\n    \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n    \"title\": \"A channel for every type of developer...\",\n    \"position\": 0,\n    \"localized\": {\n      \"title\": \"A channel for every type of developer...\"\n    }\n  },\n  \"contentDetails\": {\n    \"channels\": [\n      \"UCVHFbqXqoYvEWM1Ddxl0QDg\",\n      \"UCJS9pqu9BzkAMNTmzNMNhvg\",\n      \"UCBmwzQnSoj9b6HzNmFrg_yw\",\n      \"UCnUYZLuoy1rq1aVMwx4aTzw\",\n      \"UCWf2ZlNsCGDS89VBF_awNvA\",\n      \"UCP4bf6IHJJQehibu6ai__cg\",\n      \"UC0rqucBdTuFTjJiefW5t-IQ\",\n      \"UC8QMvQrV1bsK7WO37QpSxSg\",\n      \"UClKO7be7O9cUGL94PHnAeOA\",\n      \"UCwXdFgeE9KYzlDdR7TG9cMw\",\n      \"UCorTyjVGM-PV5CCKbosONow\",\n      \"UCXDc-ckqru8BgppXbCt0APw\",\n      \"UCXPBsjgKKG2HqsKBhWA4uQw\",\n      \"UCdIiCSqXuybzwGwJwrpHPqw\",\n      \"UCVhDYDVo3AqyMIKtMLSrcEg\",\n      \"UCK8sQmJBp8GCxrOtXWBpyEA\"\n    ]\n  },\n  \"localizations\": {\n    \"zh-Hans\": {\n      \"title\": \"中文\"\n    },\n    \"en-Us\": {\n      \"title\": \"english\"\n    }\n  }\n}\n"
  },
  {
    "path": "testdata/modeldata/channel_sections/channel_section_response.json",
    "content": "{\n  \"kind\": \"youtube#channelSectionListResponse\",\n  \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/PSyTmUO7BRU2cPplSImsWGWgOz8\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/5bNXeieMoiNVa4NokOortBf50ZA\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE\",\n      \"snippet\": {\n        \"type\": \"multipleChannels\",\n        \"style\": \"horizontalRow\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"A channel for every type of developer...\",\n        \"position\": 0,\n        \"localized\": {\n          \"title\": \"A channel for every type of developer...\"\n        }\n      },\n      \"contentDetails\": {\n        \"channels\": [\n          \"UCVHFbqXqoYvEWM1Ddxl0QDg\",\n          \"UCJS9pqu9BzkAMNTmzNMNhvg\",\n          \"UCBmwzQnSoj9b6HzNmFrg_yw\",\n          \"UCnUYZLuoy1rq1aVMwx4aTzw\",\n          \"UCWf2ZlNsCGDS89VBF_awNvA\",\n          \"UCP4bf6IHJJQehibu6ai__cg\",\n          \"UC0rqucBdTuFTjJiefW5t-IQ\",\n          \"UC8QMvQrV1bsK7WO37QpSxSg\",\n          \"UClKO7be7O9cUGL94PHnAeOA\",\n          \"UCwXdFgeE9KYzlDdR7TG9cMw\",\n          \"UCorTyjVGM-PV5CCKbosONow\",\n          \"UCXDc-ckqru8BgppXbCt0APw\",\n          \"UCXPBsjgKKG2HqsKBhWA4uQw\",\n          \"UCdIiCSqXuybzwGwJwrpHPqw\",\n          \"UCVhDYDVo3AqyMIKtMLSrcEg\",\n          \"UCK8sQmJBp8GCxrOtXWBpyEA\"\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/Qte7tDEpvtKqoGRJAwaCnZqMm3w\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.B8DTd9ZXJqM\",\n      \"snippet\": {\n        \"type\": \"singlePlaylist\",\n        \"style\": \"horizontalRow\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 1\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLOU2XLYxmsILTZ2vd-uxvunCJ1N761Oku\"\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/xAXz1Uyz3p9hcZqPjw74BF2xT1A\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.MfvRjkWLxgk\",\n      \"snippet\": {\n        \"type\": \"singlePlaylist\",\n        \"style\": \"horizontalRow\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 2\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLOU2XLYxmsII8REpkzsy1bJHj6G1WEVA1\"\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/LtytVi_D_TBebyD6FcajdZ-XeIg\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.fEjJOXRoWwg\",\n      \"snippet\": {\n        \"type\": \"singlePlaylist\",\n        \"style\": \"horizontalRow\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 3\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLOU2XLYxmsIJZVnmfwfcBhVIVfk1Ql4Do\"\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/MGoEWG4yrOMMQYvppLDDuO6blMw\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.PvTmxDBxtLs\",\n      \"snippet\": {\n        \"type\": \"singlePlaylist\",\n        \"style\": \"horizontalRow\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 4\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLOU2XLYxmsILKB7ob2wsml3HI6a4e1Qwd\"\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/CW1euDqQ430vaOh7swjuj4i3vZk\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.pmcIOsL7s98\",\n      \"snippet\": {\n        \"type\": \"singlePlaylist\",\n        \"style\": \"horizontalRow\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 5\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLOU2XLYxmsIKfARnhFe9dRCkZPgwOdZEj\"\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/XYv3zd3T7ZjN5nsRa5nEoSwVvlM\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.c3r3vYf9uD0\",\n      \"snippet\": {\n        \"type\": \"singlePlaylist\",\n        \"style\": \"horizontalRow\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 6\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLOU2XLYxmsIJs-bCAsrT21mTgen_DklG1\"\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/L_muP5V_N96m-jDnIT95WERLLVg\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.ZJpkBl-mXfM\",\n      \"snippet\": {\n        \"type\": \"singlePlaylist\",\n        \"style\": \"horizontalRow\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 7\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLOU2XLYxmsIJDw-9-88_LlOs0yR4b4Znv\"\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/zaHbYWO-Q1zjW4IYjza-bTrqeIc\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.9_wU0qhEPR8\",\n      \"snippet\": {\n        \"type\": \"singlePlaylist\",\n        \"style\": \"horizontalRow\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 8\n      },\n      \"contentDetails\": {\n        \"playlists\": [\n          \"PLOU2XLYxmsIKKMtrYD-IfPdlVunyPl9GM\"\n        ]\n      }\n    },\n    {\n      \"kind\": \"youtube#channelSection\",\n      \"etag\": \"\\\"Fznwjl6JEQdo1MGvHOGaz_YanRU/RSxEQQPXGQo3MTN75toyRTUTEmY\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw.npYvuMz0_es\",\n      \"snippet\": {\n        \"type\": \"recentUploads\",\n        \"style\": \"horizontalRow\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"position\": 9\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/channels/channel_api_response.json",
    "content": "{\n  \"kind\": \"youtube#channelListResponse\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/0lqbdkIcLGXAPiLsJ3FTHo96TDg\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 1,\n    \"resultsPerPage\": 1\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#channel\",\n      \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/HUbWoTqNN1LPZKmbyCzPgvjVuR4\\\"\",\n      \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n      \"snippet\": {\n        \"title\": \"Google Developers\",\n        \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\",\n        \"customUrl\": \"googledevelopers\",\n        \"publishedAt\": \"2007-08-23T00:34:43.000Z\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 88,\n            \"height\": 88\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 240,\n            \"height\": 240\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo\",\n            \"width\": 800,\n            \"height\": 800\n          }\n        },\n        \"localized\": {\n          \"title\": \"Google Developers\",\n          \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\"\n        },\n        \"country\": \"US\"\n      },\n      \"statistics\": {\n        \"viewCount\": \"160361638\",\n        \"commentCount\": \"0\",\n        \"subscriberCount\": \"1927873\",\n        \"hiddenSubscriberCount\": false,\n        \"videoCount\": \"5026\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/channels/channel_branding_settings.json",
    "content": "{\n  \"channel\": {\n    \"title\": \"Google Developers\",\n    \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\",\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\\\"\",\n    \"defaultTab\": \"Featured\",\n    \"trackingAnalyticsAccountId\": \"YT-9170156-1\",\n    \"moderateComments\": true,\n    \"showRelatedChannels\": true,\n    \"showBrowseView\": true,\n    \"featuredChannelsTitle\": \"Featured Channels\",\n    \"featuredChannelsUrls\": [\n      \"UCP4bf6IHJJQehibu6ai__cg\",\n      \"UCVHFbqXqoYvEWM1Ddxl0QDg\",\n      \"UCnUYZLuoy1rq1aVMwx4aTzw\",\n      \"UClKO7be7O9cUGL94PHnAeOA\",\n      \"UCdIiCSqXuybzwGwJwrpHPqw\",\n      \"UCJS9pqu9BzkAMNTmzNMNhvg\",\n      \"UCorTyjVGM-PV5CCKbosONow\",\n      \"UCTspylBf8iNobZHgwUD4PXA\",\n      \"UCeo-MamuQVFRcfQmS2N7fhw\",\n      \"UCQqa5UIHtrnpiADC3eHFupw\",\n      \"UCXPBsjgKKG2HqsKBhWA4uQw\",\n      \"UCWf2ZlNsCGDS89VBF_awNvA\"\n    ],\n    \"unsubscribedTrailer\": \"lyRPyRKHO8M\",\n    \"profileColor\": \"#000000\",\n    \"country\": \"US\"\n  },\n  \"image\": {\n    \"bannerImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\",\n    \"bannerMobileImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\",\n    \"bannerTabletLowImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1138-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\",\n    \"bannerTabletImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1707-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\",\n    \"bannerTabletHdImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2276-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\",\n    \"bannerTabletExtraHdImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\",\n    \"bannerMobileLowImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\",\n    \"bannerMobileMediumHdImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\",\n    \"bannerMobileHdImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\",\n    \"bannerMobileExtraHdImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\",\n    \"bannerTvImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\",\n    \"bannerTvLowImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\",\n    \"bannerTvMediumImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\",\n    \"bannerTvHighImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\"\n  },\n  \"hints\": [\n    {\n      \"property\": \"channel.banner.mobile.medium.image.url\",\n      \"value\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\"\n    },\n    {\n      \"property\": \"channel.featured_tab.template.string\",\n      \"value\": \"Everything\"\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/channels/channel_content_details.json",
    "content": "{\n  \"relatedPlaylists\": {\n    \"uploads\": \"UU_x5XG1OV2P6uZZ5FSM9Ttw\",\n    \"watchHistory\": \"HL\",\n    \"watchLater\": \"WL\"\n  }\n}"
  },
  {
    "path": "testdata/modeldata/channels/channel_info.json",
    "content": "{\n  \"kind\": \"youtube#channel\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/HUbWoTqNN1LPZKmbyCzPgvjVuR4\\\"\",\n  \"id\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n  \"snippet\": {\n    \"title\": \"Google Developers\",\n    \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\",\n    \"customUrl\": \"googledevelopers\",\n    \"publishedAt\": \"2007-08-23T00:34:43.000Z\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo\",\n        \"width\": 88,\n        \"height\": 88\n      },\n      \"medium\": {\n        \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo\",\n        \"width\": 240,\n        \"height\": 240\n      },\n      \"high\": {\n        \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo\",\n        \"width\": 800,\n        \"height\": 800\n      }\n    },\n    \"localized\": {\n      \"title\": \"Google Developers\",\n      \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\"\n    },\n    \"country\": \"US\"\n  },\n  \"contentDetails\": {\n    \"relatedPlaylists\": {\n      \"uploads\": \"UU_x5XG1OV2P6uZZ5FSM9Ttw\",\n      \"watchHistory\": \"HL\",\n      \"watchLater\": \"WL\"\n    }\n  },\n  \"statistics\": {\n    \"viewCount\": \"160361638\",\n    \"commentCount\": \"0\",\n    \"subscriberCount\": \"1927873\",\n    \"hiddenSubscriberCount\": false,\n    \"videoCount\": \"5026\"\n  },\n  \"topicDetails\": {\n    \"topicIds\": [\n      \"/m/019_rr\",\n      \"/m/07c1v\",\n      \"/m/02jjt\",\n      \"/m/019_rr\",\n      \"/m/07c1v\",\n      \"/m/02jjt\"\n    ],\n    \"topicCategories\": [\n      \"https://en.wikipedia.org/wiki/Entertainment\",\n      \"https://en.wikipedia.org/wiki/Technology\",\n      \"https://en.wikipedia.org/wiki/Lifestyle_(sociology)\"\n    ]\n  },\n  \"status\": {\n    \"privacyStatus\": \"public\",\n    \"isLinked\": true,\n    \"longUploadsStatus\": \"longUploadsUnspecified\"\n  },\n  \"brandingSettings\": {\n    \"channel\": {\n      \"title\": \"Google Developers\",\n      \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\",\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\\\"\",\n      \"defaultTab\": \"Featured\",\n      \"trackingAnalyticsAccountId\": \"YT-9170156-1\",\n      \"moderateComments\": true,\n      \"showRelatedChannels\": true,\n      \"showBrowseView\": true,\n      \"featuredChannelsTitle\": \"Featured Channels\",\n      \"featuredChannelsUrls\": [\n        \"UCP4bf6IHJJQehibu6ai__cg\",\n        \"UCVHFbqXqoYvEWM1Ddxl0QDg\",\n        \"UCnUYZLuoy1rq1aVMwx4aTzw\",\n        \"UClKO7be7O9cUGL94PHnAeOA\",\n        \"UCdIiCSqXuybzwGwJwrpHPqw\",\n        \"UCJS9pqu9BzkAMNTmzNMNhvg\",\n        \"UCorTyjVGM-PV5CCKbosONow\",\n        \"UCTspylBf8iNobZHgwUD4PXA\",\n        \"UCeo-MamuQVFRcfQmS2N7fhw\",\n        \"UCQqa5UIHtrnpiADC3eHFupw\",\n        \"UCXPBsjgKKG2HqsKBhWA4uQw\",\n        \"UCWf2ZlNsCGDS89VBF_awNvA\"\n      ],\n      \"unsubscribedTrailer\": \"lyRPyRKHO8M\",\n      \"profileColor\": \"#000000\",\n      \"country\": \"US\"\n    },\n    \"image\": {\n      \"bannerImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\",\n      \"bannerMobileImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\",\n      \"bannerTabletLowImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1138-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\",\n      \"bannerTabletImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1707-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\",\n      \"bannerTabletHdImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2276-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\",\n      \"bannerTabletExtraHdImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\",\n      \"bannerMobileLowImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\",\n      \"bannerMobileMediumHdImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\",\n      \"bannerMobileHdImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\",\n      \"bannerMobileExtraHdImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\",\n      \"bannerTvImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\",\n      \"bannerTvLowImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\",\n      \"bannerTvMediumImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\",\n      \"bannerTvHighImageUrl\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\"\n    },\n    \"hints\": [\n      {\n        \"property\": \"channel.banner.mobile.medium.image.url\",\n        \"value\": \"https://yt3.ggpht.com/vpeUmkxH-uuOYgdvyCXg5Bz4Rn5z2Yxj_efZ2uN62WZeQFdro2PfumdcvvLJwn9G4mRFyriF7Vk=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\"\n      },\n      {\n        \"property\": \"channel.featured_tab.template.string\",\n        \"value\": \"Everything\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "testdata/modeldata/channels/channel_snippet.json",
    "content": "{\n  \"title\": \"Google Developers\",\n  \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\",\n  \"customUrl\": \"googledevelopers\",\n  \"publishedAt\": \"2007-08-23T00:34:43.000Z\",\n  \"thumbnails\": {\n    \"default\": {\n      \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo\",\n      \"width\": 88,\n      \"height\": 88\n    },\n    \"medium\": {\n      \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s240-c-k-c0xffffffff-no-rj-mo\",\n      \"width\": 240,\n      \"height\": 240\n    },\n    \"high\": {\n      \"url\": \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s800-c-k-c0xffffffff-no-rj-mo\",\n      \"width\": 800,\n      \"height\": 800\n    }\n  },\n  \"localized\": {\n    \"title\": \"Google Developers\",\n    \"description\": \"The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.\"\n  },\n  \"country\": \"US\"\n}"
  },
  {
    "path": "testdata/modeldata/channels/channel_statistics.json",
    "content": "{\n  \"viewCount\": 160361638,\n  \"commentCount\": \"0\",\n  \"subscriberCount\": \"1927873\",\n  \"hiddenSubscriberCount\": false,\n  \"videoCount\": \"5026\"\n}"
  },
  {
    "path": "testdata/modeldata/channels/channel_status.json",
    "content": "{\n  \"privacyStatus\": \"public\",\n  \"isLinked\": true,\n  \"longUploadsStatus\": \"longUploadsUnspecified\"\n}"
  },
  {
    "path": "testdata/modeldata/channels/channel_topic_details.json",
    "content": "{\n  \"topicIds\": [\n    \"/m/019_rr\",\n    \"/m/07c1v\",\n    \"/m/02jjt\",\n    \"/m/019_rr\",\n    \"/m/07c1v\",\n    \"/m/02jjt\"\n  ],\n  \"topicCategories\": [\n    \"https://en.wikipedia.org/wiki/Entertainment\",\n    \"https://en.wikipedia.org/wiki/Technology\",\n    \"https://en.wikipedia.org/wiki/Lifestyle_(sociology)\"\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/comments/comment_api_response.json",
    "content": "{\n  \"kind\": \"youtube#commentListResponse\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/WGjMjz47HiC5hiv290at1ES2VhM\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/DNyaTRe4NG3pWMwjpCUwPAYb9uk\\\"\",\n      \"id\": \"UgxKREWxIgDrw8w2e_Z4AaABAg\",\n      \"snippet\": {\n        \"authorDisplayName\": \"Hieu Nguyen\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/-N1uydT1LhpA/AAAAAAAAAAI/AAAAAAAAAAA/nvwONlQ4ZsE/s28-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UClfzT4CU_yaZjJaI4pKqSjQ\",\n        \"authorChannelId\": {\n          \"value\": \"UClfzT4CU_yaZjJaI4pKqSjQ\"\n        },\n        \"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 ...\",\n        \"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 ...\",\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 0,\n        \"publishedAt\": \"2019-04-20T01:03:39.000Z\",\n        \"updatedAt\": \"2019-04-20T01:03:39.000Z\"\n      }\n    },\n    {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/bmZin9yRcFlXNixJ4nHaVlISbZM\\\"\",\n      \"id\": \"UgyrVQaFfEdvaSzstj14AaABAg\",\n      \"snippet\": {\n        \"authorDisplayName\": \"Mani Kanta\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/-8VVOkpYv6O4/AAAAAAAAAAI/AAAAAAAAAAA/9asGD8pGx7Y/s28-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UCJBxRADq6jctX-YdhjkB6PA\",\n        \"authorChannelId\": {\n          \"value\": \"UCJBxRADq6jctX-YdhjkB6PA\"\n        },\n        \"textDisplay\": \"super\",\n        \"textOriginal\": \"super\",\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 0,\n        \"publishedAt\": \"2019-04-04T04:14:44.000Z\",\n        \"updatedAt\": \"2019-04-04T04:14:44.000Z\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/comments/comment_info.json",
    "content": "{\n  \"kind\": \"youtube#comment\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/4cvUO3bQNuuOby5VnN9ZtVUJfk8\\\"\",\n  \"id\": \"UgwxApqcfzZzF_C5Zqx4AaABAg\",\n  \"snippet\": {\n    \"authorDisplayName\": \"Oeurn Ravuth\",\n    \"authorProfileImageUrl\": \"https://yt3.ggpht.com/-FTjrEZu33Cg/AAAAAAAAAAI/AAAAAAAAAAA/74aahJJl02c/s28-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n    \"authorChannelUrl\": \"http://www.youtube.com/channel/UCqPku3cxM-ED3poX8YtGqeg\",\n    \"authorChannelId\": {\n      \"value\": \"UCqPku3cxM-ED3poX8YtGqeg\"\n    },\n    \"videoId\": \"wtLJPvx7-ys\",\n    \"textDisplay\": \"This video is awesome! GOOD\",\n    \"textOriginal\": \"This video is awesome! GOOD\",\n    \"canRate\": true,\n    \"viewerRating\": \"none\",\n    \"likeCount\": 0,\n    \"publishedAt\": \"2019-03-28T11:33:46.000Z\",\n    \"updatedAt\": \"2019-03-28T11:33:46.000Z\"\n  }\n}"
  },
  {
    "path": "testdata/modeldata/comments/comment_snippet.json",
    "content": "{\n  \"authorDisplayName\": \"Oeurn Ravuth\",\n  \"authorProfileImageUrl\": \"https://yt3.ggpht.com/-FTjrEZu33Cg/AAAAAAAAAAI/AAAAAAAAAAA/74aahJJl02c/s28-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n  \"authorChannelUrl\": \"http://www.youtube.com/channel/UCqPku3cxM-ED3poX8YtGqeg\",\n  \"authorChannelId\": {\n    \"value\": \"UCqPku3cxM-ED3poX8YtGqeg\"\n  },\n  \"videoId\": \"wtLJPvx7-ys\",\n  \"textDisplay\": \"This video is awesome! GOOD\",\n  \"textOriginal\": \"This video is awesome! GOOD\",\n  \"canRate\": true,\n  \"viewerRating\": \"none\",\n  \"likeCount\": 0,\n  \"publishedAt\": \"2019-03-28T11:33:46.000Z\",\n  \"updatedAt\": \"2019-03-28T11:33:46.000Z\"\n}"
  },
  {
    "path": "testdata/modeldata/comments/comment_thread_api_response.json",
    "content": "{\n  \"kind\": \"youtube#commentThreadListResponse\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/x02Ynz4jiNB0_bPpgqOoltUaAIw\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 2,\n    \"resultsPerPage\": 20\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/YzERg5ywz5smx8eBpQ07fRaCWmo\\\"\",\n      \"id\": \"Ugz097FRhsQy5CVhAjp4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"cD7NPxuuXYY\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/cMMoinsp0Nc5wvMvENly9N9Llyo\\\"\",\n          \"id\": \"Ugz097FRhsQy5CVhAjp4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"Paulo José Martínez\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/-1gM871v6gjw/AAAAAAAAAAI/AAAAAAAAAAA/golxu5t1oGQ/s28-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UCCz-SK7nwY_a3I2EqrplrLQ\",\n            \"authorChannelId\": {\n              \"value\": \"UCCz-SK7nwY_a3I2EqrplrLQ\"\n            },\n            \"videoId\": \"cD7NPxuuXYY\",\n            \"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!\",\n            \"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!\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-08-23T14:06:16.000Z\",\n            \"updatedAt\": \"2019-08-23T14:06:16.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    },\n    {\n      \"kind\": \"youtube#commentThread\",\n      \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/PAJ4fUiE3D2BVdF7C8uieNxb_wc\\\"\",\n      \"id\": \"UgzhytyP79_PwaDd4UB4AaABAg\",\n      \"snippet\": {\n        \"videoId\": \"Azt8Nc-mtKM\",\n        \"topLevelComment\": {\n          \"kind\": \"youtube#comment\",\n          \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/oPcBe_mf_rOLgyZ3KY6oYJjdY_o\\\"\",\n          \"id\": \"UgzhytyP79_PwaDd4UB4AaABAg\",\n          \"snippet\": {\n            \"authorDisplayName\": \"asdf7692\",\n            \"authorProfileImageUrl\": \"https://yt3.ggpht.com/-46C1T2dCUzM/AAAAAAAAAAI/AAAAAAAAAAA/y0-4fvhE0hM/s28-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n            \"authorChannelUrl\": \"http://www.youtube.com/channel/UC9Prn_6KFp1xu-t-rf-K8cA\",\n            \"authorChannelId\": {\n              \"value\": \"UC9Prn_6KFp1xu-t-rf-K8cA\"\n            },\n            \"videoId\": \"Azt8Nc-mtKM\",\n            \"textDisplay\": \"wait, that&#39;s the guy that wrote my textbook!\",\n            \"textOriginal\": \"wait, that's the guy that wrote my textbook!\",\n            \"canRate\": true,\n            \"viewerRating\": \"none\",\n            \"likeCount\": 0,\n            \"publishedAt\": \"2019-08-23T23:12:09.000Z\",\n            \"updatedAt\": \"2019-08-23T23:12:09.000Z\"\n          }\n        },\n        \"canReply\": true,\n        \"totalReplyCount\": 0,\n        \"isPublic\": true\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/comments/comment_thread_info.json",
    "content": "{\n  \"kind\": \"youtube#commentThread\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Bu9ED8YytJP_F5IxgbsfRJg0CZI\\\"\",\n  \"id\": \"UgydxWWoeA7F1OdqypJ4AaABAg\",\n  \"snippet\": {\n    \"videoId\": \"D-lhorsDlUQ\",\n    \"topLevelComment\": {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Vcd3llXDKJW8UrWr8ndIHHDBk8g\\\"\",\n      \"id\": \"UgydxWWoeA7F1OdqypJ4AaABAg\",\n      \"snippet\": {\n        \"authorDisplayName\": \"Loren Robilio\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/-dVa9HLlQcNs/AAAAAAAAAAI/AAAAAAAAAAA/lxKAIuHR-20/s28-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UCe9i1nJCcevTa6KJa55KYog\",\n        \"authorChannelId\": {\n          \"value\": \"UCe9i1nJCcevTa6KJa55KYog\"\n        },\n        \"videoId\": \"D-lhorsDlUQ\",\n        \"textDisplay\": \"<a href=\\\"http://actions.ai/\\\">Actions.ai</a>\",\n        \"textOriginal\": \"Actions.ai\",\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 0,\n        \"publishedAt\": \"2019-06-23T08:24:24.000Z\",\n        \"updatedAt\": \"2019-06-23T08:24:24.000Z\"\n      }\n    },\n    \"canReply\": true,\n    \"totalReplyCount\": 1,\n    \"isPublic\": true\n  },\n  \"replies\": {\n    \"comments\": [\n      {\n        \"kind\": \"youtube#comment\",\n        \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Z_3RVDklwNvCP3pgufc11jc5ud0\\\"\",\n        \"id\": \"UgydxWWoeA7F1OdqypJ4AaABAg.8wWQ3tdHcFx8xcDheui-qb\",\n        \"snippet\": {\n          \"authorDisplayName\": \"Dian Anggraeni\",\n          \"authorProfileImageUrl\": \"https://yt3.ggpht.com/-WLAKDA-bqa8/AAAAAAAAAAI/AAAAAAAAAAA/4VOHyI34fuU/s28-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n          \"authorChannelUrl\": \"http://www.youtube.com/channel/UCsl2A_QzcSnFD2hpCyVwXxA\",\n          \"authorChannelId\": {\n            \"value\": \"UCsl2A_QzcSnFD2hpCyVwXxA\"\n          },\n          \"videoId\": \"D-lhorsDlUQ\",\n          \"textDisplay\": \"#\",\n          \"textOriginal\": \"#\",\n          \"parentId\": \"UgydxWWoeA7F1OdqypJ4AaABAg\",\n          \"canRate\": true,\n          \"viewerRating\": \"none\",\n          \"likeCount\": 1,\n          \"publishedAt\": \"2019-07-20T20:22:27.000Z\",\n          \"updatedAt\": \"2019-07-20T20:22:27.000Z\"\n        }\n      }\n    ]\n  }\n}"
  },
  {
    "path": "testdata/modeldata/comments/comment_thread_replies.json",
    "content": "{\n  \"comments\": [\n    {\n      \"kind\": \"youtube#comment\",\n      \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Z_3RVDklwNvCP3pgufc11jc5ud0\\\"\",\n      \"id\": \"UgydxWWoeA7F1OdqypJ4AaABAg.8wWQ3tdHcFx8xcDheui-qb\",\n      \"snippet\": {\n        \"authorDisplayName\": \"Dian Anggraeni\",\n        \"authorProfileImageUrl\": \"https://yt3.ggpht.com/-WLAKDA-bqa8/AAAAAAAAAAI/AAAAAAAAAAA/4VOHyI34fuU/s28-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n        \"authorChannelUrl\": \"http://www.youtube.com/channel/UCsl2A_QzcSnFD2hpCyVwXxA\",\n        \"authorChannelId\": {\n          \"value\": \"UCsl2A_QzcSnFD2hpCyVwXxA\"\n        },\n        \"videoId\": \"D-lhorsDlUQ\",\n        \"textDisplay\": \"#\",\n        \"textOriginal\": \"#\",\n        \"parentId\": \"UgydxWWoeA7F1OdqypJ4AaABAg\",\n        \"canRate\": true,\n        \"viewerRating\": \"none\",\n        \"likeCount\": 1,\n        \"publishedAt\": \"2019-07-20T20:22:27.000Z\",\n        \"updatedAt\": \"2019-07-20T20:22:27.000Z\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/modeldata/comments/comment_thread_snippet.json",
    "content": "{\n  \"videoId\": \"D-lhorsDlUQ\",\n  \"topLevelComment\": {\n    \"kind\": \"youtube#comment\",\n    \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/Vcd3llXDKJW8UrWr8ndIHHDBk8g\\\"\",\n    \"id\": \"UgydxWWoeA7F1OdqypJ4AaABAg\",\n    \"snippet\": {\n      \"authorDisplayName\": \"Loren Robilio\",\n      \"authorProfileImageUrl\": \"https://yt3.ggpht.com/-dVa9HLlQcNs/AAAAAAAAAAI/AAAAAAAAAAA/lxKAIuHR-20/s28-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n      \"authorChannelUrl\": \"http://www.youtube.com/channel/UCe9i1nJCcevTa6KJa55KYog\",\n      \"authorChannelId\": {\n        \"value\": \"UCe9i1nJCcevTa6KJa55KYog\"\n      },\n      \"videoId\": \"D-lhorsDlUQ\",\n      \"textDisplay\": \"<a href=\\\"http://actions.ai/\\\">Actions.ai</a>\",\n      \"textOriginal\": \"Actions.ai\",\n      \"canRate\": true,\n      \"viewerRating\": \"none\",\n      \"likeCount\": 0,\n      \"publishedAt\": \"2019-06-23T08:24:24.000Z\",\n      \"updatedAt\": \"2019-06-23T08:24:24.000Z\"\n    }\n  },\n  \"canReply\": true,\n  \"totalReplyCount\": 1,\n  \"isPublic\": true\n}\n"
  },
  {
    "path": "testdata/modeldata/common/thumbnail_info.json",
    "content": "{\n  \"url\": \"https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s88-c-k-c0xffffffff-no-rj-mo\",\n  \"width\": 88,\n  \"height\": 88\n}"
  },
  {
    "path": "testdata/modeldata/common/thumbnails_info.json",
    "content": "{\n  \"default\": {\n    \"url\": \"https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s88-c-k-c0xffffffff-no-rj-mo\",\n    \"width\": 88,\n    \"height\": 88\n  },\n  \"medium\": {\n    \"url\": \"https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s240-c-k-c0xffffffff-no-rj-mo\",\n    \"width\": 240,\n    \"height\": 240\n  },\n  \"high\": {\n    \"url\": \"https://yt3.ggpht.com/a/AGF-l7-BBIcC888A2qYc3rB44rST01IEYDG3uzbU_A=s800-c-k-c0xffffffff-no-rj-mo\",\n    \"width\": 800,\n    \"height\": 800\n  }\n}\n"
  },
  {
    "path": "testdata/modeldata/i18ns/language_info.json",
    "content": "{\n  \"kind\": \"youtube#i18nLanguage\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/GMrwiM1f-4KHxMka40cB3lysLgY\\\"\",\n  \"id\": \"af\",\n  \"snippet\": {\n    \"hl\": \"af\",\n    \"name\": \"Afrikaans\"\n  }\n}"
  },
  {
    "path": "testdata/modeldata/i18ns/language_res.json",
    "content": "{\n  \"kind\": \"youtube#i18nLanguageListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/qgFy24yvs-L_dNjr2d-Rd_Xcfw4\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#i18nLanguage\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/GMrwiM1f-4KHxMka40cB3lysLgY\\\"\",\n      \"id\": \"af\",\n      \"snippet\": {\n        \"hl\": \"af\",\n        \"name\": \"Afrikaans\"\n      }\n    },\n    {\n      \"kind\": \"youtube#i18nLanguage\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/wOlCLE4kfCyCca9_ssuNDceE0yk\\\"\",\n      \"id\": \"az\",\n      \"snippet\": {\n        \"hl\": \"az\",\n        \"name\": \"Azerbaijani\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/modeldata/i18ns/region_info.json",
    "content": "{\n  \"kind\": \"youtube#i18nRegion\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/R_GB1d7CQi3LIpoHKbakFDisvoA\\\"\",\n  \"id\": \"DZ\",\n  \"snippet\": {\n    \"gl\": \"DZ\",\n    \"name\": \"Algeria\"\n  }\n}"
  },
  {
    "path": "testdata/modeldata/i18ns/region_res.json",
    "content": "{\n  \"kind\": \"youtube#i18nRegionListResponse\",\n  \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/q85_wZeDyKDzYtt-LhNaozyi_sk\\\"\",\n  \"items\": [\n    {\n      \"kind\": \"youtube#i18nRegion\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/R_GB1d7CQi3LIpoHKbakFDisvoA\\\"\",\n      \"id\": \"DZ\",\n      \"snippet\": {\n        \"gl\": \"DZ\",\n        \"name\": \"Algeria\"\n      }\n    },\n    {\n      \"kind\": \"youtube#i18nRegion\",\n      \"etag\": \"\\\"SJZWTG6xR0eGuCOh2bX6w3s4F94/w6ci5tJWSaqFmjn3xsM2loOjo2o\\\"\",\n      \"id\": \"AR\",\n      \"snippet\": {\n        \"gl\": \"AR\",\n        \"name\": \"Argentina\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/members/member_info.json",
    "content": "{\n  \"kind\": \"youtube#member\",\n  \"etag\": \"etag\",\n  \"snippet\": {\n    \"creatorChannelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n    \"memberDetails\": {\n      \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n      \"channelUrl\": \"https://www.youtube.com/channel/UCa-vrCLQHviTOVnEKDOdetQ\",\n      \"displayName\": \"ikaros-life\",\n      \"profileImageUrl\": \"https://yt3.ggpht.com/a-/AOh14Gg1_gYcI03VLDd3FMLUY5cb5O9RC9sElj26-1SR=s288-c-k-c0xffffffff-no-rj-mo\"\n    },\n    \"membershipsDetails\": {\n      \"highestAccessibleLevel\": \"string\",\n      \"highestAccessibleLevelDisplayName\": \"string\",\n      \"accessibleLevels\": [\n        \"string\"\n      ],\n      \"membershipsDuration\": {\n        \"memberSince\": \"2007-08-23T00:34:43Z\",\n        \"memberTotalDurationMonths\": 5\n      },\n      \"membershipsDurationAtLevel\": [\n        {\n          \"level\": \"string\",\n          \"memberSince\": \"2007-08-23T00:34:43Z\",\n          \"memberTotalDurationMonths\": 6\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "testdata/modeldata/members/membership_level.json",
    "content": "{\n  \"kind\": \"youtube#membershipsLevel\",\n  \"etag\": \"etag\",\n  \"id\": \"id\",\n  \"snippet\": {\n    \"creatorChannelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\",\n    \"levelDetails\": {\n      \"displayName\": \"high\"\n    }\n  }\n}"
  },
  {
    "path": "testdata/modeldata/playlist_items/playlist_item_api_response.json",
    "content": "{\n  \"kind\": \"youtube#playlistItemListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/2r7BiOpjx2NRuQ2KuoLxRoJmZUI\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 3,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/E5rTjxNaKfzDc-GFs2Cb9jkKlGM\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-11T00:27:38.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google I/O'19 - I/O Live (Day 1 Composite)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n        \"position\": 0,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"H1HZyvc0QnI\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/3JqJ3Bv7ZIVEu4ZoeH6ZGsUe7js\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4yODlGNEE0NkRGMEEzMEQy\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-11T00:52:10.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google I/O'19 - I/O Live (Day 2  Composite)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/5NgsfxIWNls/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n        \"position\": 1,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"5NgsfxIWNls\"\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#playlistItem\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Kib3kvf3c_Bq79UyVpa2pHYzV_U\\\"\",\n      \"id\": \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4wMTcyMDhGQUE4NTIzM0Y5\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-11T00:55:44.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Google I/O'19 - I/O Live (Day 3 Composite)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/VCv-KKIkLns/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/VCv-KKIkLns/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/VCv-KKIkLns/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/VCv-KKIkLns/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/VCv-KKIkLns/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"playlistId\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n        \"position\": 2,\n        \"resourceId\": {\n          \"kind\": \"youtube#video\",\n          \"videoId\": \"VCv-KKIkLns\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/modeldata/playlist_items/playlist_item_content_details.json",
    "content": "{\n  \"videoId\": \"D-lhorsDlUQ\",\n  \"videoPublishedAt\": \"2019-03-21T20:37:49.000Z\"\n}\n"
  },
  {
    "path": "testdata/modeldata/playlist_items/playlist_item_info.json",
    "content": "{\n  \"kind\": \"youtube#playlistItem\",\n  \"etag\": \"\\\"nlUZBA6NbTS7q9G8D1GljyfTIWI/lAPls3tzYIP4Re0-vMkPDF4whaw\\\"\",\n  \"id\": \"UExPVTJYTFl4bXNJSnB1ZmVNSG5jblF2Rk9lMEszTWhWcC41NkI0NEY2RDEwNTU3Q0M2\",\n  \"snippet\": {\n    \"publishedAt\": \"2019-05-16T18:46:20.000Z\",\n    \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n    \"title\": \"What are Actions on Google (Assistant on Air)\",\n    \"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\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n        \"width\": 120,\n        \"height\": 90\n      },\n      \"medium\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n        \"width\": 320,\n        \"height\": 180\n      },\n      \"high\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n        \"width\": 480,\n        \"height\": 360\n      },\n      \"standard\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n        \"width\": 640,\n        \"height\": 480\n      },\n      \"maxres\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n        \"width\": 1280,\n        \"height\": 720\n      }\n    },\n    \"channelTitle\": \"Google Developers\",\n    \"playlistId\": \"PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp\",\n    \"position\": 0,\n    \"resourceId\": {\n      \"kind\": \"youtube#video\",\n      \"videoId\": \"D-lhorsDlUQ\"\n    },\n    \"videoOwnerChannelTitle\": \"Google Developers\",\n    \"videoOwnerChannelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\"\n  },\n  \"contentDetails\": {\n    \"videoId\": \"D-lhorsDlUQ\",\n    \"videoPublishedAt\": \"2019-03-21T20:37:49.000Z\"\n  },\n  \"status\": {\n    \"privacyStatus\": \"public\"\n  }\n}"
  },
  {
    "path": "testdata/modeldata/playlist_items/playlist_item_snippet.json",
    "content": "{\n  \"publishedAt\": \"2019-05-16T18:46:20.000Z\",\n  \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n  \"title\": \"What are Actions on Google (Assistant on Air)\",\n  \"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\",\n  \"thumbnails\": {\n    \"default\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n      \"width\": 120,\n      \"height\": 90\n    },\n    \"medium\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n      \"width\": 320,\n      \"height\": 180\n    },\n    \"high\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n      \"width\": 480,\n      \"height\": 360\n    },\n    \"standard\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n      \"width\": 640,\n      \"height\": 480\n    },\n    \"maxres\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n      \"width\": 1280,\n      \"height\": 720\n    }\n  },\n  \"channelTitle\": \"Google Developers\",\n  \"playlistId\": \"PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp\",\n  \"position\": 0,\n  \"resourceId\": {\n    \"kind\": \"youtube#video\",\n    \"videoId\": \"D-lhorsDlUQ\"\n  }\n}"
  },
  {
    "path": "testdata/modeldata/playlist_items/playlist_item_status.json",
    "content": "{\n  \"privacyStatus\": \"public\"\n}"
  },
  {
    "path": "testdata/modeldata/playlists/playlist_api_response.json",
    "content": "{\n  \"kind\": \"youtube#playlistListResponse\",\n  \"etag\": \"\\\"nlUZBA6NbTS7q9G8D1GljyfTIWI/BfhLqBNRhhd1rjH-NUyOUzazr-4\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 416,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"nlUZBA6NbTS7q9G8D1GljyfTIWI/XooPPPPffp2qIyK-PJIIwE8GJuM\\\"\",\n      \"id\": \"PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-16T18:46:20.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Assistant on Air\",\n        \"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!\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Assistant on Air\",\n          \"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!\"\n        }\n      },\n      \"status\": {\n        \"privacyStatus\": \"public\"\n      },\n      \"contentDetails\": {\n        \"itemCount\": 4\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"nlUZBA6NbTS7q9G8D1GljyfTIWI/zik79It-4mLFCMBeiYtbHEkN330\\\"\",\n      \"id\": \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-10T00:18:56.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"I/O Live - Show Composite\",\n        \"description\": \"\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/H1HZyvc0QnI/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"I/O Live - Show Composite\",\n          \"description\": \"\"\n        }\n      },\n      \"status\": {\n        \"privacyStatus\": \"public\"\n      },\n      \"contentDetails\": {\n        \"itemCount\": 3\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"nlUZBA6NbTS7q9G8D1GljyfTIWI/Q2iRVdwJ4gRZS6h0x8unUsejZvk\\\"\",\n      \"id\": \"PLOU2XLYxmsIJJVnHWmd1qfr0Caq4VZCu4\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-10T00:18:07.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"I/O Live\",\n        \"description\": \"Relive moments from Google I/O 2019\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/UVOhgly2VEc/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/UVOhgly2VEc/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/UVOhgly2VEc/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/UVOhgly2VEc/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/UVOhgly2VEc/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"I/O Live\",\n          \"description\": \"Relive moments from Google I/O 2019\"\n        }\n      },\n      \"status\": {\n        \"privacyStatus\": \"public\"\n      },\n      \"contentDetails\": {\n        \"itemCount\": 23\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"nlUZBA6NbTS7q9G8D1GljyfTIWI/QXl0bYWnpSuOs3NHzhWJ1mF78_k\\\"\",\n      \"id\": \"PLOU2XLYxmsIKW-llcbcFdpR9RjCfYHZaV\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-02T23:39:42.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Machine Learning at Google I/O 2019\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/pM9u9xcM_cs/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/pM9u9xcM_cs/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/pM9u9xcM_cs/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/pM9u9xcM_cs/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/pM9u9xcM_cs/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Machine Learning at Google I/O 2019\",\n          \"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/\"\n        }\n      },\n      \"status\": {\n        \"privacyStatus\": \"public\"\n      },\n      \"contentDetails\": {\n        \"itemCount\": 14\n      }\n    },\n    {\n      \"kind\": \"youtube#playlist\",\n      \"etag\": \"\\\"nlUZBA6NbTS7q9G8D1GljyfTIWI/_vt3bEv3Nji--Q-pDnqQi_jpO24\\\"\",\n      \"id\": \"PLOU2XLYxmsIIOSO0eWuj-6yQmdakarUzN\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-02T23:38:49.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Accessibility at Google I/O 2019\",\n        \"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/\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/vnSDqh6zT6Y/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"localized\": {\n          \"title\": \"Accessibility at Google I/O 2019\",\n          \"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/\"\n        }\n      },\n      \"status\": {\n        \"privacyStatus\": \"public\"\n      },\n      \"contentDetails\": {\n        \"itemCount\": 4\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "testdata/modeldata/playlists/playlist_content_details.json",
    "content": "{\n  \"itemCount\": 4\n}"
  },
  {
    "path": "testdata/modeldata/playlists/playlist_info.json",
    "content": "{\n  \"kind\": \"youtube#playlist\",\n  \"etag\": \"\\\"nlUZBA6NbTS7q9G8D1GljyfTIWI/XooPPPPffp2qIyK-PJIIwE8GJuM\\\"\",\n  \"id\": \"PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp\",\n  \"snippet\": {\n    \"publishedAt\": \"2019-05-16T18:46:20.000Z\",\n    \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n    \"title\": \"Assistant on Air\",\n    \"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!\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n        \"width\": 120,\n        \"height\": 90\n      },\n      \"medium\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n        \"width\": 320,\n        \"height\": 180\n      },\n      \"high\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n        \"width\": 480,\n        \"height\": 360\n      },\n      \"standard\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n        \"width\": 640,\n        \"height\": 480\n      },\n      \"maxres\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n        \"width\": 1280,\n        \"height\": 720\n      }\n    },\n    \"channelTitle\": \"Google Developers\",\n    \"localized\": {\n      \"title\": \"Assistant on Air\",\n      \"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!\"\n    }\n  },\n  \"status\": {\n    \"privacyStatus\": \"public\"\n  },\n  \"contentDetails\": {\n    \"itemCount\": 4\n  }\n}"
  },
  {
    "path": "testdata/modeldata/playlists/playlist_snippet.json",
    "content": "{\n  \"publishedAt\": \"2019-05-16T18:46:20.000Z\",\n  \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n  \"title\": \"Assistant on Air\",\n  \"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!\",\n  \"thumbnails\": {\n    \"default\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n      \"width\": 120,\n      \"height\": 90\n    },\n    \"medium\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n      \"width\": 320,\n      \"height\": 180\n    },\n    \"high\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n      \"width\": 480,\n      \"height\": 360\n    },\n    \"standard\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n      \"width\": 640,\n      \"height\": 480\n    },\n    \"maxres\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n      \"width\": 1280,\n      \"height\": 720\n    }\n  },\n  \"channelTitle\": \"Google Developers\",\n  \"localized\": {\n    \"title\": \"Assistant on Air\",\n    \"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!\"\n  }\n}\n"
  },
  {
    "path": "testdata/modeldata/playlists/playlist_status.json",
    "content": "{\n  \"privacyStatus\": \"public\"\n}"
  },
  {
    "path": "testdata/modeldata/search_result/search_result.json",
    "content": "{\n  \"kind\": \"youtube#searchResult\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/vbYWvy5RlqHHhMVjeHUTwJcQQWg\\\"\",\n  \"id\": {\n    \"kind\": \"youtube#video\",\n    \"videoId\": \"fq4N0hgOWzU\"\n  },\n  \"snippet\": {\n    \"publishedAt\": \"2018-02-23T15:00:09.000Z\",\n    \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n    \"title\": \"Introducing Flutter\",\n    \"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 ...\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://i.ytimg.com/vi/fq4N0hgOWzU/default.jpg\",\n        \"width\": 120,\n        \"height\": 90\n      },\n      \"medium\": {\n        \"url\": \"https://i.ytimg.com/vi/fq4N0hgOWzU/mqdefault.jpg\",\n        \"width\": 320,\n        \"height\": 180\n      },\n      \"high\": {\n        \"url\": \"https://i.ytimg.com/vi/fq4N0hgOWzU/hqdefault.jpg\",\n        \"width\": 480,\n        \"height\": 360\n      }\n    },\n    \"channelTitle\": \"Google Developers\",\n    \"liveBroadcastContent\": \"none\"\n  }\n}"
  },
  {
    "path": "testdata/modeldata/search_result/search_result_api_response.json",
    "content": "{\n  \"kind\": \"youtube#searchListResponse\",\n  \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/aluR_NlUCSvgLE_pAjGxhfmcHoY\\\"\",\n  \"nextPageToken\": \"CAUQAA\",\n  \"regionCode\": \"US\",\n  \"pageInfo\": {\n    \"totalResults\": 489126,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/vbYWvy5RlqHHhMVjeHUTwJcQQWg\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"fq4N0hgOWzU\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2018-02-23T15:00:09.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Introducing Flutter\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/fq4N0hgOWzU/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/fq4N0hgOWzU/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/fq4N0hgOWzU/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/NPdUn-g5a_FcBJsdRmVLX4FUjtw\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"cKxRvEZd3Mw\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2016-03-30T16:59:12.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Hello World - Machine Learning Recipes #1\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/cKxRvEZd3Mw/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/cKxRvEZd3Mw/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/cKxRvEZd3Mw/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/ZvP7ILdxl9Kzl8PrkCWA71Dh_GY\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#playlist\",\n        \"playlistId\": \"PLOU2XLYxmsIJ7imRl4jU7623pHNjZqw3t\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2018-08-31T16:03:59.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Mobile Ads Garage : Season 2\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/vfsgBBky4bg/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/vfsgBBky4bg/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/vfsgBBky4bg/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/WaD7AfMNykBwM2y31b43CKk7WKs\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#playlist\",\n        \"playlistId\": \"PLOU2XLYxmsIKX0pUJV3uqp6N3NeHwHh0c\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2016-04-05T22:45:35.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"Mobile Ads Garage\",\n        \"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 ...\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/OLLLRUPICcc/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/OLLLRUPICcc/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/OLLLRUPICcc/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    },\n    {\n      \"kind\": \"youtube#searchResult\",\n      \"etag\": \"\\\"j6xRRd8dTPVVptg711_CSPADRfg/Stj9CP60v4-ZD7b7FBgp9h9OffY\\\"\",\n      \"id\": {\n        \"kind\": \"youtube#video\",\n        \"videoId\": \"mWl45NkFBOc\"\n      },\n      \"snippet\": {\n        \"publishedAt\": \"2017-02-15T18:01:59.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"TensorFlow: Machine Learning for Everyone\",\n        \"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.\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/mWl45NkFBOc/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/mWl45NkFBOc/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/mWl45NkFBOc/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"liveBroadcastContent\": \"none\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/search_result/search_result_id.json",
    "content": "{\n  \"kind\": \"youtube#playlist\",\n  \"playlistId\": \"PLOU2XLYxmsIKX0pUJV3uqp6N3NeHwHh0c\"\n}"
  },
  {
    "path": "testdata/modeldata/search_result/search_result_snippet.json",
    "content": "{\n  \"publishedAt\": \"2016-03-30T16:59:12.000Z\",\n  \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n  \"title\": \"Hello World - Machine Learning Recipes #1\",\n  \"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 ...\",\n  \"thumbnails\": {\n    \"default\": {\n      \"url\": \"https://i.ytimg.com/vi/cKxRvEZd3Mw/default.jpg\",\n      \"width\": 120,\n      \"height\": 90\n    },\n    \"medium\": {\n      \"url\": \"https://i.ytimg.com/vi/cKxRvEZd3Mw/mqdefault.jpg\",\n      \"width\": 320,\n      \"height\": 180\n    },\n    \"high\": {\n      \"url\": \"https://i.ytimg.com/vi/cKxRvEZd3Mw/hqdefault.jpg\",\n      \"width\": 480,\n      \"height\": 360\n    }\n  }\n}"
  },
  {
    "path": "testdata/modeldata/subscriptions/contentDetails.json",
    "content": "{\n  \"totalItemCount\": 2,\n  \"newItemCount\": 0,\n  \"activityType\": \"all\"\n}"
  },
  {
    "path": "testdata/modeldata/subscriptions/resp.json",
    "content": "{\n  \"kind\": \"youtube#subscriptionListResponse\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/tcw1POI4O_SxXM12fDiwN47t82I\\\"\",\n  \"nextPageToken\": \"CAUQAA\",\n  \"pageInfo\": {\n    \"totalResults\": 16,\n    \"resultsPerPage\": 5\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/R40G3IlR9sgjUldVPi90sGvUQTE\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCBwtJ-Aho6DZeutqZiP4Q79Q\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-12-25T09:12:18.265Z\",\n        \"title\": \"Next Day Video\",\n        \"description\": \"\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCQ7dFBzZGlBvtU2hCecsBBg\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      },\n      \"contentDetails\": {\n        \"totalItemCount\": 1562,\n        \"newItemCount\": 0,\n        \"activityType\": \"all\"\n      },\n      \"subscriberSnippet\": {\n        \"title\": \"kun liu\",\n        \"description\": \"\",\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/vqJozAaZqELuPPv4HHNhUWLLY20\\\"\",\n      \"id\": \"zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-09-11T11:35:04.568Z\",\n        \"title\": \"PyCon 2015\",\n        \"description\": \"\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCgxzjK6GuOHVKR_08TT4hJQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-fLvSSJbxK4U/AAAAAAAAAAI/AAAAAAAAAAA/6YBsX_62XeI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      },\n      \"contentDetails\": {\n        \"totalItemCount\": 134,\n        \"newItemCount\": 0,\n        \"activityType\": \"all\"\n      },\n      \"subscriberSnippet\": {\n        \"title\": \"kun liu\",\n        \"description\": \"\",\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/7ArnKe3x-zHWrA235OPSLkFtcGg\\\"\",\n      \"id\": \"zqShTXi-2-S50Nc0aJJ6zdHSZbM7XNml9y9B3V6WQ9A\",\n      \"snippet\": {\n        \"publishedAt\": \"2018-06-11T01:42:48.406Z\",\n        \"title\": \"李永乐老师\",\n        \"description\": \"欢迎关注我的微信公众号“李永乐老师”，上面有超多文字版科普内容和中学视频课程哦\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCSs4A6HYKmHA2MG_0z-F0xw\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-yYEnknp3CQQ/AAAAAAAAAAI/AAAAAAAAAAA/8-SLsa_cgpI/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      },\n      \"contentDetails\": {\n        \"totalItemCount\": 278,\n        \"newItemCount\": 1,\n        \"activityType\": \"all\"\n      },\n      \"subscriberSnippet\": {\n        \"title\": \"kun liu\",\n        \"description\": \"\",\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/hZAeF0AETpmxML6TuUZfYWXtNzQ\\\"\",\n      \"id\": \"zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-11-29T03:00:56.380Z\",\n        \"title\": \"ikaros-life\",\n        \"description\": \"This is a test channel.\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      },\n      \"contentDetails\": {\n        \"totalItemCount\": 2,\n        \"newItemCount\": 0,\n        \"activityType\": \"all\"\n      },\n      \"subscriberSnippet\": {\n        \"title\": \"kun liu\",\n        \"description\": \"\",\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    },\n    {\n      \"kind\": \"youtube#subscription\",\n      \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/YH8OpiSknjSnSE5lK4iP84c-RMg\\\"\",\n      \"id\": \"zqShTXi-2-Rya5uUxEp3ZpfEZoPHGpH2MBMMdN1Yl9Y\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-05-28T01:19:42.921Z\",\n        \"title\": \"PyCon 2019\",\n        \"description\": \"\",\n        \"resourceId\": {\n          \"kind\": \"youtube#channel\",\n          \"channelId\": \"UCxs2IIVXaEHHA4BtTiWZ2mQ\"\n        },\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-K9tl0Fy3l2k/AAAAAAAAAAI/AAAAAAAAAAA/PvePZf-T5VU/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      },\n      \"contentDetails\": {\n        \"totalItemCount\": 148,\n        \"newItemCount\": 0,\n        \"activityType\": \"all\"\n      },\n      \"subscriberSnippet\": {\n        \"title\": \"kun liu\",\n        \"description\": \"\",\n        \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"medium\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          },\n          \"high\": {\n            \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n          }\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/subscriptions/snippet.json",
    "content": "{\n  \"publishedAt\": \"2018-12-25T09:12:18.265Z\",\n  \"title\": \"Next Day Video\",\n  \"description\": \"\",\n  \"resourceId\": {\n    \"kind\": \"youtube#channel\",\n    \"channelId\": \"UCQ7dFBzZGlBvtU2hCecsBBg\"\n  },\n  \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n  \"thumbnails\": {\n    \"default\": {\n      \"url\": \"https://yt3.ggpht.com/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n    },\n    \"medium\": {\n      \"url\": \"https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n    },\n    \"high\": {\n      \"url\": \"https://yt3.ggpht.com/-1s55wPtP9FA/AAAAAAAAAAI/AAAAAAAAAAA/N7BV5IiJXG8/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n    }\n  }\n}\n"
  },
  {
    "path": "testdata/modeldata/subscriptions/subscriberSnippet.json",
    "content": "{\n  \"title\": \"kun liu\",\n  \"description\": \"\",\n  \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n  \"thumbnails\": {\n    \"default\": {\n      \"url\": \"https://yt3.ggpht.com/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n    },\n    \"medium\": {\n      \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n    },\n    \"high\": {\n      \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n    }\n  }\n}"
  },
  {
    "path": "testdata/modeldata/subscriptions/subscription.json",
    "content": "{\n  \"kind\": \"youtube#subscription\",\n  \"etag\": \"\\\"p4VTdlkQv3HQeTEaXgvLePAydmU/hZAeF0AETpmxML6TuUZfYWXtNzQ\\\"\",\n  \"id\": \"zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo\",\n  \"snippet\": {\n    \"publishedAt\": \"2019-11-29T03:00:56.380Z\",\n    \"title\": \"ikaros-life\",\n    \"description\": \"This is a test channel.\",\n    \"resourceId\": {\n      \"kind\": \"youtube#channel\",\n      \"channelId\": \"UCa-vrCLQHviTOVnEKDOdetQ\"\n    },\n    \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n      },\n      \"medium\": {\n        \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n      },\n      \"high\": {\n        \"url\": \"https://yt3.ggpht.com/-yBDi3ItKNRs/AAAAAAAAAAI/AAAAAAAAAAA/ozh6Tc0Gc9w/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n      }\n    }\n  },\n  \"contentDetails\": {\n    \"totalItemCount\": 2,\n    \"newItemCount\": 0,\n    \"activityType\": \"all\"\n  },\n  \"subscriberSnippet\": {\n    \"title\": \"kun liu\",\n    \"description\": \"\",\n    \"channelId\": \"UCNvMBmCASzTNNX8lW3JRMbw\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n      },\n      \"medium\": {\n        \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s240-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n      },\n      \"high\": {\n        \"url\": \"https://yt3.ggpht.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAAAA/Zu23t9_vViQ/s800-c-k-no-mo-rj-c0xffffff/photo.jpg\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "testdata/modeldata/users/access_token.json",
    "content": "{\n  \"access_token\": \"access_token\",\n  \"id_token\": \"id_token\",\n  \"expires_in\": 3600,\n  \"token_type\": \"Bearer\",\n  \"scope\": [\n    \"https://www.googleapis.com/auth/userinfo.profile\",\n    \"https://www.googleapis.com/auth/youtube\"\n  ],\n  \"refresh_token\": \"refresh_token\"\n}"
  },
  {
    "path": "testdata/modeldata/users/user_profile.json",
    "content": "{\n  \"family_name\": \"liu\",\n  \"name\": \"kun liu\",\n  \"picture\": \"https://lh3.googleusercontent.com/-uDfN5WzhqmE/AAAAAAAAAAI/AAAAAAAAACY/1E9uN31I7cE/photo.jpg\",\n  \"locale\": \"zh-CN\",\n  \"given_name\": \"kun\",\n  \"id\": \"12345678910\"\n}"
  },
  {
    "path": "testdata/modeldata/videos/video_api_response.json",
    "content": "{\n  \"kind\": \"youtube#videoListResponse\",\n  \"etag\": \"\\\"nlUZBA6NbTS7q9G8D1GljyfTIWI/iv3lyeXWs7VXBQLwxMdaHn-GgXM\\\"\",\n  \"pageInfo\": {\n    \"totalResults\": 1,\n    \"resultsPerPage\": 1\n  },\n  \"items\": [\n    {\n      \"kind\": \"youtube#video\",\n      \"etag\": \"\\\"nlUZBA6NbTS7q9G8D1GljyfTIWI/tLwQL5gV5utoTrC4nEayyWXynfY\\\"\",\n      \"id\": \"D-lhorsDlUQ\",\n      \"snippet\": {\n        \"publishedAt\": \"2019-03-21T20:37:49.000Z\",\n        \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n        \"title\": \"What are Actions on Google (Assistant on Air)\",\n        \"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\",\n        \"thumbnails\": {\n          \"default\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n            \"width\": 120,\n            \"height\": 90\n          },\n          \"medium\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n            \"width\": 320,\n            \"height\": 180\n          },\n          \"high\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n            \"width\": 480,\n            \"height\": 360\n          },\n          \"standard\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n            \"width\": 640,\n            \"height\": 480\n          },\n          \"maxres\": {\n            \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n            \"width\": 1280,\n            \"height\": 720\n          }\n        },\n        \"channelTitle\": \"Google Developers\",\n        \"tags\": [\n          \"Google\",\n          \"developers\",\n          \"aog\",\n          \"Actions on Google\",\n          \"Assistant\",\n          \"Google Assistant\",\n          \"actions\",\n          \"google home\",\n          \"actions on google\",\n          \"google assistant developers\",\n          \"google assistant sdk\",\n          \"Actions on google developers\",\n          \"smarthome developers\",\n          \"common terminology\",\n          \"custom action on google\",\n          \"google assistant in your app\",\n          \"add google assistant\",\n          \"assistant on air\",\n          \"how to use google assistant on air\",\n          \"Actions on Google how to\"\n        ],\n        \"categoryId\": \"28\",\n        \"liveBroadcastContent\": \"none\",\n        \"defaultLanguage\": \"en\",\n        \"localized\": {\n          \"title\": \"What are Actions on Google (Assistant on Air)\",\n          \"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\"\n        },\n        \"defaultAudioLanguage\": \"en\"\n      },\n      \"contentDetails\": {\n        \"duration\": \"PT21M7S\",\n        \"dimension\": \"2d\",\n        \"definition\": \"hd\",\n        \"caption\": \"true\",\n        \"licensedContent\": false,\n        \"projection\": \"rectangular\"\n      },\n      \"status\": {\n        \"uploadStatus\": \"processed\",\n        \"privacyStatus\": \"public\",\n        \"license\": \"youtube\",\n        \"embeddable\": true,\n        \"publicStatsViewable\": true\n      },\n      \"statistics\": {\n        \"viewCount\": \"7920\",\n        \"likeCount\": \"190\",\n        \"dislikeCount\": \"23\",\n        \"favoriteCount\": \"0\",\n        \"commentCount\": \"32\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "testdata/modeldata/videos/video_category_info.json",
    "content": "{\n  \"kind\": \"youtube#videoCategory\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/9GQMSRjrZdHeb1OEM1XVQ9zbGec\\\"\",\n  \"id\": \"17\",\n  \"snippet\": {\n    \"channelId\": \"UCBR8-60-B28hp2BmDPdntcQ\",\n    \"title\": \"Sports\",\n    \"assignable\": true\n  }\n}"
  },
  {
    "path": "testdata/modeldata/videos/video_content_details.json",
    "content": "{\n    \"duration\": \"PT21M7S\",\n    \"dimension\": \"2d\",\n    \"definition\": \"hd\",\n    \"caption\": \"true\",\n    \"licensedContent\": false,\n    \"projection\": \"rectangular\"\n  }"
  },
  {
    "path": "testdata/modeldata/videos/video_info.json",
    "content": "{\n  \"kind\": \"youtube#video\",\n  \"etag\": \"\\\"8jEFfXBrqiSrcF6Ee7MQuz8XuAM/ywRH8hhCtBBffWyVbxSDNA08vr0\\\"\",\n  \"id\": \"D-lhorsDlUQ\",\n  \"snippet\": {\n    \"publishedAt\": \"2019-03-21T20:37:49.000Z\",\n    \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n    \"title\": \"What are Actions on Google (Assistant on Air)\",\n    \"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\",\n    \"thumbnails\": {\n      \"default\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n        \"width\": 120,\n        \"height\": 90\n      },\n      \"medium\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n        \"width\": 320,\n        \"height\": 180\n      },\n      \"high\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n        \"width\": 480,\n        \"height\": 360\n      },\n      \"standard\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n        \"width\": 640,\n        \"height\": 480\n      },\n      \"maxres\": {\n        \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n        \"width\": 1280,\n        \"height\": 720\n      }\n    },\n    \"channelTitle\": \"Google Developers\",\n    \"tags\": [\n      \"Google\",\n      \"developers\",\n      \"aog\",\n      \"Actions on Google\",\n      \"Assistant\",\n      \"Google Assistant\",\n      \"actions\",\n      \"google home\",\n      \"actions on google\",\n      \"google assistant developers\",\n      \"google assistant sdk\",\n      \"Actions on google developers\",\n      \"smarthome developers\",\n      \"common terminology\",\n      \"custom action on google\",\n      \"google assistant in your app\",\n      \"add google assistant\",\n      \"assistant on air\",\n      \"how to use google assistant on air\",\n      \"Actions on Google how to\"\n    ],\n    \"categoryId\": \"28\",\n    \"liveBroadcastContent\": \"none\",\n    \"defaultLanguage\": \"en\",\n    \"localized\": {\n      \"title\": \"What are Actions on Google (Assistant on Air)\",\n      \"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\"\n    },\n    \"defaultAudioLanguage\": \"en\"\n  },\n  \"contentDetails\": {\n    \"duration\": \"PT21M7S\",\n    \"dimension\": \"2d\",\n    \"definition\": \"hd\",\n    \"caption\": \"true\",\n    \"licensedContent\": false,\n    \"projection\": \"rectangular\"\n  },\n  \"status\": {\n    \"uploadStatus\": \"processed\",\n    \"privacyStatus\": \"public\",\n    \"license\": \"youtube\",\n    \"embeddable\": true,\n    \"publicStatsViewable\": true\n  },\n  \"statistics\": {\n    \"viewCount\": \"8087\",\n    \"likeCount\": \"190\",\n    \"dislikeCount\": \"23\",\n    \"favoriteCount\": \"0\",\n    \"commentCount\": \"32\"\n  },\n  \"topicDetails\": {\n    \"topicIds\": [\n      \"/m/02jjt\"\n    ],\n    \"relevantTopicIds\": [\n      \"/m/02jjt\"\n    ],\n    \"topicCategories\": [\n      \"https://en.wikipedia.org/wiki/Entertainment\"\n    ]\n  }\n}"
  },
  {
    "path": "testdata/modeldata/videos/video_paid_product_placement_details.json",
    "content": "{\n  \"hasPaidProductPlacement\": true\n}\n"
  },
  {
    "path": "testdata/modeldata/videos/video_recording_details.json",
    "content": "{\n  \"recordingDate\": \"2024-07-03T00:00:00Z\"\n}\n"
  },
  {
    "path": "testdata/modeldata/videos/video_snippet.json",
    "content": "{\n  \"publishedAt\": \"2019-03-21T20:37:49.000Z\",\n  \"channelId\": \"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n  \"title\": \"What are Actions on Google (Assistant on Air)\",\n  \"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\",\n  \"thumbnails\": {\n    \"default\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\",\n      \"width\": 120,\n      \"height\": 90\n    },\n    \"medium\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/mqdefault.jpg\",\n      \"width\": 320,\n      \"height\": 180\n    },\n    \"high\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/hqdefault.jpg\",\n      \"width\": 480,\n      \"height\": 360\n    },\n    \"standard\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/sddefault.jpg\",\n      \"width\": 640,\n      \"height\": 480\n    },\n    \"maxres\": {\n      \"url\": \"https://i.ytimg.com/vi/D-lhorsDlUQ/maxresdefault.jpg\",\n      \"width\": 1280,\n      \"height\": 720\n    }\n  },\n  \"channelTitle\": \"Google Developers\",\n  \"tags\": [\n    \"Google\",\n    \"developers\",\n    \"aog\",\n    \"Actions on Google\",\n    \"Assistant\",\n    \"Google Assistant\",\n    \"actions\",\n    \"google home\",\n    \"actions on google\",\n    \"google assistant developers\",\n    \"google assistant sdk\",\n    \"Actions on google developers\",\n    \"smarthome developers\",\n    \"common terminology\",\n    \"custom action on google\",\n    \"google assistant in your app\",\n    \"add google assistant\",\n    \"assistant on air\",\n    \"how to use google assistant on air\",\n    \"Actions on Google how to\"\n  ],\n  \"categoryId\": \"28\",\n  \"liveBroadcastContent\": \"none\",\n  \"defaultLanguage\": \"en\",\n  \"localized\": {\n    \"title\": \"What are Actions on Google (Assistant on Air)\",\n    \"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\"\n  },\n  \"defaultAudioLanguage\": \"en\"\n}"
  },
  {
    "path": "testdata/modeldata/videos/video_statistics.json",
    "content": "{\n  \"viewCount\": 8087,\n  \"likeCount\": \"190\",\n  \"dislikeCount\": \"23\",\n  \"favoriteCount\": \"0\",\n  \"commentCount\": \"32\"\n}"
  },
  {
    "path": "testdata/modeldata/videos/video_status.json",
    "content": "{\n  \"uploadStatus\": \"processed\",\n  \"privacyStatus\": \"public\",\n  \"license\": \"youtube\",\n  \"embeddable\": true,\n  \"publicStatsViewable\": true,\n  \"publishAt\": \"2019-03-21T20:37:49.000Z\",\n  \"madeForKids\": false\n}"
  },
  {
    "path": "testdata/modeldata/videos/video_topic_details.json",
    "content": "{\n  \"relevantTopicIds\": [\n    \"/m/02jjt\"\n  ],\n  \"topicCategories\": [\n    \"https://en.wikipedia.org/wiki/Entertainment\"\n  ]\n}"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/apis/__init__.py",
    "content": ""
  },
  {
    "path": "tests/apis/test_activities.py",
    "content": "import json\nimport unittest\n\nimport responses\n\nimport pyyoutube\n\n\nclass ApiActivitiesTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/activities/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/activities\"\n\n    with open(BASE_PATH + \"activities_by_channel_p1.json\", \"rb\") as f:\n        ACTIVITIES_CHANNEL_P1 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"activities_by_channel_p2.json\", \"rb\") as f:\n        ACTIVITIES_CHANNEL_P2 = json.loads(f.read().decode(\"utf-8\"))\n\n    with open(BASE_PATH + \"activities_by_mine_p1.json\", \"rb\") as f:\n        ACTIVITIES_MINE_P1 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"activities_by_mine_p2.json\", \"rb\") as f:\n        ACTIVITIES_MINE_P2 = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n        self.api_with_access_token = pyyoutube.Api(access_token=\"token\")\n\n    def testGetChannelActivities(self) -> None:\n        # test parts\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_activities_by_channel(channel_id=\"id\", parts=\"id,not_part\")\n\n        # test get all activities\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.ACTIVITIES_CHANNEL_P1)\n            m.add(\"GET\", self.BASE_URL, json=self.ACTIVITIES_CHANNEL_P2)\n\n            res = self.api.get_activities_by_channel(\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n                parts=\"id,snippet\",\n                before=\"2019-11-1T00:00:00.000Z\",\n                after=\"2019-10-1T00:00:00.000Z\",\n                region_code=\"US\",\n                count=None,\n            )\n            self.assertEqual(len(res.items), 13)\n\n        # test get by page token\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.ACTIVITIES_CHANNEL_P2)\n\n            res = self.api.get_activities_by_channel(\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n                parts=\"id,snippet\",\n                count=None,\n                page_token=\"CAoQAA\",\n                return_json=True,\n            )\n            self.assertEqual(len(res[\"items\"]), 3)\n\n    def testGetMineActivities(self) -> None:\n        # test parts\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api_with_access_token.get_activities_by_me(parts=\"id,not_part\")\n\n        # test get all activities\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.ACTIVITIES_MINE_P1)\n            m.add(\"GET\", self.BASE_URL, json=self.ACTIVITIES_MINE_P2)\n\n            res = self.api_with_access_token.get_activities_by_me(\n                parts=\"id,snippet\",\n                before=\"2019-11-1T00:00:00.000Z\",\n                after=\"2019-12-1T00:00:00.000Z\",\n                region_code=\"US\",\n                count=None,\n            )\n            self.assertEqual(len(res.items), 2)\n\n        # test page token\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.ACTIVITIES_MINE_P2)\n\n            res = self.api_with_access_token.get_activities_by_me(\n                parts=\"id,snippet\",\n                before=\"2019-11-1T00:00:00.000Z\",\n                after=\"2019-12-1T00:00:00.000Z\",\n                region_code=\"US\",\n                page_token=\"CAEQAA\",\n                count=None,\n                return_json=True,\n            )\n            self.assertEqual(len(res[\"items\"]), 1)\n"
  },
  {
    "path": "tests/apis/test_auth.py",
    "content": "import json\nimport unittest\n\nimport responses\nfrom requests import HTTPError\n\nimport pyyoutube\n\n\nclass TestOAuthApi(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/\"\n\n    with open(BASE_PATH + \"access_token.json\", \"rb\") as f:\n        ACCESS_TOKEN_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"user_profile.json\", \"rb\") as f:\n        USER_PROFILE_INFO = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(client_id=\"xx\", client_secret=\"xx\")\n\n    def testInitApi(self) -> None:\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            pyyoutube.Api()\n\n    def testOAuth(self) -> None:\n        url, statue = self.api.get_authorization_url()\n        self.assertEqual(statue, \"PyYouTube\")\n\n        redirect_response = (\n            \"https://localhost/?state=PyYouTube&code=code\"\n            \"&scope=profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile#\"\n        )\n\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.refresh_token()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                \"POST\", self.api.EXCHANGE_ACCESS_TOKEN_URL, json=self.ACCESS_TOKEN_INFO\n            )\n            token = self.api.generate_access_token(\n                authorization_response=redirect_response,\n            )\n            self.assertEqual(token.access_token, \"access_token\")\n            token_origin = self.api.generate_access_token(\n                authorization_response=redirect_response, return_json=True\n            )\n            self.assertEqual(token_origin[\"access_token\"], \"access_token\")\n\n            refresh_token = self.api.refresh_token()\n            self.assertEqual(refresh_token.access_token, \"access_token\")\n            refresh_token_origin = self.api.refresh_token(return_json=True)\n            self.assertEqual(refresh_token_origin[\"refresh_token\"], \"refresh_token\")\n\n            api = pyyoutube.Api(client_id=\"xx\", client_secret=\"xx\")\n            refresh_token = api.refresh_token(refresh_token=\"refresh_token\")\n            self.assertEqual(refresh_token.refresh_token, \"refresh_token\")\n\n    def testGetProfile(self) -> None:\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_profile()\n\n        self.api._access_token = \"access_token\"\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.api.USER_INFO_URL, json=self.USER_PROFILE_INFO)\n            profile = self.api.get_profile()\n            self.assertEqual(profile.given_name, \"kun\")\n\n            profile_origin = self.api.get_profile(return_json=True)\n            self.assertEqual(profile_origin[\"given_name\"], \"kun\")\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.api.USER_INFO_URL, body=HTTPError(\"Exception\"))\n            with self.assertRaises(pyyoutube.PyYouTubeException):\n                self.api.get_profile()\n"
  },
  {
    "path": "tests/apis/test_captions.py",
    "content": "import json\nimport unittest\n\nimport responses\n\nimport pyyoutube\n\n\nclass ApiCaptionsTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/captions/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/captions\"\n\n    with open(BASE_PATH + \"captions_by_video.json\", \"rb\") as f:\n        CAPTIONS_BY_VIDEO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"captions_filter_by_id.json\", \"rb\") as f:\n        CAPTIONS_FILTER_ID = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api_with_access_token = pyyoutube.Api(access_token=\"token\")\n\n    def testGetCaptionByVideo(self) -> None:\n        video_id = \"oHR3wURdJ94\"\n\n        # test parts\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api_with_access_token.get_captions_by_video(\n                video_id=video_id,\n                parts=\"id,not_part\",\n            )\n\n        # test by video\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.CAPTIONS_BY_VIDEO)\n\n            res = self.api_with_access_token.get_captions_by_video(\n                video_id=video_id,\n                parts=\"id,snippet\",\n                return_json=True,\n            )\n            self.assertEqual(len(res[\"items\"]), 2)\n            self.assertEqual(res[\"items\"][0][\"snippet\"][\"videoId\"], video_id)\n\n        # test filter id\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.CAPTIONS_FILTER_ID)\n\n            res = self.api_with_access_token.get_captions_by_video(\n                video_id=video_id,\n                parts=[\"id\", \"snippet\"],\n                caption_id=\"SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I\",\n            )\n\n            self.assertEqual(len(res.items), 1)\n            self.assertEqual(res.items[0].snippet.videoId, video_id)\n"
  },
  {
    "path": "tests/apis/test_categories.py",
    "content": "import json\nimport unittest\n\nimport responses\n\nimport pyyoutube\n\n\nclass ApiVideoCategoryTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/categories/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/videoCategories\"\n\n    with open(BASE_PATH + \"video_category_single.json\", \"rb\") as f:\n        VIDEO_CATEGORY_SINGLE = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_category_multi.json\", \"rb\") as f:\n        VIDEO_CATEGORY_MULTI = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_category_by_region.json\", \"rb\") as f:\n        VIDEO_CATEGORY_BY_REGION = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n\n    def testGetVideoCategories(self) -> None:\n        # test params\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_video_categories()\n        # test parts\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_video_categories(category_id=\"id\", parts=\"id,not_part\")\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEO_CATEGORY_SINGLE)\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEO_CATEGORY_MULTI)\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEO_CATEGORY_BY_REGION)\n\n            res_by_single = self.api.get_video_categories(\n                category_id=\"17\",\n                parts=[\"snippet\"],\n                return_json=True,\n            )\n            self.assertEqual(res_by_single[\"kind\"], \"youtube#videoCategoryListResponse\")\n            self.assertEqual(len(res_by_single[\"items\"]), 1)\n            self.assertEqual(res_by_single[\"items\"][0][\"id\"], \"17\")\n\n            res_by_multi = self.api.get_video_categories(\n                category_id=[\"17\", \"18\"],\n                parts=\"snippet\",\n            )\n            self.assertEqual(len(res_by_multi.items), 2)\n            self.assertEqual(res_by_multi.items[1].id, \"18\")\n\n            res_by_region = self.api.get_video_categories(\n                region_code=\"US\",\n                parts=\"snippet\",\n            )\n            self.assertEqual(len(res_by_region.items), 32)\n            self.assertEqual(res_by_region.items[0].id, \"1\")\n"
  },
  {
    "path": "tests/apis/test_channel_sections.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube\nimport responses\n\n\nclass ApiChannelSectionTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/channel_sections/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/channelSections\"\n\n    with open(BASE_PATH + \"channel_sections_by_id.json\", \"rb\") as f:\n        CHANNEL_SECTIONS_BY_ID = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_sections_by_ids.json\", \"rb\") as f:\n        CHANNEL_SECTIONS_BY_IDS = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_sections_by_channel.json\", \"rb\") as f:\n        CHANNEL_SECTIONS_BY_CHANNEL = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n\n    def testGetChannelSectionsById(self) -> None:\n        section_id = \"UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY\"\n        section_ids = [\n            \"UC_x5XG1OV2P6uZZ5FSM9Ttw.npYvuMz0_es\",\n            \"UC_x5XG1OV2P6uZZ5FSM9Ttw.9_wU0qhEPR8\",\n        ]\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.CHANNEL_SECTIONS_BY_ID)\n            m.add(\"GET\", self.BASE_URL, json=self.CHANNEL_SECTIONS_BY_IDS)\n\n            section_res = self.api.get_channel_sections_by_id(\n                section_id=section_id,\n            )\n            self.assertEqual(section_res.kind, \"youtube#channelSectionListResponse\")\n            self.assertEqual(len(section_res.items), 1)\n            self.assertEqual(section_res.items[0].id, section_id)\n\n            section_multi_res = self.api.get_channel_sections_by_id(\n                section_id=section_ids, parts=[\"id\", \"snippet\"], return_json=True\n            )\n\n            self.assertEqual(len(section_multi_res[\"items\"]), 2)\n            self.assertIn(section_multi_res[\"items\"][1][\"id\"], section_ids)\n\n    def testGetChannelSectionsByChannel(self) -> None:\n        channel_id = \"UCa-vrCLQHviTOVnEKDOdetQ\"\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.CHANNEL_SECTIONS_BY_CHANNEL)\n\n            section_by_channel = self.api.get_channel_sections_by_channel(\n                channel_id=channel_id,\n            )\n\n            self.assertEqual(len(section_by_channel.items), 3)\n            self.assertEqual(\n                section_by_channel.items[0].id, \"UCa-vrCLQHviTOVnEKDOdetQ.jNQXAC9IVRw\"\n            )\n\n            section_by_me = self.api.get_channel_sections_by_channel(\n                mine=True,\n                return_json=True,\n            )\n\n            self.assertEqual(\n                section_by_me[\"items\"][2][\"id\"], \"UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY\"\n            )\n"
  },
  {
    "path": "tests/apis/test_channels.py",
    "content": "import json\nimport unittest\n\nfrom requests import HTTPError\n\nimport pyyoutube\nimport responses\n\n\nclass ApiChannelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/channels\"\n\n    with open(BASE_PATH + \"channel_info_single.json\", \"rb\") as f:\n        CHANNELS_INFO_SINGLE = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_info_multi.json\", \"rb\") as f:\n        CHANNELS_INFO_MULTI = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n\n    def testSendRequest(self) -> None:\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            api = pyyoutube.Api(client_id=\"id\", client_secret=\"secret\")\n            api._request(\"channels\", post_args={\"a\": \"a\"})\n        with responses.RequestsMock() as m:\n            m.add(\"POST\", self.BASE_URL, json={})\n            api = pyyoutube.Api(access_token=\"access token\")\n            res = api._request(\"channels\", post_args={\"a\": \"a\"})\n            self.assertTrue(res)\n\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\"GET\", self.BASE_URL, body=HTTPError(\"Exception\"))\n                self.api.get_channel_info(channel_id=\"channel_id\", parts=\"id,snippet\")\n\n    # TODO need to separate.\n    def testParseResponse(self) -> None:\n        with open(\"testdata/error_response.json\", \"rb\") as f:\n            error_response = json.loads(f.read().decode(\"utf-8\"))\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=error_response, status=400)\n\n            with self.assertRaises(pyyoutube.PyYouTubeException):\n                self.api.get_channel_info(\n                    channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\", parts=\"id,snippet,statistics\"\n                )\n\n    def testGetChannelInfo(self) -> None:\n        # test params checker\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_channel_info(parts=\"id,invideoPromotion\")\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_channel_info()\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.CHANNELS_INFO_SINGLE)\n\n            res_by_channel_id = self.api.get_channel_info(\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\", parts=\"id,snippet,statistics\"\n            )\n            self.assertEqual(res_by_channel_id.items[0].id, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n\n            res_by_channel_handle = self.api.get_channel_info(\n                for_handle=\"googledevelopers\", return_json=True\n            )\n            self.assertEqual(\n                res_by_channel_handle[\"items\"][0][\"snippet\"][\"customUrl\"],\n                \"@googledevelopers\",\n            )\n\n            res_by_channel_name = self.api.get_channel_info(\n                for_username=\"GoogleDevelopers\", return_json=True\n            )\n            self.assertEqual(\n                res_by_channel_name[\"items\"][0][\"id\"], \"UC_x5XG1OV2P6uZZ5FSM9Ttw\"\n            )\n\n            res_by_mine = self.api.get_channel_info(mine=True)\n            self.assertEqual(res_by_mine.items[0].id, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.CHANNELS_INFO_MULTI)\n\n            res_by_channel_id_list = self.api.get_channel_info(\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw,UCK8sQmJBp8GCxrOtXWBpyEA\",\n                parts=\"id,snippet\",\n            )\n            self.assertEqual(len(res_by_channel_id_list.items), 2)\n            self.assertEqual(\n                res_by_channel_id_list.items[1].id, \"UCK8sQmJBp8GCxrOtXWBpyEA\"\n            )\n"
  },
  {
    "path": "tests/apis/test_comment_threads.py",
    "content": "import json\nimport unittest\n\nimport responses\nimport pyyoutube\n\n\nclass ApiCommentThreadTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/comment_threads/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/commentThreads\"\n\n    with open(BASE_PATH + \"comment_thread_single.json\", \"rb\") as f:\n        COMMENT_THREAD_INFO_SINGLE = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_threads_multi.json\", \"rb\") as f:\n        COMMENT_THREAD_INFO_MULTI = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_threads_all_to_me.json\", \"rb\") as f:\n        COMMENT_THREAD_ALL_TO_ME = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_threads_by_channel.json\", \"rb\") as f:\n        COMMENT_THREAD_BY_CHANNEL = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_threads_with_search.json\", \"rb\") as f:\n        COMMENT_THREAD_BY_SEARCH = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_threads_by_video_paged_1.json\", \"rb\") as f:\n        COMMENT_THREAD_BY_VIDEO_P_1 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_threads_by_video_paged_2.json\", \"rb\") as f:\n        COMMENT_THREAD_BY_VIDEO_P_2 = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n        self.api_with_token = pyyoutube.Api(access_token=\"access token\")\n\n    def testGetCommentThreadById(self) -> None:\n        # test parts\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_comment_thread_by_id(\n                comment_thread_id=\"id\", parts=\"id,not_part\"\n            )\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENT_THREAD_INFO_SINGLE)\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENT_THREAD_INFO_MULTI)\n\n            res_by_single_id = self.api.get_comment_thread_by_id(\n                comment_thread_id=\"UgxKREWxIgDrw8w2e_Z4AaABAg\",\n                parts=\"id,snippet\",\n                text_format=\"plain_text\",\n                return_json=True,\n            )\n            self.assertEqual(\n                res_by_single_id[\"kind\"], \"youtube#commentThreadListResponse\"\n            )\n            self.assertEqual(len(res_by_single_id[\"items\"]), 1)\n            self.assertEqual(\n                res_by_single_id[\"items\"][0][\"id\"], \"UgxKREWxIgDrw8w2e_Z4AaABAg\"\n            )\n\n            res_by_multi_id = self.api.get_comment_thread_by_id(\n                comment_thread_id=[\n                    \"UgxKREWxIgDrw8w2e_Z4AaABAg\",\n                    \"UgyrVQaFfEdvaSzstj14AaABAg\",\n                ],\n                parts=[\"id\", \"snippet\"],\n            )\n            self.assertEqual(res_by_multi_id.pageInfo.totalResults, 2)\n            self.assertEqual(res_by_multi_id.items[1].id, \"UgyrVQaFfEdvaSzstj14AaABAg\")\n\n    def testGetCommentThreads(self) -> None:\n        # test no params\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_comment_threads()\n        # test parts\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_comment_threads(all_to_channel_id=\"id\", parts=\"id,not_part\")\n\n        # test with all to channel.\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENT_THREAD_ALL_TO_ME)\n\n            res_by_all = self.api_with_token.get_comment_threads(\n                all_to_channel_id=\"UCa-vrCLQHviTOVnEKDOdetQ\",\n                parts=\"id,snippet\",\n                moderation_status=\"published\",\n                order=\"time\",\n                return_json=True,\n            )\n            self.assertEqual(res_by_all[\"kind\"], \"youtube#commentThreadListResponse\")\n            self.assertEqual(res_by_all[\"pageInfo\"][\"totalResults\"], 4)\n            self.assertEqual(len(res_by_all[\"items\"]), 4)\n            self.assertEqual(res_by_all[\"items\"][0][\"id\"], \"UgyWeTdgc4sc1xgmbld4AaABAg\")\n\n        # test with channel\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENT_THREAD_BY_CHANNEL)\n\n            res_by_channel = self.api.get_comment_threads(\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n            )\n            self.assertEqual(res_by_channel.pageInfo.totalResults, 2)\n            self.assertEqual(\n                res_by_channel.items[0].snippet.channelId, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\"\n            )\n\n        # test with search\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENT_THREAD_BY_SEARCH)\n\n            res_by_search = self.api.get_comment_threads(\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n                search_terms=\"Hello\",\n            )\n            self.assertEqual(res_by_search.pageInfo.totalResults, 1)\n            self.assertEqual(\n                res_by_channel.items[0].snippet.channelId, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\"\n            )\n            self.assertIn(\n                \"Hello\",\n                res_by_channel.items[0].snippet.topLevelComment.snippet.textDisplay,\n            )\n\n        # test with video\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENT_THREAD_BY_VIDEO_P_1)\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENT_THREAD_BY_VIDEO_P_2)\n\n            res_by_video = self.api.get_comment_threads(\n                video_id=\"F1UP7wRCPH8\",\n                count=8,\n                limit=5,\n            )\n            self.assertEqual(len(res_by_video.items), 8)\n            self.assertEqual(res_by_video.items[0].snippet.videoId, \"F1UP7wRCPH8\")\n\n        # test get all items\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENT_THREAD_BY_VIDEO_P_1)\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENT_THREAD_BY_VIDEO_P_2)\n\n            res_by_video = self.api.get_comment_threads(\n                video_id=\"F1UP7wRCPH8\",\n                count=None,\n            )\n            self.assertEqual(len(res_by_video.items), 10)\n\n        # test use page token\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENT_THREAD_BY_VIDEO_P_2)\n\n            res_by_video = self.api.get_comment_threads(\n                video_id=\"F1UP7wRCPH8\",\n                count=None,\n                page_token=\"QURTSl9pMzdZOUVzMkI0czlmRmNjSVBPcTBTdzVzajUydDVnbE5SNElWS0l5WU12amYweVotdzF5c1hTNmxzUmVIcEZXbmVEVFMzNVJmWk82TVVwUlB2LWh5aUpOQlA5TGQzTWZEcHlTeTd2dlNGRUFZaVF0cmtJd01BTHlnOG0=\",\n            )\n            self.assertEqual(len(res_by_video.items), 5)\n"
  },
  {
    "path": "tests/apis/test_comments.py",
    "content": "import json\nimport unittest\n\nimport responses\n\nimport pyyoutube\n\n\nclass ApiCommentTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/comments/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/comments\"\n\n    with open(BASE_PATH + \"comments_single.json\", \"rb\") as f:\n        COMMENTS_INFO_SINGLE = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comments_multi.json\", \"rb\") as f:\n        COMMENTS_INFO_MULTI = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comments_by_parent_paged_1.json\", \"rb\") as f:\n        COMMENTS_PAGED_1 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comments_by_parent_paged_2.json\", \"rb\") as f:\n        COMMENTS_PAGED_2 = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n\n    def testGetCommentById(self) -> None:\n        # test parts\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_comment_by_id(comment_id=\"id\", parts=\"id,not_part\")\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENTS_INFO_SINGLE)\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENTS_INFO_MULTI)\n\n            res_by_single = self.api.get_comment_by_id(\n                comment_id=\"UgyUBI0HsgL9emxcZpR4AaABAg\",\n                parts=[\"id\", \"snippet\"],\n                return_json=True,\n            )\n            self.assertEqual(res_by_single[\"kind\"], \"youtube#commentListResponse\")\n            self.assertEqual(len(res_by_single[\"items\"]), 1)\n            self.assertEqual(\n                res_by_single[\"items\"][0][\"id\"], \"UgyUBI0HsgL9emxcZpR4AaABAg\"\n            )\n\n            res_by_multi = self.api.get_comment_by_id(\n                comment_id=[\"UgyUBI0HsgL9emxcZpR4AaABAg\", \"Ugzi3lkqDPfIOirGFLh4AaABAg\"],\n                parts=(\"id\", \"snippet\"),\n            )\n            self.assertEqual(len(res_by_multi.items), 2)\n            self.assertEqual(res_by_multi.items[1].id, \"Ugzi3lkqDPfIOirGFLh4AaABAg\")\n\n    def testGetCommentsByParentId(self) -> None:\n        # test parts\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_comments(parent_id=\"id\", parts=\"id,not_part\")\n\n        # test paged\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENTS_PAGED_1)\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENTS_PAGED_2)\n\n            res_by_parent = self.api.get_comments(\n                parent_id=\"Ugw5zYU6n9pmIgAZWvN4AaABAg\",\n                parts=\"id,snippet\",\n                limit=2,\n            )\n            self.assertEqual(res_by_parent.kind, \"youtube#commentListResponse\")\n            self.assertEqual(len(res_by_parent.items), 3)\n            self.assertEqual(\n                res_by_parent.items[0].id,\n                \"Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za6voUoRh\",\n            )\n\n        # test count\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENTS_PAGED_1)\n\n            res_by_parent = self.api.get_comments(\n                parent_id=\"Ugw5zYU6n9pmIgAZWvN4AaABAg\",\n                parts=\"id,snippet\",\n                count=2,\n                limit=2,\n                return_json=True,\n            )\n            self.assertEqual(len(res_by_parent[\"items\"]), 2)\n            self.assertEqual(\n                res_by_parent[\"items\"][0][\"id\"],\n                \"Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za6voUoRh\",\n            )\n\n        # test get all comments\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENTS_PAGED_1)\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENTS_PAGED_2)\n            res_by_parent = self.api.get_comments(\n                parent_id=\"Ugw5zYU6n9pmIgAZWvN4AaABAg\", parts=\"id,snippet\", count=None\n            )\n            self.assertEqual(len(res_by_parent.items), 3)\n\n        # test use page token\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.COMMENTS_PAGED_2)\n            res_by_parent = self.api.get_comments(\n                parent_id=\"Ugw5zYU6n9pmIgAZWvN4AaABAg\",\n                parts=\"id,snippet\",\n                count=None,\n                page_token=\"R0FJeVZnbzBJTl9zNXRxNXlPWUNNaWtRQUJpQ3RNeW4wcFBtQWlBQktBTXdDam9XT1RGNlZETmpXV0kxUWpJNU1YcGhOV1ZLZUhwek1SSWVDQVVTR2xWbmR6VjZXVlUyYmpsd2JVbG5RVnBYZGs0MFFXRkJRa0ZuT2lBSUFSSWNOVHBWWjNjMWVsbFZObTQ1Y0cxSlowRmFWM1pPTkVGaFFVSkJadw==\",\n            )\n            self.assertEqual(len(res_by_parent.items), 1)\n"
  },
  {
    "path": "tests/apis/test_i18ns.py",
    "content": "import json\nimport unittest\n\nimport responses\nimport pyyoutube\n\n\nclass ApiI18nTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/i18ns/\"\n    REGION_URL = \"https://www.googleapis.com/youtube/v3/i18nRegions\"\n    LANGUAGE_URL = \"https://www.googleapis.com/youtube/v3/i18nLanguages\"\n\n    with open(BASE_PATH + \"regions_res.json\", \"rb\") as f:\n        REGIONS_RES = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"language_res.json\", \"rb\") as f:\n        LANGUAGE_RES = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n\n    def testGetI18nRegions(self) -> None:\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.REGION_URL, json=self.REGIONS_RES)\n\n            regions = self.api.get_i18n_regions(parts=[\"snippet\"])\n            self.assertEqual(regions.kind, \"youtube#i18nRegionListResponse\")\n            self.assertEqual(len(regions.items), 4)\n            self.assertEqual(regions.items[0].id, \"VE\")\n\n            regions_json = self.api.get_i18n_regions(return_json=True)\n            self.assertEqual(len(regions_json[\"items\"]), 4)\n\n    def testGetI18nLanguages(self) -> None:\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.LANGUAGE_URL, json=self.LANGUAGE_RES)\n\n            languages = self.api.get_i18n_languages(parts=[\"snippet\"])\n            self.assertEqual(len(languages.items), 5)\n            self.assertEqual(languages.items[0].id, \"zh-CN\")\n\n            languages_json = self.api.get_i18n_languages(return_json=True)\n            self.assertEqual(len(languages_json[\"items\"]), 5)\n"
  },
  {
    "path": "tests/apis/test_members.py",
    "content": "import json\nimport unittest\n\nimport responses\nimport pyyoutube\n\n\nclass ApiMembersTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/members/\"\n    MEMBERS_URL = \"https://www.googleapis.com/youtube/v3/members\"\n    MEMBERSHIP_LEVEL_URL = \"https://www.googleapis.com/youtube/v3/membershipsLevels\"\n\n    with open(BASE_PATH + \"members_data.json\", \"rb\") as f:\n        MEMBERS_RES = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"membership_levels.json\", \"rb\") as f:\n        MEMBERSHIP_LEVEL_RES = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(access_token=\"Authorize token\")\n\n    def testGetMembers(self) -> None:\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.MEMBERS_URL, json=self.MEMBERS_RES)\n\n            members = self.api.get_members(parts=[\"snippet\"])\n            self.assertEqual(members.kind, \"youtube#memberListResponse\")\n            self.assertEqual(len(members.items), 2)\n\n            members_json = self.api.get_members(\n                page_token=\"token\",\n                count=None,\n                has_access_to_level=\"high\",\n                filter_by_member_channel_id=\"id\",\n                return_json=True,\n            )\n            self.assertEqual(len(members_json[\"items\"]), 2)\n\n    def testGetMembershipLevels(self) -> None:\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.MEMBERSHIP_LEVEL_URL, json=self.MEMBERSHIP_LEVEL_RES)\n\n            membership_levels = self.api.get_membership_levels(parts=[\"id\", \"snippet\"])\n            self.assertEqual(\n                membership_levels.kind, \"youtube#membershipsLevelListResponse\"\n            )\n            self.assertEqual(len(membership_levels.items), 2)\n\n            membership_levels_json = self.api.get_membership_levels(return_json=True)\n            self.assertEqual(len(membership_levels_json[\"items\"]), 2)\n"
  },
  {
    "path": "tests/apis/test_playlist_items.py",
    "content": "import json\nimport unittest\n\nimport responses\n\nimport pyyoutube\n\n\nclass ApiPlaylistItemTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/playlist_items/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/playlistItems\"\n\n    with open(BASE_PATH + \"playlist_items_single.json\", \"rb\") as f:\n        PLAYLIST_ITEM_INFO_SINGLE = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_items_multi.json\", \"rb\") as f:\n        PLAYLIST_ITEM_INFO_MULTI = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_items_paged_1.json\", \"rb\") as f:\n        PLAYLIST_ITEM_PAGED_1 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_items_paged_2.json\", \"rb\") as f:\n        PLAYLIST_ITEM_PAGED_2 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_items_filter_video.json\", \"rb\") as f:\n        PLAYLIST_ITEM_FILTER = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n\n    def testGetPlaylistItemById(self) -> None:\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLIST_ITEM_INFO_SINGLE)\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLIST_ITEM_INFO_MULTI)\n\n            res_by_single_id = self.api.get_playlist_item_by_id(\n                playlist_item_id=\"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2\",\n                parts=\"id,snippet\",\n                return_json=True,\n            )\n            self.assertEqual(\n                res_by_single_id[\"kind\"], \"youtube#playlistItemListResponse\"\n            )\n            self.assertEqual(\n                res_by_single_id[\"items\"][0][\"id\"],\n                \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2\",\n            )\n\n            res_by_multi_id = self.api.get_playlist_item_by_id(\n                playlist_item_id=[\n                    \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2\",\n                    \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4yODlGNEE0NkRGMEEzMEQy\",\n                ],\n                parts=[\"id\", \"snippet\"],\n            )\n            self.assertEqual(res_by_multi_id.pageInfo.totalResults, 2)\n            self.assertEqual(len(res_by_multi_id.items), 2)\n            self.assertEqual(\n                res_by_multi_id.items[1].id,\n                \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS4yODlGNEE0NkRGMEEzMEQy\",\n            )\n\n    def testGetPlaylistItems(self) -> None:\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_playlist_items(playlist_id=\"id\", parts=\"id,not_part\")\n\n        # test paged\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_1)\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_2)\n\n            res_by_playlist = self.api.get_playlist_items(\n                playlist_id=\"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n                parts=\"id,snippet\",\n                limit=10,\n                count=20,\n            )\n            self.assertEqual(res_by_playlist.kind, \"youtube#playlistItemListResponse\")\n            self.assertEqual(res_by_playlist.pageInfo.totalResults, 13)\n            self.assertEqual(len(res_by_playlist.items), 13)\n            self.assertEqual(\n                res_by_playlist.items[0].id,\n                \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2\",\n            )\n\n        # test count\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_1)\n\n            res_by_playlist = self.api.get_playlist_items(\n                playlist_id=\"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n                parts=\"id,snippet\",\n                limit=10,\n                count=5,\n            )\n            self.assertEqual(res_by_playlist.kind, \"youtube#playlistItemListResponse\")\n            self.assertEqual(res_by_playlist.pageInfo.totalResults, 13)\n            self.assertEqual(len(res_by_playlist.items), 5)\n            self.assertEqual(\n                res_by_playlist.items[0].id,\n                \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2\",\n            )\n\n        # test get all items\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_1)\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_2)\n\n            res_by_playlist = self.api.get_playlist_items(\n                playlist_id=\"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n                parts=\"id,snippet\",\n                count=None,\n            )\n            self.assertEqual(res_by_playlist.pageInfo.totalResults, 13)\n            self.assertEqual(len(res_by_playlist.items), 13)\n\n        # test filter\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLIST_ITEM_FILTER)\n\n            res_by_filter = self.api.get_playlist_items(\n                playlist_id=\"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n                parts=(\"id\", \"snippet\"),\n                video_id=\"VCv-KKIkLns\",\n                return_json=True,\n            )\n\n            self.assertEqual(res_by_filter[\"pageInfo\"][\"totalResults\"], 1)\n            self.assertEqual(\n                res_by_filter[\"items\"][0][\"snippet\"][\"resourceId\"][\"videoId\"],\n                \"VCv-KKIkLns\",\n            )\n\n        # test use page token\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLIST_ITEM_PAGED_2)\n\n            res_by_playlist = self.api.get_playlist_items(\n                playlist_id=\"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n                parts=\"id,snippet\",\n                page_token=\"CAoQAA\",\n                count=3,\n            )\n            self.assertEqual(len(res_by_playlist.items), 3)\n"
  },
  {
    "path": "tests/apis/test_playlists.py",
    "content": "import json\nimport unittest\n\nimport responses\n\nimport pyyoutube\n\n\nclass ApiPlaylistTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/playlists/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/playlists\"\n\n    with open(BASE_PATH + \"playlists_single.json\", \"rb\") as f:\n        PLAYLISTS_INFO_SINGLE = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlists_multi.json\", \"rb\") as f:\n        PLAYLISTS_INFO_MULTI = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlists_paged_1.json\", \"rb\") as f:\n        PLAYLISTS_PAGED_1 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlists_paged_2.json\", \"rb\") as f:\n        PLAYLISTS_PAGED_2 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlists_mine.json\", \"rb\") as f:\n        PLAYLISTS_MINE = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n        self.api_with_access_token = pyyoutube.Api(access_token=\"access token\")\n\n    def testGetPlaylistById(self) -> None:\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLISTS_INFO_SINGLE)\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLISTS_INFO_MULTI)\n\n            res_by_playlist_id = self.api.get_playlist_by_id(\n                playlist_id=\"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n                parts=\"id,snippet\",\n                return_json=True,\n            )\n            self.assertEqual(res_by_playlist_id[\"kind\"], \"youtube#playlistListResponse\")\n            self.assertEqual(\n                res_by_playlist_id[\"items\"][0][\"id\"],\n                \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n            )\n\n            res_by_playlist_multi_id = self.api.get_playlist_by_id(\n                playlist_id=[\n                    \"PLOU2XLYxmsIJXsH2htG1g0NUjHGq62Q7i\",\n                    \"PLOU2XLYxmsIJJVnHWmd1qfr0Caq4VZCu4\",\n                ],\n                parts=[\"id\", \"snippet\"],\n            )\n            self.assertEqual(len(res_by_playlist_multi_id.items), 2)\n            self.assertEqual(\n                res_by_playlist_multi_id.items[1].id,\n                \"PLOU2XLYxmsIJJVnHWmd1qfr0Caq4VZCu4\",\n            )\n\n    def testGetPlaylists(self) -> None:\n        # test params checker\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_playlists(parts=\"id,not_part\")\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_playlists()\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLISTS_PAGED_1)\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLISTS_PAGED_2)\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLISTS_MINE)\n\n            res_by_channel_id = self.api.get_playlists(\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n                limit=10,\n                count=13,\n            )\n            self.assertEqual(res_by_channel_id.pageInfo.totalResults, 422)\n            self.assertEqual(len(res_by_channel_id.items), 13)\n            self.assertEqual(\n                res_by_channel_id.items[0].snippet.channelId, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\"\n            )\n\n            res_by_mine = self.api_with_access_token.get_playlists(\n                mine=True, limit=10, count=10, return_json=True\n            )\n            self.assertEqual(len(res_by_mine[\"items\"]), 2)\n            self.assertEqual(\n                res_by_mine[\"items\"][0][\"id\"], \"PLOU2XLYxmsIIOSO0eWuj-6yQmdakarUzN\"\n            )\n\n        # test for all items\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLISTS_PAGED_1)\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLISTS_PAGED_2)\n\n            res_by_channel_id = self.api.get_playlists(\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n                count=None,\n            )\n            self.assertEqual(len(res_by_channel_id.items), 20)\n\n        # test for page token\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.PLAYLISTS_PAGED_2)\n\n            res_by_channel_id = self.api.get_playlists(\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\", count=None, page_token=\"CAoQAA\"\n            )\n            self.assertEqual(len(res_by_channel_id.items), 10)\n"
  },
  {
    "path": "tests/apis/test_search.py",
    "content": "import json\nimport unittest\n\nimport responses\n\nimport pyyoutube\n\n\nclass ApiSearchTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/search/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/search\"\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n\n    def testSearch(self) -> None:\n        with open(self.BASE_PATH + \"search_videos_by_channel.json\", \"rb\") as f:\n            search_videos_by_channel = json.loads(f.read().decode(\"utf-8\"))\n        with open(self.BASE_PATH + \"search_by_location.json\", \"rb\") as f:\n            search_by_location = json.loads(f.read().decode(\"utf-8\"))\n        with open(self.BASE_PATH + \"search_by_event.json\", \"rb\") as f:\n            search_by_event = json.loads(f.read().decode(\"utf-8\"))\n        with open(self.BASE_PATH + \"search_channels.json\", \"rb\") as f:\n            search_channels = json.loads(f.read().decode(\"utf-8\"))\n\n        # test search videos with channel\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=search_videos_by_channel)\n\n            res = self.api.search(\n                parts=\"snippet\",\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n                q=\"news\",\n                count=5,\n            )\n            self.assertEqual(res.items[0].id.videoId, \"LrQWzOkC0XQ\")\n            self.assertEqual(res.items[0].snippet.channelId, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n\n        # test search locations\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=search_by_location)\n            res = self.api.search(\n                location=\"21.5922529, -158.1147114\",\n                location_radius=\"10mi\",\n                q=\"surfing\",\n                parts=[\"snippet\"],\n                count=5,\n                published_after=\"2020-02-01T00:00:00Z\",\n                published_before=\"2020-03-01T00:00:00Z\",\n                safe_search=\"moderate\",\n                search_type=\"video\",\n            )\n            self.assertEqual(res.pageInfo.resultsPerPage, 5)\n            self.assertEqual(len(res.items), 5)\n            self.assertEqual(res.items[0].snippet.channelId, \"UCo_q6aOlvPH7M-j_XGWVgXg\")\n\n        # test search event\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=search_by_event)\n            res = self.api.search(\n                event_type=\"live\",\n                q=\"news\",\n                count=25,\n                limit=25,\n                parts=[\"snippet\"],\n                search_type=\"video\",\n                topic_id=\"/m/09s1f\",\n                order=\"viewCount\",\n            )\n\n            self.assertEqual(res.pageInfo.resultsPerPage, 25)\n            self.assertEqual(len(res.items), 25)\n            self.assertEqual(res.items[0].snippet.channelId, \"UCDGiCfCZIV5phsoGiPwIcyQ\")\n\n        # test search channel\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=search_channels)\n\n            res_channels = self.api.search(\n                parts=[\"snippet\"],\n                channel_type=\"any\",\n                count=5,\n                search_type=\"channel\",\n            )\n            self.assertEqual(res_channels.pageInfo.resultsPerPage, 5)\n            self.assertEqual(\n                res_channels.items[0].snippet.channelId, \"UCxRULEz6kS0PMxCzOY25GhQ\"\n            )\n\n    def testSearchByKeywords(self) -> None:\n        with open(self.BASE_PATH + \"search_by_keywords_p1.json\", \"rb\") as f:\n            res_p1 = json.loads(f.read().decode(\"utf-8\"))\n        with open(self.BASE_PATH + \"search_by_keywords_p2.json\", \"rb\") as f:\n            res_p2 = json.loads(f.read().decode(\"utf-8\"))\n\n        # test parts\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.search_by_keywords(q=\"x\", parts=\"id,not_part\")\n\n        # test response\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=res_p1)\n            m.add(\"GET\", self.BASE_URL, json=res_p2)\n\n            res_json = self.api.search_by_keywords(\n                q=\"surfing\", count=30, limit=25, return_json=True\n            )\n            self.assertEqual(res_json[\"kind\"], \"youtube#searchListResponse\")\n            self.assertEqual(res_json[\"regionCode\"], \"JP\")\n            self.assertEqual(res_json[\"pageInfo\"][\"totalResults\"], 1000000)\n            self.assertEqual(len(res_json[\"items\"]), 30)\n\n            res = self.api.search_by_keywords(\n                q=\"surfing\",\n                parts=[\"snippet\"],\n                count=25,\n            )\n            self.assertEqual(res.pageInfo.resultsPerPage, 25)\n            self.assertEqual(res.items[0].id.videoId, \"-2IlD-x8wvY\")\n            self.assertEqual(res.items[0].snippet.channelId, \"UCeYue9Nbodzg3T1Nt88E3fg\")\n\n    def testSearchByRelatedToVideoId(self) -> None:\n        with open(self.BASE_PATH + \"search_by_related_video.json\", \"rb\") as f:\n            search_by_related_video = json.loads(f.read().decode(\"utf-8\"))\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=search_by_related_video)\n\n            res = self.api.search_by_related_video(\n                related_to_video_id=\"Ks-_Mh1QhMc\",\n                region_code=\"US\",\n                relevance_language=\"en\",\n                safe_search=\"moderate\",\n                count=5,\n            )\n\n            self.assertEqual(res.pageInfo.resultsPerPage, 5)\n            self.assertEqual(len(res.items), 5)\n            self.assertEqual(res.regionCode, \"US\")\n            self.assertEqual(res.items[0].snippet.channelId, \"UCAuUUnT6oDeKwE6v1NGQxug\")\n\n    def testSearchByDeveloper(self) -> None:\n        with open(self.BASE_PATH + \"search_by_developer.json\", \"rb\") as f:\n            search_by_developer = json.loads(f.read().decode(\"utf-8\"))\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=search_by_developer)\n\n            res_dev = self.api.search_by_developer(\n                parts=[\"snippet\"],\n                q=\"news\",\n                count=5,\n                page_token=\"CAUQAA\",\n                video_category_id=\"17\",\n                video_caption=\"any\",\n                video_definition=\"any\",\n                video_dimension=\"any\",\n                video_duration=\"any\",\n                video_embeddable=\"any\",\n                video_license=\"any\",\n                video_paid_product_placement=\"any\",\n                video_syndicated=\"any\",\n                video_type=\"any\",\n            )\n            self.assertEqual(res_dev.pageInfo.resultsPerPage, 5)\n            self.assertEqual(\n                res_dev.items[0].snippet.channelId, \"UCeY0bbntWzzVIaj2z3QigXg\"\n            )\n\n    def testSearchByMine(self) -> None:\n        with open(self.BASE_PATH + \"search_by_mine.json\", \"rb\") as f:\n            search_by_mine = json.loads(f.read().decode(\"utf-8\"))\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=search_by_mine)\n\n            res_mine = self.api.search_by_mine(\n                parts=[\"snippet\"],\n            )\n\n            self.assertEqual(res_mine.pageInfo.totalResults, 2)\n            self.assertEqual(\n                res_mine.items[0].snippet.channelId, \"UCa-vrCLQHviTOVnEKDOdetQ\"\n            )\n"
  },
  {
    "path": "tests/apis/test_subscriptions.py",
    "content": "import json\nimport unittest\n\nimport responses\n\nimport pyyoutube\n\n\nclass ApiPlaylistTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/subscriptions/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/subscriptions\"\n\n    with open(BASE_PATH + \"subscription_zero.json\", \"rb\") as f:\n        SUBSCRIPTIONS_ZERO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"subscriptions_by_id.json\", \"rb\") as f:\n        SUBSCRIPTIONS_BY_ID = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"subscriptions_by_channel_p1.json\", \"rb\") as f:\n        SUBSCRIPTIONS_BY_CHANNEL_P1 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"subscriptions_by_channel_p2.json\", \"rb\") as f:\n        SUBSCRIPTIONS_BY_CHANNEL_P2 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"subscriptions_by_channel_with_filter.json\", \"rb\") as f:\n        SUBSCRIPTIONS_BY_CHANNEL_FILTER = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"subscriptions_by_mine_p1.json\", \"rb\") as f:\n        SUBSCRIPTIONS_BY_MINE_P1 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"subscriptions_by_mine_p2.json\", \"rb\") as f:\n        SUBSCRIPTIONS_BY_MINE_P2 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"subscriptions_by_mine_filter.json\", \"rb\") as f:\n        SUBSCRIPTIONS_BY_MINE_FILTER = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n        self.api_with_access_token = pyyoutube.Api(access_token=\"access token\")\n\n    def testGetSubscriptionById(self) -> None:\n        # test params checker\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api_with_access_token.get_subscription_by_id(\n                subscription_id=\"id\", parts=\"id,not_part\"\n            )\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_ZERO)\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_ID)\n\n            res_zero = self.api.get_subscription_by_id(\n                subscription_id=(\n                    \"zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo,\"\n                    \"zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo\"\n                ),\n                parts=\"id,snippet\",\n                return_json=True,\n            )\n            self.assertEqual(len(res_zero[\"items\"]), 0)\n            self.assertEqual(res_zero[\"pageInfo\"][\"totalResults\"], 0)\n\n            res_by_id = self.api_with_access_token.get_subscription_by_id(\n                subscription_id=[\n                    \"zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo\",\n                    \"zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo\",\n                ],\n                parts=\"id,snippet\",\n            )\n            self.assertEqual(len(res_by_id.items), 2)\n            self.assertEqual(res_by_id.pageInfo.totalResults, 2)\n            self.assertEqual(\n                res_by_id.items[0].id, \"zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo\"\n            )\n\n    def testGetSubscriptionByChannel(self) -> None:\n        # test params checker\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api_with_access_token.get_subscription_by_channel(\n                channel_id=\"id\", parts=\"id,not_part\"\n            )\n\n        # test count is None\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_CHANNEL_P1)\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_CHANNEL_P2)\n\n            res = self.api.get_subscription_by_channel(\n                channel_id=\"UCAuUUnT6oDeKwE6v1NGQxug\",\n                count=None,\n                limit=5,\n            )\n\n            self.assertEqual(len(res.items), 7)\n\n        # test count\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_CHANNEL_P1)\n\n            res = self.api.get_subscription_by_channel(\n                channel_id=\"UCAuUUnT6oDeKwE6v1NGQxug\",\n                count=5,\n                limit=5,\n            )\n\n            self.assertEqual(len(res.items), 5)\n\n        # test filter\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_CHANNEL_P1)\n\n            res = self.api.get_subscription_by_channel(\n                channel_id=\"UCAuUUnT6oDeKwE6v1NGQxug\",\n                for_channel_id=[\"UCsT0YIqwnpJCM-mx7-gSA4Q\", \"UCtC8aQzdEHAmuw8YvtH1CcQ\"],\n                count=2,\n                return_json=True,\n            )\n\n            self.assertEqual(len(res[\"items\"]), 2)\n\n        # test use page token\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_CHANNEL_P2)\n\n            res = self.api.get_subscription_by_channel(\n                channel_id=\"UCAuUUnT6oDeKwE6v1NGQxug\",\n                count=None,\n                limit=5,\n                page_token=\"CAUQAA\",\n            )\n\n            self.assertEqual(len(res.items), 2)\n\n    def testGetSubscriptionByMe(self) -> None:\n        # test not have required parameters\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api_with_access_token.get_subscription_by_me()\n\n        # test get all data\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_MINE_P1)\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_MINE_P2)\n\n            sub = self.api_with_access_token.get_subscription_by_me(\n                mine=True,\n                parts=[\"id\", \"snippet\"],\n                order=\"alphabetically\",\n                count=None,\n                limit=10,\n            )\n\n            self.assertEqual(len(sub.items), 15)\n            self.assertEqual(sub.pageInfo.totalResults, 16)\n            # totalResults is only an approximation/estimate.\n            # Refer: https://stackoverflow.com/questions/43507281/totalresults-count-doesnt-match-with-the-actual-results-returned-in-youtube-v3\n\n        # test count\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_MINE_P1)\n\n            sub = self.api_with_access_token.get_subscription_by_me(\n                mine=True,\n                parts=\"id,snippet\",\n                order=\"alphabetically\",\n                count=5,\n                limit=10,\n                return_json=True,\n            )\n\n            self.assertEqual(len(sub[\"items\"]), 5)\n            self.assertEqual(sub[\"pageInfo\"][\"totalResults\"], 16)\n\n        # test filter channel id\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_MINE_FILTER)\n\n            sub = self.api_with_access_token.get_subscription_by_me(\n                mine=True,\n                parts=\"id,snippet\",\n                for_channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw,UCa-vrCLQHviTOVnEKDOdetQ\",\n                count=None,\n            )\n\n            self.assertEqual(len(sub.items), 2)\n            self.assertEqual(sub.pageInfo.totalResults, 2)\n\n        # test remain\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_ZERO)\n\n            recent = self.api_with_access_token.get_subscription_by_me(\n                recent_subscriber=True\n            )\n            self.assertEqual(len(recent.items), 0)\n\n            subscriber = self.api_with_access_token.get_subscription_by_me(\n                subscriber=True\n            )\n            self.assertEqual(len(subscriber.items), 0)\n\n        # test get all data\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.SUBSCRIPTIONS_BY_MINE_P2)\n\n            sub = self.api_with_access_token.get_subscription_by_me(\n                mine=True,\n                parts=[\"id\", \"snippet\"],\n                order=\"alphabetically\",\n                count=None,\n                limit=10,\n                page_token=\"CAoQAA\",\n            )\n\n            self.assertEqual(len(sub.items), 6)\n"
  },
  {
    "path": "tests/apis/test_video_abuse_reason.py",
    "content": "import json\nimport unittest\n\nimport responses\nimport pyyoutube\n\n\nclass ApiVideoAbuseReason(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/abuse_reasons/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/videoAbuseReportReasons\"\n\n    with open(BASE_PATH + \"abuse_reason.json\", \"rb\") as f:\n        ABUSE_REASON_RES = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api_with_token = pyyoutube.Api(access_token=\"access token\")\n\n    def testGetVideoAbuseReportReason(self) -> None:\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.ABUSE_REASON_RES)\n\n            abuse_res = self.api_with_token.get_video_abuse_report_reason(\n                parts=[\"id\", \"snippet\"],\n            )\n\n            self.assertEqual(\n                abuse_res.kind, \"youtube#videoAbuseReportReasonListResponse\"\n            )\n            self.assertEqual(len(abuse_res.items), 3)\n\n            abuse_res_json = self.api_with_token.get_video_abuse_report_reason(\n                return_json=True\n            )\n\n            self.assertEqual(len(abuse_res_json[\"items\"]), 3)\n"
  },
  {
    "path": "tests/apis/test_videos.py",
    "content": "import json\nimport unittest\n\nimport responses\n\nimport pyyoutube\n\n\nclass ApiVideoTest(unittest.TestCase):\n    BASE_PATH = \"testdata/apidata/videos/\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3/videos\"\n\n    with open(BASE_PATH + \"videos_info_single.json\", \"rb\") as f:\n        VIDEOS_INFO_SINGLE = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"videos_info_multi.json\", \"rb\") as f:\n        VIDEOS_INFO_MULTI = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"videos_chart_paged_1.json\", \"rb\") as f:\n        VIDEOS_CHART_PAGED_1 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"videos_chart_paged_2.json\", \"rb\") as f:\n        VIDEOS_CHART_PAGED_2 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"videos_myrating_paged_1.json\", \"rb\") as f:\n        VIDEOS_MYRATING_PAGED_1 = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"videos_myrating_paged_2.json\", \"rb\") as f:\n        VIDEOS_MYRATING_PAGED_2 = json.loads(f.read().decode(\"utf-8\"))\n\n    def setUp(self) -> None:\n        self.api = pyyoutube.Api(api_key=\"api key\")\n        self.api_with_token = pyyoutube.Api(access_token=\"token\")\n\n    def testGetVideoById(self) -> None:\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_video_by_id(video_id=\"id\", parts=\"id,not_part\")\n\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_INFO_SINGLE)\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_INFO_MULTI)\n\n            res_by_single_id = self.api.get_video_by_id(\n                video_id=\"D-lhorsDlUQ\",\n                parts=\"id,snippet,player\",\n                max_height=480,\n                max_width=270,\n                return_json=True,\n            )\n            self.assertEqual(res_by_single_id[\"kind\"], \"youtube#videoListResponse\")\n            self.assertEqual(res_by_single_id[\"pageInfo\"][\"totalResults\"], 1)\n            video = res_by_single_id[\"items\"][0]\n            self.assertEqual(video[\"id\"], \"D-lhorsDlUQ\")\n            self.assertEqual(\n                video[\"player\"][\"embedHtml\"],\n                (\n                    '\\u003ciframe width=\"480\" height=\"270\" src=\"//www.youtube.com/embed/D-lhorsDlUQ\" '\n                    'frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; '\n                    'picture-in-picture\" allowfullscreen\\u003e\\u003c/iframe\\u003e'\n                ),\n            )\n\n            res_by_multi_id = self.api.get_video_by_id(\n                video_id=[\"D-lhorsDlUQ\", \"ovdbrdCIP7U\"]\n            )\n            self.assertEqual(res_by_multi_id.pageInfo.totalResults, 2)\n            self.assertEqual(len(res_by_multi_id.items), 2)\n            self.assertEqual(res_by_multi_id.items[0].id, \"D-lhorsDlUQ\")\n\n    def testGetVideoByChart(self) -> None:\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_videos_by_chart(chart=\"mostPopular\", parts=\"id,not_part\")\n\n        # test paged\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_1)\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_2)\n\n            res_by_chart = self.api.get_videos_by_chart(\n                chart=\"mostPopular\",\n                region_code=\"US\",\n                category_id=\"0\",\n                max_height=480,\n                max_width=270,\n                count=20,\n                limit=5,\n                return_json=True,\n            )\n            self.assertEqual(res_by_chart[\"kind\"], \"youtube#videoListResponse\")\n            self.assertEqual(res_by_chart[\"pageInfo\"][\"totalResults\"], 8)\n            self.assertEqual(len(res_by_chart[\"items\"]), 8)\n            self.assertEqual(res_by_chart[\"items\"][0][\"id\"], \"hDeuSfo_Ys0\")\n\n        # test count\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_1)\n\n            res_by_chart = self.api.get_videos_by_chart(chart=\"mostPopular\", count=3)\n            self.assertEqual(res_by_chart.pageInfo.totalResults, 8)\n            self.assertEqual(len(res_by_chart.items), 3)\n            self.assertEqual(res_by_chart.items[0].id, \"hDeuSfo_Ys0\")\n\n        # test get all items\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_1)\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_2)\n\n            res_by_chart = self.api.get_videos_by_chart(chart=\"mostPopular\", count=None)\n            self.assertEqual(res_by_chart.pageInfo.totalResults, 8)\n\n        # test use page token\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_CHART_PAGED_2)\n\n            res_by_chart = self.api.get_videos_by_chart(\n                chart=\"mostPopular\", count=None, page_token=\"CAUQAA\"\n            )\n            self.assertEqual(len(res_by_chart.items), 3)\n\n    def testGetVideoByMyRating(self) -> None:\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api_with_token.get_videos_by_myrating(\n                rating=\"like\", parts=\"id,not_part\"\n            )\n\n        # test need authorization\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            self.api.get_videos_by_myrating(rating=\"like\", parts=\"id,not_part\")\n\n        # test paged\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_1)\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_2)\n\n            res_by_my_rating = self.api_with_token.get_videos_by_myrating(\n                rating=\"like\",\n                parts=(\"id\", \"snippet\", \"player\"),\n                max_height=480,\n                max_width=270,\n                count=10,\n                limit=2,\n                return_json=True,\n            )\n            self.assertEqual(res_by_my_rating[\"kind\"], \"youtube#videoListResponse\")\n            self.assertEqual(res_by_my_rating[\"pageInfo\"][\"totalResults\"], 3)\n            self.assertEqual(len(res_by_my_rating[\"items\"]), 3)\n            self.assertEqual(res_by_my_rating[\"items\"][0][\"id\"], \"P4IfFLAX9hY\")\n\n        # test count\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_1)\n            res_by_my_rating = self.api_with_token.get_videos_by_myrating(\n                rating=\"like\",\n                parts=(\"id\", \"snippet\", \"player\"),\n                count=1,\n                limit=2,\n            )\n            self.assertEqual(res_by_my_rating.pageInfo.totalResults, 3)\n            self.assertEqual(len(res_by_my_rating.items), 1)\n            self.assertEqual(res_by_my_rating.items[0].id, \"P4IfFLAX9hY\")\n\n        # test get all items\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_1)\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_2)\n            res_by_my_rating = self.api_with_token.get_videos_by_myrating(\n                rating=\"like\",\n                parts=(\"id\", \"snippet\", \"player\"),\n                count=None,\n            )\n            self.assertEqual(res_by_my_rating.pageInfo.totalResults, 3)\n\n        # test use page token\n        with responses.RequestsMock() as m:\n            m.add(\"GET\", self.BASE_URL, json=self.VIDEOS_MYRATING_PAGED_2)\n\n            res_by_my_rating = self.api_with_token.get_videos_by_myrating(\n                rating=\"like\",\n                parts=(\"id\", \"snippet\", \"player\"),\n                count=None,\n                page_token=\"CAIQAA\",\n            )\n            self.assertEqual(len(res_by_my_rating.items), 1)\n"
  },
  {
    "path": "tests/clients/__init__.py",
    "content": ""
  },
  {
    "path": "tests/clients/base.py",
    "content": "\"\"\"\nBase class\n\"\"\"\n\n\nclass BaseTestCase:\n    BASE_PATH = \"testdata/apidata\"\n    BASE_URL = \"https://www.googleapis.com/youtube/v3\"\n    RESOURCE = \"CHANNELS\"\n\n    @property\n    def url(self):\n        return f\"{self.BASE_URL}/{self.RESOURCE}\"\n\n    def load_json(self, filename, helpers):\n        return helpers.load_json(f\"{self.BASE_PATH}/{filename}\")\n"
  },
  {
    "path": "tests/clients/test_activities.py",
    "content": "import pytest\nimport responses\n\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\n\n\nclass TestActivitiesResource(BaseTestCase):\n    RESOURCE = \"activities\"\n\n    def test_list(self, helpers, authed_cli):\n        with pytest.raises(PyYouTubeException):\n            authed_cli.activities.list()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\n                    \"activities/activities_by_channel_p1.json\", helpers\n                ),\n            )\n            res = authed_cli.activities.list(\n                parts=[\"id\", \"snippet\"],\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n                max_results=10,\n            )\n            assert len(res.items) == 10\n            assert authed_cli.activities.access_token == \"access token\"\n\n            res = authed_cli.activities.list(\n                parts=[\"id\", \"snippet\"], mine=True, max_results=10\n            )\n            assert res.items[0].snippet.type == \"upload\"\n"
  },
  {
    "path": "tests/clients/test_captions.py",
    "content": "\"\"\"\nTests for captions resources.\n\"\"\"\n\nimport io\n\nimport pytest\nimport responses\n\nimport pyyoutube.models as mds\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\nfrom pyyoutube.media import Media\n\n\nclass TestCaptionsResource(BaseTestCase):\n    RESOURCE = \"captions\"\n\n    def test_list(self, helpers, key_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"captions/captions_by_video.json\", helpers),\n            )\n\n            res = key_cli.captions.list(parts=[\"snippet\"], video_id=\"oHR3wURdJ94\")\n            assert res.items[0].id == \"SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I\"\n\n    def test_insert(self, helpers, authed_cli):\n        video_id = \"zxTVeyG1600\"\n\n        body = mds.Caption(\n            snippet=mds.CaptionSnippet(\n                name=\"日文字幕\", language=\"ja\", videoId=video_id, isDraft=True\n            )\n        )\n        media = Media(io.StringIO(\"\"\"\n        1\n        00:00:00,036 --> 00:00:00,703\n        ジメジメした天気\n        \"\"\"))\n\n        upload = authed_cli.captions.insert(\n            body=body,\n            media=media,\n        )\n        assert upload.resumable_progress == 0\n\n    def test_update(self, helpers, authed_cli):\n        caption_id = \"AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA\"\n\n        new_body = mds.Caption(\n            id=caption_id,\n            snippet=mds.CaptionSnippet(videoId=\"zxTVeyG1600\", isDraft=False),\n        )\n        media = Media(\n            io.StringIO(\"\"\"\n                1\n                00:00:00,036 --> 00:00:00,703\n                ジメジメした天気\n                \"\"\"),\n        )\n\n        upload = authed_cli.captions.update(\n            body=new_body,\n            media=media,\n        )\n        assert upload.resumable_progress == 0\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"PUT\",\n                url=self.url,\n                json=self.load_json(\"captions/update_response.json\", helpers),\n            )\n\n            caption = authed_cli.captions.update(body=new_body)\n            assert not caption.snippet.isDraft\n\n    def test_download(self, authed_cli):\n        caption_id = \"AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA\"\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=f\"{self.url}/{caption_id}\",\n            )\n            res = authed_cli.captions.download(caption_id=caption_id)\n            assert res.status_code == 200\n\n    def test_delete(self, helpers, authed_cli):\n        caption_id = \"AUieDabWmL88_xoRtxyxjTMtmvdoF9dLTW3WxfJvaThUXkNptljUijDFS-kDjyA\"\n\n        with responses.RequestsMock() as m:\n            m.add(method=\"DELETE\", url=self.url)\n            assert authed_cli.captions.delete(caption_id=caption_id)\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"DELETE\",\n                    url=self.url,\n                    status=403,\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                )\n                authed_cli.captions.delete(caption_id=caption_id)\n"
  },
  {
    "path": "tests/clients/test_channel_banners.py",
    "content": "\"\"\"\nTests for channel banners\n\"\"\"\n\nimport io\n\nfrom .base import BaseTestCase\nfrom pyyoutube.media import Media\n\n\nclass TestChannelBanners(BaseTestCase):\n    def test_insert(self, helpers, authed_cli):\n        media = Media(fd=io.StringIO(\"jpg content\"), mimetype=\"image/jpeg\")\n        upload = authed_cli.channelBanners.insert(media=media)\n\n        assert upload.resumable_progress == 0\n"
  },
  {
    "path": "tests/clients/test_channel_sections.py",
    "content": "import pytest\nimport responses\n\nimport pyyoutube.models as mds\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\n\n\nclass TestChannelBannersResource(BaseTestCase):\n    RESOURCE = \"channelSections\"\n\n    def test_list(self, helpers, authed_cli, key_cli):\n        with pytest.raises(PyYouTubeException):\n            key_cli.channelSections.list()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\n                    \"channel_sections/channel_sections_by_channel.json\", helpers\n                ),\n            )\n\n            res = key_cli.channelSections.list(\n                parts=[\"id\", \"snippet\"],\n                channel_id=\"UCa-vrCLQHviTOVnEKDOdetQ\",\n            )\n            assert res.items[0].snippet.type == \"recentUploads\"\n\n            res = authed_cli.channelSections.list(\n                mine=True,\n                parts=[\"id\", \"snippet\"],\n            )\n            assert res.items[0].snippet.channelId == \"UCa-vrCLQHviTOVnEKDOdetQ\"\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\n                    \"channel_sections/channel_sections_by_id.json\", helpers\n                ),\n            )\n            res = key_cli.channelSections.list(\n                parts=[\"id\", \"snippet\"],\n                section_id=\"UCa-vrCLQHviTOVnEKDOdetQ.nGzAI5pLbMY\",\n            )\n            assert res.items[0].snippet.type == \"multiplePlaylists\"\n\n    def test_insert(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"POST\",\n                url=self.url,\n                json=self.load_json(\"channel_sections/insert_resp.json\", helpers),\n            )\n            section = authed_cli.channelSections.insert(\n                parts=\"id,snippet,contentDetails\",\n                body=mds.ChannelSection(\n                    snippet=mds.ChannelSectionSnippet(\n                        type=\"multiplePlaylists\",\n                        position=4,\n                    ),\n                    contentDetails=mds.ChannelSectionContentDetails(\n                        playlists=[\"PLBaidt0ilCMbUdj0EppB710c_X5OuCP2g\"]\n                    ),\n                ),\n            )\n            assert section.id == \"UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM\"\n\n    def test_update(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"PUT\",\n                url=self.url,\n                json=self.load_json(\"channel_sections/insert_resp.json\", helpers),\n            )\n            section = authed_cli.channelSections.update(\n                parts=\"id,snippet,contentDetails\",\n                body=mds.ChannelSection(\n                    id=\"UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM\",\n                    snippet=mds.ChannelSectionSnippet(\n                        type=\"multiplePlaylists\",\n                        position=4,\n                    ),\n                ),\n            )\n            assert section.id == \"UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM\"\n\n    def test_delete(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"DELETE\",\n                url=self.url,\n            )\n            assert authed_cli.channelSections.delete(\n                section_id=\"UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM\"\n            )\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"DELETE\",\n                    url=self.url,\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                    status=403,\n                )\n                authed_cli.channelSections.delete(\n                    section_id=\"UCa-vrCLQHviTOVnEKDOdetQ.Zx4DA4xg9IM\"\n                )\n"
  },
  {
    "path": "tests/clients/test_channels.py",
    "content": "import pytest\nimport responses\n\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\nimport pyyoutube.models as mds\n\n\nclass TestChannelsResource(BaseTestCase):\n    RESOURCE = \"channels\"\n    channel_id = \"UC_x5XG1OV2P6uZZ5FSM9Ttw\"\n\n    def test_list(self, helpers, authed_cli, key_cli):\n        with pytest.raises(PyYouTubeException):\n            key_cli.channels.list()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"channels/info.json\", helpers),\n            )\n\n            res = key_cli.channels.list(\n                parts=\"id,snippet\",\n                channel_id=self.channel_id,\n            )\n            assert res.items[0].id == self.channel_id\n            assert key_cli.channels.api_key == \"api key\"\n\n            res = key_cli.channels.list(\n                parts=\"id,snippet\",\n                for_handle=\"@googledevelopers\",\n            )\n            assert res.items[0].snippet.customUrl == \"@googledevelopers\"\n\n            res = key_cli.channels.list(\n                parts=[\"id\", \"snippet\"], for_username=\"googledevelopers\"\n            )\n            assert res.items[0].snippet.title == \"Google Developers\"\n\n            res = authed_cli.channels.list(\n                parts=(\"id\", \"snippet\"),\n                managed_by_me=True,\n            )\n            assert res.items[0].snippet.title == \"Google Developers\"\n\n            res = authed_cli.channels.list(\n                parts={\"id\", \"snippet\"},\n                mine=True,\n            )\n            assert res.items[0].snippet.title == \"Google Developers\"\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"channels/info_multiple.json\", helpers),\n            )\n\n            res = authed_cli.channels.list(\n                parts=\"id,snippet,statistics,contentDetails,brandingSettings\",\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw,UCK8sQmJBp8GCxrOtXWBpyEA\",\n            )\n            assert len(res.items) == 2\n\n    def test_update(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"PUT\",\n                url=self.url,\n                json=self.load_json(\"channels/update_resp.json\", helpers),\n            )\n\n            updated_channel = authed_cli.channels.update(\n                part=\"brandingSettings\",\n                body=mds.Channel(\n                    brandingSettings=mds.ChannelBrandingSetting(\n                        channel=mds.ChannelBrandingSettingChannel(\n                            title=\"ikaros data\",\n                            description=\"This is a test channel.\",\n                            keywords=\"life 学习 测试\",\n                            country=\"CN\",\n                            defaultLanguage=\"en\",\n                        )\n                    )\n                ),\n            )\n            assert updated_channel.brandingSettings.channel.defaultLanguage == \"en\"\n"
  },
  {
    "path": "tests/clients/test_client.py",
    "content": "\"\"\"\nTests for client.\n\"\"\"\n\nimport pytest\n\nimport responses\nfrom requests import Response, HTTPError\n\nfrom .base import BaseTestCase\nfrom pyyoutube import Client, PyYouTubeException\n\n\nclass TestClient(BaseTestCase):\n    BASE_PATH = \"testdata\"\n    RESOURCE = \"channels\"\n\n    def test_initial(self):\n        with pytest.raises(PyYouTubeException):\n            Client()\n\n        cli = Client(api_key=\"key\", headers={\"HA\": \"P\"})\n        assert cli.session.headers[\"HA\"] == \"P\"\n\n    def test_client_secret_web(self):\n        filename = \"apidata/client_secrets/client_secret_web.json\"\n        client_secret_path = f\"{self.BASE_PATH}/{filename}\"\n        cli = Client(client_secret_path=client_secret_path)\n\n        assert cli.client_id == \"client_id\"\n        assert cli.client_secret == \"client_secret\"\n        assert cli.DEFAULT_REDIRECT_URI == \"http://localhost:5000/oauth2callback\"\n\n    def test_client_secret_installed(self):\n        filename_good = \"apidata/client_secrets/client_secret_installed_good.json\"\n        client_secret_good_path = f\"{self.BASE_PATH}/{filename_good}\"\n\n        cli = Client(client_secret_path=client_secret_good_path)\n\n        assert cli.client_id == \"client_id\"\n        assert cli.client_secret == \"client_secret\"\n\n    def test_client_secret_bad(self):\n        filename_bad = \"apidata/client_secrets/client_secret_installed_bad.json\"\n        filename_unsupported = \"apidata/client_secrets/client_secret_unsupported.json\"\n\n        client_secret_bad_path = f\"{self.BASE_PATH}/{filename_bad}\"\n        client_secret_unsupported_path = f\"{self.BASE_PATH}/{filename_unsupported}\"\n\n        with pytest.raises(PyYouTubeException):\n            Client(client_secret_path=client_secret_bad_path)\n\n        with pytest.raises(PyYouTubeException):\n            Client(client_secret_path=client_secret_unsupported_path)\n\n    def test_request(self, key_cli):\n        with pytest.raises(PyYouTubeException):\n            cli = Client(client_id=\"id\", client_secret=\"secret\")\n            cli.request(path=\"path\", enforce_auth=True)\n\n        with responses.RequestsMock() as m:\n            m.add(method=\"GET\", url=\"https://example.com\", body=\"\")\n            key_cli.request(path=\"https://example.com\")\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(method=\"GET\", url=self.url, body=HTTPError(\"Exception\"))\n                key_cli.channels.list(channel_id=\"xxxxx\")\n\n    def test_parse_response(self, key_cli, helpers):\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"GET\",\n                    url=self.url,\n                    json=self.load_json(\"error_response.json\", helpers),\n                    status=400,\n                )\n                key_cli.channels.list(id=\"xxxx\")\n\n    def test_oauth(self, helpers):\n        cli = Client(client_id=\"id\", client_secret=\"secret\")\n        url, state = cli.get_authorize_url()\n        assert state == \"Python-YouTube\"\n\n        # test oauth flow\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"POST\",\n                url=cli.EXCHANGE_ACCESS_TOKEN_URL,\n                json=self.load_json(\"apidata/access_token.json\", helpers),\n            )\n            token = cli.generate_access_token(code=\"code\")\n            assert token.access_token == \"access_token\"\n\n            refresh_token = cli.refresh_access_token(refresh_token=\"token\")\n            assert refresh_token.access_token == \"access_token\"\n\n        # test revoke access token\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"POST\",\n                url=cli.REVOKE_TOKEN_URL,\n            )\n            assert cli.revoke_access_token(token=\"token\")\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"POST\",\n                    url=cli.REVOKE_TOKEN_URL,\n                    json={\"error\": {\"code\": 400, \"message\": \"error\"}},\n                    status=400,\n                )\n                cli.revoke_access_token(token=\"token\")\n\n    def test_subscribe_push_notification(self):\n        HUB_URL = \"https://pubsubhubbub.appspot.com/subscribe\"\n        cli = Client(client_id=\"id\", client_secret=\"secret\")\n\n        # subscribe returns True on 202 Accepted\n        with responses.RequestsMock() as m:\n            m.add(method=\"POST\", url=HUB_URL, status=202)\n            result = cli.subscribe_push_notification(\n                channel_id=\"UCxxxxxx\",\n                callback_url=\"https://example.com/webhook\",\n            )\n            assert result is True\n            # verify hub.mode and hub.topic were sent correctly\n            assert m.calls[0].request.body is not None\n            assert \"hub.mode=subscribe\" in m.calls[0].request.body\n            assert \"UCxxxxxx\" in m.calls[0].request.body\n\n        # unsubscribe returns True on 202 Accepted\n        with responses.RequestsMock() as m:\n            m.add(method=\"POST\", url=HUB_URL, status=202)\n            result = cli.subscribe_push_notification(\n                channel_id=\"UCxxxxxx\",\n                callback_url=\"https://example.com/webhook\",\n                mode=\"unsubscribe\",\n            )\n            assert result is True\n            assert \"hub.mode=unsubscribe\" in m.calls[0].request.body\n\n        # sync verify returns True on 204 No Content\n        with responses.RequestsMock() as m:\n            m.add(method=\"POST\", url=HUB_URL, status=204)\n            result = cli.subscribe_push_notification(\n                channel_id=\"UCxxxxxx\",\n                callback_url=\"https://example.com/webhook\",\n                verify=\"sync\",\n            )\n            assert result is True\n\n        # optional params: lease_seconds and secret are included in request body\n        with responses.RequestsMock() as m:\n            m.add(method=\"POST\", url=HUB_URL, status=202)\n            cli.subscribe_push_notification(\n                channel_id=\"UCxxxxxx\",\n                callback_url=\"https://example.com/webhook\",\n                lease_seconds=432000,\n                secret=\"mysecret\",\n            )\n            body = m.calls[0].request.body\n            assert \"hub.lease_seconds=432000\" in body\n            assert \"hub.secret=mysecret\" in body\n\n        # hub error raises PyYouTubeException\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"POST\",\n                    url=HUB_URL,\n                    json={\"error\": {\"code\": 400, \"message\": \"bad request\"}},\n                    status=400,\n                )\n                cli.subscribe_push_notification(\n                    channel_id=\"UCxxxxxx\",\n                    callback_url=\"https://example.com/webhook\",\n                )\n"
  },
  {
    "path": "tests/clients/test_comment_threads.py",
    "content": "import pytest\nimport responses\n\nimport pyyoutube.models as mds\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\n\n\nclass TestCommentThreadsResource(BaseTestCase):\n    RESOURCE = \"commentThreads\"\n\n    def test_list(self, helpers, key_cli):\n        with pytest.raises(PyYouTubeException):\n            key_cli.commentThreads.list()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\n                    \"comment_threads/comment_threads_by_video_paged_1.json\", helpers\n                ),\n            )\n\n            res = key_cli.commentThreads.list(\n                parts=[\"id\", \"snippet\"],\n                all_threads_related_to_channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n            )\n            assert res.items[0].snippet.totalReplyCount == 0\n\n            res = key_cli.commentThreads.list(\n                parts=[\"id\", \"snippet\"],\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n            )\n            assert res.items[0].snippet.totalReplyCount == 0\n\n            res = key_cli.commentThreads.list(\n                parts=[\"id\", \"snippet\"],\n                video_id=\"F1UP7wRCPH8\",\n            )\n            assert res.items[0].snippet.videoId == \"F1UP7wRCPH8\"\n\n            res = key_cli.commentThreads.list(\n                parts=[\"id\", \"snippet\"],\n                thread_id=\"UgyZ1jqkHKYvi1-ruOZ4AaABAg,Ugy4OzAuz5uJuFt3FH54AaABAg\",\n            )\n            assert res.items[0].id == \"UgyZ1jqkHKYvi1-ruOZ4AaABAg\"\n\n    def test_insert(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"POST\",\n                url=self.url,\n                json=self.load_json(\"comment_threads/insert_response.json\", helpers),\n            )\n\n            thread = authed_cli.commentThreads.insert(\n                body=mds.CommentThread(\n                    snippet=mds.CommentThreadSnippet(\n                        videoId=\"JE8xdDp5B8Q\",\n                        topLevelComment=mds.Comment(\n                            snippet=mds.CommentSnippet(\n                                textOriginal=\"Sun from the api\",\n                            )\n                        ),\n                    )\n                ),\n                parts=[\"id\", \"snippet\"],\n            )\n            assert thread.snippet.videoId == \"JE8xdDp5B8Q\"\n"
  },
  {
    "path": "tests/clients/test_comments.py",
    "content": "import pytest\nimport responses\n\nimport pyyoutube.models as mds\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\n\n\nclass TestCommentsResource(BaseTestCase):\n    RESOURCE = \"comments\"\n\n    def test_list(self, helpers, key_cli):\n        with pytest.raises(PyYouTubeException):\n            key_cli.comments.list()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\n                    \"comments/comments_by_parent_paged_1.json\", helpers\n                ),\n            )\n            res = key_cli.comments.list(\n                parts=[\"id\", \"snippet\"],\n                parent_id=\"Ugw5zYU6n9pmIgAZWvN4AaABAg\",\n            )\n            assert (\n                res.items[0].id == \"Ugw5zYU6n9pmIgAZWvN4AaABAg.91zT3cYb5B291za6voUoRh\"\n            )\n            assert res.items[0].snippet.parentId == \"Ugw5zYU6n9pmIgAZWvN4AaABAg\"\n\n            res = key_cli.comments.list(\n                parts=[\"id\", \"snippet\"],\n                comment_id=\"UgyUBI0HsgL9emxcZpR4AaABAg,Ugzi3lkqDPfIOirGFLh4AaABAg\",\n            )\n            assert len(res.items) == 2\n\n    def test_insert(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"POST\",\n                url=self.url,\n                json=self.load_json(\"comments/insert_response.json\", helpers),\n            )\n\n            comment = authed_cli.comments.insert(\n                body=mds.Comment(\n                    snippet=mds.CommentSnippet(\n                        parentId=\"Ugy_CAftKrIUCyPr9GR4AaABAg\",\n                        textOriginal=\"wow\",\n                    )\n                ),\n                parts=[\"id\", \"snippet\"],\n            )\n            assert comment.snippet.parentId == \"Ugy_CAftKrIUCyPr9GR4AaABAg\"\n\n    def test_update(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"PUT\",\n                url=self.url,\n                json=self.load_json(\"comments/insert_response.json\", helpers),\n            )\n            comment = authed_cli.comments.update(\n                body=mds.Comment(\n                    id=\"Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt\",\n                    snippet=mds.CommentSnippet(\n                        textOriginal=\"wow\",\n                    ),\n                ),\n                parts=[\"id\", \"snippet\"],\n            )\n            assert comment.snippet.parentId == \"Ugy_CAftKrIUCyPr9GR4AaABAg\"\n\n    def test_mark_as_spam(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(method=\"POST\", url=f\"{self.url}/markAsSpam\", status=204)\n\n            assert authed_cli.comments.mark_as_spam(\n                comment_id=\"Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt\",\n            )\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"POST\",\n                    url=f\"{self.url}/markAsSpam\",\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                    status=403,\n                )\n                authed_cli.comments.mark_as_spam(\n                    comment_id=\"Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt\",\n                )\n\n    def test_set_moderation_status(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(method=\"POST\", url=f\"{self.url}/setModerationStatus\", status=204)\n\n            assert authed_cli.comments.set_moderation_status(\n                comment_id=\"Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt\",\n                moderation_status=\"rejected\",\n            )\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"POST\",\n                    url=f\"{self.url}/setModerationStatus\",\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                    status=403,\n                )\n                authed_cli.comments.set_moderation_status(\n                    comment_id=\"Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt\",\n                    moderation_status=\"published\",\n                    ban_author=True,\n                )\n\n    def test_delete(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(method=\"DELETE\", url=f\"{self.url}\", status=204)\n\n            assert authed_cli.comments.delete(\n                comment_id=\"Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt\",\n            )\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"DELETE\",\n                    url=f\"{self.url}\",\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                    status=403,\n                )\n                authed_cli.comments.delete(\n                    comment_id=\"Ugy_CAftKrIUCyPr9GR4AaABAg.9iPXEClD9lW9iPXLqKy_Pt\",\n                )\n"
  },
  {
    "path": "tests/clients/test_i18n.py",
    "content": "import responses\n\nfrom .base import BaseTestCase\n\n\nclass TestI18nLanguagesResource(BaseTestCase):\n    RESOURCE = \"i18nLanguages\"\n\n    def test_list(self, helpers, key_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"i18ns/language_res.json\", helpers),\n            )\n            res = key_cli.i18nLanguages.list(\n                parts=[\"snippet\"],\n            )\n            assert res.items[0].snippet.name == \"Chinese\"\n\n\nclass TestI18nRegionsResource(BaseTestCase):\n    RESOURCE = \"i18nRegions\"\n\n    def test_list(self, helpers, key_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"i18ns/regions_res.json\", helpers),\n            )\n            res = key_cli.i18nRegions.list(\n                parts=[\"snippet\"],\n            )\n            assert res.items[0].snippet.name == \"Venezuela\"\n"
  },
  {
    "path": "tests/clients/test_media.py",
    "content": "\"\"\"\nTests for media upload.\n\"\"\"\n\nimport io\n\nimport pytest\nimport responses\nfrom requests import Response\n\nfrom pyyoutube.error import PyYouTubeException\nfrom pyyoutube.media import Media, MediaUpload, MediaUploadProgress\n\n\nclass TestMedia:\n    def test_initial(self, tmp_path):\n        with pytest.raises(PyYouTubeException):\n            Media()\n\n        d = tmp_path / \"sub\"\n        d.mkdir()\n        f = d / \"simple.vvv\"\n        f.write_bytes(b\"asd\")\n        m = Media(filename=str(f))\n        assert m.mimetype == \"application/octet-stream\"\n\n        f1 = d / \"video.mp4\"\n        f1.write_text(\"video\")\n        m = Media(fd=f1.open(\"rb\"))\n        assert m.size == 5\n        assert m.get_bytes(0, 2)\n\n\nclass TestMediaUploadProgress:\n    def test_progress(self):\n        pg = MediaUploadProgress(10, 20)\n        assert pg.progress() == 0.5\n        assert str(pg)\n\n        pg = MediaUploadProgress(10, 0)\n        assert pg.progress() == 0.0\n\n\nclass TestMediaUpload:\n    def test_upload(self, helpers, authed_cli):\n        location = \"https://youtube.googleapis.com/upload/youtube/v3/videos?part=snippet&alt=json&uploadType=resumable&upload_id=upload_id\"\n\n        media = Media(fd=io.StringIO(\"1234567890\"), mimetype=\"video/mp4\", chunk_size=5)\n        upload = MediaUpload(\n            client=authed_cli,\n            resource=\"videos\",\n            media=media,\n            params={\"part\": \"snippet\"},\n            body={\"body\": '{\"snippet\": {dasd}}'},\n        )\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"POST\",\n                url=\"https://www.googleapis.com/upload/youtube/v3/videos\",\n                status=200,\n                adding_headers={\"location\": location},\n            )\n            m.add(\n                method=\"PUT\",\n                url=location,\n                status=308,\n                adding_headers={\n                    \"range\": \"0-4\",\n                },\n            )\n            m.add(\n                method=\"PUT\",\n                url=location,\n                json=helpers.load_json(\"testdata/apidata/videos/insert_response.json\"),\n            )\n\n            pg, body = upload.next_chunk()\n            assert pg.progress() == 0.5\n            assert body is None\n\n            pg, body = upload.next_chunk()\n            assert pg is None\n            assert body[\"id\"] == \"D-lhorsDlUQ\"\n\n    def test_upload_response(self, authed_cli, helpers):\n        location = \"https://youtube.googleapis.com/upload/youtube/v3/videos?part=snippet&alt=json&uploadType=resumable&upload_id=upload_id\"\n        media = Media(\n            fd=io.StringIO(\"1234567890\"),\n            mimetype=\"video/mp4\",\n        )\n        upload = MediaUpload(\n            client=authed_cli,\n            resource=\"videos\",\n            media=media,\n            params={\"part\": \"snippet\"},\n            body={\"body\": '{\"snippet\": {dasd}}'},\n        )\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"POST\",\n                    url=\"https://www.googleapis.com/upload/youtube/v3/videos\",\n                    status=400,\n                    json=helpers.load_json(\"testdata/error_response.json\"),\n                )\n                upload.next_chunk()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"PUT\",\n                url=location,\n                status=308,\n            )\n            upload.resumable_uri = location\n            upload.next_chunk()\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"PUT\",\n                    url=location,\n                    status=400,\n                    json=helpers.load_json(\"testdata/error_response.json\"),\n                )\n                upload.resumable_uri = location\n                upload.next_chunk()\n\n        resp = Response()\n        resp.status_code = 308\n        resp.headers = {\"location\": location}\n        upload.process_response(resp=resp)\n"
  },
  {
    "path": "tests/clients/test_members.py",
    "content": "import responses\n\nfrom .base import BaseTestCase\n\n\nclass TestMembersResource(BaseTestCase):\n    RESOURCE = \"members\"\n\n    def test_list(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"members/members_data.json\", helpers),\n            )\n\n            res = authed_cli.members.list(\n                parts=[\"snippet\"],\n                mode=\"all_current\",\n                max_results=5,\n            )\n            assert len(res.items) == 2\n"
  },
  {
    "path": "tests/clients/test_membership_levels.py",
    "content": "import responses\n\nfrom .base import BaseTestCase\n\n\nclass TestMembershipLevelsResource(BaseTestCase):\n    RESOURCE = \"membershipsLevels\"\n\n    def test_list(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"members/membership_levels.json\", helpers),\n            )\n\n            res = authed_cli.membershipsLevels.list(\n                parts=[\"id\", \"snippet\"],\n            )\n            assert len(res.items) == 2\n"
  },
  {
    "path": "tests/clients/test_playlist_items.py",
    "content": "import pytest\nimport responses\n\nimport pyyoutube.models as mds\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\n\n\nclass TestPlaylistItemsResource(BaseTestCase):\n    RESOURCE = \"playlistItems\"\n\n    def test_list(self, helpers, key_cli):\n        with pytest.raises(PyYouTubeException):\n            key_cli.playlistItems.list()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\n                    \"playlist_items/playlist_items_paged_1.json\", helpers\n                ),\n            )\n\n            res = key_cli.playlistItems.list(\n                parts=[\"id\", \"snippet\"],\n                playlist_id=\"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n                max_results=10,\n            )\n            assert len(res.items) == 10\n\n            res = key_cli.playlistItems.list(\n                playlist_item_id=\"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2\",\n                parts=[\"id\", \"snippet\"],\n            )\n            assert (\n                res.items[0].id\n                == \"UExPVTJYTFl4bXNJS3BhVjhoMEFHRTA1c28wZkF3d2ZUdy41NkI0NEY2RDEwNTU3Q0M2\"\n            )\n\n    def test_insert(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"POST\",\n                url=self.url,\n                json=self.load_json(\"playlist_items/insert_response.json\", helpers),\n            )\n\n            item = authed_cli.playlistItems.insert(\n                body=mds.PlaylistItem(\n                    snippet=mds.PlaylistItemSnippet(\n                        playlistId=\"PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS\",\n                        position=0,\n                        resourceId=mds.ResourceId(\n                            kind=\"youtube#video\", videoId=\"2sjqTHE0zok\"\n                        ),\n                    )\n                ),\n                parts=[\"id\", \"snippet\"],\n            )\n            assert item.snippet.playlistId == \"PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS\"\n\n    def test_update(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"PUT\",\n                url=self.url,\n                json=self.load_json(\"playlist_items/insert_response.json\", helpers),\n            )\n\n            item = authed_cli.playlistItems.update(\n                body=mds.PlaylistItem(\n                    snippet=mds.PlaylistItemSnippet(\n                        playlistId=\"PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS\",\n                        position=1,\n                        resourceId=mds.ResourceId(\n                            kind=\"youtube#video\", videoId=\"2sjqTHE0zok\"\n                        ),\n                    )\n                ),\n                parts=[\"id\", \"snippet\"],\n            )\n            assert item.snippet.playlistId == \"PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvS\"\n\n    def test_delete(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(method=\"DELETE\", url=self.url, status=204)\n            assert authed_cli.playlistItems.delete(\n                playlist_item_id=\"PLBaidt0ilCManGDIKr8UVBFZwN_UvMKvSxxxxx\"\n            )\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"DELETE\",\n                    url=self.url,\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                    status=403,\n                )\n                authed_cli.playlistItems.delete(playlist_item_id=\"xxxxxx\")\n"
  },
  {
    "path": "tests/clients/test_playlists.py",
    "content": "import pytest\nimport responses\n\nimport pyyoutube.models as mds\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\n\n\nclass TestPlaylistsResource(BaseTestCase):\n    RESOURCE = \"playlists\"\n\n    def test_list(self, helpers, authed_cli, key_cli):\n        with pytest.raises(PyYouTubeException):\n            authed_cli.playlists.list()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"playlists/playlists_paged_1.json\", helpers),\n            )\n\n            res = key_cli.playlists.list(\n                parts=[\"id\", \"snippet\"],\n                channel_id=\"UC_x5XG1OV2P6uZZ5FSM9Ttw\",\n                max_results=10,\n            )\n            assert len(res.items) == 10\n\n            res = key_cli.playlists.list(\n                parts=[\"id\", \"snippet\"],\n                playlist_id=[\n                    \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\",\n                    \"PLOU2XLYxmsIJO83u2UmyC8ud41AvUnhgj\",\n                ],\n            )\n            assert res.items[0].id == \"PLOU2XLYxmsIKpaV8h0AGE05so0fAwwfTw\"\n\n            res = authed_cli.playlists.list(\n                parts=[\"id\", \"snippet\"], mine=True, max_results=10\n            )\n            assert len(res.items) == 10\n\n    def test_insert(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"POST\",\n                url=self.url,\n                json=self.load_json(\"playlists/insert_response.json\", helpers),\n            )\n\n            playlist = authed_cli.playlists.insert(\n                body=mds.Playlist(\n                    snippet=mds.PlaylistSnippet(\n                        title=\"Test playlist\",\n                    )\n                ),\n            )\n\n            assert playlist.id == \"PLBaidt0ilCMZN8XPVB5iXY6FlSYGeyn2n\"\n\n    def test_update(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"PUT\",\n                url=self.url,\n                json=self.load_json(\"playlists/insert_response.json\", helpers),\n            )\n\n            playlist = authed_cli.playlists.update(\n                body=mds.Playlist(\n                    snippet=mds.PlaylistSnippet(\n                        title=\"Test playlist\",\n                        defaultLanguage=\"\",\n                    )\n                )\n            )\n            assert playlist.snippet.description == \"\"\n\n    def test_delete(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(method=\"DELETE\", url=self.url, status=204)\n\n            assert authed_cli.playlists.delete(\n                playlist_id=\"PLBaidt0ilCMZN8XPVB5iXY6FlSYGeyn2n\"\n            )\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"DELETE\",\n                    url=self.url,\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                    status=403,\n                )\n                authed_cli.playlists.delete(\n                    playlist_id=\"PLBaidt0ilCMZN8XPVB5iXY6FlSYGeyn2n\"\n                )\n"
  },
  {
    "path": "tests/clients/test_search.py",
    "content": "import responses\n\nfrom .base import BaseTestCase\n\n\nclass TestSearchResource(BaseTestCase):\n    RESOURCE = \"search\"\n\n    def test_list(self, helpers, authed_cli, key_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"search/search_by_developer.json\", helpers),\n            )\n\n            res = authed_cli.search.list(\n                parts=[\"snippet\"],\n                for_content_owner=True,\n            )\n            assert res.items[0].id.videoId == \"WuyFniRMrxY\"\n\n            res = authed_cli.search.list(\n                for_developer=True,\n                max_results=5,\n            )\n            assert len(res.items) == 5\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"search/search_by_mine.json\", helpers),\n            )\n            res = authed_cli.search.list(for_mine=True, max_results=5)\n            assert res.items[0].snippet.channelId == \"UCa-vrCLQHviTOVnEKDOdetQ\"\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"search/search_by_related_video.json\", helpers),\n            )\n            res = authed_cli.search.list(\n                related_to_video_id=\"Ks-_Mh1QhMc\",\n                region_code=\"US\",\n                relevance_language=\"en\",\n                safe_search=\"moderate\",\n                max_results=5,\n            )\n            assert res.items[0].id.videoId == \"eIho2S0ZahI\"\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"search/search_by_keywords_p1.json\", helpers),\n            )\n            res = key_cli.search.list(\n                q=\"surfing\",\n                parts=[\"snippet\"],\n                count=25,\n            )\n            assert len(res.items) == 25\n"
  },
  {
    "path": "tests/clients/test_subscriptions.py",
    "content": "import pytest\nimport responses\n\nimport pyyoutube.models as mds\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\n\n\nclass TestSubscriptionsResource(BaseTestCase):\n    RESOURCE = \"subscriptions\"\n\n    def test_list(self, helpers, key_cli, authed_cli):\n        with pytest.raises(PyYouTubeException):\n            key_cli.subscriptions.list()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\n                    \"subscriptions/subscriptions_by_mine_p1.json\", helpers\n                ),\n            )\n\n            res = key_cli.subscriptions.list(\n                parts=[\"id\", \"snippet\"],\n                channel_id=\"UCa-vrCLQHviTOVnEKDOdetQ\",\n                max_results=10,\n            )\n            assert res.items[0].id == \"zqShTXi-2-Tx7TtwQqhCBzrqBvZj94YvFZOGA9x6NuY\"\n\n            res = authed_cli.subscriptions.list(mine=True, max_results=10)\n            assert res.items[0].snippet.channelId == \"UCNvMBmCASzTNNX8lW3JRMbw\"\n\n            res = authed_cli.subscriptions.list(\n                my_recent_subscribers=True, max_results=10\n            )\n            assert res.items[0].snippet.channelId == \"UCNvMBmCASzTNNX8lW3JRMbw\"\n\n            res = authed_cli.subscriptions.list(my_subscribers=True, max_results=10)\n            assert res.items[0].snippet.channelId == \"UCNvMBmCASzTNNX8lW3JRMbw\"\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"subscriptions/subscriptions_by_id.json\", helpers),\n            )\n            res = key_cli.subscriptions.list(\n                parts=[\"id\", \"snippet\"],\n                subscription_id=[\n                    \"zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo\",\n                    \"zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo\",\n                ],\n            )\n            assert res.items[0].id == \"zqShTXi-2-Tx7TtwQqhCBwViE_j9IEgnmRmPnqJljxo\"\n\n    def test_inset(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"POST\",\n                url=self.url,\n                json=self.load_json(\"subscriptions/insert_response.json\", helpers),\n            )\n            subscription = authed_cli.subscriptions.insert(\n                body=mds.Subscription(\n                    snippet=mds.SubscriptionSnippet(\n                        resourceId=mds.ResourceId(\n                            kind=\"youtube#channel\",\n                            channelId=\"UCQ6ptCagG3W0Bf4lexvnBEg\",\n                        )\n                    )\n                )\n            )\n            assert subscription.id == \"POsnRIYsMcp1Cghr_Fsh-6uFZRcIHmTKzzByiv9ZAro\"\n\n    def test_delete(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"DELETE\",\n                url=self.url,\n                status=204,\n            )\n            assert authed_cli.subscriptions.delete(\n                subscription_id=\"POsnRIYsMcp1Cghr_Fsh-6uFZRcIHmTKzzByiv9ZAro\"\n            )\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"DELETE\",\n                    url=self.url,\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                    status=403,\n                )\n                authed_cli.subscriptions.delete(\n                    subscription_id=\"POsnRIYsMcp1Cghr_Fsh-6uFZRcIHmTKzzByiv9ZAro\"\n                )\n"
  },
  {
    "path": "tests/clients/test_thumbnails.py",
    "content": "\"\"\"\nTests for thumbnails.\n\"\"\"\n\nimport io\n\nfrom .base import BaseTestCase\nfrom pyyoutube.media import Media\n\n\nclass TestThumbnailsResource(BaseTestCase):\n    RESOURCE = \"thumbnails\"\n\n    def test_set(self, authed_cli):\n        video_id = \"zxTVeyG1600\"\n        media = Media(fd=io.StringIO(\"jpeg content\"), mimetype=\"image/jpeg\")\n\n        upload = authed_cli.thumbnails.set(\n            video_id=video_id,\n            media=media,\n        )\n        assert upload.resumable_progress == 0\n"
  },
  {
    "path": "tests/clients/test_video_abuse_report_reasons.py",
    "content": "import responses\n\nfrom .base import BaseTestCase\n\n\nclass TestVideoAbuseReportReasonsResource(BaseTestCase):\n    RESOURCE = \"videoAbuseReportReasons\"\n\n    def test_list(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"abuse_reasons/abuse_reason.json\", helpers),\n            )\n\n            res = authed_cli.videoAbuseReportReasons.list(\n                parts=[\"id\", \"snippet\"],\n            )\n            assert res.items[0].id == \"N\"\n"
  },
  {
    "path": "tests/clients/test_video_categories.py",
    "content": "import pytest\nimport responses\n\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\n\n\nclass TestVideoCategoriesResource(BaseTestCase):\n    RESOURCE = \"videoCategories\"\n\n    def test_list(self, helpers, key_cli):\n        with pytest.raises(PyYouTubeException):\n            key_cli.videoCategories.list()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\n                    \"categories/video_category_by_region.json\", helpers\n                ),\n            )\n            res = key_cli.videoCategories.list(\n                parts=[\"snippet\"],\n                region_code=\"US\",\n            )\n            assert res.items[0].snippet.title == \"Film & Animation\"\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"categories/video_category_multi.json\", helpers),\n            )\n            res = key_cli.videoCategories.list(\n                parts=[\"snippet\"],\n                category_id=[\"17\", \"18\"],\n            )\n            assert len(res.items) == 2\n"
  },
  {
    "path": "tests/clients/test_videos.py",
    "content": "import io\n\nimport pytest\nimport responses\n\nimport pyyoutube.models as mds\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\nfrom pyyoutube.media import Media\n\n\nclass TestVideosResource(BaseTestCase):\n    RESOURCE = \"videos\"\n\n    def test_list(self, helpers, authed_cli, key_cli):\n        with pytest.raises(PyYouTubeException):\n            key_cli.videos.list()\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"videos/videos_info_multi.json\", helpers),\n            )\n\n            res = key_cli.videos.list(\n                video_id=[\"D-lhorsDlUQ\", \"ovdbrdCIP7U\"], parts=[\"snippet\"]\n            )\n            assert len(res.items) == 2\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"videos/videos_chart_paged_1.json\", helpers),\n            )\n\n            res = key_cli.videos.list(chart=\"mostPopular\", parts=[\"snippet\"])\n            assert len(res.items) == 5\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=self.url,\n                json=self.load_json(\"videos/videos_myrating_paged_1.json\", helpers),\n            )\n\n            res = key_cli.videos.list(my_rating=\"like\", parts=[\"snippet\"])\n            assert len(res.items) == 2\n\n    def test_insert(self, helpers, authed_cli):\n        body = mds.Video(\n            snippet=mds.VideoSnippet(\n                title=\"video title\",\n                description=\"video description\",\n            )\n        )\n        media = Media(fd=io.StringIO(\"video content\"), mimetype=\"video/mp4\")\n\n        upload = authed_cli.videos.insert(\n            body=body,\n            media=media,\n            notify_subscribers=True,\n        )\n        assert upload.resumable_progress == 0\n\n    def test_update(self, helpers, authed_cli):\n        body = mds.Video(\n            snippet=mds.VideoSnippet(\n                title=\"updated video title\",\n            )\n        )\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"PUT\",\n                url=self.url,\n                json=self.load_json(\"videos/insert_response.json\", helpers),\n            )\n            video = authed_cli.videos.update(body=body, parts=[\"snippet\"])\n            assert video.id == \"D-lhorsDlUQ\"\n\n    def test_rate(self, helpers, authed_cli):\n        video_id = \"D-lhorsDlUQ\"\n\n        with responses.RequestsMock() as m:\n            m.add(method=\"POST\", url=f\"{self.url}/rate\", status=204)\n\n            assert authed_cli.videos.rate(\n                video_id=video_id,\n                rating=\"like\",\n            )\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"POST\",\n                    url=f\"{self.url}/rate\",\n                    status=403,\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                )\n                authed_cli.videos.rate(\n                    video_id=video_id,\n                    rating=\"like\",\n                )\n\n    def test_get_rating(self, helpers, authed_cli):\n        video_id = \"D-lhorsDlUQ\"\n\n        with responses.RequestsMock() as m:\n            m.add(\n                method=\"GET\",\n                url=f\"{self.url}/getRating\",\n                json=self.load_json(\"videos/get_rating_response.json\", helpers),\n            )\n\n            res = authed_cli.videos.get_rating(\n                video_id=video_id,\n            )\n            assert res.items[0].rating == \"none\"\n\n    def test_report_abuse(self, helpers, authed_cli):\n        body = mds.VideoReportAbuse(\n            videoId=\"D-lhorsDlUQ\",\n            reasonId=\"xxxxxx\",\n        )\n\n        with responses.RequestsMock() as m:\n            m.add(method=\"POST\", url=f\"{self.url}/reportAbuse\", status=204)\n            assert authed_cli.videos.report_abuse(body=body)\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"POST\",\n                    url=f\"{self.url}/reportAbuse\",\n                    status=403,\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                )\n                authed_cli.videos.report_abuse(body=body)\n\n    def test_delete(self, helpers, authed_cli):\n        video_id = \"D-lhorsDlUQ\"\n\n        with responses.RequestsMock() as m:\n            m.add(method=\"DELETE\", url=self.url, status=204)\n            assert authed_cli.videos.delete(video_id=video_id)\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"DELETE\",\n                    url=self.url,\n                    status=403,\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                )\n                authed_cli.videos.delete(video_id=video_id)\n"
  },
  {
    "path": "tests/clients/test_watermarks.py",
    "content": "\"\"\"\nTests for watermarks.\n\"\"\"\n\nimport io\n\nimport pytest\nimport responses\n\nimport pyyoutube.models as mds\nfrom .base import BaseTestCase\nfrom pyyoutube.error import PyYouTubeException\nfrom pyyoutube.media import Media\n\n\nclass TestWatermarksResource(BaseTestCase):\n    RESOURCE = \"watermarks\"\n\n    def test_set(self, authed_cli):\n        body = mds.Watermark(\n            timing=mds.WatermarkTiming(\n                type=\"offsetFromStart\",\n                offsetMs=1000,\n                durationMs=3000,\n            ),\n            position=mds.WatermarkPosition(\n                type=\"corner\",\n                cornerPosition=\"topRight\",\n            ),\n        )\n        media = Media(fd=io.StringIO(\"image content\"), mimetype=\"image/jpeg\")\n\n        upload = authed_cli.watermarks.set(\n            channel_id=\"id\",\n            body=body,\n            media=media,\n        )\n        assert upload.resumable_progress == 0\n\n    def test_unset(self, helpers, authed_cli):\n        with responses.RequestsMock() as m:\n            m.add(method=\"POST\", url=f\"{self.url}/unset\", status=204)\n            assert authed_cli.watermarks.unset(channel_id=\"id\")\n\n        with pytest.raises(PyYouTubeException):\n            with responses.RequestsMock() as m:\n                m.add(\n                    method=\"POST\",\n                    url=f\"{self.url}/unset\",\n                    status=403,\n                    json=self.load_json(\"error_permission_resp.json\", helpers),\n                )\n                assert authed_cli.watermarks.unset(channel_id=\"id\")\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "import json\n\nimport pytest\n\nfrom pyyoutube import Client\n\n\nclass Helpers:\n    @staticmethod\n    def load_json(filename):\n        with open(filename, \"rb\") as f:\n            return json.loads(f.read().decode(\"utf-8\"))\n\n    @staticmethod\n    def load_file_binary(filename):\n        with open(filename, \"rb\") as f:\n            return f.read()\n\n\n@pytest.fixture\ndef helpers():\n    return Helpers()\n\n\n@pytest.fixture(scope=\"class\")\ndef authed_cli():\n    return Client(access_token=\"access token\")\n\n\n@pytest.fixture(scope=\"class\")\ndef key_cli():\n    return Client(api_key=\"api key\")\n"
  },
  {
    "path": "tests/models/__init__.py",
    "content": ""
  },
  {
    "path": "tests/models/test_abuse_reason.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass AbuseReasonModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/abuse_report_reason/\"\n\n    with open(BASE_PATH + \"abuse_reason.json\", \"rb\") as f:\n        ABUSE_REASON = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"abuse_reason_res.json\", \"rb\") as f:\n        ABUSE_REASON_RES = json.loads(f.read().decode(\"utf-8\"))\n\n    def testAbuseReason(self) -> None:\n        m = models.VideoAbuseReportReason.from_dict(self.ABUSE_REASON)\n\n        self.assertEqual(m.id, \"N\")\n        self.assertEqual(m.snippet.label, \"Sex or nudity\")\n        self.assertEqual(len(m.snippet.secondaryReasons), 3)\n\n    def testAbuseReasonResponse(self) -> None:\n        m = models.VideoAbuseReportReasonListResponse.from_dict(self.ABUSE_REASON_RES)\n\n        self.assertEqual(m.kind, \"youtube#videoAbuseReportReasonListResponse\")\n        self.assertEqual(len(m.items), 3)\n"
  },
  {
    "path": "tests/models/test_activities.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass ActivityModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/activities/\"\n\n    with open(BASE_PATH + \"activity_contentDetails.json\", \"rb\") as f:\n        ACTIVITY_CONTENT_DETAILS = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"activity_snippet.json\", \"rb\") as f:\n        ACTIVITY_SNIPPET = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"activity.json\", \"rb\") as f:\n        ACTIVITY = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"activity_response.json\", \"rb\") as f:\n        ACTIVITY_RESPONSE = json.loads(f.read().decode(\"utf-8\"))\n\n    def testActivityContentDetails(self) -> None:\n        m = models.ActivityContentDetails.from_dict(self.ACTIVITY_CONTENT_DETAILS)\n\n        self.assertEqual(m.upload.videoId, \"LDXYRzerjzU\")\n\n    def testActivitySnippet(self) -> None:\n        m = models.ActivitySnippet.from_dict(self.ACTIVITY_SNIPPET)\n\n        self.assertEqual(m.channelId, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n        self.assertEqual(\n            m.thumbnails.default.url, \"https://i.ytimg.com/vi/DQGSZTxLVrI/default.jpg\"\n        )\n\n    def testActivity(self) -> None:\n        m = models.Activity.from_dict(self.ACTIVITY)\n\n        self.assertEqual(m.snippet.channelId, \"UCa-vrCLQHviTOVnEKDOdetQ\")\n        self.assertEqual(m.contentDetails.upload.videoId, \"JE8xdDp5B8Q\")\n\n    def testActivityListResponse(self) -> None:\n        m = models.ActivityListResponse.from_dict(self.ACTIVITY_RESPONSE)\n\n        self.assertEqual(m.kind, \"youtube#activityListResponse\")\n        self.assertEqual(m.pageInfo.totalResults, 2)\n        self.assertEqual(len(m.items), 2)\n"
  },
  {
    "path": "tests/models/test_auth_models.py",
    "content": "import json\nimport unittest\nimport pyyoutube.models as models\n\n\nclass AuthModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/users/\"\n    with open(BASE_PATH + \"access_token.json\", \"rb\") as f:\n        ACCESS_TOKEN_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"user_profile.json\", \"rb\") as f:\n        USER_PROFILE_INFO = json.loads(f.read().decode(\"utf-8\"))\n\n    def testAccessToken(self) -> None:\n        m = models.AccessToken.from_dict(self.ACCESS_TOKEN_INFO)\n\n        self.assertEqual(m.access_token, \"access_token\")\n\n    def testUserProfile(self) -> None:\n        m = models.UserProfile.from_dict(self.USER_PROFILE_INFO)\n\n        self.assertEqual(m.id, \"12345678910\")\n\n        origin_data = json.dumps(self.USER_PROFILE_INFO, sort_keys=True)\n        d = m.to_json(sort_keys=True, allow_nan=False)\n        self.assertEqual(origin_data, d)\n"
  },
  {
    "path": "tests/models/test_captions.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass CaptionModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/captions/\"\n\n    with open(BASE_PATH + \"caption_snippet.json\", \"rb\") as f:\n        CAPTION_SNIPPET = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"caption.json\", \"rb\") as f:\n        CAPTION_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"caption_response.json\", \"rb\") as f:\n        CAPTION_RESPONSE = json.loads(f.read().decode(\"utf-8\"))\n\n    def testCaptionSnippet(self):\n        m = models.CaptionSnippet.from_dict(self.CAPTION_SNIPPET)\n\n        self.assertEqual(m.videoId, \"oHR3wURdJ94\")\n        self.assertEqual(\n            m.string_to_datetime(m.lastUpdated).isoformat(),\n            \"2020-01-14T09:40:49.981000+00:00\",\n        )\n\n    def testCaption(self):\n        m = models.Caption.from_dict(self.CAPTION_INFO)\n\n        self.assertEqual(m.id, \"SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I\")\n        self.assertEqual(m.snippet.videoId, \"oHR3wURdJ94\")\n\n    def testCaptionListResponse(self):\n        m = models.CaptionListResponse.from_dict(self.CAPTION_RESPONSE)\n\n        self.assertEqual(m.kind, \"youtube#captionListResponse\")\n        self.assertEqual(len(m.items), 2)\n        self.assertEqual(m.items[0].id, \"SwPOvp0r7kd9ttt_XhcHdZthMwXG7Z0I\")\n"
  },
  {
    "path": "tests/models/test_category.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass CategoryModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/categories/\"\n\n    with open(BASE_PATH + \"video_category_info.json\", \"rb\") as f:\n        VIDEO_CATEGORY_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_category_response.json\", \"rb\") as f:\n        VIDEO_CATEGORY_RESPONSE = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"guide_category_info.json\", \"rb\") as f:\n        GUIDE_CATEGORY_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"guide_category_response.json\", \"rb\") as f:\n        GUIDE_CATEGORY_RESPONSE = json.loads(f.read().decode(\"utf-8\"))\n\n    def testVideoCategory(self) -> None:\n        m = models.VideoCategory.from_dict(self.VIDEO_CATEGORY_INFO)\n        self.assertEqual(m.id, \"17\")\n        self.assertEqual(m.snippet.title, \"Sports\")\n\n    def testVideoCategoryListResponse(self) -> None:\n        m = models.VideoCategoryListResponse.from_dict(self.VIDEO_CATEGORY_RESPONSE)\n        self.assertEqual(m.kind, \"youtube#videoCategoryListResponse\")\n        self.assertEqual(len(m.items), 1)\n        self.assertEqual(m.items[0].id, \"17\")\n"
  },
  {
    "path": "tests/models/test_channel.py",
    "content": "import json\nimport unittest\nimport pyyoutube.models as models\n\n\nclass ChannelModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/channels/\"\n\n    with open(BASE_PATH + \"channel_branding_settings.json\", \"rb\") as f:\n        BRANDING_SETTINGS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_content_details.json\", \"rb\") as f:\n        CONTENT_DETAILS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_topic_details.json\", \"rb\") as f:\n        TOPIC_DETAILS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_snippet.json\", \"rb\") as f:\n        SNIPPET_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_statistics.json\", \"rb\") as f:\n        STATISTICS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_status.json\", \"rb\") as f:\n        STATUS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_info.json\", \"rb\") as f:\n        CHANNEL_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_api_response.json\", \"rb\") as f:\n        CHANNEL_API_RESPONSE = json.loads(f.read().decode(\"utf-8\"))\n\n    def testChannelBrandingSettings(self) -> None:\n        m = models.ChannelBrandingSetting.from_dict(self.BRANDING_SETTINGS_INFO)\n\n        self.assertEqual(m.channel.title, \"Google Developers\")\n\n    def testChannelContentDetails(self) -> None:\n        m = models.ChannelContentDetails.from_dict(self.CONTENT_DETAILS_INFO)\n\n        self.assertEqual(m.relatedPlaylists.uploads, \"UU_x5XG1OV2P6uZZ5FSM9Ttw\")\n\n    def testChannelTopicDetails(self) -> None:\n        m = models.ChannelTopicDetails.from_dict(self.TOPIC_DETAILS_INFO)\n\n        self.assertEqual(m.topicIds[0], \"/m/019_rr\")\n        self.assertEqual(len(m.topicCategories), 3)\n\n        full_topics = m.get_full_topics()\n        self.assertEqual(full_topics[0].id, \"/m/019_rr\")\n        self.assertEqual(full_topics[0].description, \"Lifestyle (parent topic)\")\n\n    def testChannelSnippet(self) -> None:\n        m = models.ChannelSnippet.from_dict(self.SNIPPET_INFO)\n\n        self.assertEqual(m.title, \"Google Developers\")\n        self.assertEqual(m.localized.title, \"Google Developers\")\n        self.assertEqual(\n            m.thumbnails.default.url,\n            \"https://yt3.ggpht.com/a/AGF-l78iFtAxyRZcUBzG91kbKMES19z-zGW5KT20_g=s88-c-k-c0xffffffff-no-rj-mo\",\n        )\n\n        published_at = m.string_to_datetime(m.publishedAt)\n        self.assertEqual(published_at.isoformat(), \"2007-08-23T00:34:43+00:00\")\n\n    def testChannelStatistics(self) -> None:\n        m = models.ChannelStatistics.from_dict(self.STATISTICS_INFO)\n\n        self.assertEqual(m.viewCount, 160361638)\n\n    def testChannelStatus(self) -> None:\n        m = models.ChannelStatus.from_dict(self.STATUS_INFO)\n\n        self.assertEqual(m.privacyStatus, \"public\")\n\n    def testChannel(self) -> None:\n        m = models.Channel.from_dict(self.CHANNEL_INFO)\n\n        self.assertEqual(m.id, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n\n    def testChannelListResponse(self) -> None:\n        m = models.ChannelListResponse.from_dict(self.CHANNEL_API_RESPONSE)\n\n        self.assertEqual(m.kind, \"youtube#channelListResponse\")\n        self.assertEqual(m.pageInfo.totalResults, 1)\n        self.assertEqual(m.items[0].id, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n"
  },
  {
    "path": "tests/models/test_channel_sections.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass ChannelSectionModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/channel_sections/\"\n\n    with open(BASE_PATH + \"channel_section_info.json\", \"rb\") as f:\n        CHANNEL_SECTION_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"channel_section_response.json\", \"rb\") as f:\n        CHANNEL_SECTION_RESPONSE = json.loads(f.read().decode(\"utf-8\"))\n\n    def testChannelSection(self) -> None:\n        m = models.ChannelSection.from_dict(self.CHANNEL_SECTION_INFO)\n\n        self.assertEqual(m.id, \"UC_x5XG1OV2P6uZZ5FSM9Ttw.e-Fk7vMPqLE\")\n        self.assertEqual(m.snippet.type, \"multipleChannels\")\n        self.assertEqual(len(m.contentDetails.channels), 16)\n\n    def testChannelSectionResponse(self) -> None:\n        m = models.ChannelSectionResponse.from_dict(self.CHANNEL_SECTION_RESPONSE)\n\n        self.assertEqual(m.kind, \"youtube#channelSectionListResponse\")\n        self.assertEqual(len(m.items), 10)\n"
  },
  {
    "path": "tests/models/test_comments.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass CommentModelModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/comments/\"\n\n    with open(BASE_PATH + \"comment_snippet.json\", \"rb\") as f:\n        SNIPPET_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_info.json\", \"rb\") as f:\n        COMMENT_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_api_response.json\", \"rb\") as f:\n        COMMENT_API_INFO = json.loads(f.read().decode(\"utf-8\"))\n\n    def testCommentSnippet(self) -> None:\n        m = models.CommentSnippet.from_dict(self.SNIPPET_INFO)\n\n        self.assertEqual(m.videoId, \"wtLJPvx7-ys\")\n        self.assertTrue(m.canRate)\n        self.assertEqual(m.authorChannelId.value, \"UCqPku3cxM-ED3poX8YtGqeg\")\n        self.assertEqual(\n            m.string_to_datetime(m.publishedAt).isoformat(), \"2019-03-28T11:33:46+00:00\"\n        )\n\n    def testComment(self) -> None:\n        m = models.Comment.from_dict(self.COMMENT_INFO)\n\n        self.assertEqual(m.id, \"UgwxApqcfzZzF_C5Zqx4AaABAg\")\n        self.assertEqual(m.snippet.authorDisplayName, \"Oeurn Ravuth\")\n        self.assertEqual(\n            m.snippet.string_to_datetime(m.snippet.updatedAt).isoformat(),\n            \"2019-03-28T11:33:46+00:00\",\n        )\n\n    def testCommentListResponse(self) -> None:\n        m = models.CommentListResponse.from_dict(self.COMMENT_API_INFO)\n\n        self.assertEqual(m.kind, \"youtube#commentListResponse\")\n        self.assertEqual(m.items[0].id, \"UgxKREWxIgDrw8w2e_Z4AaABAg\")\n\n\nclass CommentThreadModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/comments/\"\n\n    with open(BASE_PATH + \"comment_thread_snippet.json\", \"rb\") as f:\n        SNIPPET_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_thread_replies.json\", \"rb\") as f:\n        REPLIES_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_thread_info.json\", \"rb\") as f:\n        COMMENT_THREAD_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"comment_thread_api_response.json\", \"rb\") as f:\n        COMMENT_THREAD_API_INFO = json.loads(f.read().decode(\"utf-8\"))\n\n    def testCommentThreadSnippet(self) -> None:\n        m = models.CommentThreadSnippet.from_dict(self.SNIPPET_INFO)\n\n        self.assertEqual(m.videoId, \"D-lhorsDlUQ\")\n        self.assertEqual(m.topLevelComment.id, \"UgydxWWoeA7F1OdqypJ4AaABAg\")\n        self.assertEqual(m.topLevelComment.snippet.videoId, \"D-lhorsDlUQ\")\n\n    def testCommentThreadReplies(self) -> None:\n        m = models.CommentThreadReplies.from_dict(self.REPLIES_INFO)\n\n        self.assertEqual(len(m.comments), 1)\n        self.assertEqual(\n            m.comments[0].id, \"UgydxWWoeA7F1OdqypJ4AaABAg.8wWQ3tdHcFx8xcDheui-qb\"\n        )\n        self.assertEqual(m.comments[0].snippet.videoId, \"D-lhorsDlUQ\")\n\n    def testCommentThread(self) -> None:\n        m = models.CommentThread.from_dict(self.COMMENT_THREAD_INFO)\n\n        self.assertEqual(m.id, \"UgydxWWoeA7F1OdqypJ4AaABAg\")\n        self.assertEqual(m.snippet.videoId, \"D-lhorsDlUQ\")\n        self.assertEqual(\n            m.replies.comments[0].id,\n            \"UgydxWWoeA7F1OdqypJ4AaABAg.8wWQ3tdHcFx8xcDheui-qb\",\n        )\n\n    def testCommentThreadListResponse(self) -> None:\n        m = models.CommentThreadListResponse.from_dict(self.COMMENT_THREAD_API_INFO)\n\n        self.assertEqual(m.kind, \"youtube#commentThreadListResponse\")\n        self.assertEqual(m.items[0].id, \"Ugz097FRhsQy5CVhAjp4AaABAg\")\n"
  },
  {
    "path": "tests/models/test_i18n_models.py",
    "content": "import json\n\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass I18nModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/i18ns/\"\n\n    with open(BASE_PATH + \"region_info.json\", \"rb\") as f:\n        REGION_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"region_res.json\", \"rb\") as f:\n        REGION_RES = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"language_info.json\", \"rb\") as f:\n        LANGUAGE_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"language_res.json\", \"rb\") as f:\n        LANGUAGE_RES = json.loads(f.read().decode(\"utf-8\"))\n\n    def testI18nRegion(self) -> None:\n        m = models.I18nRegion.from_dict(self.REGION_INFO)\n\n        self.assertEqual(m.id, \"DZ\")\n        self.assertEqual(m.snippet.gl, \"DZ\")\n\n    def testI18nRegionResponse(self) -> None:\n        m = models.I18nRegionListResponse.from_dict(self.REGION_RES)\n\n        self.assertEqual(m.kind, \"youtube#i18nRegionListResponse\")\n        self.assertEqual(len(m.items), 2)\n\n    def testI18nLanguage(self) -> None:\n        m = models.I18nLanguage.from_dict(self.LANGUAGE_INFO)\n\n        self.assertEqual(m.id, \"af\")\n        self.assertEqual(m.snippet.hl, \"af\")\n\n    def testI18nLanguageResponse(self) -> None:\n        m = models.I18nRegionListResponse.from_dict(self.LANGUAGE_RES)\n\n        self.assertEqual(m.kind, \"youtube#i18nLanguageListResponse\")\n        self.assertEqual(len(m.items), 2)\n"
  },
  {
    "path": "tests/models/test_members.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass MemberModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/members/\"\n\n    with open(BASE_PATH + \"member_info.json\", \"rb\") as f:\n        MEMBER_INFO = json.loads(f.read().decode(\"utf-8\"))\n\n    def testMember(self) -> None:\n        m = models.Member.from_dict(self.MEMBER_INFO)\n\n        self.assertEqual(m.kind, \"youtube#member\")\n        self.assertEqual(m.snippet.memberDetails.channelId, \"UCa-vrCLQHviTOVnEKDOdetQ\")\n        self.assertEqual(m.snippet.membershipsDetails.highestAccessibleLevel, \"string\")\n\n\nclass MembershipLevelModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/members/\"\n\n    with open(BASE_PATH + \"membership_level.json\", \"rb\") as f:\n        MEMBERSHIP_LEVEL_INFO = json.loads(f.read().decode(\"utf-8\"))\n\n    def testMembershipLevel(self) -> None:\n        m = models.MembershipsLevel.from_dict(self.MEMBERSHIP_LEVEL_INFO)\n\n        self.assertEqual(m.kind, \"youtube#membershipsLevel\")\n        self.assertEqual(m.snippet.levelDetails.displayName, \"high\")\n"
  },
  {
    "path": "tests/models/test_playlist.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass PlaylistModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/playlists/\"\n\n    with open(BASE_PATH + \"playlist_content_details.json\", \"rb\") as f:\n        CONTENT_DETAILS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_snippet.json\", \"rb\") as f:\n        SNIPPET_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_status.json\", \"rb\") as f:\n        STATUS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_info.json\", \"rb\") as f:\n        PLAYLIST_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_api_response.json\", \"rb\") as f:\n        PLAYLIST_RESPONSE_INFO = json.loads(f.read().decode(\"utf-8\"))\n\n    def testPlayListContentDetails(self) -> None:\n        m = models.PlaylistContentDetails.from_dict(self.CONTENT_DETAILS_INFO)\n\n        self.assertEqual(m.itemCount, 4)\n\n    def testPlayListSnippet(self) -> None:\n        m = models.PlaylistSnippet.from_dict(self.SNIPPET_INFO)\n\n        self.assertEqual(\n            m.string_to_datetime(m.publishedAt).isoformat(), \"2019-05-16T18:46:20+00:00\"\n        )\n        self.assertEqual(m.title, \"Assistant on Air\")\n        self.assertEqual(\n            m.thumbnails.default.url, \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\"\n        )\n        self.assertEqual(m.localized.title, \"Assistant on Air\")\n\n    def testPlayListStatus(self) -> None:\n        m = models.PlaylistStatus.from_dict(self.STATUS_INFO)\n\n        self.assertEqual(m.privacyStatus, \"public\")\n\n    def testPlayList(self) -> None:\n        m = models.Playlist.from_dict(self.PLAYLIST_INFO)\n\n        self.assertEqual(m.id, \"PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp\")\n        self.assertEqual(m.player, None)\n        self.assertEqual(m.snippet.title, \"Assistant on Air\")\n\n    def testPlaylistListResponse(self) -> None:\n        m = models.PlaylistListResponse.from_dict(self.PLAYLIST_RESPONSE_INFO)\n\n        self.assertEqual(m.kind, \"youtube#playlistListResponse\")\n        self.assertEqual(m.pageInfo.totalResults, 416)\n        self.assertEqual(m.items[0].id, \"PLOU2XLYxmsIJpufeMHncnQvFOe0K3MhVp\")\n"
  },
  {
    "path": "tests/models/test_playlist_item.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass PlaylistItemModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/playlist_items/\"\n\n    with open(BASE_PATH + \"playlist_item_content_details.json\", \"rb\") as f:\n        CONTENT_DETAILS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_item_snippet.json\", \"rb\") as f:\n        SNIPPET_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_item_status.json\", \"rb\") as f:\n        STATUS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_item_info.json\", \"rb\") as f:\n        PLAYLIST_ITEM_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"playlist_item_api_response.json\", \"rb\") as f:\n        PLAYLIST_LIST_RESPONSE = json.loads(f.read().decode(\"utf-8\"))\n\n    def testPlaylistItemContentDetails(self) -> None:\n        m = models.PlaylistItemContentDetails.from_dict(self.CONTENT_DETAILS_INFO)\n\n        self.assertEqual(m.videoId, \"D-lhorsDlUQ\")\n        self.assertEqual(\n            m.string_to_datetime(m.videoPublishedAt).isoformat(),\n            \"2019-03-21T20:37:49+00:00\",\n        )\n\n    def testPlaylistItemSnippet(self) -> None:\n        m = models.PlaylistItemSnippet.from_dict(self.SNIPPET_INFO)\n\n        self.assertEqual(\n            m.string_to_datetime(m.publishedAt).isoformat(), \"2019-05-16T18:46:20+00:00\"\n        )\n        self.assertEqual(m.title, \"What are Actions on Google (Assistant on Air)\")\n        self.assertEqual(\n            m.thumbnails.default.url, \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\"\n        )\n        self.assertEqual(m.resourceId.videoId, \"D-lhorsDlUQ\")\n\n    def testPlaylistItemStatus(self) -> None:\n        m = models.PlaylistItemStatus.from_dict(self.STATUS_INFO)\n\n        self.assertEqual(m.privacyStatus, \"public\")\n\n    def testPlaylistItem(self) -> None:\n        m = models.PlaylistItem.from_dict(self.PLAYLIST_ITEM_INFO)\n\n        self.assertEqual(\n            m.id, \"UExPVTJYTFl4bXNJSnB1ZmVNSG5jblF2Rk9lMEszTWhWcC41NkI0NEY2RDEwNTU3Q0M2\"\n        )\n        self.assertEqual(m.snippet.channelId, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n        self.assertEqual(m.snippet.resourceId.videoId, \"D-lhorsDlUQ\")\n        self.assertEqual(m.contentDetails.videoId, \"D-lhorsDlUQ\")\n        self.assertEqual(m.status.privacyStatus, \"public\")\n        self.assertEqual(m.snippet.videoOwnerChannelId, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n\n    def testPlaylistItemListResponse(self) -> None:\n        m = models.PlaylistItemListResponse.from_dict(self.PLAYLIST_LIST_RESPONSE)\n\n        self.assertEqual(m.kind, \"youtube#playlistItemListResponse\")\n        self.assertEqual(m.pageInfo.totalResults, 3)\n        self.assertEqual(len(m.items), 3)\n        self.assertEqual(\n            m.items[0].id,\n            \"UExPVTJYTFl4bXNJSlhzSDJodEcxZzBOVWpIR3E2MlE3aS41NkI0NEY2RDEwNTU3Q0M2\",\n        )\n"
  },
  {
    "path": "tests/models/test_search_result.py",
    "content": "import json\nimport unittest\n\nimport pyyoutube.models as models\n\n\nclass SearchResultModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/search_result/\"\n\n    with open(BASE_PATH + \"search_result_id.json\", \"rb\") as f:\n        SEARCH_RES_ID_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"search_result_snippet.json\", \"rb\") as f:\n        SEARCH_RES_SNIPPET_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"search_result.json\", \"rb\") as f:\n        SEARCH_RES_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"search_result_api_response.json\", \"rb\") as f:\n        SEARCH_RES_API_INFO = json.loads(f.read().decode(\"utf-8\"))\n\n    def testSearchResultId(self):\n        m = models.SearchResultId.from_dict(self.SEARCH_RES_ID_INFO)\n        self.assertEqual(m.kind, \"youtube#playlist\")\n\n    def testSearchResultSnippet(self):\n        m = models.SearchResultSnippet.from_dict(self.SEARCH_RES_SNIPPET_INFO)\n\n        self.assertEqual(m.channelId, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n        self.assertEqual(\n            m.string_to_datetime(m.publishedAt).isoformat(),\n            \"2016-03-30T16:59:12+00:00\",\n        )\n        self.assertEqual(\n            m.thumbnails.default.url, \"https://i.ytimg.com/vi/cKxRvEZd3Mw/default.jpg\"\n        )\n\n    def testSearchResult(self):\n        m = models.SearchResult.from_dict(self.SEARCH_RES_INFO)\n        self.assertEqual(m.kind, \"youtube#searchResult\")\n        self.assertEqual(m.id.videoId, \"fq4N0hgOWzU\")\n\n    def testSearchListResponse(self):\n        m = models.SearchListResponse.from_dict(self.SEARCH_RES_API_INFO)\n\n        self.assertEqual(m.kind, \"youtube#searchListResponse\")\n        self.assertEqual(m.regionCode, \"US\")\n        self.assertEqual(m.pageInfo.totalResults, 489126)\n        self.assertEqual(len(m.items), 5)\n"
  },
  {
    "path": "tests/models/test_subscriptions.py",
    "content": "import json\nimport unittest\nimport pyyoutube.models as models\n\n\nclass SubscriptionModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/subscriptions/\"\n\n    with open(BASE_PATH + \"snippet.json\", \"rb\") as f:\n        SNIPPETS = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"contentDetails.json\", \"rb\") as f:\n        CONTENT_DETAILS = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"subscriberSnippet.json\", \"rb\") as f:\n        SUBSCRIBER_SNIPPET = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"subscription.json\", \"rb\") as f:\n        SUBSCRIPTION_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"resp.json\", \"rb\") as f:\n        SUBSCRIPTION_RESPONSE = json.loads(f.read().decode(\"utf-8\"))\n\n    def testSubscriptionSnippet(self) -> None:\n        m = models.SubscriptionSnippet.from_dict(self.SNIPPETS)\n\n        self.assertEqual(m.channelId, \"UCNvMBmCASzTNNX8lW3JRMbw\")\n        self.assertEqual(m.resourceId.channelId, \"UCQ7dFBzZGlBvtU2hCecsBBg\")\n        self.assertEqual(\n            m.thumbnails.default.url,\n            \"https://yt3.ggpht.com/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n        )\n\n    def testSubscriptionContentDetails(self) -> None:\n        m = models.SubscriptionContentDetails.from_dict(self.CONTENT_DETAILS)\n\n        self.assertEqual(m.totalItemCount, 2)\n        self.assertEqual(m.activityType, \"all\")\n\n    def testSubscriptionSubscriberSnippet(self) -> None:\n        m = models.SubscriptionSubscriberSnippet.from_dict(self.SUBSCRIBER_SNIPPET)\n\n        self.assertEqual(m.title, \"kun liu\")\n        self.assertEqual(\n            m.thumbnails.default.url,\n            \"https://yt3.ggpht.com/s88-c-k-no-mo-rj-c0xffffff/photo.jpg\",\n        )\n\n    def testSubscription(self) -> None:\n        m = models.Subscription.from_dict(self.SUBSCRIPTION_INFO)\n\n        self.assertEqual(m.id, \"zqShTXi-2-Rya5uUxEp3ZsPI3fZrFQnSXNQCwvHBGGo\")\n        self.assertEqual(m.snippet.title, \"ikaros-life\")\n        self.assertEqual(m.contentDetails.totalItemCount, 2)\n        self.assertEqual(m.subscriberSnippet.title, \"kun liu\")\n\n    def testSubscriptionResponse(self) -> None:\n        m = models.SubscriptionListResponse.from_dict(self.SUBSCRIPTION_RESPONSE)\n\n        self.assertEqual(m.nextPageToken, \"CAUQAA\")\n        self.assertEqual(m.pageInfo.totalResults, 16)\n        self.assertEqual(len(m.items), 5)\n"
  },
  {
    "path": "tests/models/test_videos.py",
    "content": "import json\nimport unittest\nimport pyyoutube\nimport pyyoutube.models as models\n\n\nclass VideoModelTest(unittest.TestCase):\n    BASE_PATH = \"testdata/modeldata/videos/\"\n\n    with open(BASE_PATH + \"video_content_details.json\", \"rb\") as f:\n        CONTENT_DETAILS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_topic_details.json\", \"rb\") as f:\n        TOPIC_DETAILS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_snippet.json\", \"rb\") as f:\n        SNIPPET_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_statistics.json\", \"rb\") as f:\n        STATISTICS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_status.json\", \"rb\") as f:\n        STATUS_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_info.json\", \"rb\") as f:\n        VIDEO_INFO = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_api_response.json\", \"rb\") as f:\n        VIDEO_API_RESPONSE = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_recording_details.json\", \"rb\") as f:\n        RECORDING_DETAILS = json.loads(f.read().decode(\"utf-8\"))\n    with open(BASE_PATH + \"video_paid_product_placement_details.json\", \"rb\") as f:\n        PAID_PRODUCT_PLACEMENT_DETAILS = json.loads(f.read().decode(\"utf-8\"))\n\n    def testVideoContentDetails(self) -> None:\n        m = models.VideoContentDetails.from_dict(self.CONTENT_DETAILS_INFO)\n\n        self.assertEqual(m.duration, \"PT21M7S\")\n\n        seconds = m.get_video_seconds_duration()\n        self.assertEqual(seconds, 1267)\n\n        m.duration = None\n        self.assertEqual(m.get_video_seconds_duration(), None)\n\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            m.duration = \"error datetime\"\n            m.get_video_seconds_duration()\n\n    def testVideoTopicDetails(self) -> None:\n        m = models.VideoTopicDetails.from_dict(self.TOPIC_DETAILS_INFO)\n\n        self.assertEqual(m.topicIds[0], \"/m/02jjt\")\n        self.assertEqual(len(m.topicCategories), 1)\n\n        full_topics = m.get_full_topics()\n\n        self.assertEqual(full_topics[0].id, \"/m/02jjt\")\n        self.assertEqual(full_topics[0].description, \"Entertainment (parent topic)\")\n\n    def testVideoSnippet(self) -> None:\n        m = models.VideoSnippet.from_dict(self.SNIPPET_INFO)\n\n        self.assertEqual(\n            m.string_to_datetime(m.publishedAt).isoformat(), \"2019-03-21T20:37:49+00:00\"\n        )\n\n        m.publishedAt = None\n        self.assertEqual(m.string_to_datetime(m.publishedAt), None)\n\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            m.string_to_datetime(\"error datetime string\")\n\n        self.assertEqual(m.channelId, \"UC_x5XG1OV2P6uZZ5FSM9Ttw\")\n        self.assertEqual(\n            m.thumbnails.default.url, \"https://i.ytimg.com/vi/D-lhorsDlUQ/default.jpg\"\n        )\n        self.assertEqual(m.tags[0], \"Google\")\n        self.assertEqual(\n            m.localized.title, \"What are Actions on Google (Assistant on Air)\"\n        )\n\n    def testVideoStatistics(self) -> None:\n        m = models.VideoStatistics.from_dict(self.STATISTICS_INFO)\n\n        self.assertEqual(m.viewCount, 8087)\n\n    def testVideoStatus(self) -> None:\n        m = models.VideoStatus.from_dict(self.STATUS_INFO)\n\n        self.assertEqual(m.uploadStatus, \"processed\")\n\n        self.assertEqual(\n            m.string_to_datetime(m.publishAt).isoformat(), \"2019-03-21T20:37:49+00:00\"\n        )\n\n    def testVideo(self) -> None:\n        m = models.Video.from_dict(self.VIDEO_INFO)\n\n        self.assertEqual(m.id, \"D-lhorsDlUQ\")\n        self.assertEqual(\n            m.snippet.title, \"What are Actions on Google (Assistant on Air)\"\n        )\n\n    def testVideoListResponse(self) -> None:\n        m = models.VideoListResponse.from_dict(self.VIDEO_API_RESPONSE)\n        self.assertEqual(m.kind, \"youtube#videoListResponse\")\n        self.assertEqual(m.pageInfo.totalResults, 1)\n        self.assertEqual(m.items[0].id, \"D-lhorsDlUQ\")\n\n    def testVideoRecordingDetails(self) -> None:\n        m = models.VideoRecordingDetails.from_dict(self.RECORDING_DETAILS)\n\n        self.assertEqual(\n            m.string_to_datetime(m.recordingDate).isoformat(),\n            \"2024-07-03T00:00:00+00:00\",\n        )\n\n    def testPaidProductPlacementDetail(self) -> None:\n        m = models.PaidProductPlacementDetail.from_dict(\n            self.PAID_PRODUCT_PLACEMENT_DETAILS\n        )\n\n        self.assertTrue(m.hasPaidProductPlacement)\n"
  },
  {
    "path": "tests/test_error_handling.py",
    "content": "import unittest\n\nfrom requests import Response\n\nfrom pyyoutube.error import ErrorCode, ErrorMessage, PyYouTubeException\n\n\nclass ErrorTest(unittest.TestCase):\n    BASE_PATH = \"testdata/\"\n    with open(BASE_PATH + \"error_response.json\", \"rb\") as f:\n        ERROR_DATA = f.read()\n\n    with open(BASE_PATH + \"error_response_simple.json\", \"rb\") as f:\n        ERROR_DATA_SIMPLE = f.read()\n\n    def testResponseError(self) -> None:\n        response = Response()\n        response.status_code = 400\n        response._content = self.ERROR_DATA\n\n        ex = PyYouTubeException(response=response)\n\n        self.assertEqual(ex.status_code, 400)\n        self.assertEqual(ex.message, \"Bad Request\")\n        self.assertEqual(ex.error_type, \"YouTubeException\")\n        error_msg = \"YouTubeException(status_code=400,message=Bad Request)\"\n        self.assertEqual(repr(ex), error_msg)\n        self.assertTrue(str(ex), error_msg)\n\n    def testResponseErrorSimple(self) -> None:\n        response = Response()\n        response.status_code = 400\n        response._content = self.ERROR_DATA_SIMPLE\n\n        ex = PyYouTubeException(response=response)\n        self.assertEqual(ex.status_code, 400)\n\n    def testErrorMessage(self):\n        response = ErrorMessage(status_code=ErrorCode.HTTP_ERROR, message=\"error\")\n\n        ex = PyYouTubeException(response=response)\n\n        self.assertEqual(ex.status_code, 10000)\n        self.assertEqual(ex.message, \"error\")\n        self.assertEqual(ex.error_type, \"PyYouTubeException\")\n"
  },
  {
    "path": "tests/test_youtube_utils.py",
    "content": "import unittest\n\nfrom pyyoutube import youtube_utils as utils\nfrom pyyoutube.error import PyYouTubeException\n\n\nclass UtilsTest(unittest.TestCase):\n    def testDurationConvert(self):\n        duration = \"PT14H23M42S\"\n        self.assertEqual(utils.get_video_duration(duration), 51822)\n\n        duration = \"PT14H23M42\"\n        with self.assertRaises(PyYouTubeException):\n            utils.get_video_duration(duration)\n"
  },
  {
    "path": "tests/utils/__init__.py",
    "content": ""
  },
  {
    "path": "tests/utils/test_params_checker.py",
    "content": "import unittest\n\nimport pyyoutube\nfrom pyyoutube.utils.params_checker import enf_comma_separated, enf_parts\n\n\nclass ParamCheckerTest(unittest.TestCase):\n    def testEnfCommaSeparated(self) -> None:\n        self.assertIsNone(enf_comma_separated(\"id\", None))\n        self.assertEqual(enf_comma_separated(\"id\", \"my_id\"), \"my_id\")\n        self.assertEqual(enf_comma_separated(\"id\", \"id1,id2\"), \"id1,id2\")\n        self.assertEqual(enf_comma_separated(\"id\", [\"id1\", \"id2\"]), \"id1,id2\")\n        self.assertEqual(enf_comma_separated(\"id\", (\"id1\", \"id2\")), \"id1,id2\")\n        self.assertTrue(enf_comma_separated(\"id\", {\"id1\", \"id2\"}))\n\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            enf_comma_separated(\"id\", 1)\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            enf_comma_separated(\"id\", [None, None])\n\n    def testEnfParts(self) -> None:\n        self.assertTrue(enf_parts(resource=\"channels\", value=None))\n        self.assertTrue(enf_parts(resource=\"channels\", value=\"id\"), \"id\")\n        self.assertTrue(enf_parts(resource=\"channels\", value=\"id,snippet\"))\n        self.assertTrue(enf_parts(resource=\"channels\", value=[\"id\", \"snippet\"]))\n        self.assertTrue(enf_parts(resource=\"channels\", value=(\"id\", \"snippet\")))\n        self.assertTrue(enf_parts(resource=\"channels\", value={\"id\", \"snippet\"}))\n\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            enf_parts(resource=\"channels\", value=1)\n\n        with self.assertRaises(pyyoutube.PyYouTubeException):\n            enf_parts(resource=\"channels\", value=\"not_part\")\n"
  }
]