The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .devcontainer
    └── devcontainer.json
├── .gitattributes
├── .github
    ├── .documentation.yml
    ├── FUNDING.yml
    ├── ISSUE_TEMPLATE
    │   ├── bug_report.md
    │   ├── feature_request.md
    │   └── installation-help.md
    ├── dependabot.yml
    ├── stale.yml
    └── workflows
    │   ├── docs.yml
    │   ├── package-test.yml
    │   └── python-publish.yml
├── .gitignore
├── .sphinx
    ├── .gitignore
    ├── Makefile
    ├── README.md
    ├── TikTokApi.api.rst
    ├── TikTokApi.rst
    ├── TikTokApi.stealth.rst
    ├── api
    │   ├── comment.rst
    │   ├── hashtag.rst
    │   ├── search.rst
    │   ├── sound.rst
    │   ├── trending.rst
    │   ├── user.rst
    │   └── video.rst
    ├── conf.py
    ├── index.rst
    ├── make.bat
    └── modules.rst
├── CITATION.cff
├── Dockerfile
├── LICENSE
├── LICENSE.txt
├── MANIFEST
├── README.md
├── TikTokApi
    ├── __init__.py
    ├── api
    │   ├── __init__.py
    │   ├── comment.py
    │   ├── hashtag.py
    │   ├── playlist.py
    │   ├── search.py
    │   ├── sound.py
    │   ├── trending.py
    │   ├── user.py
    │   └── video.py
    ├── exceptions.py
    ├── helpers.py
    ├── stealth
    │   ├── __init__.py
    │   ├── js
    │   │   ├── __init__.py
    │   │   ├── chrome_app.py
    │   │   ├── chrome_csi.py
    │   │   ├── chrome_hairline.py
    │   │   ├── chrome_load_times.py
    │   │   ├── chrome_runtime.py
    │   │   ├── generate_magic_arrays.py
    │   │   ├── iframe_contentWindow.py
    │   │   ├── media_codecs.py
    │   │   ├── navigator_hardwareConcurrency.py
    │   │   ├── navigator_languages.py
    │   │   ├── navigator_permissions.py
    │   │   ├── navigator_platform.py
    │   │   ├── navigator_plugins.py
    │   │   ├── navigator_userAgent.py
    │   │   ├── navigator_vendor.py
    │   │   ├── utils.py
    │   │   ├── webgl_vendor.py
    │   │   └── window_outerdimensions.py
    │   └── stealth.py
    └── tiktok.py
├── _config.yml
├── examples
    ├── comment_example.py
    ├── hashtag_example.py
    ├── playlist_example.py
    ├── search_example.py
    ├── sound_example.py
    ├── trending_example.py
    ├── user_example.py
    └── video_example.py
├── imgs
    ├── EnsembleData.png
    ├── tikapi.png
    ├── tiktok_captcha_solver.png
    └── webshare.png
├── requirements.txt
├── setup.cfg
├── setup.py
└── tests
    ├── __init__.py
    ├── test_comments.py
    ├── test_hashtag.py
    ├── test_integration.py
    ├── test_playlist.py
    ├── test_search.py
    ├── test_sound.py
    ├── test_trending.py
    ├── test_user.py
    └── test_video.py


/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 |     "name": "TikTokAPI",
3 |     "image": "mcr.microsoft.com/devcontainers/universal",
4 |     "postStartCommand": "python3 -m pip install -r requirements.txt && python3 -m playwright install",
5 | }


--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 | 


--------------------------------------------------------------------------------
/.github/.documentation.yml:
--------------------------------------------------------------------------------
1 | - .sphinx


--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ['https://www.paypal.me/dteather']
2 | github: davidteather


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Bug report
 3 | about: Create a report to help us improve
 4 | title: "[BUG] - Your Error Here"
 5 | labels: bug
 6 | assignees: ''
 7 | 
 8 | ---
 9 | Fill Out the template :)
10 | 
11 | **Describe the bug**
12 | 
13 | A clear and concise description of what the bug is.
14 | 
15 | **The buggy code**
16 | 
17 | Please add any relevant code that is giving you unexpected results.
18 | 
19 | Preferably the smallest amount of code to reproduce the issue.
20 | 
21 | 
22 | **SET LOGGING LEVEL TO INFO BEFORE POSTING CODE OUTPUT**
23 | ```py
24 | import logging
25 | TikTokApi(logging_level=logging.INFO) # SETS LOGGING_LEVEL TO INFO
26 | # Hopefully the info level will help you debug or at least someone else on the issue
27 | ```
28 | 
29 | ```py
30 | # Code Goes Here
31 | ```
32 | 
33 | **Expected behavior**
34 | 
35 | A clear and concise description of what you expected to happen.
36 | 
37 | **Error Trace (if any)**
38 | 
39 | Put the error trace below if there's any error thrown.
40 | ```
41 | # Error Trace Here
42 | ```
43 | 
44 | **Desktop (please complete the following information):**
45 |  - OS: [e.g. Windows 10]
46 |  - TikTokApi Version [e.g. 5.0.0] - if out of date upgrade before posting an issue
47 | 
48 | **Additional context**
49 | 
50 | Add any other context about the problem here.
51 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Feature request
 3 | about: Suggest an idea for this project
 4 | title: "[FEATURE_REQUEST] - What you want here"
 5 | labels: feature_request
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 | 
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 | 
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 | 
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/installation-help.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Installation Help
 3 | about: If you're having trouble getting this to run for the first time please use
 4 |   this template
 5 | title: "[INSTALLATION] - Your error here"
 6 | labels: installation_help
 7 | assignees: ''
 8 | 
 9 | ---
10 | 
11 | Please first check the closed issues on GitHub for people with similar problems to you.
12 | If you'd like more instant help from the community consider joining the [discord](https://discord.gg/yyPhbfma6f)
13 | 
14 | **Describe the error**
15 | 
16 | Put the error trace here.
17 | 
18 | **The buggy code**
19 | 
20 | Please insert the code that is throwing errors or is giving you weird unexpected results.
21 | 
22 | **Error Trace (if any)**
23 | 
24 | Put the error trace below if there's any error thrown.
25 | ```
26 | # Error Trace Here
27 | ```
28 | 
29 | **Desktop (please complete the following information):**
30 |  - OS: [e.g. Windows 10]
31 |  - TikTokApi Version [e.g. 3.3.1] - if out of date upgrade before posting an issue
32 | 
33 | **Additional context**
34 | 
35 | Put what you have already tried. Your problem is probably in the closed issues tab already.
36 | 


--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 |   directory: "/"
5 |   schedule:
6 |     interval: daily
7 |   open-pull-requests-limit: 10
8 | 


--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
 1 | # Number of days of inactivity before an issue becomes stale
 2 | daysUntilStale: 60
 3 | # Number of days of inactivity before a stale issue is closed
 4 | daysUntilClose: 7
 5 | # Issues with these labels will never be considered stale
 6 | exemptLabels:
 7 |   - pinned
 8 |   - security
 9 |   - severe
10 | # Label to use when marking an issue as stale
11 | staleLabel: stale
12 | # Comment to post when marking an issue as stale. Set to `false` to disable
13 | markComment: >
14 |   This issue has been automatically marked as stale because it has not had
15 |   recent activity. It will be closed if no further activity occurs. Thank you
16 |   for your contributions.
17 | # Comment to post when closing a stale issue. Set to `false` to disable
18 | closeComment: false
19 | 


--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
 1 | name: Build and Deploy Sphinx Documentation
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 | 
 8 | jobs:
 9 |   build-and-deploy:
10 |     runs-on: ubuntu-latest
11 |     steps:
12 |     - name: Checkout Code
13 |       uses: actions/checkout@v2
14 | 
15 |     - name: Set up Python
16 |       uses: actions/setup-python@v2
17 |       with:
18 |         python-version: '3.x'
19 | 
20 |     - name: Install Dependencies
21 |       run: |
22 |         python -m pip install --upgrade pip
23 |         pip install -r requirements.txt
24 |         pip install sphinx sphinx_rtd_theme myst-parser
25 | 
26 |     - name: Build Documentation
27 |       run: |
28 |         cd .sphinx
29 |         make html
30 | 
31 |     - name: Deploy to GitHub Pages
32 |       uses: peaceiris/actions-gh-pages@v3
33 |       with:
34 |         github_token: ${{ secrets.GITHUB_TOKEN }}
35 |         publish_dir: ./.sphinx/docs/html
36 | 


--------------------------------------------------------------------------------
/.github/workflows/package-test.yml:
--------------------------------------------------------------------------------
 1 | name: TikTokApi CI
 2 | on:
 3 |   push:
 4 |     branches:
 5 |       - main
 6 |   pull_request:
 7 |     branches:
 8 |       - main
 9 |       - nightly
10 |       - "releases/*"
11 | 
12 | jobs:
13 |   Unit-Tests:
14 |     timeout-minutes: 30
15 |     runs-on: ${{ matrix.os }}
16 |     strategy:
17 |       fail-fast: false
18 |       matrix:
19 |         os: [macos-latest]
20 |         python-version: ["3.9", "3.11"]
21 |     steps:
22 |       - uses: actions/checkout@v2
23 |       - uses: microsoft/playwright-github-action@v1
24 |       - name: Install Python ${{ matrix.python-version }}
25 |         uses: actions/setup-python@v2
26 |         with:
27 |           python-version: ${{ matrix.python-version }}
28 |       - name: Setup dependencies
29 |         run: |
30 |           python -m pip install --upgrade pip
31 |           pip install -r requirements.txt
32 |           pip install pytest
33 |           pip install pytest-asyncio
34 |           python -m playwright install
35 | 
36 |       - name: Run Tests
37 |         env:
38 |           ms_token: ${{ secrets.ms_token }}
39 |         run: pytest tests
40 | 


--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
 1 | # This workflows will upload a Python Package using Twine when a release is created
 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
 3 | 
 4 | name: Upload Python Package
 5 | 
 6 | on:
 7 |   release:
 8 |     types: [created]
 9 |   workflow_dispatch:
