Full Code of mixmoe/HibiAPI for AI

main ada5d2205b4f cached
100 files
201.0 KB
54.2k tokens
477 symbols
1 requests
Download .txt
Showing preview only (227K chars total). Download the full file or copy to clipboard to get everything.
Repository: mixmoe/HibiAPI
Branch: main
Commit: ada5d2205b4f
Files: 100
Total size: 201.0 KB

Directory structure:
gitextract_8q_i_0ls/

├── .all-contributorsrc
├── .flake8
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── docker.yml
│       ├── lint.yml
│       ├── mirror.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .replit
├── .vscode/
│   ├── docstring.mustache
│   ├── extensions.json
│   ├── launch.json
│   └── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── hibiapi/
│   ├── __init__.py
│   ├── __main__.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── bika/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   ├── constants.py
│   │   │   └── net.py
│   │   ├── bilibili/
│   │   │   ├── __init__.py
│   │   │   ├── api/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── base.py
│   │   │   │   ├── v2.py
│   │   │   │   └── v3.py
│   │   │   ├── constants.py
│   │   │   └── net.py
│   │   ├── netease/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   ├── constants.py
│   │   │   └── net.py
│   │   ├── pixiv/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   ├── constants.py
│   │   │   └── net.py
│   │   ├── qrcode.py
│   │   ├── sauce/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   ├── constants.py
│   │   │   └── net.py
│   │   ├── tieba/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   └── net.py
│   │   └── wallpaper/
│   │       ├── __init__.py
│   │       ├── api.py
│   │       ├── constants.py
│   │       └── net.py
│   ├── app/
│   │   ├── __init__.py
│   │   ├── application.py
│   │   ├── handlers.py
│   │   ├── middlewares.py
│   │   └── routes/
│   │       ├── __init__.py
│   │       ├── bika.py
│   │       ├── bilibili/
│   │       │   ├── __init__.py
│   │       │   ├── v2.py
│   │       │   └── v3.py
│   │       ├── netease.py
│   │       ├── pixiv.py
│   │       ├── qrcode.py
│   │       ├── sauce.py
│   │       ├── tieba.py
│   │       └── wallpaper.py
│   ├── configs/
│   │   ├── bika.yml
│   │   ├── bilibili.yml
│   │   ├── general.yml
│   │   ├── netease.yml
│   │   ├── pixiv.yml
│   │   ├── qrcode.yml
│   │   ├── sauce.yml
│   │   ├── tieba.yml
│   │   └── wallpaper.yml
│   └── utils/
│       ├── __init__.py
│       ├── cache.py
│       ├── config.py
│       ├── decorators/
│       │   ├── __init__.py
│       │   ├── enum.py
│       │   └── timer.py
│       ├── exceptions.py
│       ├── log.py
│       ├── net.py
│       ├── routing.py
│       └── temp.py
├── pyproject.toml
├── scripts/
│   └── pixiv_login.py
└── test/
    ├── __init__.py
    ├── test_base.py
    ├── test_bika.py
    ├── test_bilibili_v2.py
    ├── test_bilibili_v3.py
    ├── test_netease.py
    ├── test_pixiv.py
    ├── test_qrcode.py
    ├── test_sauce.py
    ├── test_tieba.py
    └── test_wallpaper.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .all-contributorsrc
================================================
{
  "files": [
    "README.md"
  ],
  "imageSize": 100,
  "commit": false,
  "contributors": [
    {
      "login": "Kyomotoi",
      "name": "Kyomotoi",
      "avatar_url": "https://avatars.githubusercontent.com/u/37587870?v=4",
      "profile": "http://kyomotoi.moe",
      "contributions": [
        "doc",
        "test"
      ]
    },
    {
      "login": "shirokurakana",
      "name": "城倉奏",
      "avatar_url": "https://avatars.githubusercontent.com/u/46120251?v=4",
      "profile": "http://thdog.moe",
      "contributions": [
        "example"
      ]
    },
    {
      "login": "SkipM4",
      "name": "SkipM4",
      "avatar_url": "https://avatars.githubusercontent.com/u/40311581?v=4",
      "profile": "http://skipm4.com",
      "contributions": [
        "doc"
      ]
    },
    {
      "login": "leaf7th",
      "name": "Nook",
      "avatar_url": "https://avatars.githubusercontent.com/u/38352552?v=4",
      "profile": "https://github.com/leaf7th",
      "contributions": [
        "code"
      ]
    },
    {
      "login": "jiangzhuochi",
      "name": "Jocky Chiang",
      "avatar_url": "https://avatars.githubusercontent.com/u/50538375?v=4",
      "profile": "https://github.com/jiangzhuochi",
      "contributions": [
        "code"
      ]
    },
    {
      "login": "cleoold",
      "name": "midori",
      "avatar_url": "https://avatars.githubusercontent.com/u/13920903?v=4",
      "profile": "https://github.com/cleoold",
      "contributions": [
        "doc"
      ]
    },
    {
      "login": "Pretty9",
      "name": "Pretty9",
      "avatar_url": "https://avatars.githubusercontent.com/u/41198038?v=4",
      "profile": "https://www.2yo.cc",
      "contributions": [
        "code"
      ]
    },
    {
      "login": "journey-ad",
      "name": "Jad",
      "avatar_url": "https://avatars.githubusercontent.com/u/16256221?v=4",
      "profile": "https://nocilol.me/",
      "contributions": [
        "bug",
        "ideas"
      ]
    },
    {
      "login": "asadahimeka",
      "name": "Yumine Sakura",
      "avatar_url": "https://avatars.githubusercontent.com/u/31837214?v=4",
      "profile": "http://nanoka.top",
      "contributions": [
        "code"
      ]
    },
    {
      "login": "yeyang52",
      "name": "yeyang",
      "avatar_url": "https://avatars.githubusercontent.com/u/107110851?v=4",
      "profile": "https://github.com/yeyang52",
      "contributions": [
        "code"
      ]
    }
  ],
  "contributorsPerLine": 7,
  "projectName": "HibiAPI",
  "projectOwner": "mixmoe",
  "repoType": "github",
  "repoHost": "https://github.com",
  "skipCi": true,
  "commitConvention": "none"
}


================================================
FILE: .flake8
================================================
[flake8]
max-line-length = 90
ignore = W391, W292, W503, E203

================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
  - package-ecosystem: pip # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: weekly
    versioning-strategy: lockfile-only

  - package-ecosystem: github-actions
    directory: "/"
    schedule:
      interval: weekly


================================================
FILE: .github/workflows/docker.yml
================================================
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Docker

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build-and-push-image:
    name: Build and Push Image

    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Log in to the Container registry
        uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@c4ee3adeed93b1fa6a762f209fb01608c1a22f1e
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}


================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint

on:
  push:
    branches: [main, dev]

  pull_request_target:

jobs:
  lint:
    runs-on: ubuntu-latest
    name: Lint Code

    steps:
      - uses: actions/checkout@v4

      - uses: pdm-project/setup-pdm@v4
        with:
          python-version: "3.9"
          cache: true

      - name: Install dependencies
        run: |
          pdm install -G :all
          echo `dirname $(pdm info --python)` >> $GITHUB_PATH

      - name: Lint with Ruff
        continue-on-error: true
        run: pdm lint --output-format github

      - name: Lint with Pyright
        uses: jakebailey/pyright-action@v2
        continue-on-error: true
        with:
          pylance-version: latest-release

  analyze:
    runs-on: ubuntu-latest
    name: CodeQL Analyze

    if: startsWith(github.ref, 'refs/heads/')

    steps:
      - uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v2
        with:
          languages: python

      - name: Auto build
        uses: github/codeql-action/autobuild@v2

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v2


================================================
FILE: .github/workflows/mirror.yml
================================================
name: Gitee Mirror

on:
  push:
    branches: [main, dev]

  schedule:
    - cron: "0 0 * * *"

  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: true

jobs:
  git-mirror:
    name: Mirror
    runs-on: ubuntu-latest

    steps:
      - uses: wearerequired/git-mirror-action@v1
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
        with:
          source-repo: "git@github.com:mixmoe/HibiAPI.git"
          destination-repo: "git@gitee.com:mixmoe/HibiAPI.git"


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  create:
    tags: [v*]
  workflow_dispatch:

jobs:
  release:
    name: Create release
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - uses: pdm-project/setup-pdm@v3
        with:
          python-version: 3.9

      - name: Install Dependencies
        run: |
          pdm install --prod

      - name: Release to PyPI
        env:
          PDM_PUBLISH_USERNAME: __token__
          PDM_PUBLISH_PASSWORD: ${{ secrets.PYPI_TOKEN }}
        run: |
          pdm publish


================================================
FILE: .github/workflows/test.yml
================================================
name: Test

on:
  workflow_dispatch:

  push:
    branches: [main, dev]

  pull_request_target:

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false

jobs:
  cloc:
    runs-on: ubuntu-latest
    name: Count Lines of Code

    steps:
      - uses: actions/checkout@v3

      - name: Install CLoC
        run: |
          sudo apt-get update
          sudo apt-get install cloc

      - name: Count Lines of Code
        run: |
          cloc . --md >> $GITHUB_STEP_SUMMARY

  test:
    runs-on: ${{ matrix.os }}
    name: Testing

    strategy:
      matrix:
        python: ["3.9", "3.10", "3.11", "3.12"]
        os: [ubuntu-latest, windows-latest, macos-latest]
      max-parallel: 3

    defaults:
      run:
        shell: bash

    env:
      OS: ${{ matrix.os }}
      PYTHON: ${{ matrix.python }}

    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}

      - uses: pdm-project/setup-pdm@v3
        with:
          python-version: ${{ matrix.python }}
          cache: true

      - name: Install dependencies
        timeout-minutes: 5
        run: pdm install

      - name: Testing with pytest
        timeout-minutes: 15
        run: |
          curl -L ${{ secrets.DOTENV_LINK }} > .env
          pdm test

      - name: Create step summary
        if: always()
        run: |
          echo "## Summary" >> $GITHUB_STEP_SUMMARY
          echo "OS: ${{ matrix.os }}" >> $GITHUB_STEP_SUMMARY
          echo "Python: ${{ matrix.python }}" >> $GITHUB_STEP_SUMMARY
          echo '```' >> $GITHUB_STEP_SUMMARY
          pdm run coverage report -m >> $GITHUB_STEP_SUMMARY
          echo '```' >> $GITHUB_STEP_SUMMARY

      - uses: codecov/codecov-action@v3
        if: always()
        with:
          env_vars: OS,PYTHON
          file: coverage.xml