10 | 
11 | jobs:
12 |   deploy:
13 | 
14 |     runs-on: ubuntu-latest
15 | 
16 |     steps:
17 |     - uses: actions/checkout@v2
18 |     - name: Set up Python
19 |       uses: actions/setup-python@v2
20 |       with:
21 |         python-version: '3.x'
22 |     - name: Install dependencies
23 |       run: |
24 |         python -m pip install --upgrade pip
25 |         pip install setuptools wheel twine
26 |     
27 |     - name: Install dependencies
28 |       run: |
29 |         python -m pip install --upgrade pip
30 |         pip install setuptools wheel twine
31 | 
32 |     - name: Build and publish
33 |       env:
34 |         TWINE_USERNAME: __token__
35 |         TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
36 |       run: |
37 |         python setup.py sdist bdist_wheel
38 |         twine upload dist/*
39 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | __pycache__/tiktok.cpython-37.pyc
 2 | bmp.log
 3 | geckodriver.log
 4 | server.log
 5 | TikTok-Api/bmp.log
 6 | TikTok-Api/geckodriver.log
 7 | TikTok-Api/server.log
 8 | browsermob-proxy/*
 9 | myScripts/*
10 | test.py
11 | debug.log
12 | res.html
13 | tmp/*
14 | dist/*
15 | *.egg-info
16 | tmp/
17 | tmp
18 | .pytest_cache/*
19 | test.mp4
20 | test.txt
21 | .pytest_cache/*
22 | tests/__pycache__/*
23 | *.pyc
24 | acrawl.js
25 | test2.py
26 | build
27 | MANIFEST
28 | src
29 | .vscode
30 | .env
31 | /.idea/
32 | /TikTok-Api.iml
33 | 


--------------------------------------------------------------------------------
/.sphinx/.gitignore:
--------------------------------------------------------------------------------
1 | docs


--------------------------------------------------------------------------------
/.sphinx/Makefile:
--------------------------------------------------------------------------------
 1 | # Minimal makefile for Sphinx documentation
 2 | #
 3 | 
 4 | # You can set these variables from the command line, and also
 5 | # from the environment for the first two.
 6 | SPHINXOPTS    ?=
 7 | SPHINXBUILD   ?= sphinx-build
 8 | SOURCEDIR     = .
 9 | BUILDDIR      = docs
10 | 
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | 	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 | 
15 | .PHONY: help Makefile
16 | 
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | 	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 | 
22 | serve:
23 | 	@sphinx-autobuild . docs


--------------------------------------------------------------------------------
/.sphinx/README.md:
--------------------------------------------------------------------------------
1 | The docs for TikTokAPI
2 | 
3 | Run locally with: make serve
4 | 
5 | Build with: make html


--------------------------------------------------------------------------------
/.sphinx/TikTokApi.api.rst:
--------------------------------------------------------------------------------
 1 | .. _tiktok-api-docs:
 2 | TikTokApi.api package
 3 | =====================
 4 | 
 5 | This package wraps each entity from TikTok into a class with high-level methods to interact with the TikTok object.
 6 | 
 7 | Comment
 8 | ===============================
 9 | 
10 | .. include:: api/comment.rst
11 | 
12 | User
13 | ================
14 | 
15 | .. include:: api/user.rst
16 | 
17 | Trending
18 | =================
19 | 
20 | .. include:: api/trending.rst
21 | 
22 | Search
23 | ======
24 | 
25 | .. include:: api/search.rst
26 | 
27 | Hashtags
28 | ===================
29 | 
30 | .. include:: api/hashtag.rst
31 | 
32 | Sound
33 | ================
34 | 
35 | .. include:: api/sound.rst
36 | 
37 | Video
38 | ================
39 | 
40 | .. include:: api/video.rst


--------------------------------------------------------------------------------
/.sphinx/TikTokApi.rst:
--------------------------------------------------------------------------------
 1 | .. _tiktok-api-main:
 2 | TikTokApi package
 3 | =================
 4 | 
 5 | Subpackages
 6 | ===========
 7 | 
 8 | .. toctree::
 9 |    :maxdepth: 1
10 | 
11 |    TikTokApi.api
12 | 
13 | TikTokApi Main Class
14 | ===========================
15 | 
16 | This is the main TikTokApi module.
17 | It contains the TikTokApi class which is the main class of the package.
18 | 
19 | .. automodule:: TikTokApi.tiktok
20 |    :members:
21 |    :undoc-members:
22 |    :show-inheritance:
23 | 
24 | TikTokApi.exceptions module
25 | ===========================
26 | 
27 | .. automodule:: TikTokApi.exceptions
28 |    :members:
29 |    :undoc-members:
30 |    :show-inheritance:
31 | 
32 | TikTokApi.helpers module
33 | ===========================
34 | 
35 | .. automodule:: TikTokApi.helpers
36 |    :members:
37 |    :undoc-members:
38 |    :show-inheritance:


--------------------------------------------------------------------------------
/.sphinx/TikTokApi.stealth.rst:
--------------------------------------------------------------------------------
 1 | TikTokApi.stealth package
 2 | =========================
 3 | 
 4 | This package is a modified version of `playwright_stealth <https://github.com/AtuboDad/playwright_stealth>`_, used to mask the browser as a real browser.
 5 | 
 6 | This package is used to prevent TikTok from detecting that you are using a bot.
 7 | 
 8 | You probably shouldn't be interacting with this package directly, but rather through the :class:`TikTokApi` class.
 9 | 
10 | Here's some stealth resources that I always keep forgetting, so I'm putting them here:
11 | 
12 | * `SannySoft Bot Detector <https://bot.sannysoft.com/>`_
13 | * `Are you headless? <https://arh.antoinevastel.com/bots/areyouheadless>`_
14 |     * `DataDome Detector <https://antoinevastel.com/bots/datadome>`_
15 | * `Am I Unique? <https://amiunique.org/>`_ (cool because they split up the distribution of each field, I don't really use much)
16 | 
17 | Bot Detection Resources:
18 | 
19 | * `FingerprintJS on GitHub <https://github.com/fingerprintjs/fingerprintjs>`_
20 | 
21 | TikTokApi.stealth.stealth
22 | ================================
23 | 
24 | .. automodule:: TikTokApi.stealth.stealth
25 |    :members:
26 |    :undoc-members:
27 |    :show-inheritance:


--------------------------------------------------------------------------------
/.sphinx/api/comment.rst:
--------------------------------------------------------------------------------
1 | TikTokApi.api.comment module
2 | ----------------------------
3 | 
4 | .. automodule:: TikTokApi.api.comment
5 |    :members:
6 |    :undoc-members:
7 |    :show-inheritance:
8 | 


--------------------------------------------------------------------------------
/.sphinx/api/hashtag.rst:
--------------------------------------------------------------------------------
1 | TikTokApi.api.hashtag module
2 | ----------------------------
3 | 
4 | .. automodule:: TikTokApi.api.hashtag
5 |    :members:
6 |    :undoc-members:
7 |    :show-inheritance:
8 | 


--------------------------------------------------------------------------------
/.sphinx/api/search.rst:
--------------------------------------------------------------------------------
1 | TikTokApi.api.search module
2 | ----------------------------
3 | 
4 | .. automodule:: TikTokApi.api.search
5 |    :members:
6 |    :undoc-members:
7 |    :show-inheritance:
8 | 


--------------------------------------------------------------------------------
/.sphinx/api/sound.rst:
--------------------------------------------------------------------------------
1 | TikTokApi.api.sound module
2 | ----------------------------
3 | 
4 | .. automodule:: TikTokApi.api.sound
5 |    :members:
6 |    :undoc-members:
7 |    :show-inheritance:
8 | 


--------------------------------------------------------------------------------
/.sphinx/api/trending.rst:
--------------------------------------------------------------------------------
1 | TikTokApi.api.trending module
2 | -----------------------------
3 | 
4 | .. automodule:: TikTokApi.api.trending
5 |    :members:
6 |    :undoc-members:
7 |    :show-inheritance:
8 | 


--------------------------------------------------------------------------------
/.sphinx/api/user.rst:
--------------------------------------------------------------------------------
1 | TikTokApi.api.user module
2 | ----------------------------
3 | 
4 | .. automodule:: TikTokApi.api.user
5 |    :members:
6 |    :undoc-members:
7 |    :show-inheritance:
8 | 


--------------------------------------------------------------------------------
/.sphinx/api/video.rst:
--------------------------------------------------------------------------------
1 | TikTokApi.api.video module
2 | ----------------------------
3 | 
4 | .. automodule:: TikTokApi.api.video
5 |    :members:
6 |    :undoc-members:
7 |    :show-inheritance:
8 | 


--------------------------------------------------------------------------------
/.sphinx/conf.py:
--------------------------------------------------------------------------------
 1 | # Configuration file for the Sphinx documentation builder.
 2 | #
 3 | # For the full list of built-in configuration values, see the documentation:
 4 | # https://www.sphinx-doc.org/en/main/usage/configuration.html
 5 | 
 6 | # -- Project information -----------------------------------------------------
 7 | # https://www.sphinx-doc.org/en/main/usage/configuration.html#project-information
 8 | 
 9 | import sys
10 | import os
11 | 
12 | sys.path.insert(0, os.path.abspath("../."))
13 | sys.path.insert(0, os.path.abspath(".."))
14 | sys.path.insert(0, os.path.abspath("../.."))
15 | 
16 | project = "TikTokAPI"
17 | copyright = "2023, David Teather"
18 | author = "David Teather"
19 | release = "v7.1.0"
20 | 
21 | # -- General configuration ---------------------------------------------------
22 | # https://www.sphinx-doc.org/en/main/usage/configuration.html#general-configuration
23 | 
24 | extensions = [
25 |     "sphinx.ext.autodoc",
26 |     "sphinx.ext.viewcode",
27 |     "sphinx.ext.todo",
28 |     "sphinx.ext.githubpages",
29 |     "sphinx.ext.napoleon",
30 |     "myst_parser",
31 | ]
32 | 
33 | templates_path = ["_templates"]
34 | exclude_patterns = ["docs", "Thumbs.db", ".DS_Store"]
35 | 
36 | napoleon_google_docstring = True
37 | 
38 | 
39 | # -- Options for HTML output -------------------------------------------------
40 | # https://www.sphinx-doc.org/en/main/usage/configuration.html#options-for-html-output
41 | 
42 | html_theme = "sphinx_rtd_theme"
43 | html_baseurl = "https://davidteather.github.io/TikTok-Api/"
44 | 
45 | source_suffix = {".rst": "restructuredtext", ".md": "markdown"}
46 | 


--------------------------------------------------------------------------------
/.sphinx/index.rst:
--------------------------------------------------------------------------------
 1 | TikTokAPI Quick Start
 2 | =====================================
 3 | 
 4 | .. toctree::
 5 |    :maxdepth: 2
 6 |    :caption: Contents:
 7 | 
 8 | View the above tree for detailed documentation on the package.
 9 | 
10 | .. MD -> .. mdinclude:: ../README.md
11 | 
12 | .. include:: ../README.md
13 |    :parser: myst_parser.sphinx_
14 | 
15 | TikTokAPI Full Documentation
16 | ============================
17 | * :ref:`tiktok-api-main` (main parent class)
18 | * :ref:`tiktok-api-docs` (classes for each TikTok entity)
19 | 
20 | * :ref:`modindex`
21 | * :ref:`search`
22 | * :ref:`genindex`
23 | 


--------------------------------------------------------------------------------
/.sphinx/make.bat:
--------------------------------------------------------------------------------
 1 | @ECHO OFF
 2 | 
 3 | pushd %~dp0
 4 | 
 5 | REM Command file for Sphinx documentation
 6 | 
 7 | if "%SPHINXBUILD%" == "" (
 8 | 	set SPHINXBUILD=sphinx-build
 9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=docs
12 | 
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | 	echo.
16 | 	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | 	echo.installed, then set the SPHINXBUILD environment variable to point
18 | 	echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | 	echo.may add the Sphinx directory to PATH.
20 | 	echo.
21 | 	echo.If you don't have Sphinx installed, grab it from
22 | 	echo.https://www.sphinx-doc.org/
23 | 	exit /b 1
24 | )
25 | 
26 | if "%1" == "" goto help
27 | 
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 | 
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 | 
34 | :end
35 | popd
36 | 


--------------------------------------------------------------------------------
/.sphinx/modules.rst:
--------------------------------------------------------------------------------
1 | TikTokApi
2 | =========
3 | .. toctree::
4 |    :maxdepth: 4
5 | 
6 |    TikTokApi
7 | 


--------------------------------------------------------------------------------
/CITATION.cff:
--------------------------------------------------------------------------------
 1 | cff-version: 1.2.0
 2 | authors:
 3 |   - family-names: "Teather"
 4 |     given-names: "David"
 5 |     orcid: "https://orcid.org/0000-0002-9467-4676"
 6 | title: "TikTokAPI"
 7 | url: "https://github.com/davidteather/tiktok-api"
 8 | version: 7.1.0
 9 | date-released: 2025-04-13
10 | 


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/playwright:focal
2 | 
3 | RUN apt-get update && apt-get install -y python3-pip
4 | COPY . .
5 | RUN pip3 install TikTokApi
6 | RUN python3 -m playwright install
7 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2019 David Teather
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.


--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2019 David Teather
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.


--------------------------------------------------------------------------------
/MANIFEST:
--------------------------------------------------------------------------------
1 | # file GENERATED by distutils, do NOT edit
2 | setup.cfg
3 | setup.py
4 | TikTokApi\__init__.py
5 | TikTokApi\tiktok.py
6 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # Unofficial TikTok API in Python
  2 | 
  3 | This is an unofficial api wrapper for TikTok.com in python. With this api you are able to call most trending and fetch specific user information as well as much more.
  4 | 
  5 | [![DOI](https://zenodo.org/badge/188710490.svg)](https://zenodo.org/badge/latestdoi/188710490) [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white&style=flat-square)](https://www.linkedin.com/in/davidteather/) [![Sponsor Me](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/davidteather) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/davidteather/TikTok-Api)](https://github.com/davidteather/TikTok-Api/releases) [![GitHub](https://img.shields.io/github/license/davidteather/TikTok-Api)](https://github.com/davidteather/TikTok-Api/blob/main/LICENSE) [![Downloads](https://pepy.tech/badge/tiktokapi)](https://pypi.org/project/TikTokApi/) ![](https://visitor-badge.laobi.icu/badge?page_id=davidteather.TikTok-Api) [![Support Server](https://img.shields.io/discord/783108952111579166.svg?color=7289da&logo=discord&style=flat-square)](https://discord.gg/yyPhbfma6f)
  6 | 
  7 | This api is designed to **retrieve data** TikTok. It **can not be used post or upload** content to TikTok on the behalf of a user. It has **no support for any user-authenticated routes**, if you can't access it while being logged out on their website you can't access it here.
  8 | 
  9 | ## Sponsors
 10 | 
 11 | These sponsors have paid to be placed here or are my own affiliate links which I may earn a commission from, and beyond that I do not have any affiliation with them. The TikTokAPI package will always be free and open-source. If you wish to be a sponsor of this project check out my [GitHub sponsors page](https://github.com/sponsors/davidteather).
 12 | 
 13 | <div align="center">
 14 |     <a href="https://tikapi.io/?ref=davidteather" target="_blank">
 15 |         <img src="https://raw.githubusercontent.com/davidteather/TikTok-Api/main/imgs/tikapi.png" width="100" alt="TikApi">
 16 |         <div>
 17 |             <b>TikAPI</b> is a paid TikTok API service providing a full out-of-the-box solution, making life easier for developers — trusted by 500+ companies.
 18 |         </div>
 19 |     </a>
 20 |     <br>
 21 |     <a href="https://www.ensembledata.com/?utm_source=github&utm_medium=githubpage&utm_campaign=david_thea_github&utm_id=david_thea_github" target="_blank">
 22 |         <img src="https://raw.githubusercontent.com/davidteather/TikTok-Api/main/imgs/EnsembleData.png" width="100" alt="Ensemble Data">
 23 |         <b></b>
 24 |         <div>
 25 |          <b>EnsembleData</b> is the leading API provider for scraping Tiktok, Instagram, Youtube, and more. <br> Trusted by the major influencer marketing and social media listening platforms.
 26 |         </div>
 27 |     </a>
 28 |     <br>
 29 |     <a href="https://www.sadcaptcha.com?ref=davidteather" target="_blank">
 30 |         <img src="https://raw.githubusercontent.com/davidteather/TikTok-Api/main/imgs/tiktok_captcha_solver.png" width="100" alt="TikTok Captcha Solver">
 31 |         <b></b>
 32 |         <div>
 33 |          <b>TikTok Captcha Solver: </b> Bypass any TikTok captcha in just two lines of code.<br> Scale your TikTok automation and get unblocked with SadCaptcha.
 34 |         </div>
 35 |     </a>
 36 |     <br>
 37 |     <a href="https://www.webshare.io/?referral_code=3x5812idzzzp" target="_blank">
 38 |         <img src="https://raw.githubusercontent.com/davidteather/TikTok-Api/main/imgs/webshare.png" width="100" alt="TikTok Captcha Solver">
 39 |         <b></b>
 40 |         <div>
 41 |          <b>Cheap, Reliable Proxies: </b> Supercharge your web scraping with fast, reliable proxies. Try 10 free datacenter proxies today!
 42 |         </div>
 43 |     </a>
 44 | </div>
 45 | 
 46 | ## Table of Contents
 47 | 
 48 | - [Documentation](#documentation)
 49 | - [Getting Started](#getting-started)
 50 |   - [How to Support The Project](#how-to-support-the-project)
 51 |   - [Installing](#installing)
 52 |   - [Common Issues](#common-issues)
 53 | - [Quick Start Guide](#quick-start-guide)
 54 |   - [Examples](https://github.com/davidteather/TikTok-Api/tree/main/examples)
 55 | 
 56 | [**Upgrading from V5 to V6**](#upgrading-from-v5-to-v6)
 57 | 
 58 | ## Documentation
 59 | 
 60 | You can find the full documentation [here](https://davidteather.github.io/TikTok-Api)
 61 | 
 62 | ## Getting Started
 63 | 
 64 | To get started using this API follow the instructions below.
 65 | 
 66 | **Note:** If you want to learn how to web scrape websites check my [free and open-source course for learning everything web scraping](https://github.com/davidteather/everything-web-scraping)
 67 | 
 68 | ### How to Support The Project
 69 | 
 70 | - Star the repo 😎
 71 | - Consider [sponsoring](https://github.com/sponsors/davidteather) me on GitHub
 72 | - Send me an email or a [LinkedIn](https://www.linkedin.com/in/davidteather/) message telling me what you're using the API for, I really like hearing what people are using it for.
 73 | - Submit PRs for issues :)
 74 | 
 75 | ### Installing
 76 | 
 77 | **Note:** Installation requires python3.9+
 78 | 
 79 | If you run into an issue please check the closed issues on the github, although feel free to re-open a new issue if you find an issue that's been closed for a few months. The codebase can and does run into similar issues as it has before, because TikTok changes things up.
 80 | 
 81 | ```sh
 82 | pip install TikTokApi
 83 | python -m playwright install
 84 | ```
 85 | 
 86 | If you would prefer a video walk through of setting up this package [YouTube video](https://www.youtube.com/watch?v=-uCt1x8kINQ) just for that. (is a version out of date, installation is the same though)
 87 | 
 88 | If you want a quick video to listen for [TikTok Live](https://www.youtube.com/watch?v=307ijmA3_lc) events in python.
 89 | 
 90 | #### Docker Installation
 91 | 
 92 | Clone this repository onto a local machine (or just the Dockerfile since it installs TikTokApi from pip) then run the following commands.
 93 | 
 94 | ```sh
 95 | docker pull mcr.microsoft.com/playwright:focal
 96 | docker build . -t tiktokapi:latest
 97 | docker run -v TikTokApi --rm tiktokapi:latest python3 your_script.py
 98 | ```
 99 | 
100 | **Note** this assumes your script is named your_script.py and lives in the root of this directory.
101 | 
102 | ### Common Issues
103 | 
104 | - **EmptyResponseException** - this means TikTok is blocking the request and detects you're a bot. This can be a problem with your setup or the library itself
105 |   - you may need a proxy to successfuly scrape TikTok, I've made a [web scraping lesson](https://github.com/davidteather/everything-web-scraping/tree/main/002-proxies) explaining the differences of "tiers" of proxies, I've personally had success with [webshare's residential proxies](https://www.webshare.io/?referral_code=3x5812idzzzp) (affiliate link), but you might have success on their free data center IPs or a cheaper competitor.
106 | 
107 | - **Browser Has no Attribute** - make sure you ran `python3 -m playwright install`, if your error persists try the [playwright-python](https://github.com/microsoft/playwright-python) quickstart guide and diagnose issues from there.
108 | 
109 | - **API methods returning Coroutine** - many of the API's methods are async so make sure your program awaits them for proper functionality
110 | 
111 | ## Quick Start Guide
112 | 
113 | Here's a quick bit of code to get the most recent trending videos on TikTok. There's more examples in the [examples](https://github.com/davidteather/TikTok-Api/tree/main/examples) directory.
114 | 
115 | **Note:** If you want to learn how to web scrape websites check my [free and open-source course for web scraping](https://github.com/davidteather/web-scraping-with-reverse-engineering)
116 | 
117 | ```py
118 | from TikTokApi import TikTokApi
119 | import asyncio
120 | import os
121 | 
122 | ms_token = os.environ.get("ms_token", None) # get your own ms_token from your cookies on tiktok.com
123 | 
124 | async def trending_videos():
125 |     async with TikTokApi() as api:
126 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"))
127 |         async for video in api.trending.videos(count=30):
128 |             print(video)
129 |             print(video.as_dict)
130 | 
131 | if __name__ == "__main__":
132 |     asyncio.run(trending_videos())
133 | ```
134 | 
135 | To directly run the example scripts from the repository root, use the `-m` option on python.
136 | 
137 | ```sh
138 | python -m examples.trending_example
139 | ```
140 | 
141 | You can access the full data dictionary the object was created from with `.as_dict`. On a video this may look like
142 | [this](https://gist.github.com/davidteather/7c30780bbc30772ba11ec9e0b909e99d). TikTok changes their structure from time to time so it's worth investigating the structure of the dictionary when you use this package.


--------------------------------------------------------------------------------
/TikTokApi/__init__.py:
--------------------------------------------------------------------------------
1 | from TikTokApi.tiktok import TikTokApi, TikTokPlaywrightSession
2 | 


--------------------------------------------------------------------------------
/TikTokApi/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidteather/TikTok-Api/62a8cfa8ab7bb5bbdd0f8c8b13e84731fff7ac75/TikTokApi/api/__init__.py


--------------------------------------------------------------------------------
/TikTokApi/api/comment.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from typing import ClassVar, AsyncIterator, Optional
 4 | from typing import TYPE_CHECKING, ClassVar, Optional
 5 | 
 6 | from TikTokApi.exceptions import InvalidResponseException
 7 | 
 8 | if TYPE_CHECKING:
 9 |     from ..tiktok import TikTokApi
10 |     from .user import User
11 | 
12 | 
13 | class Comment:
14 |     """
15 |     A TikTok Comment.
16 | 
17 |     Example Usage
18 |         .. code-block:: python
19 | 
20 |             for comment in video.comments:
21 |                 print(comment.text)
22 |                 print(comment.as_dict)
23 |     """
24 | 
25 |     parent: ClassVar[TikTokApi]
26 | 
27 |     id: str
28 |     """The id of the comment"""
29 |     author: ClassVar[User]
30 |     """The author of the comment"""
31 |     text: str
32 |     """The contents of the comment"""
33 |     likes_count: int
34 |     """The amount of likes of the comment"""
35 |     as_dict: dict
36 |     """The raw data associated with this comment"""
37 | 
38 |     def __init__(self, data: Optional[dict] = None):
39 |         if data is not None:
40 |             self.as_dict = data
41 |             self.__extract_from_data()
42 | 
43 |     def __extract_from_data(self):
44 |         data = self.as_dict
45 |         self.id = self.as_dict["cid"]
46 |         self.text = self.as_dict["text"]
47 | 
48 |         usr = self.as_dict["user"]
49 |         self.author = self.parent.user(
50 |             user_id=usr["uid"], username=usr["unique_id"], sec_uid=usr["sec_uid"]
51 |         )
52 |         self.likes_count = self.as_dict["digg_count"]
53 | 
54 |     async def replies(self, count=20, cursor=0, **kwargs) -> AsyncIterator[Comment]:
55 |         found = 0
56 | 
57 |         while found < count:
58 |             params = {
59 |                 "count": 20,
60 |                 "cursor": cursor,
61 |                 "item_id": self.author.user_id,
62 |                 "comment_id": self.id,
63 |             }
64 | 
65 |             resp = await self.parent.make_request(
66 |                 url="https://www.tiktok.com/api/comment/list/reply/",
67 |                 params=params,
68 |                 headers=kwargs.get("headers"),
69 |                 session_index=kwargs.get("session_index"),
70 |             )
71 | 
72 |             if resp is None:
73 |                 raise InvalidResponseException(
74 |                     resp, "TikTok returned an invalid response."
75 |                 )
76 | 
77 |             for comment in resp.get("comments", []):
78 |                 yield self.parent.comment(data=comment)
79 |                 found += 1
80 | 
81 |             if not resp.get("has_more", False):
82 |                 return
83 | 
84 |             cursor = resp.get("cursor")
85 | 
86 |     def __repr__(self):
87 |         return self.__str__()
88 | 
89 |     def __str__(self):
90 |         id = getattr(self, "id", None)
91 |         text = getattr(self, "text", None)
92 |         return f"TikTokApi.comment(comment_id='{id}', text='{text}')"
93 | 


--------------------------------------------------------------------------------
/TikTokApi/api/hashtag.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | from ..exceptions import *
  3 | 
  4 | from typing import TYPE_CHECKING, ClassVar, AsyncIterator, Optional
  5 | 
  6 | if TYPE_CHECKING:
  7 |     from ..tiktok import TikTokApi
  8 |     from .video import Video
  9 | 
 10 | 
 11 | class Hashtag:
 12 |     """
 13 |     A TikTok Hashtag/Challenge.
 14 | 
 15 |     Example Usage
 16 |         .. code-block:: python
 17 | 
 18 |             hashtag = api.hashtag(name='funny')
 19 |             async for video in hashtag.videos():
 20 |                 print(video.id)
 21 |     """
 22 | 
 23 |     parent: ClassVar[TikTokApi]
 24 | 
 25 |     id: Optional[str]
 26 |     """The ID of the hashtag"""
 27 |     name: Optional[str]
 28 |     """The name of the hashtag (omiting the #)"""
 29 |     as_dict: dict
 30 |     """The raw data associated with this hashtag."""
 31 | 
 32 |     def __init__(
 33 |         self,
 34 |         name: Optional[str] = None,
 35 |         id: Optional[str] = None,
 36 |         data: Optional[dict] = None,
 37 |     ):
 38 |         """
 39 |         You must provide the name or id of the hashtag.
 40 |         """
 41 | 
 42 |         if name is not None:
 43 |             self.name = name
 44 |         if id is not None:
 45 |             self.id = id
 46 | 
 47 |         if data is not None:
 48 |             self.as_dict = data
 49 |             self.__extract_from_data()
 50 | 
 51 |     async def info(self, **kwargs) -> dict:
 52 |         """
 53 |         Returns all information sent by TikTok related to this hashtag.
 54 | 
 55 |         Example Usage
 56 |             .. code-block:: python
 57 | 
 58 |                 hashtag = api.hashtag(name='funny')
 59 |                 hashtag_data = await hashtag.info()
 60 |         """
 61 |         if not self.name:
 62 |             raise TypeError(
 63 |                 "You must provide the name when creating this class to use this method."
 64 |             )
 65 | 
 66 |         url_params = {
 67 |             "challengeName": self.name,
 68 |             "msToken": kwargs.get("ms_token"),
 69 |         }
 70 | 
 71 |         resp = await self.parent.make_request(
 72 |             url="https://www.tiktok.com/api/challenge/detail/",
 73 |             params=url_params,
 74 |             headers=kwargs.get("headers"),
 75 |             session_index=kwargs.get("session_index"),
 76 |         )
 77 | 
 78 |         if resp is None:
 79 |             raise InvalidResponseException(resp, "TikTok returned an invalid response.")
 80 | 
 81 |         self.as_dict = resp
 82 |         self.__extract_from_data()
 83 |         return resp
 84 | 
 85 |     async def videos(self, count=30, cursor=0, **kwargs) -> AsyncIterator[Video]:
 86 |         """
 87 |         Returns TikTok videos that have this hashtag in the caption.
 88 | 
 89 |         Args:
 90 |             count (int): The amount of videos you want returned.
 91 |             cursor (int): The the offset of videos from 0 you want to get.
 92 | 
 93 |         Returns:
 94 |             async iterator/generator: Yields TikTokApi.video objects.
 95 | 
 96 |         Raises:
 97 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
 98 | 
 99 |         Example Usage:
100 |             .. code-block:: python
101 | 
102 |                 async for video in api.hashtag(name='funny').videos():
103 |                     # do something
104 |         """
105 | 
106 |         id = getattr(self, "id", None)
107 |         if id is None:
108 |             await self.info(**kwargs)
109 | 
110 |         found = 0
111 |         while found < count:
112 |             params = {
113 |                 "challengeID": self.id,
114 |                 "count": 35,
115 |                 "cursor": cursor,
116 |             }
117 | 
118 |             resp = await self.parent.make_request(
119 |                 url="https://www.tiktok.com/api/challenge/item_list/",
120 |                 params=params,
121 |                 headers=kwargs.get("headers"),
122 |                 session_index=kwargs.get("session_index"),
123 |             )
124 | 
125 |             if resp is None:
126 |                 raise InvalidResponseException(
127 |                     resp, "TikTok returned an invalid response."
128 |                 )
129 | 
130 |             for video in resp.get("itemList", []):
131 |                 yield self.parent.video(data=video)
132 |                 found += 1
133 | 
134 |             if not resp.get("hasMore", False):
135 |                 return
136 | 
137 |             cursor = resp.get("cursor")
138 | 
139 |     def __extract_from_data(self):
140 |         data = self.as_dict
141 |         keys = data.keys()
142 | 
143 |         if "title" in keys:
144 |             self.id = data["id"]
145 |             self.name = data["title"]
146 | 
147 |         if "challengeInfo" in keys:
148 |             if "challenge" in data["challengeInfo"]:
149 |                 self.id = data["challengeInfo"]["challenge"]["id"]
150 |                 self.name = data["challengeInfo"]["challenge"]["title"]
151 |                 self.split_name = data["challengeInfo"]["challenge"].get("splitTitle")
152 | 
153 |             if "stats" in data["challengeInfo"]:
154 |                 self.stats = data["challengeInfo"]["stats"]
155 | 
156 |         id = getattr(self, "id", None)
157 |         name = getattr(self, "name", None)
158 |         if None in (id, name):
159 |             Hashtag.parent.logger.error(
160 |                 f"Failed to create Hashtag with data: {data}\nwhich has keys {data.keys()}"
161 |             )
162 | 
163 |     def __repr__(self):
164 |         return self.__str__()
165 | 
166 |     def __str__(self):
167 |         return f"TikTokApi.hashtag(id='{getattr(self, 'id', None)}', name='{getattr(self, 'name', None)}')"
168 | 


--------------------------------------------------------------------------------
/TikTokApi/api/playlist.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | from typing import TYPE_CHECKING, ClassVar, AsyncIterator, Optional
  3 | from ..exceptions import InvalidResponseException
  4 | 
  5 | if TYPE_CHECKING:
  6 |     from ..tiktok import TikTokApi
  7 |     from .video import Video
  8 |     from .user import User
  9 | 
 10 | 
 11 | class Playlist:
 12 |     """
 13 |     A TikTok video playlist.
 14 | 
 15 |     Example Usage:
 16 |         .. code-block:: python
 17 | 
 18 |             playlist = api.playlist(id='7426714779919797038')
 19 |     """
 20 | 
 21 |     parent: ClassVar[TikTokApi]
 22 | 
 23 |     id: Optional[str]
 24 |     """The ID of the playlist."""
 25 |     name: Optional[str]
 26 |     """The name of the playlist."""
 27 |     video_count: Optional[int]
 28 |     """The video count of the playlist."""
 29 |     creator: Optional[User]
 30 |     """The creator of the playlist."""
 31 |     cover_url: Optional[str]
 32 |     """The cover URL of the playlist."""
 33 |     as_dict: dict
 34 |     """The raw data associated with this Playlist."""
 35 | 
 36 |     def __init__(
 37 |         self,
 38 |         id: Optional[str] = None,
 39 |         data: Optional[dict] = None,
 40 |     ):
 41 |         """
 42 |         You must provide the playlist id or playlist data otherwise this
 43 |         will not function correctly.
 44 |         """
 45 | 
 46 |         if id is None and data.get("id") is None:
 47 |             raise TypeError("You must provide id parameter.")
 48 | 
 49 |         self.id = id
 50 | 
 51 |         if data is not None:
 52 |             self.as_dict = data
 53 |             self.__extract_from_data()
 54 | 
 55 |     async def info(self, **kwargs) -> dict:
 56 |         """
 57 |         Returns a dictionary of information associated with this Playlist.
 58 | 
 59 |         Returns:
 60 |             dict: A dictionary of information associated with this Playlist.
 61 | 
 62 |         Raises:
 63 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
 64 | 
 65 |         Example Usage:
 66 |             .. code-block:: python
 67 | 
 68 |                 user_data = await api.playlist(id='7426714779919797038').info()
 69 |         """
 70 | 
 71 |         id = getattr(self, "id", None)
 72 |         if not id:
 73 |             raise TypeError(
 74 |                 "You must provide the playlist id when creating this class to use this method."
 75 |             )
 76 | 
 77 |         url_params = {
 78 |             "mixId": id,
 79 |             "msToken": kwargs.get("ms_token"),
 80 |         }
 81 | 
 82 |         resp = await self.parent.make_request(
 83 |             url="https://www.tiktok.com/api/mix/detail/",
 84 |             params=url_params,
 85 |             headers=kwargs.get("headers"),
 86 |             session_index=kwargs.get("session_index"),
 87 |         )
 88 | 
 89 |         if resp is None:
 90 |             raise InvalidResponseException(resp, "TikTok returned an invalid response.")
 91 | 
 92 |         self.as_dict = resp["mixInfo"]
 93 |         self.__extract_from_data()
 94 |         return resp
 95 | 
 96 |     async def videos(self, count=30, cursor=0, **kwargs) -> AsyncIterator[Video]:
 97 |         """
 98 |         Returns an iterator of videos in this User's playlist.
 99 | 
100 |         Returns:
101 |             Iterator[dict]: An iterator of videos in this User's playlist.
102 | 
103 |         Raises:
104 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
105 | 
106 |         Example Usage:
107 |             .. code-block:: python
108 | 
109 |                 playlist_videos = await api.playlist(id='7426714779919797038').videos()
110 |         """
111 |         id = getattr(self, "id", None)
112 |         if id is None or id == "":
113 |             await self.info(**kwargs)
114 | 
115 |         found = 0
116 |         while found < count:
117 |             params = {
118 |               "mixId": id,
119 |               "count": min(count, 30),
120 |               "cursor": cursor,
121 |           }
122 | 
123 |             resp = await self.parent.make_request(
124 |                 url="https://www.tiktok.com/api/mix/item_list/",
125 |                 params=params,
126 |                 headers=kwargs.get("headers"),
127 |                 session_index=kwargs.get("session_index"),
128 |             )
129 | 
130 |             if resp is None:
131 |                 raise InvalidResponseException(
132 |                     resp, "TikTok returned an invalid response."
133 |                 )
134 | 
135 |             for video in resp.get("itemList", []):
136 |                 yield self.parent.video(data=video)
137 |                 found += 1
138 | 
139 |             if not resp.get("hasMore", False):
140 |                 return
141 | 
142 |             cursor = resp.get("cursor")
143 | 
144 |     def __extract_from_data(self):
145 |         data = self.as_dict
146 |         keys = data.keys()
147 | 
148 |         if "mixInfo" in keys:
149 |             data = data["mixInfo"]
150 | 
151 |         self.id = data.get("id", None) or data.get("mixId", None)
152 |         self.name = data.get("name", None) or data.get("mixName", None)
153 |         self.video_count = data.get("videoCount", None)
154 |         self.creator = self.parent.user(data=data.get("creator", {}))
155 |         self.cover_url = data.get("cover", None)
156 | 
157 |         if None in [self.id, self.name, self.video_count, self.creator, self.cover_url]:
158 |             User.parent.logger.error(
159 |                 f"Failed to create Playlist with data: {data}\nwhich has keys {data.keys()}"
160 |             )
161 | 
162 |     def __repr__(self):
163 |         return self.__str__()
164 | 
165 |     def __str__(self):
166 |         id = getattr(self, "id", None)
167 |         return f"TikTokApi.playlist(id='{id}'')"
168 | 


--------------------------------------------------------------------------------
/TikTokApi/api/search.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | from urllib.parse import urlencode
  3 | from typing import TYPE_CHECKING, AsyncIterator
  4 | from .user import User
  5 | from ..exceptions import InvalidResponseException
  6 | 
  7 | if TYPE_CHECKING:
  8 |     from ..tiktok import TikTokApi
  9 | 
 10 | 
 11 | class Search:
 12 |     """Contains static methods about searching TikTok for a phrase."""
 13 | 
 14 |     parent: TikTokApi
 15 | 
 16 |     @staticmethod
 17 |     async def users(search_term, count=10, cursor=0, **kwargs) -> AsyncIterator[User]:
 18 |         """
 19 |         Searches for users.
 20 | 
 21 |         Note: Your ms_token needs to have done a search before for this to work.
 22 | 
 23 |         Args:
 24 |             search_term (str): The phrase you want to search for.
 25 |             count (int): The amount of users you want returned.
 26 | 
 27 |         Returns:
 28 |             async iterator/generator: Yields TikTokApi.user objects.
 29 | 
 30 |         Raises:
 31 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
 32 | 
 33 |         Example Usage:
 34 |             .. code-block:: python
 35 | 
 36 |                 async for user in api.search.users('david teather'):
 37 |                     # do something
 38 |         """
 39 |         async for user in Search.search_type(
 40 |             search_term, "user", count=count, cursor=cursor, **kwargs
 41 |         ):
 42 |             yield user
 43 | 
 44 |     @staticmethod
 45 |     async def search_type(
 46 |         search_term, obj_type, count=10, cursor=0, **kwargs
 47 |     ) -> AsyncIterator:
 48 |         """
 49 |         Searches for a specific type of object. But you shouldn't use this directly, use the other methods.
 50 | 
 51 |         Note: Your ms_token needs to have done a search before for this to work.
 52 |         Note: Currently only supports searching for users, other endpoints require auth.
 53 | 
 54 |         Args:
 55 |             search_term (str): The phrase you want to search for.
 56 |             obj_type (str): The type of object you want to search for (user)
 57 |             count (int): The amount of users you want returned.
 58 |             cursor (int): The the offset of users from 0 you want to get.
 59 | 
 60 |         Returns:
 61 |             async iterator/generator: Yields TikTokApi.video objects.
 62 | 
 63 |         Raises:
 64 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
 65 | 
 66 |         Example Usage:
 67 |             .. code-block:: python
 68 | 
 69 |                 async for user in api.search.search_type('david teather', 'user'):
 70 |                     # do something
 71 |         """
 72 |         found = 0
 73 |         while found < count:
 74 |             params = {
 75 |                 "keyword": search_term,
 76 |                 "cursor": cursor,
 77 |                 "from_page": "search",
 78 |                 "web_search_code": """{"tiktok":{"client_params_x":{"search_engine":{"ies_mt_user_live_video_card_use_libra":1,"mt_search_general_user_live_card":1}},"search_server":{}}}""",
 79 |             }
 80 | 
 81 |             resp = await Search.parent.make_request(
 82 |                 url=f"https://www.tiktok.com/api/search/{obj_type}/full/",
 83 |                 params=params,
 84 |                 headers=kwargs.get("headers"),
 85 |                 session_index=kwargs.get("session_index"),
 86 |             )
 87 | 
 88 |             if resp is None:
 89 |                 raise InvalidResponseException(
 90 |                     resp, "TikTok returned an invalid response."
 91 |                 )
 92 | 
 93 |             if obj_type == "user":
 94 |                 for user in resp.get("user_list", []):
 95 |                     sec_uid = user.get("user_info").get("sec_uid")
 96 |                     uid = user.get("user_info").get("user_id")
 97 |                     username = user.get("user_info").get("unique_id")
 98 |                     yield Search.parent.user(
 99 |                         sec_uid=sec_uid, user_id=uid, username=username
100 |                     )
101 |                     found += 1
102 | 
103 |             if not resp.get("has_more", False):
104 |                 return
105 | 
106 |             cursor = resp.get("cursor")
107 | 


--------------------------------------------------------------------------------
/TikTokApi/api/sound.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | from ..exceptions import *
  3 | from typing import TYPE_CHECKING, ClassVar, Iterator, Optional
  4 | 
  5 | if TYPE_CHECKING:
  6 |     from ..tiktok import TikTokApi
  7 |     from .user import User
  8 |     from .video import Video
  9 | 
 10 | 
 11 | class Sound:
 12 |     """
 13 |     A TikTok Sound/Music/Song.
 14 | 
 15 |     Example Usage
 16 |         .. code-block:: python
 17 | 
 18 |             song = api.song(id='7016547803243022337')
 19 |     """
 20 | 
 21 |     parent: ClassVar[TikTokApi]
 22 | 
 23 |     id: str
 24 |     """TikTok's ID for the sound"""
 25 |     title: Optional[str]
 26 |     """The title of the song."""
 27 |     author: Optional[User]
 28 |     """The author of the song (if it exists)"""
 29 |     duration: Optional[int]
 30 |     """The duration of the song in seconds."""
 31 |     original: Optional[bool]
 32 |     """Whether the song is original or not."""
 33 | 
 34 |     def __init__(self, id: Optional[str] = None, data: Optional[str] = None):
 35 |         """
 36 |         You must provide the id of the sound or it will not work.
 37 |         """
 38 |         if data is not None:
 39 |             self.as_dict = data
 40 |             self.__extract_from_data()
 41 |         elif id is None:
 42 |             raise TypeError("You must provide id parameter.")
 43 |         else:
 44 |             self.id = id
 45 | 
 46 |     async def info(self, **kwargs) -> dict:
 47 |         """
 48 |         Returns all information sent by TikTok related to this sound.
 49 | 
 50 |         Returns:
 51 |             dict: The raw data returned by TikTok.
 52 | 
 53 |         Raises:
 54 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
 55 | 
 56 |         Example Usage:
 57 |             .. code-block:: python
 58 | 
 59 |                 sound_info = await api.sound(id='7016547803243022337').info()
 60 |         """
 61 | 
 62 |         id = getattr(self, "id", None)
 63 |         if not id:
 64 |             raise TypeError(
 65 |                 "You must provide the id when creating this class to use this method."
 66 |             )
 67 | 
 68 |         url_params = {
 69 |             "msToken": kwargs.get("ms_token"),
 70 |             "musicId": id,
 71 |         }
 72 | 
 73 |         resp = await self.parent.make_request(
 74 |             url="https://www.tiktok.com/api/music/detail/",
 75 |             params=url_params,
 76 |             headers=kwargs.get("headers"),
 77 |             session_index=kwargs.get("session_index"),
 78 |         )
 79 | 
 80 |         if resp is None:
 81 |             raise InvalidResponseException(resp, "TikTok returned an invalid response.")
 82 | 
 83 |         self.as_dict = resp
 84 |         self.__extract_from_data()
 85 |         return resp
 86 | 
 87 |     async def videos(self, count=30, cursor=0, **kwargs) -> AsyncIterator[Video]:
 88 |         """
 89 |         Returns Video objects of videos created with this sound.
 90 | 
 91 |         Args:
 92 |             count (int): The amount of videos you want returned.
 93 |             cursor (int): The the offset of videos from 0 you want to get.
 94 | 
 95 |         Returns:
 96 |             async iterator/generator: Yields TikTokApi.video objects.
 97 | 
 98 |         Raises:
 99 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
100 | 
101 |         Example Usage:
102 |             .. code-block:: python
103 | 
104 |                 async for video in api.sound(id='7016547803243022337').videos():
105 |                     # do something
106 |         """
107 |         id = getattr(self, "id", None)
108 |         if id is None:
109 |             raise TypeError(
110 |                 "You must provide the id when creating this class to use this method."
111 |             )
112 | 
113 |         found = 0
114 |         while found < count:
115 |             params = {
116 |                 "musicID": id,
117 |                 "count": 30,
118 |                 "cursor": cursor,
119 |             }
120 | 
121 |             resp = await self.parent.make_request(
122 |                 url="https://www.tiktok.com/api/music/item_list/",
123 |                 params=params,
124 |                 headers=kwargs.get("headers"),
125 |                 session_index=kwargs.get("session_index"),
126 |             )
127 | 
128 |             if resp is None:
129 |                 raise InvalidResponseException(
130 |                     resp, "TikTok returned an invalid response."
131 |                 )
132 | 
133 |             for video in resp.get("itemList", []):
134 |                 yield self.parent.video(data=video)
135 |                 found += 1
136 | 
137 |             if not resp.get("hasMore", False):
138 |                 return
139 | 
140 |             cursor = resp.get("cursor")
141 | 
142 |     def __extract_from_data(self):
143 |         data = self.as_dict
144 |         keys = data.keys()
145 | 
146 |         if "musicInfo" in keys:
147 |             author = data.get("musicInfo").get("author")
148 |             if isinstance(author, dict):
149 |                 self.author = self.parent.user(data=author)
150 |             elif isinstance(author, str):
151 |                 self.author = self.parent.user(username=author)
152 | 
153 |             if data.get("musicInfo").get("music"):
154 |                 self.title = data.get("musicInfo").get("music").get("title")
155 |                 self.id = data.get("musicInfo").get("music").get("id")
156 |                 self.original = data.get("musicInfo").get("music").get("original")
157 |                 self.play_url = data.get("musicInfo").get("music").get("playUrl")
158 |                 self.cover_large = data.get("musicInfo").get("music").get("coverLarge")
159 |                 self.duration = data.get("musicInfo").get("music").get("duration")
160 | 
161 |         if "music" in keys:
162 |             self.title = data.get("music").get("title")
163 |             self.id = data.get("music").get("id")
164 |             self.original = data.get("music").get("original")
165 |             self.play_url = data.get("music").get("playUrl")
166 |             self.cover_large = data.get("music").get("coverLarge")
167 |             self.duration = data.get("music").get("duration")
168 | 
169 |         if "stats" in keys:
170 |             self.stats = data.get("stats")
171 | 
172 |         if getattr(self, "id", None) is None:
173 |             Sound.parent.logger.error(f"Failed to create Sound with data: {data}\n")
174 | 
175 |     def __repr__(self):
176 |         return self.__str__()
177 | 
178 |     def __str__(self):
179 |         return f"TikTokApi.sound(id='{getattr(self, 'id', None)}')"
180 | 


--------------------------------------------------------------------------------
/TikTokApi/api/trending.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | from ..exceptions import InvalidResponseException
 3 | from .video import Video
 4 | 
 5 | from typing import TYPE_CHECKING, AsyncIterator
 6 | 
 7 | if TYPE_CHECKING:
 8 |     from ..tiktok import TikTokApi
 9 | 
10 | 
11 | class Trending:
12 |     """Contains static methods related to trending objects on TikTok."""
13 | 
14 |     parent: TikTokApi
15 | 
16 |     @staticmethod
17 |     async def videos(count=30, **kwargs) -> AsyncIterator[Video]:
18 |         """
19 |         Returns Videos that are trending on TikTok.
20 | 
21 |         Args:
22 |             count (int): The amount of videos you want returned.
23 | 
24 |         Returns:
25 |             async iterator/generator: Yields TikTokApi.video objects.
26 | 
27 |         Raises:
28 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
29 | 
30 |         Example Usage:
31 |             .. code-block:: python
32 | 
33 |                 async for video in api.trending.videos():
34 |                     # do something
35 |         """
36 |         found = 0
37 |         while found < count:
38 |             params = {
39 |                 "from_page": "fyp",
40 |                 "count": count,
41 |             }
42 | 
43 |             resp = await Trending.parent.make_request(
44 |                 url="https://www.tiktok.com/api/recommend/item_list/",
45 |                 params=params,
46 |                 headers=kwargs.get("headers"),
47 |                 session_index=kwargs.get("session_index"),
48 |             )
49 | 
50 |             if resp is None:
51 |                 raise InvalidResponseException(
52 |                     resp, "TikTok returned an invalid response."
53 |                 )
54 | 
55 |             for video in resp.get("itemList", []):
56 |                 yield Trending.parent.video(data=video)
57 |                 found += 1
58 | 
59 |             if not resp.get("hasMore", False):
60 |                 return
61 | 


--------------------------------------------------------------------------------
/TikTokApi/api/user.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | from typing import TYPE_CHECKING, ClassVar, AsyncIterator, Optional
  3 | from ..exceptions import InvalidResponseException
  4 | 
  5 | if TYPE_CHECKING:
  6 |     from ..tiktok import TikTokApi
  7 |     from .video import Video
  8 |     from .playlist import Playlist
  9 | 
 10 | 
 11 | class User:
 12 |     """
 13 |     A TikTok User.
 14 | 
 15 |     Example Usage:
 16 |         .. code-block:: python
 17 | 
 18 |             user = api.user(username='therock')
 19 |     """
 20 | 
 21 |     parent: ClassVar[TikTokApi]
 22 | 
 23 |     user_id: str
 24 |     """The  ID of the user."""
 25 |     sec_uid: str
 26 |     """The sec UID of the user."""
 27 |     username: str
 28 |     """The username of the user."""
 29 |     as_dict: dict
 30 |     """The raw data associated with this user."""
 31 | 
 32 |     def __init__(
 33 |         self,
 34 |         username: Optional[str] = None,
 35 |         user_id: Optional[str] = None,
 36 |         sec_uid: Optional[str] = None,
 37 |         data: Optional[dict] = None,
 38 |     ):
 39 |         """
 40 |         You must provide the username or (user_id and sec_uid) otherwise this
 41 |         will not function correctly.
 42 |         """
 43 |         self.__update_id_sec_uid_username(user_id, sec_uid, username)
 44 |         if data is not None:
 45 |             self.as_dict = data
 46 |             self.__extract_from_data()
 47 | 
 48 |     async def info(self, **kwargs) -> dict:
 49 |         """
 50 |         Returns a dictionary of information associated with this User.
 51 | 
 52 |         Returns:
 53 |             dict: A dictionary of information associated with this User.
 54 | 
 55 |         Raises:
 56 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
 57 | 
 58 |         Example Usage:
 59 |             .. code-block:: python
 60 | 
 61 |                 user_data = await api.user(username='therock').info()
 62 |         """
 63 | 
 64 |         username = getattr(self, "username", None)
 65 |         if not username:
 66 |             raise TypeError(
 67 |                 "You must provide the username when creating this class to use this method."
 68 |             )
 69 | 
 70 |         sec_uid = getattr(self, "sec_uid", None)
 71 |         url_params = {
 72 |             "secUid": sec_uid if sec_uid is not None else "",
 73 |             "uniqueId": username,
 74 |             "msToken": kwargs.get("ms_token"),
 75 |         }
 76 | 
 77 |         resp = await self.parent.make_request(
 78 |             url="https://www.tiktok.com/api/user/detail/",
 79 |             params=url_params,
 80 |             headers=kwargs.get("headers"),
 81 |             session_index=kwargs.get("session_index"),
 82 |         )
 83 | 
 84 |         if resp is None:
 85 |             raise InvalidResponseException(resp, "TikTok returned an invalid response.")
 86 | 
 87 |         self.as_dict = resp
 88 |         self.__extract_from_data()
 89 |         return resp
 90 | 
 91 |     async def playlists(self, count=20, cursor=0, **kwargs) -> AsyncIterator[Playlist]:
 92 |         """
 93 |         Returns a user's playlists.
 94 | 
 95 |         Returns:
 96 |             async iterator/generator: Yields TikTokApi.playlist objects.
 97 | 
 98 |         Raises:
 99 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
100 | 
101 |         Example Usage:
102 |             .. code-block:: python
103 | 
104 |                 async for playlist in await api.user(username='therock').playlists():
105 |                     # do something
106 |         """
107 | 
108 |         sec_uid = getattr(self, "sec_uid", None)
109 |         if sec_uid is None or sec_uid == "":
110 |             await self.info(**kwargs)
111 |         found = 0
112 | 
113 |         while found < count:
114 |             params = {
115 |                 "secUid": self.sec_uid,
116 |                 "count": min(count, 20),
117 |                 "cursor": cursor,
118 |             }
119 | 
120 |             resp = await self.parent.make_request(
121 |                 url="https://www.tiktok.com/api/user/playlist",
122 |                 params=params,
123 |                 headers=kwargs.get("headers"),
124 |                 session_index=kwargs.get("session_index"),
125 |             )
126 | 
127 |             if resp is None:
128 |                 raise InvalidResponseException(resp, "TikTok returned an invalid response.")
129 | 
130 |             for playlist in resp.get("playList", []):
131 |                 yield self.parent.playlist(data=playlist)
132 |                 found += 1
133 | 
134 |             if not resp.get("hasMore", False):
135 |                 return
136 | 
137 |             cursor = resp.get("cursor")
138 | 
139 | 
140 |     async def videos(self, count=30, cursor=0, **kwargs) -> AsyncIterator[Video]:
141 |         """
142 |         Returns a user's videos.
143 | 
144 |         Args:
145 |             count (int): The amount of videos you want returned.
146 |             cursor (int): The the offset of videos from 0 you want to get.
147 | 
148 |         Returns:
149 |             async iterator/generator: Yields TikTokApi.video objects.
150 | 
151 |         Raises:
152 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
153 | 
154 |         Example Usage:
155 |             .. code-block:: python
156 | 
157 |                 async for video in api.user(username="davidteathercodes").videos():
158 |                     # do something
159 |         """
160 |         sec_uid = getattr(self, "sec_uid", None)
161 |         if sec_uid is None or sec_uid == "":
162 |             await self.info(**kwargs)
163 | 
164 |         found = 0
165 |         while found < count:
166 |             params = {
167 |                 "secUid": self.sec_uid,
168 |                 "count": 35,
169 |                 "cursor": cursor,
170 |             }
171 | 
172 |             resp = await self.parent.make_request(
173 |                 url="https://www.tiktok.com/api/post/item_list/",
174 |                 params=params,
175 |                 headers=kwargs.get("headers"),
176 |                 session_index=kwargs.get("session_index"),
177 |             )
178 | 
179 |             if resp is None:
180 |                 raise InvalidResponseException(
181 |                     resp, "TikTok returned an invalid response."
182 |                 )
183 | 
184 |             for video in resp.get("itemList", []):
185 |                 yield self.parent.video(data=video)
186 |                 found += 1
187 | 
188 |             if not resp.get("hasMore", False):
189 |                 return
190 | 
191 |             cursor = resp.get("cursor")
192 | 
193 |     async def liked(
194 |         self, count: int = 30, cursor: int = 0, **kwargs
195 |     ) -> AsyncIterator[Video]:
196 |         """
197 |         Returns a user's liked posts if public.
198 | 
199 |         Args:
200 |             count (int): The amount of recent likes you want returned.
201 |             cursor (int): The the offset of likes from 0 you want to get.
202 | 
203 |         Returns:
204 |             async iterator/generator: Yields TikTokApi.video objects.
205 | 
206 |         Raises:
207 |             InvalidResponseException: If TikTok returns an invalid response, the user's likes are private, or one we don't understand.
208 | 
209 |         Example Usage:
210 |             .. code-block:: python
211 | 
212 |                 async for like in api.user(username="davidteathercodes").liked():
213 |                     # do something
214 |         """
215 |         sec_uid = getattr(self, "sec_uid", None)
216 |         if sec_uid is None or sec_uid == "":
217 |             await self.info(**kwargs)
218 | 
219 |         found = 0
220 |         while found < count:
221 |             params = {
222 |                 "secUid": self.sec_uid,
223 |                 "count": 35,
224 |                 "cursor": cursor,
225 |             }
226 | 
227 |             resp = await self.parent.make_request(
228 |                 url="https://www.tiktok.com/api/favorite/item_list",
229 |                 params=params,
230 |                 headers=kwargs.get("headers"),
231 |                 session_index=kwargs.get("session_index"),
232 |             )
233 | 
234 |             if resp is None:
235 |                 raise InvalidResponseException(
236 |                     resp, "TikTok returned an invalid response."
237 |                 )
238 | 
239 |             for video in resp.get("itemList", []):
240 |                 yield self.parent.video(data=video)
241 |                 found += 1
242 | 
243 |             if not resp.get("hasMore", False):
244 |                 return
245 | 
246 |             cursor = resp.get("cursor")
247 | 
248 |     def __extract_from_data(self):
249 |         data = self.as_dict
250 |         keys = data.keys()
251 | 
252 |         if "userInfo" in keys:
253 |             self.__update_id_sec_uid_username(
254 |                 data["userInfo"]["user"]["id"],
255 |                 data["userInfo"]["user"]["secUid"],
256 |                 data["userInfo"]["user"]["uniqueId"],
257 |             )
258 |         else:
259 |             self.__update_id_sec_uid_username(
260 |                 data["id"],
261 |                 data["secUid"],
262 |                 data["uniqueId"],
263 |             )
264 | 
265 |         if None in (self.username, self.user_id, self.sec_uid):
266 |             User.parent.logger.error(
267 |                 f"Failed to create User with data: {data}\nwhich has keys {data.keys()}"
268 |             )
269 | 
270 |     def __update_id_sec_uid_username(self, id, sec_uid, username):
271 |         self.user_id = id
272 |         self.sec_uid = sec_uid
273 |         self.username = username
274 | 
275 |     def __repr__(self):
276 |         return self.__str__()
277 | 
278 |     def __str__(self):
279 |         username = getattr(self, "username", None)
280 |         user_id = getattr(self, "user_id", None)
281 |         sec_uid = getattr(self, "sec_uid", None)
282 |         return f"TikTokApi.user(username='{username}', user_id='{user_id}', sec_uid='{sec_uid}')"
283 | 


--------------------------------------------------------------------------------
/TikTokApi/api/video.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | from ..helpers import extract_video_id_from_url, requests_cookie_to_playwright_cookie
  3 | from typing import TYPE_CHECKING, ClassVar, AsyncIterator, Optional
  4 | from datetime import datetime
  5 | import requests
  6 | from ..exceptions import InvalidResponseException
  7 | import json
  8 | import httpx
  9 | from typing import Union, AsyncIterator
 10 | 
 11 | if TYPE_CHECKING:
 12 |     from ..tiktok import TikTokApi
 13 |     from .user import User
 14 |     from .sound import Sound
 15 |     from .hashtag import Hashtag
 16 |     from .comment import Comment
 17 | 
 18 | 
 19 | class Video:
 20 |     """
 21 |     A TikTok Video class
 22 | 
 23 |     Example Usage
 24 |     ```py
 25 |     video = api.video(id='7041997751718137094')
 26 |     ```
 27 |     """
 28 | 
 29 |     parent: ClassVar[TikTokApi]
 30 | 
 31 |     id: Optional[str]
 32 |     """TikTok's ID of the Video"""
 33 |     url: Optional[str]
 34 |     """The URL of the Video"""
 35 |     create_time: Optional[datetime]
 36 |     """The creation time of the Video"""
 37 |     stats: Optional[dict]
 38 |     """TikTok's stats of the Video"""
 39 |     author: Optional[User]
 40 |     """The User who created the Video"""
 41 |     sound: Optional[Sound]
 42 |     """The Sound that is associated with the Video"""
 43 |     hashtags: Optional[list[Hashtag]]
 44 |     """A List of Hashtags on the Video"""
 45 |     as_dict: dict
 46 |     """The raw data associated with this Video."""
 47 | 
 48 |     def __init__(
 49 |         self,
 50 |         id: Optional[str] = None,
 51 |         url: Optional[str] = None,
 52 |         data: Optional[dict] = None,
 53 |         **kwargs,
 54 |     ):
 55 |         """
 56 |         You must provide the id or a valid url, else this will fail.
 57 |         """
 58 |         self.id = id
 59 |         self.url = url
 60 |         if data is not None:
 61 |             self.as_dict = data
 62 |             self.__extract_from_data()
 63 |         elif url is not None:
 64 |             i, session = self.parent._get_session(**kwargs)
 65 |             self.id = extract_video_id_from_url(
 66 |                 url,
 67 |                 headers=session.headers,
 68 |                 proxy=kwargs.get("proxy")
 69 |                 if kwargs.get("proxy") is not None
 70 |                 else session.proxy,
 71 |             )
 72 | 
 73 |         if getattr(self, "id", None) is None:
 74 |             raise TypeError("You must provide id or url parameter.")
 75 | 
 76 |     async def info(self, **kwargs) -> dict:
 77 |         """
 78 |         Returns a dictionary of all data associated with a TikTok Video.
 79 | 
 80 |         Note: This is slow since it requires an HTTP request, avoid using this if possible.
 81 | 
 82 |         Returns:
 83 |             dict: A dictionary of all data associated with a TikTok Video.
 84 | 
 85 |         Raises:
 86 |             InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
 87 | 
 88 |         Example Usage:
 89 |             .. code-block:: python
 90 | 
 91 |                 url = "https://www.tiktok.com/@davidteathercodes/video/7106686413101468970"
 92 |                 video_info = await api.video(url=url).info()
 93 |         """
 94 |         i, session = self.parent._get_session(**kwargs)
 95 |         proxy = (
 96 |             kwargs.get("proxy") if kwargs.get("proxy") is not None else session.proxy
 97 |         )
 98 |         if self.url is None:
 99 |             raise TypeError("To call video.info() you need to set the video's url.")
100 | 
101 |         r = requests.get(self.url, headers=session.headers, proxies=proxy)
102 |         if r.status_code != 200:
103 |             raise InvalidResponseException(
104 |                 r.text, "TikTok returned an invalid response.", error_code=r.status_code
105 |             )
106 | 
107 |         # Try SIGI_STATE first
108 |         # extract tag <script id="SIGI_STATE" type="application/json">{..}</script>
109 |         # extract json in the middle
110 | 
111 |         start = r.text.find('<script id="SIGI_STATE" type="application/json">')
112 |         if start != -1:
113 |             start += len('<script id="SIGI_STATE" type="application/json">')
114 |             end = r.text.find("</script>", start)
115 | 
116 |             if end == -1:
117 |                 raise InvalidResponseException(
118 |                     r.text, "TikTok returned an invalid response.", error_code=r.status_code
119 |                 )
120 | 
121 |             data = json.loads(r.text[start:end])
122 |             video_info = data["ItemModule"][self.id]
123 |         else:
124 |             # Try __UNIVERSAL_DATA_FOR_REHYDRATION__ next
125 | 
126 |             # extract tag <script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">{..}</script>
127 |             # extract json in the middle
128 | 
129 |             start = r.text.find('<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">')
130 |             if start == -1:
131 |                 raise InvalidResponseException(
132 |                     r.text, "TikTok returned an invalid response.", error_code=r.status_code
133 |                 )
134 | 
135 |             start += len('<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">')
136 |             end = r.text.find("</script>", start)
137 | 
138 |             if end == -1:
139 |                 raise InvalidResponseException(
140 |                     r.text, "TikTok returned an invalid response.", error_code=r.status_code
141 |                 )
142 | 
143 |             data = json.loads(r.text[start:end])
144 |             default_scope = data.get("__DEFAULT_SCOPE__", {})
145 |             video_detail = default_scope.get("webapp.video-detail", {})
146 |             if video_detail.get("statusCode", 0) != 0: # assume 0 if not present
147 |                 raise InvalidResponseException(
148 |                     r.text, "TikTok returned an invalid response structure.", error_code=r.status_code
149 |                 )
150 |             video_info = video_detail.get("itemInfo", {}).get("itemStruct")
151 |             if video_info is None:
152 |                 raise InvalidResponseException(
153 |                     r.text, "TikTok returned an invalid response structure.", error_code=r.status_code
154 |                 )
155 | 
156 |         self.as_dict = video_info
157 |         self.__extract_from_data()
158 | 
159 |         cookies = [requests_cookie_to_playwright_cookie(c) for c in r.cookies]
160 | 
161 |         await self.parent.set_session_cookies(
162 |             session,
163 |             cookies
164 |         )
165 |         return video_info
166 | 
167 |     async def bytes(self, stream: bool = False, **kwargs) -> Union[bytes, AsyncIterator[bytes]]:
168 |         """
169 |         Returns the bytes of a TikTok Video.
170 | 
171 |         TODO:
172 |             Not implemented yet.
173 | 
174 |         Example Usage:
175 |             .. code-block:: python
176 | 
177 |                 video_bytes = await api.video(id='7041997751718137094').bytes()
178 | 
179 |                 # Saving The Video
180 |                 with open('saved_video.mp4', 'wb') as output:
181 |                     output.write(video_bytes)
182 | 
183 |                 # Streaming (if stream=True)
184 |                 async for chunk in api.video(id='7041997751718137094').bytes(stream=True):
185 |                     # Process or upload chunk
186 |         """
187 |         i, session = self.parent._get_session(**kwargs)
188 |         downloadAddr = self.as_dict["video"]["downloadAddr"]
189 | 
190 |         cookies = await self.parent.get_session_cookies(session)
191 | 
192 |         h = session.headers
193 |         h["range"] = 'bytes=0-'
194 |         h["accept-encoding"] = 'identity;q=1, *;q=0'
195 |         h["referer"] = 'https://www.tiktok.com/'
196 | 
197 |         if stream:
198 |             async def stream_bytes():
199 |                 async with httpx.AsyncClient() as client:
200 |                     async with client.stream('GET', downloadAddr, headers=h, cookies=cookies) as response:
201 |                         async for chunk in response.aiter_bytes():
202 |                             yield chunk
203 |             return stream_bytes()
204 |         else:
205 |             resp = requests.get(downloadAddr, headers=h, cookies=cookies)
206 |             return resp.content
207 | 
208 |     def __extract_from_data(self) -> None:
209 |         data = self.as_dict
210 |         self.id = data["id"]
211 | 
212 |         timestamp = data.get("createTime", None)
213 |         if timestamp is not None:
214 |             try:
215 |                 timestamp = int(timestamp)
216 |             except ValueError:
217 |                 pass
218 | 
219 |         self.create_time = datetime.fromtimestamp(timestamp)
220 |         self.stats = data.get('statsV2') or data.get('stats')
221 | 
222 |         author = data.get("author")
223 |         if isinstance(author, str):
224 |             self.author = self.parent.user(username=author)
225 |         else:
226 |             self.author = self.parent.user(data=author)
227 |         self.sound = self.parent.sound(data=data)
228 | 
229 |         self.hashtags = [
230 |             self.parent.hashtag(data=hashtag) for hashtag in data.get("challenges", [])
231 |         ]
232 | 
233 |         if getattr(self, "id", None) is None:
234 |             Video.parent.logger.error(
235 |                 f"Failed to create Video with data: {data}\nwhich has keys {data.keys()}"
236 |             )
237 | 
238 |     async def comments(self, count=20, cursor=0, **kwargs) -> AsyncIterator[Comment]:
239 |         """
240 |         Returns the comments of a TikTok Video.
241 | 
242 |         Parameters:
243 |             count (int): The amount of comments you want returned.
244 |             cursor (int): The the offset of comments from 0 you want to get.
245 | 
246 |         Returns:
247 |             async iterator/generator: Yields TikTokApi.comment objects.
248 | 
249 |         Example Usage
250 |         .. code-block:: python
251 | 
252 |             async for comment in api.video(id='7041997751718137094').comments():
253 |                 # do something
254 |         ```
255 |         """
256 |         found = 0
257 |         while found < count:
258 |             params = {
259 |                 "aweme_id": self.id,
260 |                 "count": 20,
261 |                 "cursor": cursor,
262 |             }
263 | 
264 |             resp = await self.parent.make_request(
265 |                 url="https://www.tiktok.com/api/comment/list/",
266 |                 params=params,
267 |                 headers=kwargs.get("headers"),
268 |                 session_index=kwargs.get("session_index"),
269 |             )
270 | 
271 |             if resp is None:
272 |                 raise InvalidResponseException(
273 |                     resp, "TikTok returned an invalid response."
274 |                 )
275 | 
276 |             for video in resp.get("comments", []):
277 |                 yield self.parent.comment(data=video)
278 |                 found += 1
279 | 
280 |             if not resp.get("has_more", False):
281 |                 return
282 | 
283 |             cursor = resp.get("cursor")
284 | 
285 |     async def related_videos(
286 |         self, count: int = 30, cursor: int = 0, **kwargs
287 |     ) -> AsyncIterator[Video]:
288 |         """
289 |         Returns related videos of a TikTok Video.
290 | 
291 |         Parameters:
292 |             count (int): The amount of comments you want returned.
293 |             cursor (int): The the offset of comments from 0 you want to get.
294 | 
295 |         Returns:
296 |             async iterator/generator: Yields TikTokApi.video objects.
297 | 
298 |         Example Usage
299 |         .. code-block:: python
300 | 
301 |             async for related_videos in api.video(id='7041997751718137094').related_videos():
302 |                 # do something
303 |         ```
304 |         """
305 |         found = 0
306 |         while found < count:
307 |             params = {
308 |                 "itemID": self.id,
309 |                 "count": 16,
310 |             }
311 | 
312 |             resp = await self.parent.make_request(
313 |                 url="https://www.tiktok.com/api/related/item_list/",
314 |                 params=params,
315 |                 headers=kwargs.get("headers"),
316 |                 session_index=kwargs.get("session_index"),
317 |             )
318 | 
319 |             if resp is None:
320 |                 raise InvalidResponseException(
321 |                     resp, "TikTok returned an invalid response."
322 |                 )
323 | 
324 |             for video in resp.get("itemList", []):
325 |                 yield self.parent.video(data=video)
326 |                 found += 1
327 | 
328 |     def __repr__(self):
329 |         return self.__str__()
330 | 
331 |     def __str__(self):
332 |         return f"TikTokApi.video(id='{getattr(self, 'id', None)}')"
333 | 


--------------------------------------------------------------------------------
/TikTokApi/exceptions.py:
--------------------------------------------------------------------------------
 1 | class TikTokException(Exception):
 2 |     """Generic exception that all other TikTok errors are children of."""
 3 | 
 4 |     def __init__(self, raw_response, message, error_code=None):
 5 |         self.error_code = error_code
 6 |         self.raw_response = raw_response
 7 |         self.message = message
 8 |         super().__init__(self.message)
 9 | 
10 |     def __str__(self):
11 |         return f"{self.error_code} -> {self.message}"
12 | 
13 | 
14 | class CaptchaException(TikTokException):
15 |     """TikTok is showing captcha"""
16 | 
17 | 
18 | class NotFoundException(TikTokException):
19 |     """TikTok indicated that this object does not exist."""
20 | 
21 | 
22 | class EmptyResponseException(TikTokException):
23 |     """TikTok sent back an empty response."""
24 | 
25 | 
26 | class SoundRemovedException(TikTokException):
27 |     """This TikTok sound has no id from being removed by TikTok."""
28 | 
29 | 
30 | class InvalidJSONException(TikTokException):
31 |     """TikTok returned invalid JSON."""
32 | 
33 | 
34 | class InvalidResponseException(TikTokException):
35 |     """The response from TikTok was invalid."""
36 | 


--------------------------------------------------------------------------------
/TikTokApi/helpers.py:
--------------------------------------------------------------------------------
 1 | from .exceptions import *
 2 | 
 3 | import requests
 4 | import random
 5 | 
 6 | 
 7 | def extract_video_id_from_url(url, headers={}, proxy=None):
 8 |     url = requests.head(
 9 |         url=url, allow_redirects=True, headers=headers, proxies=proxy
10 |     ).url
11 |     if "@" in url and "/video/" in url:
12 |         return url.split("/video/")[1].split("?")[0]
13 |     else:
14 |         raise TypeError(
15 |             "URL format not supported. Below is an example of a supported url.\n"
16 |             "https://www.tiktok.com/@therock/video/6829267836783971589"
17 |         )
18 | 
19 | 
20 | def random_choice(choices: list):
21 |     """Return a random choice from a list, or None if the list is empty"""
22 |     if choices is None or len(choices) == 0:
23 |         return None
24 |     return random.choice(choices)
25 | 
26 | def requests_cookie_to_playwright_cookie(req_c):
27 |     c = {
28 |         'name': req_c.name,
29 |         'value': req_c.value,
30 |         'domain': req_c.domain,
31 |         'path': req_c.path,
32 |         'secure': req_c.secure
33 |     }
34 |     if req_c.expires:
35 |         c['expires'] = req_c.expires
36 |     return c
37 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/__init__.py:
--------------------------------------------------------------------------------
1 | from .stealth import stealth_async
2 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidteather/TikTok-Api/62a8cfa8ab7bb5bbdd0f8c8b13e84731fff7ac75/TikTokApi/stealth/js/__init__.py


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/chrome_app.py:
--------------------------------------------------------------------------------
 1 | chrome_app = """
 2 | if (!window.chrome) {
 3 |     // Use the exact property descriptor found in headful Chrome
 4 |     // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
 5 |     Object.defineProperty(window, 'chrome', {
 6 |         writable: true,
 7 |         enumerable: true,
 8 |         configurable: false, // note!
 9 |         value: {} // We'll extend that later
10 |     })
11 | }
12 | 
13 | // app in window.chrome means we're running headful and don't need to mock anything
14 | if (!('app' in window.chrome)) {
15 |     const makeError = {
16 |         ErrorInInvocation: fn => {
17 |             const err = new TypeError(`Error in invocation of app.${fn}()`)
18 |             return utils.stripErrorWithAnchor(
19 |                 err,
20 |                 `at ${fn} (eval at <anonymous>`
21 |             )
22 |         }
23 |     }
24 | 
25 | // There's a some static data in that property which doesn't seem to change,
26 | // we should periodically check for updates: `JSON.stringify(window.app, null, 2)`
27 |     const APP_STATIC_DATA = JSON.parse(
28 |         `
29 | {
30 |   "isInstalled": false,
31 |   "InstallState": {
32 |     "DISABLED": "disabled",
33 |     "INSTALLED": "installed",
34 |     "NOT_INSTALLED": "not_installed"
35 |   },
36 |   "RunningState": {
37 |     "CANNOT_RUN": "cannot_run",
38 |     "READY_TO_RUN": "ready_to_run",
39 |     "RUNNING": "running"
40 |   }
41 | }
42 |         `.trim()
43 |     )
44 | 
45 |     window.chrome.app = {
46 |         ...APP_STATIC_DATA,
47 | 
48 |         get isInstalled() {
49 |             return false
50 |         },
51 | 
52 |         getDetails: function getDetails() {
53 |             if (arguments.length) {
54 |                 throw makeError.ErrorInInvocation(`getDetails`)
55 |             }
56 |             return null
57 |         },
58 |         getIsInstalled: function getDetails() {
59 |             if (arguments.length) {
60 |                 throw makeError.ErrorInInvocation(`getIsInstalled`)
61 |             }
62 |             return false
63 |         },
64 |         runningState: function getDetails() {
65 |             if (arguments.length) {
66 |                 throw makeError.ErrorInInvocation(`runningState`)
67 |             }
68 |             return 'cannot_run'
69 |         }
70 |     }
71 |     utils.patchToStringNested(window.chrome.app)
72 | }
73 | """
74 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/chrome_csi.py:
--------------------------------------------------------------------------------
 1 | chrome_csi = """
 2 | if (!window.chrome) {
 3 |     // Use the exact property descriptor found in headful Chrome
 4 |     // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
 5 |     Object.defineProperty(window, 'chrome', {
 6 |         writable: true,
 7 |         enumerable: true,
 8 |         configurable: false, // note!
 9 |         value: {} // We'll extend that later
10 |     })
11 | }
12 | 
13 | // Check if we're running headful and don't need to mock anything
14 | // Check that the Navigation Timing API v1 is available, we need that
15 | if (!('csi' in window.chrome) && (window.performance || window.performance.timing)) {
16 |     const {csi_timing} = window.performance
17 | 
18 |     log.info('loading chrome.csi.js')
19 |     window.chrome.csi = function () {
20 |         return {
21 |             onloadT: csi_timing.domContentLoadedEventEnd,
22 |             startE: csi_timing.navigationStart,
23 |             pageT: Date.now() - csi_timing.navigationStart,
24 |             tran: 15 // Transition type or something
25 |         }
26 |     }
27 |     utils.patchToString(window.chrome.csi)
28 | }
29 | """
30 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/chrome_hairline.py:
--------------------------------------------------------------------------------
 1 | chrome_hairline = """
 2 | // https://intoli.com/blog/making-chrome-headless-undetectable/
 3 | // store the existing descriptor
 4 | const elementDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight');
 5 | 
 6 | // redefine the property with a patched descriptor
 7 | Object.defineProperty(HTMLDivElement.prototype, 'offsetHeight', {
 8 |   ...elementDescriptor,
 9 |   get: function() {
10 |     if (this.id === 'modernizr') {
11 |         return 1;
12 |     }
13 |     return elementDescriptor.get.apply(this);
14 |   },
15 | });
16 | """
17 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/chrome_load_times.py:
--------------------------------------------------------------------------------
  1 | chrome_load_times = """
  2 | if (!window.chrome) {
  3 |     // Use the exact property descriptor found in headful Chrome
  4 |     // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
  5 |     Object.defineProperty(window, 'chrome', {
  6 |         writable: true,
  7 |         enumerable: true,
  8 |         configurable: false, // note!
  9 |         value: {} // We'll extend that later
 10 |     })
 11 | }
 12 | 
 13 | // That means we're running headful and don't need to mock anything
 14 | if ('loadTimes' in window.chrome) {
 15 |     throw new Error('skipping chrome loadtimes update, running in headfull mode')
 16 | }
 17 | 
 18 | // Check that the Navigation Timing API v1 + v2 is available, we need that
 19 | if (
 20 |     window.performance ||
 21 |     window.performance.timing ||
 22 |     window.PerformancePaintTiming
 23 | ) {
 24 | 
 25 |     const {performance} = window
 26 | 
 27 |     // Some stuff is not available on about:blank as it requires a navigation to occur,
 28 |     // let's harden the code to not fail then:
 29 |     const ntEntryFallback = {
 30 |         nextHopProtocol: 'h2',
 31 |         type: 'other'
 32 |     }
 33 | 
 34 |     // The API exposes some funky info regarding the connection
 35 |     const protocolInfo = {
 36 |         get connectionInfo() {
 37 |             const ntEntry =
 38 |                 performance.getEntriesByType('navigation')[0] || ntEntryFallback
 39 |             return ntEntry.nextHopProtocol
 40 |         },
 41 |         get npnNegotiatedProtocol() {
 42 |             // NPN is deprecated in favor of ALPN, but this implementation returns the
 43 |             // HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.
 44 |             const ntEntry =
 45 |                 performance.getEntriesByType('navigation')[0] || ntEntryFallback
 46 |             return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)
 47 |                 ? ntEntry.nextHopProtocol
 48 |                 : 'unknown'
 49 |         },
 50 |         get navigationType() {
 51 |             const ntEntry =
 52 |                 performance.getEntriesByType('navigation')[0] || ntEntryFallback
 53 |             return ntEntry.type
 54 |         },
 55 |         get wasAlternateProtocolAvailable() {
 56 |             // The Alternate-Protocol header is deprecated in favor of Alt-Svc
 57 |             // (https://www.mnot.net/blog/2016/03/09/alt-svc), so technically this
 58 |             // should always return false.
 59 |             return false
 60 |         },
 61 |         get wasFetchedViaSpdy() {
 62 |             // SPDY is deprecated in favor of HTTP/2, but this implementation returns
 63 |             // true for HTTP/2 or HTTP2+QUIC/39 as well.
 64 |             const ntEntry =
 65 |                 performance.getEntriesByType('navigation')[0] || ntEntryFallback
 66 |             return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)
 67 |         },
 68 |         get wasNpnNegotiated() {
 69 |             // NPN is deprecated in favor of ALPN, but this implementation returns true
 70 |             // for HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.
 71 |             const ntEntry =
 72 |                 performance.getEntriesByType('navigation')[0] || ntEntryFallback
 73 |             return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)
 74 |         }
 75 |     }
 76 | 
 77 |     const {timing} = window.performance
 78 | 
 79 | // Truncate number to specific number of decimals, most of the `loadTimes` stuff has 3
 80 |     function toFixed(num, fixed) {
 81 |         var re = new RegExp('^-?\\d+(?:.\\d{0,' + (fixed || -1) + '})?')
 82 |         return num.toString().match(re)[0]
 83 |     }
 84 | 
 85 |     const timingInfo = {
 86 |         get firstPaintAfterLoadTime() {
 87 |             // This was never actually implemented and always returns 0.
 88 |             return 0
 89 |         },
 90 |         get requestTime() {
 91 |             return timing.navigationStart / 1000
 92 |         },
 93 |         get startLoadTime() {
 94 |             return timing.navigationStart / 1000
 95 |         },
 96 |         get commitLoadTime() {
 97 |             return timing.responseStart / 1000
 98 |         },
 99 |         get finishDocumentLoadTime() {
100 |             return timing.domContentLoadedEventEnd / 1000
101 |         },
102 |         get finishLoadTime() {
103 |             return timing.loadEventEnd / 1000
104 |         },
105 |         get firstPaintTime() {
106 |             const fpEntry = performance.getEntriesByType('paint')[0] || {
107 |                 startTime: timing.loadEventEnd / 1000 // Fallback if no navigation occured (`about:blank`)
108 |             }
109 |             return toFixed(
110 |                 (fpEntry.startTime + performance.timeOrigin) / 1000,
111 |                 3
112 |             )
113 |         }
114 |     }
115 | 
116 |     window.chrome.loadTimes = function () {
117 |         return {
118 |             ...protocolInfo,
119 |             ...timingInfo
120 |         }
121 |     }
122 |     utils.patchToString(window.chrome.loadTimes)
123 | }
124 | """
125 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/chrome_runtime.py:
--------------------------------------------------------------------------------
  1 | chrome_runtime = """
  2 | const STATIC_DATA = {
  3 |     "OnInstalledReason": {
  4 |         "CHROME_UPDATE": "chrome_update",
  5 |         "INSTALL": "install",
  6 |         "SHARED_MODULE_UPDATE": "shared_module_update",
  7 |         "UPDATE": "update"
  8 |     },
  9 |     "OnRestartRequiredReason": {
 10 |         "APP_UPDATE": "app_update",
 11 |         "OS_UPDATE": "os_update",
 12 |         "PERIODIC": "periodic"
 13 |     },
 14 |     "PlatformArch": {
 15 |         "ARM": "arm",
 16 |         "ARM64": "arm64",
 17 |         "MIPS": "mips",
 18 |         "MIPS64": "mips64",
 19 |         "X86_32": "x86-32",
 20 |         "X86_64": "x86-64"
 21 |     },
 22 |     "PlatformNaclArch": {
 23 |         "ARM": "arm",
 24 |         "MIPS": "mips",
 25 |         "MIPS64": "mips64",
 26 |         "X86_32": "x86-32",
 27 |         "X86_64": "x86-64"
 28 |     },
 29 |     "PlatformOs": {
 30 |         "ANDROID": "android",
 31 |         "CROS": "cros",
 32 |         "LINUX": "linux",
 33 |         "MAC": "mac",
 34 |         "OPENBSD": "openbsd",
 35 |         "WIN": "win"
 36 |     },
 37 |     "RequestUpdateCheckStatus": {
 38 |         "NO_UPDATE": "no_update",
 39 |         "THROTTLED": "throttled",
 40 |         "UPDATE_AVAILABLE": "update_available"
 41 |     }
 42 | }
 43 | 
 44 | if (!window.chrome) {
 45 |     // Use the exact property descriptor found in headful Chrome
 46 |     // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
 47 |     Object.defineProperty(window, 'chrome', {
 48 |         writable: true,
 49 |         enumerable: true,
 50 |         configurable: false, // note!
 51 |         value: {} // We'll extend that later
 52 |     })
 53 | }
 54 | 
 55 | // That means we're running headfull and don't need to mock anything
 56 | const existsAlready = 'runtime' in window.chrome
 57 | // `chrome.runtime` is only exposed on secure origins
 58 | const isNotSecure = !window.location.protocol.startsWith('https')
 59 | if (!(existsAlready || (isNotSecure && !opts.runOnInsecureOrigins))) {
 60 |     window.chrome.runtime = {
 61 |         // There's a bunch of static data in that property which doesn't seem to change,
 62 |         // we should periodically check for updates: `JSON.stringify(window.chrome.runtime, null, 2)`
 63 |         ...STATIC_DATA,
 64 |         // `chrome.runtime.id` is extension related and returns undefined in Chrome
 65 |         get id() {
 66 |             return undefined
 67 |         },
 68 |         // These two require more sophisticated mocks
 69 |         connect: null,
 70 |         sendMessage: null
 71 |     }
 72 | 
 73 |     const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({
 74 |         NoMatchingSignature: new TypeError(
 75 |             preamble + `No matching signature.`
 76 |         ),
 77 |         MustSpecifyExtensionID: new TypeError(
 78 |             preamble +
 79 |             `${method} called from a webpage must specify an Extension ID (string) for its first argument.`
 80 |         ),
 81 |         InvalidExtensionID: new TypeError(
 82 |             preamble + `Invalid extension id: '${extensionId}'`
 83 |         )
 84 |     })
 85 | 
 86 |     // Valid Extension IDs are 32 characters in length and use the letter `a` to `p`:
 87 |     // https://source.chromium.org/chromium/chromium/src/+/main:components/crx_file/id_util.cc;drc=14a055ccb17e8c8d5d437fe080faba4c6f07beac;l=90
 88 |     const isValidExtensionID = str =>
 89 |         str.length === 32 && str.toLowerCase().match(/^[a-p]+$/)
 90 | 
 91 |     /** Mock `chrome.runtime.sendMessage` */
 92 |     const sendMessageHandler = {
 93 |         apply: function (target, ctx, args) {
 94 |             const [extensionId, options, responseCallback] = args || []
 95 | 
 96 |             // Define custom errors
 97 |             const errorPreamble = `Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): `
 98 |             const Errors = makeCustomRuntimeErrors(
 99 |                 errorPreamble,
100 |                 `chrome.runtime.sendMessage()`,
101 |                 extensionId
102 |             )
103 | 
104 |             // Check if the call signature looks ok
105 |             const noArguments = args.length === 0
106 |             const tooManyArguments = args.length > 4
107 |             const incorrectOptions = options && typeof options !== 'object'
108 |             const incorrectResponseCallback =
109 |                 responseCallback && typeof responseCallback !== 'function'
110 |             if (
111 |                 noArguments ||
112 |                 tooManyArguments ||
113 |                 incorrectOptions ||
114 |                 incorrectResponseCallback
115 |             ) {
116 |                 throw Errors.NoMatchingSignature
117 |             }
118 | 
119 |             // At least 2 arguments are required before we even validate the extension ID
120 |             if (args.length < 2) {
121 |                 throw Errors.MustSpecifyExtensionID
122 |             }
123 | 
124 |             // Now let's make sure we got a string as extension ID
125 |             if (typeof extensionId !== 'string') {
126 |                 throw Errors.NoMatchingSignature
127 |             }
128 | 
129 |             if (!isValidExtensionID(extensionId)) {
130 |                 throw Errors.InvalidExtensionID
131 |             }
132 | 
133 |             return undefined // Normal behavior
134 |         }
135 |     }
136 |     utils.mockWithProxy(
137 |         window.chrome.runtime,
138 |         'sendMessage',
139 |         function sendMessage() {
140 |         },
141 |         sendMessageHandler
142 |     )
143 | 
144 |     /**
145 |      * Mock `chrome.runtime.connect`
146 |      *
147 |      * @see https://developer.chrome.com/apps/runtime#method-connect
148 |      */
149 |     const connectHandler = {
150 |         apply: function (target, ctx, args) {
151 |             const [extensionId, connectInfo] = args || []
152 | 
153 |             // Define custom errors
154 |             const errorPreamble = `Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): `
155 |             const Errors = makeCustomRuntimeErrors(
156 |                 errorPreamble,
157 |                 `chrome.runtime.connect()`,
158 |                 extensionId
159 |             )
160 | 
161 |             // Behavior differs a bit from sendMessage:
162 |             const noArguments = args.length === 0
163 |             const emptyStringArgument = args.length === 1 && extensionId === ''
164 |             if (noArguments || emptyStringArgument) {
165 |                 throw Errors.MustSpecifyExtensionID
166 |             }
167 | 
168 |             const tooManyArguments = args.length > 2
169 |             const incorrectConnectInfoType =
170 |                 connectInfo && typeof connectInfo !== 'object'
171 | 
172 |             if (tooManyArguments || incorrectConnectInfoType) {
173 |                 throw Errors.NoMatchingSignature
174 |             }
175 | 
176 |             const extensionIdIsString = typeof extensionId === 'string'
177 |             if (extensionIdIsString && extensionId === '') {
178 |                 throw Errors.MustSpecifyExtensionID
179 |             }
180 |             if (extensionIdIsString && !isValidExtensionID(extensionId)) {
181 |                 throw Errors.InvalidExtensionID
182 |             }
183 | 
184 |             // There's another edge-case here: extensionId is optional so we might find a connectInfo object as first param, which we need to validate
185 |             const validateConnectInfo = ci => {
186 |                 // More than a first param connectInfo as been provided
187 |                 if (args.length > 1) {
188 |                     throw Errors.NoMatchingSignature
189 |                 }
190 |                 // An empty connectInfo has been provided
191 |                 if (Object.keys(ci).length === 0) {
192 |                     throw Errors.MustSpecifyExtensionID
193 |                 }
194 |                 // Loop over all connectInfo props an check them
195 |                 Object.entries(ci).forEach(([k, v]) => {
196 |                     const isExpected = ['name', 'includeTlsChannelId'].includes(k)
197 |                     if (!isExpected) {
198 |                         throw new TypeError(
199 |                             errorPreamble + `Unexpected property: '${k}'.`
200 |                         )
201 |                     }
202 |                     const MismatchError = (propName, expected, found) =>
203 |                         TypeError(
204 |                             errorPreamble +
205 |                             `Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.`
206 |                         )
207 |                     if (k === 'name' && typeof v !== 'string') {
208 |                         throw MismatchError(k, 'string', typeof v)
209 |                     }
210 |                     if (k === 'includeTlsChannelId' && typeof v !== 'boolean') {
211 |                         throw MismatchError(k, 'boolean', typeof v)
212 |                     }
213 |                 })
214 |             }
215 |             if (typeof extensionId === 'object') {
216 |                 validateConnectInfo(extensionId)
217 |                 throw Errors.MustSpecifyExtensionID
218 |             }
219 | 
220 |             // Unfortunately even when the connect fails Chrome will return an object with methods we need to mock as well
221 |             return utils.patchToStringNested(makeConnectResponse())
222 |         }
223 |     }
224 |     utils.mockWithProxy(
225 |         window.chrome.runtime,
226 |         'connect',
227 |         function connect() {
228 |         },
229 |         connectHandler
230 |     )
231 | 
232 |     function makeConnectResponse() {
233 |         const onSomething = () => ({
234 |             addListener: function addListener() {
235 |             },
236 |             dispatch: function dispatch() {
237 |             },
238 |             hasListener: function hasListener() {
239 |             },
240 |             hasListeners: function hasListeners() {
241 |                 return false
242 |             },
243 |             removeListener: function removeListener() {
244 |             }
245 |         })
246 | 
247 |         const response = {
248 |             name: '',
249 |             sender: undefined,
250 |             disconnect: function disconnect() {
251 |             },
252 |             onDisconnect: onSomething(),
253 |             onMessage: onSomething(),
254 |             postMessage: function postMessage() {
255 |                 if (!arguments.length) {
256 |                     throw new TypeError(`Insufficient number of arguments.`)
257 |                 }
258 |                 throw new Error(`Attempting to use a disconnected port object`)
259 |             }
260 |         }
261 |         return response
262 |     }
263 | }
264 | 
265 | """
266 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/generate_magic_arrays.py:
--------------------------------------------------------------------------------
  1 | generate_magic_arrays = """
  2 | generateFunctionMocks = (
  3 |     proto,
  4 |     itemMainProp,
  5 |     dataArray
  6 | ) => ({
  7 |     item: utils.createProxy(proto.item, {
  8 |         apply(target, ctx, args) {
  9 |             if (!args.length) {
 10 |                 throw new TypeError(
 11 |                     `Failed to execute 'item' on '${
 12 |                         proto[Symbol.toStringTag]
 13 |                     }': 1 argument required, but only 0 present.`
 14 |                 )
 15 |             }
 16 |             // Special behavior alert:
 17 |             // - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup
 18 |             // - If anything else than an integer (including as string) is provided it will return the first entry
 19 |             const isInteger = args[0] && Number.isInteger(Number(args[0])) // Cast potential string to number first, then check for integer
 20 |             // Note: Vanilla never returns `undefined`
 21 |             return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null
 22 |         }
 23 |     }),
 24 |     /** Returns the MimeType object with the specified name. */
 25 |     namedItem: utils.createProxy(proto.namedItem, {
 26 |         apply(target, ctx, args) {
 27 |             if (!args.length) {
 28 |                 throw new TypeError(
 29 |                     `Failed to execute 'namedItem' on '${
 30 |                         proto[Symbol.toStringTag]
 31 |                     }': 1 argument required, but only 0 present.`
 32 |                 )
 33 |             }
 34 |             return dataArray.find(mt => mt[itemMainProp] === args[0]) || null // Not `undefined`!
 35 |         }
 36 |     }),
 37 |     /** Does nothing and shall return nothing */
 38 |     refresh: proto.refresh
 39 |         ? utils.createProxy(proto.refresh, {
 40 |             apply(target, ctx, args) {
 41 |                 return undefined
 42 |             }
 43 |         })
 44 |         : undefined
 45 | })
 46 | 
 47 | function generateMagicArray(
 48 |     dataArray = [],
 49 |     proto = MimeTypeArray.prototype,
 50 |     itemProto = MimeType.prototype,
 51 |     itemMainProp = 'type'
 52 | ) {
 53 |     // Quick helper to set props with the same descriptors vanilla is using
 54 |     const defineProp = (obj, prop, value) =>
 55 |         Object.defineProperty(obj, prop, {
 56 |             value,
 57 |             writable: false,
 58 |             enumerable: false, // Important for mimeTypes & plugins: `JSON.stringify(navigator.mimeTypes)`
 59 |             configurable: false
 60 |         })
 61 | 
 62 |     // Loop over our fake data and construct items
 63 |     const makeItem = data => {
 64 |         const item = {}
 65 |         for (const prop of Object.keys(data)) {
 66 |             if (prop.startsWith('__')) {
 67 |                 continue
 68 |             }
 69 |             defineProp(item, prop, data[prop])
 70 |         }
 71 |         // navigator.plugins[i].length should always be 1
 72 |         if (itemProto === Plugin.prototype) {
 73 |             defineProp(item, 'length', 1)
 74 |         }
 75 |         // We need to spoof a specific `MimeType` or `Plugin` object
 76 |         return Object.create(itemProto, Object.getOwnPropertyDescriptors(item))
 77 |     }
 78 | 
 79 |     const magicArray = []
 80 | 
 81 |     // Loop through our fake data and use that to create convincing entities
 82 |     dataArray.forEach(data => {
 83 |         magicArray.push(makeItem(data))
 84 |     })
 85 | 
 86 |     // Add direct property access  based on types (e.g. `obj['application/pdf']`) afterwards
 87 |     magicArray.forEach(entry => {
 88 |         defineProp(magicArray, entry[itemMainProp], entry)
 89 |     })
 90 | 
 91 |     // This is the best way to fake the type to make sure this is false: `Array.isArray(navigator.mimeTypes)`
 92 |     const magicArrayObj = Object.create(proto, {
 93 |         ...Object.getOwnPropertyDescriptors(magicArray),
 94 | 
 95 |         // There's one ugly quirk we unfortunately need to take care of:
 96 |         // The `MimeTypeArray` prototype has an enumerable `length` property,
 97 |         // but headful Chrome will still skip it when running `Object.getOwnPropertyNames(navigator.mimeTypes)`.
 98 |         // To strip it we need to make it first `configurable` and can then overlay a Proxy with an `ownKeys` trap.
 99 |         length: {
100 |             value: magicArray.length,
101 |             writable: false,
102 |             enumerable: false,
103 |             configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length`
104 |         }
105 |     })
106 | 
107 |     // Generate our functional function mocks :-)
108 |     const functionMocks = generateFunctionMocks(
109 |         proto,
110 |         itemMainProp,
111 |         magicArray
112 |     )
113 | 
114 |     // Override custom object with proxy
115 |     return new Proxy(magicArrayObj, {
116 |         get(target, key = '') {
117 |             // Redirect function calls to our custom proxied versions mocking the vanilla behavior
118 |             if (key === 'item') {
119 |                 return functionMocks.item
120 |             }
121 |             if (key === 'namedItem') {
122 |                 return functionMocks.namedItem
123 |             }
124 |             if (proto === PluginArray.prototype && key === 'refresh') {
125 |                 return functionMocks.refresh
126 |             }
127 |             // Everything else can pass through as normal
128 |             return utils.cache.Reflect.get(...arguments)
129 |         },
130 |         ownKeys(target) {
131 |             // There are a couple of quirks where the original property demonstrates "magical" behavior that makes no sense
132 |             // This can be witnessed when calling `Object.getOwnPropertyNames(navigator.mimeTypes)` and the absense of `length`
133 |             // My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly
134 |             // For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing
135 |             // Specific issues when not patching this: `length` property is available, direct `types` props (e.g. `obj['application/pdf']`) are missing
136 |             const keys = []
137 |             const typeProps = magicArray.map(mt => mt[itemMainProp])
138 |             typeProps.forEach((_, i) => keys.push(`${i}`))
139 |             typeProps.forEach(propName => keys.push(propName))
140 |             return keys
141 |         }
142 |     })
143 | }
144 | """
145 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/iframe_contentWindow.py:
--------------------------------------------------------------------------------
  1 | iframe_contentWindow = """
  2 | try {
  3 |     // Adds a contentWindow proxy to the provided iframe element
  4 |     const addContentWindowProxy = iframe => {
  5 |         const contentWindowProxy = {
  6 |             get(target, key) {
  7 |                 // Now to the interesting part:
  8 |                 // We actually make this thing behave like a regular iframe window,
  9 |                 // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :)
 10 |                 // That makes it possible for these assertions to be correct:
 11 |                 // iframe.contentWindow.self === window.top // must be false
 12 |                 if (key === 'self') {
 13 |                     return this
 14 |                 }
 15 |                 // iframe.contentWindow.frameElement === iframe // must be true
 16 |                 if (key === 'frameElement') {
 17 |                     return iframe
 18 |                 }
 19 |                 return Reflect.get(target, key)
 20 |             }
 21 |         }
 22 | 
 23 |         if (!iframe.contentWindow) {
 24 |             const proxy = new Proxy(window, contentWindowProxy)
 25 |             Object.defineProperty(iframe, 'contentWindow', {
 26 |                 get() {
 27 |                     return proxy
 28 |                 },
 29 |                 set(newValue) {
 30 |                     return newValue // contentWindow is immutable
 31 |                 },
 32 |                 enumerable: true,
 33 |                 configurable: false
 34 |             })
 35 |         }
 36 |     }
 37 | 
 38 |     // Handles iframe element creation, augments `srcdoc` property so we can intercept further
 39 |     const handleIframeCreation = (target, thisArg, args) => {
 40 |         const iframe = target.apply(thisArg, args)
 41 | 
 42 |         // We need to keep the originals around
 43 |         const _iframe = iframe
 44 |         const _srcdoc = _iframe.srcdoc
 45 | 
 46 |         // Add hook for the srcdoc property
 47 |         // We need to be very surgical here to not break other iframes by accident
 48 |         Object.defineProperty(iframe, 'srcdoc', {
 49 |             configurable: true, // Important, so we can reset this later
 50 |             get: function () {
 51 |                 return _iframe.srcdoc
 52 |             },
 53 |             set: function (newValue) {
 54 |                 addContentWindowProxy(this)
 55 |                 // Reset property, the hook is only needed once
 56 |                 Object.defineProperty(iframe, 'srcdoc', {
 57 |                     configurable: false,
 58 |                     writable: false,
 59 |                     value: _srcdoc
 60 |                 })
 61 |                 _iframe.srcdoc = newValue
 62 |             }
 63 |         })
 64 |         return iframe
 65 |     }
 66 | 
 67 |     // Adds a hook to intercept iframe creation events
 68 |     const addIframeCreationSniffer = () => {
 69 |         /* global document */
 70 |         const createElementHandler = {
 71 |             // Make toString() native
 72 |             get(target, key) {
 73 |                 return Reflect.get(target, key)
 74 |             },
 75 |             apply: function (target, thisArg, args) {
 76 |                 const isIframe =
 77 |                     args && args.length && `${args[0]}`.toLowerCase() === 'iframe'
 78 |                 if (!isIframe) {
 79 |                     // Everything as usual
 80 |                     return target.apply(thisArg, args)
 81 |                 } else {
 82 |                     return handleIframeCreation(target, thisArg, args)
 83 |                 }
 84 |             }
 85 |         }
 86 |         // All this just due to iframes with srcdoc bug
 87 |         utils.replaceWithProxy(
 88 |             document,
 89 |             'createElement',
 90 |             createElementHandler
 91 |           )
 92 |     }
 93 | 
 94 |     // Let's go
 95 |     addIframeCreationSniffer()
 96 | } catch (err) {
 97 |     // console.warn(err)
 98 | }
 99 | """
100 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/media_codecs.py:
--------------------------------------------------------------------------------
 1 | media_codecs = """
 2 | /**
 3 |  * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing.
 4 |  *
 5 |  * @example
 6 |  * video/webm; codecs="vp8, vorbis"
 7 |  * video/mp4; codecs="avc1.42E01E"
 8 |  * audio/x-m4a;
 9 |  * audio/ogg; codecs="vorbis"
10 |  * @param {String} arg
11 |  */
12 | const parseInput = arg => {
13 |     const [mime, codecStr] = arg.trim().split(';')
14 |     let codecs = []
15 |     if (codecStr && codecStr.includes('codecs="')) {
16 |         codecs = codecStr
17 |             .trim()
18 |             .replace(`codecs="`, '')
19 |             .replace(`"`, '')
20 |             .trim()
21 |             .split(',')
22 |             .filter(x => !!x)
23 |             .map(x => x.trim())
24 |     }
25 |     return {
26 |         mime,
27 |         codecStr,
28 |         codecs
29 |     }
30 | }
31 | 
32 | const canPlayType = {
33 |     // Intercept certain requests
34 |     apply: function (target, ctx, args) {
35 |         if (!args || !args.length) {
36 |             return target.apply(ctx, args)
37 |         }
38 |         const {mime, codecs} = parseInput(args[0])
39 |         // This specific mp4 codec is missing in Chromium
40 |         if (mime === 'video/mp4') {
41 |             if (codecs.includes('avc1.42E01E')) {
42 |                 return 'probably'
43 |             }
44 |         }
45 |         // This mimetype is only supported if no codecs are specified
46 |         if (mime === 'audio/x-m4a' && !codecs.length) {
47 |             return 'maybe'
48 |         }
49 | 
50 |         // This mimetype is only supported if no codecs are specified
51 |         if (mime === 'audio/aac' && !codecs.length) {
52 |             return 'probably'
53 |         }
54 |         // Everything else as usual
55 |         return target.apply(ctx, args)
56 |     }
57 | }
58 | 
59 | /* global HTMLMediaElement */
60 | utils.replaceWithProxy(
61 |     HTMLMediaElement.prototype,
62 |     'canPlayType',
63 |     canPlayType
64 | )
65 | """
66 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/navigator_hardwareConcurrency.py:
--------------------------------------------------------------------------------
 1 | navigator_hardwareConcurrency = """
 2 | const patchNavigator = (name, value) =>
 3 |     utils.replaceProperty(Object.getPrototypeOf(navigator), name, {
 4 |         get() {
 5 |             return value
 6 |         }
 7 |     })
 8 | 
 9 | patchNavigator('hardwareConcurrency', opts.navigator_hardware_concurrency || 4);
10 | """
11 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/navigator_languages.py:
--------------------------------------------------------------------------------
1 | navigator_languages = """
2 | Object.defineProperty(Object.getPrototypeOf(navigator), 'languages', {
3 |     get: () => opts.languages || ['en-US', 'en']
4 | })
5 | 
6 | """
7 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/navigator_permissions.py:
--------------------------------------------------------------------------------
 1 | navigator_permissions = """
 2 | const handler = {
 3 |     apply: function (target, ctx, args) {
 4 |         const param = (args || [])[0]
 5 | 
 6 |         if (param && param.name && param.name === 'notifications') {
 7 |             const result = {state: Notification.permission}
 8 |             Object.setPrototypeOf(result, PermissionStatus.prototype)
 9 |             return Promise.resolve(result)
10 |         }
11 | 
12 |         return utils.cache.Reflect.apply(...arguments)
13 |     }
14 | }
15 | 
16 | utils.replaceWithProxy(
17 |     window.navigator.permissions.__proto__, // eslint-disable-line no-proto
18 |     'query',
19 |     handler
20 | )
21 | 
22 | """
23 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/navigator_platform.py:
--------------------------------------------------------------------------------
1 | navigator_platform = """
2 | if (opts.navigator_platform) {
3 |     Object.defineProperty(Object.getPrototypeOf(navigator), 'platform', {
4 |         get: () => opts.navigator_plaftorm,
5 |     })
6 | }
7 | """
8 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/navigator_plugins.py:
--------------------------------------------------------------------------------
 1 | navigator_plugins = """
 2 | data = {
 3 |     "mimeTypes": [
 4 |         {
 5 |             "type": "application/pdf",
 6 |             "suffixes": "pdf",
 7 |             "description": "",
 8 |             "__pluginName": "Chrome PDF Viewer"
 9 |         },
10 |         {
11 |             "type": "application/x-google-chrome-pdf",
12 |             "suffixes": "pdf",
13 |             "description": "Portable Document Format",
14 |             "__pluginName": "Chrome PDF Plugin"
15 |         },
16 |         {
17 |             "type": "application/x-nacl",
18 |             "suffixes": "",
19 |             "description": "Native Client Executable",
20 |             "__pluginName": "Native Client"
21 |         },
22 |         {
23 |             "type": "application/x-pnacl",
24 |             "suffixes": "",
25 |             "description": "Portable Native Client Executable",
26 |             "__pluginName": "Native Client"
27 |         }
28 |     ],
29 |     "plugins": [
30 |         {
31 |             "name": "Chrome PDF Plugin",
32 |             "filename": "internal-pdf-viewer",
33 |             "description": "Portable Document Format",
34 |             "__mimeTypes": ["application/x-google-chrome-pdf"]
35 |         },
36 |         {
37 |             "name": "Chrome PDF Viewer",
38 |             "filename": "mhjfbmdgcfjbbpaeojofohoefgiehjai",
39 |             "description": "",
40 |             "__mimeTypes": ["application/pdf"]
41 |         },
42 |         {
43 |             "name": "Native Client",
44 |             "filename": "internal-nacl-plugin",
45 |             "description": "",
46 |             "__mimeTypes": ["application/x-nacl", "application/x-pnacl"]
47 |         }
48 |     ]
49 | }
50 | 
51 | 
52 | // That means we're running headful
53 | const hasPlugins = 'plugins' in navigator && navigator.plugins.length
54 | if (!(hasPlugins)) {
55 | 
56 |     const mimeTypes = generateMagicArray(
57 |         data.mimeTypes,
58 |         MimeTypeArray.prototype,
59 |         MimeType.prototype,
60 |         'type'
61 |     )
62 |     const plugins = generateMagicArray(
63 |         data.plugins,
64 |         PluginArray.prototype,
65 |         Plugin.prototype,
66 |         'name'
67 |     )
68 | 
69 |     // Plugin and MimeType cross-reference each other, let's do that now
70 |     // Note: We're looping through `data.plugins` here, not the generated `plugins`
71 |     for (const pluginData of data.plugins) {
72 |         pluginData.__mimeTypes.forEach((type, index) => {
73 |             plugins[pluginData.name][index] = mimeTypes[type]
74 |             plugins[type] = mimeTypes[type]
75 |             Object.defineProperty(mimeTypes[type], 'enabledPlugin', {
76 |                 value: JSON.parse(JSON.stringify(plugins[pluginData.name])),
77 |                 writable: false,
78 |                 enumerable: false, // Important: `JSON.stringify(navigator.plugins)`
79 |                 configurable: false
80 |             })
81 |         })
82 |     }
83 | 
84 |     const patchNavigator = (name, value) =>
85 |         utils.replaceProperty(Object.getPrototypeOf(navigator), name, {
86 |             get() {
87 |                 return value
88 |             }
89 |         })
90 | 
91 |     patchNavigator('mimeTypes', mimeTypes)
92 |     patchNavigator('plugins', plugins)
93 | }
94 | """
95 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/navigator_userAgent.py:
--------------------------------------------------------------------------------
 1 | navigator_userAgent = """
 2 | // replace Headless references in default useragent
 3 | const current_ua = navigator.userAgent;
 4 | Object.defineProperty(Object.getPrototypeOf(navigator), 'userAgent', {
 5 |     get: () => {
 6 |         try {
 7 |             if (typeof opts !== 'undefined' && opts.navigator_user_agent) {
 8 |                 return opts.navigator_user_agent;
 9 |             }
10 |         } catch (error) {
11 |             console.warn('Error accessing opts:', error);
12 |         }
13 |         return current_ua.replace('HeadlessChrome/', 'Chrome/');
14 |     }
15 | });
16 | """
17 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/navigator_vendor.py:
--------------------------------------------------------------------------------
1 | navigator_vendor = """
2 | Object.defineProperty(Object.getPrototypeOf(navigator), 'vendor', {
3 |     get: () => opts.navigator_vendor || 'Google Inc.',
4 | })
5 | 
6 | """
7 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/utils.py:
--------------------------------------------------------------------------------
  1 | utils = """
  2 | /**
  3 |  * A set of shared utility functions specifically for the purpose of modifying native browser APIs without leaving traces.
  4 |  *
  5 |  * Meant to be passed down in puppeteer and used in the context of the page (everything in here runs in NodeJS as well as a browser).
  6 |  *
  7 |  * Note: If for whatever reason you need to use this outside of `puppeteer-extra`:
  8 |  * Just remove the `module.exports` statement at the very bottom, the rest can be copy pasted into any browser context.
  9 |  *
 10 |  * Alternatively take a look at the `extract-stealth-evasions` package to create a finished bundle which includes these utilities.
 11 |  *
 12 |  */
 13 | const utils = {}
 14 | 
 15 | /**
 16 |  * Wraps a JS Proxy Handler and strips it's presence from error stacks, in case the traps throw.
 17 |  *
 18 |  * The presence of a JS Proxy can be revealed as it shows up in error stack traces.
 19 |  *
 20 |  * @param {object} handler - The JS Proxy handler to wrap
 21 |  */
 22 | utils.stripProxyFromErrors = (handler = {}) => {
 23 |   const newHandler = {}
 24 |   // We wrap each trap in the handler in a try/catch and modify the error stack if they throw
 25 |   const traps = Object.getOwnPropertyNames(handler)
 26 |   traps.forEach(trap => {
 27 |     newHandler[trap] = function() {
 28 |       try {
 29 |         // Forward the call to the defined proxy handler
 30 |         return handler[trap].apply(this, arguments || [])
 31 |       } catch (err) {
 32 |         // Stack traces differ per browser, we only support chromium based ones currently
 33 |         if (!err || !err.stack || !err.stack.includes(`at `)) {
 34 |           throw err
 35 |         }
 36 | 
 37 |         // When something throws within one of our traps the Proxy will show up in error stacks
 38 |         // An earlier implementation of this code would simply strip lines with a blacklist,
 39 |         // but it makes sense to be more surgical here and only remove lines related to our Proxy.
 40 |         // We try to use a known "anchor" line for that and strip it with everything above it.
 41 |         // If the anchor line cannot be found for some reason we fall back to our blacklist approach.
 42 | 
 43 |         const stripWithBlacklist = stack => {
 44 |           const blacklist = [
 45 |             `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply
 46 |             `at Object.${trap} `, // e.g. Object.get or Object.apply
 47 |             `at Object.newHandler.<computed> [as ${trap}] ` // caused by this very wrapper :-)
 48 |           ]
 49 |           return (
 50 |             err.stack
 51 |               .split('\n')
 52 |               // Always remove the first (file) line in the stack (guaranteed to be our proxy)
 53 |               .filter((line, index) => index !== 1)
 54 |               // Check if the line starts with one of our blacklisted strings
 55 |               .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))
 56 |               .join('\n')
 57 |           )
 58 |         }
 59 | 
 60 |         const stripWithAnchor = stack => {
 61 |           const stackArr = stack.split('\n')
 62 |           const anchor = `at Object.newHandler.<computed> [as ${trap}] ` // Known first Proxy line in chromium
 63 |           const anchorIndex = stackArr.findIndex(line =>
 64 |             line.trim().startsWith(anchor)
 65 |           )
 66 |           if (anchorIndex === -1) {
 67 |             return false // 404, anchor not found
 68 |           }
 69 |           // Strip everything from the top until we reach the anchor line
 70 |           // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)
 71 |           stackArr.splice(1, anchorIndex)
 72 |           return stackArr.join('\n')
 73 |         }
 74 | 
 75 |         // Try using the anchor method, fallback to blacklist if necessary
 76 |         err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)
 77 | 
 78 |         throw err // Re-throw our now sanitized error
 79 |       }
 80 |     }
 81 |   })
 82 |   return newHandler
 83 | }
 84 | 
 85 | /**
 86 |  * Strip error lines from stack traces until (and including) a known line the stack.
 87 |  *
 88 |  * @param {object} err - The error to sanitize
 89 |  * @param {string} anchor - The string the anchor line starts with
 90 |  */
 91 | utils.stripErrorWithAnchor = (err, anchor) => {
 92 |   const stackArr = err.stack.split('\n')
 93 |   const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))
 94 |   if (anchorIndex === -1) {
 95 |     return err // 404, anchor not found
 96 |   }
 97 |   // Strip everything from the top until we reach the anchor line (remove anchor line as well)
 98 |   // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)
 99 |   stackArr.splice(1, anchorIndex)
100 |   err.stack = stackArr.join('\n')
101 |   return err
102 | }
103 | 
104 | /**
105 |  * Replace the property of an object in a stealthy way.
106 |  *
107 |  * Note: You also want to work on the prototype of an object most often,
108 |  * as you'd otherwise leave traces (e.g. showing up in Object.getOwnPropertyNames(obj)).
109 |  *
110 |  * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
111 |  *
112 |  * @example
113 |  * replaceProperty(WebGLRenderingContext.prototype, 'getParameter', { value: "alice" })
114 |  * // or
115 |  * replaceProperty(Object.getPrototypeOf(navigator), 'languages', { get: () => ['en-US', 'en'] })
116 |  *
117 |  * @param {object} obj - The object which has the property to replace
118 |  * @param {string} propName - The property name to replace
119 |  * @param {object} descriptorOverrides - e.g. { value: "alice" }
120 |  */
121 | utils.replaceProperty = (obj, propName, descriptorOverrides = {}) => {
122 |   return Object.defineProperty(obj, propName, {
123 |     // Copy over the existing descriptors (writable, enumerable, configurable, etc)
124 |     ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),
125 |     // Add our overrides (e.g. value, get())
126 |     ...descriptorOverrides
127 |   })
128 | }
129 | 
130 | /**
131 |  * Preload a cache of function copies and data.
132 |  *
133 |  * For a determined enough observer it would be possible to overwrite and sniff usage of functions
134 |  * we use in our internal Proxies, to combat that we use a cached copy of those functions.
135 |  *
136 |  * This is evaluated once per execution context (e.g. window)
137 |  */
138 | utils.preloadCache = () => {
139 |   if (utils.cache) {
140 |     return
141 |   }
142 |   utils.cache = {
143 |     // Used in our proxies
144 |     Reflect: {
145 |       get: Reflect.get.bind(Reflect),
146 |       apply: Reflect.apply.bind(Reflect)
147 |     },
148 |     // Used in `makeNativeString`
149 |     nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }`
150 |   }
151 | }
152 | 
153 | /**
154 |  * Utility function to generate a cross-browser `toString` result representing native code.
155 |  *
156 |  * There's small differences: Chromium uses a single line, whereas FF & Webkit uses multiline strings.
157 |  * To future-proof this we use an existing native toString result as the basis.
158 |  *
159 |  * The only advantage we have over the other team is that our JS runs first, hence we cache the result
160 |  * of the native toString result once, so they cannot spoof it afterwards and reveal that we're using it.
161 |  *
162 |  * Note: Whenever we add a `Function.prototype.toString` proxy we should preload the cache before,
163 |  * by executing `utils.preloadCache()` before the proxy is applied (so we don't cause recursive lookups).
164 |  *
165 |  * @example
166 |  * makeNativeString('foobar') // => `function foobar() { [native code] }`
167 |  *
168 |  * @param {string} [name] - Optional function name
169 |  */
170 | utils.makeNativeString = (name = '') => {
171 |   // Cache (per-window) the original native toString or use that if available
172 |   utils.preloadCache()
173 |   return utils.cache.nativeToStringStr.replace('toString', name || '')
174 | }
175 | 
176 | /**
177 |  * Helper function to modify the `toString()` result of the provided object.
178 |  *
179 |  * Note: Use `utils.redirectToString` instead when possible.
180 |  *
181 |  * There's a quirk in JS Proxies that will cause the `toString()` result to differ from the vanilla Object.
182 |  * If no string is provided we will generate a `[native code]` thing based on the name of the property object.
183 |  *
184 |  * @example
185 |  * patchToString(WebGLRenderingContext.prototype.getParameter, 'function getParameter() { [native code] }')
186 |  *
187 |  * @param {object} obj - The object for which to modify the `toString()` representation
188 |  * @param {string} str - Optional string used as a return value
189 |  */
190 | utils.patchToString = (obj, str = '') => {
191 |   utils.preloadCache()
192 | 
193 |   const toStringProxy = new Proxy(Function.prototype.toString, {
194 |     apply: function(target, ctx) {
195 |       // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""`
196 |       if (ctx === Function.prototype.toString) {
197 |         return utils.makeNativeString('toString')
198 |       }
199 |       // `toString` targeted at our proxied Object detected
200 |       if (ctx === obj) {
201 |         // We either return the optional string verbatim or derive the most desired result automatically
202 |         return str || utils.makeNativeString(obj.name)
203 |       }
204 |       // Check if the toString protype of the context is the same as the global prototype,
205 |       // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case
206 |       const hasSameProto = Object.getPrototypeOf(
207 |         Function.prototype.toString
208 |       ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins
209 |       if (!hasSameProto) {
210 |         // Pass the call on to the local Function.prototype.toString instead
211 |         return ctx.toString()
212 |       }
213 |       return target.call(ctx)
214 |     }
215 |   })
216 |   utils.replaceProperty(Function.prototype, 'toString', {
217 |     value: toStringProxy
218 |   })
219 | }
220 | 
221 | /**
222 |  * Make all nested functions of an object native.
223 |  *
224 |  * @param {object} obj
225 |  */
226 | utils.patchToStringNested = (obj = {}) => {
227 |   return utils.execRecursively(obj, ['function'], utils.patchToString)
228 | }
229 | 
230 | /**
231 |  * Redirect toString requests from one object to another.
232 |  *
233 |  * @param {object} proxyObj - The object that toString will be called on
234 |  * @param {object} originalObj - The object which toString result we wan to return
235 |  */
236 | utils.redirectToString = (proxyObj, originalObj) => {
237 |   utils.preloadCache()
238 | 
239 |   const toStringProxy = new Proxy(Function.prototype.toString, {
240 |     apply: function(target, ctx) {
241 |       // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""`
242 |       if (ctx === Function.prototype.toString) {
243 |         return utils.makeNativeString('toString')
244 |       }
245 | 
246 |       // `toString` targeted at our proxied Object detected
247 |       if (ctx === proxyObj) {
248 |         const fallback = () =>
249 |           originalObj && originalObj.name
250 |             ? utils.makeNativeString(originalObj.name)
251 |             : utils.makeNativeString(proxyObj.name)
252 | 
253 |         // Return the toString representation of our original object if possible
254 |         return originalObj + '' || fallback()
255 |       }
256 | 
257 |       // Check if the toString protype of the context is the same as the global prototype,
258 |       // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case
259 |       const hasSameProto = Object.getPrototypeOf(
260 |         Function.prototype.toString
261 |       ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins
262 |       if (!hasSameProto) {
263 |         // Pass the call on to the local Function.prototype.toString instead
264 |         return ctx.toString()
265 |       }
266 | 
267 |       return target.call(ctx)
268 |     }
269 |   })
270 |   utils.replaceProperty(Function.prototype, 'toString', {
271 |     value: toStringProxy
272 |   })
273 | }
274 | 
275 | /**
276 |  * All-in-one method to replace a property with a JS Proxy using the provided Proxy handler with traps.
277 |  *
278 |  * Will stealthify these aspects (strip error stack traces, redirect toString, etc).
279 |  * Note: This is meant to modify native Browser APIs and works best with prototype objects.
280 |  *
281 |  * @example
282 |  * replaceWithProxy(WebGLRenderingContext.prototype, 'getParameter', proxyHandler)
283 |  *
284 |  * @param {object} obj - The object which has the property to replace
285 |  * @param {string} propName - The name of the property to replace
286 |  * @param {object} handler - The JS Proxy handler to use
287 |  */
288 | utils.replaceWithProxy = (obj, propName, handler) => {
289 |   utils.preloadCache()
290 |   const originalObj = obj[propName]
291 |   const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))
292 | 
293 |   utils.replaceProperty(obj, propName, { value: proxyObj })
294 |   utils.redirectToString(proxyObj, originalObj)
295 | 
296 |   return true
297 | }
298 | 
299 | /**
300 |  * All-in-one method to mock a non-existing property with a JS Proxy using the provided Proxy handler with traps.
301 |  *
302 |  * Will stealthify these aspects (strip error stack traces, redirect toString, etc).
303 |  *
304 |  * @example
305 |  * mockWithProxy(chrome.runtime, 'sendMessage', function sendMessage() {}, proxyHandler)
306 |  *
307 |  * @param {object} obj - The object which has the property to replace
308 |  * @param {string} propName - The name of the property to replace or create
309 |  * @param {object} pseudoTarget - The JS Proxy target to use as a basis
310 |  * @param {object} handler - The JS Proxy handler to use
311 |  */
312 | utils.mockWithProxy = (obj, propName, pseudoTarget, handler) => {
313 |   utils.preloadCache()
314 |   const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))
315 | 
316 |   utils.replaceProperty(obj, propName, { value: proxyObj })
317 |   utils.patchToString(proxyObj)
318 | 
319 |   return true
320 | }
321 | 
322 | /**
323 |  * All-in-one method to create a new JS Proxy with stealth tweaks.
324 |  *
325 |  * This is meant to be used whenever we need a JS Proxy but don't want to replace or mock an existing known property.
326 |  *
327 |  * Will stealthify certain aspects of the Proxy (strip error stack traces, redirect toString, etc).
328 |  *
329 |  * @example
330 |  * createProxy(navigator.mimeTypes.__proto__.namedItem, proxyHandler) // => Proxy
331 |  *
332 |  * @param {object} pseudoTarget - The JS Proxy target to use as a basis
333 |  * @param {object} handler - The JS Proxy handler to use
334 |  */
335 | utils.createProxy = (pseudoTarget, handler) => {
336 |   utils.preloadCache()
337 |   const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))
338 |   utils.patchToString(proxyObj)
339 | 
340 |   return proxyObj
341 | }
342 | 
343 | /**
344 |  * Helper function to split a full path to an Object into the first part and property.
345 |  *
346 |  * @example
347 |  * splitObjPath(`HTMLMediaElement.prototype.canPlayType`)
348 |  * // => {objName: "HTMLMediaElement.prototype", propName: "canPlayType"}
349 |  *
350 |  * @param {string} objPath - The full path to an object as dot notation string
351 |  */
352 | utils.splitObjPath = objPath => ({
353 |   // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`
354 |   objName: objPath
355 |     .split('.')
356 |     .slice(0, -1)
357 |     .join('.'),
358 |   // Extract last dot entry ==> `canPlayType`
359 |   propName: objPath.split('.').slice(-1)[0]
360 | })
361 | 
362 | /**
363 |  * Convenience method to replace a property with a JS Proxy using the provided objPath.
364 |  *
365 |  * Supports a full path (dot notation) to the object as string here, in case that makes it easier.
366 |  *
367 |  * @example
368 |  * replaceObjPathWithProxy('WebGLRenderingContext.prototype.getParameter', proxyHandler)
369 |  *
370 |  * @param {string} objPath - The full path to an object (dot notation string) to replace
371 |  * @param {object} handler - The JS Proxy handler to use
372 |  */
373 | utils.replaceObjPathWithProxy = (objPath, handler) => {
374 |   const { objName, propName } = utils.splitObjPath(objPath)
375 |   const obj = eval(objName) // eslint-disable-line no-eval
376 |   return utils.replaceWithProxy(obj, propName, handler)
377 | }
378 | 
379 | /**
380 |  * Traverse nested properties of an object recursively and apply the given function on a whitelist of value types.
381 |  *
382 |  * @param {object} obj
383 |  * @param {array} typeFilter - e.g. `['function']`
384 |  * @param {Function} fn - e.g. `utils.patchToString`
385 |  */
386 | utils.execRecursively = (obj = {}, typeFilter = [], fn) => {
387 |   function recurse(obj) {
388 |     for (const key in obj) {
389 |       if (obj[key] === undefined) {
390 |         continue
391 |       }
392 |       if (obj[key] && typeof obj[key] === 'object') {
393 |         recurse(obj[key])
394 |       } else {
395 |         if (obj[key] && typeFilter.includes(typeof obj[key])) {
396 |           fn.call(this, obj[key])
397 |         }
398 |       }
399 |     }
400 |   }
401 |   recurse(obj)
402 |   return obj
403 | }
404 | 
405 | /**
406 |  * Everything we run through e.g. `page.evaluate` runs in the browser context, not the NodeJS one.
407 |  * That means we cannot just use reference variables and functions from outside code, we need to pass everything as a parameter.
408 |  *
409 |  * Unfortunately the data we can pass is only allowed to be of primitive types, regular functions don't survive the built-in serialization process.
410 |  * This utility function will take an object with functions and stringify them, so we can pass them down unharmed as strings.
411 |  *
412 |  * We use this to pass down our utility functions as well as any other functions (to be able to split up code better).
413 |  *
414 |  * @see utils.materializeFns
415 |  *
416 |  * @param {object} fnObj - An object containing functions as properties
417 |  */
418 | utils.stringifyFns = (fnObj = { hello: () => 'world' }) => {
419 |   // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine
420 |   // https://github.com/feross/fromentries
421 |   function fromEntries(iterable) {
422 |     return [...iterable].reduce((obj, [key, val]) => {
423 |       obj[key] = val
424 |       return obj
425 |     }, {})
426 |   }
427 |   return (Object.fromEntries || fromEntries)(
428 |     Object.entries(fnObj)
429 |       .filter(([key, value]) => typeof value === 'function')
430 |       .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval
431 |   )
432 | }
433 | 
434 | /**
435 |  * Utility function to reverse the process of `utils.stringifyFns`.
436 |  * Will materialize an object with stringified functions (supports classic and fat arrow functions).
437 |  *
438 |  * @param {object} fnStrObj - An object containing stringified functions as properties
439 |  */
440 | utils.materializeFns = (fnStrObj = { hello: "() => 'world'" }) => {
441 |   return Object.fromEntries(
442 |     Object.entries(fnStrObj).map(([key, value]) => {
443 |       if (value.startsWith('function')) {
444 |         // some trickery is needed to make oldschool functions work :-)
445 |         return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval
446 |       } else {
447 |         // arrow functions just work
448 |         return [key, eval(value)] // eslint-disable-line no-eval
449 |       }
450 |     })
451 |   )
452 | }
453 | 
454 | // --
455 | // Stuff starting below this line is NodeJS specific.
456 | // --
457 | // module.exports = utils
458 | """
459 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/webgl_vendor.py:
--------------------------------------------------------------------------------
 1 | webgl_vendor = """
 2 | console.log(opts)
 3 | const getParameterProxyHandler = {
 4 |     apply: function (target, ctx, args) {
 5 |         const param = (args || [])[0]
 6 |         // UNMASKED_VENDOR_WEBGL
 7 |         if (param === 37445) {
 8 |             return opts.webgl_vendor || 'Intel Inc.' // default in headless: Google Inc.
 9 |         }
10 |         // UNMASKED_RENDERER_WEBGL
11 |         if (param === 37446) {
12 |             return opts.webgl_renderer || 'Intel Iris OpenGL Engine' // default in headless: Google SwiftShader
13 |         }
14 |         return utils.cache.Reflect.apply(target, ctx, args)
15 |     }
16 | }
17 | 
18 | // There's more than one WebGL rendering context
19 | // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext#Browser_compatibility
20 | // To find out the original values here: Object.getOwnPropertyDescriptors(WebGLRenderingContext.prototype.getParameter)
21 | const addProxy = (obj, propName) => {
22 |     utils.replaceWithProxy(obj, propName, getParameterProxyHandler)
23 | }
24 | // For whatever weird reason loops don't play nice with Object.defineProperty, here's the next best thing:
25 | addProxy(WebGLRenderingContext.prototype, 'getParameter')
26 | addProxy(WebGL2RenderingContext.prototype, 'getParameter')
27 | """
28 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/js/window_outerdimensions.py:
--------------------------------------------------------------------------------
 1 | window_outerdimensions = """
 2 | 'use strict'
 3 | 
 4 | try {
 5 |     if (!!window.outerWidth && !!window.outerHeight) {
 6 |         const windowFrame = 85 // probably OS and WM dependent
 7 |         window.outerWidth = window.innerWidth
 8 |         console.log(`current window outer height ${window.outerHeight}`)
 9 |         window.outerHeight = window.innerHeight + windowFrame
10 |         console.log(`new window outer height ${window.outerHeight}`)
11 |     }
12 | } catch (err) {
13 | }
14 | 
15 | """
16 | 


--------------------------------------------------------------------------------
/TikTokApi/stealth/stealth.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | import json
  3 | from dataclasses import dataclass
  4 | from typing import Tuple, Optional, Dict
  5 | 
  6 | from playwright.async_api import Page as AsyncPage
  7 | 
  8 | from .js.chrome_app import chrome_app
  9 | from .js.chrome_csi import chrome_csi
 10 | from .js.chrome_hairline import chrome_hairline
 11 | from .js.chrome_load_times import chrome_load_times
 12 | from .js.chrome_runtime import chrome_runtime
 13 | from .js.generate_magic_arrays import generate_magic_arrays
 14 | from .js.iframe_contentWindow import iframe_contentWindow
 15 | from .js.media_codecs import media_codecs
 16 | from .js.navigator_hardwareConcurrency import navigator_hardwareConcurrency
 17 | from .js.navigator_languages import navigator_languages
 18 | from .js.navigator_permissions import navigator_permissions
 19 | from .js.navigator_platform import navigator_platform
 20 | from .js.navigator_plugins import navigator_plugins
 21 | from .js.navigator_userAgent import navigator_userAgent
 22 | from .js.navigator_vendor import navigator_vendor
 23 | from .js.webgl_vendor import webgl_vendor
 24 | from .js.window_outerdimensions import window_outerdimensions
 25 | from .js.utils import utils
 26 | 
 27 | SCRIPTS: Dict[str, str] = {
 28 |     "chrome_csi": chrome_csi,
 29 |     "chrome_app": chrome_app,
 30 |     "chrome_runtime": chrome_runtime,
 31 |     "chrome_load_times": chrome_load_times,
 32 |     "chrome_hairline": chrome_hairline,
 33 |     "generate_magic_arrays": generate_magic_arrays,
 34 |     "iframe_content_window": iframe_contentWindow,
 35 |     "media_codecs": media_codecs,
 36 |     "navigator_vendor": navigator_vendor,
 37 |     "navigator_plugins": navigator_plugins,
 38 |     "navigator_permissions": navigator_permissions,
 39 |     "navigator_languages": navigator_languages,
 40 |     "navigator_platform": navigator_platform,
 41 |     "navigator_user_agent": navigator_userAgent,
 42 |     "navigator_hardware_concurrency": navigator_hardwareConcurrency,
 43 |     "outerdimensions": window_outerdimensions,
 44 |     "utils": utils,
 45 |     "webdriver": "delete Object.getPrototypeOf(navigator).webdriver",
 46 |     "webgl_vendor": webgl_vendor,
 47 | }
 48 | 
 49 | 
 50 | @dataclass
 51 | class StealthConfig:
 52 |     """
 53 |     Playwright stealth configuration that applies stealth strategies to playwright page objects.
 54 |     The stealth strategies are contained in ./js package and are basic javascript scripts that are executed
 55 |     on every page.goto() called.
 56 |     Note:
 57 |         All init scripts are combined by playwright into one script and then executed this means
 58 |         the scripts should not have conflicting constants/variables etc. !
 59 |         This also means scripts can be extended by overriding enabled_scripts generator:
 60 |         ```
 61 |         @property
 62 |         def enabled_scripts():
 63 |             yield 'console.log("first script")'
 64 |             yield from super().enabled_scripts()
 65 |             yield 'console.log("last script")'
 66 |         ```
 67 |     """
 68 | 
 69 |     # load script options
 70 |     webdriver: bool = True
 71 |     webgl_vendor: bool = True
 72 |     chrome_app: bool = True
 73 |     chrome_csi: bool = True
 74 |     chrome_load_times: bool = True
 75 |     chrome_runtime: bool = True
 76 |     iframe_content_window: bool = True
 77 |     media_codecs: bool = True
 78 |     navigator_hardware_concurrency: int = 4
 79 |     navigator_languages: bool = False
 80 |     navigator_permissions: bool = True
 81 |     navigator_platform: bool = True
 82 |     navigator_plugins: bool = True
 83 |     navigator_user_agent: bool = False
 84 |     navigator_vendor: bool = False
 85 |     outerdimensions: bool = True
 86 |     hairline: bool = True
 87 | 
 88 |     # options
 89 |     vendor: str = "Intel Inc."
 90 |     renderer: str = "Intel Iris OpenGL Engine"
 91 |     nav_vendor: str = "Google Inc."
 92 |     nav_user_agent: str = None
 93 |     nav_platform: str = None
 94 |     languages: Tuple[str] = ("en-US", "en")
 95 |     runOnInsecureOrigins: Optional[bool] = None
 96 | 
 97 |     @property
 98 |     def enabled_scripts(self):
 99 |         opts = json.dumps(
100 |             {
101 |                 "webgl_vendor": self.vendor,
102 |                 "webgl_renderer": self.renderer,
103 |                 "navigator_vendor": self.nav_vendor,
104 |                 "navigator_platform": self.nav_platform,
105 |                 "navigator_user_agent": self.nav_user_agent,
106 |                 "languages": list(self.languages),
107 |                 "runOnInsecureOrigins": self.runOnInsecureOrigins,
108 |             }
109 |         )
110 |         # defined options constant
111 |         yield f"const opts = {opts}"
112 |         # init utils and generate_magic_arrays helper
113 |         yield SCRIPTS["utils"]
114 |         yield SCRIPTS["generate_magic_arrays"]
115 | 
116 |         if self.chrome_app:
117 |             yield SCRIPTS["chrome_app"]
118 |         if self.chrome_csi:
119 |             yield SCRIPTS["chrome_csi"]
120 |         if self.hairline:
121 |             yield SCRIPTS["chrome_hairline"]
122 |         if self.chrome_load_times:
123 |             yield SCRIPTS["chrome_load_times"]
124 |         if self.chrome_runtime:
125 |             yield SCRIPTS["chrome_runtime"]
126 |         if self.iframe_content_window:
127 |             yield SCRIPTS["iframe_content_window"]
128 |         if self.media_codecs:
129 |             yield SCRIPTS["media_codecs"]
130 |         if self.navigator_languages:
131 |             yield SCRIPTS["navigator_languages"]
132 |         if self.navigator_permissions:
133 |             yield SCRIPTS["navigator_permissions"]
134 |         if self.navigator_platform:
135 |             yield SCRIPTS["navigator_platform"]
136 |         if self.navigator_plugins:
137 |             yield SCRIPTS["navigator_plugins"]
138 |         if self.navigator_user_agent:
139 |             yield SCRIPTS["navigator_user_agent"]
140 |         if self.navigator_vendor:
141 |             yield SCRIPTS["navigator_vendor"]
142 |         if self.webdriver:
143 |             yield SCRIPTS["webdriver"]
144 |         if self.outerdimensions:
145 |             yield SCRIPTS["outerdimensions"]
146 |         if self.webgl_vendor:
147 |             yield SCRIPTS["webgl_vendor"]
148 | 
149 | 
150 | async def stealth_async(page: AsyncPage, config: StealthConfig = None):
151 |     """stealth the page"""
152 |     for script in (config or StealthConfig()).enabled_scripts:
153 |         await page.add_init_script(script)
154 | 


--------------------------------------------------------------------------------
/TikTokApi/tiktok.py:
--------------------------------------------------------------------------------
  1 | import asyncio
  2 | import logging
  3 | import dataclasses
  4 | from typing import Any
  5 | import random
  6 | import time
  7 | import json
  8 | 
  9 | from playwright.async_api import async_playwright, TimeoutError
 10 | from urllib.parse import urlencode, quote, urlparse
 11 | from .stealth import stealth_async
 12 | from .helpers import random_choice
 13 | 
 14 | from .api.user import User
 15 | from .api.video import Video
 16 | from .api.sound import Sound
 17 | from .api.hashtag import Hashtag
 18 | from .api.comment import Comment
 19 | from .api.trending import Trending
 20 | from .api.search import Search
 21 | from .api.playlist import Playlist
 22 | 
 23 | from .exceptions import (
 24 |     InvalidJSONException,
 25 |     EmptyResponseException,
 26 | )
 27 | 
 28 | 
 29 | @dataclasses.dataclass
 30 | class TikTokPlaywrightSession:
 31 |     """A TikTok session using Playwright"""
 32 | 
 33 |     context: Any
 34 |     page: Any
 35 |     proxy: str = None
 36 |     params: dict = None
 37 |     headers: dict = None
 38 |     ms_token: str = None
 39 |     base_url: str = "https://www.tiktok.com"
 40 | 
 41 | 
 42 | class TikTokApi:
 43 |     """The main TikTokApi class that contains all the endpoints.
 44 | 
 45 |     Import With:
 46 |         .. code-block:: python
 47 | 
 48 |             from TikTokApi import TikTokApi
 49 |             api = TikTokApi()
 50 |     """
 51 | 
 52 |     user = User
 53 |     video = Video
 54 |     sound = Sound
 55 |     hashtag = Hashtag
 56 |     comment = Comment
 57 |     trending = Trending
 58 |     search = Search
 59 |     playlist = Playlist
 60 | 
 61 |     def __init__(self, logging_level: int = logging.WARN, logger_name: str = None):
 62 |         """
 63 |         Create a TikTokApi object.
 64 | 
 65 |         Args:
 66 |             logging_level (int): The logging level you want to use.
 67 |             logger_name (str): The name of the logger you want to use.
 68 |         """
 69 |         self.sessions = []
 70 | 
 71 |         if logger_name is None:
 72 |             logger_name = __name__
 73 |         self.__create_logger(logger_name, logging_level)
 74 | 
 75 |         User.parent = self
 76 |         Video.parent = self
 77 |         Sound.parent = self
 78 |         Hashtag.parent = self
 79 |         Comment.parent = self
 80 |         Trending.parent = self
 81 |         Search.parent = self
 82 |         Playlist.parent = self
 83 | 
 84 |     def __create_logger(self, name: str, level: int = logging.DEBUG):
 85 |         """Create a logger for the class."""
 86 |         self.logger: logging.Logger = logging.getLogger(name)
 87 |         self.logger.setLevel(level)
 88 |         handler = logging.StreamHandler()
 89 |         formatter = logging.Formatter(
 90 |             "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
 91 |         )
 92 |         handler.setFormatter(formatter)
 93 |         self.logger.addHandler(handler)
 94 | 
 95 |     async def __set_session_params(self, session: TikTokPlaywrightSession):
 96 |         """Set the session params for a TikTokPlaywrightSession"""
 97 |         user_agent = await session.page.evaluate("() => navigator.userAgent")
 98 |         language = await session.page.evaluate(
 99 |             "() => navigator.language || navigator.userLanguage"
100 |         )
101 |         platform = await session.page.evaluate("() => navigator.platform")
102 |         device_id = str(random.randint(10**18, 10**19 - 1))  # Random device id
103 |         history_len = str(random.randint(1, 10))  # Random history length
104 |         screen_height = str(random.randint(600, 1080))  # Random screen height
105 |         screen_width = str(random.randint(800, 1920))  # Random screen width
106 |         timezone = await session.page.evaluate(
107 |             "() => Intl.DateTimeFormat().resolvedOptions().timeZone"
108 |         )
109 | 
110 |         session_params = {
111 |             "aid": "1988",
112 |             "app_language": language,
113 |             "app_name": "tiktok_web",
114 |             "browser_language": language,
115 |             "browser_name": "Mozilla",
116 |             "browser_online": "true",
117 |             "browser_platform": platform,
118 |             "browser_version": user_agent,
119 |             "channel": "tiktok_web",
120 |             "cookie_enabled": "true",
121 |             "device_id": device_id,
122 |             "device_platform": "web_pc",
123 |             "focus_state": "true",
124 |             "from_page": "user",
125 |             "history_len": history_len,
126 |             "is_fullscreen": "false",
127 |             "is_page_visible": "true",
128 |             "language": language,
129 |             "os": platform,
130 |             "priority_region": "",
131 |             "referer": "",
132 |             "region": "US",  # TODO: TikTokAPI option
133 |             "screen_height": screen_height,
134 |             "screen_width": screen_width,
135 |             "tz_name": timezone,
136 |             "webcast_language": language,
137 |         }
138 |         session.params = session_params
139 | 
140 |     async def __create_session(
141 |         self,
142 |         url: str = "https://www.tiktok.com",
143 |         ms_token: str = None,
144 |         proxy: str = None,
145 |         context_options: dict = {},
146 |         sleep_after: int = 1,
147 |         cookies: dict = None,
148 |         suppress_resource_load_types: list[str] = None,
149 |         timeout: int = 30000,
150 |     ):
151 |         try:
152 |             """Create a TikTokPlaywrightSession"""
153 |             if ms_token is not None:
154 |                 if cookies is None:
155 |                     cookies = {}
156 |                 cookies["msToken"] = ms_token
157 |     
158 |             context = await self.browser.new_context(proxy=proxy, **context_options)
159 |             if cookies is not None:
160 |                 formatted_cookies = [
161 |                     {"name": k, "value": v, "domain": urlparse(url).netloc, "path": "/"}
162 |                     for k, v in cookies.items()
163 |                     if v is not None
164 |                 ]
165 |                 await context.add_cookies(formatted_cookies)
166 |             page = await context.new_page()
167 |             await stealth_async(page)
168 |     
169 |             # Get the request headers to the url
170 |             request_headers = None
171 |     
172 |             def handle_request(request):
173 |                 nonlocal request_headers
174 |                 request_headers = request.headers
175 |     
176 |             page.once("request", handle_request)
177 |     
178 |             if suppress_resource_load_types is not None:
179 |                 await page.route(
180 |                     "**/*",
181 |                     lambda route, request: route.abort()
182 |                     if request.resource_type in suppress_resource_load_types
183 |                     else route.continue_(),
184 |                 )
185 |             
186 |             # Set the navigation timeout
187 |             page.set_default_navigation_timeout(timeout)
188 |     
189 |             await page.goto(url)
190 |             await page.goto(url) # hack: tiktok blocks first request not sure why, likely bot detection
191 |             
192 |             # by doing this, we are simulate scroll event using mouse to `avoid` bot detection
193 |             x, y = random.randint(0, 50), random.randint(0, 50)
194 |             a, b = random.randint(1, 50), random.randint(100, 200)
195 |     
196 |             await page.mouse.move(x, y)
197 |             await page.wait_for_load_state("networkidle")
198 |             await page.mouse.move(a, b)
199 |     
200 |             session = TikTokPlaywrightSession(
201 |                 context,
202 |                 page,
203 |                 ms_token=ms_token,
204 |                 proxy=proxy,
205 |                 headers=request_headers,
206 |                 base_url=url,
207 |             )
208 | 
209 |             if ms_token is None:
210 |                 await asyncio.sleep(sleep_after)  # TODO: Find a better way to wait for msToken
211 |                 cookies = await self.get_session_cookies(session)
212 |                 ms_token = cookies.get("msToken")
213 |                 session.ms_token = ms_token
214 |                 if ms_token is None:
215 |                     self.logger.info(
216 |                         f"Failed to get msToken on session index {len(self.sessions)}, you should consider specifying ms_tokens"
217 |                     )
218 |             self.sessions.append(session)
219 |             await self.__set_session_params(session)
220 |         except Exception as e:
221 |             # clean up
222 |             self.logger.error(f"Failed to create session: {e}")
223 |             # Cleanup resources if they were partially created
224 |             if 'page' in locals():
225 |                 await page.close()
226 |             if 'context' in locals():
227 |                 await context.close()
228 |             raise  # Re-raise the exception after cleanup
229 | 
230 |     async def create_sessions(
231 |         self,
232 |         num_sessions=5,
233 |         headless=True,
234 |         ms_tokens: list[str] = None,
235 |         proxies: list = None,
236 |         sleep_after=1,
237 |         starting_url="https://www.tiktok.com",
238 |         context_options: dict = {},
239 |         override_browser_args: list[dict] = None,
240 |         cookies: list[dict] = None,
241 |         suppress_resource_load_types: list[str] = None,
242 |         browser: str = "chromium",
243 |         executable_path: str = None,
244 |         timeout: int = 30000,
245 |     ):
246 |         """
247 |         Create sessions for use within the TikTokApi class.
248 | 
249 |         These sessions are what will carry out requesting your data from TikTok.
250 | 
251 |         Args:
252 |             num_sessions (int): The amount of sessions you want to create.
253 |             headless (bool): Whether or not you want the browser to be headless.
254 |             ms_tokens (list[str]): A list of msTokens to use for the sessions, you can get these from your cookies after visiting TikTok.
255 |                                    If you don't provide any, the sessions will try to get them themselves, but this is not guaranteed to work.
256 |             proxies (list): A list of proxies to use for the sessions
257 |             sleep_after (int): The amount of time to sleep after creating a session, this is to allow the msToken to be generated.
258 |             starting_url (str): The url to start the sessions on, this is usually https://www.tiktok.com.
259 |             context_options (dict): Options to pass to the playwright context.
260 |             override_browser_args (list[dict]): A list of dictionaries containing arguments to pass to the browser.
261 |             cookies (list[dict]): A list of cookies to use for the sessions, you can get these from your cookies after visiting TikTok.
262 |             suppress_resource_load_types (list[str]): Types of resources to suppress playwright from loading, excluding more types will make playwright faster.. Types: document, stylesheet, image, media, font, script, textrack, xhr, fetch, eventsource, websocket, manifest, other.
263 |             browser (str): firefox, chromium, or webkit; default is chromium
264 |             executable_path (str): Path to the browser executable
265 |             timeout (int): The timeout in milliseconds for page navigation
266 | 
267 |         Example Usage:
268 |             .. code-block:: python
269 | 
270 |                 from TikTokApi import TikTokApi
271 |                 with TikTokApi() as api:
272 |                     await api.create_sessions(num_sessions=5, ms_tokens=['msToken1', 'msToken2'])
273 |         """
274 |         self.playwright = await async_playwright().start()
275 |         if browser == "chromium":
276 |             if headless and override_browser_args is None:
277 |                 override_browser_args = ["--headless=new"]
278 |                 headless = False  # managed by the arg
279 |             self.browser = await self.playwright.chromium.launch(
280 |                 headless=headless, args=override_browser_args, proxy=random_choice(proxies), executable_path=executable_path
281 |             )
282 |         elif browser == "firefox":
283 |             self.browser = await self.playwright.firefox.launch(
284 |                 headless=headless, args=override_browser_args, proxy=random_choice(proxies), executable_path=executable_path
285 |             )
286 |         elif browser == "webkit":
287 |             self.browser = await self.playwright.webkit.launch(
288 |                 headless=headless, args=override_browser_args, proxy=random_choice(proxies), executable_path=executable_path
289 |             )
290 |         else:
291 |             raise ValueError("Invalid browser argument passed")
292 | 
293 |         await asyncio.gather(
294 |             *(
295 |                 self.__create_session(
296 |                     proxy=random_choice(proxies),
297 |                     ms_token=random_choice(ms_tokens),
298 |                     url=starting_url,
299 |                     context_options=context_options,
300 |                     sleep_after=sleep_after,
301 |                     cookies=random_choice(cookies),
302 |                     suppress_resource_load_types=suppress_resource_load_types,
303 |                     timeout=timeout,
304 |                 )
305 |                 for _ in range(num_sessions)
306 |             )
307 |         )
308 | 
309 |     async def close_sessions(self):
310 |         """
311 |         Close all the sessions. Should be called when you're done with the TikTokApi object
312 | 
313 |         This is called automatically when using the TikTokApi with "with"
314 |         """
315 |         for session in self.sessions:
316 |             await session.page.close()
317 |             await session.context.close()
318 |         self.sessions.clear()
319 | 
320 |         await self.browser.close()
321 |         await self.playwright.stop()
322 | 
323 |     def generate_js_fetch(self, method: str, url: str, headers: dict) -> str:
324 |         """Generate a javascript fetch function for use in playwright"""
325 |         headers_js = json.dumps(headers)
326 |         return f"""
327 |             () => {{
328 |                 return new Promise((resolve, reject) => {{
329 |                     fetch('{url}', {{ method: '{method}', headers: {headers_js} }})
330 |                         .then(response => response.text())
331 |                         .then(data => resolve(data))
332 |                         .catch(error => reject(error.message));
333 |                 }});
334 |             }}
335 |         """
336 | 
337 |     def _get_session(self, **kwargs):
338 |         """Get a random session
339 | 
340 |         Args:
341 |             session_index (int): The index of the session you want to use, if not provided a random session will be used.
342 | 
343 |         Returns:
344 |             int: The index of the session.
345 |             TikTokPlaywrightSession: The session.
346 |         """
347 |         if len(self.sessions) == 0:
348 |             raise Exception("No sessions created, please create sessions first")
349 | 
350 |         if kwargs.get("session_index") is not None:
351 |             i = kwargs["session_index"]
352 |         else:
353 |             i = random.randint(0, len(self.sessions) - 1)
354 |         return i, self.sessions[i]
355 | 
356 |     async def set_session_cookies(self, session, cookies):
357 |         """
358 |         Set the cookies for a session
359 | 
360 |         Args:
361 |             session (TikTokPlaywrightSession): The session to set the cookies for.
362 |             cookies (dict): The cookies to set for the session.
363 |         """
364 |         await session.context.add_cookies(cookies)
365 | 
366 |     async def get_session_cookies(self, session):
367 |         """
368 |         Get the cookies for a session
369 | 
370 |         Args:
371 |             session (TikTokPlaywrightSession): The session to get the cookies for.
372 | 
373 |         Returns:
374 |             dict: The cookies for the session.
375 |         """
376 |         cookies = await session.context.cookies()
377 |         return {cookie["name"]: cookie["value"] for cookie in cookies}
378 | 
379 |     async def run_fetch_script(self, url: str, headers: dict, **kwargs):
380 |         """
381 |         Execute a javascript fetch function in a session
382 | 
383 |         Args:
384 |             url (str): The url to fetch.
385 |             headers (dict): The headers to use for the fetch.
386 | 
387 |         Returns:
388 |             any: The result of the fetch. Seems to be a string or dict
389 |         """
390 |         js_script = self.generate_js_fetch("GET", url, headers)
391 |         _, session = self._get_session(**kwargs)
392 |         result = await session.page.evaluate(js_script)
393 |         return result
394 | 
395 |     async def generate_x_bogus(self, url: str, **kwargs):
396 |         """Generate the X-Bogus header for a url"""
397 |         _, session = self._get_session(**kwargs)
398 | 
399 |         max_attempts = 5
400 |         attempts = 0
401 |         while attempts < max_attempts:
402 |             attempts += 1
403 |             try:
404 |                 timeout_time = random.randint(5000, 20000)
405 |                 await session.page.wait_for_function("window.byted_acrawler !== undefined", timeout=timeout_time)
406 |                 break
407 |             except TimeoutError as e:
408 |                 if attempts == max_attempts:
409 |                     raise TimeoutError(f"Failed to load tiktok after {max_attempts} attempts, consider using a proxy")
410 |                 
411 |                 try_urls = ["https://www.tiktok.com/foryou", "https://www.tiktok.com", "https://www.tiktok.com/@tiktok", "https://www.tiktok.com/foryou"]
412 | 
413 |                 await session.page.goto(random.choice(try_urls))
414 |         
415 |         result = await session.page.evaluate(
416 |             f'() => {{ return window.byted_acrawler.frontierSign("{url}") }}'
417 |         )
418 |         return result
419 | 
420 |     async def sign_url(self, url: str, **kwargs):
421 |         """Sign a url"""
422 |         i, session = self._get_session(**kwargs)
423 | 
424 |         # TODO: Would be nice to generate msToken here
425 | 
426 |         # Add X-Bogus to url
427 |         x_bogus = (await self.generate_x_bogus(url, session_index=i)).get("X-Bogus")
428 |         if x_bogus is None:
429 |             raise Exception("Failed to generate X-Bogus")
430 | 
431 |         if "?" in url:
432 |             url += "&"
433 |         else:
434 |             url += "?"
435 |         url += f"X-Bogus={x_bogus}"
436 | 
437 |         return url
438 | 
439 |     async def make_request(
440 |         self,
441 |         url: str,
442 |         headers: dict = None,
443 |         params: dict = None,
444 |         retries: int = 3,
445 |         exponential_backoff: bool = True,
446 |         **kwargs,
447 |     ):
448 |         """
449 |         Makes a request to TikTok through a session.
450 | 
451 |         Args:
452 |             url (str): The url to make the request to.
453 |             headers (dict): The headers to use for the request.
454 |             params (dict): The params to use for the request.
455 |             retries (int): The amount of times to retry the request if it fails.
456 |             exponential_backoff (bool): Whether or not to use exponential backoff when retrying the request.
457 |             session_index (int): The index of the session you want to use, if not provided a random session will be used.
458 | 
459 |         Returns:
460 |             dict: The json response from TikTok.
461 | 
462 |         Raises:
463 |             Exception: If the request fails.
464 |         """
465 |         i, session = self._get_session(**kwargs)
466 |         if session.params is not None:
467 |             params = {**session.params, **params}
468 | 
469 |         if headers is not None:
470 |             headers = {**session.headers, **headers}
471 |         else:
472 |             headers = session.headers
473 | 
474 |         # get msToken
475 |         if params.get("msToken") is None:
476 |             # try to get msToken from session
477 |             if session.ms_token is not None:
478 |                 params["msToken"] = session.ms_token
479 |             else:
480 |                 # we'll try to read it from cookies
481 |                 cookies = await self.get_session_cookies(session)
482 |                 ms_token = cookies.get("msToken")
483 |                 if ms_token is None:
484 |                     self.logger.warn(
485 |                         "Failed to get msToken from cookies, trying to make the request anyway (probably will fail)"
486 |                     )
487 |                 params["msToken"] = ms_token
488 | 
489 |         encoded_params = f"{url}?{urlencode(params, safe='=', quote_via=quote)}"
490 |         signed_url = await self.sign_url(encoded_params, session_index=i)
491 | 
492 |         retry_count = 0
493 |         while retry_count < retries:
494 |             retry_count += 1
495 |             result = await self.run_fetch_script(
496 |                 signed_url, headers=headers, session_index=i
497 |             )
498 | 
499 |             if result is None:
500 |                 raise Exception("TikTokApi.run_fetch_script returned None")
501 | 
502 |             if result == "":
503 |                 raise EmptyResponseException(result, "TikTok returned an empty response. They are detecting you're a bot, try some of these: headless=False, browser='webkit', consider using a proxy")
504 | 
505 |             try:
506 |                 data = json.loads(result)
507 |                 if data.get("status_code") != 0:
508 |                     self.logger.error(f"Got an unexpected status code: {data}")
509 |                 return data
510 |             except json.decoder.JSONDecodeError:
511 |                 if retry_count == retries:
512 |                     self.logger.error(f"Failed to decode json response: {result}")
513 |                     raise InvalidJSONException()
514 | 
515 |                 self.logger.info(
516 |                     f"Failed a request, retrying ({retry_count}/{retries})"
517 |                 )
518 |                 if exponential_backoff:
519 |                     await asyncio.sleep(2**retry_count)
520 |                 else:
521 |                     await asyncio.sleep(1)
522 | 
523 |     async def close_sessions(self):
524 |         """Close all the sessions. Should be called when you're done with the TikTokApi object"""
525 |         for session in self.sessions:
526 |             await session.page.close()
527 |             await session.context.close()
528 |         self.sessions.clear()
529 | 
530 |     async def stop_playwright(self):
531 |         """Stop the playwright browser"""
532 |         await self.browser.close()
533 |         await self.playwright.stop()
534 | 
535 |     async def get_session_content(self, url: str, **kwargs):
536 |         """Get the content of a url"""
537 |         _, session = self._get_session(**kwargs)
538 |         return await session.page.content()
539 | 
540 |     async def __aenter__(self):
541 |         return self
542 | 
543 |     async def __aexit__(self, exc_type, exc, tb):
544 |         await self.close_sessions()
545 |         await self.stop_playwright()
546 | 


--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman


--------------------------------------------------------------------------------
/examples/comment_example.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import asyncio
 3 | import os
 4 | 
 5 | video_id = 7248300636498890011
 6 | ms_token = os.environ.get("ms_token", None)  # set your own ms_token
 7 | 
 8 | 
 9 | async def get_comments():
10 |     async with TikTokApi() as api:
11 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"))
12 |         video = api.video(id=video_id)
13 |         count = 0
14 |         async for comment in video.comments(count=30):
15 |             print(comment)
16 |             print(comment.as_dict)
17 | 
18 | 
19 | if __name__ == "__main__":
20 |     asyncio.run(get_comments())
21 | 


--------------------------------------------------------------------------------
/examples/hashtag_example.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import asyncio
 3 | import os
 4 | 
 5 | ms_token = os.environ.get("ms_token", None)  # set your own ms_token
 6 | 
 7 | 
 8 | async def get_hashtag_videos():
 9 |     async with TikTokApi() as api:
10 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"))
11 |         tag = api.hashtag(name="funny")
12 |         async for video in tag.videos(count=30):
13 |             print(video)
14 |             print(video.as_dict)
15 | 
16 | 
17 | if __name__ == "__main__":
18 |     asyncio.run(get_hashtag_videos())
19 | 


--------------------------------------------------------------------------------
/examples/playlist_example.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import asyncio
 3 | import os
 4 | 
 5 | ms_token = os.environ.get(
 6 |     "ms_token", None
 7 | )  # set your own ms_token, think it might need to have visited a profile
 8 | 
 9 | 
10 | async def user_example():
11 |     async with TikTokApi() as api:
12 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3)
13 |         user = api.user("therock")
14 | 
15 |         async for playlist in user.playlists(count=3):
16 |             print(playlist)
17 |             print(playlist.name)
18 | 
19 |             async for video in playlist.videos(count=3):
20 |                 print(video)
21 |                 print(video.url)
22 | 
23 | if __name__ == "__main__":
24 |     asyncio.run(user_example())
25 | 


--------------------------------------------------------------------------------
/examples/search_example.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import asyncio
 3 | import os
 4 | 
 5 | ms_token = os.environ.get(
 6 |     "ms_token", None
 7 | )  # set your own ms_token, needs to have done a search before for this to work
 8 | 
 9 | 
10 | async def search_users():
11 |     async with TikTokApi() as api:
12 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"))
13 |         async for user in api.search.users("david teather", count=10):
14 |             print(user)
15 | 
16 | 
17 | if __name__ == "__main__":
18 |     asyncio.run(search_users())
19 | 


--------------------------------------------------------------------------------
/examples/sound_example.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import asyncio
 3 | import os
 4 | 
 5 | ms_token = os.environ.get("ms_token", None)  # set your own ms_token
 6 | sound_id = "7016547803243022337"
 7 | 
 8 | 
 9 | async def sound_videos():
10 |     async with TikTokApi() as api:
11 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"))
12 |         async for sound in api.sound(id=sound_id).videos(count=30):
13 |             print(sound)
14 |             print(sound.as_dict)
15 | 
16 | 
17 | if __name__ == "__main__":
18 |     asyncio.run(sound_videos())
19 | 


--------------------------------------------------------------------------------
/examples/trending_example.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import asyncio
 3 | import os
 4 | 
 5 | ms_token = os.environ.get("ms_token", None)  # set your own ms_token
 6 | 
 7 | 
 8 | async def trending_videos():
 9 |     async with TikTokApi() as api:
10 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"))
11 |         async for video in api.trending.videos(count=30):
12 |             print(video)
13 |             print(video.as_dict)
14 | 
15 | 
16 | if __name__ == "__main__":
17 |     asyncio.run(trending_videos())
18 | 


--------------------------------------------------------------------------------
/examples/user_example.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import asyncio
 3 | import os
 4 | 
 5 | ms_token = os.environ.get(
 6 |     "ms_token", None
 7 | )  # set your own ms_token, think it might need to have visited a profile
 8 | 
 9 | 
10 | async def user_example():
11 |     async with TikTokApi() as api:
12 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"))
13 |         user = api.user("therock")
14 |         user_data = await user.info()
15 |         print(user_data)
16 | 
17 |         async for video in user.videos(count=30):
18 |             print(video)
19 |             print(video.as_dict)
20 | 
21 |         async for playlist in user.playlists():
22 |             print(playlist)
23 | 
24 | 
25 | if __name__ == "__main__":
26 |     asyncio.run(user_example())
27 | 


--------------------------------------------------------------------------------
/examples/video_example.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import asyncio
 3 | import os
 4 | 
 5 | ms_token = os.environ.get(
 6 |     "ms_token", None
 7 | )  # set your own ms_token, think it might need to have visited a profile
 8 | 
 9 | 
10 | async def get_video_example():
11 |     async with TikTokApi() as api:
12 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"))
13 |         video = api.video(
14 |             url="https://www.tiktok.com/@davidteathercodes/video/7074717081563942186"
15 |         )
16 | 
17 |         async for related_video in video.related_videos(count=10):
18 |             print(related_video)
19 |             print(related_video.as_dict)
20 | 
21 |         video_info = await video.info()  # is HTML request, so avoid using this too much
22 |         print(video_info)
23 |         video_bytes = await video.bytes()
24 |         with open("video.mp4", "wb") as f:
25 |             f.write(video_bytes)
26 | 
27 | 
28 | if __name__ == "__main__":
29 |     asyncio.run(get_video_example())
30 | 


--------------------------------------------------------------------------------
/imgs/EnsembleData.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidteather/TikTok-Api/62a8cfa8ab7bb5bbdd0f8c8b13e84731fff7ac75/imgs/EnsembleData.png


--------------------------------------------------------------------------------
/imgs/tikapi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidteather/TikTok-Api/62a8cfa8ab7bb5bbdd0f8c8b13e84731fff7ac75/imgs/tikapi.png


--------------------------------------------------------------------------------
/imgs/tiktok_captcha_solver.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidteather/TikTok-Api/62a8cfa8ab7bb5bbdd0f8c8b13e84731fff7ac75/imgs/tiktok_captcha_solver.png


--------------------------------------------------------------------------------
/imgs/webshare.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidteather/TikTok-Api/62a8cfa8ab7bb5bbdd0f8c8b13e84731fff7ac75/imgs/webshare.png


--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests>=2.31.0,<3.0
2 | playwright>=1.36.0,<2.0
3 | httpx>=0.27.0,<1.0


--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 | 
4 | [flake8]
5 | max-line-length = 120
6 | 


--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
 1 | from distutils.core import setup
 2 | import os.path
 3 | import setuptools
 4 | 
 5 | with open("README.md", "r", encoding="utf-8") as fh:
 6 |     long_description = fh.read()
 7 | 
 8 | setuptools.setup(
 9 |     name="TikTokApi",
10 |     packages=setuptools.find_packages(),
11 |     version="7.1.0",
12 |     license="MIT",
13 |     description="The Unofficial TikTok API Wrapper in Python 3.",
14 |     author="David Teather",
15 |     author_email="contact.davidteather@gmail.com",
16 |     url="https://github.com/davidteather/tiktok-api",
17 |     long_description=long_description,
18 |     long_description_content_type="text/markdown",
19 |     download_url="https://github.com/davidteather/TikTok-Api/tarball/main",
20 |     keywords=["tiktok", "python3", "api", "unofficial", "tiktok-api", "tiktok api"],
21 |     install_requires=["requests", "playwright", "httpx"],
22 |     classifiers=[
23 |         "Development Status :: 4 - Beta",
24 |         "Intended Audience :: Developers",
25 |         "Topic :: Software Development :: Build Tools",
26 |         "License :: OSI Approved :: MIT License",
27 |         "Programming Language :: Python :: 3.9",
28 |         "Programming Language :: Python :: 3.10",
29 |         "Programming Language :: Python :: 3.11",
30 |         "Programming Language :: Python :: 3.12",
31 |         "Programming Language :: Python :: 3.13",
32 |     ],
33 |     python_requires=">=3.9",
34 | )
35 | 


--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidteather/TikTok-Api/62a8cfa8ab7bb5bbdd0f8c8b13e84731fff7ac75/tests/__init__.py


--------------------------------------------------------------------------------
/tests/test_comments.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import os
 3 | import pytest
 4 | 
 5 | video_id = 7248300636498890011
 6 | ms_token = os.environ.get("ms_token", None)
 7 | headless = os.environ.get("headless", "True").lower() == "true"
 8 | 
 9 | 
10 | @pytest.mark.asyncio
11 | async def test_comment_page():
12 |     api = TikTokApi()
13 |     async with api:
14 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
15 |         video = api.video(id=video_id)
16 |         count = 0
17 |         async for comment in video.comments(count=100):
18 |             count += 1
19 | 
20 |         assert count >= 100
21 | 


--------------------------------------------------------------------------------
/tests/test_hashtag.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import os
 3 | import logging
 4 | import pytest
 5 | 
 6 | ms_token = os.environ.get("ms_token", None)
 7 | headless = os.environ.get("headless", "True").lower() == "true"
 8 | 
 9 | 
10 | @pytest.mark.asyncio
11 | async def test_hashtag_videos():
12 |     api = TikTokApi(logging_level=logging.INFO)
13 |     async with api:
14 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
15 |         tag = api.hashtag(name="funny")
16 |         video_count = 0
17 |         async for video in tag.videos(count=30):
18 |             video_count += 1
19 | 
20 |         assert video_count >= 30
21 | 
22 | 
23 | @pytest.mark.asyncio
24 | async def test_hashtag_videos_multi_page():
25 |     api = TikTokApi(logging_level=logging.INFO)
26 |     async with api:
27 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
28 |         tag = api.hashtag(name="funny", id="5424")
29 |         video_count = 0
30 |         async for video in tag.videos(count=100):
31 |             video_count += 1
32 | 
33 |         assert video_count >= 30
34 | 
35 | 
36 | @pytest.mark.asyncio
37 | async def test_hashtag_info():
38 |     api = TikTokApi(logging_level=logging.INFO)
39 |     async with api:
40 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
41 |         tag = api.hashtag(name="funny")
42 |         await tag.info()
43 | 
44 |         assert tag.id == "5424"
45 |         assert tag.name == "funny"
46 | 
47 | 
48 | @pytest.mark.asyncio
49 | async def test_non_latin1():
50 |     api = TikTokApi(logging_level=logging.INFO)
51 |     async with api:
52 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
53 |         tag = api.hashtag(name="селфи")
54 |         await tag.info()
55 | 
56 |         assert tag.name == "селфи"
57 |         assert tag.id == "4385126"
58 | 


--------------------------------------------------------------------------------
/tests/test_integration.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import os
 3 | import pytest
 4 | 
 5 | ms_token = os.environ.get("ms_token", None)
 6 | headless = os.environ.get("headless", "True").lower() == "true"
 7 | 
 8 | 
 9 | @pytest.mark.asyncio
10 | async def test_hashtag_videos():
11 |     async with TikTokApi() as api:
12 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
13 |         tag_name = "funny"
14 |         count = 0
15 |         async for video in api.hashtag(name=tag_name).videos(count=1):
16 |             count += 1
17 |             tag_included = False
18 |             for tag in video.hashtags:
19 |                 if tag.name == tag_name:
20 |                     tag_included = True
21 | 
22 |             assert tag_included
23 | 
24 |             # Test sound on video.
25 |             assert video.sound is not None
26 |             assert video.sound.id is not None
27 | 
28 |             # Test author.
29 |             assert video.author is not None
30 |             assert video.author.user_id is not None
31 |             assert video.author.sec_uid is not None
32 | 
33 |         assert count > 0
34 | 


--------------------------------------------------------------------------------
/tests/test_playlist.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import os
 3 | import pytest
 4 | 
 5 | playlist_id="7281443725770476321"
 6 | playlist_name="Doctor Who"
 7 | playlist_creator="bbc"
 8 | 
 9 | ms_token = os.environ.get("ms_token", None)
10 | headless = os.environ.get("headless", "True").lower() == "true"
11 | 
12 | 
13 | @pytest.mark.asyncio
14 | async def test_playlist_info():
15 |     api = TikTokApi()
16 |     async with api:
17 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, headless=headless)
18 |         playlist = api.playlist(id=playlist_id)
19 |         await playlist.info()
20 | 
21 |         assert playlist.id == playlist_id
22 |         assert playlist.name == playlist_name
23 |         assert playlist.creator.username == playlist_creator
24 |         assert playlist.video_count > 0
25 |         assert playlist.cover_url is not None
26 |         assert playlist.as_dict is not None
27 | 
28 | @pytest.mark.asyncio
29 | async def test_playlist_videos():
30 |     api = TikTokApi()
31 |     async with api:
32 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, headless=headless)
33 |         playlist = api.playlist(id=playlist_id)
34 | 
35 |         count = 0
36 |         async for video in playlist.videos(count=30):
37 |             count += 1
38 | 
39 |         assert count >= 30
40 | 


--------------------------------------------------------------------------------
/tests/test_search.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import os
 3 | import pytest
 4 | 
 5 | ms_token = os.environ.get("ms_token", None)
 6 | headless = os.environ.get("headless", "True").lower() == "true"
 7 | 
 8 | @pytest.mark.asyncio
 9 | async def test_users_single_page():
10 |     api = TikTokApi()
11 |     async with api:
12 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
13 |         count = 0
14 |         async for user in api.search.users("therock", count=10):
15 |             count += 1
16 | 
17 |         assert count >= 10
18 | 
19 | #@pytest.mark.asyncio
20 | @pytest.mark.skip(reason="Known issue, see #1088 (https://github.com/davidteather/TikTok-Api/issues/1088)")
21 | async def test_users_multi_page():
22 |     api = TikTokApi()
23 |     async with api:
24 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
25 |         count = 0
26 |         async for user in api.search.users("therock", count=50):
27 |             count += 1
28 | 
29 |         assert count >= 50
30 | 


--------------------------------------------------------------------------------
/tests/test_sound.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import os
 3 | import pytest
 4 | 
 5 | ms_token = os.environ.get("ms_token", None)
 6 | headless = os.environ.get("headless", "True").lower() == "true"
 7 | song_id = "7016547803243022337"
 8 | 
 9 | 