================================================
FILE: .gitignore
================================================
# Project ignore
data/**
configs/**.yml

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/#use-with-ide
.pdm.toml
.pdm-python

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/


================================================
FILE: .replit
================================================
language = "python3"
run = "python -m hibiapi run"

================================================
FILE: .vscode/docstring.mustache
================================================
{{! FastAPI Automatic docstring}}

## Name: `{{name}}`

> {{summaryPlaceholder}} {{extendedSummaryPlaceholder}}
{{#argsExist}}

---

### Required:

{{#args}}
- ***{{typePlaceholder}}*** **`{{var}}`** 
    - Description: {{descriptionPlaceholder}}
{{/args}}
{{/argsExist}}
{{#kwargsExist}}

---

### Optional:
{{#kwargs}}
- ***{{typePlaceholder}}*** `{{var}}` = `{{default}}`
    - Description: {{descriptionPlaceholder}}
{{/kwargs}}
{{/kwargsExist}}
{{#exceptionsExist}}

---

### Exceptions:

{{#exceptions}}
- **`{{var}}`** 
    - Description: {{descriptionPlaceholder}}
{{/exceptions}}
{{/exceptionsExist}}
{{#yieldsExist}}

---

### Yields:
{{#yields}}
- `{{typePlaceholder}}`
    - Description: {{descriptionPlaceholder}}
{{/yields}}
{{/yieldsExist}}
{{#returnsExist}}

---

### Returns:

{{#returns}}
- `{{typePlaceholder}}`
    - Description: {{descriptionPlaceholder}}
{{/returns}}
{{/returnsExist}}

================================================
FILE: .vscode/extensions.json
================================================
{
    "recommendations": [
        "visualstudioexptteam.vscodeintellicode",
        "ms-python.python",
        "ms-python.vscode-pylance",
        "njpwerner.autodocstring",
        "streetsidesoftware.code-spell-checker",
        "redhat.vscode-yaml",
        "seatonjiang.gitmoji-vscode"
    ]
}

================================================
FILE: .vscode/launch.json
================================================
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Module",
            "type": "python",
            "request": "launch",
            "module": "hibiapi",
            "args": [
                "run",
                "--reload"
            ],
            "justMyCode": true
        },
    ]
}

================================================
FILE: .vscode/settings.json
================================================
{
    "python.analysis.completeFunctionParens": true,
    "python.analysis.typeCheckingMode": "basic",
    "python.languageServer": "Pylance",
    "python.testing.pytestEnabled": true,
    "[python]": {
        "editor.codeActionsOnSave": {
            "source.organizeImports": "explicit",
            "source.fixAll": "explicit"
        }
    },
    "autoDocstring.customTemplatePath": ".vscode/docstring.mustache",
    "editor.formatOnSave": true,
    "files.watcherExclude": {
        "**/.git/objects/**": true,
        "**/.git/subtree-cache/**": true,
        "**/node_modules/**": true,
        "**/.hg/store/**": true,
        "**/.venv/**": true,
        "**/.mypy_cache/**": true
    },
    "files.encoding": "utf8",
    "python.analysis.diagnosticMode": "workspace",
    "cSpell.words": [
        "Bilibili",
        "DOUGA",
        "GUOCHUANG",
        "Hibi",
        "Imjad",
        "KICHIKU",
        "Pixiv",
        "RGBA",
        "Tieba",
        "aclose",
        "aenter",
        "aexit",
        "aiocache",
        "asyncio",
        "bangumi",
        "bgcolor",
        "dotenv",
        "favlist",
        "fgcolor",
        "fnmatch",
        "getrgb",
        "hibiapi",
        "httpx",
        "illusts",
        "iscoroutinefunction",
        "itertools",
        "levelno",
        "mixmoe",
        "mypy",
        "noqa",
        "proto",
        "pydantic",
        "pytest",
        "qrcode",
        "redoc",
        "referer",
        "rfind",
        "rsplit",
        "starlette",
        "ugoira",
        "uvicorn",
        "vmid",
        "weapi"
    ],
    "gitmoji.outputType": "code",
    "python.analysis.autoImportCompletions": true
}

================================================
FILE: Dockerfile
================================================
FROM python:bullseye

EXPOSE 8080

ENV PORT=8080 \
    PROCS=1 \
    GENERAL_SERVER_HOST=0.0.0.0

COPY . /hibi

WORKDIR /hibi

RUN pip install .

CMD hibiapi run --port $PORT --workers $PROCS

HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
    CMD httpx --verbose --follow-redirects http://127.0.0.1:${PORT}

================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2020-2021 Mix Technology

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
<!-- spell-checker: disable -->
<!-- markdownlint-disable MD033 MD041 -->

<img src=".github/logo.svg" align="right">

<div align="left">

# HibiAPI

**_一个实现了多种常用站点的易用化 API 的程序._**

**_A program that implements easy-to-use APIs for a variety of commonly used sites._**

[![Demo Version](https://img.shields.io/badge/dynamic/json?label=demo%20status&query=%24.info.version&url=https%3A%2F%2Fapi.obfs.dev%2Fopenapi.json&style=for-the-badge&color=lightblue)](https://api.obfs.dev)

![Lint](https://github.com/mixmoe/HibiAPI/workflows/Lint/badge.svg)
![Test](https://github.com/mixmoe/HibiAPI/workflows/Test/badge.svg)
[![Coverage](https://codecov.io/gh/mixmoe/HibiAPI/branch/main/graph/badge.svg)](https://codecov.io/gh/mixmoe/HibiAPI)

[![PyPI](https://img.shields.io/pypi/v/hibiapi)](https://pypi.org/project/hibiapi/)
![PyPI - Downloads](https://img.shields.io/pypi/dm/hibiapi)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/hibiapi)
![PyPI - License](https://img.shields.io/pypi/l/hibiapi)

![GitHub last commit](https://img.shields.io/github/last-commit/mixmoe/HibiAPI)
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/mixmoe/hibiapi)
![Lines of code](https://img.shields.io/tokei/lines/github/mixmoe/hibiapi)
[![GitHub stars](https://img.shields.io/github/stars/mixmoe/HibiAPI)](https://github.com/mixmoe/HibiAPI/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/mixmoe/HibiAPI)](https://github.com/mixmoe/HibiAPI/network)
[![GitHub issues](https://img.shields.io/github/issues/mixmoe/HibiAPI)](https://github.com/mixmoe/HibiAPI/issues)

</div>

---

## 前言

- `HibiAPI`提供多种网站公开内容的 API 集合, 它们包括:

  - Pixiv 的图片和小说相关信息获取和搜索
  - Bilibili 的视频/番剧等信息获取和搜索
  - 网易云音乐的音乐/MV 等信息获取和搜索
  - 百度贴吧的帖子内容的获取
  - [爱壁纸](https://adesk.com/)的横版和竖版壁纸获取
  - 哔咔漫画的漫画信息获取和搜索
  - …

- 该项目的前身是 Imjad API[^1]
  - 由于它的使用人数过多, 致使调用超出限制, 所以本人希望提供一个开源替代来供社区进行自由地部署和使用, 从而减轻一部分该 API 的使用压力

[^1]: [什么是 Imjad API](https://github.com/mixmoe/HibiAPI/wiki/FAQ#%E4%BB%80%E4%B9%88%E6%98%AFimjad-api)

## 优势

### 开源

- 本项目以[Apache-2.0](./LICENSE)许可开源, 请看[开源许可](#开源许可)一节

### 高效

- 使用 Python 的[异步机制](https://docs.python.org/zh-cn/3/library/asyncio.html), 由[FastAPI](https://fastapi.tiangolo.com/)驱动, 带来高效的使用体验 ~~虽然性能瓶颈压根不在这~~

### 稳定

- 在代码中广泛使用了 Python 的[类型提示支持](https://docs.python.org/zh-cn/3/library/typing.html), 使代码可读性更高且更加易于维护和调试

- 在开发初期起就一直使用多种现代 Python 开发工具辅助开发, 包括:

  - 使用 [PyLance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) 进行静态类型推断
  - 使用 [Flake8](https://flake8.pycqa.org/en/latest/) 对代码格式进行检查
  - 使用 [Black](https://black.readthedocs.io/en/stable/) 格式化代码以提升代码可读性

- 不直接使用第三方开发的 API 调用库, 而是全部用更加适合 Web 应用的逻辑重写第三方 API 请求, 更加可控 ~~疯狂造轮子~~

## 已实现 API[^2]

[^2]: 请查看 [#1](https://github.com/mixmoe/HibiAPI/issues/1)

- [x] Pixiv
- [x] 网易云音乐
- [ ] ~~一言~~ (其代替方案<https://hitokoto.cn>提供的方案已足够好, 暂不考虑支持)
- [x] Bilibili
- [x] 二维码
- [ ] ~~企鹅 FM~~ (似乎用的人不是很多)
- [x] 百度贴吧
- [x] 爱壁纸
- [x] 哔咔漫画

## 部署指南

- 手动部署指南: **[点击此处查看](https://github.com/mixmoe/HibiAPI/wiki/Deployment)**

## 应用实例

**我有更多的应用实例?** [立即 PR!](https://github.com/mixmoe/HibiAPI/pulls)

- [`journey-ad/pixiv-viewer`](https://github.com/journey-ad/pixiv-viewer)

  - **又一个 Pixiv 阅览工具**

- 公开搭建实例
  | **站点名称** | **网址** | **状态** |
  | :--------------------------: | :-----------------------------: | :---------------------: |
  | **官方 Demo[^3]** | <https://api.obfs.dev> | ![official][official] |
  | [MyCard](https://mycard.moe) | <https://hibi.moecube.com> | ![mycard][mycard] |

[^3]: 为了减轻服务器负担, Demo 服务器已开启了 Cloudflare 全站缓存, 如果有实时获取更新的需求, 请自行搭建或使用其他部署实例

[official]: https://img.shields.io/website?url=https%3A%2F%2Fapi.obfs.dev%2Fopenapi.json
[mycard]: https://img.shields.io/website?url=https%3A%2F%2Fhibi.moecube.com%2Fopenapi.json

## 特别鸣谢

[**@journey-ad**](https://github.com/journey-ad) 大佬的 **Imjad API**, 它是本项目的起源

### 参考项目

> **正是因为有了你们, 这个项目才得以存在**

- Pixiv: [`Mikubill/pixivpy-async`](https://github.com/Mikubill/pixivpy-async) [`upbit/pixivpy`](https://github.com/upbit/pixivpy)

- Bilibili: [`SocialSisterYi/bilibili-API-collect`](https://github.com/SocialSisterYi/bilibili-API-collect) [`soimort/you-get`](https://github.com/soimort/you-get)

- 网易云音乐: [`metowolf/NeteaseCloudMusicApi`](https://github.com/metowolf/NeteaseCloudMusicApi) [`greats3an/pyncm`](https://github.com/greats3an/pyncm) [`Binaryify/NeteaseCloudMusicApi`](https://github.com/Binaryify/NeteaseCloudMusicApi)

- 百度贴吧: [`libsgh/tieba-api`](https://github.com/libsgh/tieba-api)

- 哔咔漫画:[`niuhuan/pica-rust`](https://github.com/niuhuan/pica-rust) [`abbeyokgo/PicaComic-Api`](https://github.com/abbeyokgo/PicaComic-Api)

### 贡献者们

感谢这些为这个项目作出贡献的各位大佬:

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
  <tbody>
    <tr>
      <td align="center" valign="top" width="14.28%"><a href="http://kyomotoi.moe"><img src="https://avatars.githubusercontent.com/u/37587870?v=4?s=100" width="100px;" alt="Kyomotoi"/><br /><sub><b>Kyomotoi</b></sub></a><br /><a href="https://github.com/mixmoe/HibiAPI/commits?author=Kyomotoi" title="Documentation">📖</a> <a href="https://github.com/mixmoe/HibiAPI/commits?author=Kyomotoi" title="Tests">⚠️</a></td>
      <td align="center" valign="top" width="14.28%"><a href="http://thdog.moe"><img src="https://avatars.githubusercontent.com/u/46120251?v=4?s=100" width="100px;" alt="城倉奏"/><br /><sub><b>城倉奏</b></sub></a><br /><a href="#example-shirokurakana" title="Examples">💡</a></td>
      <td align="center" valign="top" width="14.28%"><a href="http://skipm4.com"><img src="https://avatars.githubusercontent.com/u/40311581?v=4?s=100" width="100px;" alt="SkipM4"/><br /><sub><b>SkipM4</b></sub></a><br /><a href="https://github.com/mixmoe/HibiAPI/commits?author=SkipM4" title="Documentation">📖</a></td>
      <td align="center" valign="top" width="14.28%"><a href="https://github.com/leaf7th"><img src="https://avatars.githubusercontent.com/u/38352552?v=4?s=100" width="100px;" alt="Nook"/><br /><sub><b>Nook</b></sub></a><br /><a href="https://github.com/mixmoe/HibiAPI/commits?author=leaf7th" title="Code">💻</a></td>
      <td align="center" valign="top" width="14.28%"><a href="https://github.com/jiangzhuochi"><img src="https://avatars.githubusercontent.com/u/50538375?v=4?s=100" width="100px;" alt="Jocky Chiang"/><br /><sub><b>Jocky Chiang</b></sub></a><br /><a href="https://github.com/mixmoe/HibiAPI/commits?author=jiangzhuochi" title="Code">💻</a></td>
      <td align="center" valign="top" width="14.28%"><a href="https://github.com/cleoold"><img src="https://avatars.githubusercontent.com/u/13920903?v=4?s=100" width="100px;" alt="midori"/><br /><sub><b>midori</b></sub></a><br /><a href="https://github.com/mixmoe/HibiAPI/commits?author=cleoold" title="Documentation">📖</a></td>
      <td align="center" valign="top" width="14.28%"><a href="https://www.2yo.cc"><img src="https://avatars.githubusercontent.com/u/41198038?v=4?s=100" width="100px;" alt="Pretty9"/><br /><sub><b>Pretty9</b></sub></a><br /><a href="https://github.com/mixmoe/HibiAPI/commits?author=Pretty9" title="Code">💻</a></td>
    </tr>
    <tr>
      <td align="center" valign="top" width="14.28%"><a href="https://nocilol.me/"><img src="https://avatars.githubusercontent.com/u/16256221?v=4?s=100" width="100px;" alt="Jad"/><br /><sub><b>Jad</b></sub></a><br /><a href="https://github.com/mixmoe/HibiAPI/issues?q=author%3Ajourney-ad" title="Bug reports">🐛</a> <a href="#ideas-journey-ad" title="Ideas, Planning, & Feedback">🤔</a></td>
      <td align="center" valign="top" width="14.28%"><a href="http://nanoka.top"><img src="https://avatars.githubusercontent.com/u/31837214?v=4?s=100" width="100px;" alt="Yumine Sakura"/><br /><sub><b>Yumine Sakura</b></sub></a><br /><a href="https://github.com/mixmoe/HibiAPI/commits?author=asadahimeka" title="Code">💻</a></td>
      <td align="center" valign="top" width="14.28%"><a href="https://github.com/yeyang52"><img src="https://avatars.githubusercontent.com/u/107110851?v=4?s=100" width="100px;" alt="yeyang"/><br /><sub><b>yeyang</b></sub></a><br /><a href="https://github.com/mixmoe/HibiAPI/commits?author=yeyang52" title="Code">💻</a></td>
    </tr>
  </tbody>
</table>

<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

_本段符合 [all-contributors](https://github.com/all-contributors/all-contributors) 规范_

## 开源许可

    Copyright 2020-2021 Mix Technology

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.


================================================
FILE: docker-compose.yml
================================================
version: "3.9"

volumes:
  hibi_redis: {}

networks:
  hibi_net: {}

services:
  redis:
    image: redis:alpine
    container_name: hibi_redis
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - hibi_net
    volumes:
      - hibi_redis:/data
    expose: [6379]

  api:
    container_name: hibiapi
    build:
      dockerfile: Dockerfile
      context: .
    restart: on-failure
    networks:
      - hibi_net
    depends_on:
      redis:
        condition: service_healthy
    ports:
      - "8080:8080"
    environment:
      PORT: "8080"
      FORWARDED_ALLOW_IPS: "*"
      GENERAL_CACHE_URI: "redis://redis:6379"
      GENERAL_SERVER_HOST: "0.0.0.0"


================================================
FILE: hibiapi/__init__.py
================================================
r"""
  _    _ _ _     _          _____ _____  
 | |  | (_) |   (_)   /\   |  __ \_   _| 
 | |__| |_| |__  _   /  \  | |__) || |   
 |  __  | | '_ \| | / /\ \ |  ___/ | |   
 | |  | | | |_) | |/ ____ \| |    _| |_  
 |_|  |_|_|_.__/|_/_/    \_\_|   |_____| 

A program that implements easy-to-use APIs for a variety of commonly used sites
Repository: https://github.com/mixmoe/HibiAPI
"""  # noqa:W291,W293

from importlib.metadata import version

__version__ = version("hibiapi")


================================================
FILE: hibiapi/__main__.py
================================================
import os
from pathlib import Path

import typer
import uvicorn

from hibiapi import __file__ as root_file
from hibiapi import __version__
from hibiapi.utils.config import CONFIG_DIR, DEFAULT_DIR, Config
from hibiapi.utils.log import LOG_LEVEL, logger

COPYRIGHT = r"""
<b><g>
  _    _ _ _     _          _____ _____  
 | |  | (_) |   (_)   /\   |  __ \_   _| 
 | |__| |_| |__  _   /  \  | |__) || |   
 |  __  | | '_ \| | / /\ \ |  ___/ | |   
 | |  | | | |_) | |/ ____ \| |    _| |_  
 |_|  |_|_|_.__/|_/_/    \_\_|   |_____| 
</g><e>
A program that implements easy-to-use APIs for a variety of commonly used sites
Repository: https://github.com/mixmoe/HibiAPI
</e></b>""".strip()  # noqa:W291


LOG_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "default": {
            "class": "hibiapi.utils.log.LoguruHandler",
        },
    },
    "loggers": {
        "uvicorn.error": {
            "handlers": ["default"],
            "level": LOG_LEVEL,
        },
        "uvicorn.access": {
            "handlers": ["default"],
            "level": LOG_LEVEL,
        },
    },
}

RELOAD_CONFIG = {
    "reload": True,
    "reload_dirs": [
        *map(str, [Path(root_file).parent.absolute(), CONFIG_DIR.absolute()])
    ],
    "reload_includes": ["*.py", "*.yml"],
}


cli = typer.Typer()


@cli.callback(invoke_without_command=True)
@cli.command()
def run(
    ctx: typer.Context,
    host: str = Config["server"]["host"].as_str(),
    port: int = Config["server"]["port"].as_number(),
    workers: int = 1,
    reload: bool = False,
):
    if ctx.invoked_subcommand is not None:
        return

    if ctx.info_name != (func_name := run.__name__):
        logger.warning(
            f"Directly usage of command <r>{ctx.info_name}</r> is <b>deprecated</b>, "
            f"please use <g>{ctx.info_name} {func_name}</g> instead."
        )

    try:
        terminal_width, _ = os.get_terminal_size()
    except OSError:
        terminal_width = 0
    logger.warning(
        "\n".join(i.center(terminal_width) for i in COPYRIGHT.splitlines()),
    )
    logger.info(f"HibiAPI version: <g><b>{__version__}</b></g>")

    uvicorn.run(
        "hibiapi.app:app",
        host=host,
        port=port,
        access_log=False,
        log_config=LOG_CONFIG,
        workers=workers,
        forwarded_allow_ips=Config["server"]["allowed-forward"].get_optional(str),
        **(RELOAD_CONFIG if reload else {}),
    )


@cli.command()
def config(force: bool = False):
    total_written = 0
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    for file in os.listdir(DEFAULT_DIR):
        default_path = DEFAULT_DIR / file
        config_path = CONFIG_DIR / file
        if not (existed := config_path.is_file()) or force:
            total_written += config_path.write_text(
                default_path.read_text(encoding="utf-8"),
                encoding="utf-8",
            )
            typer.echo(
                typer.style(("Overwritten" if existed else "Created") + ": ", fg="blue")
                + typer.style(str(config_path), fg="yellow")
            )
    if total_written > 0:
        typer.echo(f"Config folder generated, {total_written=}")


if __name__ == "__main__":
    cli()


================================================
FILE: hibiapi/api/__init__.py
================================================


================================================
FILE: hibiapi/api/bika/__init__.py
================================================
from .api import BikaEndpoints, ImageQuality, ResultSort  # noqa: F401
from .constants import BikaConstants  # noqa: F401
from .net import BikaLogin, NetRequest  # noqa: F401


================================================
FILE: hibiapi/api/bika/api.py
================================================
import hashlib
import hmac
from datetime import timedelta
from enum import Enum
from time import time
from typing import Any, Optional, cast

from httpx import URL

from hibiapi.api.bika.constants import BikaConstants
from hibiapi.api.bika.net import NetRequest
from hibiapi.utils.cache import cache_config
from hibiapi.utils.decorators import enum_auto_doc
from hibiapi.utils.net import catch_network_error
from hibiapi.utils.routing import BaseEndpoint, dont_route, request_headers


@enum_auto_doc
class ImageQuality(str, Enum):
    """哔咔API返回的图片质量"""

    low = "low"
    """低质量"""
    medium = "medium"
    """中等质量"""
    high = "high"
    """高质量"""
    original = "original"
    """原图"""


@enum_auto_doc
class ResultSort(str, Enum):
    """哔咔API返回的搜索结果排序方式"""

    date_descending = "dd"
    """最新发布"""
    date_ascending = "da"
    """最早发布"""
    like_descending = "ld"
    """最多喜欢"""
    views_descending = "vd"
    """最多浏览"""


class BikaEndpoints(BaseEndpoint):
    @staticmethod
    def _sign(url: URL, timestamp_bytes: bytes, nonce: bytes, method: bytes):
        return hmac.new(
            BikaConstants.DIGEST_KEY,
            (
                url.raw_path.lstrip(b"/")
                + timestamp_bytes
                + nonce
                + method
                + BikaConstants.API_KEY
            ).lower(),
            hashlib.sha256,
        ).hexdigest()

    @dont_route
    @catch_network_error
    async def request(
        self,
        endpoint: str,
        *,
        params: Optional[dict[str, Any]] = None,
        body: Optional[dict[str, Any]] = None,
        no_token: bool = False,
    ):
        net_client = cast(NetRequest, self.client.net_client)
        if not no_token:
            async with net_client.auth_lock:
                if net_client.token is None:
                    await net_client.login(self)

        headers = {
            "Authorization": net_client.token or "",
            "Time": (current_time := f"{time():.0f}".encode()),
            "Image-Quality": request_headers.get().get(
                "X-Image-Quality", ImageQuality.medium
            ),
            "Nonce": (nonce := hashlib.md5(current_time).hexdigest().encode()),
            "Signature": self._sign(
                request_url := self._join(
                    base=BikaConstants.API_HOST,
                    endpoint=endpoint,
                    params=params or {},
                ),
                current_time,
                nonce,
                b"GET" if body is None else b"POST",
            ),
        }

        response = await (
            self.client.get(request_url, headers=headers)
            if body is None
            else self.client.post(request_url, headers=headers, json=body)
        )
        return response.json()

    @cache_config(ttl=timedelta(days=1))
    async def collections(self):
        return await self.request("collections")

    @cache_config(ttl=timedelta(days=3))
    async def categories(self):
        return await self.request("categories")

    @cache_config(ttl=timedelta(days=3))
    async def keywords(self):
        return await self.request("keywords")

    async def advanced_search(
        self,
        *,
        keyword: str,
        page: int = 1,
        sort: ResultSort = ResultSort.date_descending,
    ):
        return await self.request(
            "comics/advanced-search",
            body={
                "keyword": keyword,
                "sort": sort,
            },
            params={
                "page": page,
                "s": sort,
            },
        )

    async def category_list(
        self,
        *,
        category: str,
        page: int = 1,
        sort: ResultSort = ResultSort.date_descending,
    ):
        return await self.request(
            "comics",
            params={
                "page": page,
                "c": category,
                "s": sort,
            },
        )

    async def author_list(
        self,
        *,
        author: str,
        page: int = 1,
        sort: ResultSort = ResultSort.date_descending,
    ):
        return await self.request(
            "comics",
            params={
                "page": page,
                "a": author,
                "s": sort,
            },
        )

    @cache_config(ttl=timedelta(days=3))
    async def comic_detail(self, *, id: str):
        return await self.request("comics/{id}", params={"id": id})

    async def comic_recommendation(self, *, id: str):
        return await self.request("comics/{id}/recommendation", params={"id": id})

    async def comic_episodes(self, *, id: str, page: int = 1):
        return await self.request(
            "comics/{id}/eps",
            params={
                "id": id,
                "page": page,
            },
        )

    async def comic_page(self, *, id: str, order: int = 1, page: int = 1):
        return await self.request(
            "comics/{id}/order/{order}/pages",
            params={
                "id": id,
                "order": order,
                "page": page,
            },
        )

    async def comic_comments(self, *, id: str, page: int = 1):
        return await self.request(
            "comics/{id}/comments",
            params={
                "id": id,
                "page": page,
            },
        )

    async def games(self, *, page: int = 1):
        return await self.request("games", params={"page": page})

    @cache_config(ttl=timedelta(days=3))
    async def game_detail(self, *, id: str):
        return await self.request("games/{id}", params={"id": id})


================================================
FILE: hibiapi/api/bika/constants.py
================================================
from hibiapi.utils.config import APIConfig


class BikaConstants:
    DIGEST_KEY = b"~d}$Q7$eIni=V)9\\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn"
    API_KEY = b"C69BAF41DA5ABD1FFEDC6D2FEA56B"
    DEFAULT_HEADERS = {
        "API-Key": API_KEY,
        "App-Channel": "2",
        "App-Version": "2.2.1.2.3.3",
        "App-Build-Version": "44",
        "App-UUID": "defaultUuid",
        "Accept": "application/vnd.picacomic.com.v1+json",
        "App-Platform": "android",
        "User-Agent": "okhttp/3.8.1",
        "Content-Type": "application/json; charset=UTF-8",
    }
    API_HOST = "https://picaapi.picacomic.com/"
    CONFIG = APIConfig("bika")


================================================
FILE: hibiapi/api/bika/net.py
================================================
import asyncio
from base64 import urlsafe_b64decode
from datetime import datetime, timezone
from functools import lru_cache
from typing import TYPE_CHECKING, Any, Literal, Optional

from pydantic import BaseModel, Field

from hibiapi.api.bika.constants import BikaConstants
from hibiapi.utils.net import BaseNetClient

if TYPE_CHECKING:
    from .api import BikaEndpoints


class BikaLogin(BaseModel):
    email: str
    password: str


class JWTHeader(BaseModel):
    alg: str
    typ: Literal["JWT"]


class JWTBody(BaseModel):
    id: str = Field(alias="_id")
    iat: datetime
    exp: datetime


@lru_cache(maxsize=4)
def load_jwt(token: str):
    def b64pad(data: str):
        return data + "=" * (-len(data) % 4)

    head, body, _ = token.split(".")
    head_data = JWTHeader.parse_raw(urlsafe_b64decode(b64pad(head)))
    body_data = JWTBody.parse_raw(urlsafe_b64decode(b64pad(body)))
    return head_data, body_data


class NetRequest(BaseNetClient):
    _token: Optional[str] = None

    def __init__(self):
        super().__init__(
            headers=BikaConstants.DEFAULT_HEADERS.copy(),
            proxies=BikaConstants.CONFIG["proxy"].as_dict(),
        )
        self.auth_lock = asyncio.Lock()

    @property
    def token(self) -> Optional[str]:
        if self._token is None:
            return None
        _, body = load_jwt(self._token)
        return None if body.exp < datetime.now(timezone.utc) else self._token

    async def login(self, endpoint: "BikaEndpoints"):
        login_data = BikaConstants.CONFIG["account"].get(BikaLogin)
        login_result: dict[str, Any] = await endpoint.request(
            "auth/sign-in",
            body=login_data.dict(),
            no_token=True,
        )
        assert login_result["code"] == 200, login_result["message"]
        if not (
            isinstance(login_data := login_result.get("data"), dict)
            and "token" in login_data
        ):
            raise ValueError("failed to read Bika account token.")
        self._token = login_data["token"]


================================================
FILE: hibiapi/api/bilibili/__init__.py
================================================
# flake8:noqa:F401
from .api import *  # noqa: F401, F403
from .constants import BilibiliConstants
from .net import NetRequest


================================================
FILE: hibiapi/api/bilibili/api/__init__.py
================================================
# flake8:noqa:F401
from .base import BaseBilibiliEndpoint, TimelineType, VideoFormatType, VideoQualityType
from .v2 import BilibiliEndpointV2, SearchType
from .v3 import BilibiliEndpointV3


================================================
FILE: hibiapi/api/bilibili/api/base.py
================================================
import hashlib
import json
from enum import Enum, IntEnum
from time import time
from typing import Any, Optional, overload

from httpx import URL

from hibiapi.api.bilibili.constants import BilibiliConstants
from hibiapi.utils.decorators import enum_auto_doc
from hibiapi.utils.net import catch_network_error
from hibiapi.utils.routing import BaseEndpoint, dont_route


@enum_auto_doc
class TimelineType(str, Enum):
    """番剧时间线类型"""

    CN = "cn"
    """国产动画"""
    GLOBAL = "global"
    """番剧"""


@enum_auto_doc
class VideoQualityType(IntEnum):
    """视频质量类型"""

    VIDEO_240P = 6
    VIDEO_360P = 16
    VIDEO_480P = 32
    VIDEO_720P = 64
    VIDEO_720P_60FPS = 74
    VIDEO_1080P = 80
    VIDEO_1080P_PLUS = 112
    VIDEO_1080P_60FPS = 116
    VIDEO_4K = 120


@enum_auto_doc
class VideoFormatType(IntEnum):
    """视频格式类型"""

    FLV = 0
    MP4 = 2
    DASH = 16


class BaseBilibiliEndpoint(BaseEndpoint):
    def _sign(self, base: str, endpoint: str, params: dict[str, Any]) -> URL:
        params.update(
            {
                **BilibiliConstants.DEFAULT_PARAMS,
                "access_key": BilibiliConstants.ACCESS_KEY,
                "appkey": BilibiliConstants.APP_KEY,
                "ts": int(time()),
            }
        )
        params = {k: params[k] for k in sorted(params.keys())}
        url = self._join(base=base, endpoint=endpoint, params=params)
        params["sign"] = hashlib.md5(url.query + BilibiliConstants.SECRET).hexdigest()
        return URL(url, params=params)

    @staticmethod
    def _parse_json(content: str) -> dict[str, Any]:
        try:
            return json.loads(content)
        except json.JSONDecodeError:
            # NOTE: this is used to parse jsonp response
            right, left = content.find("("), content.rfind(")")
            return json.loads(content[right + 1 : left].strip())

    @overload
    async def request(
        self,
        endpoint: str,
        *,
        sign: bool = True,
        params: Optional[dict[str, Any]] = None,
    ) -> dict[str, Any]: ...

    @overload
    async def request(
        self,
        endpoint: str,
        source: str,
        *,
        sign: bool = True,
        params: Optional[dict[str, Any]] = None,
    ) -> dict[str, Any]: ...

    @dont_route
    @catch_network_error
    async def request(
        self,
        endpoint: str,
        source: Optional[str] = None,
        *,
        sign: bool = True,
        params: Optional[dict[str, Any]] = None,
    ) -> dict[str, Any]:
        host = BilibiliConstants.SERVER_HOST[source or "app"]
        url = (self._sign if sign else self._join)(
            base=host, endpoint=endpoint, params=params or {}
        )
        response = await self.client.get(url)
        response.raise_for_status()
        return self._parse_json(response.text)

    async def playurl(
        self,
        *,
        aid: int,
        cid: int,
        quality: VideoQualityType = VideoQualityType.VIDEO_480P,
        type: VideoFormatType = VideoFormatType.FLV,
    ):
        return await self.request(
            "x/player/playurl",
            "api",
            sign=False,
            params={
                "avid": aid,
                "cid": cid,
                "qn": quality,
                "fnval": type,
                "fnver": 0,
                "fourk": 0 if quality >= VideoQualityType.VIDEO_4K else 1,
            },
        )

    async def view(self, *, aid: int):
        return await self.request(
            "x/v2/view",
            params={
                "aid": aid,
            },
        )

    async def search(self, *, keyword: str, page: int = 1, pagesize: int = 20):
        return await self.request(
            "x/v2/search",
            params={
                "duration": 0,
                "keyword": keyword,
                "pn": page,
                "ps": pagesize,
            },
        )

    async def search_hot(self, *, limit: int = 50):
        return await self.request(
            "x/v2/search/hot",
            params={
                "limit": limit,
            },
        )

    async def search_suggest(self, *, keyword: str, type: str = "accurate"):
        return await self.request(
            "x/v2/search/suggest",
            params={
                "keyword": keyword,
                "type": type,
            },
        )

    async def space(self, *, vmid: int, page: int = 1, pagesize: int = 10):
        return await self.request(
            "x/v2/space",
            params={
                "vmid": vmid,
                "ps": pagesize,
                "pn": page,
            },
        )

    async def space_archive(self, *, vmid: int, page: int = 1, pagesize: int = 10):
        return await self.request(
            "x/v2/space/archive",
            params={
                "vmid": vmid,
                "ps": pagesize,
                "pn": page,
            },
        )

    async def favorite_video(
        self,
        *,
        fid: int,
        vmid: int,
        page: int = 1,
        pagesize: int = 20,
    ):
        return await self.request(
            "x/v2/fav/video",
            "api",
            params={
                "fid": fid,
                "pn": page,
                "ps": pagesize,
                "vmid": vmid,
                "order": "ftime",
            },
        )

    async def event_list(
        self,
        *,
        fid: int,
        vmid: int,
        page: int = 1,
        pagesize: int = 20,
    ):  # NOTE: this endpoint is not used
        return await self.request(
            "event/getlist",
            "api",
            params={
                "fid": fid,
                "pn": page,
                "ps": pagesize,
                "vmid": vmid,
                "order": "ftime",
            },
        )

    async def season_info(self, *, season_id: int):
        return await self.request(
            "pgc/view/web/season",
            "api",
            params={
                "season_id": season_id,
            },
        )

    async def bangumi_source(self, *, episode_id: int):
        return await self.request(
            "api/get_source",
            "bgm",
            params={
                "episode_id": episode_id,
            },
        )

    async def season_recommend(self, *, season_id: int):
        return await self.request(
            "pgc/season/web/related/recommend",
            "api",
            sign=False,
            params={
                "season_id": season_id,
            },
        )

    async def timeline(self, *, type: TimelineType = TimelineType.GLOBAL):
        return await self.request(
            "web_api/timeline_{type}",
            "bgm",
            sign=False,
            params={
                "type": type,
            },
        )

    async def suggest(self, *, keyword: str):  # NOTE: this endpoint is not used
        return await self.request(
            "main/suggest",
            "search",
            sign=False,
            params={
                "func": "suggest",
                "suggest_type": "accurate",
                "sug_type": "tag",
                "main_ver": "v1",
                "keyword": keyword,
            },
        )


================================================
FILE: hibiapi/api/bilibili/api/v2.py
================================================
from collections.abc import Coroutine
from enum import Enum
from functools import wraps
from typing import Callable, Optional, TypeVar

from hibiapi.api.bilibili.api.base import (
    BaseBilibiliEndpoint,
    TimelineType,
    VideoFormatType,
    VideoQualityType,
)
from hibiapi.utils.decorators import enum_auto_doc
from hibiapi.utils.exceptions import ClientSideException
from hibiapi.utils.net import AsyncHTTPClient
from hibiapi.utils.routing import BaseEndpoint

_AnyCallable = TypeVar("_AnyCallable", bound=Callable[..., Coroutine])


def process_keyerror(function: _AnyCallable) -> _AnyCallable:
    @wraps(function)
    async def wrapper(*args, **kwargs):
        try:
            return await function(*args, **kwargs)
        except (KeyError, IndexError) as e:
            raise ClientSideException(detail=str(e)) from None

    return wrapper  # type:ignore


@enum_auto_doc
class SearchType(str, Enum):
    """搜索类型"""

    search = "search"
    """综合搜索"""

    suggest = "suggest"
    """搜索建议"""

    hot = "hot"
    """热门"""


class BilibiliEndpointV2(BaseEndpoint, cache_endpoints=False):
    def __init__(self, client: AsyncHTTPClient):
        super().__init__(client)
        self.base = BaseBilibiliEndpoint(client)

    @process_keyerror
    async def playurl(
        self,
        *,
        aid: int,
        page: Optional[int] = None,
        quality: VideoQualityType = VideoQualityType.VIDEO_480P,
        type: VideoFormatType = VideoFormatType.MP4,
    ):  # NOTE: not completely same with origin
        video_view = await self.base.view(aid=aid)
        if page is None:
            return video_view
        cid: int = video_view["data"]["pages"][page - 1]["cid"]
        return await self.base.playurl(
            aid=aid,
            cid=cid,
            quality=quality,
            type=type,
        )

    async def seasoninfo(self, *, season_id: int):  # NOTE: not same with origin
        return await self.base.season_info(season_id=season_id)

    async def source(self, *, episode_id: int):
        return await self.base.bangumi_source(episode_id=episode_id)

    async def seasonrecommend(self, *, season_id: int):  # NOTE: not same with origin
        return await self.base.season_recommend(season_id=season_id)

    async def search(
        self,
        *,
        keyword: str = "",
        type: SearchType = SearchType.search,
        page: int = 1,
        pagesize: int = 20,
        limit: int = 50,
    ):
        if type == SearchType.suggest:
            return await self.base.search_suggest(keyword=keyword)
        elif type == SearchType.hot:
            return await self.base.search_hot(limit=limit)
        else:
            return await self.base.search(
                keyword=keyword,
                page=page,
                pagesize=pagesize,
            )

    async def timeline(
        self, *, type: TimelineType = TimelineType.GLOBAL
    ):  # NOTE: not same with origin
        return await self.base.timeline(type=type)

    async def space(self, *, vmid: int, page: int = 1, pagesize: int = 10):
        return await self.base.space(
            vmid=vmid,
            page=page,
            pagesize=pagesize,
        )

    async def archive(self, *, vmid: int, page: int = 1, pagesize: int = 10):
        return await self.base.space_archive(
            vmid=vmid,
            page=page,
            pagesize=pagesize,
        )

    async def favlist(self, *, fid: int, vmid: int, page: int = 1, pagesize: int = 20):
        return await self.base.favorite_video(
            fid=fid,
            vmid=vmid,
            page=page,
            pagesize=pagesize,
        )


================================================
FILE: hibiapi/api/bilibili/api/v3.py
================================================
from hibiapi.api.bilibili.api.base import (
    BaseBilibiliEndpoint,
    TimelineType,
    VideoFormatType,
    VideoQualityType,
)
from hibiapi.utils.net import AsyncHTTPClient
from hibiapi.utils.routing import BaseEndpoint


class BilibiliEndpointV3(BaseEndpoint, cache_endpoints=False):
    def __init__(self, client: AsyncHTTPClient):
        super().__init__(client)
        self.base = BaseBilibiliEndpoint(client)

    async def video_info(self, *, aid: int):
        return await self.base.view(aid=aid)

    async def video_address(
        self,
        *,
        aid: int,
        cid: int,
        quality: VideoQualityType = VideoQualityType.VIDEO_480P,
        type: VideoFormatType = VideoFormatType.FLV,
    ):
        return await self.base.playurl(
            aid=aid,
            cid=cid,
            quality=quality,
            type=type,
        )

    async def user_info(self, *, uid: int, page: int = 1, size: int = 10):
        return await self.base.space(
            vmid=uid,
            page=page,
            pagesize=size,
        )

    async def user_uploaded(self, *, uid: int, page: int = 1, size: int = 10):
        return await self.base.space_archive(
            vmid=uid,
            page=page,
            pagesize=size,
        )

    async def user_favorite(self, *, uid: int, fid: int, page: int = 1, size: int = 10):
        return await self.base.favorite_video(
            fid=fid,
            vmid=uid,
            page=page,
            pagesize=size,
        )

    async def season_info(self, *, season_id: int):
        return await self.base.season_info(season_id=season_id)

    async def season_recommend(self, *, season_id: int):
        return await self.base.season_recommend(season_id=season_id)

    async def season_episode(self, *, episode_id: int):
        return await self.base.bangumi_source(episode_id=episode_id)

    async def season_timeline(self, *, type: TimelineType = TimelineType.GLOBAL):
        return await self.base.timeline(type=type)

    async def search(self, *, keyword: str, page: int = 1, size: int = 20):
        return await self.base.search(
            keyword=keyword,
            page=page,
            pagesize=size,
        )

    async def search_recommend(self, *, limit: int = 50):
        return await self.base.search_hot(limit=limit)

    async def search_suggestion(self, *, keyword: str):
        return await self.base.search_suggest(keyword=keyword)


================================================
FILE: hibiapi/api/bilibili/constants.py
================================================
from http.cookies import SimpleCookie
from typing import Any

from hibiapi.utils.config import APIConfig

_CONFIG = APIConfig("bilibili")


class BilibiliConstants:
    SERVER_HOST: dict[str, str] = {
        "app": "https://app.bilibili.com",
        "api": "https://api.bilibili.com",
        "interface": "https://interface.bilibili.com",
        "main": "https://www.bilibili.com",
        "bgm": "https://bangumi.bilibili.com",
        "comment": "https://comment.bilibili.com",
        "search": "https://s.search.bilibili.com",
        "mobile": "https://m.bilibili.com",
    }
    APP_HOST: str = "http://app.bilibili.com"
    DEFAULT_PARAMS: dict[str, Any] = {
        "build": 507000,
        "device": "android",
        "platform": "android",
        "mobi_app": "android",
    }
    APP_KEY: str = "1d8b6e7d45233436"
    SECRET: bytes = b"560c52ccd288fed045859ed18bffd973"
    ACCESS_KEY: str = "5271b2f0eb92f5f89af4dc39197d8e41"
    COOKIES: SimpleCookie = SimpleCookie(_CONFIG["net"]["cookie"].as_str())
    USER_AGENT: str = _CONFIG["net"]["user-agent"].as_str()
    CONFIG: APIConfig = _CONFIG


================================================
FILE: hibiapi/api/bilibili/net.py
================================================
from httpx import Cookies

from hibiapi.utils.net import BaseNetClient

from .constants import BilibiliConstants


class NetRequest(BaseNetClient):
    def __init__(self):
        super().__init__(
            headers={"user-agent": BilibiliConstants.USER_AGENT},
            cookies=Cookies({k: v.value for k, v in BilibiliConstants.COOKIES.items()}),
        )


================================================
FILE: hibiapi/api/netease/__init__.py
================================================
# flake8:noqa:F401
from .api import BitRateType, NeteaseEndpoint, RecordPeriodType, SearchType
from .constants import NeteaseConstants
from .net import NetRequest


================================================
FILE: hibiapi/api/netease/api.py
================================================
import base64
import json
import secrets
import string
from datetime import timedelta
from enum import IntEnum
from ipaddress import IPv4Address
from random import randint
from typing import Annotated, Any, Optional

from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad
from fastapi import Query

from hibiapi.api.netease.constants import NeteaseConstants
from hibiapi.utils.cache import cache_config
from hibiapi.utils.decorators import enum_auto_doc
from hibiapi.utils.exceptions import UpstreamAPIException
from hibiapi.utils.net import catch_network_error
from hibiapi.utils.routing import BaseEndpoint, dont_route


@enum_auto_doc
class SearchType(IntEnum):
    """搜索内容类型"""

    SONG = 1
    """单曲"""
    ALBUM = 10
    """专辑"""
    ARTIST = 100
    """歌手"""
    PLAYLIST = 1000
    """歌单"""
    USER = 1002
    """用户"""
    MV = 1004
    """MV"""
    LYRICS = 1006
    """歌词"""
    DJ = 1009
    """主播电台"""
    VIDEO = 1014
    """视频"""


@enum_auto_doc
class BitRateType(IntEnum):
    """歌曲码率"""

    LOW = 64000
    MEDIUM = 128000
    STANDARD = 198000
    HIGH = 320000


@enum_auto_doc
class MVResolutionType(IntEnum):
    """MV分辨率"""

    QVGA = 240
    VGA = 480
    HD = 720
    FHD = 1080


@enum_auto_doc
class RecordPeriodType(IntEnum):
    """听歌记录时段类型"""

    WEEKLY = 1
    """本周"""
    ALL = 0
    """所有时段"""


class _EncryptUtil:
    alphabets = bytearray(ord(char) for char in string.ascii_letters + string.digits)

    @staticmethod
    def _aes(data: bytes, key: bytes) -> bytes:
        data = pad(data, 16) if len(data) % 16 else data
        return base64.encodebytes(
            AES.new(
                key=key,
                mode=AES.MODE_CBC,
                iv=NeteaseConstants.AES_IV,
            ).encrypt(data)
        )

    @staticmethod
    def _rsa(data: bytes):
        result = pow(
            base=int(data.hex(), 16),
            exp=NeteaseConstants.RSA_PUBKEY,
            mod=NeteaseConstants.RSA_MODULUS,
        )
        return f"{result:0>256x}"

    @classmethod
    def encrypt(cls, data: dict[str, Any]) -> dict[str, str]:
        secret = bytes(secrets.choice(cls.alphabets) for _ in range(16))
        secure_key = cls._rsa(bytes(reversed(secret)))
        return {
            "params": cls._aes(
                data=cls._aes(
                    data=json.dumps(data).encode(),
                    key=NeteaseConstants.AES_KEY,
                ),
                key=secret,
            ).decode("ascii"),
            "encSecKey": secure_key,
        }


class NeteaseEndpoint(BaseEndpoint):
    def _construct_headers(self):
        headers = self.client.headers.copy()
        headers["X-Real-IP"] = str(
            IPv4Address(
                randint(
                    int(NeteaseConstants.SOURCE_IP_SEGMENT.network_address),
                    int(NeteaseConstants.SOURCE_IP_SEGMENT.broadcast_address),
                )
            )
        )
        return headers

    @dont_route
    @catch_network_error
    async def request(
        self, endpoint: str, *, params: Optional[dict[str, Any]] = None
    ) -> dict[str, Any]:
        params = {
            **(params or {}),
            "csrf_token": self.client.cookies.get("__csrf", ""),
        }
        response = await self.client.post(
            self._join(
                NeteaseConstants.HOST,
                endpoint=endpoint,
                params=params,
            ),
            headers=self._construct_headers(),
            data=_EncryptUtil.encrypt(params),
        )
        response.raise_for_status()
        if not response.text.strip():
            raise UpstreamAPIException(
                f"Upstream API {endpoint=} returns blank content"
            )
        return response.json()

    async def search(
        self,
        *,
        s: str,
        search_type: SearchType = SearchType.SONG,
        limit: int = 20,
        offset: int = 0,
    ):
        return await self.request(
            "api/cloudsearch/pc",
            params={
                "s": s,
                "type": search_type,
                "limit": limit,
                "offset": offset,
                "total": True,
            },
        )

    async def artist(self, *, id: int):
        return await self.request(
            "weapi/v1/artist/{artist_id}",
            params={
                "artist_id": id,
            },
        )

    async def album(self, *, id: int):
        return await self.request(
            "weapi/v1/album/{album_id}",
            params={
                "album_id": id,
            },
        )

    async def detail(
        self,
        *,
        id: Annotated[list[int], Query()],
    ):
        return await self.request(
            "api/v3/song/detail",
            params={
                "c": json.dumps(
                    [{"id": str(i)} for i in id],
                ),
            },
        )

    @cache_config(ttl=timedelta(minutes=20))
    async def song(
        self,
        *,
        id: Annotated[list[int], Query()],
        br: BitRateType = BitRateType.STANDARD,
    ):
        return await self.request(
            "weapi/song/enhance/player/url",
            params={
                "ids": [str(i) for i in id],
                "br": br,
            },
        )

    async def playlist(self, *, id: int):
        return await self.request(
            "weapi/v6/playlist/detail",
            params={
                "id": id,
                "total": True,
                "offset": 0,
                "limit": 1000,
                "n": 1000,
            },
        )

    async def lyric(self, *, id: int):
        return await self.request(
            "weapi/song/lyric",
            params={
                "id": id,
                "os": "pc",
                "lv": -1,
                "kv": -1,
                "tv": -1,
            },
        )

    async def mv(self, *, id: int):
        return await self.request(
            "api/v1/mv/detail",
            params={
                "id": id,
            },
        )

    async def mv_url(
        self,
        *,
        id: int,
        res: MVResolutionType = MVResolutionType.FHD,
    ):
        return await self.request(
            "weapi/song/enhance/play/mv/url",
            params={
                "id": id,
                "r": res,
            },
        )

    async def comments(self, *, id: int, offset: int = 0, limit: int = 1):
        return await self.request(
            "weapi/v1/resource/comments/R_SO_4_{song_id}",
            params={
                "song_id": id,
                "offset": offset,
                "total": True,
                "limit": limit,
            },
        )

    async def record(self, *, id: int, period: RecordPeriodType = RecordPeriodType.ALL):
        return await self.request(
            "weapi/v1/play/record",
            params={
                "uid": id,
                "type": period,
            },
        )

    async def djradio(self, *, id: int):
        return await self.request(
            "api/djradio/v2/get",
            params={
                "id": id,
            },
        )

    async def dj(self, *, id: int, offset: int = 0, limit: int = 20, asc: bool = False):
        # NOTE: Possible not same with origin
        return await self.request(
            "weapi/dj/program/byradio",
            params={
                "radioId": id,
                "offset": offset,
                "limit": limit,
                "asc": asc,
            },
        )

    async def detail_dj(self, *, id: int):
        return await self.request(
            "api/dj/program/detail",
            params={
                "id": id,
            },
        )

    async def user(self, *, id: int):
        return await self.request(
            "weapi/v1/user/detail/{id}",
            params={"id": id},
        )

    async def user_playlist(self, *, id: int, limit: int = 50, offset: int = 0):
        return await self.request(
            "weapi/user/playlist",
            params={
                "uid": id,
                "limit": limit,
                "offset": offset,
            },
        )


================================================
FILE: hibiapi/api/netease/constants.py
================================================
from http.cookies import SimpleCookie
from ipaddress import IPv4Network

from hibiapi.utils.config import APIConfig

_Config = APIConfig("netease")


class NeteaseConstants:
    AES_KEY: bytes = b"0CoJUm6Qyw8W8jud"
    AES_IV: bytes = b"0102030405060708"
    RSA_PUBKEY: int = int("010001", 16)
    RSA_MODULUS: int = int(
        "00e0b509f6259df8642dbc3566290147"
        "7df22677ec152b5ff68ace615bb7b725"
        "152b3ab17a876aea8a5aa76d2e417629"
        "ec4ee341f56135fccf695280104e0312"
        "ecbda92557c93870114af6c9d05c4f7f"
        "0c3685b7a46bee255932575cce10b424"
        "d813cfe4875d3e82047b97ddef52741d"
        "546b8e289dc6935b3ece0462db0a22b8e7",
        16,
    )

    HOST: str = "http://music.163.com"
    COOKIES: SimpleCookie = SimpleCookie(_Config["net"]["cookie"].as_str())
    SOURCE_IP_SEGMENT: IPv4Network = _Config["net"]["source"].get(IPv4Network)
    DEFAULT_HEADERS: dict[str, str] = {
        "user-agent": _Config["net"]["user-agent"].as_str(),
        "referer": "http://music.163.com",
    }

    CONFIG: APIConfig = _Config


================================================
FILE: hibiapi/api/netease/net.py
================================================
from httpx import Cookies

from hibiapi.utils.net import BaseNetClient

from .constants import NeteaseConstants


class NetRequest(BaseNetClient):
    def __init__(self):
        super().__init__(
            headers=NeteaseConstants.DEFAULT_HEADERS,
            cookies=Cookies({k: v.value for k, v in NeteaseConstants.COOKIES.items()}),
        )


================================================
FILE: hibiapi/api/pixiv/__init__.py
================================================
# flake8:noqa:F401
from .api import (
    IllustType,
    PixivEndpoints,
    RankingDate,
    RankingType,
    SearchDurationType,
    SearchModeType,
    SearchNovelModeType,
    SearchSortType,
)
from .constants import PixivConstants
from .net import NetRequest, PixivAuthData


================================================
FILE: hibiapi/api/pixiv/api.py
================================================
import json
import re
from datetime import date, timedelta
from enum import Enum
from typing import Any, Literal, Optional, Union, cast, overload

from hibiapi.api.pixiv.constants import PixivConstants
from hibiapi.api.pixiv.net import NetRequest as PixivNetClient
from hibiapi.utils.cache import cache_config
from hibiapi.utils.decorators import enum_auto_doc
from hibiapi.utils.net import catch_network_error
from hibiapi.utils.routing import BaseEndpoint, dont_route, request_headers


@enum_auto_doc
class IllustType(str, Enum):
    """画作类型"""

    illust = "illust"
    """插画"""
    manga = "manga"
    """漫画"""


@enum_auto_doc
class RankingType(str, Enum):
    """排行榜内容类型"""

    day = "day"
    """日榜"""
    week = "week"
    """周榜"""
    month = "month"
    """月榜"""
    day_male = "day_male"
    """男性向"""
    day_female = "day_female"
    """女性向"""
    week_original = "week_original"
    """原创周榜"""
    week_rookie = "week_rookie"
    """新人周榜"""
    day_ai = "day_ai"
    """AI日榜"""
    day_manga = "day_manga"
    """漫画日榜"""
    week_manga = "week_manga"
    """漫画周榜"""
    month_manga = "month_manga"
    """漫画月榜"""
    week_rookie_manga = "week_rookie_manga"
    """漫画新人周榜"""
    day_r18 = "day_r18"
    day_male_r18 = "day_male_r18"
    day_female_r18 = "day_female_r18"
    week_r18 = "week_r18"
    week_r18g = "week_r18g"
    day_r18_ai = "day_r18_ai"
    day_r18_manga = "day_r18_manga"
    week_r18_manga = "week_r18_manga"


@enum_auto_doc
class SearchModeType(str, Enum):
    """搜索匹配类型"""

    partial_match_for_tags = "partial_match_for_tags"
    """标签部分一致"""
    exact_match_for_tags = "exact_match_for_tags"
    """标签完全一致"""
    title_and_caption = "title_and_caption"
    """标题说明文"""


@enum_auto_doc
class SearchNovelModeType(str, Enum):
    """搜索匹配类型"""

    partial_match_for_tags = "partial_match_for_tags"
    """标签部分一致"""
    exact_match_for_tags = "exact_match_for_tags"
    """标签完全一致"""
    text = "text"
    """正文"""
    keyword = "keyword"
    """关键词"""


@enum_auto_doc
class SearchSortType(str, Enum):
    """搜索排序类型"""

    date_desc = "date_desc"
    """按日期倒序"""
    date_asc = "date_asc"
    """按日期正序"""
    popular_desc = "popular_desc"
    """受欢迎降序(Premium功能)"""


@enum_auto_doc
class SearchDurationType(str, Enum):
    """搜索时段类型"""

    within_last_day = "within_last_day"
    """一天内"""
    within_last_week = "within_last_week"
    """一周内"""
    within_last_month = "within_last_month"
    """一个月内"""


class RankingDate(date):
    @classmethod
    def yesterday(cls) -> "RankingDate":
        yesterday = cls.today() - timedelta(days=1)
        return cls(yesterday.year, yesterday.month, yesterday.day)

    def toString(self) -> str:
        return self.strftime(r"%Y-%m-%d")

    @classmethod
    def new(cls, date: date) -> "RankingDate":
        return cls(date.year, date.month, date.day)


class PixivEndpoints(BaseEndpoint):
    @staticmethod
    def _parse_accept_language(accept_language: str) -> str:
        first_language, *_ = accept_language.partition(",")
        language_code, *_ = first_language.partition(";")
        return language_code.lower().strip()

    @overload
    async def request(
        self,
        endpoint: str,
        *,
        params: Optional[dict[str, Any]] = None,
        return_text: Literal[False] = False,
    ) -> dict[str, Any]: ...

    @overload
    async def request(
        self,
        endpoint: str,
        *,
        params: Optional[dict[str, Any]] = None,
        return_text: Literal[True],
    ) -> str: ...

    @dont_route
    @catch_network_error
    async def request(
        self,
        endpoint: str,
        *,
        params: Optional[dict[str, Any]] = None,
        return_text: bool = False,
    ) -> Union[dict[str, Any], str]:
        headers = self.client.headers.copy()

        net_client = cast(PixivNetClient, self.client.net_client)
        async with net_client.auth_lock:
            auth, token = net_client.get_available_user()
            if auth is None:
                auth = await net_client.auth(token)
        headers["Authorization"] = f"Bearer {auth.access_token}"

        if language := request_headers.get().get("Accept-Language"):
            language = self._parse_accept_language(language)
            headers["Accept-Language"] = language

        response = await self.client.get(
            self._join(
                base=PixivConstants.APP_HOST,
                endpoint=endpoint,
                params=params or {},
            ),
            headers=headers,
        )
        if return_text:
            return response.text
        return response.json()

    @cache_config(ttl=timedelta(days=3))
    async def illust(self, *, id: int):
        return await self.request("v1/illust/detail", params={"illust_id": id})

    @cache_config(ttl=timedelta(days=1))
    async def member(self, *, id: int):
        return await self.request("v1/user/detail", params={"user_id": id})

    async def member_illust(
        self,
        *,
        id: int,
        illust_type: IllustType = IllustType.illust,
        page: int = 1,
        size: int = 30,
    ):
        return await self.request(
            "v1/user/illusts",
            params={
                "user_id": id,
                "type": illust_type,
                "offset": (page - 1) * size,
            },
        )

    async def favorite(
        self,
        *,
        id: int,
        tag: Optional[str] = None,
        max_bookmark_id: Optional[int] = None,
    ):
        return await self.request(
            "v1/user/bookmarks/illust",
            params={
                "user_id": id,
                "tag": tag,
                "restrict": "public",
                "max_bookmark_id": max_bookmark_id or None,
            },
        )

    # 用户收藏的小说
    async def favorite_novel(
        self,
        *,
        id: int,
        tag: Optional[str] = None,
    ):
        return await self.request(
            "v1/user/bookmarks/novel",
            params={
                "user_id": id,
                "tag": tag,
                "restrict": "public",
            },
        )

    async def following(self, *, id: int, page: int = 1, size: int = 30):
        return await self.request(
            "v1/user/following",
            params={
                "user_id": id,
                "offset": (page - 1) * size,
            },
        )

    async def follower(self, *, id: int, page: int = 1, size: int = 30):
        return await self.request(
            "v1/user/follower",
            params={
                "user_id": id,
                "offset": (page - 1) * size,
            },
        )

    @cache_config(ttl=timedelta(hours=12))
    async def rank(
        self,
        *,
        mode: RankingType = RankingType.week,
        date: Optional[RankingDate] = None,
        page: int = 1,
        size: int = 30,
    ):
        return await self.request(
            "v1/illust/ranking",
            params={
                "mode": mode,
                "date": RankingDate.new(date or RankingDate.yesterday()).toString(),
                "offset": (page - 1) * size,
            },
        )

    async def search(
        self,
        *,
        word: str,
        mode: SearchModeType = SearchModeType.partial_match_for_tags,
        order: SearchSortType = SearchSortType.date_desc,
        duration: Optional[SearchDurationType] = None,
        page: int = 1,
        size: int = 30,
        include_translated_tag_results: bool = True,
        search_ai_type: bool = True,  # 搜索结果是否包含AI作品
    ):
        return await self.request(
            "v1/search/illust",
            params={
                "word": word,
                "search_target": mode,
                "sort": order,
                "duration": duration,
                "offset": (page - 1) * size,
                "include_translated_tag_results": include_translated_tag_results,
                "search_ai_type": 1 if search_ai_type else 0,
            },
        )

    # 热门插画作品预览
    async def popular_preview(
        self,
        *,
        word: str,
        mode: SearchModeType = SearchModeType.partial_match_for_tags,
        merge_plain_keyword_results: bool = True,
        include_translated_tag_results: bool = True,
        filter: str = "for_ios",
    ):
        return await self.request(
            "v1/search/popular-preview/illust",
            params={
                "word": word,
                "search_target": mode,
                "merge_plain_keyword_results": merge_plain_keyword_results,
                "include_translated_tag_results": include_translated_tag_results,
                "filter": filter,
            },
        )

    async def search_user(
        self,
        *,
        word: str,
        page: int = 1,
        size: int = 30,
    ):
        return await self.request(
            "v1/search/user",
            params={"word": word, "offset": (page - 1) * size},
        )

    async def tags_autocomplete(
        self,
        *,
        word: str,
        merge_plain_keyword_results: bool = True,
    ):
        return await self.request(
            "/v2/search/autocomplete",
            params={
                "word": word,
                "merge_plain_keyword_results": merge_plain_keyword_results,
            },
        )

    @cache_config(ttl=timedelta(hours=12))
    async def tags(self):
        return await self.request("v1/trending-tags/illust")

    @cache_config(ttl=timedelta(minutes=15))
    async def related(self, *, id: int, page: int = 1, size: int = 30):
        return await self.request(
            "v2/illust/related",
            params={
                "illust_id": id,
                "offset": (page - 1) * size,
            },
        )

    @cache_config(ttl=timedelta(days=3))
    async def ugoira_metadata(self, *, id: int):
        return await self.request(
            "v1/ugoira/metadata",
            params={
                "illust_id": id,
            },
        )

    # 大家的新作品(插画)
    async def illust_new(
        self,
        *,
        content_type: str = "illust",
    ):
        return await self.request(
            "v1/illust/new",
            params={
                "content_type": content_type,
                "filter": "for_ios",
            },
        )

    # pixivision(亮点/特辑) 列表
    async def spotlights(
        self,
        *,
        category: str = "all",
        page: int = 1,
        size: int = 10,
    ):
        return await self.request(
            "v1/spotlight/articles",
            params={
                "filter": "for_ios",
                "category": category,
                "offset": (page - 1) * size,
            },
        )

    # 插画评论
    async def illust_comments(
        self,
        *,
        id: int,
        page: int = 1,
        size: int = 30,
    ):
        return await self.request(
            "v3/illust/comments",
            params={
                "illust_id": id,
                "offset": (page - 1) * size,
            },
        )

    # 插画评论回复
    async def illust_comment_replies(
        self,
        *,
        id: int,
    ):
        return await self.request(
            "v2/illust/comment/replies",
            params={
                "comment_id": id,
            },
        )

    # 小说评论
    async def novel_comments(
        self,
        *,
        id: int,
        page: int = 1,
        size: int = 30,
    ):
        return await self.request(
            "v3/novel/comments",
            params={
                "novel_id": id,
                "offset": (page - 1) * size,
            },
        )

    # 小说评论回复
    async def novel_comment_replies(
        self,
        *,
        id: int,
    ):
        return await self.request(
            "v2/novel/comment/replies",
            params={
                "comment_id": id,
            },
        )

    # 小说排行榜
    async def rank_novel(
        self,
        *,
        mode: str = "day",
        date: Optional[RankingDate] = None,
        page: int = 1,
        size: int = 30,
    ):
        return await self.request(
            "v1/novel/ranking",
            params={
                "mode": mode,
                "date": RankingDate.new(date or RankingDate.yesterday()).toString(),
                "offset": (page - 1) * size,
            },
        )

    async def member_novel(self, *, id: int, page: int = 1, size: int = 30):
        return await self.request(
            "/v1/user/novels",
            params={
                "user_id": id,
                "offset": (page - 1) * size,
            },
        )

    async def novel_series(self, *, id: int):
        return await self.request("/v2/novel/series", params={"series_id": id})

    async def novel_detail(self, *, id: int):
        return await self.request("/v2/novel/detail", params={"novel_id": id})

    # 已被官方移除,调用 webview/v2/novel 作兼容处理
    async def novel_text(self, *, id: int):
        # return await self.request("/v1/novel/text", params={"novel_id": id})
        response = await self.webview_novel(id=id)
        return {"novel_text": response["text"] or ""}

    # 获取小说 HTML 后解析 JSON
    async def webview_novel(self, *, id: int):
        response = await self.request(
            "webview/v2/novel",
            params={
                "id": id,
                "viewer_version": "20221031_ai",
            },
            return_text=True,
        )

        novel_match = re.search(r"novel:\s+(?P<data>{.+?}),\s+isOwnWork", response)
        return json.loads(novel_match["data"] if novel_match else response)

    @cache_config(ttl=timedelta(hours=12))
    async def tags_novel(self):
        return await self.request("v1/trending-tags/novel")

    async def search_novel(
        self,
        *,
        word: str,
        mode: SearchNovelModeType = SearchNovelModeType.partial_match_for_tags,
        sort: SearchSortType = SearchSortType.date_desc,
        merge_plain_keyword_results: bool = True,
        include_translated_tag_results: bool = True,
        duration: Optional[SearchDurationType] = None,
        page: int = 1,
        size: int = 30,
        search_ai_type: bool = True,  # 搜索结果是否包含AI作品
    ):
        return await self.request(
            "/v1/search/novel",
            params={
                "word": word,
                "search_target": mode,
                "sort": sort,
                "merge_plain_keyword_results": merge_plain_keyword_results,
                "include_translated_tag_results": include_translated_tag_results,
                "duration": duration,
                "offset": (page - 1) * size,
                "search_ai_type": 1 if search_ai_type else 0,
            },
        )

    # 热门小说作品预览
    async def popular_preview_novel(
        self,
        *,
        word: str,
        mode: SearchNovelModeType = SearchNovelModeType.partial_match_for_tags,
        merge_plain_keyword_results: bool = True,
        include_translated_tag_results: bool = True,
        filter: str = "for_ios",
    ):
        return await self.request(
            "v1/search/popular-preview/novel",
            params={
                "word": word,
                "search_target": mode,
                "merge_plain_keyword_results": merge_plain_keyword_results,
                "include_translated_tag_results": include_translated_tag_results,
                "filter": filter,
            },
        )

    async def novel_new(self, *, max_novel_id: Optional[int] = None):
        return await self.request(
            "/v1/novel/new", params={"max_novel_id": max_novel_id}
        )

    # 人气直播列表
    async def live_list(self, *, page: int = 1, size: int = 30):
        params = {"list_type": "popular", "offset": (page - 1) * size}
        if not params["offset"]:
            del params["offset"]
        return await self.request("v1/live/list", params=params)

    # 相关小说作品
    async def related_novel(self, *, id: int, page: int = 1, size: int = 30):
        return await self.request(
            "v1/novel/related",
            params={
                "novel_id": id,
                "offset": (page - 1) * size,
            },
        )

    # 相关用户
    async def related_member(self, *, id: int):
        return await self.request("v1/user/related", params={"seed_user_id": id})

    # 漫画系列
    async def illust_series(self, *, id: int, page: int = 1, size: int = 30):
        return await self.request(
            "v1/illust/series",
            params={"illust_series_id": id, "offset": (page - 1) * size},
        )

    # 用户的漫画系列
    async def member_illust_series(self, *, id: int, page: int = 1, size: int = 30):
        return await self.request(
            "v1/user/illust-series",
            params={"user_id": id, "offset": (page - 1) * size},
        )

    # 用户的小说系列
    async def member_novel_series(self, *, id: int, page: int = 1, size: int = 30):
        return await self.request(
            "v1/user/novel-series", params={"user_id": id, "offset": (page - 1) * size}
        )


================================================
FILE: hibiapi/api/pixiv/constants.py
================================================
from typing import Any

from hibiapi.utils.config import APIConfig


class PixivConstants:
    DEFAULT_HEADERS: dict[str, Any] = {
        "App-OS": "ios",
        "App-OS-Version": "14.6",
        "User-Agent": "PixivIOSApp/7.13.3 (iOS 14.6; iPhone13,2)",
    }
    CLIENT_ID: str = "MOBrBDS8blbauoSck0ZfDbtuzpyT"
    CLIENT_SECRET: str = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj"
    HASH_SECRET: bytes = (
        b"28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c"
    )
    CONFIG: APIConfig = APIConfig("pixiv")
    APP_HOST: str = "https://app-api.pixiv.net"
    AUTH_HOST: str = "https://oauth.secure.pixiv.net"


================================================
FILE: hibiapi/api/pixiv/net.py
================================================
import asyncio
import hashlib
from datetime import datetime, timedelta, timezone
from itertools import cycle

from httpx import URL
from pydantic import BaseModel, Extra, Field

from hibiapi.utils.log import logger
from hibiapi.utils.net import BaseNetClient

from .constants import PixivConstants


class AccountDataModel(BaseModel):
    class Config:
        extra = Extra.allow


class PixivUserData(AccountDataModel):
    account: str
    id: int
    is_premium: bool
    mail_address: str
    name: str


class PixivAuthData(AccountDataModel):
    time: datetime = Field(default_factory=datetime.now)
    expires_in: int
    access_token: str
    refresh_token: str
    user: PixivUserData


class NetRequest(BaseNetClient):
    def __init__(self, tokens: list[str]):
        super().__init__(
            headers=PixivConstants.DEFAULT_HEADERS.copy(),
            proxies=PixivConstants.CONFIG["proxy"].as_dict(),
        )
        self.user_tokens = cycle(tokens)
        self.auth_lock = asyncio.Lock()
        self.user_tokens_dict: dict[str, PixivAuthData] = {}
        self.headers["accept-language"] = PixivConstants.CONFIG["language"].as_str()

    def get_available_user(self):
        token = next(self.user_tokens)
        if (auth_data := self.user_tokens_dict.get(token)) and (
            auth_data.time + timedelta(minutes=1, seconds=auth_data.expires_in)
            > datetime.now()
        ):
            return auth_data, token
        return None, token

    async def auth(self, refresh_token: str):
        url = URL(PixivConstants.AUTH_HOST).join("/auth/token")
        time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S+00:00")
        headers = {
            **self.headers,
            "X-Client-Time": time,
            "X-Client-Hash": hashlib.md5(
                time.encode() + PixivConstants.HASH_SECRET
            ).hexdigest(),
        }
        payload = {
            "get_secure_url": 1,
            "client_id": PixivConstants.CLIENT_ID,
            "client_secret": PixivConstants.CLIENT_SECRET,
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
        }

        async with self as client:
            response = await client.post(url, data=payload, headers=headers)
            response.raise_for_status()

        self.user_tokens_dict[refresh_token] = PixivAuthData.parse_obj(response.json())
        user_data = self.user_tokens_dict[refresh_token].user
        logger.opt(colors=True).info(
            f"Pixiv account <m>{user_data.id}</m> info <b>Updated</b>: "
            f"<b><e>{user_data.name}</e>({user_data.account})</b>."
        )

        return self.user_tokens_dict[refresh_token]


================================================
FILE: hibiapi/api/qrcode.py
================================================
from datetime import datetime
from enum import Enum
from io import BytesIO
from os import fdopen
from pathlib import Path
from typing import Literal, Optional, cast

from PIL import Image
from pydantic import AnyHttpUrl, BaseModel, Field, validate_arguments
from pydantic.color import Color
from qrcode import constants
from qrcode.image.pil import PilImage
from qrcode.main import QRCode

from hibiapi.utils.config import APIConfig
from hibiapi.utils.decorators import ToAsync, enum_auto_doc
from hibiapi.utils.exceptions import ClientSideException
from hibiapi.utils.net import BaseNetClient
from hibiapi.utils.routing import BaseHostUrl
from hibiapi.utils.temp import TempFile

Config = APIConfig("qrcode")


class HostUrl(BaseHostUrl):
    allowed_hosts = Config["qrcode"]["icon-site"].get(list[str])


@enum_auto_doc
class QRCodeLevel(str, Enum):
    """二维码容错率"""

    LOW = "L"
    """最低容错率"""
    MEDIUM = "M"
    """中等容错率"""
    QUARTILE = "Q"
    """高容错率"""
    HIGH = "H"
    """最高容错率"""


@enum_auto_doc
class ReturnEncode(str, Enum):
    """二维码返回的编码方式"""

    raw = "raw"
    """直接重定向到二维码图片"""
    json = "json"
    """返回JSON格式的二维码信息"""
    js = "js"
    jsc = "jsc"


COLOR_WHITE = Color("FFFFFF")
COLOR_BLACK = Color("000000")


class QRInfo(BaseModel):
    url: Optional[AnyHttpUrl] = None
    path: Path
    time: datetime = Field(default_factory=datetime.now)
    data: str
    logo: Optional[HostUrl] = None
    level: QRCodeLevel = QRCodeLevel.MEDIUM
    size: int = 200
    code: Literal[0] = 0
    status: Literal["success"] = "success"

    @classmethod
    @validate_arguments
    async def new(
        cls,
        text: str,
        *,
        size: int = Field(
            200,
            gt=Config["qrcode"]["min-size"].as_number(),
            lt=Config["qrcode"]["max-size"].as_number(),
        ),
        logo: Optional[HostUrl] = None,
        level: QRCodeLevel = QRCodeLevel.MEDIUM,
        bgcolor: Color = COLOR_WHITE,
        fgcolor: Color = COLOR_BLACK,
    ):
        icon_stream = None
        if logo is not None:
            async with BaseNetClient() as client:
                response = await client.get(
                    logo, headers={"user-agent": "HibiAPI@GitHub"}, timeout=6
                )
                response.raise_for_status()
            icon_stream = BytesIO(response.content)
        return cls(
            data=text,
            logo=logo,
            level=level,
            size=size,
            path=await cls._generate(
                text,
                size=size,
                level=level,
                icon_stream=icon_stream,
                bgcolor=bgcolor.as_hex(),
                fgcolor=fgcolor.as_hex(),
            ),
        )

    @classmethod
    @ToAsync
    def _generate(
        cls,
        text: str,
        *,
        size: int = 200,
        level: QRCodeLevel = QRCodeLevel.MEDIUM,
        icon_stream: Optional[BytesIO] = None,
        bgcolor: str = "#FFFFFF",
        fgcolor: str = "#000000",
    ) -> Path:
        qr = QRCode(
            error_correction={
                QRCodeLevel.LOW: constants.ERROR_CORRECT_L,
                QRCodeLevel.MEDIUM: constants.ERROR_CORRECT_M,
                QRCodeLevel.QUARTILE: constants.ERROR_CORRECT_Q,
                QRCodeLevel.HIGH: constants.ERROR_CORRECT_H,
            }[level],
            border=2,
            box_size=8,
        )
        qr.add_data(text)
        image = cast(
            Image.Image,
            qr.make_image(
                PilImage,
                back_color=bgcolor,
                fill_color=fgcolor,
            ).get_image(),
        )
        image = image.resize((size, size))
        if icon_stream is not None:
            try:
                icon = Image.open(icon_stream)
            except ValueError as e:
                raise ClientSideException("Invalid image format.") from e
            icon_width, icon_height = icon.size
            image.paste(
                icon,
                box=(
                    int(size / 2 - icon_width / 2),
                    int(size / 2 - icon_height / 2),
                    int(size / 2 + icon_width / 2),
                    int(size / 2 + icon_height / 2),
                ),
                mask=icon if icon.mode == "RGBA" else None,
            )
        descriptor, path = TempFile.create(".png")
        with fdopen(descriptor, "wb") as f:
            image.save(f, format="PNG")
        return path


================================================
FILE: hibiapi/api/sauce/__init__.py
================================================
# flake8:noqa:F401
from .api import DeduplicateType, HostUrl, SauceEndpoint, UploadFileIO
from .constants import SauceConstants
from .net import NetRequest


================================================
FILE: hibiapi/api/sauce/api.py
================================================
import random
from enum import IntEnum
from io import BytesIO
from typing import Any, Optional, overload

from httpx import HTTPError

from hibiapi.api.sauce.constants import SauceConstants
from hibiapi.utils.decorators import enum_auto_doc
from hibiapi.utils.exceptions import ClientSideException
from hibiapi.utils.net import catch_network_error
from hibiapi.utils.routing import BaseEndpoint, BaseHostUrl


class UnavailableSourceException(ClientSideException):
    code = 422
    detail = "given image is not avaliable to fetch"


class ImageSourceOversizedException(UnavailableSourceException):
    code = 413
    detail = (
        "given image size is rather than maximum limit "
        f"{SauceConstants.IMAGE_MAXIMUM_SIZE} bytes"
    )


class HostUrl(BaseHostUrl):
    allowed_hosts = SauceConstants.IMAGE_ALLOWED_HOST


class UploadFileIO(BytesIO):
    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v: Any) -> BytesIO:
        if not isinstance(v, BytesIO):
            raise ValueError(f"Expected UploadFile, received: {type(v)}")
        return v


@enum_auto_doc
class DeduplicateType(IntEnum):
    DISABLED = 0
    """no result deduplicating"""
    IDENTIFIER = 1
    """consolidate search results and deduplicate by item identifier"""
    ALL = 2
    """all implemented deduplicate methods such as by series name"""


class SauceEndpoint(BaseEndpoint, cache_endpoints=False):
    base = "https://saucenao.com"

    async def fetch(self, host: HostUrl) -> UploadFileIO:
        try:
            response = await self.client.get(
                url=host,
                headers=SauceConstants.IMAGE_HEADERS,
                timeout=SauceConstants.IMAGE_TIMEOUT,
            )
            response.raise_for_status()
            if len(response.content) > SauceConstants.IMAGE_MAXIMUM_SIZE:
                raise ImageSourceOversizedException
            return UploadFileIO(response.content)
        except HTTPError as e:
            raise UnavailableSourceException(detail=str(e)) from e

    @catch_network_error
    async def request(
        self, *, file: UploadFileIO, params: dict[str, Any]
    ) -> dict[str, Any]:
        response = await self.client.post(
            url=self._join(
                self.base,
                "search.php",
                params={
                    **params,
                    "api_key": random.choice(SauceConstants.API_KEY),
                    "output_type": 2,
                },
            ),
            files={"file": file},
        )
        if response.status_code >= 500:
            response.raise_for_status()
        return response.json()

    @overload
    async def search(
        self,
        *,
        url: HostUrl,
        size: int = 30,
        deduplicate: DeduplicateType = DeduplicateType.ALL,
        database: Optional[int] = None,
        enabled_mask: Optional[int] = None,
        disabled_mask: Optional[int] = None,
    ) -> dict[str, Any]:
        ...

    @overload
    async def search(
        self,
        *,
        file: UploadFileIO,
        size: int = 30,
        deduplicate: DeduplicateType = DeduplicateType.ALL,
        database: Optional[int] = None,
        enabled_mask: Optional[int] = None,
        disabled_mask: Optional[int] = None,
    ) -> dict[str, Any]:
        ...

    async def search(
        self,
        *,
        url: Optional[HostUrl] = None,
        file: Optional[UploadFileIO] = None,
        size: int = 30,
        deduplicate: DeduplicateType = DeduplicateType.ALL,
        database: Optional[int] = None,
        enabled_mask: Optional[int] = None,
        disabled_mask: Optional[int] = None,
    ):
        if url is not None:
            file = await self.fetch(url)
        assert file is not None
        return await self.request(
            file=file,
            params={
                "dbmask": enabled_mask,
                "dbmaski": disabled_mask,
                "db": database,
                "numres": size,
                "dedupe": deduplicate,
            },
        )


================================================
FILE: hibiapi/api/sauce/constants.py
================================================
from typing import Any

from hibiapi.utils.config import APIConfig

_Config = APIConfig("sauce")


class SauceConstants:
    CONFIG: APIConfig = _Config
    API_KEY: list[str] = _Config["net"]["api-key"].as_str_seq()
    USER_AGENT: str = _Config["net"]["user-agent"].as_str()
    PROXIES: dict[str, str] = _Config["proxy"].as_dict()
    IMAGE_HEADERS: dict[str, Any] = _Config["image"]["headers"].as_dict()
    IMAGE_ALLOWED_HOST: list[str] = _Config["image"]["allowed"].get(list[str])
    IMAGE_MAXIMUM_SIZE: int = _Config["image"]["max-size"].as_number() * 1024
    IMAGE_TIMEOUT: int = _Config["image"]["timeout"].as_number()


================================================
FILE: hibiapi/api/sauce/net.py
================================================
from hibiapi.utils.net import BaseNetClient

from .constants import SauceConstants


class NetRequest(BaseNetClient):
    def __init__(self):
        super().__init__(
            headers={"user-agent": SauceConstants.USER_AGENT},
            proxies=SauceConstants.PROXIES,
        )


================================================
FILE: hibiapi/api/tieba/__init__.py
================================================
# flake8:noqa:F401
from .api import Config, TiebaEndpoint
from .net import NetRequest


================================================
FILE: hibiapi/api/tieba/api.py
================================================
import hashlib
from enum import Enum
from random import randint
from typing import Any, Optional

from hibiapi.utils.config import APIConfig
from hibiapi.utils.net import catch_network_error
from hibiapi.utils.routing import BaseEndpoint, dont_route

Config = APIConfig("tieba")


class TiebaSignUtils:
    salt = b"tiebaclient!!!"

    @staticmethod
    def random_digit(length: int) -> str:
        return "".join(map(str, [randint(0, 9) for _ in range(length)]))

    @staticmethod
    def construct_content(params: dict[str, Any]) -> bytes:
        # NOTE: this function used to construct form content WITHOUT urlencode
        # Don't ask me why this is necessary, ask Tieba's programmers instead
        return b"&".join(
            map(
                lambda k, v: (
                    k.encode()
                    + b"="
                    + str(v.value if isinstance(v, Enum) else v).encode()
                ),
                params.keys(),
                params.values(),
            )
        )

    @classmethod
    def sign(cls, params: dict[str, Any]) -> bytes:
        params.update(
            {
                "_client_id": (
                    "wappc_" + cls.random_digit(13) + "_" + cls.random_digit(3)
                ),
                "_client_type": 2,
                "_client_version": "9.9.8.32",
                **{
                    k.upper(): str(v).strip()
                    for k, v in Config["net"]["params"].as_dict().items()
                    if v
                },
            }
        )
        params = {k: params[k] for k in sorted(params.keys())}
        params["sign"] = (
            hashlib.md5(cls.construct_content(params).replace(b"&", b"") + cls.salt)
            .hexdigest()
            .upper()
        )
        return cls.construct_content(params)


class TiebaEndpoint(BaseEndpoint):
    base = "http://c.tieba.baidu.com"

    @dont_route
    @catch_network_error
    async def request(
        self, endpoint: str, *, params: Optional[dict[str, Any]] = None
    ) -> dict[str, Any]:
        response = await self.client.post(
            url=self._join(self.base, endpoint, {}),
            content=TiebaSignUtils.sign(params or {}),
        )
        response.raise_for_status()
        return response.json()

    async def post_list(self, *, name: str, page: int = 1, size: int = 50):
        return await self.request(
            "c/f/frs/page",
            params={
                "kw": name,
                "pn": page,
                "rn": size,
            },
        )

    async def post_detail(
        self,
        *,
        tid: int,
        page: int = 1,
        size: int = 50,
        reversed: bool = False,
    ):
        return await self.request(
            "c/f/pb/page",
            params={
                **({"last": 1, "r": 1} if reversed else {}),
                "kz": tid,
                "pn": page,
                "rn": size,
            },
        )

    async def subpost_detail(
        self,
        *,
        tid: int,
        pid: int,
        page: int = 1,
        size: int = 50,
    ):
        return await self.request(
            "c/f/pb/floor",
            params={
                "kz": tid,
                "pid": pid,
                "pn": page,
                "rn": size,
            },
        )

    async def user_profile(self, *, uid: int):
        return await self.request(
            "c/u/user/profile",
            params={
                "uid": uid,
                "need_post_count": 1,
                "has_plist": 1,
            },
        )

    async def user_subscribed(
        self, *, uid: int, page: int = 1
    ):  # XXX This API required user login!
        return await self.request(
            "c/f/forum/like",
            params={
                "is_guest": 0,
                "uid": uid,
                "page_no": page,
            },
        )


================================================
FILE: hibiapi/api/tieba/net.py
================================================
from hibiapi.utils.net import BaseNetClient


class NetRequest(BaseNetClient):
    pass


================================================
FILE: hibiapi/api/wallpaper/__init__.py
================================================
# flake8:noqa:F401
from .api import Config, WallpaperCategoryType, WallpaperEndpoint, WallpaperOrderType
from .net import NetRequest


================================================
FILE: hibiapi/api/wallpaper/api.py
================================================
from datetime import timedelta
from enum import Enum
from typing import Any, Optional

from hibiapi.utils.cache import cache_config
from hibiapi.utils.config import APIConfig
from hibiapi.utils.decorators import enum_auto_doc
from hibiapi.utils.net import catch_network_error
from hibiapi.utils.routing import BaseEndpoint, dont_route

Config = APIConfig("wallpaper")


@enum_auto_doc
class WallpaperCategoryType(str, Enum):
    """壁纸分类"""

    girl = "girl"
    """女生"""
    animal = "animal"
    """动物"""
    landscape = "landscape"
    """自然"""
    anime = "anime"
    """二次元"""
    drawn = "drawn"
    """手绘"""
    mechanics = "mechanics"
    """机械"""
    boy = "boy"
    """男生"""
    game = "game"
    """游戏"""
    text = "text"
    """文字"""


CATEGORY: dict[WallpaperCategoryType, str] = {
    WallpaperCategoryType.girl: "4e4d610cdf714d2966000000",
    WallpaperCategoryType.animal: "4e4d610cdf714d2966000001",
    WallpaperCategoryType.landscape: "4e4d610cdf714d2966000002",
    WallpaperCategoryType.anime: "4e4d610cdf714d2966000003",
    WallpaperCategoryType.drawn: "4e4d610cdf714d2966000004",
    WallpaperCategoryType.mechanics: "4e4d610cdf714d2966000005",
    WallpaperCategoryType.boy: "4e4d610cdf714d2966000006",
    WallpaperCategoryType.game: "4e4d610cdf714d2966000007",
    WallpaperCategoryType.text: "5109e04e48d5b9364ae9ac45",
}


@enum_auto_doc
class WallpaperOrderType(str, Enum):
    """壁纸排序方式"""

    hot = "hot"
    """热门"""
    new = "new"
    """最新"""


class WallpaperEndpoint(BaseEndpoint):
    base = "http://service.aibizhi.adesk.com"

    @dont_route
    @catch_network_error
    async def request(
        self, endpoint: str, *, params: Optional[dict[str, Any]] = None
    ) -> dict[str, Any]:

        response = await self.client.get(
            self._join(
                base=WallpaperEndpoint.base,
                endpoint=endpoint,
                params=params or {},
            )
        )
        return response.json()

    # 壁纸有防盗链token, 不建议长时间缓存
    @cache_config(ttl=timedelta(hours=2))
    async def wallpaper(
        self,
        *,
        category: WallpaperCategoryType,
        limit: int = 20,
        skip: int = 0,
        adult: bool = True,
        order: WallpaperOrderType = WallpaperOrderType.hot,
    ):

        return await self.request(
            "v1/wallpaper/category/{category}/wallpaper",
            params={
                "limit": limit,
                "skip": skip,
                "adult": adult,
                "order": order,
                "first": 0,
                "category": CATEGORY[category],
            },
        )

    # 壁纸有防盗链token, 不建议长时间缓存
    @cache_config(ttl=timedelta(hours=2))
    async def vertical(
        self,
        *,
        category: WallpaperCategoryType,
        limit: int = 20,
        skip: int = 0,
        adult: bool = True,
        order: WallpaperOrderType = WallpaperOrderType.hot,
    ):

        return await self.request(
            "v1/vertical/category/{category}/vertical",
            params={
                "limit": limit,
                "skip": skip,
                "adult": adult,
                "order": order,
                "first": 0,
                "category": CATEGORY[category],
            },
        )


================================================
FILE: hibiapi/api/wallpaper/constants.py
================================================
from hibiapi.utils.config import APIConfig

_CONFIG = APIConfig("wallpaper")


class WallpaperConstants:
    CONFIG: APIConfig = _CONFIG
    USER_AGENT: str = _CONFIG["net"]["user-agent"].as_str()


================================================
FILE: hibiapi/api/wallpaper/net.py
================================================
from hibiapi.utils.net import BaseNetClient

from .constants import WallpaperConstants


class NetRequest(BaseNetClient):
    def __init__(self):
        super().__init__(headers={"user-agent": WallpaperConstants.USER_AGENT})


================================================
FILE: hibiapi/app/__init__.py
================================================
# flake8:noqa:F401
from . import application, handlers, middlewares

app = application.app


================================================
FILE: hibiapi/app/application.py
================================================
import asyncio
import re
from contextlib import asynccontextmanager
from ipaddress import ip_address
from secrets import compare_digest
from typing import Annotated

import sentry_sdk
from fastapi import Depends, FastAPI, Request, Response
from fastapi.responses import RedirectResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from sentry_sdk.integrations.logging import LoggingIntegration

from hibiapi import __version__
from hibiapi.app.routes import router as ImplRouter
from hibiapi.utils.cache import cache
from hibiapi.utils.config import Config
from hibiapi.utils.exceptions import ClientSideException, RateLimitReachedException
from hibiapi.utils.log import logger
from hibiapi.utils.net import BaseNetClient
from hibiapi.utils.temp import TempFile

DESCRIPTION = (
    """
**A program that implements easy-to-use APIs for a variety of commonly used sites**

- *Documents*:
    - [Redoc](/docs) (Easier to read and more beautiful)
    - [Swagger UI](/docs/test) (Integrated interactive testing function)

Project: [mixmoe/HibiAPI](https://github.com/mixmoe/HibiAPI)

"""
    + Config["content"]["slogan"].as_str().strip()
).strip()


if Config["log"]["sentry"]["enabled"].as_bool():
    sentry_sdk.init(
        dsn=Config["log"]["sentry"]["dsn"].as_str(),
        send_default_pii=Config["log"]["sentry"]["pii"].as_bool(),
        integrations=[LoggingIntegration(level=None, event_level=None)],
        traces_sample_rate=Config["log"]["sentry"]["sample"].get(float),
    )
else:
    sentry_sdk.init()


class AuthorizationModel(BaseModel):
    username: str
    password: str


AUTHORIZATION_ENABLED = Config["authorization"]["enabled"].as_bool()
AUTHORIZATION_ALLOWED = Config["authorization"]["allowed"].get(list[AuthorizationModel])

security = HTTPBasic()


async def basic_authorization_depend(
    credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
    # NOTE: We use `compare_digest` to avoid timing attacks.
    # Ref: https://fastapi.tiangolo.com/advanced/security/http-basic-auth/
    for allowed in AUTHORIZATION_ALLOWED:
        if compare_digest(credentials.username, allowed.username) and compare_digest(
            credentials.password, allowed.password
        ):
            return credentials.username, credentials.password
    raise ClientSideException(
        f"Invalid credentials for user {credentials.username!r}",
        status_code=401,
        headers={"WWW-Authenticate": "Basic"},
    )


RATE_LIMIT_ENABLED = Config["limit"]["enabled"].as_bool()
RATE_LIMIT_MAX = Config["limit"]["max"].as_number()
RATE_LIMIT_INTERVAL = Config["limit"]["interval"].as_number()


async def rate_limit_depend(request: Request):
    if not request.client:
        return

    try:
        client_ip = ip_address(request.client.host)
        client_ip_hex = client_ip.packed.hex()
        limit_key = f"rate_limit:IPv{client_ip.version}-{client_ip_hex:x}"
    except ValueError:
        limit_key = f"rate_limit:fallback-{request.client.host}"

    request_count = await cache.incr(limit_key)
    if request_count <= 1:
        await cache.expire(limit_key, timeout=RATE_LIMIT_INTERVAL)
    elif request_count > RATE_LIMIT_MAX:
        limit_remain: int = await cache.get_expire(limit_key)
        raise RateLimitReachedException(headers={"Retry-After": limit_remain})

    return


async def flush_sentry():
    client = sentry_sdk.Hub.current.client
    if client is not None:
        client.close()
    sentry_sdk.flush()
    logger.debug("Sentry client has been closed")


async def cleanup_clients():
    opened_clients = [
        client for client in BaseNetClient.clients if not client.is_closed
    ]
    if opened_clients:
        await asyncio.gather(
            *map(lambda client: client.aclose(), opened_clients),
            return_exceptions=True,
        )
    logger.debug(f"Cleaned <r>{len(opened_clients)}</r> unclosed HTTP clients")


@asynccontextmanager
async def fastapi_lifespan(app: FastAPI):
    yield
    await asyncio.gather(cleanup_clients(), flush_sentry())


app = FastAPI(
    title="HibiAPI",
    version=__version__,
    description=DESCRIPTION,
    docs_url="/docs/test",
    redoc_url="/docs",
    lifespan=fastapi_lifespan,
)
app.include_router(
    ImplRouter,
    prefix="/api",
    dependencies=(
        ([Depends(basic_authorization_depend)] if AUTHORIZATION_ENABLED else [])
        + ([Depends(rate_limit_depend)] if RATE_LIMIT_ENABLED else [])
    ),
)
app.mount("/temp", StaticFiles(directory=TempFile.path, check_dir=False))


@app.get("/", include_in_schema=False)
async def redirect():
    return Response(status_code=302, headers={"Location": "/docs"})


@app.get("/robots.txt", include_in_schema=False)
async def robots():
    content = Config["content"]["robots"].as_str().strip()
    return Response(content, status_code=200)


@app.middleware("http")
async def redirect_workaround_middleware(request: Request, call_next):
    """Temporary redirection workaround for #12"""
    if matched := re.match(
        r"^/(qrcode|pixiv|netease|bilibili)/(\w*)$", request.url.path
    ):
        service, path = matched.groups()
        redirect_url = request.url.replace(path=f"/api/{service}/{path}")
        return RedirectResponse(redirect_url, status_code=301)
    return await call_next(request)


================================================
FILE: hibiapi/app/handlers.py
================================================
from fastapi import Request, Response
from fastapi.exceptions import HTTPException as FastAPIHTTPException
from fastapi.exceptions import RequestValidationError as FastAPIValidationError
from pydantic.error_wrappers import ValidationError as PydanticValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException

from hibiapi.utils import exceptions
from hibiapi.utils.log import logger

from .application import app


@app.exception_handler(exceptions.BaseServerException)
async def exception_handler(
    request: Request,
    exc: exceptions.BaseServerException,
) -> Response:
    if isinstance(exc, exceptions.UncaughtException):
        logger.opt(exception=exc).exception(f"Uncaught exception raised {exc.data=}:")

    exc.data.url = str(request.url)  # type:ignore
    return Response(
        content=exc.data.json(),
        status_code=exc.data.code,
        headers=exc.data.headers,
        media_type="application/json",
    )


@app.exception_handler(StarletteHTTPException)
async def override_handler(
    request: Request,
    exc: StarletteHTTPException,
):
    return await exception_handler(
        request,
        exceptions.BaseHTTPException(
            exc.detail,
            code=exc.status_code,
            headers={} if not isinstance(exc, FastAPIHTTPException) else exc.headers,
        ),
    )


@app.exception_handler(AssertionError)
async def assertion_handler(request: Request, exc: AssertionError):
    return await exception_handler(
        request,
        exceptions.ClientSideException(detail=f"Assertion: {exc}"),
    )


@app.exception_handler(FastAPIValidationError)
@app.exception_handler(PydanticValidationError)
async def validation_handler(request: Request, exc: PydanticValidationError):
    return await exception_handler(
        request,
        exceptions.ValidationException(detail=str(exc), validation=exc.errors()),
    )


================================================
FILE: hibiapi/app/middlewares.py
================================================
from collections.abc import Awaitable
from datetime import datetime
from typing import Callable

from fastapi import Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from sentry_sdk.integrations.httpx import HttpxIntegration
from starlette.datastructures import MutableHeaders

from hibiapi.utils.config import Config
from hibiapi.utils.exceptions import BaseServerException, UncaughtException
from hibiapi.utils.log import LoguruHandler, logger
from hibiapi.utils.routing import request_headers, response_headers

from .application import app
from .handlers import exception_handler

RequestHandler = Callable[[Request], Awaitable[Response]]


if Config["server"]["gzip"].as_bool():
    app.add_middleware(GZipMiddleware)
app.add_middleware(
    CORSMiddleware,
    allow_origins=Config["server"]["cors"]["origins"].get(list[str]),
    allow_credentials=Config["server"]["cors"]["credentials"].as_bool(),
    allow_methods=Config["server"]["cors"]["methods"].get(list[str]),
    allow_headers=Config["server"]["cors"]["headers"].get(list[str]),
)
app.add_middleware(
    TrustedHostMiddleware,
    allowed_hosts=Config["server"]["allowed"].get(list[str]),
)
app.add_middleware(SentryAsgiMiddleware)

HttpxIntegration.setup_once()


@app.middleware("http")
async def request_logger(request: Request, call_next: RequestHandler) -> Response:
    start_time = datetime.now()
    host, port = request.client or (None, None)
    response = await call_next(request)
    process_time = (datetime.now() - start_time).total_seconds() * 1000
    response_headers.get().setdefault("X-Process-Time", f"{process_time:.3f}")
    bg, fg = (
        ("green", "red")
        if response.status_code < 400
        else ("yellow", "blue")
        if response.status_code < 500
        else ("red", "green")
    )
    status_code, method = response.status_code, request.method.upper()
    user_agent = (
        LoguruHandler.escape_tag(request.headers["user-agent"])
        if "user-agent" in request.headers
        else "<d>Unknown</d>"
    )
    logger.info(
        f"<m><b>{host}</b>:{port}</m>"
        f" | <{bg.upper()}><b><{fg}>{method}</{fg}></b></{bg.upper()}>"
        f" | <n><b>{str(request.url)!r}</b></n>"
        f" | <c>{process_time:.3f}ms</c>"
        f" | <e>{user_agent}</e>"
        f" | <b><{bg}>{status_code}</{bg}></b>"
    )
    return response


@app.middleware("http")
async def contextvar_setter(request: Request, call_next: RequestHandler):
    request_headers.set(request.headers)
    response_headers.set(MutableHeaders())
    response = await call_next(request)
    response.headers.update({**response_headers.get()})
    return response


@app.middleware("http")
async def uncaught_exception_handler(
    request: Request, call_next: RequestHandler
) -> Response:
    try:
        response = await call_next(request)
    except Exception as error:
        response = await exception_handler(
            request,
            exc=(
                error
                if isinstance(error, BaseServerException)
                else UncaughtException.with_exception(error)
            ),
        )
    return response


================================================
FILE: hibiapi/app/routes/__init__.py
================================================
from typing import Protocol, cast

from hibiapi.app.routes import (
    bika,
    bilibili,
    netease,
    pixiv,
    qrcode,
    sauce,
    tieba,
    wallpaper,
)
from hibiapi.utils.config import APIConfig
from hibiapi.utils.exceptions import ExceptionReturn
from hibiapi.utils.log import logger
from hibiapi.utils.routing import SlashRouter

router = SlashRouter(
    responses={
        code: {
            "model": ExceptionReturn,
        }
        for code in (400, 422, 500, 502)
    }
)


class RouteInterface(Protocol):
    router: SlashRouter
    __mount__: str
    __config__: APIConfig


modules = cast(
    list[RouteInterface],
    [bilibili, netease, pixiv, qrcode, sauce, tieba, wallpaper, bika],
)

for module in modules:
    mount = (
        mount_point
        if (mount_point := module.__mount__).startswith("/")
        else f"/{mount_point}"
    )

    if not module.__config__["enabled"].as_bool():
        logger.warning(
            f"API Route <y><b>{mount}</b></y> has been "
            "<r><b>disabled</b></r> in config."
        )
        continue
    router.include_router(module.router, prefix=mount)


================================================
FILE: hibiapi/app/routes/bika.py
================================================
from typing import Annotated

from fastapi import Depends, Header

from hibiapi.api.bika import (
    BikaConstants,
    BikaEndpoints,
    BikaLogin,
    ImageQuality,
    NetRequest,
)
from hibiapi.utils.log import logger
from hibiapi.utils.routing import EndpointRouter

try:
    BikaConstants.CONFIG["account"].get(BikaLogin)
except Exception as e:
    logger.warning(f"Bika account misconfigured: {e}")
    BikaConstants.CONFIG["enabled"].set(False)


async def x_image_quality(
    x_image_quality: Annotated[ImageQuality, Header()] = ImageQuality.medium,
):
    if x_image_quality is None:
        return BikaConstants.CONFIG["image_quality"].get(ImageQuality)
    return x_image_quality


__mount__, __config__ = "bika", BikaConstants.CONFIG
router = EndpointRouter(tags=["Bika"], dependencies=[Depends(x_image_quality)])

BikaAPIRoot = NetRequest()


router.include_endpoint(BikaEndpoints, BikaAPIRoot)


================================================
FILE: hibiapi/app/routes/bilibili/__init__.py
================================================
from hibiapi.api.bilibili import BilibiliConstants
from hibiapi.app.routes.bilibili.v2 import router as RouterV2
from hibiapi.app.routes.bilibili.v3 import router as RouterV3
from hibiapi.utils.routing import SlashRouter

__mount__, __config__ = "bilibili", BilibiliConstants.CONFIG

router = SlashRouter()
router.include_router(RouterV2, prefix="/v2")
router.include_router(RouterV3, prefix="/v3")


================================================
FILE: hibiapi/app/routes/bilibili/v2.py
================================================
from hibiapi.api.bilibili.api import BilibiliEndpointV2
from hibiapi.api.bilibili.net import NetRequest
from hibiapi.utils.routing import EndpointRouter

router = EndpointRouter(tags=["Bilibili V2"])
router.include_endpoint(BilibiliEndpointV2, NetRequest())


================================================
FILE: hibiapi/app/routes/bilibili/v3.py
================================================
from hibiapi.api.bilibili import BilibiliEndpointV3, NetRequest
from hibiapi.utils.routing import EndpointRouter

router = EndpointRouter(tags=["Bilibili V3"])
router.include_endpoint(BilibiliEndpointV3, NetRequest())


================================================
FILE: hibiapi/app/routes/netease.py
================================================
from hibiapi.api.netease import NeteaseConstants, NeteaseEndpoint, NetRequest
from hibiapi.utils.routing import EndpointRouter

__mount__, __config__ = "netease", NeteaseConstants.CONFIG

router = EndpointRouter(tags=["Netease"])
router.include_endpoint(NeteaseEndpoint, NetRequest())


================================================
FILE: hibiapi/app/routes/pixiv.py
================================================
from typing import Optional

from fastapi import Depends, Header

from hibiapi.api.pixiv import NetRequest, PixivConstants, PixivEndpoints
from hibiapi.utils.log import logger
from hibiapi.utils.routing import EndpointRouter

if not (refresh_tokens := PixivConstants.CONFIG["account"]["token"].as_str_seq()):
    logger.warning("Pixiv API token is not set, pixiv endpoint will be unavailable.")
    PixivConstants.CONFIG["enabled"].set(False)


async def accept_language(
    accept_language: Optional[str] = Header(
        None,
        description="Accepted tag translation language",
    )
):
    return accept_language


__mount__, __config__ = "pixiv", PixivConstants.CONFIG

router = EndpointRouter(tags=["Pixiv"], dependencies=[Depends(accept_language)])
router.include_endpoint(PixivEndpoints, api_root := NetRequest(refresh_tokens))


================================================
FILE: hibiapi/app/routes/qrcode.py
================================================
from typing import Optional

from fastapi import Request, Response
from pydantic.color import Color

from hibiapi.api.qrcode import (
    COLOR_BLACK,
    COLOR_WHITE,
    Config,
    HostUrl,
    QRCodeLevel,
    QRInfo,
    ReturnEncode,
)
from hibiapi.utils.routing import SlashRouter
from hibiapi.utils.temp import TempFile

QR_CALLBACK_TEMPLATE = (
    r"""function {fun}(){document.write('<img class="qrcode" src="{url}"/>');}"""
)

__mount__, __config__ = "qrcode", Config
router = SlashRouter(tags=["QRCode"])


@router.get(
    "/",
    responses={
        200: {
            "content": {"image/png": {}, "text/javascript": {}, "application/json": {}},
            "description": "Avaliable to return an javascript, image or json.",
        }
    },
    response_model=QRInfo,
)
async def qrcode_api(
    request: Request,
    *,
    text: str,
    size: int = 200,
    logo: Optional[HostUrl] = None,
    encode: ReturnEncode = ReturnEncode.raw,
    level: QRCodeLevel = QRCodeLevel.MEDIUM,
    bgcolor: Color = COLOR_BLACK,
    fgcolor: Color = COLOR_WHITE,
    fun: str = "qrcode",
):
    qr = await QRInfo.new(
        text, size=size, logo=logo, level=level, bgcolor=bgcolor, fgcolor=fgcolor
    )
    qr.url = TempFile.to_url(request, qr.path)  # type:ignore
    """function {fun}(){document.write('<img class="qrcode" src="{url}"/>');}"""
    return (
        qr
        if encode == ReturnEncode.json
        else Response(
            content=qr.json(),
            media_type="application/json",
            headers={"Location": qr.url},
            status_code=302,
        )
        if encode == ReturnEncode.raw
        else Response(
            content=f"{fun}({qr.json()})",
            media_type="text/javascript",
        )
        if encode == ReturnEncode.jsc
        else Response(
            content="function "
            + fun
            + '''(){document.write('<img class="qrcode" src="'''
            + qr.url
            + """"/>');}""",
            media_type="text/javascript",
        )
    )


================================================
FILE: hibiapi/app/routes/sauce.py
================================================
from typing import Annotated, Optional

from fastapi import Depends, File, Form
from loguru import logger

from hibiapi.api.sauce import (
    DeduplicateType,
    HostUrl,
    NetRequest,
    SauceConstants,
    SauceEndpoint,
    UploadFileIO,
)
from hibiapi.utils.routing import SlashRouter

if (not SauceConstants.API_KEY) or (not all(map(str.strip, SauceConstants.API_KEY))):
    logger.warning("Sauce API key not set, SauceNAO endpoint will be unavailable")
    SauceConstants.CONFIG["enabled"].set(False)

__mount__, __config__ = "sauce", SauceConstants.CONFIG
router = SlashRouter(tags=["SauceNAO"])

SauceAPIRoot = NetRequest()


async def request_client():
    async with SauceAPIRoot as client:
        yield SauceEndpoint(client)


@router.get("/")
async def sauce_url(
    endpoint: Annotated[SauceEndpoint, Depends(request_client)],
    url: HostUrl,
    size: int = 30,
    deduplicate: DeduplicateType = DeduplicateType.ALL,
    database: Optional[int] = None,
    enabled_mask: Optional[int] = None,
    disabled_mask: Optional[int] = None,
):
    """
    ## Name: `sauce_url`

    > 使用SauceNAO检索网络图片

    ---

    ### Required:

    - ***HostUrl*** **`url`**
        - Description: 图片URL

    ---

    ### Optional:
    - ***int*** `size` = `30`
        - Description: 搜索结果数目
    - ***DeduplicateType*** `deduplicate` = `DeduplicateType.ALL`
        - Description: 结果去重模式
    - ***Optional[int]*** `database` = `None`
        - Description: 检索的数据库ID, 999为全部检索
    - ***Optional[int]*** `enabled_mask` = `None`
        - Description: 启用的检索数据库
    - ***Optional[int]*** `disabled_mask` = `None`
        - Description: 禁用的检索数据库
    """
    return await endpoint.search(
        url=url,
        size=size,
        deduplicate=deduplicate,
        database=database,
        enabled_mask=enabled_mask,
        disabled_mask=disabled_mask,
    )


@router.post("/")
async def sauce_form(
    endpoint: Annotated[SauceEndpoint, Depends(request_client)],
    file: bytes = File(..., max_length=SauceConstants.IMAGE_MAXIMUM_SIZE),
    size: int = Form(30),
    deduplicate: Annotated[DeduplicateType, Form()] = DeduplicateType.ALL,
    database: Optional[int] = Form(None),
    enabled_mask: Optional[int] = Form(None),
    disabled_mask: Optional[int] = Form(None),
):
    """
    ## Name: `sauce_form`

    > 使用SauceNAO检索表单上传图片

    ---

    ### Required:
    - ***bytes*** `file`
        - Description: 上传的图片

    ---

    ### Optional:
    - ***int*** `size` = `30`
        - Description: 搜索结果数目
    - ***DeduplicateType*** `deduplicate` = `DeduplicateType.ALL`
        - Description: 结果去重模式
    - ***Optional[int]*** `database` = `None`
        - Description: 检索的数据库ID, 999为全部检索
    - ***Optional[int]*** `enabled_mask` = `None`
        - Description: 启用的检索数据库
    - ***Optional[int]*** `disabled_mask` = `None`
        - Description: 禁用的检索数据库

    """
    return await endpoint.search(
        file=UploadFileIO(file),
        size=size,
        deduplicate=deduplicate,
        database=database,
        disabled_mask=disabled_mask,
        enabled_mask=enabled_mask,
    )


================================================
FILE: hibiapi/app/routes/tieba.py
================================================
from hibiapi.api.tieba import Config, NetRequest, TiebaEndpoint
from hibiapi.utils.routing import EndpointRouter

__mount__, __config__ = "tieba", Config

router = EndpointRouter(tags=["Tieba"])
router.include_endpoint(TiebaEndpoint, NetRequest())


================================================
FILE: hibiapi/app/routes/wallpaper.py
================================================
from hibiapi.api.wallpaper import Config, NetRequest, WallpaperEndpoint
from hibiapi.utils.routing import EndpointRouter

__mount__, __config__ = "wallpaper", Config

router = EndpointRouter(tags=["Wallpaper"])
router.include_endpoint(WallpaperEndpoint, NetRequest())


================================================
FILE: hibiapi/configs/bika.yml
================================================
enabled: true

proxy: {}

account:
  # 请在此处填写你的哔咔账号密码
  email:
  password:


================================================
FILE: hibiapi/configs/bilibili.yml
================================================
enabled: true

net:
  cookie: > # Bilibili的Cookie, 在一些需要用户登录的场景下需要
    DedeUserID=; 
    DedeUserID__ckMd5=; 
    SESSDATA=;
  user-agent: "Mozilla/5.0 (mixmoe@GitHub.com/HibiAPI) Chrome/114.514.1919810" # UA头, 一般没必要改


================================================
FILE: hibiapi/configs/general.yml
================================================
#   _    _ _ _     _          _____ _____
#  | |  | (_) |   (_)   /\   |  __ \_   _|
#  | |__| |_| |__  _   /  \  | |__) || |
#  |  __  | | '_ \| | / /\ \ |  ___/ | |
#  | |  | | | |_) | |/ ____ \| |    _| |_
#  |_|  |_|_|_.__/|_/_/    \_\_|   |_____|
#
# An alternative implement of Imjad API

data:
  temp-expiry: 7 # 临时文件目录文件过期时间, 单位为天
  path: ./data # data目录所在位置

server:
  host: 127.0.0.1 # 监听主机
  port: 8080 # 端口
  gzip: true

  # 限定来源域名, 支持通配符, 参考:
  # https://fastapi.tiangolo.com/advanced/middleware/#trustedhostmiddleware
  allowed: ["*"]

  cors:
    origins:
      - "http://localhost.tiangolo.com"
      - "https://localhost.tiangolo.com"
      - "http://localhost"
      - "http://localhost:8080"
    credentials: true
    methods: ["*"]
    headers: ["*"]

  allowed-forward: null # Reference: https://stackoverflow.com/questions/63511413

limit: # 单IP速率限制策略
  enabled: true
  max: 60 # 每个单位时间内最大请求数
  interval: 60 # 单位时间长度, 单位为秒

cache:
  enabled: true # 设置是否启用缓存
  ttl: 3600 # 缓存默认生存时间, 单位为秒
  uri: "mem://" # 缓存URI
  controllable: true # 配置是否可以通过Cache-Control请求头刷新缓存

log:
  level: INFO # 日志等级, 可选 [TRACE,DEBUG,INFO,WARNING,ERROR]
  format: > # 输出日志格式, 如果没有必要请不要修改
    <level>
    <v>{level:<8}</v>
    [{time:YYYY/MM/DD} {time:HH:mm:ss.SSS} <d>{module}:{name}:{line}</d>]</level>
    {message}

  # file: logs/{time.log}
  file: null # 日志输出文件位置, 相对于data目录, 为空则不保存

  sentry:
    enabled: false
    sample: 1
    dsn: ""
    pii: false

content:
  slogan: | # 在文档附加的标语, 可以用于自定义内容
    ![](https://img.shields.io/github/stars/mixmoe/HibiAPI?color=brightgreen&logo=github&style=for-the-badge)
  robots: | # 提供的robots.txt内容, 用于提供搜索引擎抓取
    User-agent: *
    Disallow: /api/

authorization:
  enabled: false # 是否开启验证
  allowed:
    - username: admin # 用户名
      password: admin # 密码


================================================
FILE: hibiapi/configs/netease.yml
================================================
enabled: true

net:
  cookie: > # 网易云的Cookie, 可能有些API需要
    os=pc;
    osver=Microsoft-Windows-10-Professional-build-10586-64bit; 
    appver=2.0.3.131777; 
    channel=netease; 
    __remember_me=true
  user-agent: "Mozilla/5.0 (mixmoe@GitHub.com/HibiAPI) Chrome/114.514.1919810" # UA头, 一般没必要改
  source: 118.88.64.0/18 # 伪造来源IP以绕过地区限制 #68


================================================
FILE: hibiapi/configs/pixiv.yml
================================================
enabled: true

# HTTP代理地址
# 示例格式
# proxy: { "all://": "http://127.0.0.1:1081" }
proxy: {}

account:
  # Pixiv 登录凭证刷新令牌 (Refresh Token)
  # 获取方法请参考: https://github.com/mixmoe/HibiAPI/issues/53
  # 支持使用多个账户进行负载均衡, 每行一个token
  token: ""

language: zh-cn # 返回语言, 会影响标签的翻译


================================================
FILE: hibiapi/configs/qrcode.yml
================================================
enabled: true

qrcode:
  max-size: 1000 # 允许的二维码最大尺寸, 单位像素
  min-size: 50 # 允许的二维码最小尺寸, 单位像素
  icon-site: # 图标支持的站点, 可以阻止服务器ip泄漏, 支持通配符
    - localhost
    - i.loli.net
    # - "*"


================================================
FILE: hibiapi/configs/sauce.yml
================================================
enabled: true

# HTTP代理地址
# 示例格式
# proxy:
#   http_proxy: http://127.0.0.1:1081
#   https_proxy: https://127.0.0.1:1081
proxy: {}

net:
  # SauceNAO 的API KEY, 支持多个以进行负载均衡, 每个KEY以换行分隔
  # api-key: |
  #   aaaaaaa
  #   bbbbbbb
  api-key: ""

  keys: # SauceNAO 的API KEY, 支持多个以进行负载均衡
    - ""
  user-agent: &ua "Mozilla/5.0 (mixmoe@GitHub.com/HibiAPI) Chrome/114.514.1919810" # UA头, 一般没必要改

image:
  max-size: 4096 # 获取图片最大大小, 单位为 KBytes
  timeout: 6 # 获取图片超时时间, 单位为秒
  headers: { "user-agent": *ua } # 获取图片时携带的请求头
  allowed: # 获取图片的站点白名单, 可以阻止服务器ip泄漏, 支持通配符
    - localhost
    - i.loli.net
    # - "*"


================================================
FILE: hibiapi/configs/tieba.yml
================================================
enabled: true

net:
  user-agent: "Mozilla/5.0 (mixmoe@GitHub.com/HibiAPI) Chrome/114.514.1919810" # UA头, 一般没必要改
  params:
    BDUSS: "" # 百度的BDUSS登录凭证, 在使用部分API时需要


================================================
FILE: hibiapi/configs/wallpaper.yml
================================================
enabled: true

net:
  user-agent: "Mozilla/5.0 (mixmoe@GitHub.com/HibiAPI) Chrome/114.514.1919810" # UA头, 一般没必要改


================================================
FILE: hibiapi/utils/__init__.py
================================================


================================================
FILE: hibiapi/utils/cache.py
================================================
import hashlib
from collections.abc import Awaitable
from datetime import timedelta
from functools import wraps
from typing import Any, Callable, Optional, TypeVar, cast

from cashews import Cache
from pydantic import BaseModel
from pydantic.decorator import ValidatedFunction

from .config import Config
from .log import logger

CACHE_CONFIG_KEY = "_cache_config"

AsyncFunc = Callable[..., Awaitable[Any]]
T_AsyncFunc = TypeVar("T_AsyncFunc", bound=AsyncFunc)


CACHE_ENABLED = Config["cache"]["enabled"].as_bool()
CACHE_DELTA = timedelta(seconds=Config["cache"]["ttl"].as_number())
CACHE_URI = Config["cache"]["uri"].as_str()
CACHE_CONTROLLABLE = Config["cache"]["controllable"].as_bool()

cache = Cache(name="hibiapi")
try:
    cache.setup(CACHE_URI)
except Exception as e:
    logger.warning(
        f"Cache URI <y>{CACHE_URI!r}</y> setup <r><b>failed</b></r>: "
        f"<r>{e!r}</r>, use memory backend instead."
    )


class CacheConfig(BaseModel):
    endpoint: AsyncFunc
    namespace: str
    enabled: bool = True
    ttl: timedelta = CACHE_DELTA

    @staticmethod
    def new(
        function: AsyncFunc,
        *,
        enabled: bool = True,
        ttl: timedelta = CACHE_DELTA,
        namespace: Optional[str] = None,
    ):
        return CacheConfig(
            endpoint=function,
            enabled=enabled,
            ttl=ttl,
            namespace=namespace or function.__qualname__,
        )


def cache_config(
    enabled: bool = True,
    ttl: timedelta = CACHE_DELTA,
    namespace: Optional[str] = None,
):
    def decorator(function: T_AsyncFunc) -> T_AsyncFunc:
        setattr(
            function,
            CACHE_CONFIG_KEY,
            CacheConfig.new(function, enabled=enabled, ttl=ttl, namespace=namespace),
        )
        return function

    return decorator


disable_cache = cache_config(enabled=False)


class CachedValidatedFunction(ValidatedFunction):
    def serialize(self, args: tuple[Any, ...], kwargs: dict[str, Any]) -> BaseModel:
        values = self.build_values(args=args, kwargs=kwargs)
        return self.model(**values)


def endpoint_cache(function: T_AsyncFunc) -> T_AsyncFunc:
    from .routing import request_headers, response_headers

    vf = CachedValidatedFunction(function, config={})
    config = cast(
        CacheConfig,
        getattr(function, CACHE_CONFIG_KEY, None) or CacheConfig.new(function),
    )

    config.enabled = CACHE_ENABLED and config.enabled

    @wraps(function)
    async def wrapper(*args, **kwargs):
        cache_policy = "public"

        if CACHE_CONTROLLABLE:
            cache_policy = request_headers.get().get("cache-control", cache_policy)

        if not config.enabled or cache_policy.casefold() == "no-store":
            return await vf.call(*args, **kwargs)

        key = (
            f"{config.namespace}:"
            + hashlib.md5(
                (model := vf.serialize(args=args, kwargs=kwargs))
                .json(exclude={"self"}, sort_keys=True, ensure_ascii=False)
                .encode()
            ).hexdigest()
        )

        response_header = response_headers.get()
        result: Optional[Any] = None

        if cache_policy.casefold() == "no-cache":
            await cache.delete(key)
        elif result := await cache.get(key):
            logger.debug(f"Request hit cache <b><e>{key}</e></b>")
            response_header.setdefault("X-Cache-Hit", key)

        if result is None:
            result = await vf.execute(model)
            await cache.set(key, result, expire=config.ttl)

        if (cache_remain := await cache.get_expire(key)) > 0:
            response_header.setdefault("Cache-Control", f"max-age={cache_remain}")

        return result

    return wrapper  # type:ignore


================================================
FILE: hibiapi/utils/config.py
================================================
import json
import os
from pathlib import Path
from typing import Any, Optional, TypeVar, overload

import confuse
import dotenv
from pydantic import parse_obj_as

from hibiapi import __file__ as root_file

CONFIG_DIR = Path(".") / "configs"
DEFAULT_DIR = Path(root_file).parent / "configs"

_T = TypeVar("_T")


class ConfigSubView(confuse.Subview):
    @overload
    def get(self) -> Any: ...

    @overload
    def get(self, template: type[_T]) -> _T: ...

    def get(self, template: Optional[type[_T]] = None):  # type: ignore
        object_ = super().get()
        if template is not None:
            return parse_obj_as(template, object_)
        return object_

    def get_optional(self, template: type[_T]) -> Optional[_T]:
        try:
            return self.get(template)
        except Exception:
            return None

    def as_str(self) -> str:
        return self.get(str)

    def as_str_seq(self, split: str = "\n") -> list[str]:  # type: ignore
        return [
            stripped
            for line in self.as_str().strip().split(split)
            if (stripped := line.strip())
        ]

    def as_number(self) -> int:
        return self.get(int)

    def as_bool(self) -> bool:
        return self.get(bool)

    def as_path(self) -> Path:
        return self.get(Path)

    def as_dict(self) -> dict[str, Any]:
        return self.get(dict[str, Any])

    def __getitem__(self, key: str) -> "ConfigSubView":
        return self.__class__(self, key)


class AppConfig(confuse.Configuration):
    def __init__(self, name: str):
        self._config_name = name
        self._config = CONFIG_DIR / (filename := f"{name}.yml")
        self._default = DEFAULT_DIR / filename
        super().__init__(name)
        self._add_env_source()

    def config_dir(self) -> str:
        return str(CONFIG_DIR)

    def user_config_path(self) -> str:
        return str(self._config)

    def _add_env_source(self):
        if dotenv.find_dotenv():
            dotenv.load_dotenv()
        config_name = f"{self._config_name.lower()}_"
        env_configs = {
            k[len(config_name) :].lower(): str(v)
            for k, v in os.environ.items()
            if k.lower().startswith(config_name)
        }
        # Convert `AAA_BBB_CCC=DDD` to `{'aaa':{'bbb':{'ccc':'ddd'}}}`
        source_tree: dict[str, Any] = {}
        for key, value in env_configs.items():
            _tmp = source_tree
            *nodes, name = key.split("_")
            for node in nodes:
                _tmp = _tmp.setdefault(node, {})
            if value == "":
                continue
            try:
                _tmp[name] = json.loads(value)
            except json.JSONDecodeError:
                _tmp[name] = value

        self.sources.insert(0, confuse.ConfigSource.of(source_tree))

    def _add_default_source(self):
        self.add(confuse.YamlSource(self._default, default=True))

    def _add_user_source(self):
        self.add(confuse.YamlSource(self._config, optional=True))

    def __getitem__(self, key: str) -> ConfigSubView:
        return ConfigSubView(self, key)


class GeneralConfig(AppConfig):
    def __init__(self, name: str):
        super().__init__(name)


class APIConfig(GeneralConfig):
    pass


Config = GeneralConfig("general")


================================================
FILE: hibiapi/utils/decorators/__init__.py
================================================
from __future__ import annotations

import asyncio
from asyncio import sleep as async_sleep
from collections.abc import Awaitable, Iterable
from functools import partial, wraps
from inspect import iscoroutinefunction
from time import sleep as sync_sleep
from typing import Callable, Protocol, TypeVar, overload

from typing_extensions import ParamSpec

from hibiapi.utils.decorators.enum import enum_auto_doc as enum_auto_doc
from hibiapi.utils.decorators.timer import Callable_T, TimeIt
from hibiapi.utils.log import logger

Argument_T = ParamSpec("Argument_T")
Return_T = TypeVar("Return_T")


class RetryT(Protocol):
    @overload
    def __call__(self, function: Callable_T) -> Callable_T: ...

    @overload
    def __call__(
        self,
        *,
        retries: int = ...,
        delay: float = ...,
        exceptions: Iterable[type[Exception]] | None = ...,
    ) -> RetryT: ...

    def __call__(
        self,
        function: Callable | None = ...,
        *,
        retries: int = ...,
        delay: float = ...,
        exceptions: Iterable[type[Exception]] | None = ...,
    ) -> Callable | RetryT: ...


@overload
def Retry(function: Callable_T) -> Callable_T: ...


@overload
def Retry(
    *,
    retries: int = ...,
    delay: float = ...,
    exceptions: Iterable[type[Exception]] | None = ...,
) -> RetryT: ...


def Retry(
    function: Callable | None = None,
    *,
    retries: int = 3,
    delay: float = 0.1,
    exceptions: Iterable[type[Exception]] | None = None,
) -> Callable | RetryT:
    if function is None:
        return partial(
            Retry,
            retries=retries,
            delay=delay,
            exceptions=exceptions,
        )

    timed_func = TimeIt(function)
    allowed_exceptions: tuple[type[Exception], ...] = tuple(exceptions or [Exception])
    assert (retries >= 1) and (delay >= 0)

    @wraps(timed_func)
    def sync_wrapper(*args, **kwargs):
        error: Exception | None = None
        for retried in range(retries):
            try:
                return timed_func(*args, **kwargs)
            except Exception as exception:
                error = exception
                if not isinstance(exception, allowed_exceptions):
                    raise
                logger.opt().debug(
                    f"Retry of {timed_func=} trigged "
                    f"due to {exception=} raised ({retried=}/{retries=})"
                )
                sync_sleep(delay)
        assert isinstance(error, Exception)
        raise error

    @wraps(timed_func)
    async def async_wrapper(*args, **kwargs):
        error: Exception | None = None
        for retried in range(retries):
            try:
                return await timed_func(*args, **kwargs)
            except Exception as exception:
                error = exception
                if not isinstance(exception, allowed_exceptions):
                    raise
                logger.opt().debug(
                    f"Retry of {timed_func=} trigged "
                    f"due to {exception=} raised ({retried=}/{retries})"
                )
                await async_sleep(delay)
        assert isinstance(error, Exception)
        raise error

    return async_wrapper if iscoroutinefunction(function) else sync_wrapper


def ToAsync(
    function: Callable[Argument_T, Return_T],
) -> Callable[Argument_T, Awaitable[Return_T]]:
    @TimeIt
    @wraps(function)
    async def wrapper(*args: Argument_T.args, **kwargs: Argument_T.kwargs) -> Return_T:
        return await asyncio.get_running_loop().run_in_executor(
            None, lambda: function(*args, **kwargs)
        )

    return wrapper


================================================
FILE: hibiapi/utils/decorators/enum.py
================================================
import ast
import inspect
from enum import Enum
from typing import TypeVar

_ET = TypeVar("_ET", bound=type[Enum])


def enum_auto_doc(enum: _ET) -> _ET:
    enum_class_ast, *_ = ast.parse(inspect.getsource(enum)).body
    assert isinstance(enum_class_ast, ast.ClassDef)

    enum_value_comments: dict[str, str] = {}
    for index, body in enumerate(body_list := enum_class_ast.body):
        if (
            isinstance(body, ast.Assign)
            and (next_index := index + 1) < len(body_list)
            and isinstance(next_body := body_list[next_index], ast.Expr)
        ):
            target, *_ = body.targets
            assert isinstance(target, ast.Name)
            assert isinstance(next_body.value, ast.Constant)
            assert isinstance(member_doc := next_body.value.value, str)
            enum[target.id].__doc__ = member_doc
            enum_value_comments[target.id] = inspect.cleandoc(member_doc)

    if not enum_value_comments and all(member.name == member.value for member in enum):
        return enum

    members_doc = ""
    for member in enum:
        value_document = "-"
        if member.name != member.value:
            value_document += f" `{member.name}` ="
        value_document += f" *`{member.value}`*"
        if doc := enum_value_comments.get(member.name):
            value_document += f" : {doc}"
        members_doc += value_document + "\n"

    enum.__doc__ = f"{enum.__doc__}\n{members_doc}"
    return enum


================================================
FILE: hibiapi/utils/decorators/timer.py
================================================
from __future__ import annotations

import time
from dataclasses import dataclass, field
from functools import wraps
from inspect import iscoroutinefunction
from typing import Any, Callable, ClassVar, TypeVar

from hibiapi.utils.log import logger

Callable_T = TypeVar("Callable_T", bound=Callable)


class TimerError(Exception):
    """A custom exception used to report errors in use of Timer class"""


@dataclass
class Timer:
    """Time your code using a class, context manager, or decorator"""

    timers: ClassVar[dict[str, float]] = dict()
    name: str | None = None
    text: str = "Elapsed time: {:0.3f} seconds"
    logger_func: Callable[[str], None] | None = print
    _start_time: float | None = field(default=None, init=False, repr=False)

    def __post_init__(self) -> None:
        """Initialization: add timer to dict of timers"""
        if self.name:
            self.timers.setdefault(self.name, 0)

    def start(self) -> None:
        """Start a new timer"""
        if self._start_time is not None:
            raise TimerError("Timer is running. Use .stop() to stop it")

        self._start_time = time.perf_counter()

    def stop(self) -> float:
        """Stop the timer, and report the elapsed time"""
        if self._start_time is None:
            raise TimerError("Timer is not running. Use .start() to start it")

        # Calculate elapsed time
        elapsed_time = time.perf_counter() - self._start_time
        self._start_time = None

        # Report elapsed time
        if self.logger_func:
            self.logger_func(self.text.format(elapsed_time * 1000))
        if self.name:
            self.timers[self.name] += elapsed_time

        return elapsed_time

    def __enter__(self) -> Timer:
        """Start a new timer as a context manager"""
        self.start()
        return self

    def __exit__(self, *exc_info: Any) -> None:
        """Stop the context manager timer"""
        self.stop()

    def _recreate_cm(self) -> Timer:
        return self.__class__(self.name, self.text, self.logger_func)

    def __call__(self, function: Callable_T) -> Callable_T:
        @wraps(function)
        async def async_wrapper(*args: Any, **kwargs: Any):
            self.text = (
                f"<g>Async</g> function <y>{function.__qualname__}</y> "
                "cost <e>{:.3f}ms</e>"
            )

            with self._recreate_cm():
                return await function(*args, **kwargs)

        @wraps(function)
        def sync_wrapper(*args: Any, **kwargs: Any):
            self.text = (
                f"<g>sync</g> function <y>{function.__qualname__}</y> "
                "cost <e>{:.3f}ms</e>"
            )

            with self._recreate_cm():
                return function(*args, **kwargs)

        return (
            async_wrapper if iscoroutinefunction(function) else sync_wrapper
        )  # type:ignore


TimeIt = Timer(logger_func=logger.trace)


================================================
FILE: hibiapi/utils/exceptions.py
================================================
from datetime import datetime
from typing import Any, Optional

from pydantic import AnyHttpUrl, BaseModel, Extra, Field


class ExceptionReturn(BaseModel):
    url: Optional[AnyHttpUrl] = None
    time: datetime = Field(default_factory=datetime.now)
    code: int = Field(ge=400, le=599)
    detail: str
    headers: dict[str, str] = {}

    class Config:
        extra = Extra.allow


class BaseServerException(Exception):
    code: int = 500
    detail: str = "Server Fault"
    headers: dict[str, Any] = {}

    def __init__(
        self,
        detail: Optional[str] = None,
        *,
        code: Optional[int] = None,
        headers: Optional[dict[str, Any]] = None,
        **params
    ) -> None:
        self.data = ExceptionReturn(
            detail=detail or self.__class__.detail,
            code=code or self.__class__.code,
            headers=headers or self.__class__.headers,
            **params
        )
        super().__init__(detail)


class BaseHTTPException(BaseServerException):
    pass


class ServerSideException(BaseServerException):
    code = 500
    detail = "Internal Server Error"


class UpstreamAPIException(ServerSideException):
    code = 502
    detail = "Upstram API request failed"


class UncaughtException(ServerSideException):
    code = 500
    detail = "Uncaught exception raised during processing"
    exc: Exception

    @classmethod
    def with_exception(cls, e: Exception):
        c = cls(e.__class__.__qualname__)
        c.exc = e
        return c


class ClientSideException(BaseServerException):
    code = 400
    detail = "Bad Request"


class ValidationException(ClientSideException):
    code = 422


class RateLimitReachedException(ClientSideException):
    code = 429
    detail = "Rate limit reached"


================================================
FILE: hibiapi/utils/log.py
================================================
import logging
import re
import sys
from datetime import timedelta
from pathlib import Path

import sentry_sdk.integrations.logging as sentry
from loguru import logger as _logger

from hibiapi.utils.config import Config

LOG_FILE = Config["log"]["file"].get_optional(Path)
LOG_LEVEL = Config["log"]["level"].as_str().strip().upper()
LOG_FORMAT = Config["log"]["format"].as_str().strip()


class LoguruHandler(logging.Handler):
    _tag_escape_re = re.compile(r"</?((?:[fb]g\s)?[^<>\s]*)>")

    @classmethod
    def escape_tag(cls, string: str) -> str:
        return cls._tag_escape_re.sub(r"\\\g<0>", string)

    def emit(self, record: logging.LogRecord):
        try:
            level = logger.level(record.levelname).name
        except ValueError:
            level = record.levelno

        frame, depth, message = logging.currentframe(), 2, record.getMessage()
        while frame.f_code.co_filename == logging.__file__:  # type: ignore
            frame = frame.f_back  # type: ignore
            depth += 1

        logger.opt(depth=depth, exception=record.exc_info, colors=True).log(
            level, f"<e>{self.escape_tag(message)}</e>"
        )


logger = _logger.opt(colors=True)
logger.remove()
logger.add(
    sys.stdout,
    level=LOG_LEVEL,
    format=LOG_FORMAT,
    filter=lambda record: record["level"].no < logging.WARNING,
)
logger.add(
    sys.stderr,
    level=LOG_LEVEL,
    filter=lambda record: record["level"].no >= logging.WARNING,
    format=LOG_FORMAT,
)
logger.add(sentry.BreadcrumbHandler(), level=LOG_LEVEL)
logger.add(sentry.EventHandler(), level="ERROR")

if LOG_FILE is not None:
    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)

    logger.add(
        str(LOG_FILE),
        level=LOG_LEVEL,
        encoding="utf-8",
        rotation=timedelta(days=1),
    )

logger.level(LOG_LEVEL)


================================================
FILE: hibiapi/utils/net.py
================================================
import functools
from collections.abc import Coroutine
from types import TracebackType
from typing import (
    Any,
    Callable,
    ClassVar,
    Optional,
    TypeVar,
    Union,
)

from httpx import (
    URL,
    AsyncClient,
    Cookies,
    HTTPError,
    HTTPStatusError,
    Request,
    Response,
    ResponseNotRead,
    TransportError,
)

from .decorators import Retry, TimeIt
from .exceptions import UpstreamAPIException
from .log import logger

AsyncCallable_T = TypeVar("AsyncCallable_T", bound=Callable[..., Coroutine])


class AsyncHTTPClient(AsyncClient):
    net_client: "BaseNetClient"

    @staticmethod
    async def _log_request(request: Request):
        method, url = request.method, request.url
        logger.debug(
            f"Network request <y>sent</y>: <b><e>{method}</e> <u>{url}</u></b>"
        )

    @staticmethod
    async def _log_response(response: Response):
        method, url = response.request.method, response.url
        try:
            length, code = len(response.content), response.status_code
        except ResponseNotRead:
            length, code = -1, response.status_code
        logger.debug(
            f"Network request <g>finished</g>: <b><e>{method}</e> "
            f"<u>{url}</u> <m>{code}</m></b> <m>{length}</m>"
        )

    @Retry(exceptions=[TransportError])
    async def request(self, method: str, url: Union[URL, str], **kwargs):
        self.event_hooks = {
            "request": [self._log_request],
            "response": [self._log_response],
        }
        return await super().request(method, url, **kwargs)


class BaseNetClient:
    connections: ClassVar[int] = 0
    clients: ClassVar[list[AsyncHTTPClient]] = []

    client: Optional[AsyncHTTPClient] = None

    def __init__(
        self,
        headers: Optional[dict[str, Any]] = None,
        cookies: Optional[Cookies] = None,
        proxies: Optional[dict[str, str]] = None,
        client_class: type[AsyncHTTPClient] = AsyncHTTPClient,
    ):
        self.cookies, self.client_class = cookies or Cookies(), client_class
        self.headers: dict[str, Any] = headers or {}
        self.proxies: Any = proxies or {}  # Bypass type checker

        self.create_client()

    def create_client(self):
        self.client = self.client_class(
            headers=self.headers,
            proxies=self.proxies,
            cookies=self.cookies,
            http2=True,
            follow_redirects=True,
        )
        self.client.net_client = self
        BaseNetClient.clients.append(self.client)
        return self.client

    async def __aenter__(self):
        if not self.client or self.client.is_closed:
            self.client = await self.create_client().__aenter__()

        self.__class__.connections += 1
        return self.client

    async def __aexit__(
        self,
        exc_type: Optional[type[BaseException]] = None,
        exc_value: Optional[BaseException] = None,
        traceback: Optional[TracebackType] = None,
    ):
        self.__class__.connections -= 1

        if not (exc_type and exc_value and traceback):
            return
        if self.client and not self.client.is_closed:
            client = self.client
            self.client = None
            await client.__aexit__(exc_type, exc_value, traceback)
        return


def catch_network_error(function: AsyncCallable_T) -> AsyncCallable_T:
    timed_func = TimeIt(function)

    @functools.wraps(timed_func)
    async def wrapper(*args, **kwargs):
        try:
            return await timed_func(*args, **kwargs)
        except HTTPStatusError as e:
            raise UpstreamAPIException(detail=e.response.text) from e
        except HTTPError as e:
            raise UpstreamAPIException from e

    return wrapper  # type:ignore


================================================
FILE: hibiapi/utils/routing.py
================================================
import inspect
from collections.abc import Mapping
from contextvars import ContextVar
from enum import Enum
from fnmatch import fnmatch
from functools import wraps
from typing import Annotated, Any, Callable, Literal, Optional
from urllib.parse import ParseResult, urlparse

from fastapi import Depends, Request
from fastapi.routing import APIRouter
from httpx import URL
from pydantic import AnyHttpUrl
from pydantic.errors import UrlHostError
from starlette.datastructures import Headers, MutableHeaders

from hibiapi.utils.cache import endpoint_cache
from hibiapi.utils.net import AsyncCallable_T, AsyncHTTPClient, BaseNetClient

DONT_ROUTE_KEY = "_dont_route"


def dont_route(func: AsyncCallable_T) -> AsyncCallable_T:
    setattr(func, DONT_ROUTE_KEY, True)
    return func


class EndpointMeta(type):
    @staticmethod
    def _list_router_function(members: dict[str, Any]):
        return {
            name: object
            for name, object in members.items()
            if (
                inspect.iscoroutinefunction(object)
                and not name.startswith("_")
                and not getattr(object, DONT_ROUTE_KEY, False)
            )
        }

    def __new__(
        cls,
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, Any],
        *,
        cache_endpoints: bool = True,
        **kwargs,
    ):
        for object_name, object in cls._list_router_function(namespace).items():
            namespace[object_name] = (
                endpoint_cache(object) if cache_endpoints else object
            )
        return super().__new__(cls, name, bases, namespace, **kwargs)

    @property
    def router_functions(self):
        return self._list_router_function(dict(inspect.getmembers(self)))


class BaseEndpoint(metaclass=EndpointMeta, cache_endpoints=False):
    def __init__(self, client: AsyncHTTPClient):
        self.client = client

    @staticmethod
    def _join(base: str, endpoint: str, params: dict[str, Any]) -> URL:
        host: ParseResult = urlparse(base)
        params = {
            k: (v.value if isinstance(v, Enum) else v)
            for k, v in params.items()
            if v is not None
        }
        return URL(
            url=ParseResult(
                scheme=host.scheme,
                netloc=host.netloc,
                path=endpoint.format(**params),
                params="",
                query="",
                fragment="",
            ).geturl(),
            params=params,
        )


class SlashRouter(APIRouter):
    def api_route(self, path: str, **kwargs):
        path = path if path.startswith("/") else f"/{path}"
        return super().api_route(path, **kwargs)


class EndpointRouter(SlashRouter):
    @staticmethod
    def _exclude_params(func: Callable, params: Mapping[str, Any]) -> dict[str, Any]:
        func_params = inspect.signature(func).parameters
        return {k: v for k, v in params.items() if k in func_params}

    @staticmethod
    def _router_signature_convert(
        func,
        endpoint_class: type["BaseEndpoint"],
        request_client: Callable,
        method_name: Optional[str] = None,
    ):
        @wraps(func)
        async def route_func(endpoint: endpoint_class, **kwargs):
            endpoint_method = getattr(endpoint, method_name or func.__name__)
            return await endpoint_method(**kwargs)

        route_func.__signature__ = inspect.signature(route_func).replace(  # type:ignore
            parameters=[
                inspect.Parameter(
                    name="endpoint",
                    kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
                    annotation=endpoint_class,
                    default=Depends(request_client),
                ),
                *(
                    param
                    for param in inspect.signature(func).parameters.values()
                    if param.kind == inspect.Parameter.KEYWORD_ONLY
                ),
            ]
        )
        return route_func

    def include_endpoint(
        self,
        endpoint_class: type[BaseEndpoint],
        net_client: BaseNetClient,
        add_match_all: bool = True,
    ):
        router_functions = endpoint_class.router_functions

        async def request_client():
            async with net_client as client:
                yield endpoint_class(client)

        for func_name, func in router_functions.items():
            self.add_api_route(
                path=f"/{func_name}",
                endpoint=self._router_signature_convert(
                    func,
                    endpoint_class=endpoint_class,
                    request_client=request_client,
                    method_name=func_name,
                ),
                methods=["GET"],
            )

        if not add_match_all:
            return

        @self.get("/", description="JournalAD style API routing", deprecated=True)
        async def match_all(
            endpoint: Annotated[endpoint_class, Depends(request_client)],
            request: Request,
            type: Literal[tuple(router_functions.keys())],  # type: ignore
        ):
            func = router_functions[type]
            return await func(
                endpoint, **self._exclude_params(func, request.query_params)
            )


class BaseHostUrl(AnyHttpUrl):
    allowed_hosts: list[str] = []

    @classmethod
    def validate_host(cls, parts) -> tuple[str, Optional[str], str, bool]:
        host, tld, host_type, rebuild = super().validate_host(parts)
        if not cls._check_domain(host):
            raise UrlHostError(allowed=cls.allowed_hosts)
        return host, tld, host_type, rebuild

    @classmethod
    def _check_domain(cls, host: str) -> bool:
        return any(
            filter(
                lambda x: fnmatch(host, x),  # type:ignore
                cls.allowed_hosts,
            )
        )


request_headers = ContextVar[Headers]("request_headers")
response_headers = ContextVar[MutableHeaders]("response_headers")


================================================
FILE: hibiapi/utils/temp.py
================================================
from pathlib import Path
from tempfile import mkdtemp, mkstemp
from threading import Lock
from urllib.parse import ParseResult

from fastapi import Request


class TempFile:
    path = Path(mkdtemp())
    path_depth = 3
    name_length = 16

    _lock = Lock()

    @classmethod
    def create(cls, ext: str = ".tmp"):
        descriptor, str_path = mkstemp(suffix=ext, dir=str(cls.path))
        return descriptor, Path(str_path)

    @classmethod
    def to_url(cls, request: Request, path: Path) -> str:
        assert cls.path
        return ParseResult(
            scheme=request.url.scheme,
            netloc=request.url.netloc,
            path=f"/temp/{path.relative_to(cls.path)}",
            params="",
            query="",
            fragment="",
        ).geturl()


================================================
FILE: pyproject.toml
================================================
[project]
name = "HibiAPI"
version = "0.8.0"
description = "A program that implements easy-to-use APIs for a variety of commonly used sites"
readme = "README.md"
license = { text = "Apache-2.0" }
authors = [{ name = "mixmoe", email = "admin@obfs.dev" }]
requires-python = ">=3.9,<4.0"
dependencies = [
    "fastapi>=0.110.2",
    "httpx[http2]>=0.27.0",
    "uvicorn[standard]>=0.29.0",
    "confuse>=2.0.1",
    "loguru>=0.7.2",
    "python-dotenv>=1.0.1",
    "qrcode[pil]>=7.4.2",
    "pycryptodomex>=3.20.0",
    "sentry-sdk>=1.45.0",
    "pydantic<2.0.0,>=1.9.0",
    "python-multipart>=0.0.9",
    "cashews[diskcache,redis]>=7.0.2",
    "typing-extensions>=4.11.0",
    "typer[all]>=0.12.3",
]

[project.urls]
homepage = "https://api.obfs.dev"
repository = "https://github.com/mixmoe/HibiAPI"
documentation = "https://github.com/mixmoe/HibiAPI/wiki"

[project.optional-dependencies]
scripts = ["pyqt6>=6.6.1", "pyqt6-webengine>=6.6.0", "requests>=2.31.0"]

[project.scripts]
hibiapi = "hibiapi.__main__:cli"

[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"

[tool.pdm.dev-dependencies]
dev = [
    "pytest>=8.1.1",
    "pytest-httpserver>=1.0.10",
    "pytest-cov>=5.0.0",
    "pytest-benchmark>=4.0.0",
    "pytest-pretty>=1.2.0",
    "ruff>=0.4.1",
]

[tool.pdm.build]
includes = []

[tool.pdm.scripts]
test = """pytest \
    --cov ./hibiapi/ \
    --cov-report xml \
    --cov-report term-missing \
    ./test"""
start = "hibiapi run"
lint = "ruff check"

[tool.pyright]
typeCheckingMode = "standard"

[tool.ruff]
lint.select = [
    # pycodestyle
    "E",
    # Pyflakes
    "F",
    # pyupgrade
    "UP",
    # flake8-bugbear
    "B",
    # flake8-simplify
    "SIM",
    # isort
    "I",
]
target-version = "py39"


================================================
FILE: scripts/pixiv_login.py
================================================
import hashlib
import sys
from base64 import urlsafe_b64encode
from secrets import token_urlsafe
from typing import Any, Callable, Optional, TypeVar
from urllib.parse import parse_qs, urlencode

import requests
from loguru import logger as _logger
from PyQt6.QtCore import QUrl
from PyQt6.QtNetwork import QNetworkCookie
from PyQt6.QtWebEngineCore import (
    QWebEngineUrlRequestInfo,
    QWebEngineUrlRequestInterceptor,
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QMainWindow,
    QPlainTextEdit,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

USER_AGENT = "PixivAndroidApp/5.0.234 (Android 11; Pixel 5)"
REDIRECT_URI = "https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback"
LOGIN_URL = "https://app-api.pixiv.net/web/v1/login"
AUTH_TOKEN_URL = "https://oauth.secure.pixiv.net/auth/token"
CLIENT_ID = "MOBrBDS8blbauoSck0ZfDbtuzpyT"
CLIENT_SECRET = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj"


app = QApplication(sys.argv)
logger = _logger.opt(colors=True)


class RequestInterceptor(QWebEngineUrlRequestInterceptor):
    code_listener: Optional[Callable[[str], None]] = None

    def __init__(self):
        super().__init__()

    def interceptRequest(self, info: QWebEngineUrlRequestInfo) -> None:
        method = info.requestMethod().data().decode()
        url = info.requestUrl().url()

        if (
            self.code_listener
            and "app-api.pixiv.net" in info.requestUrl().host()
            and info.requestUrl().path().endswith("callback")
        ):
            query = parse_qs(info.requestUrl().query())
            code, *_ = query["code"]
            self.code_listener(code)

        logger.debug(f"<y>{method}</y> <u>{url}</u>")


class WebView(QWebEngineView):
    def __init__(self):
        super().__init__()

        self.cookies: dict[str, str] = {}

        page = self.page()
        assert page is not None
        profile = page.profile()
        assert profile is not None
        profile.setHttpUserAgent(USER_AGENT)
        page.contentsSize().setHeight(768)
        page.contentsSize().setWidth(432)

        self.interceptor = RequestInterceptor()
        profile.setUrlRequestInterceptor(self.interceptor)
        cookie_store = profile.cookieStore()
        assert cookie_store is not None
        cookie_store.cookieAdded.connect(self._on_cookie_added)

        self.setFixedHeight(896)
        self.setFixedWidth(414)

        self.start("about:blank")

    def start(self, goto: str):
        self.page().profile().cookieStore().deleteAllCookies()  # type: ignore
        self.cookies.clear()
        self.load(QUrl(goto))

    def _on_cookie_added(self, cookie: QNetworkCookie):
        domain = cookie.domain()
        name = cookie.name().data().decode()
        value = cookie.value().data().decode()
        self.cookies[name] = value
        logger.debug(f"<m>Set-Cookie</m> <r>{domain}</r> <g>{name}</g> -> {value!r}")


class ResponseDataWidget(QWidget):
    def __init__(self, webview: WebView):
        super().__init__()
        self.webview = webview

        layout = QVBoxLayout()

        self.cookie_paste = QPlainTextEdit()
        self.cookie_paste.setDisabled(True)
        self.cookie_paste.setPlaceholderText("得到的登录数据将会展示在这里")

        layout.addWidget(self.cookie_paste)

        copy_button = QPushButton()
        copy_button.clicked.connect(self._on_clipboard_copy)
        copy_button.setText("复制上述登录数据到剪贴板")

        layout.addWidget(copy_button)

        self.setLayout(layout)

    def _on_clipboard_copy(self, checked: bool):
        if paste_string := self.cookie_paste.toPlainText().strip():
            app.clipboard().setText(paste_string)  # type: ignore


_T = TypeVar("_T", bound="LoginPhrase")


class LoginPhrase:
    @staticmethod
    def s256(data: bytes):
        return urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=").decode()

    @classmethod
    def oauth_pkce(cls) -> tuple[str, str]:
        code_verifier = token_urlsafe(32)
        code_challenge = cls.s256(code_verifier.encode())
        return code_verifier, code_challenge

    def __init__(self: _T, url_open_callback: Callable[[str, _T], None]):
        self.code_verifier, self.code_challenge = self.oauth_pkce()

        login_params = {
            "code_challenge": self.code_challenge,
            "code_challenge_method": "S256",
            "client": "pixiv-android",
        }
        login_url = f"{LOGIN_URL}?{urlencode(login_params)}"
        url_open_callback(login_url, self)

    def code_received(self, code: str):
        response = requests.post(
            AUTH_TOKEN_URL,
            data={
                "client_id": CLIENT_ID,
                "client_secret": CLIENT_SECRET,
                "code": code,
                "code_verifier": self.code_verifier,
                "grant_type": "authorization_code",
                "include_policy": "true",
                "redirect_uri": REDIRECT_URI,
            },
            headers={"User-Agent": USER_AGENT},
        )
        response.raise_for_status()
        data: dict[str, Any] = response.json()

        access_token = data["access_token"]
        refresh_token = data["refresh_token"]
        expires_in = data.get("expires_in", 0)

        return_text = ""
        return_text += f"access_token: {access_token}\n"
        return_text += f"refresh_token: {refresh_token}\n"
        return_text += f"expires_in: {expires_in}\n"

        return return_text


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Pixiv login helper")

        layout = QHBoxLayout()

        self.webview = WebView()
        layout.addWidget(self.webview)

        self.form = ResponseDataWidget(self.webview)
        layout.addWidget(self.form)

        widget = QWidget()
        widget.setLayout(layout)

        self.setCentralWidget(widget)


if __name__ == "__main__":
    window = MainWindow()
    window.show()

    def url_open_callback(url: str, login_phrase: LoginPhrase):
        def code_listener(code: str):
            response = login_phrase.code_received(code)
            window.form.cookie_paste.setPlainText(response)

        window.webview.interceptor.code_listener = code_listener
        window.webview.start(url)

    LoginPhrase(url_open_callback)

    exit(app.exec())


================================================
FILE: test/__init__.py
================================================



================================================
FILE: test/test_base.py
================================================
from typing import Annotated, Any

import pytest
from fastapi import Depends
from fastapi.testclient import TestClient
from pytest_benchmark.fixture import BenchmarkFixture


@pytest.fixture(scope="package")
def client():
    from hibiapi.app import app

    with TestClient(app, base_url="http://testserver/") as client:
        yield client


def test_openapi(client: TestClient, in_stress: bool = False):
    response = client.get("/openapi.json")
    assert response.status_code == 200
    assert response.json()

    if in_stress:
        return True


def test_doc_page(client: TestClient, in_stress: bool = False):
    response = client.get("/docs")
    assert response.status_code == 200
    assert response.text

    response = client.get("/docs/test")
    assert response.status_code == 200
    assert response.text

    if in_stress:
        return True


def test_openapi_stress(client: TestClient, benchmark: BenchmarkFixture):
    assert benchmark.pedantic(
        test_openapi,
        args=(client, True),
        rounds=200,
        warmup_rounds=10,
        iterations=3,
    )


def test_doc_page_stress(client: TestClient, benchmark: BenchmarkFixture):
    assert benchmark.pedantic(
        test_doc_page, args=(client, True), rounds=200, iterations=3
    )


def test_notfound(client: TestClient):
    from hibiapi.utils.exceptions import ExceptionReturn

    response = client.get("/notexistpath")
    assert response.status_code == 404
    assert ExceptionReturn.parse_obj(response.json())


@pytest.mark.xfail(reason="not implemented yet")
def test_net_request():
    from hibiapi.utils.net import BaseNetClient
    from hibiapi.utils.routing import BaseEndpoint, SlashRouter

    test_headers = {"x-test-header": "random-string"}
    test_data = {"test": "test"}

    class TestEndpoint(BaseEndpoint):
        base = "https://httpbin.org"

        async def request(self, path: str, params: dict[str, Any]):
            url = self._join(self.base, path, params)
            response = await self.client.post(url, data=params)
            response.raise_for_status()
            return response.json()

        async def form(self, *, data: dict[str, Any]):
            return await self.request("/post", data)

        async def teapot(self):
            return await self.request("/status/{codes}", {"codes": 418})

    class TestNetClient(BaseNetClient):
        pass

    async def net_client():
        async with TestNetClient(headers=test_headers) as client:
            yield TestEndpoint(client)

    router = SlashRouter()

    @router.post("form")
    async def form(
        *,
        endpoint: Annotated[TestEndpoint, Depends(net_client)],
        data: dict[str, Any],
    ):
        return await endpoint.form(data=data)

    @router.post("teapot")
    async def teapot(endpoint: Annotated[TestEndpoint, Depends(net_client)]):
        return await endpoint.teapot()

    from hibiapi.app.routes import router as api_router

    api_router.include_router(router, prefix="/test")

    from hibiapi.app import app
    from hibiapi.utils.exceptions import ExceptionReturn

    with TestClient(app, base_url="http://testserver/api/test/") as client:
        response = client.post("form", json=test_data)
        assert response.status_code == 200
        response_data = response.json()
        assert response_data["form"] == test_data
        request_headers = {k.lower(): v for k, v in response_data["headers"].items()}
        assert test_headers.items() <= request_headers.items()

        response = client.post("teapot", json=test_data)
        exception_return = ExceptionReturn.parse_obj(response.json())
        assert exception_return.code == response.status_code


================================================
FILE: test/test_bika.py
================================================
from math import inf

import pytest
from fastapi.testclient import TestClient


@pytest.fixture(scope="package")
def client():
    from hibiapi.app import app, application

    application.RATE_LIMIT_MAX = inf

    with TestClient(app, base_url="http://testserver/api/bika/") as client:
        client.headers["Cache-Control"] = "no-cache"
        yield client


def test_collections(client: TestClient):
    response = client.get("collections")
    assert response.status_code == 200
    assert response.json()["code"] == 200


def test_categories(client: TestClient):
    response = client.get("categories")
    assert response.status_code == 200
    assert response.json()["code"] == 200


def test_keywords(client: TestClient):
    response = client.get("keywords")
    assert response.status_code == 200
    assert response.json()["code"] == 200


def test_advanced_search(client: TestClient):
    response = client.get(
        "advanced_search", params={"keyword": "blend", "page": 1, "sort": "vd"}
    )
    assert response.status_code == 200
    assert response.json()["code"] == 200 and response.json()["data"]


def test_category_list(client: TestClient):
    response = client.get(
        "category_list", params={"category": "全彩", "page": 1, "sort": "vd"}
    )
    assert response.status_code == 200
    assert response.json()["code"] == 200 and response.json()["data"]


def test_author_list(client: TestClient):
    response = client.get(
        "author_list", params={"author": "ゆうき", "page": 1, "sort": "vd"}
    )
    assert response.status_code == 200
    assert response.json()["code"] == 200 and response.json()["data"]


def test_comic_detail(client: TestClient):
    response = client.get("comic_detail", params={"id": "5873aa128fe1fa02b156863a"})
    assert response.status_code == 200
    assert response.json()["code"] == 200 and response.json()["data"]


def test_comic_recommendation(client: TestClient):
    response = client.get(
        "comic_recommendation", params={"id": "5873aa128fe1fa02b156863a"}
    )
    assert response.status_code == 200
    assert response.json()["code"] == 200 and response.json()["data"]


def test_comic_episodes(client: TestClient):
    response = client.get("comic_episodes", params={"id": "5873aa128fe1fa02b156863a"})
    assert response.status_code == 200
    assert response.json()["code"] == 200 and response.json()["data"]


def test_comic_page(client: TestClient):
    response = client.get("comic_page", params={"id": "5873aa128fe1fa02b156863a"})
    assert response.status_code == 200
    assert response.json()["code"] == 200 and response.json()["data"]


def test_comic_comments(client: TestClient):
    response = client.get("comic_comments", params={"id": "5873aa128fe1fa02b156863a"})
    assert response.status_code == 200
    assert response.json()["code"] == 200 and response.json()["data"]


def test_games(client: TestClient):
    response = client.get("games")
    assert response.status_code == 200
    assert response.json()["code"] == 200 and response.json()["data"]["games"]


def test_game_detail(client: TestClient):
    response = client.get("game_detail", params={"id": "6298dc83fee4a055417cdd98"})
    assert response.status_code == 200
    assert response.json()["code"] == 200 and response.json()["data"]


================================================
FILE: test/test_bilibili_v2.py
================================================
from math import inf

import pytest
from fastapi.testclient import TestClient


@pytest.fixture(scope="package")
def client():
    from hibiapi.app import app, application

    application.RATE_LIMIT_MAX = inf

    with TestClient(app, base_url="http://testserver/api/bilibili/v2/") as client:
        yield client


def test_playurl(client: TestClient):
    response = client.get("playurl", params={"aid": 2})
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_paged_playurl(client: TestClient):
    response = client.get("playurl", params={"aid": 2, "page": 1})
    assert response.status_code == 200

    if response.json()["code"] != 0:
        pytest.xfail(reason=response.text)


def test_seasoninfo(client: TestClient):
    response = client.get("seasoninfo", params={"season_id": 425})
    assert response.status_code == 200
    assert response.json()["code"] in (0, -404)


def test_seasonrecommend(client: TestClient):
    response = client.get("seasonrecommend", params={"season_id": 425})
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_search(client: TestClient):
    response = client.get("search", params={"keyword": "railgun"})
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_search_suggest(client: TestClient):
    from hibiapi.api.bilibili import SearchType

    response = client.get(
        "search", params={"keyword": "paperclip", "type": SearchType.suggest.value}
    )
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_search_hot(client: TestClient):
    from hibiapi.api.bilibili import SearchType

    response = client.get(
        "search", params={"limit": "10", "type": SearchType.hot.value}
    )
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_timeline(client: TestClient):
    from hibiapi.api.bilibili import TimelineType

    response = client.get("timeline", params={"type": TimelineType.CN.value})
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_space(client: TestClient):
    response = client.get("space", params={"vmid": 2})
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_archive(client: TestClient):
    response = client.get("archive", params={"vmid": 2})
    assert response.status_code == 200
    assert response.json()["code"] == 0


@pytest.mark.skip(reason="not implemented yet")
def test_favlist(client: TestClient):
    # TODO:add test case
    pass


================================================
FILE: test/test_bilibili_v3.py
================================================
from math import inf

import pytest
from fastapi.testclient import TestClient


@pytest.fixture(scope="package")
def client():
    from hibiapi.app import app, application

    application.RATE_LIMIT_MAX = inf

    with TestClient(app, base_url="http://testserver/api/bilibili/v3/") as client:
        yield client


def test_video_info(client: TestClient):
    response = client.get("video_info", params={"aid": 2})
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_video_address(client: TestClient):
    response = client.get(
        "video_address",
        params={"aid": 2, "cid": 62131},
    )
    assert response.status_code == 200

    if response.json()["code"] != 0:
        pytest.xfail(reason=response.text)


def test_user_info(client: TestClient):
    response = client.get("user_info", params={"uid": 2})
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_user_uploaded(client: TestClient):
    response = client.get("user_uploaded", params={"uid": 2})
    assert response.status_code == 200
    assert response.json()["code"] == 0


@pytest.mark.skip(reason="not implemented yet")
def test_user_favorite(client: TestClient):
    # TODO:add test case
    pass


def test_season_info(client: TestClient):
    response = client.get("season_info", params={"season_id": 425})
    assert response.status_code == 200
    assert response.json()["code"] in (0, -404)


def test_season_recommend(client: TestClient):
    response = client.get("season_recommend", params={"season_id": 425})
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_season_episode(client: TestClient):
    response = client.get("season_episode", params={"episode_id": 84340})
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_season_timeline(client: TestClient):
    response = client.get("season_timeline")
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_search(client: TestClient):
    response = client.get("search", params={"keyword": "railgun"})
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_search_recommend(client: TestClient):
    response = client.get("search_recommend")
    assert response.status_code == 200
    assert response.json()["code"] == 0


def test_search_suggestion(client: TestClient):
    response = client.get("search_suggestion", params={"keyword": "paperclip"})
    assert response.status_code == 200
    assert response.json()["code"] == 0


================================================
FILE: test/test_netease.py
================================================
from math import inf

import pytest
from fastapi.testclient import TestClient


@pytest.fixture(scope="package")
def client():
    from hibiapi.app import app, application

    application.RATE_LIMIT_MAX = inf

    with TestClient(app, base_url="http://testserver/api/netease/") as client:
        yield client


def test_search(client: TestClient):
    response = client.get("search", params={"s": "test"})
    assert response.status_code == 200

    data = response.json()
    assert data["code"] == 200
    assert data["result"]["songs"]


def test_artist(client: TestClient):
    response = client.get("artist", params={"id": 1024317})
    assert response.status_code == 200
    assert response.json()["code"] == 200


def test_album(client: TestClient):
    response = client.get("album", params={"id": 63263})
    assert response.status_code == 200
    assert response.json()["code"] == 200


def test_detail(client: TestClient):
    response = client.get("detail", params={"id": 657666})
    assert response.status_code == 200
    assert response.json()["code"] == 200


def test_detail_multiple(client: TestClient):
    response = client.get("detail", params={"id": [657666, 657667, 77185]})
    assert response.status_code == 200
    data = response.json()

    assert data["code"] == 200
    assert len(data["songs"]) == 3


def test_song(client: TestClient):
    response = client.get("song", params={"id": 657666})
    assert response.status_code == 200
    assert response.json()["code"] == 200


def test_song_multiple(client: TestClient):
    response = client.get(
        "song", params={"id": (input_ids := [657666, 657667, 77185, 86369])}
    )
    assert response.status_code == 200
    data = response.json()

 
Download .txt
gitextract_8q_i_0ls/

├── .all-contributorsrc
├── .flake8
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── docker.yml
│       ├── lint.yml
│       ├── mirror.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .replit
├── .vscode/
│   ├── docstring.mustache
│   ├── extensions.json
│   ├── launch.json
│   └── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── hibiapi/
│   ├── __init__.py
│   ├── __main__.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── bika/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   ├── constants.py
│   │   │   └── net.py
│   │   ├── bilibili/
│   │   │   ├── __init__.py
│   │   │   ├── api/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── base.py
│   │   │   │   ├── v2.py
│   │   │   │   └── v3.py
│   │   │   ├── constants.py
│   │   │   └── net.py
│   │   ├── netease/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   ├── constants.py
│   │   │   └── net.py
│   │   ├── pixiv/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   ├── constants.py
│   │   │   └── net.py
│   │   ├── qrcode.py
│   │   ├── sauce/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   ├── constants.py
│   │   │   └── net.py
│   │   ├── tieba/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   └── net.py
│   │   └── wallpaper/
│   │       ├── __init__.py
│   │       ├── api.py
│   │       ├── constants.py
│   │       └── net.py
│   ├── app/
│   │   ├── __init__.py
│   │   ├── application.py
│   │   ├── handlers.py
│   │   ├── middlewares.py
│   │   └── routes/
│   │       ├── __init__.py
│   │       ├── bika.py
│   │       ├── bilibili/
│   │       │   ├── __init__.py
│   │       │   ├── v2.py
│   │       │   └── v3.py
│   │       ├── netease.py
│   │       ├── pixiv.py
│   │       ├── qrcode.py
│   │       ├── sauce.py
│   │       ├── tieba.py
│   │       └── wallpaper.py
│   ├── configs/
│   │   ├── bika.yml
│   │   ├── bilibili.yml
│   │   ├── general.yml
│   │   ├── netease.yml
│   │   ├── pixiv.yml
│   │   ├── qrcode.yml
│   │   ├── sauce.yml
│   │   ├── tieba.yml
│   │   └── wallpaper.yml
│   └── utils/
│       ├── __init__.py
│       ├── cache.py
│       ├── config.py
│       ├── decorators/
│       │   ├── __init__.py
│       │   ├── enum.py
│       │   └── timer.py
│       ├── exceptions.py
│       ├── log.py
│       ├── net.py
│       ├── routing.py
│       └── temp.py
├── pyproject.toml
├── scripts/
│   └── pixiv_login.py
└── test/
    ├── __init__.py
    ├── test_base.py
    ├── test_bika.py
    ├── test_bilibili_v2.py
    ├── test_bilibili_v3.py
    ├── test_netease.py
    ├── test_pixiv.py
    ├── test_qrcode.py
    ├── test_sauce.py
    ├── test_tieba.py
    └── test_wallpaper.py
Download .txt
SYMBOL INDEX (477 symbols across 53 files)

FILE: hibiapi/__main__.py
  function run (line 60) | def run(
  function config (line 98) | def config(force: bool = False):

FILE: hibiapi/api/bika/api.py
  class ImageQuality (line 19) | class ImageQuality(str, Enum):
  class ResultSort (line 33) | class ResultSort(str, Enum):
  class BikaEndpoints (line 46) | class BikaEndpoints(BaseEndpoint):
    method _sign (line 48) | def _sign(url: URL, timestamp_bytes: bytes, nonce: bytes, method: bytes):
    method request (line 63) | async def request(
    method collections (line 104) | async def collections(self):
    method categories (line 108) | async def categories(self):
    method keywords (line 112) | async def keywords(self):
    method advanced_search (line 115) | async def advanced_search(
    method category_list (line 134) | async def category_list(
    method author_list (line 150) | async def author_list(
    method comic_detail (line 167) | async def comic_detail(self, *, id: str):
    method comic_recommendation (line 170) | async def comic_recommendation(self, *, id: str):
    method comic_episodes (line 173) | async def comic_episodes(self, *, id: str, page: int = 1):
    method comic_page (line 182) | async def comic_page(self, *, id: str, order: int = 1, page: int = 1):
    method comic_comments (line 192) | async def comic_comments(self, *, id: str, page: int = 1):
    method games (line 201) | async def games(self, *, page: int = 1):
    method game_detail (line 205) | async def game_detail(self, *, id: str):

FILE: hibiapi/api/bika/constants.py
  class BikaConstants (line 4) | class BikaConstants:

FILE: hibiapi/api/bika/net.py
  class BikaLogin (line 16) | class BikaLogin(BaseModel):
  class JWTHeader (line 21) | class JWTHeader(BaseModel):
  class JWTBody (line 26) | class JWTBody(BaseModel):
  function load_jwt (line 33) | def load_jwt(token: str):
  class NetRequest (line 43) | class NetRequest(BaseNetClient):
    method __init__ (line 46) | def __init__(self):
    method token (line 54) | def token(self) -> Optional[str]:
    method login (line 60) | async def login(self, endpoint: "BikaEndpoints"):

FILE: hibiapi/api/bilibili/api/base.py
  class TimelineType (line 16) | class TimelineType(str, Enum):
  class VideoQualityType (line 26) | class VideoQualityType(IntEnum):
  class VideoFormatType (line 41) | class VideoFormatType(IntEnum):
  class BaseBilibiliEndpoint (line 49) | class BaseBilibiliEndpoint(BaseEndpoint):
    method _sign (line 50) | def _sign(self, base: str, endpoint: str, params: dict[str, Any]) -> URL:
    method _parse_json (line 65) | def _parse_json(content: str) -> dict[str, Any]:
    method request (line 74) | async def request(
    method request (line 83) | async def request(
    method request (line 94) | async def request(
    method playurl (line 110) | async def playurl(
    method view (line 132) | async def view(self, *, aid: int):
    method search (line 140) | async def search(self, *, keyword: str, page: int = 1, pagesize: int =...
    method search_hot (line 151) | async def search_hot(self, *, limit: int = 50):
    method search_suggest (line 159) | async def search_suggest(self, *, keyword: str, type: str = "accurate"):
    method space (line 168) | async def space(self, *, vmid: int, page: int = 1, pagesize: int = 10):
    method space_archive (line 178) | async def space_archive(self, *, vmid: int, page: int = 1, pagesize: i...
    method favorite_video (line 188) | async def favorite_video(
    method event_list (line 208) | async def event_list(
    method season_info (line 228) | async def season_info(self, *, season_id: int):
    method bangumi_source (line 237) | async def bangumi_source(self, *, episode_id: int):
    method season_recommend (line 246) | async def season_recommend(self, *, season_id: int):
    method timeline (line 256) | async def timeline(self, *, type: TimelineType = TimelineType.GLOBAL):
    method suggest (line 266) | async def suggest(self, *, keyword: str):  # NOTE: this endpoint is no...

FILE: hibiapi/api/bilibili/api/v2.py
  function process_keyerror (line 20) | def process_keyerror(function: _AnyCallable) -> _AnyCallable:
  class SearchType (line 32) | class SearchType(str, Enum):
  class BilibiliEndpointV2 (line 45) | class BilibiliEndpointV2(BaseEndpoint, cache_endpoints=False):
    method __init__ (line 46) | def __init__(self, client: AsyncHTTPClient):
    method playurl (line 51) | async def playurl(
    method seasoninfo (line 70) | async def seasoninfo(self, *, season_id: int):  # NOTE: not same with ...
    method source (line 73) | async def source(self, *, episode_id: int):
    method seasonrecommend (line 76) | async def seasonrecommend(self, *, season_id: int):  # NOTE: not same ...
    method search (line 79) | async def search(
    method timeline (line 99) | async def timeline(
    method space (line 104) | async def space(self, *, vmid: int, page: int = 1, pagesize: int = 10):
    method archive (line 111) | async def archive(self, *, vmid: int, page: int = 1, pagesize: int = 10):
    method favlist (line 118) | async def favlist(self, *, fid: int, vmid: int, page: int = 1, pagesiz...

FILE: hibiapi/api/bilibili/api/v3.py
  class BilibiliEndpointV3 (line 11) | class BilibiliEndpointV3(BaseEndpoint, cache_endpoints=False):
    method __init__ (line 12) | def __init__(self, client: AsyncHTTPClient):
    method video_info (line 16) | async def video_info(self, *, aid: int):
    method video_address (line 19) | async def video_address(
    method user_info (line 34) | async def user_info(self, *, uid: int, page: int = 1, size: int = 10):
    method user_uploaded (line 41) | async def user_uploaded(self, *, uid: int, page: int = 1, size: int = ...
    method user_favorite (line 48) | async def user_favorite(self, *, uid: int, fid: int, page: int = 1, si...
    method season_info (line 56) | async def season_info(self, *, season_id: int):
    method season_recommend (line 59) | async def season_recommend(self, *, season_id: int):
    method season_episode (line 62) | async def season_episode(self, *, episode_id: int):
    method season_timeline (line 65) | async def season_timeline(self, *, type: TimelineType = TimelineType.G...
    method search (line 68) | async def search(self, *, keyword: str, page: int = 1, size: int = 20):
    method search_recommend (line 75) | async def search_recommend(self, *, limit: int = 50):
    method search_suggestion (line 78) | async def search_suggestion(self, *, keyword: str):

FILE: hibiapi/api/bilibili/constants.py
  class BilibiliConstants (line 9) | class BilibiliConstants:

FILE: hibiapi/api/bilibili/net.py
  class NetRequest (line 8) | class NetRequest(BaseNetClient):
    method __init__ (line 9) | def __init__(self):

FILE: hibiapi/api/netease/api.py
  class SearchType (line 24) | class SearchType(IntEnum):
  class BitRateType (line 48) | class BitRateType(IntEnum):
  class MVResolutionType (line 58) | class MVResolutionType(IntEnum):
  class RecordPeriodType (line 68) | class RecordPeriodType(IntEnum):
  class _EncryptUtil (line 77) | class _EncryptUtil:
    method _aes (line 81) | def _aes(data: bytes, key: bytes) -> bytes:
    method _rsa (line 92) | def _rsa(data: bytes):
    method encrypt (line 101) | def encrypt(cls, data: dict[str, Any]) -> dict[str, str]:
  class NeteaseEndpoint (line 116) | class NeteaseEndpoint(BaseEndpoint):
    method _construct_headers (line 117) | def _construct_headers(self):
    method request (line 131) | async def request(
    method search (line 154) | async def search(
    method artist (line 173) | async def artist(self, *, id: int):
    method album (line 181) | async def album(self, *, id: int):
    method detail (line 189) | async def detail(
    method song (line 204) | async def song(
    method playlist (line 218) | async def playlist(self, *, id: int):
    method lyric (line 230) | async def lyric(self, *, id: int):
    method mv (line 242) | async def mv(self, *, id: int):
    method mv_url (line 250) | async def mv_url(
    method comments (line 264) | async def comments(self, *, id: int, offset: int = 0, limit: int = 1):
    method record (line 275) | async def record(self, *, id: int, period: RecordPeriodType = RecordPe...
    method djradio (line 284) | async def djradio(self, *, id: int):
    method dj (line 292) | async def dj(self, *, id: int, offset: int = 0, limit: int = 20, asc: ...
    method detail_dj (line 304) | async def detail_dj(self, *, id: int):
    method user (line 312) | async def user(self, *, id: int):
    method user_playlist (line 318) | async def user_playlist(self, *, id: int, limit: int = 50, offset: int...

FILE: hibiapi/api/netease/constants.py
  class NeteaseConstants (line 9) | class NeteaseConstants:

FILE: hibiapi/api/netease/net.py
  class NetRequest (line 8) | class NetRequest(BaseNetClient):
    method __init__ (line 9) | def __init__(self):

FILE: hibiapi/api/pixiv/api.py
  class IllustType (line 16) | class IllustType(str, Enum):
  class RankingType (line 26) | class RankingType(str, Enum):
  class SearchModeType (line 64) | class SearchModeType(str, Enum):
  class SearchNovelModeType (line 76) | class SearchNovelModeType(str, Enum):
  class SearchSortType (line 90) | class SearchSortType(str, Enum):
  class SearchDurationType (line 102) | class SearchDurationType(str, Enum):
  class RankingDate (line 113) | class RankingDate(date):
    method yesterday (line 115) | def yesterday(cls) -> "RankingDate":
    method toString (line 119) | def toString(self) -> str:
    method new (line 123) | def new(cls, date: date) -> "RankingDate":
  class PixivEndpoints (line 127) | class PixivEndpoints(BaseEndpoint):
    method _parse_accept_language (line 129) | def _parse_accept_language(accept_language: str) -> str:
    method request (line 135) | async def request(
    method request (line 144) | async def request(
    method request (line 154) | async def request(
    method illust (line 187) | async def illust(self, *, id: int):
    method member (line 191) | async def member(self, *, id: int):
    method member_illust (line 194) | async def member_illust(
    method favorite (line 211) | async def favorite(
    method favorite_novel (line 229) | async def favorite_novel(
    method following (line 244) | async def following(self, *, id: int, page: int = 1, size: int = 30):
    method follower (line 253) | async def follower(self, *, id: int, page: int = 1, size: int = 30):
    method rank (line 263) | async def rank(
    method search (line 280) | async def search(
    method popular_preview (line 306) | async def popular_preview(
    method search_user (line 326) | async def search_user(
    method tags_autocomplete (line 338) | async def tags_autocomplete(
    method tags (line 353) | async def tags(self):
    method related (line 357) | async def related(self, *, id: int, page: int = 1, size: int = 30):
    method ugoira_metadata (line 367) | async def ugoira_metadata(self, *, id: int):
    method illust_new (line 376) | async def illust_new(
    method spotlights (line 390) | async def spotlights(
    method illust_comments (line 407) | async def illust_comments(
    method illust_comment_replies (line 423) | async def illust_comment_replies(
    method novel_comments (line 436) | async def novel_comments(
    method novel_comment_replies (line 452) | async def novel_comment_replies(
    method rank_novel (line 465) | async def rank_novel(
    method member_novel (line 482) | async def member_novel(self, *, id: int, page: int = 1, size: int = 30):
    method novel_series (line 491) | async def novel_series(self, *, id: int):
    method novel_detail (line 494) | async def novel_detail(self, *, id: int):
    method novel_text (line 498) | async def novel_text(self, *, id: int):
    method webview_novel (line 504) | async def webview_novel(self, *, id: int):
    method tags_novel (line 518) | async def tags_novel(self):
    method search_novel (line 521) | async def search_novel(
    method popular_preview_novel (line 549) | async def popular_preview_novel(
    method novel_new (line 569) | async def novel_new(self, *, max_novel_id: Optional[int] = None):
    method live_list (line 575) | async def live_list(self, *, page: int = 1, size: int = 30):
    method related_novel (line 582) | async def related_novel(self, *, id: int, page: int = 1, size: int = 30):
    method related_member (line 592) | async def related_member(self, *, id: int):
    method illust_series (line 596) | async def illust_series(self, *, id: int, page: int = 1, size: int = 30):
    method member_illust_series (line 603) | async def member_illust_series(self, *, id: int, page: int = 1, size: ...
    method member_novel_series (line 610) | async def member_novel_series(self, *, id: int, page: int = 1, size: i...

FILE: hibiapi/api/pixiv/constants.py
  class PixivConstants (line 6) | class PixivConstants:

FILE: hibiapi/api/pixiv/net.py
  class AccountDataModel (line 15) | class AccountDataModel(BaseModel):
    class Config (line 16) | class Config:
  class PixivUserData (line 20) | class PixivUserData(AccountDataModel):
  class PixivAuthData (line 28) | class PixivAuthData(AccountDataModel):
  class NetRequest (line 36) | class NetRequest(BaseNetClient):
    method __init__ (line 37) | def __init__(self, tokens: list[str]):
    method get_available_user (line 47) | def get_available_user(self):
    method auth (line 56) | async def auth(self, refresh_token: str):

FILE: hibiapi/api/qrcode.py
  class HostUrl (line 25) | class HostUrl(BaseHostUrl):
  class QRCodeLevel (line 30) | class QRCodeLevel(str, Enum):
  class ReturnEncode (line 44) | class ReturnEncode(str, Enum):
  class QRInfo (line 59) | class QRInfo(BaseModel):
    method new (line 72) | async def new(
    method _generate (line 111) | def _generate(

FILE: hibiapi/api/sauce/api.py
  class UnavailableSourceException (line 15) | class UnavailableSourceException(ClientSideException):
  class ImageSourceOversizedException (line 20) | class ImageSourceOversizedException(UnavailableSourceException):
  class HostUrl (line 28) | class HostUrl(BaseHostUrl):
  class UploadFileIO (line 32) | class UploadFileIO(BytesIO):
    method __get_validators__ (line 34) | def __get_validators__(cls):
    method validate (line 38) | def validate(cls, v: Any) -> BytesIO:
  class DeduplicateType (line 45) | class DeduplicateType(IntEnum):
  class SauceEndpoint (line 54) | class SauceEndpoint(BaseEndpoint, cache_endpoints=False):
    method fetch (line 57) | async def fetch(self, host: HostUrl) -> UploadFileIO:
    method request (line 72) | async def request(
    method search (line 92) | async def search(
    method search (line 105) | async def search(
    method search (line 117) | async def search(

FILE: hibiapi/api/sauce/constants.py
  class SauceConstants (line 8) | class SauceConstants:

FILE: hibiapi/api/sauce/net.py
  class NetRequest (line 6) | class NetRequest(BaseNetClient):
    method __init__ (line 7) | def __init__(self):

FILE: hibiapi/api/tieba/api.py
  class TiebaSignUtils (line 13) | class TiebaSignUtils:
    method random_digit (line 17) | def random_digit(length: int) -> str:
    method construct_content (line 21) | def construct_content(params: dict[str, Any]) -> bytes:
    method sign (line 37) | def sign(cls, params: dict[str, Any]) -> bytes:
  class TiebaEndpoint (line 61) | class TiebaEndpoint(BaseEndpoint):
    method request (line 66) | async def request(
    method post_list (line 76) | async def post_list(self, *, name: str, page: int = 1, size: int = 50):
    method post_detail (line 86) | async def post_detail(
    method subpost_detail (line 104) | async def subpost_detail(
    method user_profile (line 122) | async def user_profile(self, *, uid: int):
    method user_subscribed (line 132) | async def user_subscribed(

FILE: hibiapi/api/tieba/net.py
  class NetRequest (line 4) | class NetRequest(BaseNetClient):

FILE: hibiapi/api/wallpaper/api.py
  class WallpaperCategoryType (line 15) | class WallpaperCategoryType(str, Enum):
  class WallpaperOrderType (line 52) | class WallpaperOrderType(str, Enum):
  class WallpaperEndpoint (line 61) | class WallpaperEndpoint(BaseEndpoint):
    method request (line 66) | async def request(
    method wallpaper (line 81) | async def wallpaper(
    method vertical (line 105) | async def vertical(

FILE: hibiapi/api/wallpaper/constants.py
  class WallpaperConstants (line 6) | class WallpaperConstants:

FILE: hibiapi/api/wallpaper/net.py
  class NetRequest (line 6) | class NetRequest(BaseNetClient):
    method __init__ (line 7) | def __init__(self):

FILE: hibiapi/app/application.py
  class AuthorizationModel (line 51) | class AuthorizationModel(BaseModel):
  function basic_authorization_depend (line 62) | async def basic_authorization_depend(
  function rate_limit_depend (line 84) | async def rate_limit_depend(request: Request):
  function flush_sentry (line 105) | async def flush_sentry():
  function cleanup_clients (line 113) | async def cleanup_clients():
  function fastapi_lifespan (line 126) | async def fastapi_lifespan(app: FastAPI):
  function redirect (line 151) | async def redirect():
  function robots (line 156) | async def robots():
  function redirect_workaround_middleware (line 162) | async def redirect_workaround_middleware(request: Request, call_next):

FILE: hibiapi/app/handlers.py
  function exception_handler (line 14) | async def exception_handler(
  function override_handler (line 31) | async def override_handler(
  function assertion_handler (line 46) | async def assertion_handler(request: Request, exc: AssertionError):
  function validation_handler (line 55) | async def validation_handler(request: Request, exc: PydanticValidationEr...

FILE: hibiapi/app/middlewares.py
  function request_logger (line 43) | async def request_logger(request: Request, call_next: RequestHandler) ->...
  function contextvar_setter (line 74) | async def contextvar_setter(request: Request, call_next: RequestHandler):
  function uncaught_exception_handler (line 83) | async def uncaught_exception_handler(

FILE: hibiapi/app/routes/__init__.py
  class RouteInterface (line 28) | class RouteInterface(Protocol):

FILE: hibiapi/app/routes/bika.py
  function x_image_quality (line 22) | async def x_image_quality(

FILE: hibiapi/app/routes/pixiv.py
  function accept_language (line 14) | async def accept_language(

FILE: hibiapi/app/routes/qrcode.py
  function qrcode_api (line 36) | async def qrcode_api(

FILE: hibiapi/app/routes/sauce.py
  function request_client (line 26) | async def request_client():
  function sauce_url (line 32) | async def sauce_url(
  function sauce_form (line 78) | async def sauce_form(

FILE: hibiapi/utils/cache.py
  class CacheConfig (line 35) | class CacheConfig(BaseModel):
    method new (line 42) | def new(
  function cache_config (line 57) | def cache_config(
  class CachedValidatedFunction (line 76) | class CachedValidatedFunction(ValidatedFunction):
    method serialize (line 77) | def serialize(self, args: tuple[Any, ...], kwargs: dict[str, Any]) -> ...
  function endpoint_cache (line 82) | def endpoint_cache(function: T_AsyncFunc) -> T_AsyncFunc:

FILE: hibiapi/utils/config.py
  class ConfigSubView (line 18) | class ConfigSubView(confuse.Subview):
    method get (line 20) | def get(self) -> Any: ...
    method get (line 23) | def get(self, template: type[_T]) -> _T: ...
    method get (line 25) | def get(self, template: Optional[type[_T]] = None):  # type: ignore
    method get_optional (line 31) | def get_optional(self, template: type[_T]) -> Optional[_T]:
    method as_str (line 37) | def as_str(self) -> str:
    method as_str_seq (line 40) | def as_str_seq(self, split: str = "\n") -> list[str]:  # type: ignore
    method as_number (line 47) | def as_number(self) -> int:
    method as_bool (line 50) | def as_bool(self) -> bool:
    method as_path (line 53) | def as_path(self) -> Path:
    method as_dict (line 56) | def as_dict(self) -> dict[str, Any]:
    method __getitem__ (line 59) | def __getitem__(self, key: str) -> "ConfigSubView":
  class AppConfig (line 63) | class AppConfig(confuse.Configuration):
    method __init__ (line 64) | def __init__(self, name: str):
    method config_dir (line 71) | def config_dir(self) -> str:
    method user_config_path (line 74) | def user_config_path(self) -> str:
    method _add_env_source (line 77) | def _add_env_source(self):
    method _add_default_source (line 102) | def _add_default_source(self):
    method _add_user_source (line 105) | def _add_user_source(self):
    method __getitem__ (line 108) | def __getitem__(self, key: str) -> ConfigSubView:
  class GeneralConfig (line 112) | class GeneralConfig(AppConfig):
    method __init__ (line 113) | def __init__(self, name: str):
  class APIConfig (line 117) | class APIConfig(GeneralConfig):

FILE: hibiapi/utils/decorators/__init__.py
  class RetryT (line 21) | class RetryT(Protocol):
    method __call__ (line 23) | def __call__(self, function: Callable_T) -> Callable_T: ...
    method __call__ (line 26) | def __call__(
    method __call__ (line 34) | def __call__(
  function Retry (line 45) | def Retry(function: Callable_T) -> Callable_T: ...
  function Retry (line 49) | def Retry(
  function Retry (line 57) | def Retry(
  function ToAsync (line 115) | def ToAsync(

FILE: hibiapi/utils/decorators/enum.py
  function enum_auto_doc (line 9) | def enum_auto_doc(enum: _ET) -> _ET:

FILE: hibiapi/utils/decorators/timer.py
  class TimerError (line 14) | class TimerError(Exception):
  class Timer (line 19) | class Timer:
    method __post_init__ (line 28) | def __post_init__(self) -> None:
    method start (line 33) | def start(self) -> None:
    method stop (line 40) | def stop(self) -> float:
    method __enter__ (line 57) | def __enter__(self) -> Timer:
    method __exit__ (line 62) | def __exit__(self, *exc_info: Any) -> None:
    method _recreate_cm (line 66) | def _recreate_cm(self) -> Timer:
    method __call__ (line 69) | def __call__(self, function: Callable_T) -> Callable_T:

FILE: hibiapi/utils/exceptions.py
  class ExceptionReturn (line 7) | class ExceptionReturn(BaseModel):
    class Config (line 14) | class Config:
  class BaseServerException (line 18) | class BaseServerException(Exception):
    method __init__ (line 23) | def __init__(
  class BaseHTTPException (line 40) | class BaseHTTPException(BaseServerException):
  class ServerSideException (line 44) | class ServerSideException(BaseServerException):
  class UpstreamAPIException (line 49) | class UpstreamAPIException(ServerSideException):
  class UncaughtException (line 54) | class UncaughtException(ServerSideException):
    method with_exception (line 60) | def with_exception(cls, e: Exception):
  class ClientSideException (line 66) | class ClientSideException(BaseServerException):
  class ValidationException (line 71) | class ValidationException(ClientSideException):
  class RateLimitReachedException (line 75) | class RateLimitReachedException(ClientSideException):

FILE: hibiapi/utils/log.py
  class LoguruHandler (line 17) | class LoguruHandler(logging.Handler):
    method escape_tag (line 21) | def escape_tag(cls, string: str) -> str:
    method emit (line 24) | def emit(self, record: logging.LogRecord):

FILE: hibiapi/utils/net.py
  class AsyncHTTPClient (line 32) | class AsyncHTTPClient(AsyncClient):
    method _log_request (line 36) | async def _log_request(request: Request):
    method _log_response (line 43) | async def _log_response(response: Response):
    method request (line 55) | async def request(self, method: str, url: Union[URL, str], **kwargs):
  class BaseNetClient (line 63) | class BaseNetClient:
    method __init__ (line 69) | def __init__(
    method create_client (line 82) | def create_client(self):
    method __aenter__ (line 94) | async def __aenter__(self):
    method __aexit__ (line 101) | async def __aexit__(
  function catch_network_error (line 118) | def catch_network_error(function: AsyncCallable_T) -> AsyncCallable_T:

FILE: hibiapi/utils/routing.py
  function dont_route (line 23) | def dont_route(func: AsyncCallable_T) -> AsyncCallable_T:
  class EndpointMeta (line 28) | class EndpointMeta(type):
    method _list_router_function (line 30) | def _list_router_function(members: dict[str, Any]):
    method __new__ (line 41) | def __new__(
    method router_functions (line 57) | def router_functions(self):
  class BaseEndpoint (line 61) | class BaseEndpoint(metaclass=EndpointMeta, cache_endpoints=False):
    method __init__ (line 62) | def __init__(self, client: AsyncHTTPClient):
    method _join (line 66) | def _join(base: str, endpoint: str, params: dict[str, Any]) -> URL:
  class SlashRouter (line 86) | class SlashRouter(APIRouter):
    method api_route (line 87) | def api_route(self, path: str, **kwargs):
  class EndpointRouter (line 92) | class EndpointRouter(SlashRouter):
    method _exclude_params (line 94) | def _exclude_params(func: Callable, params: Mapping[str, Any]) -> dict...
    method _router_signature_convert (line 99) | def _router_signature_convert(
    method include_endpoint (line 127) | def include_endpoint(
  class BaseHostUrl (line 166) | class BaseHostUrl(AnyHttpUrl):
    method validate_host (line 170) | def validate_host(cls, parts) -> tuple[str, Optional[str], str, bool]:
    method _check_domain (line 177) | def _check_domain(cls, host: str) -> bool:

FILE: hibiapi/utils/temp.py
  class TempFile (line 9) | class TempFile:
    method create (line 17) | def create(cls, ext: str = ".tmp"):
    method to_url (line 22) | def to_url(cls, request: Request, path: Path) -> str:

FILE: scripts/pixiv_login.py
  class RequestInterceptor (line 39) | class RequestInterceptor(QWebEngineUrlRequestInterceptor):
    method __init__ (line 42) | def __init__(self):
    method interceptRequest (line 45) | def interceptRequest(self, info: QWebEngineUrlRequestInfo) -> None:
  class WebView (line 61) | class WebView(QWebEngineView):
    method __init__ (line 62) | def __init__(self):
    method start (line 86) | def start(self, goto: str):
    method _on_cookie_added (line 91) | def _on_cookie_added(self, cookie: QNetworkCookie):
  class ResponseDataWidget (line 99) | class ResponseDataWidget(QWidget):
    method __init__ (line 100) | def __init__(self, webview: WebView):
    method _on_clipboard_copy (line 120) | def _on_clipboard_copy(self, checked: bool):
  class LoginPhrase (line 128) | class LoginPhrase:
    method s256 (line 130) | def s256(data: bytes):
    method oauth_pkce (line 134) | def oauth_pkce(cls) -> tuple[str, str]:
    method __init__ (line 139) | def __init__(self: _T, url_open_callback: Callable[[str, _T], None]):
    method code_received (line 150) | def code_received(self, code: str):
  class MainWindow (line 179) | class MainWindow(QMainWindow):
    method __init__ (line 180) | def __init__(self):
  function url_open_callback (line 202) | def url_open_callback(url: str, login_phrase: LoginPhrase):

FILE: test/test_base.py
  function client (line 10) | def client():
  function test_openapi (line 17) | def test_openapi(client: TestClient, in_stress: bool = False):
  function test_doc_page (line 26) | def test_doc_page(client: TestClient, in_stress: bool = False):
  function test_openapi_stress (line 39) | def test_openapi_stress(client: TestClient, benchmark: BenchmarkFixture):
  function test_doc_page_stress (line 49) | def test_doc_page_stress(client: TestClient, benchmark: BenchmarkFixture):
  function test_notfound (line 55) | def test_notfound(client: TestClient):
  function test_net_request (line 64) | def test_net_request():

FILE: test/test_bika.py
  function client (line 8) | def client():
  function test_collections (line 18) | def test_collections(client: TestClient):
  function test_categories (line 24) | def test_categories(client: TestClient):
  function test_keywords (line 30) | def test_keywords(client: TestClient):
  function test_advanced_search (line 36) | def test_advanced_search(client: TestClient):
  function test_category_list (line 44) | def test_category_list(client: TestClient):
  function test_author_list (line 52) | def test_author_list(client: TestClient):
  function test_comic_detail (line 60) | def test_comic_detail(client: TestClient):
  function test_comic_recommendation (line 66) | def test_comic_recommendation(client: TestClient):
  function test_comic_episodes (line 74) | def test_comic_episodes(client: TestClient):
  function test_comic_page (line 80) | def test_comic_page(client: TestClient):
  function test_comic_comments (line 86) | def test_comic_comments(client: TestClient):
  function test_games (line 92) | def test_games(client: TestClient):
  function test_game_detail (line 98) | def test_game_detail(client: TestClient):

FILE: test/test_bilibili_v2.py
  function client (line 8) | def client():
  function test_playurl (line 17) | def test_playurl(client: TestClient):
  function test_paged_playurl (line 23) | def test_paged_playurl(client: TestClient):
  function test_seasoninfo (line 31) | def test_seasoninfo(client: TestClient):
  function test_seasonrecommend (line 37) | def test_seasonrecommend(client: TestClient):
  function test_search (line 43) | def test_search(client: TestClient):
  function test_search_suggest (line 49) | def test_search_suggest(client: TestClient):
  function test_search_hot (line 59) | def test_search_hot(client: TestClient):
  function test_timeline (line 69) | def test_timeline(client: TestClient):
  function test_space (line 77) | def test_space(client: TestClient):
  function test_archive (line 83) | def test_archive(client: TestClient):
  function test_favlist (line 90) | def test_favlist(client: TestClient):

FILE: test/test_bilibili_v3.py
  function client (line 8) | def client():
  function test_video_info (line 17) | def test_video_info(client: TestClient):
  function test_video_address (line 23) | def test_video_address(client: TestClient):
  function test_user_info (line 34) | def test_user_info(client: TestClient):
  function test_user_uploaded (line 40) | def test_user_uploaded(client: TestClient):
  function test_user_favorite (line 47) | def test_user_favorite(client: TestClient):
  function test_season_info (line 52) | def test_season_info(client: TestClient):
  function test_season_recommend (line 58) | def test_season_recommend(client: TestClient):
  function test_season_episode (line 64) | def test_season_episode(client: TestClient):
  function test_season_timeline (line 70) | def test_season_timeline(client: TestClient):
  function test_search (line 76) | def test_search(client: TestClient):
  function test_search_recommend (line 82) | def test_search_recommend(client: TestClient):
  function test_search_suggestion (line 88) | def test_search_suggestion(client: TestClient):

FILE: test/test_netease.py
  function client (line 8) | def client():
  function test_search (line 17) | def test_search(client: TestClient):
  function test_artist (line 26) | def test_artist(client: TestClient):
  function test_album (line 32) | def test_album(client: TestClient):
  function test_detail (line 38) | def test_detail(client: TestClient):
  function test_detail_multiple (line 44) | def test_detail_multiple(client: TestClient):
  function test_song (line 53) | def test_song(client: TestClient):
  function test_song_multiple (line 59) | def test_song_multiple(client: TestClient):
  function test_playlist (line 70) | def test_playlist(client: TestClient):
  function test_lyric (line 76) | def test_lyric(client: TestClient):
  function test_mv (line 82) | def test_mv(client: TestClient):
  function test_mv_url (line 88) | def test_mv_url(client: TestClient):
  function test_comments (line 94) | def test_comments(client: TestClient):
  function test_record (line 100) | def test_record(client: TestClient):
  function test_djradio (line 107) | def test_djradio(client: TestClient):
  function test_dj (line 113) | def test_dj(client: TestClient):
  function test_detail_dj (line 119) | def test_detail_dj(client: TestClient):
  function test_user (line 125) | def test_user(client: TestClient):
  function test_user_playlist (line 131) | def test_user_playlist(client: TestClient):
  function test_search_redirect (line 137) | def test_search_redirect(client: TestClient):

FILE: test/test_pixiv.py
  function client (line 10) | def client():
  function test_illust (line 21) | def test_illust(client: TestClient):
  function test_member (line 28) | def test_member(client: TestClient):
  function test_member_illust (line 34) | def test_member_illust(client: TestClient):
  function test_favorite (line 40) | def test_favorite(client: TestClient):
  function test_favorite_novel (line 45) | def test_favorite_novel(client: TestClient):
  function test_following (line 50) | def test_following(client: TestClient):
  function test_follower (line 56) | def test_follower(client: TestClient):
  function test_rank (line 62) | def test_rank(client: TestClient):
  function test_search (line 71) | def test_search(client: TestClient):
  function test_popular_preview (line 77) | def test_popular_preview(client: TestClient):
  function test_search_user (line 83) | def test_search_user(client: TestClient):
  function test_tags (line 89) | def test_tags(client: TestClient):
  function test_tags_autocomplete (line 95) | def test_tags_autocomplete(client: TestClient):
  function test_related (line 101) | def test_related(client: TestClient):
  function test_ugoira_metadata (line 107) | def test_ugoira_metadata(client: TestClient):
  function test_spotlights (line 113) | def test_spotlights(client: TestClient):
  function test_illust_new (line 119) | def test_illust_new(client: TestClient):
  function test_illust_comments (line 125) | def test_illust_comments(client: TestClient):
  function test_illust_comment_replies (line 131) | def test_illust_comment_replies(client: TestClient):
  function test_novel_comments (line 137) | def test_novel_comments(client: TestClient):
  function test_novel_comment_replies (line 143) | def test_novel_comment_replies(client: TestClient):
  function test_rank_novel (line 149) | def test_rank_novel(client: TestClient):
  function test_member_novel (line 158) | def test_member_novel(client: TestClient):
  function test_novel_series (line 164) | def test_novel_series(client: TestClient):
  function test_novel_detail (line 170) | def test_novel_detail(client: TestClient):
  function test_novel_text (line 176) | def test_novel_text(client: TestClient):
  function test_webview_novel (line 182) | def test_webview_novel(client: TestClient):
  function test_live_list (line 188) | def test_live_list(client: TestClient):
  function test_related_novel (line 194) | def test_related_novel(client: TestClient):
  function test_related_member (line 200) | def test_related_member(client: TestClient):
  function test_illust_series (line 206) | def test_illust_series(client: TestClient):
  function test_member_illust_series (line 212) | def test_member_illust_series(client: TestClient):
  function test_member_novel_series (line 218) | def test_member_novel_series(client: TestClient):
  function test_tags_novel (line 224) | def test_tags_novel(client: TestClient):
  function test_search_novel (line 230) | def test_search_novel(client: TestClient):
  function test_popular_preview_novel (line 236) | def test_popular_preview_novel(client: TestClient):
  function test_novel_new (line 242) | def test_novel_new(client: TestClient):
  function test_request_cache (line 248) | def test_request_cache(client: TestClient, benchmark: BenchmarkFixture):
  function test_rank_redirect (line 271) | def test_rank_redirect(client: TestClient):
  function test_rate_limit (line 279) | def test_rate_limit(client: TestClient):

FILE: test/test_qrcode.py
  function client (line 11) | def client():
  function test_qrcode_generate (line 20) | def test_qrcode_generate(client: TestClient, in_stress: bool = False):
  function test_qrcode_all (line 35) | def test_qrcode_all(client: TestClient):
  function test_qrcode_stress (line 51) | def test_qrcode_stress(client: TestClient, benchmark: BenchmarkFixture):
  function test_qrcode_redirect (line 60) | def test_qrcode_redirect(client: TestClient):

FILE: test/test_sauce.py
  function client (line 12) | def client():
  function test_sauce_url (line 22) | def test_sauce_url(client: TestClient, httpserver: HTTPServer):
  function test_sauce_file (line 31) | def test_sauce_file(client: TestClient):

FILE: test/test_tieba.py
  function client (line 8) | def client():
  function test_post_list (line 17) | def test_post_list(client: TestClient):
  function test_post_list_chinese (line 24) | def test_post_list_chinese(client: TestClient):
  function test_post_detail (line 32) | def test_post_detail(client: TestClient):
  function test_subpost_detail (line 39) | def test_subpost_detail(client: TestClient):
  function test_user_profile (line 47) | def test_user_profile(client: TestClient):

FILE: test/test_wallpaper.py
  function client (line 8) | def client():
  function test_wallpaper (line 18) | def test_wallpaper(client: TestClient):
  function test_wallpaper_limit (line 24) | def test_wallpaper_limit(client: TestClient):
  function test_wallpaper_skip (line 32) | def test_wallpaper_skip(client: TestClient):
  function test_vertical (line 47) | def test_vertical(client: TestClient):
  function test_vertical_limit (line 53) | def test_vertical_limit(client: TestClient):
  function test_vertical_skip (line 60) | def test_vertical_skip(client: TestClient):
Condensed preview — 100 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (226K chars).
[
  {
    "path": ".all-contributorsrc",
    "chars": 2648,
    "preview": "{\n  \"files\": [\n    \"README.md\"\n  ],\n  \"imageSize\": 100,\n  \"commit\": false,\n  \"contributors\": [\n    {\n      \"login\": \"Kyo"
  },
  {
    "path": ".flake8",
    "chars": 61,
    "preview": "[flake8]\nmax-line-length = 90\nignore = W391, W292, W503, E203"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 632,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/docker.yml",
    "chars": 1449,
    "preview": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 1138,
    "preview": "name: Lint\n\non:\n  push:\n    branches: [main, dev]\n\n  pull_request_target:\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    "
  },
  {
    "path": ".github/workflows/mirror.yml",
    "chars": 515,
    "preview": "name: Gitee Mirror\n\non:\n  push:\n    branches: [main, dev]\n\n  schedule:\n    - cron: \"0 0 * * *\"\n\n  workflow_dispatch:\n\nco"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 526,
    "preview": "name: Release\n\non:\n  create:\n    tags: [v*]\n  workflow_dispatch:\n\njobs:\n  release:\n    name: Create release\n    runs-on:"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1843,
    "preview": "name: Test\n\non:\n  workflow_dispatch:\n\n  push:\n    branches: [main, dev]\n\n  pull_request_target:\n\nconcurrency:\n  group: $"
  },
  {
    "path": ".gitignore",
    "chars": 3131,
    "preview": "# Project ignore\ndata/**\nconfigs/**.yml\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C "
  },
  {
    "path": ".replit",
    "chars": 50,
    "preview": "language = \"python3\"\nrun = \"python -m hibiapi run\""
  },
  {
    "path": ".vscode/docstring.mustache",
    "chars": 907,
    "preview": "{{! FastAPI Automatic docstring}}\n\n## Name: `{{name}}`\n\n> {{summaryPlaceholder}} {{extendedSummaryPlaceholder}}\n{{#argsE"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 299,
    "preview": "{\n    \"recommendations\": [\n        \"visualstudioexptteam.vscodeintellicode\",\n        \"ms-python.python\",\n        \"ms-pyt"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 330,
    "preview": "{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Python: Module\",\n            \"type\": \"p"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 1686,
    "preview": "{\n    \"python.analysis.completeFunctionParens\": true,\n    \"python.analysis.typeCheckingMode\": \"basic\",\n    \"python.langu"
  },
  {
    "path": "Dockerfile",
    "chars": 333,
    "preview": "FROM python:bullseye\n\nEXPOSE 8080\n\nENV PORT=8080 \\\n    PROCS=1 \\\n    GENERAL_SERVER_HOST=0.0.0.0\n\nCOPY . /hibi\n\nWORKDIR "
  },
  {
    "path": "LICENSE",
    "chars": 11349,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 9011,
    "preview": "<!-- spell-checker: disable -->\n<!-- markdownlint-disable MD033 MD041 -->\n\n<img src=\".github/logo.svg\" align=\"right\">\n\n<"
  },
  {
    "path": "docker-compose.yml",
    "chars": 740,
    "preview": "version: \"3.9\"\n\nvolumes:\n  hibi_redis: {}\n\nnetworks:\n  hibi_net: {}\n\nservices:\n  redis:\n    image: redis:alpine\n    cont"
  },
  {
    "path": "hibiapi/__init__.py",
    "chars": 480,
    "preview": "r\"\"\"\n  _    _ _ _     _          _____ _____  \n | |  | (_) |   (_)   /\\   |  __ \\_   _| \n | |__| |_| |__  _   /  \\  | |_"
  },
  {
    "path": "hibiapi/__main__.py",
    "chars": 3244,
    "preview": "import os\nfrom pathlib import Path\n\nimport typer\nimport uvicorn\n\nfrom hibiapi import __file__ as root_file\nfrom hibiapi "
  },
  {
    "path": "hibiapi/api/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "hibiapi/api/bika/__init__.py",
    "chars": 175,
    "preview": "from .api import BikaEndpoints, ImageQuality, ResultSort  # noqa: F401\nfrom .constants import BikaConstants  # noqa: F40"
  },
  {
    "path": "hibiapi/api/bika/api.py",
    "chars": 5618,
    "preview": "import hashlib\nimport hmac\nfrom datetime import timedelta\nfrom enum import Enum\nfrom time import time\nfrom typing import"
  },
  {
    "path": "hibiapi/api/bika/constants.py",
    "chars": 662,
    "preview": "from hibiapi.utils.config import APIConfig\n\n\nclass BikaConstants:\n    DIGEST_KEY = b\"~d}$Q7$eIni=V)9\\\\RK/P.RM4;9[7|@/CA}"
  },
  {
    "path": "hibiapi/api/bika/net.py",
    "chars": 2041,
    "preview": "import asyncio\nfrom base64 import urlsafe_b64decode\nfrom datetime import datetime, timezone\nfrom functools import lru_ca"
  },
  {
    "path": "hibiapi/api/bilibili/__init__.py",
    "chars": 127,
    "preview": "# flake8:noqa:F401\nfrom .api import *  # noqa: F401, F403\nfrom .constants import BilibiliConstants\nfrom .net import NetR"
  },
  {
    "path": "hibiapi/api/bilibili/api/__init__.py",
    "chars": 189,
    "preview": "# flake8:noqa:F401\nfrom .base import BaseBilibiliEndpoint, TimelineType, VideoFormatType, VideoQualityType\nfrom .v2 impo"
  },
  {
    "path": "hibiapi/api/bilibili/api/base.py",
    "chars": 7243,
    "preview": "import hashlib\nimport json\nfrom enum import Enum, IntEnum\nfrom time import time\nfrom typing import Any, Optional, overlo"
  },
  {
    "path": "hibiapi/api/bilibili/api/v2.py",
    "chars": 3664,
    "preview": "from collections.abc import Coroutine\nfrom enum import Enum\nfrom functools import wraps\nfrom typing import Callable, Opt"
  },
  {
    "path": "hibiapi/api/bilibili/api/v3.py",
    "chars": 2460,
    "preview": "from hibiapi.api.bilibili.api.base import (\n    BaseBilibiliEndpoint,\n    TimelineType,\n    VideoFormatType,\n    VideoQu"
  },
  {
    "path": "hibiapi/api/bilibili/constants.py",
    "chars": 1111,
    "preview": "from http.cookies import SimpleCookie\nfrom typing import Any\n\nfrom hibiapi.utils.config import APIConfig\n\n_CONFIG = APIC"
  },
  {
    "path": "hibiapi/api/bilibili/net.py",
    "chars": 363,
    "preview": "from httpx import Cookies\n\nfrom hibiapi.utils.net import BaseNetClient\n\nfrom .constants import BilibiliConstants\n\n\nclass"
  },
  {
    "path": "hibiapi/api/netease/__init__.py",
    "chars": 163,
    "preview": "# flake8:noqa:F401\nfrom .api import BitRateType, NeteaseEndpoint, RecordPeriodType, SearchType\nfrom .constants import Ne"
  },
  {
    "path": "hibiapi/api/netease/api.py",
    "chars": 8186,
    "preview": "import base64\nimport json\nimport secrets\nimport string\nfrom datetime import timedelta\nfrom enum import IntEnum\nfrom ipad"
  },
  {
    "path": "hibiapi/api/netease/constants.py",
    "chars": 1066,
    "preview": "from http.cookies import SimpleCookie\nfrom ipaddress import IPv4Network\n\nfrom hibiapi.utils.config import APIConfig\n\n_Co"
  },
  {
    "path": "hibiapi/api/netease/net.py",
    "chars": 349,
    "preview": "from httpx import Cookies\n\nfrom hibiapi.utils.net import BaseNetClient\n\nfrom .constants import NeteaseConstants\n\n\nclass "
  },
  {
    "path": "hibiapi/api/pixiv/__init__.py",
    "chars": 280,
    "preview": "# flake8:noqa:F401\nfrom .api import (\n    IllustType,\n    PixivEndpoints,\n    RankingDate,\n    RankingType,\n    SearchDu"
  },
  {
    "path": "hibiapi/api/pixiv/api.py",
    "chars": 17038,
    "preview": "import json\nimport re\nfrom datetime import date, timedelta\nfrom enum import Enum\nfrom typing import Any, Literal, Option"
  },
  {
    "path": "hibiapi/api/pixiv/constants.py",
    "chars": 637,
    "preview": "from typing import Any\n\nfrom hibiapi.utils.config import APIConfig\n\n\nclass PixivConstants:\n    DEFAULT_HEADERS: dict[str"
  },
  {
    "path": "hibiapi/api/pixiv/net.py",
    "chars": 2695,
    "preview": "import asyncio\nimport hashlib\nfrom datetime import datetime, timedelta, timezone\nfrom itertools import cycle\n\nfrom httpx"
  },
  {
    "path": "hibiapi/api/qrcode.py",
    "chars": 4465,
    "preview": "from datetime import datetime\nfrom enum import Enum\nfrom io import BytesIO\nfrom os import fdopen\nfrom pathlib import Pat"
  },
  {
    "path": "hibiapi/api/sauce/__init__.py",
    "chars": 156,
    "preview": "# flake8:noqa:F401\nfrom .api import DeduplicateType, HostUrl, SauceEndpoint, UploadFileIO\nfrom .constants import SauceCo"
  },
  {
    "path": "hibiapi/api/sauce/api.py",
    "chars": 4104,
    "preview": "import random\nfrom enum import IntEnum\nfrom io import BytesIO\nfrom typing import Any, Optional, overload\n\nfrom httpx imp"
  },
  {
    "path": "hibiapi/api/sauce/constants.py",
    "chars": 630,
    "preview": "from typing import Any\n\nfrom hibiapi.utils.config import APIConfig\n\n_Config = APIConfig(\"sauce\")\n\n\nclass SauceConstants:"
  },
  {
    "path": "hibiapi/api/sauce/net.py",
    "chars": 285,
    "preview": "from hibiapi.utils.net import BaseNetClient\n\nfrom .constants import SauceConstants\n\n\nclass NetRequest(BaseNetClient):\n  "
  },
  {
    "path": "hibiapi/api/tieba/__init__.py",
    "chars": 86,
    "preview": "# flake8:noqa:F401\nfrom .api import Config, TiebaEndpoint\nfrom .net import NetRequest\n"
  },
  {
    "path": "hibiapi/api/tieba/api.py",
    "chars": 3908,
    "preview": "import hashlib\nfrom enum import Enum\nfrom random import randint\nfrom typing import Any, Optional\n\nfrom hibiapi.utils.con"
  },
  {
    "path": "hibiapi/api/tieba/net.py",
    "chars": 88,
    "preview": "from hibiapi.utils.net import BaseNetClient\n\n\nclass NetRequest(BaseNetClient):\n    pass\n"
  },
  {
    "path": "hibiapi/api/wallpaper/__init__.py",
    "chars": 133,
    "preview": "# flake8:noqa:F401\nfrom .api import Config, WallpaperCategoryType, WallpaperEndpoint, WallpaperOrderType\nfrom .net impor"
  },
  {
    "path": "hibiapi/api/wallpaper/api.py",
    "chars": 3260,
    "preview": "from datetime import timedelta\nfrom enum import Enum\nfrom typing import Any, Optional\n\nfrom hibiapi.utils.cache import c"
  },
  {
    "path": "hibiapi/api/wallpaper/constants.py",
    "chars": 197,
    "preview": "from hibiapi.utils.config import APIConfig\n\n_CONFIG = APIConfig(\"wallpaper\")\n\n\nclass WallpaperConstants:\n    CONFIG: API"
  },
  {
    "path": "hibiapi/api/wallpaper/net.py",
    "chars": 226,
    "preview": "from hibiapi.utils.net import BaseNetClient\n\nfrom .constants import WallpaperConstants\n\n\nclass NetRequest(BaseNetClient)"
  },
  {
    "path": "hibiapi/app/__init__.py",
    "chars": 91,
    "preview": "# flake8:noqa:F401\nfrom . import application, handlers, middlewares\n\napp = application.app\n"
  },
  {
    "path": "hibiapi/app/application.py",
    "chars": 5384,
    "preview": "import asyncio\nimport re\nfrom contextlib import asynccontextmanager\nfrom ipaddress import ip_address\nfrom secrets import"
  },
  {
    "path": "hibiapi/app/handlers.py",
    "chars": 1908,
    "preview": "from fastapi import Request, Response\nfrom fastapi.exceptions import HTTPException as FastAPIHTTPException\nfrom fastapi."
  },
  {
    "path": "hibiapi/app/middlewares.py",
    "chars": 3333,
    "preview": "from collections.abc import Awaitable\nfrom datetime import datetime\nfrom typing import Callable\n\nfrom fastapi import Req"
  },
  {
    "path": "hibiapi/app/routes/__init__.py",
    "chars": 1137,
    "preview": "from typing import Protocol, cast\n\nfrom hibiapi.app.routes import (\n    bika,\n    bilibili,\n    netease,\n    pixiv,\n    "
  },
  {
    "path": "hibiapi/app/routes/bika.py",
    "chars": 912,
    "preview": "from typing import Annotated\n\nfrom fastapi import Depends, Header\n\nfrom hibiapi.api.bika import (\n    BikaConstants,\n   "
  },
  {
    "path": "hibiapi/app/routes/bilibili/__init__.py",
    "chars": 399,
    "preview": "from hibiapi.api.bilibili import BilibiliConstants\nfrom hibiapi.app.routes.bilibili.v2 import router as RouterV2\nfrom hi"
  },
  {
    "path": "hibiapi/app/routes/bilibili/v2.py",
    "chars": 258,
    "preview": "from hibiapi.api.bilibili.api import BilibiliEndpointV2\nfrom hibiapi.api.bilibili.net import NetRequest\nfrom hibiapi.uti"
  },
  {
    "path": "hibiapi/app/routes/bilibili/v3.py",
    "chars": 218,
    "preview": "from hibiapi.api.bilibili import BilibiliEndpointV3, NetRequest\nfrom hibiapi.utils.routing import EndpointRouter\n\nrouter"
  },
  {
    "path": "hibiapi/app/routes/netease.py",
    "chars": 285,
    "preview": "from hibiapi.api.netease import NeteaseConstants, NeteaseEndpoint, NetRequest\nfrom hibiapi.utils.routing import Endpoint"
  },
  {
    "path": "hibiapi/app/routes/pixiv.py",
    "chars": 843,
    "preview": "from typing import Optional\n\nfrom fastapi import Depends, Header\n\nfrom hibiapi.api.pixiv import NetRequest, PixivConstan"
  },
  {
    "path": "hibiapi/app/routes/qrcode.py",
    "chars": 2036,
    "preview": "from typing import Optional\n\nfrom fastapi import Request, Response\nfrom pydantic.color import Color\n\nfrom hibiapi.api.qr"
  },
  {
    "path": "hibiapi/app/routes/sauce.py",
    "chars": 3092,
    "preview": "from typing import Annotated, Optional\n\nfrom fastapi import Depends, File, Form\nfrom loguru import logger\n\nfrom hibiapi."
  },
  {
    "path": "hibiapi/app/routes/tieba.py",
    "chars": 248,
    "preview": "from hibiapi.api.tieba import Config, NetRequest, TiebaEndpoint\nfrom hibiapi.utils.routing import EndpointRouter\n\n__moun"
  },
  {
    "path": "hibiapi/app/routes/wallpaper.py",
    "chars": 268,
    "preview": "from hibiapi.api.wallpaper import Config, NetRequest, WallpaperEndpoint\nfrom hibiapi.utils.routing import EndpointRouter"
  },
  {
    "path": "hibiapi/configs/bika.yml",
    "chars": 75,
    "preview": "enabled: true\n\nproxy: {}\n\naccount:\n  # 请在此处填写你的哔咔账号密码\n  email:\n  password:\n"
  },
  {
    "path": "hibiapi/configs/bilibili.yml",
    "chars": 218,
    "preview": "enabled: true\n\nnet:\n  cookie: > # Bilibili的Cookie, 在一些需要用户登录的场景下需要\n    DedeUserID=; \n    DedeUserID__ckMd5=; \n    SESSDA"
  },
  {
    "path": "hibiapi/configs/general.yml",
    "chars": 1796,
    "preview": "#   _    _ _ _     _          _____ _____\n#  | |  | (_) |   (_)   /\\   |  __ \\_   _|\n#  | |__| |_| |__  _   /  \\  | |__)"
  },
  {
    "path": "hibiapi/configs/netease.yml",
    "chars": 340,
    "preview": "enabled: true\n\nnet:\n  cookie: > # 网易云的Cookie, 可能有些API需要\n    os=pc;\n    osver=Microsoft-Windows-10-Professional-build-105"
  },
  {
    "path": "hibiapi/configs/pixiv.yml",
    "chars": 268,
    "preview": "enabled: true\n\n# HTTP代理地址\n# 示例格式\n# proxy: { \"all://\": \"http://127.0.0.1:1081\" }\nproxy: {}\n\naccount:\n  # Pixiv 登录凭证刷新令牌 ("
  },
  {
    "path": "hibiapi/configs/qrcode.yml",
    "chars": 181,
    "preview": "enabled: true\n\nqrcode:\n  max-size: 1000 # 允许的二维码最大尺寸, 单位像素\n  min-size: 50 # 允许的二维码最小尺寸, 单位像素\n  icon-site: # 图标支持的站点, 可以阻"
  },
  {
    "path": "hibiapi/configs/sauce.yml",
    "chars": 602,
    "preview": "enabled: true\n\n# HTTP代理地址\n# 示例格式\n# proxy:\n#   http_proxy: http://127.0.0.1:1081\n#   https_proxy: https://127.0.0.1:1081\n"
  },
  {
    "path": "hibiapi/configs/tieba.yml",
    "chars": 165,
    "preview": "enabled: true\n\nnet:\n  user-agent: \"Mozilla/5.0 (mixmoe@GitHub.com/HibiAPI) Chrome/114.514.1919810\" # UA头, 一般没必要改\n  param"
  },
  {
    "path": "hibiapi/configs/wallpaper.yml",
    "chars": 113,
    "preview": "enabled: true\n\nnet:\n  user-agent: \"Mozilla/5.0 (mixmoe@GitHub.com/HibiAPI) Chrome/114.514.1919810\" # UA头, 一般没必要改\n"
  },
  {
    "path": "hibiapi/utils/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "hibiapi/utils/cache.py",
    "chars": 3747,
    "preview": "import hashlib\nfrom collections.abc import Awaitable\nfrom datetime import timedelta\nfrom functools import wraps\nfrom typ"
  },
  {
    "path": "hibiapi/utils/config.py",
    "chars": 3285,
    "preview": "import json\nimport os\nfrom pathlib import Path\nfrom typing import Any, Optional, TypeVar, overload\n\nimport confuse\nimpor"
  },
  {
    "path": "hibiapi/utils/decorators/__init__.py",
    "chars": 3648,
    "preview": "from __future__ import annotations\n\nimport asyncio\nfrom asyncio import sleep as async_sleep\nfrom collections.abc import "
  },
  {
    "path": "hibiapi/utils/decorators/enum.py",
    "chars": 1461,
    "preview": "import ast\nimport inspect\nfrom enum import Enum\nfrom typing import TypeVar\n\n_ET = TypeVar(\"_ET\", bound=type[Enum])\n\n\ndef"
  },
  {
    "path": "hibiapi/utils/decorators/timer.py",
    "chars": 2931,
    "preview": "from __future__ import annotations\n\nimport time\nfrom dataclasses import dataclass, field\nfrom functools import wraps\nfro"
  },
  {
    "path": "hibiapi/utils/exceptions.py",
    "chars": 1773,
    "preview": "from datetime import datetime\nfrom typing import Any, Optional\n\nfrom pydantic import AnyHttpUrl, BaseModel, Extra, Field"
  },
  {
    "path": "hibiapi/utils/log.py",
    "chars": 1835,
    "preview": "import logging\nimport re\nimport sys\nfrom datetime import timedelta\nfrom pathlib import Path\n\nimport sentry_sdk.integrati"
  },
  {
    "path": "hibiapi/utils/net.py",
    "chars": 3784,
    "preview": "import functools\nfrom collections.abc import Coroutine\nfrom types import TracebackType\nfrom typing import (\n    Any,\n   "
  },
  {
    "path": "hibiapi/utils/routing.py",
    "chars": 6012,
    "preview": "import inspect\nfrom collections.abc import Mapping\nfrom contextvars import ContextVar\nfrom enum import Enum\nfrom fnmatch"
  },
  {
    "path": "hibiapi/utils/temp.py",
    "chars": 782,
    "preview": "from pathlib import Path\nfrom tempfile import mkdtemp, mkstemp\nfrom threading import Lock\nfrom urllib.parse import Parse"
  },
  {
    "path": "pyproject.toml",
    "chars": 1750,
    "preview": "[project]\nname = \"HibiAPI\"\nversion = \"0.8.0\"\ndescription = \"A program that implements easy-to-use APIs for a variety of "
  },
  {
    "path": "scripts/pixiv_login.py",
    "chars": 6380,
    "preview": "import hashlib\nimport sys\nfrom base64 import urlsafe_b64encode\nfrom secrets import token_urlsafe\nfrom typing import Any,"
  },
  {
    "path": "test/__init__.py",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/test_base.py",
    "chars": 3713,
    "preview": "from typing import Annotated, Any\n\nimport pytest\nfrom fastapi import Depends\nfrom fastapi.testclient import TestClient\nf"
  },
  {
    "path": "test/test_bika.py",
    "chars": 3301,
    "preview": "from math import inf\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\n\n@pytest.fixture(scope=\"package\")\ndef cli"
  },
  {
    "path": "test/test_bilibili_v2.py",
    "chars": 2590,
    "preview": "from math import inf\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\n\n@pytest.fixture(scope=\"package\")\ndef cli"
  },
  {
    "path": "test/test_bilibili_v3.py",
    "chars": 2595,
    "preview": "from math import inf\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\n\n@pytest.fixture(scope=\"package\")\ndef cli"
  },
  {
    "path": "test/test_netease.py",
    "chars": 4101,
    "preview": "from math import inf\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\n\n@pytest.fixture(scope=\"package\")\ndef cli"
  },
  {
    "path": "test/test_pixiv.py",
    "chars": 9116,
    "preview": "from datetime import date, timedelta\nfrom math import inf\n\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom "
  },
  {
    "path": "test/test_qrcode.py",
    "chars": 1885,
    "preview": "from math import inf\nfrom secrets import token_urlsafe\n\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom htt"
  },
  {
    "path": "test/test_sauce.py",
    "chars": 1202,
    "preview": "from math import inf\nfrom pathlib import Path\n\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom pytest_https"
  },
  {
    "path": "test/test_tieba.py",
    "chars": 1541,
    "preview": "from math import inf\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\n\n@pytest.fixture(scope=\"package\")\ndef cli"
  },
  {
    "path": "test/test_wallpaper.py",
    "chars": 2273,
    "preview": "from math import inf\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\n\n@pytest.fixture(scope=\"package\")\ndef cli"
  }
]

About this extraction

This page contains the full source code of the mixmoe/HibiAPI GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 100 files (201.0 KB), approximately 54.2k tokens, and a symbol index with 477 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!