10 | @pytest.mark.asyncio
11 | async def test_sound_videos():
12 |     api = TikTokApi()
13 |     async with api:
14 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
15 |         sound = api.sound(id=song_id)
16 |         video_count = 0
17 |         async for video in sound.videos(count=100):
18 |             video_count += 1
19 | 
20 |         assert video_count >= 100
21 | 
22 | 
23 | @pytest.mark.asyncio
24 | async def test_sound_info():
25 |     api = TikTokApi()
26 |     async with api:
27 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
28 |         sound = api.sound(id=song_id)
29 |         await sound.info()
30 |         assert sound.id == song_id
31 |         assert sound.title == "Face Off - Dwayne Johnson"
32 |         assert sound.duration == 60
33 | 


--------------------------------------------------------------------------------
/tests/test_trending.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import os
 3 | import pytest
 4 | 
 5 | ms_token = os.environ.get("ms_token", None)
 6 | headless = os.environ.get("headless", "True").lower() == "true"
 7 | 
 8 | 
 9 | @pytest.mark.asyncio
10 | async def test_trending():
11 |     api = TikTokApi()
12 |     async with api:
13 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
14 |         count = 0
15 |         async for video in api.trending.videos(count=100):
16 |             count += 1
17 | 
18 |         assert count >= 100
19 | 


--------------------------------------------------------------------------------
/tests/test_user.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import os
 3 | import pytest
 4 | 
 5 | username = "charlidamelio"
 6 | user_id = "5831967"
 7 | sec_uid = "MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s"
 8 | 
 9 | ms_token = os.environ.get("ms_token", None)
10 | headless = os.environ.get("headless", "True").lower() == "true"
11 | 
12 | 
13 | @pytest.mark.asyncio
14 | async def test_user_info():
15 |     api = TikTokApi()
16 |     async with api:
17 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
18 |         user = api.user(username=username)
19 |         await user.info()
20 | 
21 |         assert user.username == username
22 |         assert user.user_id == user_id
23 |         assert user.sec_uid == sec_uid
24 | 
25 | @pytest.mark.asyncio
26 | async def test_user_videos():
27 |     api = TikTokApi()
28 |     async with api:
29 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
30 |         user = api.user(username=username, sec_uid=sec_uid, user_id=user_id)
31 | 
32 |         count = 0
33 |         async for video in user.videos(count=30):
34 |             count += 1
35 | 
36 |         assert count >= 30
37 | 
38 | @pytest.mark.asyncio
39 | async def test_user_likes():
40 |     api = TikTokApi()
41 |     async with api:
42 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
43 |         user = api.user(
44 |             username="publicliketest",
45 |             sec_uid="MS4wLjABAAAAHjhwCIwmvzVZfRrDAZ2aZy74LciLnoyaPfM2rrX9N7bwbWMFuwTFG4YrByYvsH5c",
46 |         )
47 | 
48 |         count = 0
49 |         async for video in user.liked(count=30):
50 |             count += 1
51 | 
52 |         assert count >= 30
53 | 
54 | @pytest.mark.asyncio
55 | async def test_user_playlists():
56 |     api = TikTokApi()
57 |     async with api:
58 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
59 |         user = api.user(username="mrbeast")
60 | 
61 |         count = 0
62 |         async for playlist in user.playlists(count=5):
63 |             count += 1
64 |         
65 |         assert count >= 5
66 | 


--------------------------------------------------------------------------------
/tests/test_video.py:
--------------------------------------------------------------------------------
 1 | from TikTokApi import TikTokApi
 2 | import os
 3 | import pytest
 4 | 
 5 | ms_token = os.environ.get("ms_token", None)
 6 | headless = os.environ.get("headless", "True").lower() == "true"
 7 | 
 8 | 
 9 | @pytest.mark.asyncio
10 | async def test_video_id_from_url():
11 |     api = TikTokApi()
12 |     async with api:
13 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
14 | 
15 |         expected_id = "7074717081563942186"
16 |         video = api.video(
17 |             url="https://www.tiktok.com/@davidteathercodes/video/7074717081563942186"
18 |         )
19 | 
20 |         assert video.id == expected_id
21 | 
22 |         # mobile_url = "https://www.tiktok.com/t/ZT8LCfcUC/"
23 |         # video = api.video(url=mobile_url)
24 | 
25 |         # assert video.id == expected_id
26 | 
27 | 
28 | @pytest.mark.asyncio
29 | async def test_video_info():
30 |     api = TikTokApi()
31 |     async with api:
32 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
33 |         video_id = "7074717081563942186"
34 |         video = api.video(
35 |             url="https://www.tiktok.com/@davidteathercodes/video/7074717081563942186"
36 |         )
37 | 
38 |         data = await video.info()
39 | 
40 |         assert data["id"] == video_id
41 |         video.author.username = "davidteathercodes"
42 | 
43 | 
44 | @pytest.mark.asyncio
45 | async def test_video_bytes():
46 |     pytest.skip("Not implemented yet")
47 |     api = TikTokApi()
48 |     async with api:
49 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
50 |         video_id = "7107272719166901550"
51 |         video = api.video(id=video_id)
52 | 
53 |         data = await video.bytes()
54 |         assert len(data) > 10000
55 | 
56 | 
57 | @pytest.mark.asyncio
58 | async def test_related_videos():
59 |     api = TikTokApi()
60 |     async with api:
61 |         await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium"), headless=headless)
62 |         video_id = "7107272719166901550"
63 |         video = api.video(id=video_id)
64 |         count = 0
65 |         async for related_video in video.related_videos(count=10):
66 |             print(related_video)
67 |             count += 1
68 | 
69 |         assert count >= 10
70 | 


--------------------------------------------------------------------------------