diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..190f51b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.github/workflows/build-image.yml +.git +.gitignore +.dockerignore +README.md +LICENSE diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..781dd11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,91 @@ +name: 🐛 Bug report +description: 创建Bug报告以帮助项目改进。 +title: 🐛[BUG] 请输入标题 +labels: bug +body: + - type: markdown + attributes: + value: | + 📝 **请在上方的`title`中填写一个简洁明了的标题**,格式建议为:🐛[Bug] 简短描述。 + 例如:🐛[Bug] B站某些直播间无法录制。 + - type: checkboxes + attributes: + label: ⚠️ 确认是否已存在类似问题 + description: > + 🔍 [点击这里搜索历史issue](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) + 请确保你的问题没有被报告过。 + options: + - label: 我已经搜索过issues,没有找到类似问题 + required: true + - type: dropdown + attributes: + label: 🔧 运行方式 + description: 请选择你是如何运行程序的。 + options: + - 直接运行的exe文件 + - 使用源代码运行 + - 使用docker运行 + validations: + required: true + - type: dropdown + attributes: + label: 🐍 如果是使用源代码运行,请选择你的Python环境版本 + description: 请选择你运行程序的Python版本。 + options: + - Python 3.10 + - Python 3.11 + - Python 3.12 + - Python 3.13 + - Other (请在问题中说明) + validations: + required: false + - type: dropdown + attributes: + label: 💻 请选择你的系统环境 + description: 请选择你运行程序的具体系统版本。 + options: + - Windows 10 + - Windows 11 + - macOS + - Ubuntu + - CentOS + - Fedora + - Debian + - Other (请在问题中说明) + validations: + required: true + - type: checkboxes + attributes: + label: ⚠️ 确认是否已经重试多次 + description: > + 有时可能是你的设备或者网络问题导致的。 + options: + - label: 我已经尝试过多次,仍然出现问题 + required: true + - type: textarea + attributes: + label: 🕹 复现步骤 + description: | + **⚠️ 不能复现将会关闭issue.** + 请按照以下格式填写: + 1. 录制的直播间地址是... + 2. 使用的录制格式是... + 3. ... + placeholder: | + 1. ... + 2. ... + 3. ... + validations: + required: true + - type: textarea + attributes: + label: 😯 问题描述 + description: 详细描述出现的问题,或提供有关截图。 + validations: + required: true + - type: textarea + attributes: + label: 📜 错误信息 + description: 如果有,请贴出相关的日志错误信息或者截图。 + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_en.yml b/.github/ISSUE_TEMPLATE/bug_en.yml new file mode 100644 index 0000000..36887b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_en.yml @@ -0,0 +1,91 @@ +name: 🐛 (English)Bug report +description: Create a bug report to help improve the project. +title: 🐛[BUG] Please enter a title +labels: bug +body: + - type: markdown + attributes: + value: | + 📝 **Please fill in a concise and clear title in the `title` above**, the format is suggested as: 🐛[Bug] Short description. + For example: 🐛[Bug] Unable to record certain TikTok live rooms. + - type: checkboxes + attributes: + label: ⚠️ Confirm if similar issues exist + description: > + 🔍 [Click here to search historical issues](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) + Please make sure your issue hasn't been reported before. + options: + - label: I have searched the issues and found no similar problems + required: true + - type: dropdown + attributes: + label: 🔧 How did you run the program? + description: Please select how you ran the program. + options: + - Directly running the exe file + - Running with source code + - Running with docker + validations: + required: true + - type: dropdown + attributes: + label: 🐍 If running with source code, please select your Python environment version + description: Please select the Python version you used to run the program. + options: + - Python 3.10 + - Python 3.11 + - Python 3.12 + - Python 3.13 + - Other (please specify in the issue) + validations: + required: false + - type: dropdown + attributes: + label: 💻 Please select your system environment + description: Please select the specific system version you are running the program on. + options: + - Windows 10 + - Windows 11 + - macOS + - Ubuntu + - CentOS + - Fedora + - Debian + - Other (please specify in the issue) + validations: + required: true + - type: checkboxes + attributes: + label: ⚠️ Confirm if you have retried multiple times + description: > + Sometimes it might be due to your device or network issues. + options: + - label: I have tried multiple times and still encounter the problem + required: true + - type: textarea + attributes: + label: 🕹 Reproduction steps + description: | + **⚠️ Issues that cannot be reproduced will be closed.** + Please fill in according to the following format: + 1. The live room address I tried to record is... + 2. The recording format I used is... + 3. ... + placeholder: | + 1. ... + 2. ... + 3. ... + validations: + required: true + - type: textarea + attributes: + label: 😯 Problem description + description: Describe the problem in detail or provide relevant screenshots. + validations: + required: true + - type: textarea + attributes: + label: 📜 Error information + description: If available, please paste the relevant log error information or screenshots. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..07b7612 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,38 @@ +name: 🚀 Feature request +description: 提出你对项目的新想法或建议。 +title: 🚀[Feature] 请输入标题 +labels: enhancement +body: + - type: markdown + attributes: + value: | + 📝 **请在上方的`title`中填写一个简洁明了的标题**,格式建议为:🚀[Feature] 简短描述。 + 例如:🚀[Feature] 添加xx直播录制。 + - type: checkboxes + attributes: + label: ⚠️ 搜索是否存在类似issue + description: > + 🔍 [点击这里搜索历史issue](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) 使用关键词搜索,确保没有重复的issue。 + options: + - label: 我已经搜索过issues,没有发现相似issue + required: true + - type: textarea + attributes: + label: 📜 功能描述 + description: 请详细描述你希望添加的功能,包括它的工作方式和预期效果。 + placeholder: | + 功能描述: + - type: textarea + attributes: + label: 🌐 举例(可选) + description: 如果可能,请提供功能相关的示例、截图或相关网址。 + placeholder: | + 直播间示例地址: + `https://www.example.com/live/xxxx` + + - type: textarea + attributes: + label: 💡 动机 + description: 描述你提出该feature的动机,以及没有这项feature对你的使用造成了怎样的影响。 + placeholder: | + 我需要这个功能是因为... \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_en.yml b/.github/ISSUE_TEMPLATE/feature_en.yml new file mode 100644 index 0000000..42b1183 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_en.yml @@ -0,0 +1,37 @@ +name: 🚀 (English)Feature request +description: Propose new ideas or suggestions for the project. +title: 🚀[Feature] Please enter a title +labels: enhancement +body: + - type: markdown + attributes: + value: | + 📝 **Please fill in a concise and clear title in the `title` above**, the format is suggested as: 🚀[Feature] Short description. + For example: 🚀[Feature] Add xx live recording. + - type: checkboxes + attributes: + label: ⚠️ Search for similar issues + description: > + 🔍 [Click here to search historical issues](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) using keywords to ensure there are no duplicate issues. + options: + - label: I have searched the issues and found no similar issues + required: true + - type: textarea + attributes: + label: 📜 Feature description + description: Please describe in detail the feature you would like to add, including how it should work and what its expected outcomes are. + placeholder: | + Feature description: + - type: textarea + attributes: + label: 🌐 Example (Optional) + description: If possible, provide examples, screenshots, or related URLs related to the feature. + placeholder: | + Live room example URL: + `https://www.example.com/live/xxxx` + - type: textarea + attributes: + label: 💡 Motivation + description: Describe the motivation behind your feature request and how not having this feature impacts your use of the project. + placeholder: | + I need this feature because... \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..68492d1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,63 @@ +name: ❓ Question +description: 对程序使用有疑问?在这里提出你的问题。 +title: ❓[Question] 请输入标题 +labels: question +body: + - type: markdown + attributes: + value: | + 📝 **请在上方的`title`中填写一个简洁明了的问题标题**。这将帮助其他人快速理解你的问题。 + 例如:❓[Question] 如果设置单个直播间的录制清晰度。 + - type: checkboxes + attributes: + label: ⚠️ 搜索是否存在类似问题 + description: > + 🔍 [点击这里搜索历史issue](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) 使用关键词搜索,看看是否已经有人问过类似的问题。 + options: + - label: 我已经搜索过issues,没有找到相似的问题 + required: true + - type: dropdown + attributes: + label: 🔧 运行方式 + description: 请选择你是如何运行程序的。 + options: + - 直接运行的exe文件 + - 使用源代码运行 + - 使用docker运行 + validations: + required: true + - type: dropdown + attributes: + label: 🐍 如果是使用源代码运行,请选择你的Python环境版本 + description: 请选择你运行程序的Python版本。 + options: + - Python 3.10 + - Python 3.11 + - Python 3.12 + - Python 3.13 + - Other (请在问题中说明) + validations: + required: false + - type: dropdown + attributes: + label: 💻 请选择你的系统环境 + description: 请选择你运行程序的具体系统版本。 + options: + - Windows 10 + - Windows 11 + - macOS + - Ubuntu + - CentOS + - Fedora + - Debian + - Other (请在问题中说明) + validations: + required: true + - type: textarea + attributes: + label: 🤔 问题详情 + description: 请提供与你的问题相关的所有详细信息。 + placeholder: | + 你的问题具体是关于什么? + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question_en.yml b/.github/ISSUE_TEMPLATE/question_en.yml new file mode 100644 index 0000000..d63d7b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question_en.yml @@ -0,0 +1,63 @@ +name: ❓ (English)Question +description: Have questions about using the program? Ask them here. +title: ❓[Question] Please enter a title +labels: question +body: + - type: markdown + attributes: + value: | + 📝 **Please fill in a concise and clear question title in the `title` above**. This will help others quickly understand your question. + For example: ❓[Question] How to set the recording quality for a single live room. + - type: checkboxes + attributes: + label: ⚠️ Search for similar issues + description: > + 🔍 [Click here to search historical issues](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) see if your question has already been asked. + options: + - label: I have searched the issues and found no similar questions + required: true + - type: dropdown + attributes: + label: 🔧 How did you run the program? + description: Please select how you ran the program. + options: + - Executable file run directly + - Running with source code + - Running with docker + validations: + required: true + - type: dropdown + attributes: + label: 🐍 If running with source code, please select your Python environment version + description: Please select the Python version you used to run the program. + options: + - Python 3.10 + - Python 3.11 + - Python 3.12 + - Python 3.13 + - Other (please specify in the question) + validations: + required: false + - type: dropdown + attributes: + label: 💻 Please select your system environment + description: Please select the specific system version you are running the program on. + options: + - Windows 10 + - Windows 11 + - macOS + - Ubuntu + - CentOS + - Fedora + - Debian + - Other (please specify in the question) + validations: + required: true + - type: textarea + attributes: + label: 🤔 Question details + description: Please provide all the details relevant to your question. + placeholder: | + What is your question about? + validations: + required: true \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f2d495a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,56 @@ +### 📜 标题(Title) + +**请提供这个Pull Request中提议的更改的简洁描述:** + + +- + +### 🔍 描述(Description) + +**请描述这个PR做了什么/为什么这些更改是必要的:** + + +- + +### 📝 类型(Type of Change) + +**这个PR引入了哪种类型的更改?(请勾选所有适用的选项)** + + +- [ ] 修复Bug +- [ ] 新功能 +- [ ] 代码风格更新(格式化,局部变量) +- [ ] 重构(改进代码结构) +- [ ] 构建相关更改(依赖项,构建脚本等) +- [ ] 其他:_请描述_ + +### 🏗️ 测试(Testing) + +**请描述您已经进行的测试:** + + +- + +**如果适用,请提供测试更改的说明:** + + +- + +### 📋 检查清单(Checklist) + +在您创建这个PR之前,请确保以下所有框都被勾选,方法是在每个框中放置一个`x`: + + +- [ ] 我已经阅读了**贡献指南**文档 +- [ ] 我的更改没有产生新的警告 +- [ ] 我已经添加了覆盖我更改的测试 +- [ ] 我已经相应地更新了文档(如果适用) +- [ ] 我遵循了这个项目的代码风格 + +**注意:** 这个PR在所有复选框被勾选之前不会被合并。 + + +--- + +**感谢您的贡献!** + \ No newline at end of file diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 0000000..25600f7 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,54 @@ +name: Build and Push Docker Image + +on: + push: + tags: + - '*' + workflow_dispatch: + inputs: + tag_name: + description: 'Tag name for the Docker image' + required: false + default: 'latest' + +jobs: + build_and_push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + registry: docker.io + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: | + ihmily/douyin-live-recorder:${{ github.event.inputs.tag_name || github.ref_name }} + ihmily/douyin-live-recorder:latest + platforms: linux/amd64,linux/arm64 + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache diff --git a/.github/workflows/issue-translator.yml b/.github/workflows/issue-translator.yml new file mode 100644 index 0000000..8eeb05f --- /dev/null +++ b/.github/workflows/issue-translator.yml @@ -0,0 +1,15 @@ +name: Issue Translator +on: + issue_comment: + types: [created] + issues: + types: [opened] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: usthe/issues-translate-action@v2.7 + with: + IS_MODIFY_TITLE: false + CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. \ No newline at end of file diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 0000000..da70b89 --- /dev/null +++ b/.github/workflows/sync.yml @@ -0,0 +1,65 @@ +name: 'Upstream Sync' + +permissions: + contents: write + +on: + schedule: + - cron: "0 0 * * *" # every day + + workflow_dispatch: # click the button on Github repo! + inputs: + sync_test_mode: # Adds a boolean option that appears during manual workflow run for easy test mode config + description: 'Fork Sync Test Mode' + type: boolean + default: false + +jobs: + sync_latest_from_upstream: + runs-on: ubuntu-latest + name: Sync latest commits from upstream repo + if: ${{ github.event.repository.fork }} + + steps: + # Step 1: run a standard checkout action, provided by github + - name: Checkout target repo + uses: actions/checkout@v3 + with: + # optional: set the branch to checkout, + # sync action checks out your 'target_sync_branch' anyway + ref: ${{ secrets.MY_TARGET_SYNC_BRANCH }} + # REQUIRED if your upstream repo is private (see wiki) + persist-credentials: false + + # Step 2: run the sync action + - name: Sync upstream changes + id: sync + uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1 + with: + target_sync_branch: ${{ secrets.MY_TARGET_SYNC_BRANCH }} # need to set + # REQUIRED 'target_repo_token' exactly like this! + target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set + upstream_sync_branch: main + upstream_sync_repo: ihmily/DouyinLiveRecorder + + # Set test_mode true during manual dispatch to run tests instead of the true action!! + test_mode: ${{ inputs.sync_test_mode }} + + # Step 3: Display a sample message based on the sync output var 'has_new_commits' + - name: New commits found + if: steps.sync.outputs.has_new_commits == 'true' + run: echo "New commits were found to sync." + + - name: No new commits + if: steps.sync.outputs.has_new_commits == 'false' + run: echo "There were no new commits." + + - name: Show value of 'has_new_commits' + run: echo ${{ steps.sync.outputs.has_new_commits }} + + - name: Sync check + if: failure() + run: | + echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次" + echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork." + exit 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b7997c --- /dev/null +++ b/.gitignore @@ -0,0 +1,184 @@ +# 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 +*.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 + +# DouyinLiveRecord +backup_config/ +logs/ +node/ +node-v*.zip + +# 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 + +# 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/ + +backup_config/ + +# Google OAuth(含 client_secret,切勿提交) +config/google_oauth.json +config/youtube_oauth_token.json +config/web2_api_token.txt +config/web2_bind_host.txt +config/easytier_network.json +config/wechat_chatbot.json +config/wechat_chatbot.state.json + +electron/resources/easytier/easytier-core.exe +electron/resources/easytier/_extract_tmp/ + +# Electron 桌面壳 +electron/node_modules/ +electron/dist/ +electron/out/ diff --git a/.pm2/supervisor_state.json b/.pm2/supervisor_state.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.pm2/supervisor_state.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e3c2cb1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY . /app + +RUN apt-get update && \ + apt-get install -y curl gnupg && \ + curl -sL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get install -y nodejs + +RUN pip install --no-cache-dir -r requirements.txt + +RUN apt-get update && \ + apt-get install -y ffmpeg tzdata && \ + ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + dpkg-reconfigure -f noninteractive tzdata + +CMD ["python", "main.py"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9036379 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Hmily + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 8a4951e..ad39e0d 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,721 @@ -# d2yexeapp +![video_spider](https://socialify.git.ci/ihmily/DouyinLiveRecorder/image?font=Inter&forks=1&language=1&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Light) +## 💡简介 +[![Python Version](https://img.shields.io/badge/python-3.11.6-blue.svg)](https://www.python.org/downloads/release/python-3116/) +[![Supported Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20Linux-blue.svg)](https://github.com/ihmily/DouyinLiveRecorder) +[![Docker Pulls](https://img.shields.io/docker/pulls/ihmily/douyin-live-recorder?label=Docker%20Pulls&color=blue&logo=docker)](https://hub.docker.com/r/ihmily/douyin-live-recorder/tags) +![GitHub issues](https://img.shields.io/github/issues/ihmily/DouyinLiveRecorder.svg) +[![Latest Release](https://img.shields.io/github/v/release/ihmily/DouyinLiveRecorder)](https://github.com/ihmily/DouyinLiveRecorder/releases/latest) +[![Downloads](https://img.shields.io/github/downloads/ihmily/DouyinLiveRecorder/total)](https://github.com/ihmily/DouyinLiveRecorder/releases/latest) +--- -## Getting started +## 本分支扩展(Electron 控制台 · 抖音 → YouTube) -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +在上游 **DouyinLiveRecorder** 能力之上,本仓库增加了 **Windows Electron 客户端**、**web2 FastAPI 控制台**、**EasyTier 远程访问**、**本地 SQLite 工作台**、**微信 Chatbot 桥接** 等。 -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! +### 架构概览 -## Add your files +| 层级 | 说明 | +|------|------| +| Python / FFmpeg | 多平台录制主逻辑;`douyin_youtube_ffplay.py` 将抖音流推至 YouTube(RTMPS);`web2.py` 提供进程管理、配置读写、`/client/overview` 工作台数据、网络/代理/系统指标、YouTube OAuth 与微信代理 API。 | +| Electron 主进程 | 启动 uvicorn;内置 **EasyTier**(`easytier-core` + `easytier-cli`);**sql.js** 持久化 `client-data.sqlite`(直播事件、用户操作、运行日志、快照);**YouTube Studio 侧窗** 注入与油猴脚本 [YouTube Studio Auto Dismiss](https://greasyfork.org/en/scripts/557378-youtube-studio-auto-dismiss) 等价的自动 Dismiss 逻辑。 | +| 渲染进程 | Verge 风格 UI:录制、网络(远程绑定 + 测速)、代理、系统、微信、**工作台**(频道 / EasyTier / 微信 / 快照 / 事件表)。 | -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +### 配置与数据路径 -``` -cd existing_repo -git remote add origin http://gitlab.yidooplanet.com/gitlab-instance-0a899031/d2yexeapp.git -git branch -M main -git push -uf origin main +| 路径 | 用途 | +|------|------| +| `config/youtube.ini` | 单路串流密钥等(与上游一致)。 | +| `config/youtube_channels.json` | **多路一对一**:每条为 `douyinUrl` + `youtubeKey`(不同抖音 ↔ 不同 YouTube),见 example;**禁止**同一抖音绑多个 YouTube、**禁止**多路共用一个密钥。未使用该文件时退化为单路 `youtube.ini` + `URL_config.ini`。 | +| `config/easytier_network.json` | 组网名、密钥、peer 引导节点。 | +| `config/wechat_chatbot.json` | 微信代理 API 配置(与 openclaw-wechat 兼容)。 | +| `%AppData%`/应用用户数据目录下的 `client-data.sqlite` | Electron 工作台 SQLite。 | + +### 功能摘要 + +- **工作台**:展示 **YouTube 频道(名称 : 密钥预览)**、**EasyTier `easytier-cli peer list` 解析的节点数(含手机/RN 等)**、**微信是否已配置 / 昵称与 wcId / 代理在线状态**;直播事件表含 **relay_intent**(抖音房间 → YouTube key 后缀)、**relay_session**(转播时长秒数)等。 +- **多路 Pro**:多组并行「抖音直播间 → YouTube 串流」,每组**唯一配对**(禁止一抖音多推、禁止多抖音同密钥)。录制页开关仅存偏好;**多进程推流引擎仍在开发**。 +- **Tampermonkey**:默认用 Electron 内建注入实现 Studio 自动 Dismiss,无需强制安装油猴扩展;若需完整 Tampermonkey,可自行使用 `session.loadExtension` 加载扩展目录。 +- **SRS**:[SRS](https://github.com/ossrs/srs) 计划在 `third_party/srs` 集成,具体能力待定(占位说明见该目录 `README.txt`)。 + +### Electron 开发 + +```bash +cd electron +pnpm install +pnpm run prepare-easytier # 下载 Windows easytier 二进制(可选,见 package.json) +pnpm dev ``` -## Integrate with your tools +--- -- [ ] [Set up project integrations](http://gitlab.yidooplanet.com/gitlab-instance-0a899031/d2yexeapp/-/settings/integrations) +一款**简易**的可循环值守的直播录制工具,基于FFmpeg实现多平台直播源录制,支持自定义配置录制以及直播状态推送。 -## Collaborate with your team + -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +## 😺已支持平台 -## Test and Deploy +- [x] 抖音 +- [x] TikTok +- [x] 快手 +- [x] 虎牙 +- [x] 斗鱼 +- [x] YY +- [x] B站 +- [x] 小红书 +- [x] bigo +- [x] blued +- [x] SOOP(原AfreecaTV) +- [x] 网易cc +- [x] 千度热播 +- [x] PandaTV +- [x] 猫耳FM +- [x] Look直播 +- [x] WinkTV +- [x] TTingLive(原Flextv) +- [x] PopkonTV +- [x] TwitCasting +- [x] 百度直播 +- [x] 微博直播 +- [x] 酷狗直播 +- [x] TwitchTV +- [x] LiveMe +- [x] 花椒直播 +- [x] 流星直播 +- [x] ShowRoom +- [x] Acfun +- [x] 映客直播 +- [x] 音播直播 +- [x] 知乎直播 +- [x] CHZZK +- [x] 嗨秀直播 +- [x] vv星球直播 +- [x] 17Live +- [x] 浪Live +- [x] 畅聊直播 +- [x] 飘飘直播 +- [x] 六间房直播 +- [x] 乐嗨直播 +- [x] 花猫直播 +- [x] Shopee +- [x] Youtube +- [x] 淘宝 +- [x] 京东 +- [x] Faceit +- [x] 咪咕 +- [x] 连接直播 +- [x] 来秀直播 +- [x] Picarto +- [ ] 更多平台正在更新中 -Use the built-in continuous integration in GitLab. + -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +## 🎈项目结构 -*** +``` +. +└── DouyinLiveRecorder/ + ├── /config -> (config record) + ├── /logs -> (save runing log file) + ├── /backup_config -> (backup file) + ├── /douyinliverecorder -> (package) + ├── initializer.py-> (check and install nodejs) + ├── spider.py-> (get live data) + ├── stream.py-> (get live stream address) + ├── utils.py -> (contains utility functions) + ├── logger.py -> (logger handdle) + ├── room.py -> (get room info) + ├── ab_sign.py-> (generate dy token) + ├── /javascript -> (some decrypt code) + ├── main.py -> (main file) + ├── ffmpeg_install.py -> (ffmpeg install script) + ├── demo.py -> (call package test demo) + ├── msg_push.py -> (send live status update message) + ├── ffmpeg.exe -> (record video) + ├── index.html -> (play m3u8 and flv video) + ├── requirements.txt -> (library dependencies) + ├── docker-compose.yaml -> (Container Orchestration File) + ├── Dockerfile -> (Application Build Recipe) + ├── StopRecording.vbs -> (stop recording script on Windows) + ... +``` -# Editing this README + -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. +## 🌱使用说明 -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +- 对于只想使用录制软件的小白用户,进入[Releases](https://github.com/ihmily/DouyinLiveRecorder/releases) 中下载最新发布的 zip压缩包即可,里面有打包好的录制软件。(有些电脑可能会报毒,直接忽略即可,如果下载时被浏览器屏蔽,请更换浏览器下载) -## Name -Choose a self-explaining name for your project. +- 压缩包解压后,在 `config` 文件夹内的 `URL_config.ini` 中添加录制直播间地址,一行一个直播间地址。如果要自定义配置录制,可以修改`config.ini` 文件,推荐将录制格式修改为`ts`。 +- 以上步骤都做好后,就可以运行`DouyinLiveRecorder.exe` 程序进行录制了。录制的视频文件保存在同目录下的 `downloads` 文件夹内。 -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +- 另外,如果需要录制TikTok、AfreecaTV等海外平台,请在配置文件中设置开启代理并添加proxy_addr链接 如:`127.0.0.1:7890` (这只是示例地址,具体根据实际填写)。 -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +- 假如`URL_config.ini`文件中添加的直播间地址,有个别直播间暂时不想录制又不想移除链接,可以在对应直播间的链接开头加上`#`,那么将停止该直播间的监测以及录制。 -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +- 软件默认录制清晰度为 `原画` ,如果要单独设置某个直播间的录制画质,可以在添加直播间地址时前面加上画质即可,如`超清,https://live.douyin.com/745964462470` 记得中间要有`,` 分隔。 -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +- 如果要长时间挂着软件循环监测直播,最好循环时间设置长一点(咱也不差没录制到的那几分钟),避免因请求频繁导致被官方封禁IP 。 -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +- 要停止直播录制,Windows平台可执行StopRecording.vbs脚本文件,或者在录制界面使用 `Ctrl+C ` 组合键中断录制,若要停止其中某个直播间的录制,可在`URL_config.ini`文件中的地址前加#,会自动停止对应直播间的录制并正常保存已录制的视频。 +- 最后,欢迎右上角给本项目一个star,同时也非常乐意大家提交pr。 -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +  -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +直播间链接示例: -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +``` +抖音: +https://live.douyin.com/745964462470 +https://v.douyin.com/iQFeBnt/ +https://live.douyin.com/yall1102 (链接+抖音号) +https://v.douyin.com/CeiU5cbX (主播主页地址) -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +TikTok: +https://www.tiktok.com/@pearlgaga88/live -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +快手: +https://live.kuaishou.com/u/yall1102 -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +虎牙: +https://www.huya.com/52333 -## License -For open source projects, say how it is licensed. +斗鱼: +https://www.douyu.com/3637778?dyshid= +https://www.douyu.com/topic/wzDBLS6?rid=4921614&dyshid= -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +YY: +https://www.yy.com/22490906/22490906 + +B站: +https://live.bilibili.com/320 + +小红书(直播间分享地址): +http://xhslink.com/xpJpfM + +bigo直播: +https://www.bigo.tv/cn/716418802 + +buled直播: +https://app.blued.cn/live?id=Mp6G2R + +SOOP: +https://play.sooplive.co.kr/sw7love + +网易cc: +https://cc.163.com/583946984 + +千度热播: +https://qiandurebo.com/web/video.php?roomnumber=33333 + +PandaTV: +https://www.pandalive.co.kr/live/play/bara0109 + +猫耳FM: +https://fm.missevan.com/live/868895007 + +Look直播: +https://look.163.com/live?id=65108820&position=3 + +WinkTV: +https://www.winktv.co.kr/live/play/anjer1004 + +FlexTV(TTinglive):: +https://www.flextv.co.kr/channels/593127/live + +PopkonTV: +https://www.popkontv.com/live/view?castId=wjfal007&partnerCode=P-00117 +https://www.popkontv.com/channel/notices?mcid=wjfal007&mcPartnerCode=P-00117 + +TwitCasting: +https://twitcasting.tv/c:uonq + +百度直播: +https://live.baidu.com/m/media/pclive/pchome/live.html?room_id=9175031377&tab_category + +微博直播: +https://weibo.com/l/wblive/p/show/1022:2321325026370190442592 + +酷狗直播: +https://fanxing2.kugou.com/50428671?refer=2177&sourceFrom= + +TwitchTV: +https://www.twitch.tv/gamerbee + +LiveMe: +https://www.liveme.com/zh/v/17141543493018047815/index.html + +花椒直播: +https://www.huajiao.com/l/345096174 + +流星直播: +https://www.7u66.com/100960 + +ShowRoom: +https://www.showroom-live.com/room/profile?room_id=480206 (主播主页地址) + +Acfun: +https://live.acfun.cn/live/179922 + +映客直播: +https://www.inke.cn/liveroom/index.html?uid=22954469&id=1720860391070904 + +音播直播: +https://live.ybw1666.com/800002949 + +知乎直播: +https://www.zhihu.com/people/ac3a467005c5d20381a82230101308e9 (主播主页地址) + +CHZZK: +https://chzzk.naver.com/live/458f6ec20b034f49e0fc6d03921646d2 + +嗨秀直播: +https://www.haixiutv.com/6095106 + +VV星球直播: +https://h5webcdn-pro.vvxqiu.com//activity/videoShare/videoShare.html?h5Server=https://h5p.vvxqiu.com&roomId=LP115924473&platformId=vvstar + +17Live: +https://17.live/en/live/6302408 + +浪Live: +https://www.lang.live/en-US/room/3349463 + +畅聊直播: +https://live.tlclw.com/106188 + +飘飘直播: +https://m.pp.weimipopo.com/live/preview.html?uid=91648673&anchorUid=91625862&app=plpl + +六间房直播: +https://v.6.cn/634435 + +乐嗨直播: +https://www.lehaitv.com/8059096 + +花猫直播: +https://h.catshow168.com/live/preview.html?uid=19066357&anchorUid=18895331 + +Shopee: +https://sg.shp.ee/GmpXeuf?uid=1006401066&session=802458 + +Youtube: +https://www.youtube.com/watch?v=cS6zS5hi1w0 + +淘宝(需cookie): +https://tbzb.taobao.com/live?liveId=532359023188 +https://m.tb.cn/h.TWp0HTd + +京东: +https://3.cn/28MLBy-E + +Faceit: +https://www.faceit.com/zh/players/Compl1/stream + +连接直播: +https://show.lailianjie.com/10000258 + +咪咕直播: +https://www.miguvideo.com/p/live/120000541321 + +来秀直播: +https://www.imkktv.com/h5/share/video.html?uid=1845195&roomId=1710496 + +Picarto: +https://www.picarto.tv/cuteavalanche +``` + +  + +## 🎃源码运行 +使用源码运行,可参考下面的步骤。 + +1.首先拉取或手动下载本仓库项目代码 + +```bash +git clone https://github.com/ihmily/DouyinLiveRecorder.git +``` + +2.进入项目文件夹,安装依赖 + +```bash +cd DouyinLiveRecorder +``` + +> [!TIP] +> - 不论你是否已安装 **Python>=3.10** 环境, 都推荐使用 [**uv**](https://github.com/astral-sh/uv) 运行, 因为它可以自动管理虚拟环境和方便地管理 **Python** 版本, **不过这完全是可选的**
+> 使用以下命令安装 +> ```bash +> # 在 macOS 和 Linux 上安装 uv +> curl -LsSf https://astral.sh/uv/install.sh | sh +> ``` +> ```powershell +> # 在 Windows 上安装 uv +> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +> ``` +> - 如果安装依赖速度太慢, 你可以考虑使用国内 pip 镜像源:
+> 在 `pip` 命令使用 `-i` 参数指定, 如 `pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple`
+> 或者在 `uv` 命令 `--index` 选项指定, 如 `uv sync --index https://pypi.tuna.tsinghua.edu.cn/simple` + +
+ + 如果已安装 Python>=3.10 环境 + + - :white_check_mark: 在虚拟环境中安装 (推荐) + + 1. 创建虚拟环境 + + - 使用系统已安装的 Python, 不使用 uv + + ```bash + python -m venv .venv + ``` + + - 使用 uv, 默认使用系统 Python, 你可以添加 `--python` 选项指定 Python 版本而不使用系统 Python [uv官方文档](https://docs.astral.sh/uv/concepts/python-versions/) + + ```bash + uv venv + ``` + + 2. 在终端激活虚拟环境 (在未安装 uv 或你想要手动激活虚拟环境时执行, 若已安装 uv, 可以跳过这一步, uv 会自动激活并使用虚拟环境) + + **Bash** 中 + ```bash + source .venv/Scripts/activate + ``` + + **Powershell** 中 + ```powershell + .venv\Scripts\activate.ps1 + ``` + + **Windows CMD** 中 + ```bat + .venv\Scripts\activate.bat + ``` + + 3. 安装依赖 + + ```bash + # 使用 pip (若安装太慢或失败, 可使用 `-i` 指定镜像源) + pip3 install -U pip && pip3 install -r requirements.txt + # 或者使用 uv (可使用 `--index` 指定镜像源) + uv sync + # 或者 + uv pip sync requirements.txt + ``` + + - :x: 在系统 Python 环境中安装 (不推荐) + + ```bash + pip3 install -U pip && pip3 install -r requirements.txt + ``` + +
+ +
+ + 如果未安装 Python>=3.10 环境 + + 你可以使用 [**uv**](https://github.com/astral-sh/uv) 安装依赖 + + ```bash + # uv 将使用 3.10 及以上的最新 python 发行版自动创建并使用虚拟环境, 可使用 --python 选项指定 python 版本, 参见 https://docs.astral.sh/uv/reference/cli/#uv-sync--python 和 https://docs.astral.sh/uv/reference/cli/#uv-pip-sync--python + uv sync + # 或 + uv pip sync requirements.txt + ``` + +
+ +3.安装[FFmpeg](https://ffmpeg.org/download.html#build-linux),如果是Windows系统,这一步可跳过。对于Linux系统,执行以下命令安装 + +CentOS执行 + +```bash +yum install epel-release +yum install ffmpeg +``` + +Ubuntu则执行 + +```bash +apt update +apt install ffmpeg +``` + +macOS 执行 + +**如果已经安装 Homebrew 请跳过这一步** + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +```bash +brew install ffmpeg +``` + +4.运行程序 + +```python +python main.py + +``` +或 + +```bash +uv run main.py +``` + +其中Linux系统请使用`python3 main.py` 运行。 + +  +## 🐋容器运行 + +在运行命令之前,请确保您的机器上安装了 [Docker](https://docs.docker.com/get-docker/) 和 [Docker Compose](https://docs.docker.com/compose/install/) + +1.快速启动 + +最简单方法是运行项目中的 [docker-compose.yaml](https://github.com/ihmily/DouyinLiveRecorder/blob/main/docker-compose.yaml) 文件,只需简单执行以下命令: + +```bash +docker-compose up +``` + +可选 `-d` 在后台运行。 + + + +2.构建镜像(可选) + +如果你只想简单的运行程序,则不需要做这一步。Docker镜像仓库中代码版本可能不是最新的,如果要运行本仓库主分支最新代码,可以本地自定义构建,通过修改 [docker-compose.yaml](https://github.com/ihmily/DouyinLiveRecorder/blob/main/docker-compose.yaml) 文件,如将镜像名修改为 `douyin-live-recorder:latest`,并取消 `# build: .` 注释,然后再执行 + +```bash +docker build -t douyin-live-recorder:latest . +docker-compose up +``` + +或者直接使用下面命令进行构建并启动 + +```bash +docker-compose -f docker-compose.yaml up +``` + + + +3.停止容器实例 + +```bash +docker-compose stop +``` + + + +4.注意事项 + +①在docker容器内运行本程序之前,请先在配置文件中添加要录制的直播间地址。 + +②在容器内时,如果手动中断容器运行停止录制,会导致正在录制的视频文件损坏! + +**无论哪种运行方式,为避免手动中断或者异常中断导致录制的视频文件损坏的情况,推荐使用 `ts` 格式保存**。 + +  + +## 🤖相关项目 + +- StreamCap: https://github.com/ihmily/StreamCap +- streamget: https://github.com/ihmily/streamget + +  + +## ❤️贡献者 + +   [![Hmily](https://github.com/ihmily.png?size=50)](https://github.com/ihmily) +[![iridescentGray](https://github.com/iridescentGray.png?size=50)](https://github.com/iridescentGray) +[![annidy](https://github.com/annidy.png?size=50)](https://github.com/annidy) +[![wwkk2580](https://github.com/wwkk2580.png?size=50)](https://github.com/wwkk2580) +[![missuo](https://github.com/missuo.png?size=50)](https://github.com/missuo) +xueli12 +kaine1973 +yinruiqing +Max-Tortoise +[![justdoiting](https://github.com/justdoiting.png?size=50)](https://github.com/justdoiting) +[![dhbxs](https://github.com/dhbxs.png?size=50)](https://github.com/dhbxs) +[![wujiyu115](https://github.com/wujiyu115.png?size=50)](https://github.com/wujiyu115) +[![zhanghao333](https://github.com/zhanghao333.png?size=50)](https://github.com/zhanghao333) +gyc0123 + +   [![HoratioShaw](https://github.com/HoratioShaw.png?size=50)](https://github.com/HoratioShaw) +[![nov30th](https://github.com/nov30th.png?size=50)](https://github.com/nov30th) +[![727155455](https://github.com/727155455.png?size=50)](https://github.com/727155455) +[![nixingshiguang](https://github.com/nixingshiguang.png?size=50)](https://github.com/nixingshiguang) +[![1411430556](https://github.com/1411430556.png?size=50)](https://github.com/1411430556) +[![Ovear](https://github.com/Ovear.png?size=50)](https://github.com/Ovear) +  + +## ⏳提交日志 + +- 20251024 + - 修复抖音风控无法获取数据问题 + + - 新增soop.com录制支持 + + - 修复bigo录制 + +- 20250127 + - 新增淘宝、京东、faceit直播录制 + - 修复小红书直播流录制以及转码问题 + - 修复畅聊、VV星球、flexTV直播录制 + - 修复批量微信直播推送 + - 新增email发送ssl和port配置 + - 新增强制转h264配置 + - 更新ffmpeg版本 + - 重构包为异步函数! + +- 20241130 + - 新增shopee、youtube直播录制 + - 新增支持自定义m3u8、flv地址录制 + - 新增自定义执行脚本,支持python、bat、bash等 + - 修复YY直播、花椒直播和小红书直播录制 + - 修复b站标题获取错误 + - 修复log日志错误 +- 20241030 + - 新增嗨秀直播、vv星球直播、17Live、浪Live、SOOP、畅聊直播(原时光直播)、飘飘直播、六间房直播、乐嗨直播、花猫直播等10个平台直播录制 + - 修复小红书直播录制,支持小红书作者主页地址录制直播 + - 新增支持ntfy消息推送,以及新增支持批量推送多个地址(逗号分隔多个推送地址) + - 修复Liveme直播录制、twitch直播录制 + - 新增Windows平台一键停止录制VB脚本程序 +- 20241005 + - 新增邮箱和Bark推送 + - 新增直播注释停止录制 + - 优化分段录制 + - 重构部分代码 +- 20240928 + - 新增知乎直播、CHZZK直播录制 + - 修复音播直播录制 +- 20240903 + - 新增抖音双屏录制、音播直播录制 + - 修复PandaTV、bigo直播录制 +- 20240713 + - 新增映客直播录制 +- 20240705 + - 新增时光直播录制 +- 20240701 + - 修复虎牙直播录制2分钟断流问题 + - 新增自定义直播推送内容 +- 20240621 + - 新增Acfun、ShowRoom直播录制 + - 修复微博录制、新增直播源线路 + - 修复斗鱼直播60帧录制 + - 修复酷狗直播录制 + - 修复TikTok部分无法解析直播源 + - 修复抖音无法录制连麦直播 +- 20240510 + - 修复部分虎牙直播间录制错误 +- 20240508 + - 修复花椒直播录制 + - 更改文件路径解析方式 [@kaine1973](https://github.com/kaine1973) +- 20240506 + - 修复抖音录制画质解析bug + - 修复虎牙录制 60帧最高画质问题 + - 新增流星直播录制 +- 20240427 + - 新增LiveMe、花椒直播录制 +- 20240425 + - 新增TwitchTV直播录制 +- 20240424 + - 新增酷狗直播录制、优化PopkonTV直播录制 +- 20240423 + - 新增百度直播录制、微博直播录制 + - 修复斗鱼录制直播回放的问题 + - 新增直播源地址显示以及输出到日志文件设置 +- 20240311 + - 修复海外平台录制bug,增加画质选择,增强录制稳定性 + - 修复虎牙录制bug (虎牙`一起看`频道 有特殊限制,有时无法录制) +- 20240309 + - 修复虎牙直播、小红书直播和B站直播录制 + - 新增5个直播平台录制,包括winktv、flextv、look、popkontv、twitcasting + - 新增部分海外平台账号密码配置,实现自动登录并更新配置文件中的cookie + - 新增自定义配置需要使用代理录制的平台 + - 新增只推送开播消息不进行录制设置 + - 修复了一些bug +- 20240209 + - 优化AfreecaTV录制,新增账号密码登录获取cookie以及持久保存 + - 修复了小红书直播因官方更新直播域名,导致无法录制直播的问题 + - 修复了更新URL配置文件的bug + - 最后,祝大家新年快乐! + +
点击展开更多提交日志 + +- 20240129 + - 新增猫耳FM直播录制 +- 20240127 + - 新增千度热播直播录制、新增pandaTV(韩国)直播录制 + - 新增telegram直播状态消息推送,修复了某些bug + - 新增自定义设置不同直播间的录制画质(即每个直播间录制画质可不同) + - 修改录制视频保存路径为 `downloads` 文件夹,并且分平台进行保存。 +- 20240114 + - 新增网易cc直播录制,优化ffmpeg参数,修改AfreecaTV输入直播地址格式 + - 修改日志记录器 @[iridescentGray](https://github.com/iridescentGray) +- 20240102 + - 修复Linux上运行,新增docker配置文件 +- 20231210 + - 修复录制分段bug,修复bigo录制检测bug + - 新增自定义修改录制主播名 + - 新增AfreecaTV直播录制,修复某些可能会发生的bug +- 20231207 + - 新增blued直播录制,修复YY直播录制,新增直播结束消息推送 +- 20231206 + - 新增bigo直播录制 +- 20231203 + - 新增小红书直播录制(全网首发),目前小红书官方没有切换清晰度功能,因此直播录制也只有默认画质 + - 小红书录制暂时无法循环监测,每次主播开启直播,都要重新获取一次链接 + - 获取链接的方式为 将直播间转发到微信,在微信中打开后,复制页面的链接。 +- 20231030 + - 本次更新只是进行修复,没时间新增功能。 + - 欢迎各位大佬提pr 帮忙更新维护 +- 20230930 + - 新增抖音从接口获取直播流,增强稳定性 + - 修改快手获取直播流的方式,改用从官方接口获取 + - 祝大家中秋节快乐! +- 20230919 + - 修复了快手版本更新后录制出错的问题,增加了其自动获取cookie(~~稳定性未知~~) + - 修复了TikTok显示正在直播但不进行录制的问题 +- 20230907 + - 修复了因抖音官方更新了版本导致的录制出错以及短链接转换出错 + - 修复B站无法录制原画视频的bug + - 修改了配置文件字段,新增各平台自定义设置Cookie +- 20230903 + - 修复了TikTok录制时报644无法录制的问题 + - 新增直播状态推送到钉钉和微信的功能,如有需要请看 [设置推送教程](https://d04vqdiqwr3.feishu.cn/docx/XFPwdDDvfobbzlxhmMYcvouynDh?from=from_copylink) + - 最近比较忙,其他问题有时间再更新 +- 20230816 + - 修复斗鱼直播(官方更新了字段)和快手直播录制出错的问题 +- 20230814 + - 新增B站直播录制 + - 写了一个在线播放M3U8和FLV视频的网页源码,打开即可食用 +- 20230812 + - 新增YY直播录制 +- 20230808 + - 修复主播重新开播无法再次录制的问题 +- 20230807 + - 新增了斗鱼直播录制 + - 修复显示录制完成之后会重新开始录制的问题 +- 20230805 + - 新增了虎牙直播录制,其暂时只能用flv视频流进行录制 + - Web API 新增了快手和虎牙这两个平台的直播流解析(TikTok要代理) +- 20230804 + - 新增了快手直播录制,优化了部分代码 + - 上传了一个自动化获取抖音直播间页面Cookie的代码,可以用于录制 +- 20230803 + - 通宵更新 + - 新增了国际版抖音TikTok的直播录制,去除冗余 简化了部分代码 +- 20230724 + - 新增了一个通过抖音直播间地址获取直播视频流链接的API接口,上传即可用 +
+   + +## 有问题可以提issue, 我会在这里持续添加更多直播平台的录制 欢迎Star +#### diff --git a/StopRecording.vbs b/StopRecording.vbs new file mode 100644 index 0000000..f6c47c1 --- /dev/null +++ b/StopRecording.vbs @@ -0,0 +1,69 @@ +'********************************************************************************************/ +'* File Name : StopRecording.vbs +'* Created Date : 2024-10-15 01:50:30 +'* Author : Hmily +'* GitHub : http://github.com/ihmily +'* Description : This script is designed to terminate the process of live recording +'********************************************************************************************/ + +Dim objWMIService, colProcesses, objProcess +Dim intResponse +strComputer = "." +On Error Resume Next +intResponse = MsgBox("ȷҪкֱ̨¼ƽ", vbYesNo + vbQuestion, "ȷϽ") + +If intResponse = vbYes Then + Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2") + If Err.Number <> 0 Then + Err.Clear + End If + + Set colProcesses = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'ffmpeg.exe'") + Set colProcesses2 = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'pythonw.exe'") + Set colProcesses3 = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'DouyinLiveRecorder.exe'") + If Err.Number <> 0 Then + Err.Clear + End If + + If Not objWMIService Is Nothing And Not colProcesses Is Nothing And Not colProcesses2 Is Nothing Then + If colProcesses2.Count = 0 And colProcesses3.Count = 0 Then + MsgBox "ûҵ¼ƳĽ", vbExclamation, "ʾϢ" + WScript.Quit(1) + Else + For Each objProcess in colProcesses + objProcess.Terminate() + If Err.Number <> 0 Then + objShell.Run "taskkill /f /im " & objProcess.Name, 0, True + Err.Clear + End If + Next + End If + Else + objShell.Run "taskkill /f /im " & objProcess.Name, 0, True + End If + MsgBox "ѳɹ¼ֱḶ̌" & vbCrLf & "رմ˴30Զֹͣ¼Ƴ", vbInformation, "ʾϢ" + + WScript.Sleep 10000 + If colProcesses3.Count <> 0 Then + Set colProcesses_ = colProcesses3 + Else + Set colProcesses_ = colProcesses2 + End If + For Each objProcess in colProcesses_ + objProcess.Terminate() + If Err.Number <> 0 Then + objShell.Run "taskkill /f /im " & objProcess.Name, 0, True + Err.Clear + End If + Next +Else + MsgBox "ȡ¼Ʋ", vbExclamation, "ʾϢ" +End If + +On Error GoTo 0 +Set objWMIService = Nothing +Set colProcesses = Nothing +Set colProcesses2 = Nothing +Set colProcesses3 = Nothing +Set objProcess = Nothing +Set objShell = Nothing \ No newline at end of file diff --git a/arnndn-models/README.md b/arnndn-models/README.md new file mode 100644 index 0000000..1095a4d --- /dev/null +++ b/arnndn-models/README.md @@ -0,0 +1,16 @@ +# RNNoise 模型(FFmpeg arnndn 用) + +用于背景音乐/噪音降噪,开源方案。 + +## 获取模型 + +```bash +git clone https://github.com/richardpl/arnndn-models.git . +``` + +或手动下载 `cb.rnnn` 放到此目录。 + +## 说明 + +- `cb.rnnn`:对话/人声场景,推荐 +- 无模型时程序自动回退到 afftdn 降噪 diff --git a/arnndn-models/cb.rnnn b/arnndn-models/cb.rnnn new file mode 100644 index 0000000..bcbf6b5 --- /dev/null +++ b/arnndn-models/cb.rnnn @@ -0,0 +1,22 @@ +rnnoise-nu model file version 1 +42 24 0 +-7 -20 -22 13 -4 -54 11 -9 -3 0 6 23 15 -20 -5 12 25 3 -22 18 -4 -18 -16 -14 -35 -14 -17 4 36 -42 -30 -21 19 -10 28 9 -4 6 -13 1 -6 -32 1 31 -49 14 19 0 -1 -35 -32 29 -26 -29 -99 -40 -39 13 -25 22 29 -15 -19 8 -33 52 55 44 -2 35 13 9 -55 54 -16 50 -57 60 -31 -20 -66 16 12 50 50 -8 -17 5 26 -51 29 -2 10 42 24 10 26 50 43 67 7 34 64 -12 -6 1 26 -1 28 22 50 7 29 55 41 37 28 -16 -32 -4 -69 -7 40 52 57 -17 58 -1 7 31 13 -48 47 -4 -9 5 -66 -59 57 -9 2 6 -12 20 -12 106 64 65 108 7 -86 65 123 110 -67 -86 107 18 -89 -56 92 94 86 -13 119 -63 -99 3 -1 8 -14 16 -106 40 -35 25 104 -24 -93 -28 51 -119 -125 104 127 -110 -35 -66 -38 7 39 -11 -24 127 65 15 99 -20 42 -6 127 -49 -116 -128 20 -119 -118 -96 126 121 -54 73 115 64 -20 118 -23 118 121 83 85 63 -120 122 63 -58 17 64 104 -58 -120 -1 61 -52 -54 -100 123 -47 -29 -6 21 30 53 -37 -18 -30 -120 36 -4 -6 9 -49 -19 3 27 -128 24 106 -94 4 77 -27 -36 -23 -28 -121 38 92 74 -32 19 35 -112 -37 104 -46 -51 120 -125 -81 -106 5 47 -57 121 0 83 -52 -64 -8 85 23 96 -50 118 71 -111 -84 58 -5 -35 -72 107 -6 83 127 -128 97 -64 48 -97 -13 -52 30 -46 87 73 72 56 10 -92 -39 -4 -128 -101 40 -118 10 32 -38 -6 106 -119 2 -34 -84 -75 -118 -119 -19 -90 -72 -110 -48 -37 -19 -98 -112 55 -99 76 -80 61 105 -111 0 -125 -87 -119 -122 -100 -119 -110 87 63 -5 -117 -101 -59 24 -26 -67 56 -112 83 -1 126 72 -127 127 -107 60 -119 -118 -101 23 26 49 84 -18 -119 -98 -78 127 -128 -128 -37 -122 -17 -55 109 -16 -94 -6 -112 80 -91 -13 -51 112 -78 114 10 109 38 -23 -52 -72 89 -63 -61 34 3 122 -93 1 -62 59 -114 -107 -128 -39 -45 73 56 76 121 74 -76 122 -33 37 31 58 -93 56 -13 -4 59 101 -82 -13 -112 -53 -84 -34 -118 -23 -18 -60 68 32 -112 -4 -47 105 -61 59 -66 -34 -128 -62 24 -22 -45 -63 -88 -61 20 42 -38 55 35 -128 46 -4 -55 -83 -127 -49 -76 -27 -78 127 -51 11 -47 27 88 119 30 122 -115 10 -54 127 32 -128 78 -127 -32 -92 127 88 -104 -125 61 -113 -22 5 84 -26 -54 78 -97 -24 113 -13 52 -49 -119 -13 -36 17 -75 -88 -48 -10 39 35 127 127 120 127 -25 -3 39 127 -16 -128 -120 -78 -13 -120 10 -41 2 -104 -31 -103 -68 94 -45 106 66 127 124 127 -44 -28 36 97 26 92 -101 -99 -95 -122 -114 107 -122 11 -102 -113 -14 102 -35 -6 22 -23 -56 65 -127 57 -14 91 0 102 -96 112 50 112 -110 -37 -94 37 15 -101 -17 -34 127 107 20 5 119 89 14 -46 -54 -67 -25 -39 84 -1 85 11 -78 73 -24 -5 -50 -126 -22 70 -12 -85 -5 84 19 -49 -59 4 -73 24 3 25 15 64 26 91 -47 -46 121 28 36 -84 49 38 -5 -20 -6 119 -84 74 82 3 -107 -81 67 -123 -20 -71 -18 16 113 -89 8 -128 -61 25 25 -1 25 48 -44 86 111 126 -67 18 106 -21 -26 41 71 -96 101 -37 -24 102 -9 68 -10 -13 -22 89 8 48 71 61 37 0 -119 -30 101 20 29 72 -27 -64 -117 1 56 -36 7 -98 75 -6 111 127 67 -51 -92 9 70 66 -66 97 80 -124 -72 67 56 -75 9 44 -5 13 -17 71 23 -57 47 5 28 -126 -15 -70 -107 64 -4 -31 -95 93 -109 17 -1 -6 -22 65 -24 -39 27 -57 -29 3 -36 10 63 18 71 70 11 -12 -14 117 -12 -93 71 0 10 47 -94 12 1 -34 0 36 -4 17 73 11 -36 -22 71 88 -73 -22 -82 -51 57 90 119 -84 6 2 117 -120 -120 -110 127 -128 127 -126 19 -128 127 127 121 116 -30 127 -53 -10 42 121 67 -32 39 -127 121 -120 -120 -21 127 -128 121 -122 -89 -128 127 60 121 -65 127 127 -5 38 11 121 57 -70 -125 -128 6 49 -42 121 -109 -19 49 -65 -120 22 -102 23 16 -108 88 -49 90 -44 -69 74 -21 -98 -85 -6 -14 -1 -86 115 -20 -28 127 0 12 -15 96 -26 -58 -4 39 -51 -25 -89 8 -9 8 -27 92 47 2 87 -72 98 -30 -35 122 -80 -9 8 24 11 12 -12 13 0 -30 -22 7 -33 -78 67 99 53 15 89 65 -100 34 38 -8 99 31 39 -19 78 -36 68 -65 -1 -67 35 -64 -68 -117 -21 -8 99 121 50 -47 106 -121 30 -89 61 65 -12 -49 -108 -57 -66 12 82 33 39 -9 28 49 49 12 19 76 51 -90 80 -61 -122 50 47 12 4 81 36 41 8 21 -26 55 15 -51 -16 -79 -1 -118 -100 +122 -52 -26 127 -107 124 81 61 -128 -5 -127 -118 43 54 -77 61 -128 -67 98 9 122 101 21 -44 +24 24 0 +113 -120 49 -37 121 83 34 -73 -124 83 -114 -128 -81 -112 18 -102 -52 -71 31 -45 -102 67 -107 -116 -15 -60 30 -51 121 -39 -56 -24 -14 -15 -25 -121 -80 68 -18 -33 -53 -75 84 -28 25 115 -43 -79 -13 31 19 26 -23 -23 11 108 18 54 61 29 -56 -33 -12 -43 -16 -11 -33 -5 -12 -46 25 5 -39 -35 0 58 16 65 61 127 -40 63 107 -18 -23 99 -71 -31 77 127 81 127 82 58 108 83 -66 -20 101 -35 -36 80 21 -17 126 -89 -122 61 36 -7 29 114 -9 9 88 11 31 -71 108 -14 54 69 -21 -124 -31 -9 -82 111 -35 13 26 -74 -11 90 -105 63 90 63 11 -96 121 2 -93 -33 117 49 -11 -15 65 -25 9 -21 -11 -17 -46 -29 -67 -23 105 -71 16 118 29 73 36 47 -30 120 -76 45 -10 -86 81 64 18 -41 43 -80 -39 14 37 -124 -89 107 -87 48 56 -48 58 -3 121 -119 -40 52 26 13 -54 33 -111 34 -97 19 -13 -4 13 123 -18 59 41 40 25 4 56 34 -44 -82 -36 -99 -4 -72 114 44 115 -15 -76 127 -90 -53 -63 27 -11 -42 3 -57 26 107 -13 38 122 -112 104 -71 101 -34 30 111 127 -50 35 -8 -120 77 40 91 120 124 18 42 35 -5 95 -100 -43 46 59 22 2 13 65 -13 -31 -35 14 -4 -3 -2 -8 31 45 -23 32 18 -18 36 -67 36 -52 30 94 -3 -121 99 -120 -33 42 -100 28 -77 -128 52 36 25 53 6 45 75 -120 -119 -76 63 -82 65 60 -25 -30 30 -5 34 -31 41 -128 27 57 51 -127 -118 -47 -31 -29 -82 -128 -86 -126 52 -38 -31 -11 -77 57 0 -18 19 62 20 95 -50 35 34 20 7 13 -72 2 54 -35 -1 -36 17 119 11 110 90 -80 -106 127 78 -85 50 14 51 11 -113 -21 4 -71 -2 -18 -125 41 127 30 -55 54 111 -29 54 93 -97 80 10 -25 33 10 8 -45 18 61 -8 -38 -28 -90 -31 119 -79 4 -37 71 -26 87 12 80 -38 23 -21 -33 -25 -57 -22 33 -43 -62 11 32 -7 31 50 -19 -57 6 24 -12 -63 127 -127 69 -119 60 81 -45 -124 -100 75 -52 -118 -74 -127 -47 73 37 -33 71 -122 -49 -104 -112 -53 -52 -103 -98 30 41 10 -38 46 19 -87 -121 -81 -97 35 -72 101 47 46 -44 31 66 121 -34 -34 57 -13 15 37 25 -68 31 -44 -48 58 17 39 9 40 -6 34 30 41 41 93 -57 -38 55 86 99 -113 108 -105 127 117 58 84 -106 123 -119 -122 -107 -112 -26 23 51 -17 122 119 -111 72 -30 -29 -53 -42 -31 48 6 -29 -18 -122 -58 -98 -23 -2 102 -40 20 39 -9 24 127 -8 80 -28 125 -41 78 27 -29 -115 -80 63 -67 32 -45 65 109 -9 -74 119 -84 76 127 116 36 -61 21 9 -78 64 13 -112 16 95 -127 -117 47 124 117 -66 0 -36 6 -9 53 117 -56 118 -2 90 80 41 -91 106 66 -10 66 29 -18 -47 64 55 -10 -39 44 101 -96 -93 -52 -18 -8 -20 -46 48 -63 -95 33 14 -32 -37 51 8 48 -25 78 54 -38 -27 0 75 48 67 -76 93 -39 -50 -10 1 -11 64 33 28 0 127 52 -4 -76 11 100 49 104 117 119 127 -2 112 -98 107 22 89 124 124 111 -119 116 36 80 74 40 24 -1 -65 -31 -56 -127 -27 119 75 91 43 16 -13 127 111 52 119 40 5 59 76 106 -65 -56 -8 14 41 -122 -5 -14 -60 11 -35 -59 51 16 21 47 -68 7 -54 41 15 -108 31 -12 -123 -35 68 -128 -55 -73 50 12 -81 29 61 -41 -9 43 -78 1 -34 -55 -59 -53 92 -10 -107 -14 6 -36 12 -14 8 90 31 -87 52 -27 83 -123 -95 -55 60 31 -115 -114 16 -18 4 14 15 -67 14 -30 43 -6 -61 12 -32 68 20 -96 7 -3 -58 28 -35 -27 -71 -18 9 -71 26 10 -20 1 -30 -59 93 -128 22 -28 1 -36 -29 25 20 116 -19 -40 -100 7 -74 -1 -2 -109 11 79 -48 56 46 10 1 -77 -15 -48 56 -75 21 7 55 -59 6 24 -113 52 30 -37 78 -62 32 34 55 -64 -63 -68 75 -29 -8 0 -81 -7 19 -6 89 11 23 98 -82 -111 -58 -3 61 -20 -29 -10 -6 -75 0 -3 -63 38 21 -34 98 -24 -15 -5 -88 -31 101 17 -24 42 -111 -7 -3 55 103 116 -10 38 20 -58 -39 -23 -57 5 92 -61 95 -84 -13 32 73 79 -50 48 -33 -26 32 -24 90 -107 70 -79 -45 13 33 -10 70 125 6 27 20 -44 20 -16 -7 72 -54 -32 -91 -24 -56 -4 2 71 -38 0 -112 -30 -34 19 93 -6 -23 -11 -30 -2 18 11 32 12 -71 -21 -95 64 1 -49 50 -52 103 30 38 70 -53 -24 53 -51 34 -23 24 116 64 12 1 36 -59 29 -103 83 -11 -5 13 115 -32 -71 -71 -28 46 -32 12 -28 -61 19 -10 58 52 10 28 -12 -24 -64 -65 4 15 -7 9 101 -62 -116 18 -63 -2 -72 -110 65 -123 -17 -100 34 -9 48 -9 58 -53 -73 -15 26 -86 -102 -49 23 28 46 12 -118 92 -55 -128 -10 -9 -53 -54 82 91 37 50 19 -95 110 51 97 12 0 -61 -20 127 -34 -72 -62 80 -79 -36 91 4 -5 -97 58 100 -32 52 44 -88 -60 -52 -65 27 43 -128 -20 -98 39 -121 15 -73 20 12 -51 33 -56 -100 57 -28 -52 -22 61 -86 51 50 98 -11 -119 5 -75 76 111 -103 -124 33 61 65 127 86 96 -37 -65 -42 21 53 127 66 100 -54 8 29 25 -128 -3 103 -11 -23 79 103 31 59 75 -29 57 41 127 88 4 -125 127 4 -83 -80 -35 24 -67 -127 -17 -120 -25 -45 16 19 -113 -99 84 120 127 -73 50 -38 -55 68 7 91 108 121 -77 110 91 119 124 64 18 -10 -61 3 -128 -81 127 4 19 -90 12 127 63 -61 25 59 75 -104 -51 75 87 -128 -40 5 65 -9 -61 -85 28 -5 -11 0 -29 -76 -3 -2 18 -53 -1 -5 19 7 4 30 -17 51 5 -21 -87 67 -70 82 8 -51 43 -117 -47 85 -91 104 -1 62 -91 -21 -76 -111 12 -120 16 -33 30 37 -7 35 -9 17 65 -1 -128 38 3 26 35 16 -10 64 -13 -42 -106 31 55 4 22 100 -50 -36 54 67 -2 -108 -82 83 -14 67 10 10 -54 -35 -85 67 15 24 16 99 3 -70 65 -115 -15 50 53 123 -109 -13 119 9 1 -118 -116 88 54 27 -8 -18 68 -120 -116 -117 -14 -95 -118 -119 32 -25 10 100 -11 -82 19 -28 -53 -24 58 -39 -17 -37 -113 43 49 -89 -111 50 67 -37 -36 109 58 -79 14 61 44 14 -40 -23 47 13 -38 23 -66 -2 -61 59 28 63 1 -44 20 -10 107 -102 14 -41 8 10 -19 27 -73 -122 20 -12 44 -47 -52 104 -37 107 -3 -58 -62 -31 -13 -119 97 102 54 -46 88 -66 25 26 -106 21 39 47 89 21 -12 -18 -30 34 -20 -8 34 -52 20 -80 -75 9 -128 32 -1 0 -8 62 24 44 64 -36 53 -6 -30 53 -38 -123 13 -57 -25 -30 -47 85 -81 25 36 -120 -35 47 25 -80 127 102 -52 -64 -101 104 47 -33 -17 2 94 -65 6 -2 24 40 -97 5 16 34 82 110 18 -56 84 64 63 16 19 -46 -29 -68 103 -9 32 46 -13 85 19 -33 77 32 73 -12 44 -34 -15 -78 -62 79 1 43 -43 59 31 23 -36 20 -63 -12 32 61 28 -3 24 50 5 4 -106 105 -31 -115 19 -42 -10 -120 17 -57 -126 -12 19 -104 14 -42 35 -49 -85 -56 -107 -119 -122 -91 6 -2 42 -74 54 72 -5 -33 -34 -47 -20 -63 -75 105 -25 20 -13 96 31 -125 5 34 -49 -13 -37 -28 -5 -91 -51 75 -14 10 -76 19 -53 -82 -67 107 -55 45 15 62 68 -27 12 -24 -90 61 76 25 33 67 93 44 -94 -120 115 71 12 12 3 -11 -20 -76 30 108 -56 -35 28 -12 -123 35 -96 -64 1 -113 62 127 106 -47 102 90 -68 -31 -87 -49 74 -7 -47 -45 -90 -14 -47 -78 69 -56 10 72 -24 36 21 -52 -80 -46 -67 95 -27 -21 22 36 13 96 -31 -48 -18 7 50 67 -26 -21 55 100 52 69 -61 -44 -31 12 82 121 72 115 -11 127 8 -6 6 93 52 97 126 -64 78 -5 15 94 37 -18 -114 -4 45 -128 12 -75 -50 100 37 27 -52 7 -31 123 95 73 -76 -110 92 -26 127 99 42 -111 51 -40 -120 49 -108 26 -11 -101 13 -54 -49 92 47 -54 23 2 26 103 -65 24 +-102 43 -24 -86 127 105 23 -26 -17 29 60 62 55 -42 24 9 -51 36 117 33 -27 58 51 40 -43 31 -36 -4 -28 7 -68 46 90 44 3 29 113 -14 25 -44 45 113 92 89 3 46 27 40 -8 19 -27 10 -110 8 25 -16 -13 127 64 -3 43 14 22 -8 -45 -81 -42 25 -36 28 -35 -2 -41 36 -72 -66 73 -99 -75 -65 -59 -120 98 21 127 29 -93 -122 75 -34 41 -13 2 58 -36 90 -57 56 -45 5 -20 -11 16 40 -60 -40 -44 -21 1 36 -31 2 56 -24 30 -20 -20 -46 21 12 -29 53 -113 -125 110 -21 -48 30 -64 -81 0 -59 -42 58 1 -33 -1 1 127 -86 97 38 -37 33 -98 41 -73 99 127 -112 117 -29 111 -115 -122 83 -87 54 101 29 -106 50 76 -51 21 106 31 56 -46 17 -41 -83 -7 -61 -91 60 100 35 -122 53 -8 -43 -31 -46 -14 52 74 30 72 99 -31 -79 9 -14 36 112 -98 23 89 -57 6 -48 85 54 38 -10 83 -33 -98 -54 -113 101 -56 8 96 -21 107 -127 24 28 -6 34 20 -110 67 105 -28 -94 -123 -36 2 -9 -103 -65 4 -59 2 35 7 -128 75 9 18 -7 55 102 33 54 77 -5 51 -10 -24 59 24 34 -84 -66 18 61 43 3 -94 -40 -36 57 -6 96 21 -38 97 -5 36 -23 -2 51 8 -29 32 -65 -113 -38 -71 0 -17 -66 -2 11 -127 -3 76 -128 112 127 -121 -21 -94 89 127 -73 -15 -119 127 -81 124 -84 -124 -68 -120 78 -19 -4 -93 -26 -91 16 58 -1 -120 76 -26 -123 3 -61 -64 107 -55 -7 -100 -52 80 -89 -67 80 -9 37 108 61 -18 -114 117 -22 76 111 -112 70 -26 -103 -107 111 -72 108 -9 109 81 -102 90 -114 -71 -10 -66 -21 117 -81 127 -11 -19 -88 12 -128 82 -106 0 -34 71 59 -128 -46 94 91 98 81 -128 19 18 28 -82 55 -25 -28 -67 -40 99 65 -41 38 97 -43 9 -9 -17 52 29 -10 106 -47 -45 44 -23 -79 21 45 -63 102 -12 -100 115 -13 108 27 70 -49 40 -84 25 -111 -75 84 -38 -19 93 -35 24 -72 82 -128 46 88 15 -58 -6 34 -66 -128 -46 -21 123 54 -45 -49 -48 -128 -107 -60 -112 -20 19 61 -17 8 67 -57 17 93 13 13 -27 94 45 16 46 34 -64 107 29 0 3 83 -52 76 -6 -77 -60 109 58 -56 38 -57 127 -45 46 63 123 -124 85 -115 -110 -104 -77 105 -71 -118 0 -17 -128 20 -82 -18 -6 -128 -35 -128 -96 -127 -76 80 -5 2 -70 -18 99 46 6 -105 6 19 90 118 -33 -117 -2 -17 36 -31 1 -41 16 -36 24 -42 -24 -8 9 36 81 31 -8 8 -73 23 36 28 7 -5 29 -106 13 -7 -21 104 -47 7 -66 -33 4 -8 -77 13 27 62 -14 -9 26 91 -79 124 18 -11 107 15 -85 14 106 -12 127 15 -43 -37 -75 50 66 94 -52 -50 -90 -53 5 -22 -96 -127 94 3 120 24 -40 -22 76 79 -11 122 -1 41 -9 37 27 7 -11 -21 46 72 -27 -63 -34 45 -7 -123 -22 86 6 75 -56 -85 116 32 0 117 78 -12 9 4 -81 -79 -51 50 -84 -88 22 8 -105 6 57 -114 127 105 8 -70 -4 -40 46 -101 11 -35 95 61 -128 -105 123 119 2 86 -71 -4 -8 49 42 -120 58 76 99 11 66 79 -7 -13 50 9 80 108 -3 23 92 61 118 44 13 25 -51 -62 30 46 20 43 24 -70 44 46 48 59 49 -29 25 -39 -86 -112 -64 47 6 -34 -13 -117 8 -123 123 76 127 117 56 120 60 114 105 4 22 -70 119 63 -126 -86 121 113 -1 18 45 -62 13 -25 -74 -16 3 35 -83 -35 17 -17 33 11 19 -41 -70 -9 -84 -106 32 12 49 39 -14 -67 -33 -74 43 116 88 -63 -30 -128 62 -6 127 21 -7 -45 29 -1 16 -48 -86 127 -125 -35 31 -109 34 -29 87 107 55 90 127 68 127 64 -103 49 -126 54 98 106 22 71 -111 -41 56 48 87 -1 17 38 5 -10 19 33 77 23 74 54 41 88 28 8 -15 42 -30 16 11 77 100 25 -56 -15 -2 -128 -5 125 29 -20 50 -48 109 42 43 78 43 -63 -27 -87 -126 -111 -62 70 -114 -111 87 -60 48 45 46 127 -19 45 127 127 127 -6 -9 118 -33 110 125 127 -38 127 -23 105 116 95 118 -53 -7 20 58 15 10 36 36 96 115 28 4 95 2 -14 6 15 -84 -13 -12 88 16 -87 -23 30 -44 -116 -8 127 -34 -58 -10 -6 64 -103 23 87 127 -16 -65 79 -102 -127 -127 0 -73 -110 35 -91 -27 -121 1 -126 24 114 69 42 69 19 5 -127 -45 41 35 93 -69 53 126 127 7 -104 80 -14 42 11 33 -37 48 23 -70 2 -54 23 119 4 67 -55 -24 25 86 -8 -32 -17 -15 2 100 -74 -10 65 10 3 11 -27 -51 49 -50 0 -85 -92 -127 89 -110 121 79 69 59 -122 -11 0 -102 127 90 -13 77 -128 45 72 -28 -75 66 98 -5 -110 -3 -31 127 127 -94 -107 34 -30 -70 -79 -110 -25 -23 55 13 -87 -1 7 -8 10 51 9 -104 91 35 23 88 17 -19 112 96 7 20 1 -120 -90 -19 -73 -11 104 122 10 71 -73 123 -18 47 101 115 -33 84 -71 -99 -120 -36 125 -73 1 81 -68 -17 -5 -71 115 25 3 -39 50 -9 -53 -6 24 80 59 -62 -125 71 56 86 127 48 87 127 -6 -79 -54 -32 -98 -35 47 -31 -26 28 -45 12 -2 -71 34 8 -11 -23 -45 10 -34 -17 11 59 -35 23 123 -55 -70 -87 78 -96 100 -121 61 28 -19 -49 89 -93 94 64 39 46 -72 86 9 -41 56 -98 -89 -38 -118 -75 -85 -116 -128 -116 -60 -27 -35 59 -99 -74 -102 -41 -5 88 -24 -118 -94 -127 63 -15 -39 -84 13 -11 -26 -23 -29 -29 -20 -36 11 -13 -7 41 91 72 -28 -8 -17 -28 10 -70 44 115 12 117 -35 -12 113 44 -91 -25 103 101 -76 -58 -69 95 -30 49 60 68 16 39 68 -68 85 -119 -108 -22 -128 66 37 -115 -128 -105 -53 -28 -109 -30 -101 -57 -104 56 -110 53 63 -94 -118 -78 53 16 -28 -70 76 27 11 -66 0 6 -45 -110 -83 -58 -19 -34 17 32 6 11 -93 -67 49 -41 33 72 32 37 -127 66 -29 11 89 -104 33 -102 -72 -124 2 103 -21 127 127 49 21 56 55 -81 94 -123 124 -110 -91 -38 -23 -56 -57 -117 -127 122 17 88 48 -84 -99 103 -26 -63 -112 -13 -48 -95 99 -52 90 -1 -2 -37 -119 -12 55 89 -103 6 -15 3 107 -16 -116 -70 14 22 86 12 -16 67 96 -87 127 -118 -121 68 -127 -46 116 -90 -118 -124 -122 -48 92 -66 -3 79 127 82 -110 116 44 -50 52 120 -123 61 -115 77 118 68 119 54 126 80 -101 -72 -76 109 76 -103 -73 -4 43 -58 11 -50 -119 70 26 -50 111 13 71 -15 -16 28 65 -5 48 -31 20 37 46 37 -9 -11 41 1 10 -47 -20 13 -119 18 94 -16 8 103 -114 6 -28 78 118 75 -6 72 14 -38 -42 -126 66 -85 -126 110 -127 -70 104 -86 -34 -5 -116 -75 -52 -128 -45 64 1 86 75 -128 -27 40 59 78 30 -9 -46 15 118 -26 3 29 1 0 -72 -79 32 -50 -88 -1 -77 11 -42 -12 -1 -11 20 -66 5 -83 51 117 -1 104 93 9 -126 -54 -67 -63 49 -81 -2 -89 -64 -110 5 -32 -33 109 91 85 -128 36 111 -49 122 -33 20 -99 26 -4 -57 -72 -91 -86 -127 -72 -3 107 -18 -125 -17 95 113 31 3 -71 -13 -105 20 -33 -23 -84 -13 -81 22 65 -90 -62 -11 70 -78 -71 -65 -61 -69 50 -30 -44 -64 -93 -42 4 8 -87 14 -64 92 46 -18 75 -118 75 -92 31 11 70 6 86 -61 -31 -109 64 41 -106 0 -1 -121 24 94 -85 127 -5 -8 0 76 77 -41 -128 72 -72 83 4 23 -55 -30 -123 -120 5 92 89 19 26 41 -51 -6 -39 9 -53 -53 -23 52 -57 70 2 -58 -123 -45 -76 -85 -52 4 -41 91 30 3 -128 -84 -42 127 -47 -25 59 -51 38 -119 -30 127 84 -108 -1 -72 -45 -85 -118 86 -29 -75 73 -88 -63 -53 -54 -22 -59 -70 -107 -126 -79 -12 -85 -88 115 -26 -128 -107 105 16 45 -105 -121 -119 -93 123 -2 -47 38 -1 27 8 -68 -10 -43 4 24 -76 -51 3 48 -38 35 26 -47 -55 -61 50 27 65 65 26 29 -100 -36 97 -28 0 -63 109 -73 -127 -35 39 -25 31 -11 98 66 -39 55 -70 -91 57 +-78 -70 122 -124 127 119 -107 -72 -88 117 127 -128 12 -102 35 -50 -22 -112 76 119 -80 -99 -104 -92 10 34 53 -24 72 92 57 0 -10 13 63 28 127 28 113 83 37 19 127 47 79 -28 55 46 44 -11 -4 59 53 4 50 -53 19 78 62 25 -48 -15 42 -31 -69 8 29 13 -20 -43 10 -36 +90 48 2 +0 48 -94 -16 -92 -34 12 5 1 -9 37 63 18 21 49 -39 8 59 -41 15 5 47 15 26 -2 36 -16 6 55 -12 45 -44 -14 -74 0 6 106 -4 -11 -19 -59 -30 12 -38 -36 46 27 -10 -53 5 -2 -21 13 22 65 -4 36 -7 59 -44 -10 36 -21 11 24 14 -14 -40 -29 -11 21 7 75 11 19 15 -2 24 -11 44 -4 -11 12 17 67 -9 -73 -2 44 11 36 25 17 -17 24 -20 9 -41 -2 18 -33 15 12 41 27 3 -25 13 49 -27 -25 -42 29 -9 -17 -25 49 0 -17 21 -69 5 73 51 10 -26 9 -9 -12 1 -10 -8 -1 -2 14 43 9 19 45 -53 31 18 -22 22 73 -100 89 -6 65 28 -36 70 -20 -4 -24 -47 114 54 109 33 7 40 56 -25 -15 77 -26 -17 72 48 -115 0 -53 8 5 -46 47 53 -48 -46 10 11 63 4 86 56 26 84 -74 60 32 31 -22 -53 -38 76 56 7 -68 47 19 -8 -52 59 9 -41 49 -33 62 -19 -50 2 35 -49 11 21 18 43 110 -10 70 -20 43 -2 70 13 -10 -62 -98 75 5 -8 -35 52 23 7 -107 29 1 36 -42 -27 13 37 -5 19 -1 -4 -38 -106 31 30 39 26 9 10 31 -11 15 -8 -5 43 31 13 11 -39 64 66 32 -3 -2 -33 -33 26 -42 -49 -49 -38 12 2 -28 -6 -27 -113 -27 22 -21 30 44 56 25 29 44 -82 -15 -47 -55 52 -49 -15 -31 25 -29 84 -6 99 63 42 -20 36 -57 -24 -10 -14 -41 42 45 -24 6 -47 25 -25 43 -21 -36 -110 -70 7 -20 -95 -8 34 -71 73 -76 -5 -58 -9 -43 7 -9 -24 65 44 71 50 102 81 -7 -72 -7 5 -65 -28 12 54 30 57 -13 -19 21 63 91 -42 23 -8 -42 -50 50 -27 36 41 66 -5 61 49 -9 55 47 38 3 0 -11 -79 41 -10 -55 46 1 37 -48 -4 56 -30 12 53 -33 5 -28 36 -27 37 26 27 8 -4 6 20 -20 19 6 -7 14 -13 -5 -46 5 28 28 -22 78 -21 13 11 -39 -7 44 -26 -15 0 -16 3 56 -34 127 -28 68 -41 44 -71 -10 15 -11 -28 20 74 77 43 6 59 20 -46 9 58 -59 65 -4 6 -23 -20 -26 9 -26 -36 29 -70 30 52 18 86 -26 40 19 23 37 92 -53 66 33 10 15 47 6 68 30 14 -5 2 32 -34 -25 -51 1 -64 -53 8 28 -29 22 -100 -22 -32 -87 31 11 -11 29 35 27 -91 -12 95 -41 -49 12 -45 -73 47 8 24 -43 76 105 -17 -123 18 -83 51 -22 6 -2 55 -36 18 35 -13 -5 18 27 -32 50 12 -16 63 0 28 18 17 21 10 -11 30 -6 -1 23 9 8 -28 25 -57 16 19 33 -12 45 3 10 19 37 -10 -41 -61 -3 5 2 1 -49 9 -25 34 -67 4 -10 -6 59 26 -70 -128 5 -36 -76 -31 -39 18 -56 27 -3 -21 10 52 -38 -5 34 -14 25 -40 -48 4 -51 -13 -21 -53 -95 -9 16 -22 50 -32 -40 -123 0 -40 -10 27 4 57 95 14 -16 -2 43 -31 -6 -82 13 12 71 15 -57 73 66 14 -49 -5 -46 -39 -10 -34 -54 -15 -30 7 20 3 -3 -15 51 -9 64 21 6 -23 -52 -8 54 18 -39 41 51 -33 13 7 44 -22 -21 -28 -5 33 12 -18 40 13 18 -29 -22 -34 -11 -14 -38 -31 -31 39 -19 30 -36 -25 31 8 19 -4 24 22 5 20 -18 -3 -3 10 92 -15 19 10 34 -9 21 82 27 -35 -15 -9 14 -37 -7 45 49 48 -5 29 9 -32 16 7 -24 1 33 -31 65 50 6 23 -3 22 32 13 19 31 -89 -40 63 -26 -45 1 32 51 -37 -4 78 8 2 17 -37 -2 -12 87 -13 12 25 8 -26 14 14 -32 0 8 -9 38 -8 -1 -4 58 55 -90 73 78 28 -38 -54 -53 71 63 -23 33 -1 4 30 -20 61 27 -32 43 37 -57 -12 -112 8 4 18 -61 7 -74 0 -23 -10 41 -6 -28 -25 13 11 1 15 -19 -9 -15 -16 -27 -13 19 -3 27 -52 -39 30 31 -28 20 68 -5 26 -2 -10 13 7 36 13 8 -9 -17 -9 29 48 7 22 7 -36 23 -23 -6 -13 -4 13 -8 -23 -15 -5 -46 -74 -15 -8 40 8 -31 28 -6 72 92 -81 -46 18 -34 52 13 6 0 47 -39 9 31 34 -5 54 20 48 -35 -127 -33 14 45 -44 -21 -65 52 -48 55 -48 -38 52 -29 -9 22 -49 -8 -63 15 -12 4 -83 46 -3 -21 -13 18 -7 -111 23 -14 23 74 3 72 -40 -29 64 -5 -24 -67 76 -36 123 1 25 40 10 -4 27 -61 -15 -116 49 -20 -16 2 17 32 16 6 -37 -68 -30 -57 -30 -3 3 -29 -68 -19 64 9 14 -9 50 -26 -34 0 -36 -25 15 -24 46 -22 20 -42 -57 2 -70 -2 3 -52 -34 23 -24 16 13 -18 19 51 -18 -9 -9 -10 -2 -12 13 30 32 -8 -13 31 0 -103 10 12 123 -57 -114 24 -72 -1 55 17 48 7 98 41 14 62 108 8 -33 9 -20 13 99 -10 -21 3 37 -12 14 -18 6 43 -77 -18 -35 -59 5 55 30 52 81 113 -38 -49 70 -53 -9 -35 8 -87 36 -85 15 17 5 -34 -2 -80 -15 -22 -80 -19 11 5 -22 -47 6 53 30 14 63 60 14 -3 30 -18 -11 -99 8 -43 60 -27 120 25 -37 17 2 18 126 73 -40 -39 -17 27 3 -44 37 -18 62 31 23 -41 -49 -38 14 20 70 -7 -2 -24 -63 3 28 29 33 55 -10 -7 -40 7 72 -15 -23 -30 22 -1 7 75 19 -7 52 52 11 -4 1 -30 40 104 -27 46 -31 -17 -19 -30 -30 -54 -51 11 -97 -78 23 -16 -42 6 33 9 79 -15 -31 92 20 8 117 -50 -2 29 9 10 -79 31 -46 14 47 -2 -23 84 -11 -66 -86 -13 45 45 10 36 27 -14 38 20 -76 25 -43 15 19 -64 21 28 8 45 29 33 -6 -70 -71 -81 -79 71 18 13 -15 22 -88 -48 90 45 56 -16 29 -5 12 -17 32 -47 -28 -11 2 -15 -61 -41 42 45 65 -59 21 6 -17 -31 44 -32 -35 -21 35 -26 -23 39 -40 -16 -14 44 7 0 -34 -4 41 35 32 32 -24 -34 -24 17 22 -19 61 7 -79 -21 7 39 8 20 -50 -9 1 -58 23 2 12 1 18 -59 -28 -17 -24 -73 -29 -67 -11 8 33 -114 26 -28 -13 68 -72 101 -49 51 50 17 67 27 20 -60 -6 -1 76 -2 7 49 44 21 -22 -20 -25 62 -96 -61 68 4 17 -56 -25 -2 13 23 30 2 18 -2 -14 53 -15 51 -75 0 -36 -1 124 -49 97 -48 -50 -26 -31 -5 -11 -39 54 -34 -31 -21 50 -1 -30 114 -44 -37 -29 34 -117 -19 -22 22 -75 -1 6 11 50 12 -31 -1 -43 77 -72 51 102 26 11 -48 -30 80 51 -81 14 -23 64 52 44 -75 5 -105 -8 65 -7 53 41 25 -37 -13 46 -26 -3 -7 -23 -77 64 69 1 88 -25 -70 40 21 -36 44 2 -18 -30 -5 -8 -15 -4 119 48 -77 24 -5 55 -34 22 -2 15 -44 15 -7 -4 -22 -1 34 -19 -10 77 42 -122 4 -32 54 -39 20 18 -27 13 -13 17 -32 99 -3 -21 36 -21 54 -40 -54 32 6 -83 -39 45 15 9 8 -22 -54 34 6 2 2 17 11 37 29 30 24 52 -37 -44 38 -39 -84 -60 26 -22 6 -39 56 -50 54 18 -73 2 -18 -2 -59 -13 -27 -2 -41 1 62 -5 15 -46 -2 20 -10 -39 -27 3 5 -46 -24 -43 -10 -3 47 34 32 -15 -17 15 30 2 11 -9 16 -18 -25 -4 8 -10 9 -14 -29 14 -49 -27 -18 23 19 7 9 -22 -29 -23 -26 2 38 2 9 11 9 6 -44 5 -6 29 -35 -11 47 13 16 -18 30 22 66 41 -18 -18 -3 -23 28 -15 -8 -4 -21 10 40 21 -28 17 -38 -27 -14 56 -34 7 -46 -12 -41 33 -28 -20 -34 53 47 36 -24 -8 27 -30 -31 2 0 50 45 -10 -15 -14 4 -39 -25 -76 -6 54 51 -31 -23 38 -20 -44 5 -36 40 -85 1 9 45 4 -23 -84 63 -2 21 80 -15 -4 2 3 -25 -45 -33 -33 -33 -20 29 -55 29 -8 13 11 18 -27 -104 -19 -58 -26 -57 42 47 -25 25 -9 -25 19 44 -26 11 1 21 24 -8 0 15 19 26 -33 4 6 11 -52 0 -3 11 24 49 -29 12 -18 20 -52 30 -8 27 -14 -14 -6 3 0 19 20 -12 -1 -6 0 -9 -12 -10 -20 -88 -72 35 11 23 56 17 22 -64 -58 -94 2 -94 -5 -23 10 -82 -70 -15 -71 112 12 -92 -1 25 -23 -61 -21 22 18 -5 30 -93 -5 33 5 7 2 -73 65 44 -6 -5 31 11 24 -28 -10 -50 -42 26 -22 -20 -96 50 82 -29 -116 -2 -44 3 21 -75 96 -31 -28 -46 -23 -65 43 -23 83 19 47 -7 -31 -93 27 53 -69 53 38 -48 -84 -35 -45 -28 77 -85 14 -14 5 22 -24 17 21 -14 2 10 20 -10 -61 6 -63 1 6 22 -58 -12 -22 59 7 -54 -46 -31 -58 -16 -36 57 24 3 -40 -18 34 16 27 -53 3 1 -38 -49 21 -8 -57 -50 21 -16 -51 101 20 45 17 46 23 21 3 -18 -25 -9 18 78 -27 37 -42 -40 15 -13 -28 -46 -6 19 -109 59 45 -49 0 -38 -52 17 -55 -25 -18 69 -25 -14 14 -83 -23 -10 -27 -115 -9 34 59 -26 -1 32 86 60 -82 -28 -19 8 -25 3 -4 -58 1 -10 49 25 -67 29 -119 35 -60 18 23 14 -14 -53 31 -8 -23 20 -23 -37 22 7 -36 -51 9 42 13 -44 -39 -48 -66 12 -64 52 7 78 -29 -44 23 -19 -55 12 -25 -11 12 18 -5 -26 -19 29 9 -17 14 -4 -51 5 -26 22 32 -10 6 -24 9 -5 41 102 -5 -16 -76 -15 -25 24 -19 -29 -24 -15 -18 -23 75 -68 -27 -14 -17 -4 18 -30 60 -5 4 2 -34 53 -71 35 39 -8 53 37 -32 -7 7 -100 9 31 -14 -3 -59 77 -124 15 -30 -16 13 -91 -35 -1 -16 33 -82 25 -78 53 -66 -68 -107 -11 35 -77 -74 -62 -91 -4 -48 96 6 -102 3 31 -75 14 -40 -58 114 50 55 -62 -23 -34 4 -24 122 13 23 20 25 35 -110 23 -89 -15 -15 10 -27 31 -48 126 91 -46 -62 82 -61 48 31 34 66 -10 -70 -24 3 -31 -73 -15 -37 -64 45 89 42 64 -36 74 32 73 -8 8 -13 18 -95 6 -97 52 -15 8 45 -32 -39 -9 -31 23 1 88 24 -26 -70 -43 -21 8 20 -6 10 86 8 -34 -97 -8 -46 -47 -19 2 37 26 25 54 72 35 -37 -14 -19 -61 -2 -56 47 -30 31 4 11 -84 12 20 -128 71 -8 1 -122 -16 -88 39 8 -49 -44 29 2 71 68 -29 -84 4 23 -3 -41 78 52 -76 -1 -49 52 -45 -41 31 90 -71 20 -14 40 -29 -16 55 -77 32 -48 98 -5 3 19 59 -23 -12 4 -13 -2 -38 -47 67 92 -22 -12 -13 64 97 -7 -17 95 45 23 3 -120 9 53 -14 -36 43 94 1 117 68 3 -14 29 -18 -28 -10 -11 2 -37 27 5 6 29 -2 5 -2 36 -37 -15 -30 -65 -46 -15 -12 -102 47 -36 -25 8 43 -45 49 -2 -40 -2 57 -86 34 -6 21 -15 -55 31 -20 -66 47 23 85 -31 -1 64 17 -72 18 57 23 50 11 80 22 -12 -5 55 13 55 18 68 27 16 43 -57 32 58 9 46 20 5 44 31 -5 12 15 12 -16 -89 58 31 87 -47 -46 -39 -3 21 16 -4 28 52 9 -35 3 40 -24 -47 18 -20 20 -47 17 -21 0 -22 -10 14 -5 29 -19 41 24 46 -16 -38 -13 35 -63 56 14 57 25 -71 -48 -20 7 -1 -5 77 24 -56 -3 -21 0 62 19 -26 -93 -26 -4 -39 5 -34 -59 -20 2 36 -6 20 58 -26 8 -13 -3 -6 0 -7 -8 35 28 -24 55 41 -58 -25 6 8 1 48 8 -9 -17 -33 -19 -18 33 -16 10 21 -16 -10 4 -28 8 59 1 -27 70 -30 13 -3 -27 -29 44 -23 -19 -54 -8 -4 -13 -27 20 20 21 -15 29 -23 45 -22 8 -48 52 70 -17 -1 14 23 -1 -38 56 -58 -21 37 20 1 -80 44 -64 112 14 -36 30 31 -20 -63 68 -58 11 107 -7 44 25 -90 -44 -23 -7 4 -51 33 73 16 17 41 -15 -3 20 18 47 5 -55 34 39 -52 57 69 -103 -26 -10 -108 29 88 8 100 -29 43 -66 50 8 9 49 20 11 40 48 56 75 3 58 20 -38 16 -24 27 45 19 0 3 -13 8 -3 1 -6 51 -13 -43 -42 29 -44 12 39 -6 -35 -45 -40 18 -11 41 1 83 29 15 -4 14 18 -25 -17 1 -31 -56 -59 54 20 23 19 26 -9 -30 36 18 -21 -42 36 -3 67 -61 -37 51 -32 -49 -39 -4 20 52 48 -82 39 39 -25 25 -69 -58 -37 54 11 79 113 31 13 -52 -41 -9 -4 -27 -69 11 6 -40 -31 30 51 82 -16 44 25 -75 -14 -18 -12 -22 22 -11 34 -37 85 0 54 0 -4 -37 8 7 -22 -6 -27 45 -20 -16 -45 9 39 -13 3 32 -31 18 62 -49 -13 12 50 52 -56 -37 19 43 32 36 -12 7 15 45 59 10 -30 27 8 -19 -60 21 -9 18 17 -14 10 -26 12 -30 -45 15 -41 -58 -43 -7 15 -10 13 0 -31 -2 21 -46 18 -15 -20 -25 -30 -8 -6 31 4 -2 8 -50 46 45 -3 66 -13 16 18 -22 56 36 34 -13 -1 41 -49 -18 -45 -69 -9 6 -46 -56 -1 69 -30 -17 -56 37 38 62 49 114 14 95 5 -45 23 127 6 12 -13 -89 49 2 68 30 -69 40 -47 -40 -10 28 42 -25 3 -60 78 -122 -9 -16 23 -40 86 46 22 37 59 -32 63 71 14 38 -37 35 -16 24 -89 23 119 4 -39 24 -20 -123 -42 9 -7 -59 -32 -56 -14 -17 -29 15 71 10 -12 -22 20 89 3 -47 8 10 20 32 -37 19 17 26 16 -26 7 16 2 -54 5 -44 -19 -25 6 -23 -52 41 3 -10 25 -11 9 -23 15 51 -24 17 -49 -28 -57 -16 -38 -64 -41 -8 28 -48 15 -37 7 21 10 -20 -26 -11 7 57 -28 22 4 79 -4 -50 -25 43 -88 -29 47 -72 -57 23 13 -12 -16 -48 81 -78 7 -5 7 -89 -53 80 -94 -35 -11 35 -60 -40 27 53 41 3 -51 -59 -14 -38 75 0 8 91 -50 26 -58 -58 6 -16 4 -25 -24 28 10 4 127 48 -5 7 -7 71 -27 55 14 77 79 -49 -14 17 67 -23 -25 16 -2 -10 -2 54 -9 86 -32 28 -1 12 -25 -26 12 6 26 42 50 -58 13 50 -21 -19 18 36 -7 -37 38 14 4 5 49 -3 -20 -21 -41 -9 -15 37 -45 -12 20 -27 14 -54 5 20 53 21 22 -2 -29 9 -6 5 -61 3 15 33 74 25 -16 41 33 19 -5 -22 31 20 27 23 5 -34 -52 -9 23 101 -2 -2 5 -42 51 58 -29 38 35 -68 11 118 -8 52 -36 -28 31 -1 -19 -28 8 79 -47 25 -43 0 12 57 -18 -35 -103 -96 45 -40 19 -35 -68 69 106 31 15 40 24 -22 -1 26 -4 33 -32 26 46 -25 1 -83 4 47 -27 -14 -61 43 6 -3 -6 -14 -40 24 -9 -1 -23 51 -6 -37 12 -46 16 -29 -50 17 47 4 8 -29 -31 44 -40 -23 -29 -14 -55 -35 25 4 -53 32 20 -12 -48 37 -21 40 -5 -54 -5 11 12 63 27 10 4 -64 -9 -62 -42 14 -19 10 -13 3 11 1 -13 -17 67 -26 -27 10 12 32 -14 -7 -69 48 -33 -40 47 118 -6 34 36 11 25 -55 0 30 63 -33 -16 14 10 -26 -15 39 -19 51 -6 44 11 -20 40 27 30 21 63 52 5 -57 12 13 -6 19 68 50 -45 42 0 13 -17 35 2 3 -14 -40 -13 2 -25 17 43 -42 -25 -27 -15 -18 0 29 4 -59 69 -10 27 17 -8 33 -4 37 -50 5 -47 23 0 -50 27 -32 27 39 40 -72 -16 27 114 -5 -14 65 -53 -10 65 10 -35 13 12 53 10 2 54 -4 -33 -41 -42 -29 -11 61 23 -2 -36 32 0 -34 -31 44 -15 -25 -4 -7 4 -43 10 -17 -4 17 -11 4 35 -5 -7 13 -10 -13 17 47 -7 50 -10 -24 -42 -11 -16 -34 2 -35 -5 -1 -10 76 9 102 -29 -27 -10 0 0 54 10 -104 80 89 -56 67 -12 117 -40 21 29 -48 80 20 -69 22 29 31 -2 120 -84 61 2 -48 31 62 55 38 55 1 69 62 70 -54 27 65 -103 55 18 9 32 68 1 -17 25 12 64 -56 -14 40 -36 -5 -27 70 3 94 -38 -67 49 -39 -13 -19 -31 -57 -67 -28 59 -45 23 46 -66 -25 55 -56 27 51 47 2 72 119 -13 -6 51 14 5 42 -16 50 -67 16 28 -2 -70 16 5 63 -38 3 15 78 -11 -97 4 38 17 26 0 -37 -97 -44 23 2 89 -42 -39 9 -31 -45 -30 -40 -27 -29 14 -18 -25 -36 -50 55 -19 -23 -12 5 -79 52 -4 -57 7 -24 -11 97 -16 -15 9 -21 -37 1 7 -11 22 0 -29 -18 -27 -46 14 1 7 -11 -9 -17 23 -15 5 -51 -30 15 -21 -22 10 -11 15 0 28 51 26 -18 7 -6 -27 22 -75 1 -49 37 13 -13 49 -13 15 13 18 33 1 82 24 13 68 8 5 45 -27 39 28 -16 -17 0 20 53 8 0 17 31 -15 20 0 -35 -36 36 -38 -13 -7 -9 60 34 38 6 49 4 21 17 -1 5 33 -45 2 5 -24 -29 -16 29 28 -15 21 -2 23 -4 -25 -8 7 -19 -1 -47 -19 12 21 16 -39 -7 -4 9 -22 -12 -20 -13 11 -4 -9 -12 -10 -15 9 -8 17 42 12 -2 30 -27 -11 -6 -19 -18 -41 9 12 -34 -10 -47 0 -10 -35 7 25 -61 -10 -82 -9 -15 18 -51 4 -21 8 31 -27 -4 79 13 -11 36 9 -47 39 32 22 36 34 20 19 17 -64 47 -17 19 10 -70 36 25 3 0 2 -12 33 -55 -16 -7 35 0 -17 11 28 55 -33 57 21 43 -41 -4 25 11 -2 4 12 -25 39 -29 31 9 23 -39 -9 -17 -2 16 -8 8 -57 -42 45 23 15 -29 23 -7 34 25 6 -41 1 7 31 -4 1 -13 26 -5 -9 34 2 7 -28 -41 -2 4 -21 -33 -33 30 -7 9 15 66 19 17 -32 -25 2 -30 -12 -28 -55 -29 8 -40 10 10 3 -44 5 -38 -8 -22 35 -46 6 -16 28 33 -4 -61 -3 -59 -3 -89 -2 -32 -33 -38 56 -64 -14 -10 77 -31 -8 -38 68 -78 27 25 65 35 -41 -98 99 -83 -2 -19 0 1 -6 -7 7 15 -36 -63 100 -7 -57 6 -70 11 -29 -7 43 63 7 69 91 9 39 7 -71 13 8 6 -64 34 -46 -105 -29 -17 -49 -20 54 43 19 59 29 61 -1 27 1 -9 37 2 14 -2 34 -20 56 -43 15 -46 32 34 -65 5 -2 -11 -8 13 12 39 26 30 37 23 -58 70 57 -62 -10 -22 2 -23 31 30 23 48 -24 6 -41 -3 -10 -17 -45 5 -23 0 -33 -43 23 43 29 15 -12 -2 12 49 19 6 40 8 0 1 10 31 -18 -39 38 -47 14 -43 -32 -29 23 -24 5 -20 24 -27 21 -42 -12 50 29 25 -38 2 19 -28 -9 -21 10 -8 -2 -18 -23 -31 4 -21 20 15 32 -49 -47 4 11 40 18 -16 -40 10 34 26 -8 -1 15 -9 9 35 -2 -25 12 -24 -37 -38 31 -35 -13 -83 20 -48 23 -40 -16 -32 -28 43 -38 44 -19 -21 -25 -26 -27 15 19 23 -6 23 -19 18 -4 65 -27 9 46 -32 -33 -10 -38 44 24 -20 -7 -12 39 -32 -23 42 -10 50 21 11 1 13 -24 12 16 40 32 -6 -8 8 -19 -3 5 -21 -39 18 -42 -24 -17 -49 -5 -5 -30 9 -1 -2 -1 -21 -14 -29 9 19 -14 51 39 76 -2 -57 57 20 -62 106 42 8 22 89 103 37 -1 -14 25 53 105 25 30 -22 -10 5 58 -91 6 70 -45 -64 91 106 5 33 42 -11 -101 51 -45 62 16 -38 -23 62 77 -35 -38 83 5 0 -29 -30 -15 -79 36 -30 -79 -42 -10 -45 -16 30 -37 62 39 -86 56 -56 18 -20 -13 45 74 -66 1 46 -19 -8 -58 -58 27 94 -70 -49 -3 36 -16 -20 5 27 48 21 -85 3 -34 10 -23 -12 -12 -36 25 4 -15 -5 14 -14 -26 -58 -37 -25 11 57 -12 -10 19 -21 -23 -5 -51 21 38 -22 4 113 -8 -8 0 94 41 -20 5 -56 30 -15 22 -11 -43 -48 -14 -76 -3 20 -50 -55 22 -25 -111 30 11 99 8 -57 34 -31 30 -84 -40 33 -26 38 -42 -24 33 -81 -6 -53 53 61 47 -5 12 40 33 32 93 -36 -10 -34 24 -11 77 67 31 47 -20 16 28 -18 26 72 56 0 4 -3 -69 -13 -30 -20 -8 -13 -17 53 1 -66 -7 6 -94 -37 -82 43 48 85 -52 43 8 18 -38 -45 -29 -20 -28 -61 -26 74 11 -18 -21 -1 6 -1 58 -43 28 -42 23 90 21 4 -4 27 -35 46 5 -7 25 -4 -63 -72 -5 -38 44 79 20 -3 -33 16 -51 0 54 -13 26 -20 41 -37 -46 -15 12 9 3 18 21 -10 -13 -29 46 -27 -20 -30 -28 47 -20 -6 0 39 -8 -9 53 -45 43 38 9 44 3 11 -4 26 0 8 1 -29 41 41 36 1 22 -88 -1 -31 1 -30 -96 24 39 -47 -68 -2 -12 5 10 -27 15 -50 -42 -25 -5 33 36 -3 51 15 3 26 15 -17 7 49 -5 -4 -29 -13 41 -83 4 -16 19 -13 -7 -37 -24 -49 10 22 -16 -23 13 -32 -94 29 -54 -24 -1 70 55 -12 11 10 7 -22 31 4 -59 12 -64 -39 -6 18 -27 0 32 -8 28 -74 20 15 5 -3 29 -3 -18 -7 -13 -30 51 -32 38 18 52 29 -14 44 2 -26 -20 -45 63 -28 -9 -20 -54 8 13 -19 -26 26 -26 44 10 -14 16 5 11 19 -54 -9 -24 -63 -24 -23 5 -54 27 24 15 -50 -12 -15 10 -1 36 -60 0 30 -16 18 25 -2 -15 -8 -75 -10 -17 12 4 42 22 -40 -18 -15 0 -1 0 17 -9 -74 -36 -51 -29 -24 -18 -41 -58 -18 -27 -23 40 -37 17 -27 24 7 -7 -35 -21 7 -16 78 35 25 21 67 39 51 -4 -17 -13 27 34 13 -8 -11 -4 -29 20 6 -4 51 -7 72 -15 14 17 13 -6 6 -51 26 69 15 7 -33 60 -20 44 -6 4 33 -27 35 -36 6 -3 32 -9 32 19 -2 -14 30 -9 -10 13 -24 -24 7 13 -6 19 -19 -8 -23 -10 -5 40 -13 29 23 -18 35 -49 -16 15 15 -17 57 22 -16 -5 25 15 1 -6 29 -43 15 25 -2 -48 -53 -13 -25 23 1 16 -26 -22 -57 25 31 -24 -6 -10 -45 50 32 -20 11 -45 0 -25 21 -49 -32 -6 -8 37 -51 -45 -9 11 -19 35 19 23 39 -22 20 -27 34 -48 33 -7 -27 -3 -7 -12 5 4 10 13 -19 -44 6 -47 -20 -29 18 2 -38 -14 -38 30 -17 32 10 -32 5 24 -81 46 56 44 37 32 4 -18 37 48 30 34 -26 -2 1 1 -8 -32 -19 -14 -15 19 12 3 -3 -13 26 -12 -3 -19 -19 -12 14 -23 -26 19 -9 18 22 -12 -8 -18 -32 30 -23 -28 -19 -39 16 -12 26 3 -15 9 37 25 8 -29 9 5 18 1 42 -20 -38 -19 -11 -14 12 3 32 18 -14 -33 92 42 57 5 25 3 15 68 63 -39 0 19 38 19 116 38 23 -52 20 69 31 49 57 46 -5 31 84 92 43 36 46 -22 26 40 16 71 60 78 7 47 75 -47 -7 -1 8 1 -11 -37 3 38 36 9 -11 -30 -33 51 -7 -43 -87 32 -44 -6 10 -36 47 20 49 -6 -38 -10 -38 63 4 8 7 -12 30 -16 52 -13 54 31 -66 -22 -24 -45 35 44 33 8 64 3 -12 68 -8 -22 -26 16 -9 -46 -28 27 -3 20 0 2 23 -1 -24 -80 -7 -71 0 -4 -10 -1 13 -29 -43 -22 18 54 -16 -10 2 -24 32 -52 -28 -27 4 -32 11 10 -18 13 22 81 -11 -9 -19 14 -26 -20 7 7 8 44 4 -8 8 -38 28 -87 82 34 53 24 -23 20 16 18 4 -29 14 -21 -39 35 63 4 9 54 8 43 46 32 -16 -19 -16 -41 -4 6 -21 5 -27 -32 -2 12 6 16 6 6 -32 55 38 44 -26 19 -12 -68 36 -32 2 17 -9 60 -57 30 -10 -9 -24 12 -13 -44 3 48 -11 -3 47 19 -41 -1 20 47 -5 23 26 -12 -37 16 -61 69 -2 -12 -23 103 48 9 11 -41 -45 55 40 -19 11 58 -4 -26 26 21 20 -65 22 -24 15 -10 -39 16 -26 38 38 -9 40 -8 -12 -47 -5 -6 19 39 -16 23 25 -23 34 -24 -10 11 8 12 51 59 -27 -43 -19 -24 -11 -39 21 -15 77 -5 5 49 33 -23 -25 17 14 -13 -18 -23 -22 39 -24 36 0 36 -7 47 9 -15 -39 -27 42 -22 10 1 18 -7 -23 31 -16 -4 -18 -3 -28 2 32 43 16 -20 -63 0 44 3 -50 -22 -15 -8 27 -60 9 0 41 -23 30 14 -23 -1 -40 21 -17 -11 -39 31 58 23 14 47 -28 43 23 -33 -24 -13 18 -45 -24 21 11 -1 32 9 44 -4 -30 21 -40 -4 23 11 -14 30 -12 -17 -1 11 0 0 26 15 -17 21 -9 -11 3 -14 -41 4 -21 23 -10 10 -19 19 15 26 -15 20 -10 8 -6 -15 -10 -7 -14 -6 7 -13 -45 -16 8 -6 7 8 -18 -7 0 -26 56 32 12 -15 57 -7 -75 -6 -9 18 37 2 41 13 6 -40 22 -33 112 -22 17 -43 41 18 -50 1 0 34 6 -12 57 29 4 36 64 -37 -6 6 -19 -9 69 10 -16 -22 -28 29 41 30 14 9 -48 31 54 -28 -41 35 -42 6 -21 62 35 -27 -2 12 -5 38 -11 40 20 -26 55 -75 -26 13 -30 37 53 -18 41 -23 -43 -32 -32 51 19 3 -7 -44 50 20 7 6 -15 20 -10 56 -31 1 7 14 -9 -33 -25 8 14 11 5 29 -37 -35 49 12 26 38 4 -15 0 5 -6 -7 23 38 23 -2 29 -5 -14 -33 -25 36 26 -28 -14 -45 6 -46 -18 -6 -40 -19 11 1 -28 29 0 -45 33 -45 -26 -22 58 -6 -47 41 -48 -17 25 3 47 -7 -10 50 -28 51 -35 2 -67 -24 -55 2 -40 -2 -12 10 14 28 -19 -83 -12 -31 -21 -4 -9 33 21 -34 -31 55 -28 24 29 36 -40 -6 -1 10 51 -22 19 -29 14 5 -56 -15 1 -41 -26 22 7 14 -12 49 -54 27 -84 0 76 13 44 -10 37 -22 -26 -62 22 -37 66 0 -13 -45 10 -1 -19 94 44 -3 -53 -36 -78 44 -54 -40 -38 12 -19 1 -24 27 -38 42 19 15 -18 -26 -6 63 1 -7 25 8 -4 -19 19 15 0 -42 -40 -3 10 5 20 -19 45 25 75 -43 85 8 53 17 9 -43 -2 30 -75 31 7 61 41 74 10 29 40 -33 16 33 -7 38 57 31 -59 -11 7 13 -11 14 -34 22 41 -24 33 -33 -36 27 -6 -38 -13 15 -4 70 -28 26 17 13 47 -21 12 -24 -18 -6 62 26 -33 7 15 -32 -14 -42 71 -41 -39 40 -78 4 -11 -113 20 -12 -26 21 -2 3 -57 4 -18 40 31 12 -16 -45 14 42 -69 1 11 34 -6 22 -14 14 5 -27 7 -46 4 59 -19 38 8 15 -3 37 -33 92 9 -21 37 -37 2 -16 -20 -27 37 -18 17 -8 53 -48 -13 -45 42 -7 30 -15 18 1 -20 7 -21 16 -30 5 -30 25 -33 13 35 -8 36 -16 31 -44 6 -11 -11 43 37 -45 -21 -10 -20 -38 -9 -31 36 9 -20 -17 0 18 10 -19 18 47 38 15 -51 -9 -22 85 60 -9 -44 51 1 29 3 -21 75 11 -20 -45 -23 -12 11 73 8 15 56 22 -50 -65 3 10 11 18 -21 -19 -16 22 2 -19 -6 17 36 2 102 80 18 -9 -31 -9 7 30 25 -30 -7 13 30 -15 -12 36 12 -1 67 -16 -8 -23 26 -54 23 5 -43 -72 14 5 26 21 69 -19 -32 33 -27 -6 -13 -63 46 -83 42 45 19 23 11 -27 7 -5 -25 -32 2 -37 -52 -23 -40 21 -23 4 -29 63 29 -1 -37 22 3 -9 3 14 44 36 13 -25 25 9 27 -51 -5 -37 23 14 55 31 30 1 6 56 17 -58 18 43 -45 3 10 23 73 -3 -23 -37 20 -12 4 30 42 -67 -31 -19 -14 -19 -66 10 -9 47 -55 18 -31 -12 28 22 29 -15 -31 45 -47 48 12 -14 -41 -32 -47 10 29 10 -53 10 -7 44 13 7 10 -8 -4 43 50 -23 16 -54 20 -25 31 -44 21 -12 13 2 44 -37 -21 -24 -37 25 45 -31 -18 26 -57 -49 -21 10 0 30 38 -64 -1 -21 -20 83 27 -25 86 -17 6 -6 -46 -36 -41 2 -10 17 2 29 32 -22 6 -1 26 1 24 33 35 36 -43 8 27 31 -6 27 29 11 4 -11 -11 12 -24 -38 31 -23 15 -46 1 -39 -14 18 13 -24 10 -3 10 16 -8 12 23 41 30 -2 22 -15 19 49 42 9 -11 22 -2 9 38 18 -18 33 -5 17 17 29 -38 -3 33 -34 -28 21 34 -13 24 -28 26 19 -2 -30 2 -51 -22 -12 -1 -25 -8 8 34 43 -15 23 -13 36 16 -35 22 -14 25 15 41 1 8 -46 2 26 11 -1 2 -14 -11 -7 57 -59 32 -41 24 18 18 3 -2 -10 16 -5 36 25 5 21 29 -101 2 22 -23 10 8 -21 8 39 24 4 -1 5 -41 21 -9 -12 12 -20 -29 -14 1 -28 46 -21 35 -15 -7 10 15 -49 -27 20 -19 42 -1 3 -20 23 10 17 -19 6 -22 23 -36 -35 -5 17 4 -37 18 -25 -28 23 12 14 -10 34 20 -23 -113 12 -14 15 21 37 -52 26 -16 35 19 -5 46 -20 -45 -37 22 -12 -55 -1 61 -34 -52 -43 63 5 -7 -3 -65 9 1 -23 50 -17 32 -7 -37 21 19 -7 19 -11 14 27 -9 -21 -36 -49 -17 33 42 -35 -26 -20 29 -40 6 64 -1 17 23 -54 -56 32 53 -29 -11 22 42 16 8 -18 10 -6 31 10 -7 32 -49 47 -36 53 96 11 -7 16 26 42 89 -18 -24 -1 10 17 -20 9 32 2 -27 35 12 -16 -29 -15 31 11 -24 -14 3 24 -2 -57 8 23 37 15 -11 21 -14 -8 5 -1 -19 -24 -4 -1 -12 13 -22 41 -31 0 48 47 -4 46 3 -9 -15 12 -47 28 -9 3 -18 -2 2 25 7 -3 15 -19 -30 51 -67 13 8 71 -31 34 65 -32 0 30 -22 3 42 -46 -72 -24 91 39 28 2 -32 1 -5 -40 -25 -48 -4 17 -76 -2 -52 -10 38 -43 10 -67 31 24 -23 71 -68 33 -47 -24 -52 -19 2 -47 3 31 -62 44 26 20 -25 49 -19 -42 -92 -8 26 30 0 25 -3 0 -34 -21 -61 -13 -12 -33 -56 -21 33 -25 20 -42 -16 21 -16 -20 -25 -83 -19 23 19 -2 46 -28 -15 -15 6 -8 -38 0 -33 -34 -42 -39 -13 -39 -44 13 19 -13 4 28 -32 10 -9 -9 19 3 33 8 15 -7 -18 17 -4 15 -2 20 -12 24 -45 -38 -5 15 -65 -7 -31 -2 11 70 2 21 43 -3 14 6 0 32 -21 -32 9 -25 4 20 -22 -8 0 48 22 18 16 -32 5 -42 -18 31 12 21 29 -5 -67 -8 -45 -39 -5 -15 25 -18 73 34 17 -4 -40 37 -10 -9 -38 30 -33 -22 32 11 35 -27 -3 48 16 1 25 29 21 22 -25 -38 13 -18 33 -18 -36 29 -1 1 -3 21 7 7 14 -36 32 29 -8 -21 26 -12 47 3 6 -21 9 -2 22 -39 36 18 18 -35 -41 -1 32 5 26 -23 16 13 18 28 5 -9 -26 -13 9 0 -42 -5 -5 14 -9 26 17 17 8 28 -10 -8 -20 -13 18 -18 -21 -9 13 -6 -5 3 5 -2 -26 -46 30 -38 32 -7 43 1 -34 33 -31 -10 47 -49 6 -17 10 -15 -11 -58 -48 -9 -21 1 33 -57 -29 -33 30 27 -6 -42 61 -17 25 -27 62 -42 -33 3 39 33 -76 94 1 -4 17 -5 3 -33 -27 -42 -28 40 -3 17 -26 13 -31 12 25 16 7 -35 0 -85 -6 27 5 46 3 14 69 -73 5 -33 -47 -17 -1 0 -39 -36 4 9 12 16 38 -13 -18 -31 45 33 -15 -8 2 33 -14 4 54 -52 9 40 5 -22 -23 -19 -42 -32 -14 14 8 -1 -45 -11 3 -40 29 -5 11 15 41 2 -26 -1 -9 -14 0 -11 -38 -4 28 37 28 28 -52 -3 -1 27 10 2 2 -35 41 -5 4 17 38 2 -19 -25 -16 21 -51 31 -36 16 -12 5 33 9 33 -57 -4 -35 0 -46 -25 29 -37 -49 0 -7 3 43 13 -32 56 -49 -21 -54 -39 8 13 -53 -27 -45 -67 -5 24 -54 -34 25 44 38 1 -25 -17 6 -17 59 -44 -21 -24 6 -42 -17 -3 -6 2 -62 6 39 -46 -44 -65 34 -46 1 -54 -59 30 -31 -4 20 42 6 20 -8 -32 -19 -28 12 -18 40 -22 -30 12 -51 0 -33 -8 -7 31 -78 97 -52 -47 -8 -4 15 12 12 24 1 -16 -40 22 -13 -1 58 -15 4 -45 -34 8 21 2 54 19 -7 -23 23 -4 -9 -3 23 32 11 42 -7 2 64 12 -18 35 -17 2 -14 15 89 74 5 10 -1 -2 -28 -5 -13 -18 22 48 -35 41 -28 6 -21 -13 -7 -11 12 21 50 -24 -58 11 10 -56 -30 36 23 -62 -6 6 45 27 38 -24 -29 -19 23 -34 -7 50 34 -40 -22 30 12 -27 -40 -60 -4 12 -65 32 7 8 31 22 -62 -22 53 6 -48 11 -37 -46 0 12 13 38 21 20 -31 5 -14 -44 -13 10 42 -61 -36 28 -18 -37 25 -32 -65 -19 13 -39 38 40 -24 -18 -24 -22 42 12 5 -5 0 38 -13 30 -20 37 -4 2 -22 -24 16 -11 -30 22 27 -3 -19 4 -32 2 11 25 19 -2 -1 32 -2 10 -24 -11 14 -33 -16 7 -32 -25 9 37 -42 -4 21 -4 22 23 -29 -11 -38 -8 -9 -21 -3 7 -23 4 30 -8 -11 -1 -5 -6 2 10 -10 11 37 -9 -31 -45 1 7 -8 -7 12 -20 -36 8 50 11 -20 -29 -1 -19 -2 18 6 -1 49 -18 4 16 -20 -6 4 18 12 34 -2 -23 10 10 18 7 -24 -11 7 11 -3 28 -4 -7 -22 -9 -9 -94 -11 -59 4 -13 2 40 28 4 17 -1 -19 11 6 13 11 -11 11 8 -5 -14 38 -7 19 13 -25 11 13 1 22 -9 -6 19 0 1 -11 2 11 1 -7 2 9 22 7 -11 -9 -17 -5 -13 -5 20 17 9 8 -7 -17 8 -1 -20 -9 3 -8 -7 -10 -3 13 9 -16 -2 7 -10 5 26 8 19 -3 5 11 -47 43 -2 -43 1 16 20 15 -36 -31 -23 -27 -3 -19 -15 41 -17 28 -20 13 -15 23 -17 -20 13 1 -10 20 16 -1 13 20 23 -59 21 16 25 72 -31 -18 -28 -2 -26 -9 30 12 8 -19 50 -62 -10 12 19 -38 0 1 53 14 23 16 -53 43 43 70 38 -42 -26 -35 3 -18 12 24 -115 -43 35 55 9 -4 43 21 -48 -21 29 43 -27 -73 -24 -4 -64 72 34 -34 38 52 38 -21 33 -35 6 -30 25 -5 15 -5 -10 7 -16 2 -9 -44 18 -12 -8 -24 27 19 2 -22 -7 -18 -42 -13 -19 27 32 48 -33 -13 25 11 -25 -8 -24 10 -1 24 -32 -3 21 4 8 -15 -30 17 -18 40 8 -39 -10 -26 -7 -36 -53 8 -11 -30 2 -5 -28 14 -43 -51 -30 22 -8 7 -39 -23 -13 -10 -20 5 -18 13 -35 -28 13 -40 38 -4 39 52 0 17 13 24 -50 -39 36 0 15 -50 -16 -25 -8 39 10 -46 5 58 -18 -9 -16 -69 -20 37 23 -36 -39 58 20 -3 -14 3 -21 36 61 37 -128 4 -52 5 56 98 -3 -4 62 -21 -44 -105 -55 47 27 -21 29 1 3 10 27 76 42 23 -26 -48 16 -6 39 10 19 -31 5 -20 16 -44 -7 -24 -5 18 -5 -48 -15 -58 0 -8 2 14 8 24 32 53 -6 -19 -19 0 -45 -20 -21 32 -11 10 -64 -9 -19 4 -13 -12 -39 13 40 2 -21 6 -18 62 36 -40 -75 22 16 -29 -69 67 -64 42 37 -29 13 -23 15 37 55 -50 -72 9 15 -15 -32 25 -45 -23 59 48 82 37 43 -14 34 12 21 59 -4 92 -10 -56 -48 -84 43 -74 -19 73 -28 30 -8 -23 5 -12 -72 51 49 30 28 -102 38 -24 -24 -15 52 49 44 15 22 -51 1 -27 -25 -33 56 119 -31 42 -8 -61 -7 -1 11 -37 -32 -82 -16 -28 -73 -31 -6 1 -39 41 15 -24 -43 6 40 49 -6 -46 29 -44 12 35 22 -4 -35 12 -27 -10 -36 -3 22 11 52 29 9 -16 -13 27 -40 -36 -50 22 -9 0 -22 -39 0 -5 -10 -1 -71 -24 -4 26 -18 15 28 -54 2 -29 0 12 39 -20 -26 17 30 -42 56 115 -44 12 -1 101 -15 12 -34 13 -13 -55 32 -29 37 -25 -36 -31 29 12 13 -7 34 5 29 2 32 51 -19 30 15 45 10 -28 -124 -78 16 -91 -5 66 56 4 4 121 124 -29 -125 -51 -20 -81 13 78 8 43 -45 -61 6 -89 -80 2 -15 -21 84 11 -25 -118 -21 109 40 -48 -5 14 50 72 31 -5 13 14 9 -32 -35 54 -15 -1 -45 -32 2 0 -64 -9 9 -32 -26 -63 16 5 -60 42 20 34 -12 66 -29 -37 -28 13 46 -4 -33 -10 -29 12 -26 -21 65 -30 16 12 10 9 29 -20 9 -34 48 -19 15 -22 -16 21 71 14 -44 -3 24 -9 9 -54 23 -68 -72 -1 -14 -96 19 22 -11 17 -12 -20 24 61 33 -5 -80 -23 37 -33 -38 -43 -7 54 -2 -2 -12 77 5 -27 -35 -18 -22 15 -27 -10 1 -9 -11 70 -18 0 32 44 2 16 38 42 85 2 -45 -43 -13 7 111 -69 0 98 76 -25 59 -23 -26 -48 -51 25 13 35 -30 -47 54 -11 16 -3 -22 -6 67 -106 -29 32 -93 -69 9 38 43 -32 -61 72 -68 -55 58 18 0 -21 -32 11 -33 -17 28 -28 0 59 -38 -17 2 24 16 -49 -22 47 -3 3 -9 -5 -12 -9 26 -23 3 24 -81 -62 5 13 -15 -41 32 -36 -11 -10 27 -13 49 91 -30 -41 90 -2 10 -120 33 87 -27 -120 116 -17 75 -53 -49 -16 -3 -121 -62 -22 8 0 13 71 6 34 -40 -38 -47 -59 -73 5 46 70 -128 3 45 23 63 -117 45 54 123 78 121 74 -66 10 69 124 100 48 -24 -87 -79 58 -52 89 -127 106 87 63 50 7 -35 -127 -92 -123 37 -124 -45 1 120 -60 -64 90 15 24 102 -111 -15 -28 123 -89 48 86 127 -4 -71 -125 6 19 -46 -119 125 51 -70 31 -125 12 20 -14 -30 18 40 31 -121 55 -91 -80 127 127 66 -36 3 91 -65 20 -9 19 -38 -44 10 -88 127 79 94 19 15 -31 127 -66 -26 60 9 -100 -1 -82 79 -56 13 -18 -10 81 -99 -128 81 15 49 -117 50 -28 12 -13 124 -76 -41 34 108 26 52 -115 77 18 41 53 74 -49 0 -89 -126 -85 3 26 -92 -126 65 -66 -25 -34 -24 117 20 -121 58 65 117 123 39 123 -26 -22 -1 -85 -48 25 -79 -52 -77 -21 -112 106 -124 127 78 -48 126 -20 -128 114 -109 -72 -33 -125 65 -73 -85 13 -33 -31 125 45 36 -15 60 -37 32 -62 -66 66 50 20 -89 127 124 -46 -70 -79 -69 74 -90 124 33 -55 -62 68 29 95 -117 -3 -127 79 35 -41 8 123 -103 61 92 14 77 28 16 12 -35 81 -12 -3 -87 -31 43 -32 31 -63 -128 -59 -67 -38 -63 -52 -125 -33 125 11 -36 -13 -32 -8 -54 127 -120 -2 -27 -113 18 -26 -7 -74 21 -117 -72 -96 56 -98 -31 -60 52 -19 4 15 36 -70 -58 -25 -125 -5 18 -5 -109 -44 -86 -25 -6 -126 -58 39 -32 -87 29 -99 103 70 117 127 -110 -90 -10 16 28 33 82 -66 58 -66 28 109 -119 127 108 63 127 68 30 -41 20 -63 110 -15 31 57 9 -38 -55 17 -114 37 125 -52 -104 -3 61 -127 88 29 -121 -68 14 -93 126 -9 78 -18 5 126 -10 -58 -102 -10 -32 36 -31 -40 -16 53 -78 113 -28 11 40 123 0 77 68 127 55 20 52 -18 51 -18 3 40 41 -10 124 -15 89 -69 52 -18 121 -81 -74 -18 -30 22 124 -95 -2 -47 27 -23 59 108 -127 62 68 -43 125 20 -53 -41 -32 -59 40 56 -39 -98 -29 62 39 -46 29 -65 28 -20 -118 17 61 9 62 -20 62 -30 -84 -62 -43 -47 40 122 -108 -52 51 -128 39 127 -62 126 -42 10 35 -20 127 -44 127 39 49 38 2 -124 -124 -19 37 -7 -3 127 -125 7 -127 -106 39 -76 -68 -93 27 -127 82 123 29 -100 22 26 46 -20 17 4 122 -22 -11 -18 106 -107 -28 -67 71 69 127 -63 77 -39 56 -26 57 46 1 19 127 0 -127 -59 92 0 32 126 -64 -23 15 95 -68 23 16 83 -53 -45 102 104 -20 -43 126 -9 1 48 -27 -126 -32 7 4 -52 127 -5 79 -50 -14 -126 -67 70 -117 -59 -54 70 -19 112 -115 -118 -126 -125 21 -16 -127 -36 126 118 -106 13 -80 -98 9 -55 33 -93 -24 41 -17 26 107 7 -32 15 -127 22 108 -81 -113 -128 28 -113 -58 10 -29 -101 -106 85 -4 -125 -114 -108 32 -113 23 -92 44 -102 1 -81 -125 12 39 -94 127 -122 -53 -108 28 -58 -83 -128 55 -48 -123 12 51 83 9 -127 45 27 127 126 127 57 -12 124 21 -54 27 67 126 -109 85 23 -72 14 -60 124 118 112 20 119 -67 38 44 124 -127 27 12 -40 -122 -15 10 46 -88 122 26 -45 19 18 -23 1 127 45 69 -102 94 -50 -44 -24 -11 -35 32 21 63 -80 -22 18 109 -29 114 -118 -56 120 4 -45 21 33 -27 75 76 -95 -14 -120 124 -35 -128 -55 127 54 2 41 -77 -3 -91 -55 -102 51 110 -41 -69 88 104 -80 -1 2 89 26 119 -12 63 -128 37 39 -76 59 -107 126 10 -127 125 -68 19 -45 18 -94 -1 123 126 127 -9 -20 94 39 -121 31 -1 -114 5 127 125 80 22 -45 33 -42 123 48 4 -16 -127 -56 -119 30 -116 -5 -33 65 40 -2 125 22 112 6 -78 67 -119 127 -73 49 62 127 -104 25 -21 -127 -81 40 12 -62 -119 -20 42 83 -96 -128 92 -40 -43 24 75 19 38 -41 -59 -98 -93 -99 -51 -18 -33 19 48 11 -6 -42 31 94 127 -72 84 -123 127 -47 108 93 57 109 111 111 126 53 -82 13 -104 87 127 -115 116 30 21 -8 78 29 -19 -38 116 -31 -46 63 111 41 -44 -48 -71 -33 6 -63 -4 -99 65 -23 52 27 -82 -110 26 26 104 96 -76 60 -100 -119 -118 -124 -128 77 81 -77 -93 121 -101 88 -74 55 -25 7 -44 26 31 -79 39 -2 29 -1 112 42 -3 127 -54 38 -50 102 -125 -95 -79 15 -106 6 -18 -92 127 -87 16 -6 127 6 24 -55 -45 -125 52 69 5 -45 -128 74 45 122 -56 52 2 54 -59 69 23 5 17 -44 62 36 -52 83 39 -94 2 -16 12 0 -127 -110 -128 -15 123 -18 41 68 -105 -104 82 -23 88 10 126 -124 -36 1 2 -6 120 119 40 88 95 96 -62 31 107 70 123 -107 127 -123 58 96 19 106 56 -111 -125 -47 61 79 -82 -26 -124 -8 -47 -128 71 62 125 -47 -66 79 127 124 -106 2 70 -5 -127 28 -1 80 -128 -14 -83 -115 113 122 127 -128 -52 76 61 -127 82 -34 11 120 115 -109 119 -127 127 60 40 -9 -51 84 -20 -122 123 -43 -57 121 22 24 127 72 88 -21 -7 -121 94 -35 75 13 119 37 -127 8 -128 -127 40 65 -90 -128 -127 -51 -126 -99 -118 110 -52 121 11 -124 -80 -11 121 -9 -102 12 89 56 -68 -25 67 -79 71 6 57 99 -51 -18 127 23 125 18 89 -40 123 -45 30 126 -15 -124 21 61 -5 -125 -73 -51 -27 108 76 -42 -2 -126 101 -20 56 -44 -14 117 126 -127 -122 48 -127 12 -49 -109 -102 35 13 -39 -49 108 125 35 -104 95 68 -57 22 -122 34 119 -94 -117 92 -64 100 -2 21 -127 -84 -2 -100 95 24 22 5 8 61 88 72 -128 12 86 90 25 -10 23 76 2 100 -18 24 -20 -127 -2 -14 -88 -30 21 -2 65 36 -96 -128 121 -66 -91 126 123 -11 -19 -117 -53 37 -116 -123 -73 -122 -128 -50 -18 -62 108 -44 -128 -125 88 68 126 -126 23 40 -62 -121 -20 11 -37 -48 -117 -34 108 -90 -121 -24 115 -21 27 -7 24 96 24 41 -128 -80 -55 2 124 -127 -103 31 32 -127 51 29 -9 126 -77 -13 -30 -24 -73 117 -123 94 19 29 124 -90 -56 -126 -12 -71 -114 -74 127 -121 96 -111 -117 -47 -128 30 -125 -45 -17 84 127 120 -45 88 95 20 -126 15 30 -87 33 -61 -68 127 70 -117 -77 64 110 127 -68 -41 -126 20 67 57 -1 -123 43 -62 80 121 -17 -124 -20 114 123 11 127 39 -46 -59 34 54 -69 33 96 91 127 -120 -64 96 -63 -15 -68 -95 49 126 -58 123 -72 4 127 99 -115 -87 -119 45 64 76 127 112 -117 54 -125 117 50 -122 127 127 -56 -128 -17 17 1 -8 16 -58 104 -120 99 -9 -27 -122 61 126 34 41 -4 -100 -66 101 118 -119 127 -123 -123 54 82 -126 38 118 -98 -30 -29 -11 37 39 -77 127 -38 5 -17 -127 94 -55 10 113 -75 127 53 -7 127 -3 113 -111 -62 76 5 -99 -112 -81 -94 -21 108 -126 -31 -106 125 -49 -98 -128 90 -35 101 -39 -76 114 74 60 -18 56 127 78 -81 -43 -106 114 117 -124 127 -26 86 -22 6 48 44 -108 23 120 114 80 -70 124 65 -19 48 35 -127 -96 15 10 127 -106 -118 97 58 41 127 122 -128 -127 -98 62 -22 -127 127 -18 61 -120 127 -101 32 -12 92 -80 -127 -82 -43 -62 127 -4 29 59 5 -79 -113 119 118 -29 12 69 94 -16 47 42 -95 -127 -45 -88 -123 13 12 69 -12 124 17 -62 3 25 -127 -45 104 71 -94 119 16 24 117 -66 -122 -78 31 55 65 127 -30 -1 127 23 26 111 -115 -21 71 -66 127 6 112 46 -127 -101 127 -59 34 -123 127 -115 74 -60 37 82 17 71 35 126 112 -26 -92 126 83 38 -81 75 -68 63 6 53 71 17 76 -10 70 116 -44 -87 80 26 126 -36 27 41 -6 127 -48 44 -51 23 16 -86 14 -49 -50 -49 -47 -103 127 -128 -60 97 79 127 123 124 7 -90 -4 126 -18 -108 -29 22 43 -121 -127 127 119 -123 122 -128 -43 -125 -30 -78 124 -18 82 126 83 -86 -126 126 114 -127 29 121 127 78 63 -38 13 8 -101 114 120 -127 -124 127 55 -2 -54 61 -35 123 -63 -38 126 -7 -106 69 111 -28 -86 109 -72 -65 5 117 30 -108 77 26 -38 55 125 41 13 -113 -64 117 119 44 -48 -115 -118 -22 -15 -12 49 98 123 -29 -31 46 -123 -83 -71 -119 67 80 57 -53 -106 106 -9 127 -87 -9 72 33 5 -117 -19 -38 -45 29 81 79 34 -2 -126 95 80 60 14 -49 -11 119 -127 -5 -48 -67 67 45 82 -64 -73 -120 -48 -88 63 -46 -69 -58 -10 120 45 117 -125 -16 10 127 91 85 90 60 -110 -1 3 -50 36 -20 117 -38 69 -125 53 -125 101 -113 90 -126 5 -19 -69 49 44 -19 85 73 105 70 61 -120 62 -17 59 82 117 -69 71 29 117 -12 15 -31 13 -74 -47 50 -23 -52 57 90 -58 -65 40 106 -53 14 72 -87 53 55 114 119 -96 -128 97 -125 -53 117 -113 15 127 -1 109 -59 -107 127 -125 -8 126 64 27 127 127 27 -49 -73 -83 99 98 -116 85 -102 127 104 -40 127 -18 119 -9 30 123 -77 -81 -73 5 -11 103 -36 74 -23 -127 42 29 28 -70 87 -56 23 116 39 -80 -120 -80 23 124 68 95 -37 127 -107 127 62 -68 27 -66 -126 7 -15 11 126 126 117 -8 -110 127 127 38 -127 -83 127 20 -20 -25 24 71 44 -53 -49 -42 98 111 123 112 -128 -2 -7 124 -78 48 89 125 -122 127 -74 126 84 87 85 -67 -20 88 -25 118 36 -41 87 120 -40 -122 66 66 -78 -1 127 -123 -60 -103 54 -56 -128 -61 -117 -120 -45 103 85 -105 86 -118 -35 -122 -126 -122 99 127 -51 73 118 -125 127 -60 -66 -41 -28 -86 -35 41 120 -39 126 -122 56 -104 -39 -95 -19 6 118 -91 42 39 77 -50 -41 10 -96 18 38 42 79 -35 -122 21 49 -118 -124 -109 -61 94 -126 -30 127 -31 -103 109 -76 40 -69 5 24 34 42 -3 125 59 91 -12 124 -121 -72 13 -82 -17 90 49 11 -25 -116 127 123 0 1 -57 50 16 82 81 -121 -122 36 73 -126 12 -111 14 26 126 -37 -9 127 -24 -127 44 29 22 -21 14 116 73 107 -38 127 39 -7 -92 111 60 97 -94 -78 70 119 -124 -118 46 1 -25 77 -10 -116 -122 -125 35 97 39 -125 -86 -128 43 -14 127 -94 -70 -8 -87 127 -121 26 79 -32 -105 -8 11 -43 -16 -73 -31 6 -24 -112 66 -25 106 -73 -7 -127 -98 126 127 113 -127 -123 -102 -125 -65 20 -60 -128 58 74 -89 -53 -106 -59 52 22 -127 -35 127 115 -51 -8 -39 -6 -90 117 -127 22 -32 127 25 -128 46 -11 -11 -127 -107 21 -51 38 118 -27 -12 100 10 -42 -111 -54 28 -63 -22 38 -43 -67 40 -86 123 -125 -33 -90 -64 -107 -127 -124 -125 47 118 -14 -40 -55 45 -54 -26 -111 -93 -124 -122 51 -128 -12 116 18 -121 -21 67 -15 -83 -128 68 65 -55 127 43 107 -45 -128 72 90 -128 -6 -118 -128 36 119 127 -125 -128 -98 -49 -65 127 -46 61 60 34 -24 23 80 92 -41 -5 -79 -95 -26 -38 -87 -37 -1 76 -127 -44 -14 121 -12 -122 2 47 -21 47 5 -26 -29 -24 -63 11 20 85 25 -92 -18 73 28 -57 74 -53 -2 30 36 -19 -107 -18 -79 -25 16 118 32 22 -118 71 -41 38 -62 1 71 -17 1 1 94 94 -59 1 -17 107 68 39 -49 24 0 -91 33 20 11 41 -32 33 -9 0 39 -13 -115 -52 -83 -4 12 68 -50 63 16 -29 -29 127 -12 127 -58 119 -46 -5 -128 76 -27 18 34 -47 22 21 -121 -63 -23 -39 58 40 21 -5 36 15 25 -77 72 16 4 -18 -14 -81 54 -16 -128 -16 -127 64 -16 -15 28 126 25 8 7 10 53 26 -95 -106 -7 -62 -69 127 -13 54 52 -101 -21 -7 -27 -32 -76 82 8 -97 -17 -127 28 22 -41 12 122 49 31 -32 87 -22 -18 -37 -16 14 -16 -57 75 114 -92 33 127 64 -53 -56 -127 -32 -47 -1 -53 -22 4 -96 50 91 112 24 109 127 109 100 -67 50 111 85 -88 43 -71 -50 -33 5 127 7 66 -125 -61 52 -44 14 -49 57 -54 -127 -27 12 29 59 -128 -117 -52 -81 -28 -97 12 -127 -20 106 88 -100 45 -62 121 78 -107 16 -113 71 -20 46 41 18 -34 21 -8 55 -44 48 -22 4 -119 50 111 30 -65 115 -47 15 74 -101 8 55 -68 72 -54 101 10 26 18 62 -63 56 -40 -35 4 -27 -58 -6 48 56 21 20 63 -91 21 -12 79 -53 3 -14 12 -99 -23 -127 -17 110 6 44 15 -28 -111 2 -42 -90 -39 -27 67 32 66 47 -66 -16 -52 37 -53 12 21 124 71 -81 69 127 -113 21 13 -117 -5 -45 99 -4 -30 -39 20 -17 62 -1 -126 75 -32 43 55 -12 95 -40 -13 -17 -26 -20 -65 53 11 127 -60 61 122 -110 -26 52 65 108 83 12 127 67 120 -47 47 27 93 -47 -26 27 55 28 15 -126 74 -69 122 -69 -64 1 -88 112 13 8 73 -7 -96 -27 -108 -74 -78 78 109 87 53 -11 -3 -36 -52 -35 -17 -65 -15 -9 30 -45 -17 27 102 -120 -27 -118 -71 17 -76 100 64 -80 54 16 81 -37 -118 -30 63 27 53 119 -74 -57 -88 -92 -52 -16 64 76 81 104 -26 -4 68 -54 -127 -48 -113 4 19 25 51 -63 17 99 47 -74 127 64 -73 -58 16 -5 80 -127 -57 27 57 115 122 -59 -6 67 127 31 -120 -44 -52 -126 28 -51 25 115 97 80 -107 55 65 41 71 19 -9 20 -104 35 -99 127 72 115 23 -42 -128 3 45 -55 3 45 -126 -125 -120 51 126 -40 42 110 -60 -95 -39 49 45 52 -71 -122 113 -100 -98 127 -78 -102 31 -20 -63 -128 -57 79 -4 -17 88 57 -32 -23 -117 15 -45 124 -67 56 -16 -50 -39 -79 14 -86 9 -3 56 -112 41 -102 -43 127 -48 -108 -29 -95 88 -46 127 42 71 127 76 54 -104 84 29 -123 34 -13 -44 92 76 -86 -99 15 -68 -120 -119 -99 -128 25 -20 -25 84 -62 59 -48 -34 35 -55 -114 2 67 -128 55 -69 29 -122 -10 72 -125 -31 56 120 -113 79 127 127 -85 -62 39 69 -107 83 126 -11 -106 58 -73 109 7 -25 -52 40 63 11 -101 -50 83 111 13 123 -35 34 72 83 93 78 -50 -40 127 -71 -94 -46 -29 55 67 35 -120 -70 126 94 -34 -9 52 4 52 -11 26 -8 29 -10 35 -22 -34 -24 -19 75 41 -56 55 26 -122 47 -119 -123 36 127 -45 -106 42 28 -90 -9 -76 91 -8 -19 -28 124 -120 51 61 59 -10 -90 -4 15 16 70 123 57 113 44 -16 89 -47 6 -19 37 -80 -25 48 92 83 2 -119 67 -41 -6 127 98 42 -121 -93 -7 -72 -14 -121 125 -58 4 80 -101 -27 51 42 14 73 54 69 -127 127 -128 127 -80 118 91 5 -116 -48 -1 81 -11 -2 -127 -126 -32 -99 42 121 -25 -114 96 -64 -63 126 -50 90 50 -54 81 41 -22 -114 123 -107 -99 -125 37 6 -52 115 110 38 127 102 51 -56 71 12 102 90 32 -47 -3 31 83 123 -75 127 77 110 127 37 115 -58 -61 25 -47 124 -2 -61 50 44 64 -8 95 -78 -95 -36 -127 -123 7 21 124 -3 115 -58 -25 -120 124 -12 127 33 36 -83 54 115 -13 6 108 -10 -4 -28 -9 -47 -14 38 -24 -78 33 37 24 -76 61 5 8 95 40 -53 -22 32 11 9 -45 64 -68 -52 -21 17 -51 -89 67 -34 116 -127 -4 -35 -26 -53 -2 72 33 8 42 23 3 50 -114 19 46 6 -84 -17 -111 44 117 28 -30 -29 -127 -41 -5 -31 50 78 91 -65 -11 20 1 -26 -36 79 -9 -14 -7 127 26 -126 33 59 33 28 -51 46 42 -6 -40 30 40 -63 -15 8 -21 95 64 101 46 11 -57 -103 50 -1 -54 92 47 -37 -37 -43 -42 -113 -58 100 -26 -38 21 87 -17 108 -64 -45 -27 -118 97 -15 7 3 25 52 47 -121 94 -28 -119 -73 44 -70 -66 42 30 12 88 125 -80 13 18 -4 -28 52 16 -45 -88 -21 115 -83 61 -41 98 -5 -41 -42 61 22 -58 -122 -16 9 -38 -36 12 -87 39 92 88 -71 -23 36 1 -19 7 14 61 30 34 -77 -10 -17 -18 70 18 -78 -104 88 38 -115 11 89 81 -104 5 32 -15 -2 -29 43 -100 90 35 13 -102 78 123 -65 57 63 12 8 72 -71 -101 -38 -1 -5 -64 -30 5 124 -42 -34 16 56 69 -5 -5 -46 -27 78 -17 43 -21 50 54 -60 61 96 7 -59 111 62 13 12 -2 -3 32 -37 48 28 -8 75 -25 50 19 52 87 9 -62 43 -25 69 126 -37 -45 48 -37 -10 -20 -52 56 -23 -4 18 66 13 36 72 -13 9 66 28 -124 -1 36 -76 -36 -65 56 41 -35 -10 26 -22 -69 22 -97 70 -54 -1 12 26 92 -38 109 -85 83 -12 -29 1 20 -51 27 -27 -106 33 5 47 43 19 -103 -22 -12 63 -62 33 -61 -104 -13 32 -38 127 -27 4 -46 -3 -115 -74 -27 -50 45 2 5 -4 -51 -92 -102 29 -15 -14 29 -42 68 43 -10 15 -19 56 -37 114 -9 51 -65 -114 45 -64 -101 -40 -45 -10 -69 28 5 72 -101 -118 52 123 -23 8 69 6 12 3 14 -16 -33 32 -62 34 -13 32 122 -81 -72 -7 -64 -10 -5 89 25 -19 -20 70 -10 -43 -88 8 -56 -34 -90 89 -68 -25 12 45 121 16 21 46 24 94 74 44 -37 -40 -60 50 19 28 -112 -82 23 -4 127 41 118 16 63 -35 44 -86 47 -94 -103 10 -11 -48 -56 109 -39 16 54 72 127 30 90 125 18 -127 -46 -7 -83 -19 -43 -92 69 47 -12 -111 -45 120 67 108 126 -55 -107 5 -116 -89 -120 38 -7 -106 15 -8 6 40 32 34 125 -19 -75 -17 123 -24 32 24 -107 109 -105 55 -32 88 61 -83 -89 -108 31 -65 -28 -28 -22 -70 114 127 100 29 -11 16 48 20 64 -45 109 41 -38 42 16 79 -54 -40 -50 127 75 58 40 60 39 -100 -26 -20 -57 -55 -32 71 123 -23 21 21 -10 75 97 -112 -26 -16 98 6 -14 26 -43 -13 13 -12 36 -80 10 5 25 -90 -47 -48 -54 -55 15 36 -81 89 83 1 91 -69 67 -62 -126 -15 -26 33 -125 -22 127 24 -73 -117 8 81 -53 -13 -8 48 33 9 26 102 -66 -9 35 127 -95 -19 6 -5 -1 -20 94 -19 -29 -41 47 -8 -120 -53 -36 -70 8 120 -125 -8 92 -75 126 -40 65 67 24 100 24 -3 -45 -87 -48 -20 10 19 127 -42 60 -49 64 127 114 79 3 8 -127 124 -69 -62 21 -20 1 -62 44 -24 -10 95 12 44 85 -36 27 -3 62 -31 -41 105 -77 -47 7 -45 35 -70 -20 -10 -27 -13 121 -9 14 -64 -95 -105 -8 -7 -98 -48 -79 -62 52 94 24 -88 -43 -80 120 -39 -114 -4 26 108 26 63 18 10 -42 2 -74 13 -52 -54 -79 0 -120 50 79 -118 75 5 56 25 4 -97 0 -108 -125 -42 70 87 -64 43 38 16 42 24 25 -28 105 30 52 -93 62 -52 63 64 72 19 -80 -7 -54 99 44 79 -96 29 -33 -106 43 42 70 -56 4 97 44 -128 -9 116 -57 -27 -72 106 22 -26 -67 -8 120 -22 38 24 -72 20 65 62 -128 -94 87 -97 -4 -6 74 -116 -63 45 -2 -71 6 76 -6 -5 33 -98 -40 114 -98 95 37 104 70 -1 10 34 -18 68 120 -64 38 58 15 -14 -29 -5 22 90 31 28 -78 21 -30 -49 -14 120 89 -92 33 -48 -25 4 15 -57 -111 41 102 1 -6 -81 75 -38 -65 122 -128 29 53 -60 127 -96 -64 -84 -80 -125 -128 -39 -124 -11 -121 -59 77 -110 0 21 -117 24 64 6 -29 -123 32 36 -57 -30 5 17 -106 35 62 -128 60 -16 -57 -123 -32 -10 80 -74 -22 -24 41 127 -128 127 -128 44 69 116 93 -118 -30 -74 -125 -42 61 20 -45 -36 16 -127 71 31 38 15 88 -124 -39 69 123 74 -120 27 63 -10 -64 -98 53 118 127 -83 -101 39 127 -23 -30 -113 -105 -124 10 9 -127 -75 -11 29 -77 127 3 -5 -46 -105 -127 26 43 -70 -128 106 -96 -13 40 107 27 57 9 -90 65 127 95 35 -42 -96 -34 -86 123 7 85 125 -126 -33 -128 126 41 -127 -128 8 -3 -117 -45 127 -122 79 46 -50 28 -101 4 -19 -44 -116 -128 30 -77 -38 -80 67 33 -121 -43 73 -119 98 70 39 29 -22 -46 56 -10 -24 -18 116 6 1 26 -128 75 -32 -22 -35 13 -36 -26 59 111 -73 3 126 -128 126 -122 62 -3 22 0 -64 16 101 15 10 -62 34 -38 -82 -14 -127 37 -12 -56 -93 42 -115 107 -42 34 -120 -98 -50 29 -79 92 -121 -89 126 127 0 -115 101 127 -13 26 -123 10 -61 115 85 -126 17 -23 125 -13 127 47 -48 -51 -123 -124 -38 104 -62 -127 -89 -90 -9 13 89 -86 -44 48 -116 21 127 99 -70 3 -8 -21 4 -84 125 -127 121 87 54 -128 122 107 -56 -75 93 -16 -118 66 56 127 123 26 10 -29 102 25 -47 85 -4 64 33 115 -44 83 3 12 92 28 52 -49 61 -33 -52 87 127 9 8 -45 89 65 124 121 -128 79 71 -66 0 123 -34 -92 101 91 127 0 123 -9 126 -58 115 -69 13 -34 -108 -111 -67 71 115 126 79 68 81 123 72 -60 53 -119 -15 -37 -74 -22 102 113 -128 -127 96 123 -103 -46 -50 -52 117 111 -30 -88 19 -49 127 28 20 79 95 97 56 68 100 73 122 -109 109 34 -53 110 -2 15 -27 117 111 -24 -39 113 -105 88 29 -53 123 -124 -15 4 127 57 -126 -59 -66 -5 -22 119 127 -126 54 33 -127 17 100 107 -127 18 3 11 -28 22 14 94 116 20 29 -20 123 37 -3 -42 -24 85 -55 72 26 -78 29 -5 -100 126 56 105 -23 -66 -10 -27 49 120 59 26 -47 -101 -127 -104 127 -9 -110 -32 121 72 124 124 -95 -128 95 -107 -28 -111 -77 36 127 -127 1 -68 -62 78 6 -126 -62 -74 -110 -34 -106 118 -52 84 -43 19 -34 -124 -119 127 101 25 -23 -9 -127 -39 123 91 64 -51 -56 -122 61 72 -70 -48 -9 -17 84 76 -128 45 101 21 -119 126 30 44 -27 -21 -122 22 127 55 -54 92 31 -28 38 -27 -47 60 38 48 70 -21 46 -27 34 -14 116 -30 -109 102 108 46 54 90 120 120 -127 38 -127 -33 56 -1 -125 34 -33 8 -5 -60 30 -1 121 93 30 69 65 14 -128 104 -15 -41 84 -29 -45 -10 94 -29 -37 -28 57 37 125 -110 44 -56 123 17 -7 -33 -84 43 -127 38 93 -128 33 -125 121 -85 27 62 -62 -128 -53 -42 -36 -123 105 112 127 -94 -39 -92 -37 67 18 -122 -56 55 -92 36 5 96 -45 0 -120 -24 -62 -90 -119 3 28 -1 -126 -28 -94 -96 109 -93 -2 -34 -74 77 -66 127 126 13 -31 -8 -86 -128 12 55 -58 -37 78 86 8 -30 48 26 -74 35 127 -16 -51 33 -9 -3 54 11 81 -19 18 23 41 6 22 13 16 -8 -81 -118 47 26 55 -70 -59 -82 51 66 25 67 -59 16 16 0 32 -1 4 61 26 -90 -39 5 32 105 -47 50 33 -118 104 -33 -118 -5 -49 120 22 94 7 127 -124 -115 49 -100 65 -116 -49 -58 5 -14 -18 0 -111 31 50 -40 -53 -128 63 31 53 -120 32 -19 74 -77 -12 9 -111 -68 119 -62 127 -67 89 -115 40 41 99 -75 21 116 -28 55 49 -50 -50 9 -37 21 81 -6 53 125 -30 -69 36 117 -113 -101 18 26 -44 1 -117 -38 -95 126 126 53 78 127 -60 -58 127 17 5 -23 55 -36 24 15 5 41 -96 35 54 0 -22 27 86 41 13 10 68 -24 0 114 118 38 17 9 54 -60 -31 -56 -8 -47 -74 63 35 73 -17 83 -36 -33 -44 34 -27 -30 -94 66 38 30 30 -14 32 -1 -27 -32 -97 8 -51 -6 -13 6 -15 38 71 45 110 -16 34 -53 73 23 31 25 -14 19 -29 -76 34 40 -3 -21 61 -9 127 -14 -12 126 47 10 71 37 -8 6 127 -36 41 -32 16 -17 15 -45 6 35 39 -63 47 23 -53 11 104 2 32 95 10 106 -38 -14 -120 35 22 93 21 77 85 -7 -36 33 5 -37 -33 29 28 -25 -86 -110 65 41 -7 -66 -3 49 11 -17 106 41 -29 -13 -3 13 1 37 56 -28 -29 -1 -35 62 2 -21 76 49 -122 -39 -3 -23 -66 88 -19 15 35 14 -29 -4 -41 32 -20 -13 -48 51 4 8 -22 -123 31 -17 19 8 2 -3 -110 8 2 55 23 -59 18 -49 4 -21 89 4 41 -16 -27 14 -12 -57 23 63 19 41 -12 5 -72 -32 -17 -27 -73 -42 22 86 -78 -125 -20 31 -12 -3 -51 13 -53 -102 -51 27 -21 13 12 57 -30 -82 6 -28 28 16 -69 -15 12 -18 45 35 52 10 -55 -122 -33 -29 8 -54 -8 -9 60 -49 55 -62 26 38 8 -22 31 -17 -22 -48 -18 22 0 33 68 36 -32 -2 -24 2 -27 120 -26 2 42 -28 -14 25 -20 -20 7 -10 -12 33 -67 -42 7 -128 -5 -7 10 -15 42 8 7 43 -1 -39 -39 -16 -45 -15 1 31 36 -7 -88 1 -64 21 -19 6 -40 -11 2 -28 -16 -24 -25 -11 -50 46 -1 61 -45 -15 62 +-65 -18 10 -5 -42 31 -1 77 22 -4 82 6 -64 -31 6 -7 89 -78 -119 -45 20 -93 64 17 -17 99 -39 -30 22 29 -55 92 23 33 60 43 68 115 -38 49 -8 86 61 -25 5 -59 -35 -6 -18 8 -6 28 37 89 -44 51 -22 -23 -20 -68 -38 104 12 4 40 13 87 71 -91 99 -29 -23 48 52 38 -2 -76 -45 -39 -28 -92 -2 -8 103 -27 24 77 68 -27 58 2 -26 62 22 -27 22 -33 -14 74 48 53 -13 45 94 13 -27 -4 -4 93 -10 -75 -19 9 94 -39 -10 30 80 39 -35 -61 -55 -39 -39 -65 12 67 -59 74 -19 53 40 -61 115 -61 58 -127 -7 -33 -44 58 3 46 29 -65 -1 104 -8 -18 7 -75 55 -13 82 42 -37 22 29 -82 3 -63 51 -31 37 45 48 31 -48 26 -7 -5 19 -60 83 -3 29 -50 -20 12 -128 -68 -24 18 -73 -56 29 32 -55 30 43 11 -5 93 -32 105 5 43 -6 66 15 -11 -38 70 -15 13 -66 -77 -40 -85 -44 -7 120 -31 46 -43 26 0 -13 -13 -45 -116 -49 -6 102 28 -19 -58 -10 19 -29 -27 -51 -8 -128 -56 -14 -127 1 -9 55 95 110 -4 17 103 -18 -38 25 43 -102 -20 48 -127 29 -18 48 56 -41 -36 -1 3 4 -46 -67 69 60 42 -12 0 20 -67 31 -110 3 93 -17 5 -91 -21 33 -15 15 -28 -61 -55 -60 -31 -40 71 -34 95 7 -12 84 8 7 10 93 52 27 37 77 5 -2 -28 -23 -1 110 127 82 -41 75 67 71 65 55 29 -82 29 70 87 -23 22 -14 23 -62 41 -39 -17 20 -16 33 -1 9 -25 -35 -27 -49 5 -97 -33 23 -8 59 56 12 -75 -26 -26 69 52 66 57 29 -18 -25 -11 25 -40 29 14 -52 -25 -85 -27 -24 -36 -74 -19 -116 -71 53 28 33 -37 58 8 -25 -18 95 7 -27 83 35 -23 -26 -10 -35 54 58 21 -78 68 10 47 41 -81 48 12 51 -18 20 -9 61 19 -26 16 -98 18 40 -11 -65 36 -67 40 33 -14 51 -26 -52 0 91 59 -66 93 37 -69 -127 -25 -35 -16 -110 -10 -69 -23 -54 5 -58 29 115 -29 -18 -28 -34 -25 -6 31 3 39 16 -32 1 2 70 -20 -24 -4 122 15 4 100 117 -64 16 22 40 -34 82 -18 15 37 94 23 8 99 50 -20 42 27 7 -23 -34 58 -64 -11 34 -23 43 48 104 84 -22 -63 7 23 27 -28 -93 19 -29 67 -69 9 -20 -98 6 38 31 0 22 35 9 2 16 7 16 48 -24 -86 27 -45 73 -116 26 16 -21 11 42 57 -125 46 11 52 4 -46 14 -69 16 69 30 6 -23 -82 -31 109 -39 -63 -30 -127 -30 -68 44 -89 -47 -24 -3 8 9 31 -43 -54 -40 -51 30 21 -63 15 6 -107 11 -88 -128 -22 -122 -17 67 71 4 94 -4 -36 -31 20 -22 -50 -5 22 37 -65 34 22 -44 7 39 57 67 -5 -9 65 21 -31 -99 28 105 -115 89 -59 1 66 -46 52 -57 18 -35 -47 31 -27 -24 7 -80 -33 57 54 12 -15 -4 -60 -81 9 -5 -23 41 63 -122 -77 3 -57 74 -5 -9 44 -102 39 18 -8 0 -31 61 18 -18 -87 -36 -31 33 -106 -13 52 -80 -8 -14 37 14 -34 -8 -22 -45 6 -58 13 8 59 7 16 6 12 -5 57 -53 -44 59 -13 -92 -28 18 -28 11 64 -51 -7 34 23 -72 38 56 -21 -91 -44 9 -25 52 24 -39 7 -35 11 11 -7 -24 13 -4 -23 24 24 -87 -17 -59 -11 43 22 -84 16 -39 6 3 72 -20 47 82 -10 -54 -11 -55 83 -28 -5 -29 67 60 -38 -83 -120 14 -46 56 14 69 4 -57 44 16 -8 15 -23 -35 -55 -65 -21 9 50 86 -54 69 124 114 -128 5 24 17 -46 -3 -105 29 91 14 -7 -32 -46 69 33 -12 78 -3 -25 19 27 38 85 33 -61 43 54 45 -93 30 47 9 -26 -68 -58 33 2 -61 -95 29 38 -19 -2 90 26 48 55 -48 12 42 50 23 -80 59 -10 75 -31 19 -125 46 -74 66 -6 44 -115 86 -67 0 2 -87 -37 -53 47 13 23 46 -70 10 -37 -45 -66 -2 -38 -9 -14 -74 -1 22 28 -36 -48 -1 -31 68 68 44 46 -63 1 89 44 114 21 2 17 -16 -61 -24 53 -38 -33 -4 25 39 -52 -61 -15 -72 -42 -41 71 -65 20 63 91 16 9 -38 13 -40 -39 25 42 48 1 31 -97 46 39 -17 64 -56 -1 -42 46 2 67 18 -15 25 -6 -61 -13 -45 24 0 -71 -128 -21 -83 -31 60 -25 40 -24 63 -50 -116 29 6 9 -38 36 39 28 -20 -17 34 38 -56 18 29 -75 -28 -25 50 12 46 32 21 -1 123 -50 16 -13 79 79 53 -32 -50 24 -1 -5 2 -6 -13 15 -125 1 19 -6 0 -44 85 9 20 -65 0 -23 -25 84 61 -4 87 39 69 36 40 32 16 -2 -8 52 -46 32 -6 9 -24 -8 9 -60 -34 27 -65 82 -9 106 96 -106 -49 -94 5 -12 -12 -19 -12 102 23 -72 -36 3 49 2 -39 101 -11 -27 -73 -102 -46 16 -102 -56 59 -111 88 92 32 113 97 -80 43 105 53 63 -40 24 -51 113 -54 -121 -41 127 9 22 73 127 77 -36 -45 -76 -73 3 14 58 4 39 58 43 -33 43 46 41 -43 -111 -82 -59 -28 -83 -123 -30 26 8 36 -15 -44 -4 127 49 -24 50 63 17 32 -56 38 38 22 -26 49 67 29 -25 51 8 30 -36 1 3 54 44 -29 -104 -104 -128 -3 114 2 -120 48 -114 6 41 64 35 68 23 -49 -6 12 -22 -52 -22 20 -126 -5 -11 67 -23 -68 -59 47 -35 -55 3 -10 25 -38 92 -28 -16 3 38 16 97 -59 -49 21 -127 5 104 45 -72 39 113 -21 115 3 -93 -16 -102 -42 17 43 -60 32 -71 78 62 -8 89 59 13 51 -4 20 -33 -3 -106 42 8 -18 105 -79 34 94 34 10 31 67 48 4 122 21 -46 24 22 1 48 59 11 60 14 5 23 3 -17 7 -43 -72 6 -1 -62 -87 -9 -28 -25 57 92 -36 -11 -38 31 19 46 -27 32 -11 4 35 -59 53 15 51 -33 -33 13 2 -24 -16 38 -50 -4 -12 -41 -117 -125 17 -116 -10 -55 -45 -32 32 -8 -50 -69 53 -55 49 28 -41 -9 29 91 98 -57 22 -24 -15 26 -20 8 57 21 -123 -32 86 -60 31 -38 52 58 -18 23 -47 127 -9 -59 -45 121 -44 24 -32 24 12 -53 56 98 39 71 10 55 -124 107 105 84 48 74 51 45 -37 -3 59 26 -14 27 -69 9 -32 71 -80 -42 10 62 7 -2 -36 81 5 -25 -8 33 -76 -36 3 -127 29 -44 -6 -45 98 -1 -58 -118 -99 -60 -4 -21 -15 -25 -124 -19 -44 -46 44 49 2 31 -54 64 -66 -19 23 -87 -69 -25 -12 88 53 -13 -27 77 55 21 -48 62 33 18 -71 6 1 -11 34 -40 -35 -20 45 5 -48 64 14 -32 -111 -18 -35 -68 24 -38 43 1 -97 -109 -43 -30 -123 42 60 24 -44 -82 -67 -9 -32 -34 -91 -25 -20 -54 35 -24 24 80 -34 -22 -70 22 -56 102 5 90 18 -39 7 -46 89 -32 -57 39 -33 31 -48 127 69 63 -30 24 -17 -127 30 28 -84 -17 85 20 41 -26 -12 61 7 95 39 96 10 -45 38 -25 -6 -20 -13 51 11 -50 11 54 -9 -10 33 -19 12 64 57 19 -76 -34 -38 51 -13 11 -21 108 -18 45 -19 -31 32 -4 19 49 48 -6 -120 112 55 -56 26 41 -105 20 16 -101 -22 -13 -14 49 -58 19 -102 -59 -32 56 -17 4 41 -82 40 6 51 -71 -6 58 -33 17 -24 -50 -64 101 -21 -80 23 90 -2 10 49 41 -53 -1 -13 110 -46 10 -26 13 113 -61 32 -31 -75 -94 -24 106 15 42 -24 -55 -14 -33 21 -48 -9 11 24 53 -42 0 16 125 -19 82 -1 -83 -72 -33 -36 -19 65 -35 43 -53 56 -14 -36 -16 -56 53 125 -22 20 -5 118 55 46 -95 -24 4 -60 -21 -13 21 -27 7 -32 -2 -17 -2 20 -23 79 114 -10 -18 -65 15 54 -11 108 84 -7 50 -35 31 -89 -12 23 -20 59 -21 -78 56 22 -12 50 24 -48 -24 40 -60 -60 31 -11 -17 97 102 -9 -77 2 32 5 28 -16 -62 34 -109 39 1 -17 36 0 28 6 49 -46 -127 36 -29 61 -35 43 -104 43 41 54 -20 44 -6 -17 -6 48 -8 -65 44 -7 22 8 -30 -62 -24 71 31 -17 -35 -40 19 87 -58 64 110 -48 -33 -4 -68 26 6 -42 126 -119 95 11 50 41 48 -13 -46 31 -17 -24 73 -69 -35 44 57 -61 78 70 86 -76 -44 -22 75 44 50 -41 12 -49 -10 56 114 44 61 -13 6 -24 -23 -2 14 -27 25 29 9 -26 -67 -39 4 47 -3 11 -23 -127 -95 -17 36 -34 -20 -38 12 -84 -45 65 62 23 6 -28 -57 7 -34 3 90 46 32 -68 -6 -39 -16 22 41 -12 -32 57 13 -13 74 4 65 -15 -13 -12 -62 -9 33 97 -50 9 -26 -31 38 14 26 31 39 71 37 -9 -76 -39 86 -9 -58 10 -4 -11 -31 -60 -92 -23 3 94 -8 75 73 0 -27 44 30 49 -41 -34 -61 -59 34 31 17 -41 25 -27 -70 11 7 -13 34 -127 72 19 -43 -66 -5 32 37 19 -55 -20 -2 -69 -44 -85 2 38 45 -2 54 -33 -49 -25 -4 -67 -111 85 -27 101 -5 -24 -102 -62 47 8 13 112 -72 86 37 24 -28 -25 22 61 -102 29 52 -21 -46 44 -11 -58 71 95 106 -10 2 -11 105 -23 15 -41 47 68 9 36 53 42 33 -75 107 -62 58 -43 -42 -11 5 -54 12 -82 39 -20 -80 12 -23 14 -107 33 14 12 -7 24 68 -27 73 -58 -31 -27 -25 16 62 32 -20 -34 -66 30 -21 -8 20 -8 -43 -16 31 -58 30 -7 7 24 -105 -44 23 78 26 75 -97 9 -1 -53 8 -13 -37 -27 -1 6 -85 -2 17 80 -38 -20 0 42 -7 -71 3 -27 -31 7 38 13 -35 -18 -72 -47 26 -14 -72 -57 4 41 -21 17 -16 -38 -38 6 -74 -28 -89 77 -6 43 64 -42 -55 2 17 -22 -3 -9 -103 45 -13 6 28 -63 -77 13 -80 115 65 11 -22 -61 -45 25 2 7 80 60 -5 48 3 -48 -5 24 -52 -57 -7 -85 30 43 -15 28 -40 -33 36 -8 -94 -40 -34 -34 4 -46 -2 15 -84 -5 3 8 -5 -19 40 -33 -96 38 -2 -28 -13 -26 -76 46 -32 22 66 92 18 -45 7 12 -49 -42 -12 2 7 38 -6 -85 11 -22 -20 31 -64 -21 -31 76 46 -41 -92 15 -46 -81 -124 -32 -31 -21 -56 -32 -42 49 60 -72 30 31 24 -14 23 13 -41 -2 35 19 81 -28 -77 -54 46 -38 -126 16 21 -65 25 -65 -5 -13 43 87 -15 -63 27 94 -117 -6 5 -125 -40 27 -98 41 16 3 18 -62 45 -33 -92 12 18 23 -96 -31 76 69 -9 -70 -51 75 18 -17 5 -83 -44 42 31 49 33 -56 -72 -37 -31 -10 -35 -85 -68 -63 -11 -70 49 21 119 24 -113 -25 -52 47 -5 38 43 63 79 -40 23 127 25 -47 -21 62 -19 -49 -35 -77 -77 37 -32 -20 -45 -80 10 -104 -22 -22 -70 32 -6 -64 -55 -5 37 17 -16 -46 34 -12 -10 66 -49 -51 -16 -29 -15 -33 -69 32 -20 -39 11 -41 67 2 -26 -96 -18 61 -31 -38 9 -40 9 2 -31 -33 -25 9 47 19 15 123 66 -48 67 -64 70 -22 0 -10 29 -42 -56 6 27 -6 30 -66 -10 54 -15 57 -45 26 16 19 -4 -74 -28 53 77 68 12 111 -27 23 59 -32 -115 44 -15 4 9 41 -46 31 28 15 -13 -127 12 62 -19 -71 2 22 -19 41 -15 -65 52 -48 -51 -30 16 -44 -14 27 -2 -46 39 -37 18 -29 -36 92 -37 24 -128 -7 -20 -82 -88 10 127 -9 0 -11 42 45 16 -44 106 -9 -11 -14 -109 34 25 33 -49 37 -9 -24 23 -50 -51 -71 -10 -18 -9 40 -74 -4 30 -30 -6 -11 -92 -51 9 -37 27 -55 18 26 32 -64 45 -17 -69 41 27 87 -2 -30 -28 74 13 32 -16 -28 -15 -26 52 10 -48 38 -79 -1 10 30 48 12 58 -37 56 -21 67 69 -41 66 40 77 -1 -25 4 -46 17 14 84 -5 37 29 20 -68 8 -17 49 10 19 -14 -15 -8 -37 -12 -20 -40 -3 -51 -10 35 -22 0 63 127 -14 31 -14 -28 -46 -34 -63 8 -14 -117 32 -9 41 45 19 -17 33 12 -18 26 -12 -19 -16 46 38 16 -60 -19 43 16 63 27 54 -81 28 -2 19 -65 29 -54 119 -13 -6 -29 39 46 -45 9 12 -25 -8 -35 -36 50 -56 -56 -40 -128 4 -65 1 11 -128 -40 -3 -6 26 57 1 58 19 7 -30 26 74 -44 -13 22 -12 -25 -10 -49 42 -22 5 40 -13 -4 95 24 14 -16 26 112 79 17 85 -83 -86 15 36 25 103 40 -10 -7 30 22 -41 32 -15 47 -16 13 54 9 57 -4 -43 -27 79 76 57 60 0 41 -126 55 61 -67 -25 -20 119 120 -37 27 -47 -3 6 -36 57 16 95 35 47 12 -116 39 11 72 -2 6 14 -63 39 72 21 64 26 -87 108 -6 -47 19 9 44 -2 -26 -14 120 -21 -40 18 -12 41 15 37 26 26 31 73 17 4 26 51 -5 2 68 32 60 68 -91 16 30 -125 -18 11 -45 -66 -10 -58 -3 -95 -128 2 42 -71 -29 -27 51 27 -63 -4 52 15 -81 29 105 24 -84 8 13 -1 16 91 -49 -11 -20 33 -28 96 -97 94 28 -27 97 43 51 90 41 -64 34 11 30 64 6 117 0 -32 -125 -106 42 5 28 116 -18 26 60 106 -41 41 81 -31 43 87 -20 39 40 12 13 -19 -23 32 -66 72 -69 26 2 71 -110 -63 -54 -13 4 27 41 0 -69 25 26 30 -43 21 110 -37 -121 13 9 29 23 -58 12 -17 58 67 -83 -69 48 127 73 62 6 40 61 -16 -39 28 -68 -3 23 44 22 -93 99 54 18 55 -34 -60 33 18 6 -8 120 -2 -68 99 -85 -33 -102 29 -3 -43 -18 12 37 2 -27 -4 -25 -18 -38 46 65 -33 13 -62 -94 -36 -46 -57 5 40 13 -72 -10 -111 49 127 15 51 49 -29 46 49 -124 34 -28 -49 -4 -33 -37 -42 -49 23 89 1 44 -19 92 78 -14 12 39 -9 4 -12 47 -94 -28 -126 73 53 40 29 -60 43 -100 -7 -8 -14 41 8 37 20 -76 -56 -90 0 60 -15 13 127 -1 -25 -69 33 66 126 43 -23 -42 43 -27 25 47 -2 39 -34 -67 65 50 26 16 -18 38 54 45 9 -121 54 -12 -33 71 28 13 20 -14 -5 27 121 7 78 109 33 8 -25 -21 -20 55 39 -22 -39 -8 -11 -62 47 -38 38 -55 23 45 67 29 -41 -13 -4 -1 -106 -30 119 -27 28 3 -34 87 -41 106 18 108 42 31 -2 116 -41 86 -53 -35 -35 6 46 -3 43 29 -53 -52 75 -58 -1 -10 50 -3 -16 -18 -107 27 19 8 -49 -55 37 -81 -34 14 126 50 114 28 46 24 -62 1 -28 3 -94 -36 35 -25 31 13 68 26 14 86 -38 -29 -1 -35 -101 -16 -16 47 11 -39 -72 49 -12 3 -64 -12 9 -100 25 6 -33 -4 6 95 24 3 -31 -44 98 -21 -93 -85 -44 -7 -127 25 127 107 60 -126 -7 -9 -7 -2 13 -7 -69 -12 -46 -69 9 -48 76 -11 -10 31 16 -10 67 14 31 -44 -35 30 41 44 -39 -48 -21 35 43 -93 -27 -3 54 31 -78 17 -14 -64 -18 -76 -18 -68 10 -5 -42 19 6 -21 0 60 5 33 4 -40 -36 109 84 42 22 -79 7 -38 -9 26 -48 -106 31 -32 19 25 -18 47 30 22 127 -12 14 4 64 -3 -26 90 28 -27 -60 -43 46 66 -52 127 -50 68 28 -47 -8 36 48 -53 32 1 47 31 -16 -27 120 -26 -21 37 -20 3 39 -43 -77 60 113 -14 96 -66 -6 85 -20 6 6 -45 -7 39 -66 -6 -3 -38 -35 6 12 -38 -48 51 30 -40 3 -50 46 33 -18 -25 -31 -23 24 121 -1 -12 -19 70 -37 60 -18 -47 -20 -33 -13 36 4 0 -9 -42 2 16 -21 -21 -15 42 -67 -18 -35 -72 -40 -36 -10 -16 -36 -126 81 -61 67 14 -49 14 -32 2 19 -22 -6 -22 -65 51 -16 -100 -7 12 -3 72 4 -51 36 74 -1 73 -20 -53 18 62 70 -34 8 16 -21 -51 -97 21 -21 54 50 -22 29 -38 37 10 -7 45 -43 41 -19 -36 1 41 17 53 68 15 4 86 -9 75 -21 -28 43 -72 10 -16 42 20 -33 -3 -45 -49 4 -30 23 7 75 26 -31 -28 51 -16 -2 5 26 -3 -46 -3 -28 -28 -61 10 -76 56 -40 -43 21 2 -15 -10 9 -53 82 66 -77 28 -37 -28 -2 -7 -18 -6 48 34 -21 19 55 -36 -71 72 -18 8 -1 26 46 -27 34 -7 1 -11 106 23 0 -8 26 32 3 -23 -62 7 -55 40 -55 6 -58 17 15 67 8 82 114 66 32 5 -35 -15 -2 -13 -92 20 -46 -98 16 38 -16 -110 -28 -52 -79 -56 60 5 83 -57 18 -28 -28 20 89 7 112 38 23 -15 62 61 6 16 -51 -7 -16 15 63 27 65 -17 -95 47 -43 77 -28 -47 -67 -18 50 -14 -48 -73 28 76 18 3 -8 40 -12 108 7 30 -36 23 27 24 79 -59 -55 39 -60 -16 52 48 -23 -25 56 -58 -61 -24 -20 27 -10 35 -16 125 -13 -38 -8 -6 -117 -70 -58 77 -22 -19 -1 63 -43 -30 -14 -47 -4 -76 21 -53 -62 -45 -108 9 -81 -47 56 -34 76 42 -29 -102 -12 -38 -37 -12 -4 8 -42 -1 -19 -12 -72 -2 -12 -44 -12 13 41 -3 55 41 -13 16 -37 -68 54 8 41 19 12 -36 -6 22 3 75 -18 -59 -77 -79 -49 34 -29 0 22 -45 -36 -55 5 5 63 75 74 -35 47 1 -27 -11 -96 24 64 -43 -34 65 3 46 12 -22 -39 26 -115 42 0 -65 -8 25 -48 53 -53 -21 73 61 -13 -3 -67 21 -20 -38 -127 66 41 -5 29 -31 -59 11 -17 49 -7 -36 -53 24 -47 -60 54 83 0 13 -29 27 39 29 8 9 -15 -31 15 7 -8 1 36 -19 7 -14 -14 75 36 -56 -54 -34 40 26 -75 59 -27 53 20 16 0 -14 -39 -106 -17 -82 53 -23 -18 -104 18 23 -99 -5 17 -9 5 -44 -71 -39 -16 -51 93 81 -72 -20 -4 -21 18 63 0 -65 34 32 -9 -63 13 52 -16 2 -19 53 -59 34 44 17 -5 7 -29 -31 -111 2 72 39 17 15 6 -15 -21 -53 20 82 49 -7 68 10 18 -2 110 -11 -56 49 -106 -35 -21 12 0 21 -27 95 76 -61 37 33 -28 10 52 -52 -57 41 -63 14 -1 -13 -55 -29 -10 64 45 -5 -20 -9 -3 -68 -43 28 14 1 27 -26 81 31 83 -14 40 -2 106 3 -7 50 -8 -38 -40 -9 -26 -98 -128 -66 14 -65 71 -9 -17 -3 20 -128 -39 -44 -25 -60 -24 -65 -44 20 -74 67 -68 -38 -87 21 -59 -16 -58 62 10 -29 126 5 -48 -10 52 62 17 -52 64 -66 -46 -23 -11 24 67 -19 33 -27 -29 -15 -7 -22 56 -5 -33 19 11 40 -11 68 59 -53 -39 -53 9 7 6 13 78 -120 49 46 -41 97 -88 64 -125 -19 -41 59 -44 -12 3 -95 61 -25 47 -82 40 51 -118 125 11 67 -123 -77 86 46 14 -35 29 -84 16 68 -48 51 55 127 -17 -4 104 -85 66 -25 -42 58 42 62 11 32 33 73 -18 3 -28 60 -10 33 -57 -25 -80 -35 64 71 -24 -32 -1 -10 -35 -1 -1 21 -47 6 -63 -7 -11 -30 63 -28 -55 9 -119 -4 -24 5 102 -2 9 64 8 -1 -71 -33 54 54 -34 38 66 -42 28 46 -74 -48 24 22 18 43 -35 -108 22 -59 17 -3 45 54 -45 -50 36 -41 -77 -17 -55 -9 29 16 4 18 -41 22 1 74 -36 -8 -73 -31 52 -68 -40 14 15 61 26 -31 12 47 -22 -39 82 125 -19 -11 -76 -57 -122 -1 21 -26 4 7 -61 12 18 -20 -71 57 -3 58 -97 -63 41 33 16 -6 18 3 8 5 14 8 -51 -1 -68 1 67 -65 -91 50 -75 33 -60 10 -12 -12 36 110 -18 -124 -50 121 -116 -41 -40 -53 38 48 -31 8 39 -92 -48 41 -35 33 23 43 84 63 8 10 17 68 4 72 -35 55 -28 -55 28 119 -5 82 10 -30 -32 -21 46 28 30 -4 11 31 -8 34 -77 -24 -52 29 7 84 -50 31 -27 -103 -13 -20 2 -63 58 58 85 97 -22 -69 -18 0 -63 -8 -3 -65 42 -47 -9 -8 55 -61 -40 26 39 73 73 -44 62 50 41 47 -12 127 -36 103 127 -4 31 35 83 -2 4 -117 41 -39 44 -10 0 -13 -21 -128 124 -126 -71 -10 0 24 -20 19 -30 78 -63 43 79 -62 84 48 122 50 113 30 96 -19 100 -21 -22 56 81 34 87 73 29 55 106 22 26 -105 -54 17 -87 -21 -101 127 -27 -5 -29 -43 53 8 19 8 13 43 -56 -52 2 6 6 -23 33 60 67 25 15 41 -13 -34 16 -15 72 105 96 -59 -17 40 -41 -39 39 11 2 45 -26 -52 -66 34 -27 -117 -53 24 39 -121 -53 53 44 -14 21 -90 -114 -81 -59 -11 -102 -28 41 57 -29 13 41 40 85 -125 -96 -62 -18 1 -41 82 -62 25 -73 -79 -74 44 30 -122 115 -21 90 36 65 73 14 -15 -25 68 -122 -9 -24 16 -48 57 40 -24 -70 11 25 -38 -32 -54 -98 -67 14 113 -16 -21 26 56 -4 -33 3 1 34 21 -6 -17 -19 48 22 24 -83 53 27 49 41 -128 -103 17 51 -23 -112 16 58 -30 22 1 40 -23 -3 2 -23 64 19 -9 -54 -98 -68 -25 23 6 38 -53 -10 -120 25 44 65 -23 61 -105 9 100 27 15 -12 30 -43 -8 84 61 65 51 -25 -69 -34 -7 -4 48 -93 42 -70 -86 -2 118 -19 4 -11 17 67 36 12 -7 -28 -14 30 -95 36 -10 38 63 -49 -124 54 -12 36 72 -16 -5 -96 12 -20 42 -3 -127 -100 -19 -54 -3 -36 8 17 82 58 -37 -35 12 -78 23 -73 19 66 -88 15 27 -118 48 -23 10 -7 -40 10 43 14 -20 61 -13 -50 -18 49 -32 -74 43 11 100 -32 32 44 7 -85 7 -2 -24 30 4 97 -24 -51 12 19 21 -55 -59 28 -46 -3 -66 42 19 24 55 -30 37 -78 45 -33 -85 -36 -23 44 28 -38 67 20 10 -20 -103 -9 -36 70 -30 -63 62 81 -23 -14 11 -38 33 -48 -29 -18 26 16 14 2 47 55 -96 32 -116 -24 24 -5 18 -84 75 -22 -29 -1 -22 -7 127 -108 -2 -20 -55 -39 -48 -3 42 -66 -28 -128 -35 -19 85 18 -18 -10 19 -43 -2 61 -39 -33 39 -128 -36 -42 -75 -52 -80 59 38 69 26 117 100 -8 35 63 77 -24 76 45 -30 32 33 6 -29 39 49 -39 -23 44 22 -71 36 21 80 -7 -34 20 -25 -77 18 -14 11 59 -73 43 -3 56 36 -31 -12 17 -49 -31 -16 -4 24 -19 -55 -65 -6 33 72 71 2 -78 67 -41 49 89 -13 20 10 46 -19 30 -7 -103 31 -41 -11 84 2 75 -36 -1 -97 -63 32 -21 -25 65 60 -57 24 -21 -44 2 20 20 63 16 -20 0 -36 70 -45 -60 -17 -27 28 -55 53 -45 41 -42 57 -65 -13 25 68 12 64 -32 -43 -32 18 -50 -99 -43 86 -53 -45 4 -14 12 -55 -26 -44 -26 59 35 -39 -3 -29 58 -35 65 -8 65 -19 25 -86 62 25 35 -63 29 41 94 97 -2 7 -10 -66 -46 -54 47 57 2 5 -52 36 10 29 37 -22 -11 91 -64 7 69 10 47 -38 34 -73 -34 9 -14 54 -90 39 19 20 33 6 -32 14 -70 50 -4 -5 9 46 -72 54 -7 -34 -33 49 -110 -67 5 47 42 -6 -72 -47 18 -33 55 47 -71 -13 -6 -28 46 -7 45 -46 11 47 34 -18 -4 43 -27 24 5 -66 8 -49 -40 1 3 43 -34 27 -2 -41 -127 30 70 2 -37 16 -58 33 24 51 82 100 -39 17 6 -27 -18 -86 98 113 -37 -80 3 -44 35 8 -36 16 102 5 27 2 -95 66 -40 87 -35 -100 -50 21 40 -26 118 32 -37 21 -77 -39 10 -14 20 94 -111 -87 107 32 4 -12 50 -8 -60 124 10 16 -67 -49 52 -31 13 2 63 59 30 3 -25 -57 16 0 92 -31 39 -77 0 67 -45 28 -45 -36 -61 -42 9 -73 -29 31 -10 -72 -11 21 -74 -11 -1 57 7 -32 12 54 -16 -5 24 -69 -90 -74 -5 0 91 39 -25 -57 57 10 86 -44 -5 -126 3 -59 -52 23 -38 14 26 29 16 77 -96 -57 -79 -29 -44 58 44 -85 50 -122 -54 -14 51 31 38 70 -20 -97 -7 33 -12 -127 -46 -12 -6 -3 14 -40 -64 -24 37 57 -107 -28 40 18 31 -27 60 24 -34 6 1 24 24 -57 -25 -4 -108 -28 -11 46 122 -126 -27 -6 118 38 114 44 -66 -20 9 19 101 30 11 -34 18 -82 -2 1 49 96 117 68 24 -79 40 16 45 34 -77 -42 -68 -71 -13 89 63 69 -17 -21 -20 -118 39 -30 -9 11 -78 -53 3 41 -2 -11 -90 17 74 -56 -27 -128 32 -3 45 -10 38 13 16 28 -10 123 -26 59 -70 -101 26 23 -69 -45 34 39 -8 2 30 7 31 45 46 -19 63 38 -52 96 -38 15 21 56 19 41 -45 67 -9 2 31 4 20 30 -20 -45 32 7 4 9 33 -11 -40 -13 28 -3 -38 -23 119 -16 -31 -31 -97 -51 13 3 29 -8 -25 -101 35 -25 -16 17 9 -41 25 122 81 -29 42 -30 -2 -43 -64 31 -14 -24 48 73 -35 -24 88 69 50 -44 19 43 -70 -73 90 -99 68 95 50 -12 0 -6 -44 -10 40 -28 28 79 -12 31 29 -24 4 -12 122 124 54 -35 -27 -96 -11 23 -26 18 -24 -72 119 -60 72 69 -17 -41 51 22 -40 -32 -6 11 -108 44 -56 18 23 4 -124 36 -57 66 -33 -5 -100 -76 -58 -56 -2 102 17 -7 -3 127 11 13 57 15 42 65 -30 -71 -42 64 -16 69 -88 -45 42 -36 -109 27 98 -10 46 -40 -16 44 26 -44 16 -19 55 -4 17 -101 49 -117 32 -55 -88 24 -69 2 -29 -123 -24 66 36 70 28 -87 14 -47 80 127 35 28 44 -8 47 -48 63 -3 -10 43 15 -31 -31 -122 -44 -15 12 -41 -57 -17 34 117 6 23 52 58 22 106 -3 -78 -21 81 -6 9 15 70 27 -9 -9 75 -61 18 22 72 40 19 16 1 13 91 77 2 -40 -108 -30 39 -8 -32 -97 19 -7 -11 51 36 40 -21 41 38 -20 30 -19 -19 16 5 38 -80 19 24 73 81 -24 36 51 34 46 -11 41 62 33 -25 63 -36 63 -58 -74 -3 -10 65 -18 -45 -127 -15 27 14 88 -103 -7 -47 -21 9 -9 12 79 -5 -16 0 8 85 18 -47 -22 -75 -20 -53 -34 -13 -71 -30 0 32 -30 -38 92 76 -20 -60 74 -45 -21 -16 -22 4 -98 -71 -40 102 15 9 25 -12 -23 -20 -4 12 127 -2 22 127 10 -64 9 -3 0 53 15 36 -4 55 -35 41 -75 -34 -32 15 13 -5 42 0 -69 -34 -9 7 -27 1 -28 45 -103 -29 -32 62 -52 -15 68 -59 26 47 -24 -19 -15 49 8 59 -23 33 68 57 32 26 -19 -39 -5 -59 -11 -13 -8 11 -2 -4 -5 -67 -8 -69 -36 -94 -51 -64 -32 43 32 32 33 -92 111 -8 -9 -17 -80 -19 -3 -70 9 37 -12 -65 -19 -38 25 -59 -6 -18 -41 -11 62 -62 -99 -48 -121 -27 -21 -7 -7 40 -44 -49 -28 19 -61 -22 -7 20 -30 -61 29 11 -127 -127 -14 8 -48 25 14 26 14 -24 -22 84 61 -23 -66 -71 -20 -69 16 -8 -5 -98 -10 -46 -120 32 -70 -48 0 -41 67 6 46 34 65 -40 73 60 40 6 80 -53 12 126 59 0 -1 9 85 61 17 -51 -8 -69 118 38 70 77 -127 99 127 44 78 62 10 -17 15 -3 32 29 38 -40 23 -56 44 -33 56 65 -30 10 46 -35 -35 -24 14 -58 8 16 -30 45 -126 -73 -50 -51 1 -23 63 60 -58 -47 62 -4 -30 -37 21 21 -68 -29 5 -41 -75 54 -60 -38 -60 30 -27 -40 -84 -2 55 -43 -6 -37 -7 -35 60 24 -18 -33 13 50 -42 -24 -36 28 -36 -79 -8 -24 32 35 8 46 -125 -19 27 -22 3 -110 -19 -17 -56 44 13 41 -23 -60 -74 8 -33 57 20 -52 21 0 -13 -57 10 8 -71 76 96 -76 96 5 55 19 -36 49 23 47 47 -6 -36 81 25 -29 79 56 -5 11 65 -50 26 -3 -46 16 3 1 -65 -21 -55 19 27 46 3 -6 -125 23 -52 -2 -82 -73 -4 21 20 122 -76 12 -44 10 -111 -65 -14 -14 34 -14 -7 66 24 35 95 -27 -6 13 -4 -48 -63 -25 81 40 -90 -41 -14 61 69 -42 -13 21 -34 -46 17 -81 -29 3 52 36 -29 -12 41 0 -29 23 -76 45 -41 -2 -66 26 30 -52 -56 26 -24 -83 -39 16 -47 -70 -11 -107 -4 -77 21 -95 -51 -86 -126 -5 -5 22 -3 39 53 38 -94 -30 20 36 61 -20 6 -58 -6 40 -39 81 35 -102 -26 6 -22 8 -39 -29 -19 -33 -57 66 51 -21 107 115 49 -15 -83 26 49 22 72 -41 61 -24 -1 66 4 11 -17 -11 36 61 -41 37 95 60 -56 80 15 -32 18 -44 -4 -2 -9 34 -78 -46 121 6 -20 -49 26 -3 59 -38 11 47 11 14 -58 -8 -79 27 -16 63 18 100 -45 -14 -14 -20 11 67 49 -65 -39 -87 5 84 0 93 17 -85 -107 -16 -110 113 -59 65 55 22 -37 79 35 56 -114 93 2 -18 -18 21 26 -68 29 -50 53 2 -32 9 19 22 -29 -11 32 -14 -53 -9 69 -104 -10 -13 4 44 17 41 7 -127 -84 13 -90 -78 -32 -73 -30 27 42 61 39 31 -19 -81 17 28 -25 -25 33 -91 23 127 20 -58 35 84 -23 -62 38 -11 15 6 18 -18 -31 6 7 37 127 1 69 -31 -17 98 -59 26 5 22 -6 20 -5 -61 -45 5 39 18 81 -14 56 82 -36 57 42 -62 -12 33 40 -29 6 -2 17 86 -11 50 45 -6 87 -9 16 -12 29 101 13 -40 -21 88 -62 -47 27 -31 -118 43 -64 56 -1 37 29 -54 63 14 32 -73 -51 -92 -56 101 -5 127 35 -36 58 13 -26 -33 25 69 20 -31 32 71 -24 -50 21 14 -25 65 -5 5 112 -26 -17 37 37 27 -112 8 40 -25 -16 -15 -16 -83 -11 40 -5 89 -67 -31 120 -64 -6 -55 15 -39 29 40 81 -23 67 11 -4 28 -40 -35 -30 -9 64 48 -37 -47 17 -52 76 4 50 -11 -95 -65 1 -100 40 19 -28 -78 11 96 -88 53 -1 4 37 -73 2 -59 99 -5 -9 -4 -66 123 -53 -1 8 -128 -7 24 16 -54 -17 10 -23 13 88 44 43 -86 28 74 17 -58 -24 46 -15 1 19 53 86 -30 -52 2 -86 26 64 -23 5 -53 11 -15 -18 -128 3 -92 -45 121 -4 -7 -33 -40 24 6 -8 -95 -120 -18 36 48 -22 9 117 120 19 -18 14 -31 36 -18 2 35 -56 -15 31 0 22 92 -2 -110 36 117 59 -54 21 -33 -1 44 62 61 -20 -27 -15 120 31 127 -4 -110 -21 59 32 -10 28 -2 15 8 -107 17 -95 -39 -17 -115 6 23 126 41 -2 24 -23 -1 1 81 -22 102 57 12 -43 -62 -40 -3 -83 121 29 -65 -64 62 69 84 -27 25 16 26 -15 68 -56 -12 8 -35 23 -29 -19 -5 -44 -40 -85 11 -22 7 -26 -11 50 -36 79 -38 -23 -6 38 -1 37 43 -20 -15 43 -54 -59 99 -52 -9 -3 27 -16 32 7 -16 10 -45 0 25 37 63 63 -9 -38 -42 -27 38 -20 -38 -107 50 -47 39 67 68 29 -43 -11 -42 85 -49 126 5 -3 -13 -24 30 81 50 47 5 -54 4 7 -51 30 4 -3 -1 -66 -55 -116 -40 16 50 57 -81 -73 24 -26 47 26 71 10 -111 91 61 48 -44 -104 15 0 -6 41 -41 3 -48 -2 -19 -23 -43 123 42 -3 17 127 61 -31 19 -100 34 41 -16 -33 24 90 23 -88 122 -22 -55 35 -23 -25 63 -5 90 1 66 -13 -20 22 -48 22 62 19 53 -4 40 -3 -76 -3 -4 -14 -10 13 -39 122 11 19 -37 -19 44 53 65 51 45 -9 9 -20 79 -36 9 12 45 11 -44 74 -15 58 8 39 -49 28 15 -40 -95 41 -12 51 5 10 -8 22 -45 -15 56 -15 5 -86 64 -45 -25 -18 -55 11 18 30 106 -100 8 -78 62 3 7 -20 49 36 -26 17 23 -23 71 22 -86 -65 -70 -11 -12 -71 -2 -13 33 -36 -86 -11 19 25 -40 -69 -27 16 -11 36 3 -19 64 -21 33 -22 0 11 4 29 -81 61 70 36 -24 18 60 75 36 20 51 119 -10 13 82 -57 -58 -42 46 83 26 10 -25 22 55 -75 -1 80 -23 -34 -5 53 30 50 90 29 73 14 30 32 28 53 26 -23 -2 -9 -76 -18 -5 12 28 -47 91 72 -72 22 103 1 -23 47 -1 3 2 -42 30 60 -12 17 56 -11 -56 -20 6 -48 62 -11 -107 -4 13 103 39 25 1 -24 10 24 70 -51 41 58 -12 -38 68 -59 -20 2 -7 25 3 -57 2 11 8 -4 4 14 -102 -10 -31 -58 -23 -27 58 43 -23 -74 47 76 59 82 30 -39 -7 -19 49 -41 24 17 59 7 43 6 -18 -81 -82 66 60 51 15 -107 -116 0 15 66 -23 -5 57 -1 2 -4 50 59 -102 -21 8 -38 -111 62 25 41 48 -52 119 -11 -34 3 -52 -56 83 1 -33 -45 75 13 93 86 -2 -57 -57 112 -13 42 24 23 84 -67 -38 -9 -58 121 33 57 5 -50 70 48 19 -51 81 -12 10 -27 5 15 -29 -64 -11 -21 11 -74 16 38 14 -3 0 2 77 1 62 37 10 24 -59 4 -95 -39 45 -76 -3 8 21 -27 -48 -9 71 -100 -82 -34 45 -1 -19 12 -77 78 -73 57 -73 35 -18 9 -12 17 -10 103 31 114 28 10 -12 -62 -22 -22 -6 -8 119 0 32 58 -78 13 -28 36 98 -5 -43 -31 -8 -11 -40 -38 -7 50 -59 57 -23 -36 -30 +36 -16 99 -2 2 36 22 -5 11 -1 49 -45 19 1 -11 29 109 -2 63 -31 14 44 16 48 19 58 -22 24 62 49 -78 45 73 32 8 91 20 18 -28 62 -8 39 61 -64 40 -50 80 78 24 -27 -24 11 -4 -14 -30 23 5 -23 27 0 -41 -13 -32 21 19 -9 1 -32 29 -16 -16 -17 10 46 23 -19 18 1 -13 2 -22 -5 38 48 -16 -4 -26 8 24 39 16 14 15 10 -18 18 28 -44 12 18 -4 -33 -35 25 14 18 37 9 -11 -84 -33 -31 -8 -18 10 21 -38 -19 -11 1 -66 -12 26 10 -6 -8 28 19 -17 -17 -9 -27 31 18 9 10 -12 -3 -10 51 -8 -11 -43 -17 +114 96 0 +13 9 -28 -63 23 -8 37 1 -40 14 -14 -2 -47 7 32 -33 -9 19 26 81 -15 26 36 47 22 -26 27 -3 9 -22 34 -57 -18 71 -3 38 -42 -34 -10 47 -15 9 6 5 -77 12 -37 51 -20 2 28 18 27 -33 -38 15 -29 -8 43 18 4 -72 22 -18 44 -22 27 22 -7 21 4 -40 16 -9 18 -2 85 10 -18 74 14 -23 5 -22 4 21 14 -25 -1 -5 -2 42 -29 10 -53 -23 13 33 -12 28 -18 41 45 7 3 -35 13 -39 52 33 35 -44 1 -31 -7 -22 2 11 -10 -19 -20 26 12 -32 42 5 -6 -7 9 25 19 -17 15 34 -20 5 2 2 28 14 -36 35 14 17 24 -19 0 -18 2 40 0 -10 0 21 41 1 24 4 48 18 35 7 -14 6 -14 20 26 -8 32 15 -5 -1 -28 -27 -12 14 20 28 14 30 3 8 25 -34 21 43 -17 19 -17 4 -10 36 -24 -20 10 -5 -4 -3 10 3 8 15 6 14 -8 16 14 19 3 12 18 2 -6 4 1 19 5 -17 14 -10 -19 -20 -6 3 9 20 -7 14 -11 -4 -19 -2 -44 -14 0 22 10 -16 34 -21 2 -7 -8 -40 -21 -26 -5 -2 20 18 -16 -18 -14 -6 -2 2 5 25 12 -6 -6 18 -7 6 8 -28 -22 -36 2 -23 -24 10 2 39 15 0 -14 -2 -32 -18 -33 34 -13 16 41 10 33 16 -15 16 -5 -7 13 -3 6 -23 35 -34 -15 -3 10 31 -16 45 29 -13 -30 -55 6 32 23 -12 7 19 3 -39 -41 77 48 -7 -9 52 -19 -10 -3 -38 -30 9 -38 -20 19 3 53 -25 -25 20 32 0 8 -2 -19 42 20 -33 -35 -45 -2 3 54 24 66 -68 22 -79 31 27 11 42 -17 27 9 21 -6 -6 34 -9 -33 44 0 39 -25 96 -54 -8 -10 56 -30 -31 28 31 21 15 92 -59 -1 25 8 14 -10 -1 -28 -12 0 4 7 42 -21 -14 55 24 -26 12 66 3 -10 26 -5 -3 11 46 -28 -14 38 -22 10 -7 -3 -50 40 12 22 1 24 30 -16 32 11 -24 -11 -14 11 -17 29 26 -7 -17 27 -28 4 -9 6 7 -26 -30 8 -24 -34 31 5 17 0 -27 -4 29 6 -22 4 38 19 -19 19 34 55 1 -18 -9 -19 -35 -21 15 37 10 4 -44 15 -2 27 -3 12 34 -21 -1 38 30 6 -3 -54 33 -5 -6 -8 -24 6 -19 -4 -5 1 27 10 31 -26 -3 -25 -33 32 -10 16 24 25 -39 20 -3 -25 -53 45 12 -32 -30 7 0 -5 -20 -21 -18 -1 29 -12 -13 9 14 -10 21 -2 -25 11 24 -13 5 -16 -12 9 14 -4 -46 -12 6 -13 -13 6 -30 -1 -27 -16 19 -20 -6 -14 28 25 15 24 -9 9 5 4 27 0 16 22 -21 16 17 -30 36 -21 -13 -28 27 -78 23 -17 -95 -26 54 -30 -44 5 -14 -43 -74 48 -24 55 -5 -2 106 -77 -29 -59 -21 -12 14 45 18 -91 42 -2 12 -25 13 1 -5 -31 3 -2 40 -72 18 16 -13 35 -4 -43 -13 -17 5 40 -5 -50 -16 -121 -25 35 -35 4 -23 22 -9 115 -19 58 72 -113 -32 29 -27 29 -34 -31 -8 -14 -44 -50 25 -8 -15 0 -57 -8 -84 44 -15 10 -75 -26 -63 -71 2 -15 -71 12 -26 -60 55 -1 66 2 5 -23 37 35 -78 44 31 80 -8 -53 28 15 7 -22 -48 5 -41 -5 37 51 1 39 0 13 42 19 17 -1 30 25 25 14 -16 -1 10 -24 4 -46 29 -47 10 47 -17 20 -44 11 -24 4 56 23 42 19 19 33 39 0 10 46 52 -66 91 38 11 33 30 4 21 36 -20 26 48 27 19 41 54 4 -7 7 -34 36 25 40 -65 28 22 61 31 -23 21 12 45 -6 -15 -16 -22 18 15 -18 18 0 17 16 55 11 34 -48 16 27 -5 -27 -14 -12 -45 -11 -7 23 7 -67 12 52 23 -13 -1 -52 -38 10 10 -46 -4 -13 -11 -40 6 0 35 11 11 4 -17 -27 -8 8 -8 -18 -17 27 -47 -7 -44 -5 -31 -4 25 15 54 -46 0 5 -10 4 -24 11 -32 -1 -2 5 -10 13 -17 24 -1 44 -18 10 -20 -5 2 -6 -1 -26 29 27 22 45 11 26 7 -14 -21 -41 14 18 -25 -34 30 -1 -29 -6 -21 -36 -26 -19 -21 20 0 -18 -39 19 -16 26 26 -18 9 -22 -18 0 -9 -6 -8 27 23 -30 -4 10 -6 11 11 -8 -17 -4 5 -8 28 6 -29 -15 -28 -15 -14 -4 59 -31 -5 -14 -15 -13 -12 1 34 45 -49 -10 10 8 -13 22 -4 17 19 -31 5 33 -30 16 17 -4 16 -13 -15 -11 -42 -18 -15 -36 4 -57 -17 -13 -20 36 -7 29 17 -16 -26 -25 34 -6 -7 -11 6 -6 29 -6 30 4 20 -11 -16 -6 -16 4 9 -4 -7 38 -25 40 -14 2 2 -8 -8 25 -19 -56 16 28 -29 -29 -42 -16 35 42 -16 23 -16 -10 16 18 -18 -52 -38 -16 -55 -21 2 -11 10 -11 12 7 -10 -6 -10 17 11 -26 31 -9 39 -14 -54 52 -7 -41 24 17 -3 -12 13 22 -15 -3 6 -5 -26 -39 -20 16 0 -19 0 -3 -19 -16 -9 -9 2 2 21 16 18 3 -4 -15 8 -17 -6 -2 15 2 -7 10 -5 -31 -16 2 19 -22 -45 12 20 -10 -20 -33 -6 38 17 8 17 -28 15 -47 1 35 4 9 -13 -23 -19 21 12 1 -22 2 -27 -7 25 22 -22 -4 -11 3 -10 33 8 3 6 14 12 24 10 18 -25 -2 -23 15 -26 -33 -1 -8 -29 -25 -32 -20 -10 -22 7 5 -30 -17 19 -10 -1 3 -12 -17 5 14 29 -13 27 7 -19 26 -2 -50 4 7 5 -33 -9 -29 0 1 -4 19 23 -34 19 8 70 60 7 24 -34 -25 20 57 28 11 47 62 -60 -27 67 19 -48 84 49 16 90 7 23 -25 -76 26 -47 -38 41 61 29 -18 6 -20 40 61 8 123 -66 23 34 -37 78 -44 -9 -14 -37 20 -28 -20 14 20 -1 67 15 -49 35 85 -21 33 -11 20 -28 -3 109 30 58 59 -21 7 51 17 53 44 24 62 -49 -67 93 -47 -20 18 -19 34 -4 35 -38 -40 42 -21 1 -76 -5 42 52 -35 15 41 100 -10 -107 -27 20 -1 41 -35 -33 10 105 -15 -55 91 15 10 -17 -30 75 40 -72 19 -40 -10 14 9 13 -19 -64 40 -23 -59 -11 -32 -27 -120 -8 87 -33 -80 7 -2 -14 39 -27 21 -9 38 -64 -19 -28 89 -4 -46 -8 12 -54 -23 44 11 48 52 -32 47 20 45 83 12 28 34 26 7 -28 45 -19 -33 -15 -6 14 4 -15 -38 -76 -33 5 -38 -45 -59 -23 -12 18 16 -2 -25 26 16 22 -16 6 -12 15 2 22 1 -19 6 -91 29 -9 22 -31 -22 6 24 16 34 45 -44 26 51 16 -26 -21 -8 -15 -24 14 54 -9 29 28 -15 7 -19 23 22 27 -37 -28 -12 -16 43 1 22 9 -11 44 -15 -28 -24 -2 -18 15 11 -43 -21 -12 -17 -13 -29 -3 12 23 -4 7 23 -9 19 42 -51 -17 -7 -97 34 -29 23 -32 12 22 20 -54 -11 -93 13 -30 37 -20 -11 -19 -48 -6 -1 -12 10 33 39 -21 -83 -26 39 -34 11 -69 30 24 10 -14 -56 11 -46 -17 -2 -12 -6 16 27 -23 -27 -1 -65 30 -8 -31 16 -31 3 -52 47 -27 -2 -22 -39 38 -2 -19 -29 8 -64 -26 -11 -31 6 -10 69 10 -6 11 -23 -6 27 25 -7 21 7 18 -9 9 -29 41 -3 -12 -49 6 -23 -15 23 -12 -47 -64 -2 -17 36 -57 11 -28 -95 8 0 1 11 -20 3 4 -23 41 12 -16 -25 -71 0 13 -22 11 -46 -10 37 -14 -76 26 -4 -42 -7 42 -16 13 -32 16 31 29 -43 0 -7 39 -37 -17 -26 2 21 -1 37 46 -14 -7 89 30 -13 -20 24 34 19 4 55 -15 39 -16 -34 34 -14 16 21 11 24 -35 12 -33 -58 -55 -41 25 35 15 24 -34 10 -14 81 -42 8 76 4 -34 -16 -15 -3 -5 11 -25 21 4 -40 -27 18 12 65 14 -10 -13 -1 -8 -20 46 7 34 17 -17 -9 -35 -10 18 12 11 -22 33 49 -25 43 11 -17 16 23 5 28 27 -21 -34 29 -19 -27 25 12 41 8 6 12 -30 48 -4 13 19 2 -37 9 -17 -35 13 5 -10 -17 15 -1 13 29 -18 -37 8 -5 -56 25 -17 16 -15 -29 -34 6 10 -5 -60 36 18 2 23 14 -24 -40 -15 -5 4 24 32 16 -16 -14 37 -6 -2 -52 -73 -22 -53 -11 21 -6 35 16 -33 -6 46 29 -46 -54 15 22 1 0 -47 -6 54 2 40 -30 11 11 17 4 7 -9 -41 -41 -66 11 9 22 -17 29 16 68 -14 -15 -65 24 6 7 11 -42 -20 -9 8 -51 -4 -48 15 16 -18 -19 4 1 16 -34 23 15 -14 26 13 -4 4 -86 42 0 -41 37 23 3 17 0 -34 -30 39 7 -27 -40 -26 7 -29 -28 -10 26 -106 -34 -2 5 -6 44 -7 -24 19 -15 -49 -17 -21 18 18 17 4 -1 14 -9 -36 10 -28 3 -64 71 -19 -43 -14 -33 12 -26 -12 15 27 6 -18 25 9 2 12 15 -52 -74 1 -39 -35 10 54 -13 8 -15 -14 14 -7 -5 -9 -15 -5 14 19 -13 -45 -6 -15 -8 5 7 3 51 -77 8 3 -22 21 42 -23 28 0 29 -53 50 12 33 15 35 66 -47 -2 7 8 0 8 -7 15 16 70 -2 -6 11 12 9 26 13 56 24 13 19 -4 16 22 -7 38 9 -12 9 -15 -24 4 -36 -32 24 -19 23 -36 33 -5 15 42 -10 -15 6 -21 -32 4 20 -36 35 -15 12 30 -20 -11 -15 0 -18 -1 7 40 -22 1 8 21 -19 -12 -13 0 6 -15 -30 -64 -3 -15 -19 13 -18 28 6 39 -8 6 -1 24 5 24 -7 -22 -7 1 -16 -13 -33 13 55 13 -52 -39 18 49 -19 24 -24 -48 -23 -10 2 -50 65 25 -15 -15 -14 21 32 30 -43 -31 -14 2 20 1 -15 74 -40 -6 -17 -38 10 30 -31 4 10 0 1 15 20 -25 27 -2 26 -21 -12 17 16 51 -47 29 36 -7 -3 -8 4 0 -12 -7 -11 -27 -34 11 27 4 29 -14 -32 35 -8 -34 -17 42 29 22 -26 8 19 -46 -55 -25 1 10 -39 -36 -8 48 12 -47 6 -48 30 -15 4 0 -19 -21 -9 -42 -8 9 -7 3 29 4 27 -15 8 -27 17 8 -17 -13 12 -18 10 4 23 8 -10 -26 -7 32 7 -34 -49 32 -7 -42 11 17 15 -42 -24 -25 -25 -33 37 34 21 -15 -50 -16 12 -7 -38 -4 6 14 -7 42 -23 26 -15 28 -24 19 -5 0 8 -7 19 -6 11 -20 43 -20 -19 20 3 30 25 16 4 22 -1 -15 -15 -11 -2 4 -2 81 40 30 -34 7 31 -29 -4 33 13 51 -25 -31 -20 54 -25 0 -12 -3 25 33 15 -34 14 -17 -23 -26 17 -1 15 1 -8 11 7 -18 -1 7 15 -13 7 3 -10 19 -23 -4 6 2 -43 2 23 -13 -45 -30 -10 -2 0 -23 24 10 17 0 15 13 7 -8 -5 -2 29 -3 12 1 -11 2 15 -13 2 -17 -4 -15 -4 74 9 16 18 17 5 34 -26 -40 0 -23 6 -15 -28 -4 10 -5 -20 2 -14 18 -22 -1 24 -31 -17 -11 -2 21 11 13 -2 13 -13 12 17 -9 -16 16 -6 30 -7 27 20 46 -52 29 -14 10 21 5 14 3 -12 42 12 -14 -23 0 31 -12 -19 -26 -4 21 -6 -9 -46 -19 -4 25 -7 16 -28 4 -10 5 26 36 -36 13 -14 -6 -7 10 -10 -12 43 -39 4 -7 -43 43 -37 -10 -10 -15 -32 -35 17 -8 -14 19 -52 16 31 25 -4 0 35 10 36 -12 -5 14 -4 -6 -36 10 -6 72 1 9 -13 8 38 -5 2 2 12 -28 12 -29 -7 19 16 -40 -17 -7 -25 -2 12 -5 15 -3 43 -31 12 -20 -23 5 -13 3 -35 -27 -10 3 -37 1 15 23 -5 33 32 36 -31 11 29 -29 10 -21 5 -31 29 -6 -5 35 8 25 29 21 -2 18 -10 -9 -12 -7 -3 9 40 -28 4 -5 3 13 -6 -29 30 -4 -7 -7 -17 6 14 8 51 -64 -9 -12 -10 -21 25 3 6 17 7 30 27 -36 -23 43 14 5 26 5 -24 -12 32 17 -22 -20 24 15 -28 -16 4 -2 4 12 -15 14 -7 -13 5 13 13 -29 -27 1 -13 -10 -3 -27 -3 -4 25 -3 24 -10 11 -7 12 -11 9 6 1 -8 26 5 42 34 -5 -10 -25 2 -8 -11 -19 -9 -10 -48 41 0 -13 3 8 -23 -19 10 -21 33 -4 -8 33 7 5 -4 -3 18 26 2 -16 42 50 -16 19 -11 -13 11 -25 30 34 -6 -20 -25 0 -15 20 2 -4 -15 109 2 -28 -38 -90 30 7 -48 -8 2 -85 11 -68 14 50 37 -36 55 -23 -2 89 -9 -15 -25 34 28 -3 33 1 -5 24 -5 18 37 52 29 15 -55 -7 75 14 14 26 23 9 -17 -14 7 36 -66 10 50 -7 50 23 66 3 -25 70 19 -23 2 69 -32 8 23 -25 41 29 0 1 3 27 -3 -38 -30 -79 67 39 29 57 -18 11 -17 71 -34 0 -8 -21 -30 0 5 61 -3 29 -6 24 51 -11 -19 -25 42 9 -11 40 -5 -8 -69 71 37 8 28 -34 64 -50 -54 -2 -37 30 -31 29 27 10 -52 -6 -35 -10 -13 7 -5 23 -17 -1 44 4 11 -38 50 14 30 -48 -22 10 -65 -14 -12 -6 -31 -7 38 54 40 5 -26 31 24 12 30 35 -52 -10 33 -23 -34 51 76 -3 -2 -2 14 9 69 -28 -3 -7 -8 -15 -14 9 0 -10 -15 7 -38 -18 19 12 12 -24 18 32 35 10 27 46 -30 5 16 -6 16 20 -9 -26 17 5 19 -9 -14 14 -2 2 29 -36 -28 -23 -9 -17 19 24 10 -15 10 30 25 -6 -17 0 -3 -13 -3 -2 -3 -26 -36 -10 -33 -9 29 -12 11 -31 25 1 17 25 24 13 -5 -14 -47 -4 15 -17 -21 5 -34 0 -5 9 -10 7 10 7 -22 -10 37 11 -22 12 5 25 -21 18 6 35 18 -14 23 -2 -28 12 -9 -7 25 1 -6 -4 -43 -19 28 -32 -26 0 48 27 63 8 -7 35 30 -69 32 -20 -12 29 -14 8 -20 3 26 -27 27 13 29 10 50 6 -6 4 -23 49 27 28 45 40 -30 22 59 42 40 -73 75 -25 -18 -21 -31 51 37 -51 20 2 -26 -55 14 7 10 37 30 -13 24 -11 -12 63 36 57 -15 9 -41 -2 20 -42 -18 -9 14 31 19 49 14 7 -12 71 6 28 -13 -23 -61 35 6 6 -23 14 -12 19 -17 -5 2 5 -14 7 1 -11 14 41 -28 25 -13 6 5 45 -14 28 -19 -55 39 -25 21 -18 1 -8 -19 31 4 -12 38 1 42 1 16 9 -24 -60 63 33 -14 -18 22 32 -20 -25 12 -17 10 6 15 4 -38 15 50 49 14 -63 -5 68 -20 -36 17 70 27 0 20 11 -25 15 -2 47 6 -7 -41 -10 55 27 23 -5 -12 10 -7 -22 7 -9 17 4 0 6 -13 68 44 -31 20 20 -19 26 -10 41 -1 -3 -15 15 14 3 5 9 46 5 13 -5 0 49 -39 -25 -11 -46 -14 12 5 -5 -24 -4 -8 -18 -3 -13 12 -6 -30 -10 -18 -31 5 -18 -9 -17 19 36 -22 -22 -33 9 -7 -10 15 33 17 3 1 24 -5 0 -11 8 -8 -5 -2 -21 -21 -1 30 19 -23 13 -14 11 -30 38 9 11 -28 -7 7 -26 0 -5 41 23 19 -16 11 -15 -7 -8 24 -19 -16 18 -19 -2 31 -28 -15 8 -19 10 25 20 -12 -4 18 -51 18 9 33 3 29 11 -45 -19 42 -4 27 25 -47 6 -30 -15 3 -13 22 23 -23 -19 -24 10 -9 -17 -9 -25 -36 28 44 3 -14 29 7 -9 11 -18 -55 36 -16 -1 -33 -26 19 -12 -2 -45 -5 -14 -25 -36 -1 11 -4 27 -2 -7 -3 23 -39 -26 30 -16 12 -92 -46 13 79 23 0 -13 45 5 -12 -14 -22 25 1 24 17 14 39 -16 -11 -28 4 -17 -13 -15 -10 -26 5 -20 -27 34 -26 9 20 29 -26 9 -23 -9 8 -13 13 32 17 -6 -13 -19 -37 -28 3 25 27 0 -7 3 13 -31 8 -30 16 -2 21 -32 17 17 -28 5 -38 -48 13 -1 -36 -34 -27 29 -25 -6 12 -3 13 15 -29 -37 -33 6 6 24 -5 -8 21 11 -22 -6 -7 -33 -25 30 19 -18 -1 9 -14 -34 -8 18 -9 1 16 -20 8 15 -28 14 -14 -15 5 5 -11 -25 15 15 -21 -20 -18 19 10 -8 -1 2 0 -19 17 18 31 30 16 20 23 10 -27 -3 1 36 28 21 20 -8 13 28 -12 5 24 -5 13 -20 6 6 -8 3 -9 5 8 -6 14 2 -14 -6 1 0 26 21 27 7 -9 -1 -25 -25 -10 -22 16 22 13 19 -6 -23 -4 -6 5 -13 26 -14 -14 7 18 10 29 2 -5 -9 12 -22 -8 -6 -30 3 -6 -22 28 2 2 -6 11 19 22 -3 -50 7 11 14 4 -22 -34 11 -6 -31 -2 3 -1 -24 51 0 9 37 61 -13 -55 47 29 83 8 24 3 -13 30 -36 -68 -31 91 8 3 -47 36 10 37 2 27 5 33 1 -45 2 43 52 6 9 -54 -2 -17 64 4 13 -45 -30 -36 -33 -49 -23 12 22 6 -13 -9 9 -33 -16 -15 16 -3 22 27 -13 -8 14 -13 -19 34 74 -22 29 43 29 10 -13 -9 -35 41 9 54 38 -6 -21 35 -26 33 -7 26 4 42 5 25 27 20 2 1 -14 9 -6 26 -37 0 44 -31 -26 -3 -35 -39 -4 17 15 17 -33 6 -47 18 10 2 12 -11 -22 26 34 32 -2 -66 19 -1 -27 -21 31 -38 3 -2 33 -26 16 2 -44 -29 -6 -14 15 -17 -20 15 -8 24 46 -4 21 -13 26 45 -17 42 -10 19 -9 1 28 -42 -27 -16 -13 -43 6 6 -14 40 11 -25 64 32 0 11 21 5 2 -27 11 3 -12 -17 27 22 -21 -7 -20 -35 -14 1 50 17 -13 15 -7 32 -1 3 -20 8 17 0 -35 -21 37 -25 24 -2 12 10 25 -12 -2 12 -3 6 -25 7 -6 -32 -2 18 -14 -14 -23 9 12 3 -21 40 -7 8 6 -20 -6 17 3 40 -1 -18 9 28 27 -16 11 -31 37 -13 11 25 -1 1 34 -13 7 -23 -25 8 12 -4 -13 -16 32 -3 10 20 3 -30 -35 -2 10 -18 -53 -1 -70 42 -3 -6 -7 -8 17 12 -77 -2 -2 59 30 -26 14 -6 -25 -40 3 -38 21 11 21 29 46 -3 2 38 15 -12 -2 49 -18 -61 -95 21 52 -11 -53 52 -125 32 -36 -66 -48 -29 -15 -9 -48 -85 18 51 8 6 10 -1 -56 -17 -69 101 -7 -9 47 -69 11 30 -29 -8 -42 -15 10 -36 -80 11 15 -5 -1 14 14 -1 -50 -100 -8 -17 34 50 17 -47 -40 -61 -18 -11 -29 23 -35 42 -33 18 -8 17 -47 6 -15 -34 32 -4 -56 0 -42 -2 -18 -21 6 -18 -7 -15 15 -30 0 -89 -9 25 -32 -19 -19 -18 -23 12 44 8 -90 34 -19 3 -30 23 -4 2 25 -3 4 -6 34 -40 13 3 37 33 -23 7 25 -38 6 -52 -28 -25 2 20 27 27 -5 -20 18 -5 -2 -1 -45 -14 -43 -2 -10 -28 -48 -27 8 4 42 -2 0 -27 -7 12 11 7 3 15 -28 -11 6 39 -24 18 -15 -8 -36 -1 -15 -7 10 -15 19 -9 28 16 -6 -13 12 -15 45 -41 -25 22 -47 -32 12 27 14 -3 0 0 16 -22 11 4 55 49 3 -18 -9 4 -8 -12 -31 15 54 0 -1 -9 -3 -26 14 -34 -13 40 41 -12 14 -11 4 8 47 -73 36 5 -20 -8 34 37 18 29 9 -56 13 -5 -47 7 -29 16 -19 -42 8 -19 -56 -9 22 -6 6 -26 -6 8 30 -7 -61 2 15 12 6 22 9 22 17 -50 23 30 13 -16 43 26 32 -39 0 -36 -32 11 3 28 37 -48 -30 -5 -32 -12 16 32 41 -39 -20 2 -26 -18 -29 7 44 -28 28 9 46 -42 32 -76 -24 8 30 17 -17 25 -12 9 -9 -5 11 9 7 -38 84 -41 96 19 -45 -24 -17 -17 -38 21 37 -8 25 -33 27 8 33 2 -19 21 57 -8 4 18 -19 32 -7 42 43 13 -37 21 -31 -25 23 -6 -5 -6 -53 15 23 -44 -20 7 36 25 -17 -36 -4 4 21 -8 7 20 19 -54 -25 -35 31 -62 52 -29 21 18 -2 21 42 10 -31 68 22 -7 52 -27 1 41 -10 -13 5 -1 -24 -24 10 14 -47 -29 -5 22 4 -24 24 22 -25 -11 19 21 -5 22 20 29 -33 -35 33 -32 -9 13 -5 46 67 28 -16 10 25 -7 30 -2 40 -11 0 -5 -1 29 32 1 -12 -15 52 19 17 -32 26 -17 -26 -13 1 -8 6 -24 8 22 -34 -1 10 12 20 -2 3 -18 -17 -28 8 15 -12 -2 9 -5 -9 29 41 11 -7 -2 21 15 -1 30 -19 17 6 6 20 10 4 2 -24 19 14 -4 13 30 16 4 4 44 2 12 18 -33 -12 8 -3 -9 6 -9 25 -28 21 22 -18 -16 -17 7 -19 21 -6 17 -7 4 -21 -6 -11 -6 32 -12 -7 1 -4 -10 0 8 32 -16 14 31 12 11 1 20 25 0 54 26 33 57 29 65 -16 -36 34 -44 -57 -37 27 6 -5 30 31 50 -30 -19 -72 -61 4 -16 -40 15 -14 37 9 4 7 10 16 -14 38 45 14 40 63 -28 19 3 -29 11 -31 -21 -11 -29 -30 -10 20 57 -23 5 -5 2 65 -12 29 12 23 -3 34 84 -37 -41 21 -12 57 13 -23 -11 38 0 -6 -5 -12 -13 -16 -11 -7 0 -36 -65 16 24 4 1 7 -15 46 -55 13 17 27 11 -10 -32 7 18 -21 -31 12 11 10 -34 17 23 32 -39 -40 43 -1 -11 30 30 3 9 1 6 -16 -14 2 17 14 -36 -23 -9 -13 -15 -16 -43 44 -42 -12 21 38 -5 -13 18 -8 -62 26 7 6 -21 -59 20 -31 -45 7 33 -48 -26 -22 -19 29 -6 -34 96 18 17 -23 53 30 8 -11 -24 -26 4 7 60 -11 43 30 1 -26 -25 -29 -44 24 21 -23 -10 32 6 7 -31 21 15 8 19 10 9 12 -10 10 -15 2 -5 -12 18 28 19 -29 -32 -23 6 7 -3 44 -2 -31 -8 7 -17 -3 -8 -45 8 10 -12 -13 -5 -9 35 15 -2 -8 -36 -29 -9 -62 -18 -24 -18 -32 4 2 -18 -14 36 2 17 2 9 20 12 -28 -16 -1 20 17 34 20 -1 -41 -13 23 21 3 10 -14 -25 8 14 -21 -20 -9 -20 -20 14 3 -18 -21 0 -53 -3 -12 56 15 -5 -16 -5 -27 -3 8 -11 -34 7 -20 0 -2 -11 39 32 -22 -10 14 66 9 30 -21 6 1 66 -23 -24 11 -59 -44 20 -15 -15 -29 21 -14 13 -7 -11 -10 -5 13 -46 -45 -5 9 39 3 -6 26 18 6 -27 -28 -33 38 -24 66 -23 -24 -8 -37 12 13 -26 -19 17 21 39 -14 -1 -12 42 -25 17 7 -35 9 -15 10 4 10 -16 38 38 80 -61 -28 -27 28 48 -13 13 37 -22 4 -21 53 -13 -8 12 0 -14 -20 -6 32 4 -16 5 28 -12 7 18 -16 -16 -43 5 40 -19 9 -30 -10 9 -4 -1 0 -42 34 22 17 52 31 34 -1 -19 -24 -49 -18 -40 -75 3 -25 13 -12 5 -6 24 12 25 7 -7 11 -5 -40 5 -26 28 -18 -17 31 13 28 -9 60 -11 19 7 -11 -29 12 4 -17 3 21 21 -43 31 36 -5 7 41 -20 -7 9 -26 6 -23 -11 15 4 -5 34 -6 -19 -35 -5 -45 34 -15 -27 -28 -9 -13 12 -17 32 30 18 19 -12 17 16 4 25 1 -9 -5 -40 7 13 -15 -14 -21 -5 -16 19 49 -25 27 -10 31 2 23 -8 13 -19 19 9 -23 5 31 5 -17 7 -4 -12 -14 9 -23 -11 -16 -22 -9 15 10 20 5 8 10 -11 12 5 -1 -16 -7 -13 0 -10 5 -14 19 14 -11 -7 7 22 -4 -10 20 0 7 -27 -4 14 -34 -25 -3 17 29 4 -10 -6 -19 0 -5 -2 -8 -42 19 -13 -32 -38 13 -91 2 26 14 -4 -29 -44 -2 -25 15 19 18 -15 -30 -18 13 46 11 7 -14 -17 14 12 -79 -15 9 14 67 -37 -5 -28 6 -5 31 -44 6 -21 -83 0 -37 -7 -7 9 -12 -42 -1 23 25 4 -7 5 -24 -21 55 -32 39 29 -7 -52 -11 -27 -25 -32 -21 -43 16 -30 -35 54 -34 -4 -8 -24 1 59 -5 -11 -55 25 9 -13 -18 -39 -5 -40 1 -34 -3 -37 15 -9 29 5 22 -27 6 -30 41 10 44 -15 -47 18 -32 -15 28 33 14 -33 37 17 -30 4 -19 33 27 -4 17 10 7 -43 -24 26 -17 18 -27 28 -6 -38 10 -14 -21 20 6 42 -10 4 45 2 -15 -5 -42 -41 51 -2 7 -24 -36 -18 2 11 0 -12 24 -9 11 12 13 13 -8 -33 33 23 28 -21 6 -13 -6 16 57 61 -32 -31 10 -10 46 15 -2 -8 -11 41 5 -8 25 -8 4 13 -1 26 8 -8 -34 -28 12 21 1 -23 13 -10 12 5 -2 31 -15 -10 8 -9 21 20 -11 -54 30 4 16 30 24 18 -18 10 12 -37 22 -1 2 46 -4 11 -16 -9 2 -19 -10 -3 -24 18 16 12 14 11 -10 -8 -17 -19 -24 -15 22 6 -2 27 46 -26 -32 -7 5 5 13 -1 13 20 -29 -1 22 17 -34 3 -15 -16 3 3 -19 8 -12 -11 -3 26 1 -30 -1 2 0 10 42 -21 -70 10 53 -12 9 47 39 -16 59 -7 -37 -22 -12 -7 -21 49 50 -6 -2 -47 -8 -7 32 16 -52 -1 -7 28 -22 -64 -14 -27 50 -6 -51 18 -26 -15 -62 3 -28 -64 -9 -2 54 0 -36 -18 14 -52 54 -24 27 -24 -40 12 -3 -19 -112 -22 -11 64 -9 -12 -30 -37 24 -5 -8 -11 -31 -36 -11 -1 26 29 75 1 43 -4 22 -14 9 -12 -6 -12 14 -41 -29 -24 -1 35 -45 30 -37 5 48 -32 -7 16 -10 -13 7 59 72 16 -36 8 32 -44 -10 -43 18 8 -2 -58 -49 -5 3 -3 -33 -13 2 -24 -15 -47 14 2 -11 25 -1 17 -7 13 4 10 -2 -3 14 -2 -29 39 -24 11 16 24 20 -38 15 7 -23 -29 7 -60 -23 1 -29 -37 -67 -55 -32 -21 -32 19 40 36 12 -21 -51 -31 -62 25 0 -2 80 1 0 -9 -19 0 -13 -28 -5 1 -45 -3 44 20 -6 15 -3 -18 7 11 5 1 -8 -14 -8 -1 -28 17 9 11 -35 15 -4 -26 -52 13 13 -27 12 -4 -19 51 1 19 -25 -32 -5 -14 -8 -3 2 38 -20 13 19 3 22 27 33 -18 12 5 17 11 19 -22 -19 36 -21 10 21 -10 30 4 -49 -17 -12 -5 -9 -8 -22 -22 3 -11 5 3 -24 -3 15 -8 -10 -3 -9 12 -35 -49 34 24 -15 41 1 11 -25 -20 25 -4 -29 11 -5 -16 -50 3 -2 -59 -53 12 -17 -27 -30 -20 37 -30 -58 -15 -17 -2 28 20 19 -32 33 -22 78 22 -59 4 13 19 -26 9 -25 6 -7 3 -28 11 18 -19 -5 33 -55 -73 11 0 10 -30 -19 5 26 -30 -10 -61 5 5 19 23 4 -14 9 5 -30 -15 74 -40 16 49 19 -37 -27 -54 63 -33 -9 -44 11 -64 -6 -17 -56 -22 53 -45 28 -32 -20 11 28 -3 -18 36 -30 -28 -7 -8 -27 -53 42 -2 20 12 -3 17 8 -36 30 31 21 14 36 -2 -17 -25 -10 57 28 -42 -25 23 33 -15 -18 19 8 -9 -26 25 -6 4 -7 -5 17 -48 41 -36 -29 60 -7 14 -1 -6 8 10 26 -1 -2 12 -36 -24 -33 -57 31 -10 -24 -52 -29 -15 -18 -3 -32 -34 4 25 7 -26 -32 6 -21 -38 19 -11 0 -10 16 9 -24 -31 -47 19 24 44 18 13 -15 -20 -15 -43 12 -15 -24 -16 1 0 3 0 -8 -29 -2 0 3 -31 -19 0 -29 -5 -17 24 -7 -44 -5 22 7 -21 -16 33 26 0 -17 9 -4 3 -22 -8 -2 -1 36 32 -16 -15 -3 -2 -9 -21 0 32 -8 0 -9 -19 16 21 0 13 -5 -23 -12 45 29 -19 14 -16 36 11 10 8 -52 -2 -14 -27 -9 1 24 4 -11 -11 -9 -7 4 -7 -17 -18 23 17 -7 -39 -8 4 -4 -23 -10 19 -26 -1 20 -4 -2 -29 -12 -11 -9 -5 -8 20 33 5 -4 45 -26 4 -13 36 -12 7 10 21 1 -24 8 31 -3 -17 5 5 8 40 27 -13 24 -27 15 -21 36 21 13 -9 -31 -4 -18 -46 31 -23 -2 3 -49 -33 -51 -23 19 -6 55 -24 -35 -11 6 29 -53 -20 5 25 7 7 36 14 4 14 -20 -50 -19 32 30 -44 -10 -10 -4 -10 -13 -16 22 -50 -21 46 -5 -60 12 11 6 5 -9 32 -7 -82 30 48 14 5 -8 1 -16 11 20 -11 1 7 -29 8 10 -18 -52 25 2 -4 -5 -12 -9 -8 -18 -19 13 -16 -1 22 -11 -21 -12 9 -4 14 -18 21 -27 -12 29 -41 -6 -15 -7 -6 44 7 -23 37 11 -18 -34 -35 41 54 -37 41 -7 -11 13 13 38 16 40 -9 -8 44 0 39 -27 -22 -16 0 2 18 1 5 -24 -47 -41 1 -15 -14 25 22 -9 6 25 30 21 -1 14 -7 -15 -21 -4 -11 -49 -32 23 17 -14 60 -2 -4 30 20 19 30 32 -31 27 13 22 9 -12 -7 13 10 -5 43 -29 -9 -33 -24 -26 24 -22 8 7 24 4 6 14 34 17 0 39 11 4 -28 -5 -40 -14 1 -23 14 -5 32 18 10 -29 -6 -18 -15 -10 8 5 -2 -2 30 -29 7 -16 18 1 -26 22 17 -10 -7 -1 4 -5 -22 33 -6 -27 6 -11 25 27 -7 -12 -12 -6 -2 19 9 1 -31 -23 1 16 9 -16 5 -42 -24 -3 -17 20 -20 14 -29 2 -14 3 19 27 -19 33 -3 -4 37 -2 33 -12 -17 -71 12 -80 -68 -6 -14 -15 -43 -8 1 28 -66 57 1 -9 14 -13 -3 2 -13 39 29 19 35 14 19 -40 13 8 39 3 10 51 -2 31 -47 -87 -22 -43 -49 22 -22 28 22 44 8 -92 46 -4 10 -11 -29 25 19 -28 11 -20 -8 23 47 69 -4 -31 37 11 -38 -16 15 14 15 -13 1 21 7 28 -1 1 -26 10 2 0 -9 4 -9 17 0 -28 43 -5 -37 54 -19 -31 -24 -23 34 7 -9 -5 -11 -16 12 17 0 9 19 67 -1 -32 13 12 -23 44 37 -25 -8 -20 -7 -31 23 -10 -19 1 -3 -5 16 -26 15 46 -4 59 9 -21 -11 44 1 -9 -9 -4 13 0 -6 -10 -44 -30 4 17 -14 37 1 26 4 -26 -41 -23 25 -4 -11 4 -8 -16 50 -6 5 -3 -2 41 -16 -22 -6 3 -14 18 24 -7 17 8 -5 5 1 -4 -25 3 22 18 8 8 -11 11 25 10 -10 18 15 4 18 4 7 2 -5 -6 -33 23 9 -11 -4 -11 -13 2 -19 26 9 -43 -23 0 -20 -3 -34 19 13 18 -9 13 32 20 -12 11 -11 21 -11 14 18 -8 1 -19 24 -10 -49 0 20 -10 11 5 30 36 7 10 24 25 -29 6 18 -16 -8 -36 29 -20 -20 25 3 14 16 -4 -65 -57 -6 -45 -59 44 62 -43 -8 -10 -53 27 101 33 19 -55 -63 24 -16 -14 22 15 -32 0 -54 10 13 -35 -18 34 -48 -9 -48 -8 3 -15 -3 5 -49 -34 -15 -32 -38 3 19 -25 -42 -93 3 -21 -19 1 68 19 59 100 40 3 79 6 64 8 -18 80 -24 36 10 26 -60 24 -19 -29 -26 -16 -42 -17 31 24 10 5 9 -37 -21 68 36 37 -14 -46 40 -37 -34 -13 -51 -33 17 20 28 51 15 -44 -51 -33 -9 -11 -26 13 -36 32 -6 -31 9 -54 17 54 -8 -37 25 -15 -17 22 -26 9 37 -46 -11 -44 56 19 31 -31 -12 -21 36 3 -56 5 -35 28 -18 36 -40 21 17 24 -14 -32 -20 5 10 -18 21 -47 -12 1 3 17 -28 29 -20 -40 -28 14 26 11 -48 -5 -67 -27 10 -2 25 -33 -1 19 2 -22 -3 -19 -5 46 11 3 -23 38 8 32 22 -13 -64 7 31 13 -19 -5 18 -2 33 -10 50 -30 22 29 31 11 -11 43 -2 22 33 -5 22 -4 -23 22 39 23 3 54 -28 -6 30 17 -44 11 -47 -30 -45 24 9 -39 -22 -2 0 -22 -32 -6 -20 -12 -31 -44 -4 14 4 10 -15 -22 0 -9 9 9 -27 -29 13 -27 26 1 27 -13 -13 12 -11 17 7 -23 -5 -6 22 11 6 12 40 -17 17 25 -23 3 8 -6 -20 -8 -1 -12 -18 14 -6 19 -38 57 -71 4 4 -42 -20 -50 -6 4 11 -6 34 30 30 13 -57 1 -30 -61 -25 -55 1 101 62 34 -21 11 16 -1 0 4 -2 -12 -33 -19 -58 -26 3 -56 -2 -26 5 6 -38 -47 -1 10 -16 -1 -33 -5 -26 -26 15 35 49 -40 8 -36 -72 -8 9 30 39 17 12 12 -47 -34 -17 -15 -16 -4 4 -20 3 22 64 2 -13 -49 22 -9 -6 -6 -25 33 -44 19 -10 -32 -56 1 -20 16 18 4 28 6 -5 -32 -36 -35 -4 -2 1 -24 -6 -6 14 4 -39 26 -4 -39 12 -10 9 1 -28 29 -25 -27 5 -35 17 -8 7 5 -8 14 12 -20 32 -14 -39 1 3 -13 -20 -21 4 -28 21 42 31 36 23 15 -17 -31 -56 -2 6 -58 22 -8 5 -27 -23 8 12 0 -15 33 -1 -31 -1 -17 8 0 28 -34 21 0 28 14 -5 15 7 -9 -21 54 39 -49 18 -32 21 -4 26 -2 -23 24 15 -22 -10 -3 6 10 -12 7 17 24 8 -16 -26 -13 -16 -1 28 -11 19 15 -9 22 -17 -16 9 15 -19 8 4 6 3 -2 -21 -8 -23 -13 5 3 33 -6 13 -7 26 18 15 -8 6 9 1 7 -9 -31 -25 -3 -20 -13 -26 -1 8 -13 4 -25 7 -36 -28 1 -5 14 4 -21 17 28 3 6 -29 -14 -2 23 49 12 6 -18 -28 5 20 13 -31 40 20 -37 26 -20 -14 -20 -15 17 18 47 17 33 -9 -17 6 47 11 -6 4 36 56 62 61 -101 94 -51 11 15 41 104 21 27 31 46 -4 57 4 -5 23 11 45 -10 37 -2 47 -86 27 61 22 -24 -115 54 46 19 54 -2 5 -17 -45 26 23 48 39 30 -47 -36 61 39 15 67 20 91 100 -24 64 -79 -52 -77 -28 91 -16 13 6 -8 10 -41 17 -4 -36 -49 37 -26 6 -25 -4 29 34 -27 22 29 -1 3 75 -19 10 1 -14 0 47 -29 12 32 -3 -21 -8 -17 -13 27 -22 -39 54 25 -15 75 -2 -13 80 -18 -31 97 68 -1 110 20 82 43 -4 -41 24 24 -33 -70 17 -35 -79 19 -7 66 -48 83 38 11 38 -83 8 -7 36 22 24 -20 -17 18 22 21 -7 66 11 -51 -52 -25 27 -72 1 -7 19 -6 19 0 -10 69 22 7 43 -23 52 46 15 -50 -14 -13 11 -3 -29 -6 -21 -28 -40 36 73 -21 -5 4 58 -52 -7 -10 18 13 -59 26 25 13 8 40 14 11 -75 -34 16 2 -44 -37 -14 -5 40 25 7 15 13 26 32 25 17 -27 7 -11 -9 4 -23 38 -6 -13 -22 -13 -36 4 1 -32 6 -27 -1 30 16 7 4 -22 -15 -12 3 -8 34 18 -3 90 -5 -18 -4 -9 11 -15 -4 3 37 -12 -39 -23 -3 6 6 30 -3 10 5 21 -57 45 -10 -20 -17 -2 1 3 31 38 9 -5 18 30 17 -31 -54 32 55 -14 -16 66 32 4 4 34 -6 -115 -53 38 -25 -86 -80 45 -62 92 7 109 -34 -31 18 -65 52 39 -46 -25 -72 -28 62 35 27 44 -1 -54 30 -39 -124 -21 -13 -62 21 -16 -25 49 42 -3 -47 -67 -44 14 61 -43 -26 56 2 95 22 64 109 -52 -3 4 26 -16 6 125 14 -18 6 -60 -13 14 16 24 -26 0 12 -4 -74 -61 -31 -4 -16 66 25 -29 53 -41 85 40 -42 15 -89 14 7 1 -10 19 -24 29 -57 56 -35 -71 40 -11 31 69 -2 -10 11 -106 -31 47 -103 25 -9 -23 -3 61 119 -18 -8 36 123 -65 47 -4 16 -52 -54 -10 -20 -43 -73 23 -30 18 21 8 -98 29 -33 13 -48 18 -47 -60 -91 14 -16 22 26 108 -12 6 -6 54 -12 -40 0 -12 48 31 -33 -36 88 35 66 -37 -67 51 -39 20 -67 -57 -16 51 -16 -4 68 -33 -8 0 12 -12 39 4 17 -51 71 -59 24 -11 -1 29 47 -1 31 -27 -54 -4 -21 35 -11 43 45 25 -19 -15 6 17 35 -21 -19 36 9 -9 -5 -25 -2 48 19 14 9 7 81 -26 -48 -51 17 38 22 20 -39 35 53 -15 -6 16 -15 26 32 -17 -40 11 -18 -59 5 -51 16 25 -3 7 -42 -68 -11 -35 12 3 8 -15 -30 9 -9 11 2 -73 -27 -36 31 -3 125 29 -30 9 -38 -11 36 0 7 26 -3 -31 -56 -23 -25 -11 -9 40 -72 -16 57 94 -26 -28 -21 32 -49 -9 42 37 21 -51 5 -12 13 16 59 -78 30 -8 15 -29 34 -2 30 12 -45 0 17 -18 39 23 -45 -6 32 -5 -13 47 31 11 37 -38 79 72 51 24 -36 -5 -33 43 -38 -49 -67 -25 24 -18 -14 83 22 -75 0 47 36 -2 5 -21 1 19 -9 65 -46 40 30 -37 27 113 14 15 50 -23 0 -1 -1 1 13 -28 -7 -34 20 83 -51 -30 20 46 -61 -7 -22 23 40 -4 -43 -64 -31 8 47 -25 -116 74 31 -36 -53 -56 63 11 -97 -43 51 22 -32 19 24 -32 47 2 26 -61 19 4 55 -24 17 -3 78 -52 28 29 -42 -126 -22 -21 42 30 -31 -14 -20 -41 -120 -59 37 -20 11 -29 21 11 30 -16 -22 -13 -39 -15 -10 -18 -49 -33 -38 13 -19 75 7 4 9 -22 -38 2 5 -21 24 7 -56 18 -19 15 -50 53 -10 -31 -45 -35 14 -37 -60 -3 -8 -64 61 7 -58 8 2 -4 55 0 -19 18 -11 12 18 -42 -11 -51 12 -51 41 -23 11 16 4 -17 19 43 41 17 -8 -24 7 18 10 -4 -6 28 -16 -10 25 11 -24 41 13 5 40 44 -15 -40 -16 -39 5 9 34 54 13 -28 -9 -26 -26 -49 14 36 11 -16 5 -4 -30 -6 -19 -24 11 8 -6 18 23 -22 25 -33 -28 -42 -46 -71 -7 5 -69 -28 -50 -37 33 0 67 69 -29 52 29 -17 -4 -1 -45 8 -9 4 31 25 106 35 -75 -15 -4 -66 1 -6 86 -33 36 -112 -10 -53 -39 -51 25 38 15 -26 48 26 -6 86 -2 -24 4 -12 7 44 -56 -75 -65 36 -3 -70 -61 -6 -42 15 84 44 8 -31 33 33 -17 20 -13 60 -51 25 1 42 -4 76 9 58 -3 26 -27 17 6 8 4 18 -81 -6 -29 50 55 27 -41 29 17 46 -46 -7 -9 -5 -3 80 -20 -9 -22 -15 49 -42 -28 68 -31 41 9 41 -2 3 21 13 36 -43 39 21 15 13 -17 22 -55 30 -34 -47 -65 -81 98 27 -48 -15 -41 23 -9 5 -12 -14 31 17 -18 6 -4 -40 -15 37 11 -11 27 7 30 -53 -26 29 24 -9 -60 16 -3 -32 3 12 79 11 -59 8 59 26 57 8 6 -29 -18 -37 -28 -4 -5 -31 -11 -1 10 5 37 53 15 30 -35 -31 8 -31 7 25 -37 -5 -5 -18 8 0 -15 3 -12 14 47 5 11 22 -39 -21 -28 -4 39 -17 -8 -4 -1 19 28 20 25 1 -9 11 -45 -12 37 13 -34 8 8 -1 11 13 11 -4 45 30 26 13 -10 -41 17 -40 -8 -21 -64 34 11 -8 -34 -29 -17 27 -9 -25 32 -38 37 21 6 -63 26 -15 -21 -16 33 37 37 -9 -10 -21 31 27 -25 -6 62 -23 16 -14 7 -8 -8 -25 7 36 91 -31 -28 -13 12 9 -47 27 24 -11 37 -5 51 -42 51 -7 12 -36 0 3 -45 80 37 90 -18 86 126 -9 -16 -7 -26 -19 27 1 71 80 31 88 62 -35 -17 109 126 -70 75 -6 -28 -40 -23 108 10 47 67 44 51 41 2 44 -28 31 -121 10 84 -2 68 43 -72 97 -8 42 119 6 39 58 63 13 3 -77 -27 -4 14 -52 47 -1 54 4 -39 67 105 -24 -51 -59 51 11 36 -28 58 16 -4 89 -6 43 -73 29 -37 -4 -112 4 32 35 15 -54 4 50 34 14 -25 -57 55 29 -61 85 16 -73 55 -23 28 -85 -4 -121 -55 -89 70 -2 16 -2 57 44 -36 -4 28 -15 -30 -28 37 92 66 -32 106 34 -6 -53 49 -47 -88 40 -20 -60 54 -93 53 37 25 13 -4 -14 56 4 -18 -33 -14 -54 14 25 -44 -30 -47 -2 16 -36 11 -42 -12 -50 -5 -71 -73 -84 59 -52 29 15 -19 1 2 -49 -45 -14 -35 -37 -3 -13 18 -59 28 33 -45 2 -36 -1 -11 29 -3 43 -20 11 50 -15 9 -35 17 -14 21 2 -7 -21 23 21 3 87 -9 39 -25 -70 -45 20 33 -6 19 -7 50 -84 37 40 30 21 -5 11 15 -11 8 18 10 -87 -28 31 41 51 -37 57 3 -39 15 -10 -23 -9 11 16 -58 -14 -30 -46 -4 50 -6 -38 -28 49 0 41 19 -23 12 11 -28 10 -65 20 -22 -6 50 53 -7 -18 10 -53 15 -8 18 16 103 21 24 -33 -18 -31 45 85 -28 88 -60 0 42 8 17 53 26 12 -14 -30 -27 2 -8 32 43 34 26 -45 7 24 -15 0 -25 47 -2 43 78 -30 28 10 80 81 -34 16 -15 26 1 37 -40 115 79 71 -36 4 29 69 55 4 11 -1 -29 -90 -1 47 58 17 39 -44 -12 55 -38 52 -20 49 126 -12 24 1 97 33 44 73 -99 70 -1 -23 -2 4 2 24 10 20 51 2 5 -44 -21 -81 8 -8 -15 6 -1 4 19 -13 12 56 -12 -33 85 -25 -49 3 -96 -52 -66 -6 35 54 -22 -8 -65 29 19 -5 -18 13 24 22 39 -50 -69 13 38 90 13 31 20 -6 -44 42 29 33 52 -45 5 7 31 -37 -13 62 -28 19 -7 44 -23 -39 -16 -5 5 -9 50 19 -38 -105 -16 -28 9 -31 -40 -5 14 18 49 56 -26 -24 -22 4 35 7 30 62 63 19 -38 32 -11 48 -10 35 -1 3 45 25 2 -8 -25 -31 -10 8 -2 55 -16 9 14 83 -13 -14 -27 -46 -42 5 26 1 -15 9 29 -14 19 -25 -36 15 -17 9 10 14 -24 -11 -15 -20 45 -19 11 -15 -15 19 -11 11 26 -4 -72 -23 11 -35 45 18 75 -4 -26 12 23 -15 19 3 23 -15 44 28 -1 -4 -7 18 1 -16 -2 -16 22 16 36 38 -11 17 -6 34 109 35 -35 118 79 64 -66 78 19 49 -37 50 64 -47 3 83 15 -12 123 -68 -11 -11 3 -32 13 19 3 77 99 -45 27 -73 18 91 40 100 -6 3 85 -95 -37 -4 59 16 34 -60 -67 8 23 10 124 -56 14 -38 -37 44 -31 31 8 48 36 39 9 28 41 21 113 -2 102 27 27 -24 -24 -25 -47 -3 37 -27 -26 32 3 117 49 -78 103 -19 30 24 -19 41 -54 52 86 19 87 -16 17 -58 28 -43 -2 -16 47 10 -38 28 -26 -41 -5 89 -28 -49 -35 82 -11 71 44 13 123 38 -11 -10 -21 18 65 5 17 38 52 44 -7 6 25 -7 -21 -12 23 117 42 117 15 14 2 -55 -39 102 -7 11 -19 29 -3 5 29 -99 -9 25 -1 -26 75 4 25 -83 -17 -27 9 -38 20 -16 14 -14 11 -42 -88 -33 22 -5 -18 -29 34 33 15 32 13 63 -32 -25 -71 -16 12 -65 -1 -51 32 33 -24 -28 -10 55 63 -14 44 67 -5 -47 23 41 28 27 1 -34 -19 27 1 37 -3 -16 22 3 42 -13 48 -5 -38 -16 -11 -27 28 12 -39 -4 -2 23 -12 15 12 -44 -4 4 -34 24 -37 -10 31 9 -47 14 27 20 32 -4 -21 -35 -3 29 39 -9 17 7 -14 40 6 55 39 14 -17 22 46 64 7 17 11 16 -17 -6 -66 -28 48 44 5 3 28 -26 4 22 44 2 -17 54 122 -19 25 19 -22 32 48 83 -78 53 1 35 60 -7 -57 120 42 6 -3 32 49 54 35 63 -73 73 -84 10 10 4 49 -40 -16 -25 42 23 -80 1 18 -18 59 -9 44 40 73 -34 50 51 -4 -16 -9 29 97 36 18 -1 -8 -13 3 58 58 -16 -30 -27 -5 51 30 70 26 24 -18 28 -34 97 39 -27 9 -16 -43 91 49 27 -40 -74 4 32 100 39 -20 50 25 29 -4 -41 -55 -77 49 43 50 5 -24 -36 7 -16 -9 -6 -46 31 36 -6 35 44 -31 -18 49 31 112 -23 28 7 -8 -15 34 -6 80 9 33 29 -23 -31 7 59 24 -48 60 44 40 -13 54 23 -14 -112 17 19 35 6 -16 62 -113 51 -25 7 34 -36 -10 -46 -15 28 12 75 -34 -10 -2 -41 22 0 17 36 22 7 52 -4 18 -73 24 27 -28 32 34 -44 -10 -20 -75 -4 12 7 -42 -36 52 78 0 22 -54 1 -21 -17 -28 34 20 -29 4 5 -5 1 -4 -4 2 31 6 -7 -38 -2 4 -8 2 -11 0 -37 6 -17 -17 -11 33 0 8 -28 53 -8 38 39 -17 -28 16 20 -16 -24 17 22 7 -5 13 8 2 -8 20 10 8 20 2 12 2 -13 -50 -81 7 2 9 3 -21 -10 -10 35 19 23 -15 -24 9 -6 -24 48 -46 -28 -13 26 19 -7 -10 15 51 -13 20 -13 -6 -20 -17 -15 -7 -64 69 127 -35 17 26 47 55 51 96 6 -79 72 13 -53 2 125 14 66 24 25 -9 -27 81 14 43 95 11 49 -15 16 -22 41 31 42 -16 97 0 87 14 7 -30 19 -24 18 60 -59 84 -10 -10 15 96 8 14 -4 9 39 127 66 1 51 20 23 127 44 54 74 107 -5 1 -14 4 55 42 0 -9 54 15 -77 -11 -16 58 106 9 -60 23 -19 62 -1 -49 81 -2 26 15 114 -16 -20 33 7 39 -31 9 60 22 -16 39 -29 -45 -9 51 18 47 22 -5 58 -49 25 29 -55 -53 36 24 47 11 -8 72 -6 -24 -20 -65 -22 -25 38 91 -29 -9 25 39 57 24 87 -28 -59 7 -14 -22 -43 18 39 37 36 37 20 -17 31 44 26 21 -84 3 -42 -53 37 71 -37 22 9 48 -13 33 -2 -33 37 24 10 23 29 -20 -52 33 30 -14 -60 -46 34 -56 8 23 -16 46 -15 37 11 -24 -49 12 -14 26 -22 -11 15 -28 -6 -40 26 6 -50 -28 45 -23 -41 14 24 -21 11 -11 -2 24 -16 -14 -5 -5 -14 -41 -17 29 -23 14 36 -1 -10 14 6 -42 1 -51 6 -50 -2 -48 -32 -31 6 -17 18 -14 40 30 -8 -14 20 -16 44 -6 -89 73 16 5 26 45 -1 -6 -20 -38 38 -10 -76 -19 -12 -50 -3 1 -12 29 4 -17 -5 -10 -25 28 21 -51 20 -29 48 -58 -23 42 31 -68 31 -30 -14 45 -82 8 -39 -24 45 22 34 11 5 -1 -1 41 22 40 34 83 35 -14 -37 -7 -58 -1 -38 62 -57 8 -30 36 11 2 46 6 -22 -30 -9 -41 -38 -87 37 41 15 -16 34 -94 -13 -21 -8 64 -44 63 57 73 6 27 36 -14 59 -31 67 -27 52 -56 -65 -21 -25 13 16 -20 44 19 27 52 100 -30 -15 35 79 -17 94 74 123 33 -31 70 9 25 -87 -27 -69 -36 34 93 7 -40 47 81 -24 41 80 -87 14 47 22 -35 -69 -56 35 -12 -33 -76 -37 30 -54 71 18 26 43 67 107 51 31 -22 -1 94 15 18 -33 -29 0 19 3 -52 5 49 45 -8 -9 63 0 100 11 22 23 -9 -1 -66 -53 3 7 36 29 7 -16 32 66 27 12 -88 -35 23 -19 -50 0 52 -40 -27 -10 79 97 -116 -47 9 -45 1 -9 71 -126 -4 -46 -48 -58 -62 -65 -13 -53 -17 -58 9 -16 -11 -49 17 17 -94 12 52 -52 0 41 27 -15 -29 -24 47 18 25 -38 38 48 28 12 7 -13 8 19 -28 -41 9 13 -9 44 1 -7 23 -12 11 -3 89 0 5 3 -55 -46 -25 -8 -12 17 -38 4 55 -14 7 -12 -29 -8 17 -17 -54 -126 -5 4 -23 8 29 -41 16 34 36 46 -23 26 24 29 -22 -6 27 29 -33 -23 58 2 44 63 46 -36 25 -46 56 60 24 30 96 93 44 -5 19 69 -23 85 -35 73 38 91 41 26 -35 1 47 15 11 -22 15 -29 15 48 47 -31 6 -30 50 -76 6 2 26 44 -7 14 39 93 -42 91 19 52 -11 75 2 3 99 -16 24 47 0 -27 22 15 -38 15 50 83 60 -26 116 52 67 3 -9 -72 40 82 -53 25 -3 -36 -7 -1 92 29 24 49 -59 44 73 37 31 18 66 -12 127 10 5 39 26 31 38 63 -45 27 -24 9 25 -30 -17 39 -88 -71 19 20 -62 32 48 41 31 23 -68 58 33 -22 12 5 89 5 30 3 -39 7 45 -18 40 -13 -77 10 -24 123 49 28 46 -18 49 46 40 -1 -3 30 14 39 102 4 26 56 -3 40 21 -15 15 35 -66 -25 50 30 42 -38 7 -3 33 -9 -60 7 14 22 21 46 9 28 33 -16 -41 -20 17 18 16 -54 3 -3 22 -25 -31 73 -72 -15 -49 -17 19 11 -93 -59 40 -37 -9 10 -48 -37 -21 -52 15 -30 6 12 19 14 -35 8 17 5 -23 13 -18 14 -31 13 -5 -38 4 -50 -13 -11 -16 -19 55 42 20 2 -15 -31 -14 -19 -36 12 -9 38 35 27 -24 19 22 -2 -22 57 1 9 -18 18 -4 -22 -19 -19 16 -25 13 13 -52 24 -27 -10 5 -19 -19 16 -13 46 25 -5 28 24 34 -43 -46 -61 -6 31 31 12 -40 26 -37 12 26 -4 28 -31 -21 126 -50 3 3 -97 -44 26 74 25 46 55 26 72 22 32 -17 31 -81 -24 -18 47 113 -7 41 55 0 78 61 90 -9 -27 -33 -23 3 -10 49 102 -30 115 90 19 -2 24 0 -28 106 -7 2 -42 121 10 39 -120 24 -12 -61 -77 -6 -40 -58 -13 -124 -46 19 10 47 -37 -2 3 8 -48 45 -18 12 -65 28 23 13 -96 -60 -8 3 -25 69 -16 97 -49 -57 64 81 71 -9 -12 95 -8 20 -36 -21 -34 -38 -23 -77 97 -29 -99 -5 27 -107 16 32 63 -16 -24 11 -11 -9 -66 -2 -33 -45 68 25 5 -14 -31 46 58 34 64 -14 8 -35 -108 -18 73 -70 50 -74 -43 -57 16 56 49 -29 -31 31 -9 62 32 -5 3 81 24 -5 -32 58 60 -43 -81 -30 48 60 -38 75 -62 -17 -70 -1 102 -51 -32 49 -29 34 18 47 -40 -63 -18 93 -14 -45 -36 16 -45 -28 23 -30 42 -25 -65 59 -32 54 18 1 -42 25 31 -6 -24 14 34 26 -38 60 -11 -47 -30 -22 -28 -107 -72 28 20 -4 15 9 35 22 -36 29 -54 45 3 17 -30 16 56 6 36 -25 9 30 4 14 -10 -11 27 39 -24 127 -19 42 -42 -26 1 17 16 35 0 5 15 -21 -55 -6 27 41 2 -10 44 -18 -55 36 -3 7 0 21 31 20 -75 -35 10 11 4 -126 12 -24 14 58 69 -13 5 -41 7 48 25 35 44 -9 17 -23 81 -5 81 -42 33 30 64 21 -48 -51 -18 7 37 -13 -36 86 -28 -8 -108 -18 34 -39 51 30 21 82 59 -90 -56 37 60 -54 26 -20 41 34 -27 34 -86 -30 10 5 26 54 -73 106 -26 27 -39 27 61 -90 79 -13 27 38 89 -14 5 -44 -32 19 -22 -47 -12 -41 -13 -33 -73 118 -8 1 -11 -1 -23 53 27 -19 -46 35 -27 41 -51 -13 34 81 51 -41 27 66 41 -53 -30 -41 -70 -11 25 15 -21 58 31 35 11 20 -8 -3 -45 23 13 -8 17 -25 5 -13 -43 28 -3 17 23 -20 48 -44 -26 -13 -53 31 7 7 -62 16 3 22 72 -14 -29 -28 7 39 -22 28 1 -7 23 -62 21 27 -26 18 27 17 32 21 1 -44 -17 -40 -68 -9 32 2 77 -38 -15 -6 -3 -37 -40 59 9 17 46 9 -2 63 33 -14 -39 54 -17 -30 -48 41 59 -42 -28 41 -35 13 21 -39 28 -8 -20 -34 -31 -21 -7 -7 -8 17 -27 42 -39 -6 6 -34 -33 -39 43 10 -47 -44 -21 -22 -11 -23 -26 -10 -20 -13 -27 75 54 6 -3 9 -10 29 5 38 -20 -48 -17 -2 24 -42 12 -4 -36 4 25 -44 -5 15 -7 -12 2 -8 6 17 -27 23 -25 6 5 -19 39 -52 -44 4 -15 17 8 -18 14 -13 0 -6 -7 -10 -4 -8 -8 26 12 5 -39 54 -10 -10 -29 -32 -22 -9 17 94 -27 29 38 11 110 -86 7 41 -23 13 60 45 39 45 37 10 59 62 -41 -104 20 31 -37 -5 28 18 37 -34 -8 -32 -4 63 39 70 6 -15 126 -20 -73 40 30 15 21 62 -25 -63 -2 35 1 10 -5 13 48 -66 1 -2 64 15 54 23 -30 53 61 -5 18 -77 123 -43 65 39 15 74 -41 -109 11 18 -4 -6 24 1 9 -61 18 51 54 -70 66 -17 34 16 -19 3 -14 -4 -98 40 -1 16 1 -69 2 4 18 62 -33 -4 -36 31 42 1 -75 -92 17 27 7 -30 48 -61 38 -1 6 101 74 -40 24 -17 -19 -20 -94 -17 2 -19 16 2 -20 -18 32 46 2 -11 -30 10 -53 -40 19 -66 8 7 72 -16 -53 94 -55 12 71 -93 -20 72 7 18 -20 -2 23 -83 25 -22 33 12 9 20 -14 -81 -77 14 84 -105 -54 -49 -71 67 -58 -12 28 32 27 18 -24 20 -51 21 -44 10 -24 -3 -68 -36 -1 -45 -10 -3 9 41 -1 -57 6 -5 1 -17 -23 34 -23 27 46 -29 -35 -10 -8 -27 -20 8 0 -36 0 51 18 41 -14 -37 66 -20 12 -40 -4 -4 -44 69 -16 -18 -15 22 -25 42 54 -64 -24 -13 21 42 20 -15 20 -9 52 18 -14 5 -19 24 -3 -17 3 -13 -12 3 17 4 -51 12 37 7 16 32 42 -15 31 -21 -32 34 44 12 41 -32 -34 42 -3 77 -11 26 -29 45 27 -4 2 -34 -18 36 -118 48 3 31 -31 -1 -22 27 15 -45 6 -34 56 -16 28 45 25 87 61 -35 28 91 -12 84 62 68 -35 -3 18 -25 -35 -48 43 53 39 59 -34 -44 -30 -4 7 30 68 2 120 -21 -14 54 21 1 47 39 -3 70 64 62 -7 -71 31 15 101 3 36 15 8 6 10 21 31 -14 -3 -60 0 16 16 38 -25 -3 17 18 24 -4 -39 -26 21 -35 -6 -22 -14 46 -50 -16 50 -8 -49 -35 32 -26 9 -12 -9 44 10 -15 29 39 -15 11 60 -5 18 0 -52 -46 -10 1 -26 8 1 -30 50 1 -14 47 -74 -17 26 23 41 16 58 0 -19 7 4 -46 -22 38 23 -48 -20 18 -41 34 -26 -19 43 21 -29 16 11 1 -16 21 28 -36 17 -2 5 21 43 -3 20 9 -26 32 -2 -12 -39 -14 5 -61 39 13 39 64 25 51 12 23 -12 36 49 -4 -24 8 -4 -43 -50 59 -18 0 -4 46 -53 22 1 -34 5 -5 -11 -12 -5 -26 2 -5 -12 -33 43 -36 11 -1 -11 35 25 -23 -5 54 33 -11 -15 -29 -4 -7 -42 -12 10 15 26 5 13 -4 -49 -4 -9 -49 -9 13 16 -26 -6 -12 44 19 -54 31 -16 59 38 -14 19 -16 7 -14 -50 19 -41 23 -18 -43 20 -12 16 44 13 -45 11 18 -14 -4 76 6 16 -33 -11 -7 30 75 -56 21 -3 29 -16 95 15 -32 -9 22 -3 103 2 -104 21 -79 80 -27 70 11 33 68 -6 -43 27 10 20 -29 15 15 10 10 -11 12 21 -4 -47 -22 26 86 -29 65 46 84 -39 -103 -33 -8 32 107 25 32 103 37 55 65 -6 -2 19 -38 30 -110 -15 85 -35 -26 -53 55 85 43 -45 64 67 -54 42 -1 -3 47 63 -1 66 -3 51 -112 -23 -4 -9 -62 26 16 66 1 -31 26 1 5 40 -6 -67 -14 57 -59 37 52 71 -27 27 -15 -23 -32 28 -36 -46 24 49 -11 34 -51 -24 29 -29 -19 46 -53 31 22 43 15 23 -14 -8 5 39 18 45 28 43 7 -40 26 20 35 -25 33 17 23 -78 -30 -31 -3 8 18 34 -25 42 18 0 -25 -28 10 -6 -3 -105 -23 -7 7 51 17 84 -41 -8 -22 9 -2 -74 -2 -61 -40 12 6 26 23 -26 -57 -8 20 -8 38 -19 -11 3 -11 -12 11 36 -2 30 -37 -8 29 52 -20 33 9 7 25 6 -7 -4 33 22 -30 19 -52 42 9 72 -5 -11 2 27 40 -8 -30 -17 -23 -21 -65 10 -25 -12 20 -19 -26 10 39 -41 36 1 23 6 -20 -10 -30 29 -28 -39 31 -7 -11 46 16 -29 9 12 -39 2 -9 -27 -8 5 -15 7 -20 -69 -7 5 33 -1 39 1 -38 -30 -16 -5 29 64 5 -32 39 -33 25 21 11 -13 -2 10 66 32 29 26 -65 11 -23 -47 44 -63 24 -27 11 -85 6 10 -19 -40 54 25 -13 48 -33 4 -5 32 42 21 -4 30 -35 -16 -9 35 71 -1 -2 -57 43 6 -15 -10 -41 118 18 6 -26 2 33 -51 52 55 -44 43 -26 54 33 -9 23 6 -14 -65 37 7 99 -14 -27 15 1 -62 24 -34 -5 24 15 -23 50 31 16 -7 15 7 -28 -7 -25 -31 13 -97 -16 24 -25 -37 48 -24 23 -22 -38 -44 -21 38 5 -33 -19 60 115 23 32 -31 50 61 -31 10 30 3 19 -59 -12 0 17 -98 74 22 26 30 50 -79 31 18 -78 67 -59 -4 -22 83 -16 33 -72 -61 -51 15 38 24 34 -28 45 23 27 -37 6 16 29 -9 -43 -76 19 48 12 56 26 2 -30 4 -31 -3 -19 28 -21 -50 52 -17 5 -20 86 -59 25 7 63 25 -22 -65 -36 -8 -33 57 -73 -31 -9 7 -15 -3 21 -20 50 -46 25 -12 19 7 29 41 -51 9 58 -1 9 2 -15 -31 -30 -45 -24 -15 -67 -6 -57 -31 -6 -30 -13 44 -2 59 1 38 -77 -15 -13 12 29 14 -26 6 -41 -6 -9 8 15 -2 14 -16 -6 3 35 -11 7 -8 41 6 -38 -75 18 -42 4 34 28 -9 -33 14 36 -7 52 -3 20 4 53 -17 25 12 -22 -13 -25 -29 43 26 27 5 -8 -4 -44 -11 -1 -14 25 99 37 50 -4 14 10 0 28 8 -108 40 71 29 121 23 13 63 -40 -12 23 31 -65 -7 -24 78 -63 25 27 54 -11 12 -52 2 -20 31 7 -40 19 26 48 6 27 -8 -31 -54 3 -2 -48 -28 -21 100 21 -3 -3 -27 -21 10 -26 -35 -32 -6 -89 5 37 -30 -64 72 38 50 37 -30 -28 23 58 100 2 -37 -49 -91 10 63 -61 75 -23 2 65 16 -31 24 24 56 1 53 57 16 36 79 21 0 8 66 27 -52 1 63 15 13 -69 21 -5 35 -23 -1 -36 -69 15 11 14 -2 1 -28 15 -18 -89 20 97 -32 16 34 112 31 34 -19 -5 -26 -22 -60 -16 0 37 74 52 -12 17 -9 30 -15 47 15 113 -42 -15 40 25 23 35 -38 -8 -3 32 -32 -31 -9 -34 -19 50 -22 25 53 -75 -17 -15 44 74 7 -33 6 -67 -49 -5 76 36 -48 -20 -1 -7 -71 -18 -12 7 4 37 -38 -56 -49 -26 13 31 5 -32 -5 9 -29 -5 2 23 -20 -22 6 10 22 -8 -25 19 19 9 0 2 8 47 -13 -53 7 9 55 49 -1 34 35 -33 -23 12 -22 11 25 -25 28 3 -21 8 43 -1 -2 -17 18 -28 15 -17 -21 45 -16 12 29 -7 -11 -12 14 5 -38 -2 41 -23 15 0 18 -13 34 26 -41 18 51 -6 3 -20 10 19 18 -29 2 -18 -7 -26 0 8 102 -7 32 7 -32 -17 -51 47 31 90 -17 45 61 -44 90 11 47 85 46 31 54 17 21 54 2 37 26 -1 32 -30 26 52 36 16 83 -32 59 -15 -19 -19 45 16 38 82 13 20 -16 60 41 27 -24 127 69 69 -31 -11 -76 35 -10 -9 64 -28 16 21 22 23 19 7 45 23 44 -3 40 17 39 15 8 -21 50 26 61 48 46 79 -3 4 23 85 3 11 59 84 52 84 -106 26 101 54 -4 -39 70 40 36 89 52 -34 -23 13 -28 -35 -9 15 -24 5 -11 42 -56 -8 -20 11 12 21 -18 38 -40 21 46 21 29 32 -1 -12 8 3 -18 20 -3 50 -4 -18 -24 22 14 45 -42 -12 -13 18 32 -17 81 96 -45 63 15 -48 16 -46 52 7 -37 53 22 -35 8 13 11 36 -39 29 41 104 -38 -22 20 -30 -5 44 -31 -6 11 -37 71 -26 -33 -58 -35 -53 -1 -6 -2 31 -11 10 -9 52 15 -50 -8 -22 -13 10 1 3 16 -13 -48 15 -26 -6 -23 8 -39 25 -4 -10 5 -48 -33 7 28 -5 -21 19 -6 -11 34 4 1 -57 36 10 34 33 -20 -6 -11 0 7 -24 -11 -19 5 19 -20 -11 -8 45 27 -25 -5 -10 23 18 20 -51 -23 4 13 18 21 24 29 10 -17 9 -23 44 4 -20 42 -72 5 21 -63 33 -63 -12 -14 -9 20 -11 -38 6 15 -8 7 -54 -9 43 86 -17 -22 21 -21 -23 25 -2 3 -81 -5 58 -64 -29 55 -61 59 -32 127 -19 100 65 -44 37 -2 24 90 -2 11 -17 44 -39 88 30 88 20 87 -39 -75 22 -24 -11 16 75 -19 -91 40 39 -25 0 72 -29 11 -14 -42 -34 74 16 9 13 -28 27 93 36 16 50 90 0 27 49 28 -46 7 -28 25 0 7 20 -17 1 27 57 2 26 -29 18 31 5 67 -55 60 62 17 0 -71 -28 -10 -13 -12 58 67 38 -30 40 -4 21 -18 20 -42 5 -24 -5 22 41 -4 96 21 5 -31 -38 -7 -64 -128 -13 -47 55 76 9 -47 22 -58 -18 24 19 -6 21 39 64 1 45 -33 -55 94 40 -7 91 -18 -23 11 35 27 56 64 -10 -21 26 0 22 69 34 -21 -11 -49 8 37 34 42 -37 -13 -6 -99 67 -16 21 -1 -65 39 35 5 -39 -52 -51 65 -27 7 4 -5 28 -24 24 -12 -15 -1 2 33 25 -26 -48 15 -50 0 13 -67 -26 -9 -35 -48 -16 0 -26 -31 27 7 -21 4 15 36 -14 -14 1 12 -26 20 17 19 33 11 -28 4 -3 -23 -20 -10 -47 13 3 7 42 2 31 2 7 2 23 18 -4 27 -8 -42 15 25 22 34 -25 30 5 -4 5 67 -6 -21 13 -76 19 -17 13 53 -12 -58 43 11 24 11 24 58 7 14 26 -59 17 -17 29 39 -4 3 23 9 -107 23 -24 6 -30 34 -22 8 -56 -34 -53 0 -7 -51 -31 -35 -91 15 9 -11 -85 63 -28 -35 8 -39 -13 -74 -19 -15 -14 -25 -20 85 4 39 55 37 -22 0 -56 42 62 4 56 12 -29 74 -78 -12 -58 68 -25 44 23 -71 14 -22 -51 6 -30 2 4 -46 15 60 0 -6 14 1 -48 -33 -50 -33 21 14 -57 -36 55 -42 44 -4 100 -73 39 -1 -23 -17 -15 59 -33 83 67 -22 43 -19 -78 18 97 -54 -5 12 12 20 29 42 -17 -31 20 35 -44 -5 78 10 -15 4 -61 23 -30 89 1 -17 -28 92 21 -33 -23 32 9 14 -26 -12 1 41 29 -21 38 -33 -5 -86 35 43 6 2 -22 -39 -49 -29 8 10 40 -31 55 -16 -18 8 -37 20 66 -40 32 9 7 -25 -36 4 -41 17 53 -68 -28 39 -1 15 -10 -22 2 -40 39 -23 6 16 -9 14 3 22 -3 17 19 73 8 -23 44 8 22 15 22 43 -47 73 25 25 -12 -11 -13 -1 0 -38 -29 -52 -52 -3 15 -22 16 29 1 7 -15 -3 -6 -28 -24 5 8 -24 -9 54 -13 -17 -2 -4 34 1 31 -22 3 -40 34 25 47 30 48 -19 -27 15 -22 -1 -12 29 1 -14 -26 4 29 -34 36 20 8 25 -13 1 27 -31 6 0 1 19 -17 -24 1 13 1 27 21 -22 -9 51 -60 12 -3 -25 35 46 14 -22 14 -39 -5 39 -38 -19 26 8 29 36 -28 38 120 34 -34 -42 42 58 26 -17 74 71 -82 -10 50 -30 14 97 15 -9 28 51 26 21 -17 -10 -13 59 -4 20 25 46 -85 12 3 24 11 60 -24 -22 39 -4 -10 24 33 58 48 32 29 62 0 -56 -11 9 -49 10 -30 50 2 56 -15 33 33 5 -49 -14 58 50 37 -3 106 46 42 -79 93 52 47 63 -32 47 -6 37 26 -54 -38 56 86 41 43 74 7 79 -42 -2 34 -23 -65 -4 24 -2 25 -70 24 -13 -6 15 -45 -26 -47 91 -18 -25 -40 -7 -19 17 -12 -3 68 -16 29 -25 1 49 11 34 4 -50 -2 -20 -25 44 23 2 -3 86 34 47 -29 -30 -13 18 22 2 54 -1 2 4 -42 -1 -61 18 -9 42 11 24 49 -9 -96 -21 -20 -11 30 59 -15 -19 -79 4 30 -1 5 -33 -6 5 -1 -18 -53 21 45 -15 6 -9 -5 -43 -20 12 12 -78 -27 37 4 25 32 35 -8 22 13 -21 -8 -36 31 -22 -3 -2 -11 -49 3 -12 -36 -23 12 -43 25 -16 -19 -13 -7 14 -31 -11 -9 -30 -16 -35 -13 10 19 6 18 -13 22 -14 26 -9 -9 -24 -4 -4 -27 -7 -34 33 -5 -3 -2 29 -1 12 19 26 8 32 -46 -8 25 -44 4 -7 18 10 -5 -12 -48 35 -47 22 -29 37 -12 -31 -5 0 -24 -19 -28 13 -23 -17 52 17 40 -48 -32 -32 5 17 -24 4 -24 15 25 33 -43 -8 25 4 31 63 -20 45 -34 4 24 22 -77 -14 -61 -124 -31 60 97 -6 -11 -12 1 -46 0 52 66 -22 -15 -15 -20 -31 27 -39 -4 -57 -12 -51 29 0 18 85 37 -4 -31 32 116 -27 64 1 75 -38 16 -13 19 12 15 -4 -16 31 3 7 -63 -82 74 60 27 34 25 38 61 27 -12 36 -36 19 -14 47 16 -15 -35 52 25 -43 14 15 -4 41 -32 21 36 68 16 15 8 -36 15 -19 16 36 -20 -36 -87 62 -82 -58 44 112 41 51 -67 -5 -11 14 -3 5 -6 -17 7 -4 37 37 71 95 -27 4 25 66 -65 -18 30 42 -20 -17 -22 -7 58 33 -27 10 58 -55 30 69 25 61 5 -51 7 2 -14 39 -2 -10 18 -12 -24 3 -28 -79 47 -13 28 -6 -15 39 12 0 -8 16 -22 9 89 8 -14 -45 -16 23 -2 32 -22 43 6 -39 48 38 32 23 -28 -15 -8 32 -7 16 6 65 -18 -9 40 -13 44 -3 15 17 -6 -20 12 6 29 13 68 -8 38 39 19 19 -49 2 -1 1 -20 -22 -2 -12 42 -19 -10 -5 23 -17 18 -35 21 -14 -9 19 23 11 27 -2 9 16 -10 -31 14 -9 -43 51 31 -20 -16 10 -23 -22 -6 -2 4 -26 3 -65 -19 36 8 20 -8 7 18 -20 15 -12 -15 -50 7 6 18 65 38 -11 -15 76 -27 -44 23 -32 -43 53 -58 -54 -70 -67 -61 -32 46 14 -17 -52 21 -3 -13 19 29 24 -89 -47 -37 -54 96 10 -15 -39 4 -6 -59 -25 39 -38 46 51 -1 -33 -46 3 -40 2 20 -38 78 -97 -127 122 -47 57 35 -31 37 -5 -45 -24 9 -33 -87 -64 -58 -9 -89 -74 -65 -51 -60 -53 -23 94 -15 -60 42 -47 -17 -12 -18 20 2 -6 -48 -5 29 42 -6 33 -1 46 30 6 -13 8 -77 -48 5 5 26 6 22 58 -3 -5 -13 -4 -45 29 45 37 -2 12 -9 -10 -24 45 -14 29 -76 -55 -9 46 -20 42 -40 -12 -12 50 45 -5 50 -103 -16 -59 9 37 11 -21 55 57 38 9 1 -35 30 46 -18 -68 98 31 6 71 0 -56 0 2 15 -23 -35 -51 -6 52 -78 -32 4 -16 -48 -48 24 -74 -4 33 -53 39 -19 -19 -13 -94 -8 -14 9 -17 56 -40 20 -15 -12 10 -18 9 -24 88 30 20 -10 11 42 -28 1 -14 -10 14 -24 46 13 -23 -36 2 5 7 -11 19 13 15 11 11 -27 47 -16 4 36 -14 -29 -44 2 -30 18 -16 -16 -14 -23 34 -12 1 11 36 -27 -29 -39 29 4 -11 36 19 -28 22 -39 25 -6 -4 28 13 -28 -18 11 9 -35 -58 -12 52 -45 19 -16 67 3 -41 11 -11 25 53 -8 36 15 19 19 31 20 24 -23 -7 52 -7 59 -2 23 -51 13 -16 -33 -2 25 -52 19 -47 20 15 -29 -1 -43 -2 52 -16 -1 -14 91 -7 32 -51 15 -8 -32 90 -39 -50 15 -16 0 52 22 75 23 76 17 2 41 52 -33 23 77 102 18 -3 14 -19 -29 0 -127 23 112 -19 9 -19 4 84 49 -52 58 38 27 69 1 55 52 -7 -53 16 -38 45 15 78 -69 -35 -12 61 14 85 7 7 4 -44 57 43 -11 8 -9 35 -26 -70 -3 -42 -8 14 38 -23 20 -14 -43 -63 79 -24 -81 -21 -71 4 24 35 33 60 -93 -65 -34 38 39 -1 6 -2 -49 -35 -70 0 16 -18 43 85 -7 31 -72 5 27 -13 50 -27 8 -31 -2 -49 -3 5 -5 118 11 87 -8 9 36 54 23 -32 49 118 -57 -5 -21 -33 -28 27 -29 35 -26 -6 -29 -47 27 -71 -7 23 -11 -76 -93 32 26 -25 -41 -50 15 -68 8 -68 34 67 -8 53 22 -78 -4 43 -44 92 -69 12 -27 48 -14 -49 -15 -46 2 -35 -8 -23 4 -38 -25 23 -3 15 74 -13 -5 13 -27 -20 9 -13 -58 27 18 6 -7 7 72 7 -49 23 10 12 17 37 15 -52 50 28 10 -26 45 -23 -27 -26 -6 -71 -44 -11 -3 -28 31 -16 25 -1 15 36 17 -16 -27 -10 62 8 -42 -15 22 7 26 24 23 23 19 -86 6 28 -35 13 7 -15 -8 17 0 -24 -13 -66 -8 13 20 14 82 -26 -28 18 32 39 -21 -1 7 -29 27 -40 -9 -24 -59 -69 5 -51 28 48 -19 -7 33 -62 20 -24 -41 -26 -5 -6 27 -43 20 4 40 36 -17 11 27 -21 4 4 41 -2 4 20 -1 14 -23 -14 -68 46 -10 98 3 30 19 -39 48 58 -13 -27 70 19 34 23 46 -18 -46 -26 -70 81 -36 14 -24 7 -36 73 34 0 -8 -4 37 -86 -11 20 -1 4 55 -18 -84 5 -44 27 43 -19 -110 28 -46 -72 -9 -75 63 -28 -34 -13 -60 -8 -4 -21 8 79 36 31 -26 -39 20 3 -44 87 -35 -25 0 5 -62 -37 -3 -6 71 -42 -10 -21 46 -36 -4 7 -63 22 10 -48 45 1 -37 6 -6 -33 7 -9 45 20 37 52 34 -29 18 44 1 -45 5 15 24 -72 50 65 18 54 -27 -13 42 1 10 11 41 43 -25 5 -31 31 15 39 47 -50 -26 15 55 -6 -45 4 -22 -24 19 -18 43 -26 -25 -23 -23 59 -8 -32 46 1 -5 -24 -8 -55 -26 40 -19 -5 22 14 -39 -35 7 39 12 -1 -14 11 -19 18 50 3 -24 -16 -26 34 5 -20 13 -1 -50 25 -19 -39 -29 7 8 10 -17 -1 -48 21 20 -3 -21 5 -37 -1 1 -14 -13 30 40 -17 2 -99 6 38 3 44 -21 5 0 17 27 -51 18 -7 19 -46 34 12 32 9 -17 22 14 24 17 40 -22 -27 5 4 -24 9 73 27 13 43 8 59 23 37 88 78 7 22 89 67 36 15 42 43 -37 -23 -71 -5 -20 -23 -17 65 36 20 63 -55 16 40 38 52 63 -16 -4 66 120 -38 20 -5 20 18 72 20 -42 14 42 -20 1 23 -69 -42 85 -12 4 23 31 -20 -19 -37 -8 -32 -26 38 0 3 -27 12 71 37 -12 -11 25 15 20 -83 -50 25 21 -71 -13 35 -35 -14 -6 10 -20 16 9 -15 9 8 46 11 1 61 14 33 40 33 25 -33 -70 -9 -39 -27 -6 102 -18 -46 83 1 -17 -22 121 43 -39 33 23 -1 33 20 3 3 71 24 -14 50 26 -5 44 22 43 -9 -28 14 -16 125 20 -12 16 -69 -4 31 -23 33 -94 35 40 10 22 -22 -11 46 -13 71 116 -41 67 3 -3 0 -34 15 16 -21 -53 23 -16 6 -30 84 -27 -43 38 23 -26 -33 32 3 1 1 55 -1 -21 -14 45 -21 31 18 -18 30 -60 -52 -45 46 -13 19 -54 -18 30 26 4 7 33 48 -28 -58 3 21 -4 -24 31 -19 -13 -24 -22 -22 1 12 24 -33 -31 -10 57 40 -49 -1 -32 -12 -2 -15 0 27 16 62 -7 -21 -13 -11 12 -4 -10 14 -20 -5 -14 -45 -20 3 4 -26 6 -79 65 17 43 -34 -11 -50 -15 -54 42 14 46 9 37 30 -9 -27 11 -55 10 -1 39 -19 -26 36 -31 -51 -5 -6 -44 -26 40 -7 39 -1 -52 6 31 -25 51 -43 30 12 74 57 3 0 -5 -27 6 -21 43 9 -66 107 55 -11 90 32 7 -23 56 88 127 -33 5 29 8 89 93 17 16 -12 -39 -9 43 27 44 11 -4 53 78 46 127 77 1 27 62 -34 51 77 49 44 6 121 77 60 18 6 60 -2 37 57 60 4 3 4 28 -23 45 66 39 19 5 54 -66 57 -36 -4 -17 5 6 39 -14 53 -10 -28 86 -4 -1 49 -2 56 42 77 36 -28 123 71 -25 62 -42 36 66 -15 55 -2 44 43 12 -16 1 20 -14 -31 23 3 -3 -31 -54 -54 -34 16 -1 3 24 30 92 59 21 1 -8 -64 -68 -56 -16 -66 1 -58 -17 -26 41 38 23 7 -15 -11 -42 21 18 60 19 -7 23 22 -82 -12 25 2 90 16 17 66 49 2 19 19 20 16 0 16 20 44 17 -20 7 -24 -9 -83 -18 -81 -25 -10 16 -39 -30 -4 -40 -8 -55 -16 -6 -15 -61 -2 60 7 -19 -17 -20 1 14 2 -4 -10 -39 35 -7 -43 3 9 3 21 -20 -24 6 -13 -30 -52 -23 21 37 -6 -24 -2 -21 -21 29 -43 22 -10 1 49 -21 -12 35 17 -6 -39 21 -1 -12 -13 25 -2 -2 9 -12 -21 -4 0 21 3 -2 22 33 -42 32 -3 10 -5 -10 -9 55 -27 -8 15 -14 -40 -27 8 -12 -7 15 -33 12 12 -29 -5 -20 -11 21 26 23 7 17 9 29 2 -3 -8 -29 -36 -86 -25 3 19 -80 54 18 78 -6 -13 39 13 8 58 18 87 24 30 31 -21 -30 42 42 -52 -12 7 30 11 34 -26 -38 -55 -54 7 -11 -75 20 -2 84 -22 43 32 -3 -10 0 14 58 6 -94 -5 29 -28 46 -60 -62 50 -22 -85 -15 47 -33 45 28 -19 56 -16 -28 40 28 10 47 40 32 36 -10 7 16 34 -18 -28 23 -12 20 28 -52 -20 74 83 29 -50 76 -38 126 -24 -39 -2 -30 33 7 29 -12 -8 -9 -34 3 37 -31 19 -8 14 17 -16 -38 -10 25 4 15 6 20 22 89 20 -31 -11 -22 5 29 51 6 -10 -57 49 48 -30 36 -40 -16 13 -6 39 -21 -51 71 40 -65 16 -14 63 7 -52 -26 36 21 -10 47 -22 16 -27 -12 1 6 -11 -23 1 20 -37 17 -20 -101 1 -17 18 61 24 3 -78 -25 28 37 -5 -77 -40 42 9 0 58 -23 6 -7 37 11 13 -30 -28 32 24 77 37 -15 -45 -10 10 -43 -8 12 8 -9 10 -68 -17 50 8 51 -36 -23 9 44 -6 -9 -15 -40 -7 -10 -35 -44 13 -47 -33 45 -21 17 10 18 29 20 -2 17 8 -1 27 1 1 29 -45 1 -34 -3 -14 3 25 -17 -10 14 21 -9 -6 -3 39 48 -7 28 61 17 -16 -28 5 31 -5 0 -15 -19 -11 1 29 -10 41 33 0 19 -15 5 34 -19 10 8 -17 -95 43 20 -70 40 52 -44 11 -25 18 -32 -1 24 -55 23 13 -1 55 -3 23 12 -1 21 -59 -20 -11 36 21 116 2 78 21 -71 17 -125 -43 40 -2 22 -103 37 -46 -116 -27 -37 2 54 -9 21 -21 6 20 -9 84 -20 46 46 -12 -31 -82 -38 82 -44 20 -27 -6 125 24 21 23 48 -26 -18 34 -28 28 33 -43 67 55 -54 50 -10 38 -5 -11 -48 -43 86 49 19 8 120 -59 -46 2 43 -50 35 35 -21 -19 -2 -34 23 30 26 32 14 -20 52 -19 -52 -11 -66 3 -3 6 48 12 76 29 76 -69 35 9 23 59 50 -32 -80 -45 -46 18 115 -103 39 21 -87 54 14 58 -8 -91 10 21 57 -6 112 32 -38 7 4 19 -65 20 -3 -26 -26 33 -2 22 6 27 -24 103 -48 -42 -22 -27 32 15 28 -104 -30 14 1 0 17 24 -26 -28 -16 36 3 -13 -18 45 114 -14 5 -40 15 8 -3 -32 47 -23 46 38 22 40 -42 19 -22 -11 24 31 22 -21 -40 25 34 -5 -20 -7 -24 51 -9 -22 -18 -7 -5 -17 10 -18 -25 -6 -73 -47 24 -9 117 3 35 -4 -38 -41 -24 -15 46 23 23 -13 -20 -24 -27 -8 -7 8 -18 -47 -15 -67 -43 -9 -23 -16 -13 -23 -2 1 -2 53 -11 44 -1 1 36 -12 -18 -27 54 22 -17 5 6 -7 52 20 -19 -8 24 3 40 -16 -71 50 80 34 -80 -124 12 -20 -63 74 83 -28 -74 125 53 15 -61 -35 43 -85 95 37 -62 -9 64 19 54 23 124 -32 52 -83 70 -71 76 8 -8 -1 57 44 15 3 57 58 -52 -11 62 -48 23 -87 -118 -6 -16 -37 -1 85 -10 44 -13 -6 -30 33 -40 -108 57 -2 23 36 18 25 -80 29 60 -63 125 -13 12 -31 35 -2 2 11 -35 57 38 -54 -13 -14 -39 -16 33 15 -41 31 67 6 42 -33 72 -35 112 77 -10 23 -40 0 67 -121 -7 36 9 -119 63 63 49 -91 -77 -55 27 37 -35 53 -52 12 45 5 -71 -61 28 49 -100 86 17 -68 -14 -18 38 7 -66 57 -14 4 24 -117 43 47 33 -13 42 41 125 89 -30 -16 47 -5 -56 39 -33 -99 19 -41 -40 14 16 -85 -100 -44 -19 -18 51 -121 -57 -8 21 23 -71 10 -11 27 -54 -8 -45 -6 9 21 -9 -17 37 120 -59 23 -16 72 1 -4 26 33 6 -6 -24 -24 -96 -41 40 -72 -51 -10 12 24 -27 -39 46 12 -26 -35 8 -1 5 38 46 -46 -5 -8 -6 13 25 17 -25 55 -15 46 118 64 4 7 -58 -35 -11 -4 -13 -51 18 5 22 -28 42 -7 -11 3 6 118 -22 27 7 -82 28 51 -57 28 11 4 29 -51 16 -14 -25 49 -12 -19 -14 -47 8 -16 29 89 -2 18 27 -119 -7 3 14 -32 -19 25 80 9 43 22 3 -84 -7 -17 62 16 64 -33 -3 118 65 -1 -50 -8 -91 67 -11 -19 55 71 37 24 49 -8 40 12 79 -101 8 -6 19 -40 -5 39 40 -31 44 -12 -15 26 -34 12 -62 -13 9 -36 33 51 58 -40 -22 -76 18 36 19 -43 -15 1 65 -54 31 6 123 -78 -63 8 37 -12 -12 -30 -58 13 2 5 65 79 10 -76 24 -52 19 -72 58 -1 28 30 124 32 -36 14 -33 16 16 -5 -61 19 -116 45 -19 85 3 -1 100 -32 39 -31 -81 7 -12 -34 24 -64 -25 -9 -70 68 -33 26 -22 106 20 45 -56 -10 18 39 -10 -28 27 62 38 17 6 -57 112 32 35 56 -35 15 -19 81 -6 75 -11 13 82 -72 -28 48 80 32 44 67 -25 73 25 -51 40 11 21 -9 -36 -22 -108 -24 35 -19 15 7 13 13 -25 -32 36 -55 -2 -45 -27 -31 13 21 -53 18 -2 27 -109 42 79 -69 34 28 1 -35 -29 -27 -72 13 -48 1 -11 7 -23 2 20 -40 2 -24 5 40 -7 14 10 56 47 58 -39 -1 -49 27 -38 12 39 -15 -17 -41 -2 13 -14 33 -6 -23 -50 41 6 19 -18 -19 -3 -25 -16 2 -3 31 14 -7 28 49 28 23 2 23 -5 -27 -15 -1 3 -39 33 4 -72 -11 -27 17 -18 -3 3 16 34 7 -42 -3 -10 -61 -2 -21 -26 24 28 2 7 27 -5 16 30 -16 12 -36 50 26 127 47 36 4 34 43 21 27 -25 22 67 13 23 64 17 19 34 80 84 -42 -7 48 28 6 23 16 13 26 33 22 53 -3 24 -28 32 -36 52 -66 4 6 -11 20 33 6 -29 -49 90 18 67 56 3 -18 30 9 3 49 8 1 2 40 49 -6 5 99 7 104 43 -18 -2 5 21 -7 -46 -38 -24 -33 59 25 -31 28 7 9 -26 80 40 -44 -27 66 7 54 -8 1 22 22 -34 -13 47 -11 -3 -50 38 -19 46 -3 15 7 4 19 -21 17 49 95 3 -19 12 -22 -7 -20 -19 -55 66 72 -23 6 67 -50 53 16 80 22 21 -21 -58 0 84 75 32 40 39 -10 59 -41 -9 -3 -8 -31 17 2 28 -102 -36 -62 19 31 7 36 20 50 61 -48 -13 29 40 -97 54 38 26 28 -29 -32 65 -17 -25 -17 -14 -45 -3 -17 -77 23 55 -34 5 59 64 -4 -29 18 -29 32 81 14 65 14 -33 -8 -14 6 24 86 -3 -66 8 35 -17 -24 -10 20 -50 -50 -18 -33 -35 -2 14 -3 -28 -1 16 15 -23 -25 -81 -21 -36 23 -33 14 -12 1 -8 2 -33 -4 -6 14 30 -11 27 0 18 -27 -31 -30 -25 -25 36 -65 -12 27 -20 -18 -10 11 48 3 -22 -25 -22 0 -43 -22 -29 -11 -39 -4 -16 5 6 -13 -26 -7 -30 22 12 -17 -3 12 13 -7 -2 -17 -37 2 46 -14 -25 -12 -28 15 -61 34 42 85 7 -48 61 12 102 64 44 -49 -4 52 35 -42 106 -43 -61 86 24 -24 -2 41 29 55 30 24 79 -19 10 98 56 -2 -38 39 33 -37 35 45 21 -33 12 -19 -57 64 2 -48 26 41 22 -17 24 10 48 29 57 -28 41 15 -21 -14 -20 -18 82 82 19 24 21 10 -13 -5 83 -10 -94 42 23 69 11 -3 29 -47 -125 -47 2 49 11 -9 63 -34 45 31 -8 -6 63 -25 -67 51 -22 -15 -10 30 4 31 -4 -18 -50 1 -2 18 7 15 7 -7 -48 -4 4 10 -51 6 -41 -8 -12 -12 -22 -49 18 89 59 13 -57 1 39 23 30 -31 62 6 -30 -61 47 -29 20 -45 -13 5 1 12 18 -2 12 8 22 -17 32 5 45 4 -68 -23 -5 50 47 -8 22 -5 -24 22 -3 5 -30 57 0 -1 -48 26 52 -13 -64 31 -33 -19 38 -44 9 -41 16 -31 -31 27 11 -58 1 4 49 23 -1 9 16 -27 2 -15 -21 27 -36 -5 -3 -37 16 -2 -9 -17 25 5 11 -21 -14 10 -37 -31 18 -2 32 -9 9 46 -6 21 37 16 -13 29 -17 30 -35 -5 3 -40 32 -2 17 -22 9 10 23 5 29 -8 -29 -78 3 11 52 4 7 -3 10 -16 23 -19 3 -13 -51 28 -11 -36 -6 -5 27 33 -53 -23 9 -50 64 1 -9 27 -2 -21 35 7 -69 32 -10 -18 -9 56 18 -10 -33 9 31 -87 12 -4 -24 -25 -5 9 -7 7 -38 15 49 1 -27 2 14 -43 14 -22 -7 -18 53 7 -23 70 -53 -6 -76 24 -35 -11 -22 19 -10 -113 38 -1 15 -27 -21 31 -74 -39 9 2 53 23 -4 12 -16 -81 -32 -7 40 -37 10 3 -30 17 28 -12 -43 67 -13 -7 7 22 -1 32 15 -17 -21 -20 -7 19 57 14 -32 -14 28 39 69 40 40 33 -43 -40 -6 66 24 12 -11 -52 15 45 -10 25 -22 -16 -14 45 -5 -17 -69 101 -23 1 43 -28 -13 32 -32 27 81 11 -4 33 18 -49 45 49 -15 106 -33 -11 49 6 97 66 61 -3 2 -1 79 -36 -1 -4 25 -53 39 1 -7 10 -6 -77 -24 62 9 -23 76 -41 12 12 -17 50 -20 25 -6 -2 -13 -5 13 -5 -12 -16 21 -19 31 -86 36 -38 72 47 -31 -16 13 -64 -31 43 -3 -14 50 -10 70 -13 1 -22 -31 31 20 3 -4 -45 -31 9 18 -54 19 -57 6 16 28 0 -20 -12 16 21 -20 -20 19 11 -13 9 1 30 -2 -23 -29 -11 -5 -17 -53 -2 -33 -38 -33 43 5 0 -12 6 -33 33 13 -3 -22 -10 -5 11 -40 24 7 -32 34 -1 -60 -20 -28 -27 -9 -43 27 -25 -44 -8 -9 -8 -3 -8 -30 5 -1 27 9 -19 13 -14 -18 -28 67 36 19 -33 22 -4 48 18 78 42 55 2 38 30 14 59 22 74 4 34 -11 59 -90 -3 -35 44 30 40 -12 -41 -16 -23 -2 26 -1 6 -1 127 -39 66 16 -12 -41 18 57 85 -41 8 2 -5 25 -27 -42 12 -3 17 23 27 -35 7 40 -23 32 9 3 47 51 59 -35 -6 -6 33 78 23 61 83 38 -2 33 86 10 -13 60 -25 3 22 -15 -19 51 21 34 -21 2 54 43 -1 39 116 46 -13 2 13 61 -19 14 -32 6 35 31 24 15 -25 -12 63 -39 28 47 -20 6 11 -35 -24 15 2 -52 28 -38 85 2 -11 5 -8 34 55 -15 -51 -11 -30 -21 -19 20 12 25 34 -2 40 -8 -7 -5 26 -12 -12 76 50 -2 33 -21 -12 77 -31 -39 6 16 -4 79 -6 39 -16 125 4 19 41 -38 7 -4 -26 38 2 -15 0 4 8 1 40 -91 -15 35 2 -49 -44 58 5 -47 4 16 -44 -14 -9 -46 -7 -24 5 -28 9 -17 15 -45 -15 -9 -37 12 -10 2 10 47 -1 -23 8 20 -25 17 10 33 -42 -23 0 14 -7 -49 1 21 -22 28 -1 32 27 32 -28 6 35 23 38 48 -22 22 31 48 18 18 14 3 -35 24 -23 1 -33 26 -44 9 21 -2 -21 37 -41 -7 -5 35 9 24 -41 -5 17 -17 -25 -17 17 -18 11 12 -12 -33 -46 -6 -21 -21 -47 -19 14 38 -10 40 -61 11 -20 -46 -12 -82 4 -17 38 -32 8 38 -56 52 -8 38 34 19 15 126 98 -46 56 -6 124 127 87 35 31 -33 -30 -23 39 21 41 69 -128 -42 -12 49 -8 -33 62 -28 -69 71 -29 22 -19 24 -7 -16 13 36 0 62 -15 -25 8 0 40 13 57 12 17 23 -9 -42 5 -9 -3 67 -36 39 69 45 46 10 -14 18 73 15 39 61 49 -52 -53 11 0 3 35 -36 1 43 65 -3 4 -17 -62 21 72 9 86 -46 17 -50 69 4 4 -15 51 86 28 100 65 47 -23 -31 -15 -27 -43 -81 21 58 -12 56 -23 -3 33 15 115 -14 90 -8 118 37 62 80 56 2 102 -4 -4 -57 11 -5 -14 89 42 -80 5 18 -19 24 12 82 -81 -21 -73 7 12 32 -36 36 -84 5 27 83 -79 -30 36 -12 22 28 -7 0 42 6 12 66 -12 23 23 53 7 73 43 -11 -24 -30 127 26 3 30 -50 36 -10 6 10 12 -23 -45 -26 -1 62 16 108 8 22 -44 -12 -30 14 40 -8 -2 -6 27 -27 -12 -7 9 38 -34 -11 -22 -7 3 -14 -66 21 26 18 -28 -19 26 -46 -6 -41 -4 8 8 -39 -67 -27 -48 3 48 35 -24 10 -3 23 14 29 -30 -27 9 -24 -67 27 -18 -7 -9 -9 35 17 -36 9 24 60 -45 13 14 21 0 3 0 0 -21 -61 -11 1 0 51 0 -36 11 -1 17 32 -18 41 8 1 -35 -27 3 25 -45 -17 66 47 -40 46 9 10 24 -56 34 70 -12 116 7 69 -6 -2 27 88 -10 3 38 38 14 -52 30 -65 62 -28 34 16 83 -50 -43 -38 8 -8 26 -17 30 74 29 -25 7 -10 91 36 -24 -19 43 11 -41 24 -26 4 -47 60 -62 -52 -45 -22 30 37 56 -28 9 61 -7 -29 29 76 -53 -45 23 -18 22 3 1 20 9 104 11 35 -43 -51 -12 18 102 -40 -36 23 42 -61 12 -10 -29 9 10 47 -38 -12 -61 29 -2 -27 -11 24 -59 -9 42 11 45 -66 -43 -14 39 -77 -15 -4 -18 -77 4 19 34 -40 4 -34 38 23 75 -57 49 75 -21 57 42 -12 -5 -67 51 32 39 43 -71 -39 -37 51 -5 -1 -9 9 -93 12 -2 -11 -72 5 0 9 -17 54 16 -76 57 31 35 44 -11 63 -21 38 -8 17 -58 67 -75 -32 72 24 -99 -34 26 16 50 0 -23 15 -29 -31 -47 -38 13 7 6 29 -51 17 21 30 -8 -11 -3 29 -24 18 -14 -19 8 -26 3 35 48 4 13 0 39 30 -8 3 -7 -2 4 41 15 -4 -13 36 -4 38 -23 6 48 -26 -11 24 7 22 11 -11 -25 -21 14 -26 -1 32 -8 -14 -3 6 10 -2 19 -15 11 -39 -26 -13 15 -8 -47 29 -33 -29 -6 -12 27 16 30 16 -8 4 -27 72 -9 46 37 60 2 31 -31 13 90 48 12 20 -9 58 -66 -13 -5 71 -2 7 13 12 -48 -36 15 44 -29 -40 -9 13 -13 -10 -1 -96 40 96 9 46 -1 -26 -2 -7 -1 2 52 86 -113 -15 96 -9 4 23 46 41 -5 71 4 -19 73 54 122 -76 35 42 -61 23 75 18 23 85 -58 -111 90 41 58 -51 17 -21 10 -32 23 25 15 -49 5 -103 -11 -16 16 -3 -95 27 17 48 14 -58 -45 87 56 30 26 -21 41 13 -9 -12 -19 -89 49 48 -28 -1 20 11 -23 1 19 -47 -77 -6 -37 -11 -18 19 1 4 -36 14 -12 60 -33 -2 55 -3 5 22 -23 -8 -38 -17 24 -47 -32 -27 126 23 -89 -21 13 2 -8 -42 35 -36 -64 -76 53 -80 28 10 37 73 29 66 84 -16 -33 0 13 -39 0 -30 -56 -3 -18 -34 44 2 28 5 42 -34 -65 -10 31 -90 -17 -2 -6 58 -9 -36 -12 11 -7 -62 -8 -6 -38 45 -45 -13 -2 -51 -2 -67 -10 -18 -38 -12 -71 -24 17 24 63 -8 -39 8 -13 -5 -5 9 -8 13 -18 5 43 17 36 -69 13 6 -42 30 23 -43 35 40 -31 27 -21 -15 -21 13 -9 -10 35 11 -9 -7 23 -6 71 -24 -19 40 -19 4 17 34 -36 -9 -21 -49 -31 30 35 16 -24 -33 -9 -22 4 -63 74 50 43 22 57 -14 22 -21 -28 -53 -69 21 41 -35 6 12 1 -34 -4 -41 -17 -41 83 60 -51 -3 -82 -31 36 4 74 41 -6 65 -6 -2 -9 -35 -45 -20 5 -72 3 48 -36 -26 -50 24 -21 -8 62 -32 -21 28 49 -76 -7 -41 25 15 -65 22 23 -19 90 -30 -18 107 33 -11 -16 108 -5 17 -69 43 26 15 -1 13 -44 28 40 71 43 69 56 7 21 40 -10 -26 -8 61 12 55 -9 16 -31 -9 14 24 -1 3 -44 -39 -9 -23 58 72 127 94 82 60 53 20 -25 2 -32 -7 -24 -21 -8 48 72 -55 34 18 48 -12 10 -56 77 -39 -19 -5 22 49 -14 17 124 -18 27 10 -28 -4 103 17 67 57 12 76 -4 8 -6 -84 -5 18 25 -6 35 -23 32 47 -5 -27 12 -85 13 -2 -28 -64 -6 -57 5 7 36 -53 -86 -9 10 -14 -20 19 -44 -75 -62 -46 48 53 -50 17 -29 -19 -1 -9 56 -97 -9 -93 -47 22 -18 -68 11 -50 -25 12 -31 -37 -12 -57 127 25 2 -11 29 -22 18 16 -38 23 5 7 9 -14 -32 -34 -7 56 -4 -4 3 -4 -102 8 0 11 32 5 43 15 -40 -38 26 22 7 24 -45 -1 -35 -16 5 2 -8 2 -10 -9 1 -35 20 -15 -32 -18 -10 -12 -45 52 3 16 -9 25 -26 -1 -1 15 -2 48 34 3 -22 -39 -18 -1 -34 -23 7 23 15 30 2 8 27 24 24 -38 -26 8 16 15 -32 16 32 38 28 29 -32 44 46 -7 -46 -46 -82 16 -97 53 42 3 -21 122 -27 51 -7 -10 -6 36 48 -26 73 -60 -34 25 27 99 28 -17 66 84 -4 55 56 23 125 -62 11 11 50 46 12 34 55 9 -45 22 39 1 28 10 -8 -61 -4 44 10 -72 6 31 32 -46 11 -29 -26 -3 -54 -95 4 -28 90 -13 -41 -22 -53 60 -14 -15 -24 45 26 73 -26 -12 -18 50 -54 53 29 48 -37 -7 23 -89 44 89 17 14 39 3 33 107 3 0 -9 21 29 -72 -57 17 -37 36 31 -51 -25 -42 -6 82 -85 120 30 -14 42 -29 -24 23 -55 -4 23 -40 34 41 38 -19 46 34 11 -72 29 -48 67 3 118 16 6 -80 -14 15 23 -30 36 7 -6 -78 -23 -17 50 104 -49 41 -12 24 -124 -8 -23 -29 -61 59 55 -14 20 -41 22 8 56 1 31 83 -55 -82 -15 -30 -44 -10 13 52 -64 40 -29 -35 -19 10 -80 -10 -74 0 28 -24 21 -35 7 -13 -4 -2 15 -61 21 -25 -27 -12 23 10 -37 -5 -23 -10 -17 47 8 -22 -58 19 51 -9 1 -64 41 32 -34 -10 34 -45 44 32 -1 36 -37 -10 -8 -68 16 -115 28 9 -46 39 -37 1 64 33 7 23 15 7 54 -7 18 -26 -31 -20 -2 17 -19 8 30 -21 21 71 -3 1 -28 19 23 6 31 -30 17 -19 -21 -54 -34 -23 10 2 -16 0 -54 11 7 -10 -13 -18 -28 -66 -106 -33 -60 -39 -2 -109 59 -5 -17 -3 -127 -8 64 -77 -39 8 42 -58 86 -30 88 4 -74 30 -9 -11 47 -18 92 18 49 -48 -39 -40 40 53 71 32 9 -27 61 -7 -30 21 35 -27 48 9 100 -28 -33 -44 -5 -65 0 -56 -22 -73 38 -52 -10 64 -33 -11 -6 25 38 -25 56 -62 1 1 27 -66 47 30 -24 -48 56 66 -50 7 -17 75 -34 10 -30 30 -24 36 28 -27 -66 -44 -41 -12 -45 52 -20 -8 -29 43 34 9 27 -31 30 4 -26 15 -47 -60 -13 -34 -11 14 103 -40 10 -64 46 -93 -12 85 41 -20 -41 127 -31 60 7 45 49 -66 -47 -124 0 20 -2 29 34 103 53 -55 -39 -64 27 109 -20 -21 -2 -44 -19 8 9 -22 124 45 28 26 -8 -101 43 24 11 41 43 75 29 20 -21 -14 -22 -2 108 63 -85 47 -50 79 66 -13 -11 82 38 29 -13 23 2 -49 -2 -10 -15 -1 -47 36 -44 33 -17 14 6 15 -45 0 5 -11 22 -12 5 18 19 -30 24 26 -6 3 -29 -6 51 15 -5 13 -7 -15 2 2 9 -21 -17 -9 -54 -5 -28 45 12 -24 -25 32 33 -29 -13 -7 -14 28 -10 -2 8 -6 -9 1 24 0 -6 -5 -6 3 22 -60 35 -36 -20 3 9 34 -24 5 -11 -12 -6 -13 39 14 -19 -3 -4 0 -9 17 -34 -6 10 -9 23 13 14 -7 -14 101 114 -2 33 -23 33 79 9 -3 5 26 26 -48 -15 10 10 30 2 19 -37 66 19 -6 5 28 -31 -2 36 33 11 50 54 19 62 49 4 41 -68 51 18 14 -79 10 -6 -22 114 -5 10 42 26 -17 34 6 -68 -114 35 18 -47 16 -17 20 32 17 36 17 1 29 74 16 13 -7 -14 44 51 79 27 -48 -34 2 16 -44 36 -14 26 -57 33 77 81 17 14 10 -12 30 -14 24 -14 -59 1 32 -18 -39 42 -127 61 -8 21 2 -9 34 -81 -9 -49 43 12 -14 -12 -15 53 -34 -31 55 -57 31 22 -40 -34 44 -61 19 4 34 52 -128 77 -28 35 -44 -16 -67 13 -69 84 -28 9 21 -17 -25 7 15 45 -58 7 -12 -7 -3 2 -15 11 5 -43 -31 -2 28 72 -21 -36 -48 -14 -27 9 -43 -17 -11 -2 9 70 4 -14 63 -25 -36 -54 -26 -18 66 15 127 50 -60 -4 52 -41 9 -68 12 -3 16 -37 -20 -51 -42 -3 -20 16 -24 47 25 -27 27 14 -15 -11 -11 25 -46 -9 -17 -13 -30 22 -41 -3 5 46 36 30 31 14 -14 9 37 20 37 54 24 -13 3 34 7 17 15 3 -14 -13 -3 -44 60 -2 26 27 -9 -12 -32 -17 -110 19 -9 10 33 -13 -6 33 66 -5 -38 12 -8 -24 3 -15 26 -33 -15 -19 -6 26 -27 11 2 29 27 -21 1 -25 1 -64 -20 31 31 9 -3 83 -29 12 22 -20 43 -20 123 -32 -15 26 78 -85 -6 -18 -11 115 -17 1 -18 -37 -20 -60 -49 -55 -55 -16 5 -55 97 -37 -69 -104 -28 14 -52 -16 -87 60 -116 -104 -43 33 -49 38 -11 65 -70 -28 -17 22 -62 -100 -37 -26 2 -21 38 -57 44 -40 23 -20 -53 -65 -17 41 75 -66 2 23 29 0 106 47 -9 27 -69 -14 -58 56 21 65 28 47 16 -70 -11 43 88 -85 -28 73 -6 75 7 7 40 55 -9 -5 -19 -38 59 0 -38 17 -31 40 -17 -46 -85 -81 -15 38 49 -64 -45 -6 28 -60 23 12 31 6 -22 -15 -26 19 97 -28 121 16 25 -25 -18 -6 3 121 -11 -22 13 -34 -43 23 120 -81 -13 40 23 -50 -128 -78 12 -43 -46 -15 -61 72 13 -81 -82 -9 -35 -109 -48 44 -36 30 21 -23 -32 -4 -35 104 -5 9 59 39 -29 -86 53 -83 41 -50 25 25 1 -9 12 -37 10 -2 28 11 -2 -18 -46 12 2 -27 9 -6 22 -10 33 39 -17 2 35 15 -7 28 39 -47 14 13 -7 6 -23 -21 -30 20 -45 0 59 -36 -5 37 -4 4 -3 17 22 -101 -12 -1 13 13 40 9 23 10 -52 20 37 -13 29 21 -3 -28 -83 53 -2 -25 64 11 12 39 94 28 56 -24 20 -35 -13 -5 -33 10 17 -28 21 -12 8 63 18 20 14 -13 26 53 16 -96 45 25 11 41 23 -39 52 -12 24 17 -47 25 23 -33 -25 20 -40 -23 36 -22 6 45 55 122 -42 18 -57 -46 22 39 1 4 -27 -57 22 -11 -5 -66 -11 65 -3 85 -33 -31 4 -7 24 -35 -17 26 86 58 37 18 -117 4 23 -31 32 -14 26 4 7 -65 34 -15 -62 47 -76 17 -42 41 -63 3 -58 3 -3 3 -3 54 -24 21 36 -28 -9 -10 49 37 31 5 52 -28 87 -25 11 -19 -19 -59 -19 23 -42 46 -26 -8 -23 92 50 -51 66 22 9 60 27 -8 17 -16 6 -89 -22 8 -35 103 -58 -33 35 37 78 18 24 -64 -41 -53 -11 10 17 12 62 -5 -47 108 85 14 63 37 20 35 -39 29 23 73 13 -71 2 2 -26 22 -17 17 -2 22 1 28 9 -60 41 -31 10 23 -7 -38 22 82 9 36 46 -41 6 34 -119 -36 -60 -22 -3 13 42 -21 49 2 -2 -63 36 11 -28 3 19 26 17 -14 -17 37 4 5 -27 7 -21 1 -16 -27 29 0 39 2 -5 -8 36 32 1 7 -1 -11 -15 -47 40 25 -36 -61 16 14 -11 -12 0 -16 7 75 25 -24 0 5 48 -42 6 -54 4 -37 -4 -30 30 -51 17 -27 -10 26 -9 -26 24 18 10 30 1 -18 -36 29 -8 -11 -13 -31 -63 36 44 4 -4 30 23 -13 34 -32 -29 -12 37 -37 -22 -35 -25 -19 -39 -15 17 8 -11 44 6 39 17 -55 -15 -11 -58 -38 24 -22 -65 -10 12 -37 -1 -43 -30 -63 -12 6 -20 -79 42 -70 15 -30 -4 10 -55 20 -37 38 49 11 31 -33 -116 41 71 -10 -42 -51 -21 12 53 -20 29 -22 -14 92 65 10 -31 20 -37 -88 -18 -94 7 11 32 -12 46 33 -40 3 19 18 16 -8 51 40 26 -37 -1 4 -85 26 32 18 -7 -55 23 9 -12 95 -1 -88 13 -27 16 -19 -22 35 31 72 -21 -60 -65 -39 6 -40 23 -10 -36 50 21 -29 -33 -44 34 -16 2 27 -11 -35 11 -17 42 -6 26 40 7 1 -23 21 38 69 30 39 -41 33 13 33 23 -19 -70 -4 47 -47 -1 -7 -27 31 15 17 11 -60 -43 3 8 -80 -42 61 44 -36 17 7 19 3 -83 107 49 58 -17 5 5 -18 86 52 -19 14 -3 98 12 102 10 2 4 32 32 -19 30 -10 11 17 -30 -49 -4 54 14 66 -10 -18 48 -29 -30 28 96 -14 45 -14 -20 21 -30 43 -35 -23 -4 -1 11 -61 -10 -70 -21 63 1 -27 67 19 26 35 -7 -53 48 7 -54 -32 19 -9 17 -17 24 12 10 -28 -32 45 15 21 -36 4 15 -55 -83 25 -16 -32 -64 -20 1 -49 9 11 -39 -8 -5 12 -28 -21 33 -10 -23 1 -18 -21 -41 -20 25 -48 21 33 -13 13 20 38 65 18 18 11 0 -46 -20 -9 1 -29 -7 0 15 -25 -59 -8 -71 -30 41 49 11 -87 36 38 -11 10 -43 -42 35 -33 -26 29 127 14 40 13 19 -2 35 34 -48 100 31 45 -17 -20 15 26 -49 -42 15 47 -14 37 -47 -62 18 3 -62 -41 69 42 9 -29 -41 -8 -53 -40 -4 35 35 90 32 18 72 -74 1 -15 1 17 26 -1 -16 -32 123 -22 15 15 -21 80 12 39 -15 2 85 43 -27 8 20 67 49 -9 19 24 -69 10 9 9 85 3 10 -2 38 23 15 -10 -31 -18 15 -9 -40 -25 -1 -36 89 -50 17 -9 -5 47 72 58 -95 11 -28 8 92 50 -8 61 -50 98 19 -43 -41 9 2 0 -2 6 76 57 14 23 33 86 -1 46 23 0 84 -8 10 -54 -64 47 -46 -39 62 -4 -21 -25 -35 99 -112 80 1 -18 0 16 23 29 -31 61 11 88 -1 37 7 -41 -24 41 30 30 59 -2 -14 -22 21 4 19 -45 -11 -52 25 -22 21 -19 -5 15 92 -28 -66 42 -14 -54 22 10 51 -26 -32 7 -1 36 9 -15 -4 -9 12 11 -25 41 7 18 30 3 27 -22 -4 -39 -40 0 -39 8 -37 3 -19 -4 -64 23 -17 9 -8 -21 -11 -46 3 44 0 -68 16 -33 -1 57 4 -47 43 43 -2 -17 -83 7 38 -20 38 51 22 -30 -29 22 6 22 37 59 -6 -38 -4 58 5 54 -1 -28 16 29 78 27 -32 32 29 63 15 -8 43 -57 15 -17 10 -50 40 9 -4 -22 -2 -26 -59 18 14 63 12 -56 -20 52 -95 -5 -30 27 9 -37 73 3 2 -33 -74 -16 47 -6 44 -10 -37 -48 -29 -12 38 -55 25 -18 3 67 8 6 -29 21 -9 27 -40 0 42 9 34 0 13 17 8 29 9 1 -71 -75 -21 -30 10 6 60 24 -53 1 -58 1 -15 23 -34 -32 -9 56 -84 12 -28 30 -37 -41 -44 45 -25 17 -51 -41 25 38 -10 -16 34 57 -74 -7 3 2 9 121 50 15 -46 57 -21 -12 -86 -21 -17 113 -74 18 -43 -12 -45 50 33 54 -25 -80 40 -37 -38 4 61 98 -73 53 2 28 48 27 25 -32 25 28 -8 24 -32 15 13 115 -43 45 63 67 34 -37 -48 36 -22 26 -75 5 -69 11 42 -94 15 42 25 14 -13 -22 -1 32 -27 49 -3 -1 8 2 -67 82 12 -5 -25 -73 -8 39 76 70 -2 -4 -21 -24 -6 -52 22 2 -2 -63 -13 3 0 -16 46 -16 13 0 -17 -11 -2 -33 10 7 -8 -42 15 -38 -40 9 15 -26 -30 -5 -8 2 28 -24 -11 -4 -2 -6 -19 80 29 37 49 1 39 23 35 1 36 0 19 -4 -2 38 -11 34 -52 10 2 -78 19 -21 7 -23 -17 20 -21 42 -51 22 5 -1 52 20 -13 1 -4 37 -12 -31 -12 58 -30 12 18 20 -59 5 67 17 -2 -62 -45 7 -4 -8 2 1 2 -9 -11 9 -19 3 -2 -8 -5 14 0 6 -19 6 17 -10 5 -1 10 0 7 17 -6 -6 5 -3 -6 6 18 -1 -2 3 -16 12 2 4 7 -5 7 8 -2 -19 4 0 -17 -6 12 -10 11 -9 5 -7 -15 7 0 1 -17 10 21 17 8 -13 -9 8 -15 -4 -8 0 -3 -16 -16 -8 19 2 4 10 -14 -5 -11 -9 6 -9 -9 1 -11 -15 2 14 -12 24 -5 13 21 -2 16 -1 29 -1 16 40 -10 9 -7 11 -18 -12 3 10 14 -6 -10 2 0 16 -8 0 6 21 2 -14 -8 2 9 -13 0 8 -9 1 37 -29 5 -33 4 -24 6 -14 2 -3 8 -9 15 -7 -4 13 16 19 4 -8 -16 14 13 2 13 18 10 -6 10 -3 -2 4 -1 3 8 14 -10 10 13 -7 2 3 39 9 11 -7 -32 -5 8 3 5 36 6 -1 27 -1 23 4 0 -2 -6 14 -10 8 4 13 7 -22 12 9 10 -2 20 -13 -4 13 -8 -3 5 -8 -14 -8 -4 3 1 -9 11 11 20 4 10 -16 -6 -7 -4 -6 2 -18 -7 -20 -4 -25 4 0 0 -1 -4 1 -14 15 5 0 -8 2 -12 -2 -16 -7 -9 7 15 2 6 -15 1 0 -11 9 1 16 -26 9 -2 10 -12 -8 0 2 1 10 9 13 6 -6 5 -13 -10 -13 11 5 13 13 13 7 15 -10 -19 -26 -24 -22 -24 -3 1 -8 -30 35 -4 -21 6 18 13 2 17 -9 -28 -40 -7 -12 16 -6 -8 0 -8 14 -27 6 -14 -12 19 1 3 7 20 -17 21 -14 12 -4 -7 -22 -27 -18 44 18 8 14 13 -34 1 -20 -6 -11 -15 -23 20 -15 4 1 -5 16 -11 -9 9 -21 -27 -28 27 4 -3 0 -23 6 6 6 3 -12 3 -10 11 -15 40 15 -11 4 37 -12 3 3 -17 -42 7 -24 13 6 2 -43 19 -14 -4 -15 16 24 -46 9 2 10 -12 -18 -51 6 -16 62 -4 21 4 21 25 18 55 -8 -8 7 -31 -1 55 -13 14 -7 3 -31 4 11 -15 -21 9 -20 44 -1 11 -3 2 12 13 38 28 40 63 25 16 28 23 -17 18 29 -41 21 -38 7 -6 35 8 -40 22 -10 -3 10 17 29 -30 -14 -50 35 11 -14 13 -4 -46 38 3 24 24 -21 40 11 12 39 -30 10 -10 0 -7 -31 0 10 27 9 11 -5 -37 -13 -7 -17 -32 17 26 -13 -2 15 5 3 -8 6 2 -17 6 25 -10 -13 -24 -8 30 41 10 -17 -19 -10 -6 10 2 -35 -13 -7 17 -8 -11 -19 4 3 12 -12 25 -6 3 23 5 -18 22 0 -1 23 1 11 -3 -3 -16 43 10 -25 6 6 22 -9 -10 3 -14 -6 -2 -13 5 21 -2 8 -3 -2 -3 -14 10 12 12 1 10 23 17 29 -17 2 -33 39 -5 -4 -11 -31 -28 18 1 -2 -16 27 5 36 -7 13 -2 -54 -10 2 26 -19 -18 -3 -31 34 -22 -26 12 -15 23 19 13 4 23 -5 13 -45 -7 -1 -20 -31 -35 -31 28 -12 33 0 11 -58 29 6 -19 8 -13 -36 10 -5 33 44 30 19 22 13 -2 -37 -16 47 -1 39 34 -20 -30 -15 1 -1 -6 -55 0 -20 -7 -2 31 -8 8 -50 1 -8 24 -2 -9 5 21 -3 -64 48 -48 -40 -12 12 -33 -11 12 38 37 -6 -21 24 -6 -74 -94 57 -12 12 -7 -18 0 32 -1 -12 64 26 -18 2 -17 -31 66 -9 -7 -24 -22 42 -44 32 -23 0 -13 -7 46 -51 14 -7 -17 7 16 -3 10 41 37 24 -5 -7 -18 -52 -39 10 29 -34 40 -31 -76 -50 -22 55 -8 -61 -4 26 -4 28 -20 -6 28 -22 -106 -25 -26 -32 -35 27 -10 0 38 -53 4 -4 -38 -3 0 -8 -30 9 2 34 18 -14 -11 0 20 -7 2 4 -16 -42 31 -3 67 3 22 38 24 -16 -7 13 22 -9 1 -1 5 -39 -39 -5 18 -27 18 -6 -28 -15 8 30 8 -9 -46 -12 10 -18 -14 -7 34 3 21 -20 20 5 13 18 31 -10 25 -23 -10 22 -12 1 16 29 21 9 -2 3 -26 -39 1 -5 16 14 -21 -6 4 15 5 -6 9 7 6 5 -7 18 -12 31 -5 -39 -11 41 23 -1 64 34 -10 -3 -7 37 -8 -8 -19 -5 18 8 3 49 -57 48 12 -16 -23 0 43 -35 32 -33 -18 21 -68 24 -5 28 -8 14 -15 34 -14 19 -45 -28 -29 -8 -2 29 -49 -29 -7 38 -22 -28 37 -27 -26 -43 62 -11 16 21 6 -5 -13 -14 21 26 23 42 23 -7 -13 -1 20 -34 -6 -7 28 -27 13 -27 -32 -4 -33 -6 -62 11 -8 25 -11 -3 -48 -31 -65 -13 20 -8 -4 -3 -12 58 9 48 19 91 1 -56 14 24 9 -3 -27 15 -6 -4 -53 -50 1 -13 -8 25 -29 9 -9 -1 42 2 77 -31 -40 -16 49 -6 17 -7 39 55 47 45 43 8 -13 23 -30 -25 -52 73 8 21 1 13 25 -45 17 61 -22 -84 -39 -18 -3 -35 1 -15 -66 2 11 46 -41 -60 -23 -42 114 -46 -6 54 0 8 -20 44 -13 8 87 36 -25 -72 -49 4 -9 -10 27 -20 39 6 -13 0 -46 -27 -24 51 -18 4 9 -32 -14 5 11 8 -1 44 26 -34 34 -30 -21 19 -5 32 6 -15 -24 25 4 4 10 -30 -9 -52 6 10 1 33 26 -13 18 -33 29 -3 2 25 -17 0 -31 -25 -25 9 12 36 -6 -11 -30 -4 11 -66 -18 10 -45 -46 -15 2 -9 30 14 -13 35 -51 4 -10 -36 -33 -12 21 30 -14 -26 -19 34 -14 39 -47 1 14 -1 24 19 26 -13 18 39 6 -34 5 22 13 73 -20 27 -74 10 -43 -4 9 -39 -21 7 50 25 16 -61 47 0 9 -7 13 -23 -31 18 -14 26 3 -36 -16 12 30 -27 33 -23 -37 -18 12 -40 -10 0 4 -4 -8 -2 4 4 20 -6 -24 18 21 -28 10 53 16 15 30 36 83 3 40 -8 -29 61 -15 -36 -15 5 -18 -34 -4 -24 0 23 -56 6 7 -13 30 -13 14 -19 13 -11 3 33 -33 -33 -31 -35 28 3 -25 -48 -9 -39 11 47 52 -17 -110 33 -52 24 -43 -15 -23 -66 27 29 -13 -3 -26 19 -88 58 44 -2 -11 29 39 32 66 59 33 -48 17 -3 -8 1 -16 11 1 9 16 101 26 37 -13 -30 -14 -11 -2 -15 15 15 -18 -52 -19 30 66 33 -126 25 28 24 -27 37 -35 -42 -33 -117 12 -41 5 25 -10 75 -40 14 -25 19 21 16 46 -42 -35 -49 66 -10 16 5 21 -10 -12 20 -51 -12 -26 16 -19 -29 -12 5 -32 -18 -44 -30 -36 -52 -36 -9 -28 -57 50 -14 -9 -19 -8 -43 18 -1 19 8 22 10 0 38 21 0 0 -4 -39 -11 -2 -8 21 10 15 -3 18 21 10 4 9 -42 15 -34 -4 0 -7 -18 -15 -28 15 -13 8 6 -102 -25 -1 13 1 -12 -33 39 4 -62 -29 40 -38 -12 9 -20 -31 -25 -12 27 10 12 -2 11 -10 40 -44 14 21 -10 19 4 5 -27 -2 -1 10 -23 -3 17 44 30 -17 15 47 56 -16 28 60 -22 3 -21 18 -38 -46 19 8 -25 -19 -23 -8 -17 77 -18 -1 -1 23 -18 -24 13 -53 -38 39 32 25 1 41 -20 29 51 21 17 -42 50 -12 -9 30 11 15 -11 25 -3 -17 -14 -8 17 20 -4 56 5 55 -12 -7 -34 28 9 -34 -7 -14 -7 34 -19 27 20 23 -27 -52 10 10 10 -18 2 2 -17 -12 31 8 35 45 34 13 29 34 -40 -13 3 -35 32 -1 33 1 16 -64 18 70 22 -14 108 70 -17 -11 -13 5 -51 -47 59 5 -17 -79 -3 64 0 19 55 -31 -31 -57 -14 -63 56 14 32 25 23 45 -47 -19 9 3 -37 -49 -15 -21 -73 26 14 18 -99 29 22 27 21 0 45 71 20 29 31 13 53 69 45 19 24 77 13 -7 19 -15 21 74 -4 23 18 -17 14 11 80 11 -13 36 -31 -15 20 -16 12 -34 66 57 -21 -24 0 -14 29 -27 18 44 -49 -20 -16 -44 6 -29 35 -10 29 27 50 17 -6 -27 2 49 -9 45 3 -34 3 -22 -7 17 -2 -17 -1 19 16 -40 19 -26 -4 28 -2 11 26 17 -22 -11 1 -15 11 12 4 -2 -32 13 47 -25 -2 -38 10 -15 38 75 24 -48 29 -8 10 -4 26 53 13 -49 -20 27 27 -22 -21 -2 36 22 6 -13 17 7 6 5 -2 35 19 -50 16 -39 15 -40 -21 -25 7 38 50 -117 -8 91 -9 59 93 11 -89 108 20 -7 -34 -54 -32 101 -84 57 37 -10 -60 88 -126 -6 -33 127 -61 -87 126 -80 -84 127 -107 85 -49 -21 22 -5 85 -85 60 -54 101 -120 -40 32 -77 14 -124 9 -25 -91 -118 -37 -24 72 -41 127 109 127 72 54 -36 71 126 38 -24 2 -12 -48 27 -22 1 66 -107 -73 54 -29 -58 -72 -5 31 9 126 4 102 -99 49 127 12 46 1 -72 -34 -11 -96 -68 8 98 -23 -35 -74 -91 -93 3 -128 -128 121 -8 116 56 -30 17 124 33 -76 -46 -95 42 -93 49 47 29 -34 -45 -3 -33 -125 47 -68 -4 44 -8 -94 -42 -4 7 -91 -66 78 -95 120 -32 -12 22 -100 10 8 -115 82 -38 127 -53 80 12 86 -28 -44 -73 89 -74 29 87 -117 -98 -44 -90 16 98 -120 -88 77 124 15 -121 -124 -99 -58 -123 68 -128 70 67 -32 23 31 -41 -128 -63 -114 -18 -64 30 -81 5 -72 -17 -64 -127 16 53 34 -122 -64 -51 45 -116 -66 19 -127 -123 124 53 68 0 -75 -35 10 0 18 67 -51 36 46 62 127 -6 58 127 20 -16 112 75 -39 3 -91 -30 59 -1 25 -32 20 -48 59 -12 17 55 127 54 -1 45 127 20 -126 -35 81 56 44 -32 59 -16 65 -3 37 -90 61 -42 74 68 33 70 32 -95 64 4 -95 -89 -56 72 54 103 4 -23 82 -60 -9 -33 -41 50 -44 -56 -100 -79 -17 -95 -36 74 -32 31 -62 -5 -17 -72 -12 -108 81 107 80 -34 -127 6 -36 -33 9 -68 30 116 10 30 125 103 35 16 -7 -127 -21 66 51 57 -70 125 -2 126 -20 -65 61 -12 45 50 -18 8 18 20 -96 -46 106 -39 -87 -47 127 -128 30 -4 122 1 34 99 119 -63 -53 -54 -127 11 -106 127 -41 8 11 11 57 -37 126 -54 122 -99 63 8 -90 -49 21 73 -69 33 -14 -103 -85 77 17 -69 33 -45 10 14 -39 99 123 19 -21 -98 -32 87 79 -34 127 93 -45 117 -114 -1 -7 8 112 36 -60 127 -12 -8 -27 -6 -23 -55 28 21 22 75 -116 38 -125 -100 -75 -108 -35 -127 33 95 -128 -57 88 -108 127 56 20 22 4 -89 -126 51 96 101 -128 0 96 37 127 116 -99 25 125 124 111 108 80 127 124 -89 -63 -76 -73 -16 13 -43 -87 0 127 40 -55 66 123 5 88 -34 -21 1 -11 -33 70 -53 3 -16 15 67 51 36 24 -21 25 -59 -96 126 34 -22 -48 -17 3 -42 -1 -15 -94 24 -109 28 24 -91 -127 -4 73 104 -33 90 10 -5 2 20 26 -36 64 38 55 -37 -24 -91 92 -100 -59 8 126 -5 10 -51 -82 -45 -69 -5 -28 -43 78 -105 -32 -5 -40 -116 -82 -104 61 18 99 120 74 49 -23 -30 -74 -60 -43 -87 24 -29 127 2 15 -76 -124 -118 -73 -37 18 86 -107 119 -72 -38 -83 -94 21 5 -12 27 57 43 26 -13 -97 -54 84 127 -51 36 -121 -128 -45 58 -89 -15 -12 122 -85 -64 -4 -127 51 -11 19 8 -112 -41 69 42 13 127 122 -37 -60 19 20 -42 10 19 44 -124 43 19 25 38 69 -34 -121 -16 -50 -72 124 -32 -38 26 -17 4 -37 -58 -15 -20 -120 -87 -23 43 79 -40 -35 -1 54 -9 76 -2 125 -25 117 -5 51 17 -65 -15 21 86 100 -22 -62 -35 105 3 -88 -23 127 -102 -114 -8 19 77 53 86 65 99 -78 72 -27 -104 -14 -18 127 -44 -60 50 81 1 34 95 -111 -68 -127 -46 55 93 -93 -6 53 -87 9 -67 125 -18 94 -126 95 89 24 9 108 -34 -127 36 -96 26 18 -81 -127 55 -56 -7 -95 18 -103 -107 -39 -62 -97 33 -83 -10 -127 -3 -18 85 22 -128 -96 -36 -81 37 125 -119 -124 -67 -15 -71 -100 -7 -84 -23 -78 125 -65 -27 -123 55 65 40 -10 20 35 -97 -37 10 125 -72 67 74 34 -5 -50 -109 0 75 -70 91 -29 44 80 -4 -6 42 0 -69 -52 -11 26 -28 101 74 -51 14 -29 126 -39 70 32 -4 24 92 -4 28 1 -45 74 -91 -15 36 -7 -85 51 5 -128 8 62 58 45 -43 19 103 -20 2 -45 78 -69 15 -4 42 127 116 -78 -2 -30 -128 25 -53 1 -116 51 -40 4 63 -71 72 38 -74 -66 29 -58 123 0 52 107 -24 -78 -8 -1 47 -97 -92 61 -23 43 120 31 81 -43 123 -19 -57 -13 -128 -34 -24 -107 -43 -44 88 -29 127 -91 -125 111 45 64 112 -128 -52 121 25 -117 -117 -124 -72 123 -48 -30 -97 31 -20 71 54 9 63 117 62 33 -105 -126 5 27 127 59 50 -52 -39 0 14 127 -68 -27 79 -67 -87 -88 -66 40 -90 -126 -73 50 127 79 -113 8 -127 52 -23 -114 -124 7 -9 -40 -13 72 127 -110 90 -125 63 -121 -12 -127 23 -128 -36 -122 -112 -16 -18 53 -105 -127 115 -121 26 14 2 -39 70 115 124 51 -92 -56 -16 40 -73 -19 39 41 56 -69 40 93 1 -60 51 -103 21 -117 -70 -117 -117 -96 65 -49 48 121 -100 -127 -22 8 -51 -127 -89 97 -128 38 120 -8 -13 44 40 0 -63 -55 -50 123 127 70 60 124 125 -4 -128 -103 1 127 5 64 112 13 -128 -112 -57 127 -35 10 59 -58 24 10 116 39 35 92 -93 19 -46 94 -16 127 123 -1 -47 66 65 59 -124 41 -40 25 -56 34 -14 105 -41 79 33 -17 -56 105 -53 89 5 6 55 -82 -35 -8 97 29 -53 -37 32 -19 97 39 -22 20 4 114 30 102 -71 -86 -34 -38 -43 -47 -15 63 119 -41 -40 24 -17 62 25 115 -38 37 -6 5 59 120 105 -8 -72 -109 -121 57 53 -84 5 106 17 -43 100 -125 37 -30 -62 -36 -87 -13 33 12 -17 55 4 35 76 -43 55 -74 30 20 -47 -125 65 36 127 -75 87 3 29 116 -120 0 -62 -126 -84 -17 -124 -109 100 -121 -20 -38 123 93 7 -122 16 77 -113 -101 -63 -79 47 120 -107 49 -77 -116 56 69 -127 5 34 -99 -74 127 25 -32 23 -125 41 -75 -127 11 -114 -20 44 -111 -111 11 124 -127 101 -128 68 -67 106 40 -122 -20 -11 -41 -48 28 -22 -73 -16 18 -32 -32 12 -85 -3 38 64 -38 -119 108 123 -69 121 57 38 -34 55 109 -128 -42 -104 68 95 -60 -57 127 -97 47 -3 -127 127 -103 91 -5 71 -41 127 60 -50 9 -6 -121 -27 7 60 -8 90 -22 -32 127 -53 -77 -114 2 -79 98 7 53 -32 -116 -42 -70 12 126 35 -39 -75 -103 86 31 118 -123 -29 -59 55 116 55 113 -53 28 -85 114 -124 -95 -42 -128 68 106 -44 93 -31 -125 -44 -106 -128 -84 -83 -127 109 -119 104 9 -70 64 -20 -10 37 102 -33 34 -25 39 63 -40 53 52 118 18 15 120 77 127 -127 110 83 22 62 127 -64 -55 -20 -75 -33 -23 -106 -95 64 115 -6 -27 90 -54 126 125 -85 37 7 72 -41 52 109 55 -38 116 -60 -60 -73 118 -3 -38 71 127 -6 -78 120 60 -49 109 -47 91 -17 -128 -14 51 4 124 33 126 18 69 23 12 24 120 -79 -106 -6 87 15 -35 -91 -56 90 -67 -13 -32 42 -31 47 -17 -125 -128 10 -18 53 -96 -46 -122 -50 -17 22 -57 -69 -60 0 94 8 -125 31 83 -75 50 -15 -42 32 -111 -127 30 -46 -127 -117 -70 60 126 126 -128 -9 -88 -52 72 -47 100 86 -6 -126 40 -38 -25 86 -37 -117 1 16 -125 62 21 124 -31 99 -19 53 19 -18 56 -100 -11 36 6 -24 -1 -89 -40 99 -128 113 -82 -124 -66 -37 15 -43 77 22 104 -29 -98 -34 80 25 -124 -124 19 4 127 -128 -125 -70 -127 123 -74 -128 -7 -16 58 -89 122 127 12 93 -98 104 123 -5 -127 -99 -85 127 127 -127 -75 15 117 43 16 -109 51 -49 -128 -113 23 -16 -97 -127 89 126 -70 118 -87 -109 14 -79 -36 87 -46 8 -79 5 26 119 121 28 -29 127 -37 49 97 127 17 -126 114 127 -77 28 2 -120 -55 84 71 -103 53 27 7 -81 72 -60 119 54 46 127 126 127 -38 -58 87 -34 106 -12 80 86 10 -93 -77 -82 -83 118 -59 42 -12 5 35 -91 -128 -88 45 -126 110 19 59 -101 127 36 -18 34 -20 -26 127 -46 -125 59 -18 -88 -42 18 125 -119 79 -121 -80 10 -99 36 74 5 44 -49 -108 -46 -61 4 12 -34 58 -77 38 8 59 -8 -12 87 86 -102 -38 -25 -2 14 -40 -17 68 55 75 37 127 19 -19 0 92 120 24 -106 50 -106 -11 -19 -49 127 -62 -94 -128 61 -104 -61 25 -71 -16 98 -57 98 121 -22 83 120 2 46 -123 125 -2 -12 76 -63 -41 125 87 103 127 14 -41 20 -91 -127 -102 -3 85 -127 19 -128 -29 -103 76 89 -125 25 -86 125 -79 -95 -84 79 -60 17 127 -127 -26 -128 -53 -122 -64 50 127 -11 -42 7 104 -9 62 -109 -122 127 59 -11 39 101 53 122 18 5 5 -1 -109 43 -128 43 -128 -114 127 -127 -26 -121 -84 -121 49 -105 0 -102 -127 -106 8 67 60 -128 127 118 -15 -39 -128 19 11 26 -68 -116 -121 -38 0 125 -101 15 -13 -65 -18 -67 127 -22 27 17 -120 11 127 -128 -49 101 58 43 11 -49 127 -55 127 44 70 -128 -126 34 15 89 -38 102 70 28 98 127 27 122 53 -12 -88 -24 -56 54 -34 -64 -50 -111 -39 -86 19 -100 -127 -90 127 -121 -1 29 -106 -126 42 46 -34 -89 -57 62 125 43 -7 -62 -122 3 7 51 -10 22 58 -127 -128 -105 52 -64 -87 127 -31 25 84 61 36 121 -7 42 -56 -86 82 -21 -126 91 47 65 -48 -21 -124 127 85 -71 -47 34 0 -101 9 73 124 87 13 32 32 -49 -107 -86 -26 28 -70 -101 -42 13 127 65 91 25 -16 -39 108 127 -58 30 -39 -11 1 51 -122 17 37 -127 -67 17 -35 127 -55 70 -27 66 -13 34 -94 10 -12 85 -38 5 127 -10 -124 34 -64 -54 -62 -40 -10 -33 83 9 -7 -97 -95 -33 49 -88 14 48 44 108 -66 3 5 119 24 -125 9 84 32 -51 127 82 -59 -69 -125 -122 99 -54 -22 -21 127 -62 -60 57 -47 36 -43 -122 -34 -13 -6 75 11 88 60 104 -70 -20 -19 -126 -57 -125 -104 -28 -41 -31 69 -66 71 -76 -15 -40 119 3 -8 -88 112 -33 49 -94 127 -112 59 107 1 127 -3 114 13 -68 126 126 -36 -112 -128 -127 87 -128 -74 -126 109 -53 18 -64 106 -10 6 -104 -81 108 -55 -98 -104 -30 95 125 -75 75 -93 126 -100 -114 86 -68 43 -29 -125 42 110 120 -53 127 127 6 -128 7 32 37 -124 122 -126 36 127 -73 121 -121 77 -14 39 97 127 16 -13 -14 117 -34 -43 -122 56 -77 -98 127 -13 -102 -96 123 -124 -49 10 42 -10 8 -11 -103 -90 -64 127 -77 32 -128 72 -89 -128 70 127 109 103 109 52 -86 110 -49 56 -127 -54 -27 -104 83 85 -50 127 108 76 -101 50 100 -126 -128 127 -66 -29 14 26 20 23 81 -18 55 -33 -26 -78 -43 -46 22 -33 23 -22 104 -127 33 -128 -60 82 126 98 -27 51 74 21 33 28 75 96 30 -124 -84 -33 38 -102 -120 36 49 -114 -38 22 -38 83 -104 127 -114 23 79 -8 -127 -24 -95 -3 13 -75 91 -128 43 -7 -29 5 16 -89 -101 -122 -9 127 24 -60 -37 31 125 127 11 -99 -35 -127 -55 -51 -126 -4 127 74 15 14 60 89 98 74 55 4 10 125 64 -127 123 -128 101 -128 -54 -45 -109 105 127 125 -43 -128 -128 110 75 -127 -106 -127 -52 107 74 -59 108 -1 58 -19 -128 11 3 -63 -12 -62 11 -26 123 -111 -128 16 5 124 55 -47 52 63 -56 125 -118 56 -127 -96 -124 -126 -126 -128 -17 -12 -125 -87 9 94 -127 15 127 31 40 126 -21 -83 -121 -13 123 -107 -113 -128 -97 -56 -119 18 -14 -17 -45 127 -127 66 40 -82 -107 46 126 28 14 94 -128 123 -23 -128 19 -36 43 121 -10 106 -72 -122 -108 -127 117 -50 53 -124 -128 -127 40 122 79 -107 127 123 -87 -126 8 24 32 -39 -124 -81 -37 55 75 -123 -68 -1 0 111 -64 -124 -122 -123 64 -11 127 105 72 87 126 125 86 127 -93 -91 121 -110 31 79 -58 -3 118 -67 -86 -126 -108 32 -120 -110 106 -118 -126 121 -9 127 102 -39 -88 -119 -115 19 126 127 127 -9 86 54 43 43 127 -122 96 57 -63 3 38 -74 -20 -112 33 25 -39 -12 5 80 -6 -64 88 95 57 70 122 127 61 41 91 122 125 14 3 30 -79 19 7 51 127 21 53 -12 -126 10 -81 -100 -33 10 51 55 -114 -90 -46 88 -16 14 43 -5 66 100 119 -122 -52 126 -47 62 -122 120 -101 42 23 71 113 96 -15 109 -77 -127 -37 63 -91 46 127 127 101 127 39 -71 -124 -121 -79 114 -40 65 -52 -128 -20 125 -23 52 47 32 -69 121 108 127 124 -14 -30 -3 45 -127 69 -94 -32 119 -127 -37 -28 33 48 -128 -114 -5 -49 -57 92 -113 26 -10 -48 7 -118 44 3 -72 -126 -128 -26 122 -122 -117 -128 62 12 -119 -2 55 13 21 -127 92 -1 -85 -99 112 -120 68 -128 -124 12 68 127 -126 -12 98 -97 71 -126 121 -8 95 126 124 118 -122 -75 -40 -42 -117 -9 -32 -55 124 -20 17 74 127 -67 16 -68 69 -101 -116 -19 -39 127 -24 -122 -66 53 -120 -119 -123 124 44 9 117 -121 -127 -77 -127 120 -84 -39 -101 -4 -128 -75 -72 123 30 126 28 113 55 19 46 -55 72 -61 -128 6 56 123 -127 -43 -30 -56 -86 76 95 85 -80 121 20 127 105 -106 11 16 121 90 -4 -24 -121 125 -19 -126 127 -32 118 -123 21 127 -85 -114 85 -36 81 -58 -114 -2 73 123 124 -103 -124 -65 127 5 127 89 127 45 -122 125 -90 127 64 -51 -122 100 124 111 -91 127 31 -75 -17 42 -1 99 -23 -125 2 -1 92 65 -43 102 -42 86 125 41 -87 120 112 126 118 97 24 -127 127 -37 -121 -51 -48 -120 50 -57 14 -128 -75 -83 4 4 52 15 78 -110 -23 2 17 -10 -77 -12 97 -88 -20 28 61 7 -52 127 127 -60 12 111 122 -52 -47 -21 40 -12 127 -7 5 -44 36 125 125 127 127 125 126 -65 -38 127 -54 124 -2 21 -95 -106 61 46 -74 29 127 12 103 127 70 -118 -74 10 126 -36 108 25 4 -109 96 -70 -124 -94 -126 -9 -43 -44 -34 104 36 -29 7 -125 -12 121 -5 -5 -8 -123 -57 17 -18 -81 0 91 -125 27 -58 114 -65 123 30 100 -111 -103 -124 61 54 -122 33 37 75 46 52 -90 101 -19 -74 122 127 -50 106 127 124 48 121 127 64 -27 18 -42 127 -68 -121 113 31 -15 -124 -55 -17 61 -115 -43 122 -125 53 -81 -2 94 2 -41 117 -128 -72 10 -24 102 -33 77 5 123 -128 76 122 -5 -72 126 17 20 -21 28 126 116 -2 58 -87 -91 127 -8 127 26 -23 -127 89 -3 -128 -57 27 -30 18 77 -19 120 -54 115 -124 -32 -128 -26 -121 31 28 -22 -17 -23 85 10 -89 -21 -59 83 127 127 54 -27 122 -97 -126 22 82 -9 57 -63 -95 -122 52 -31 13 -36 12 17 63 -31 114 35 -44 -19 123 -4 83 105 34 -124 102 -35 68 58 -128 105 -116 -40 21 16 64 102 97 -27 95 -107 -117 -43 65 -119 78 62 -93 63 10 -50 -34 49 104 47 -118 -63 -122 31 127 -79 -35 -56 32 17 11 -107 -64 -34 -75 -18 33 28 -74 -60 126 -57 20 -101 16 -1 72 -103 -59 6 88 47 -127 30 -86 26 93 -24 20 -27 52 4 -116 -47 -63 -8 -82 62 8 -49 -55 -39 93 119 40 -55 -122 81 -44 0 -88 124 92 -68 -65 -108 90 25 121 124 -35 -120 107 127 39 4 -118 -8 2 80 -5 100 -21 27 42 13 -48 -128 -106 83 127 93 37 19 -22 -123 -47 89 119 103 110 96 -67 -61 16 95 -89 43 96 119 -127 127 -128 -91 -121 127 77 -85 -69 -19 -128 82 62 -49 10 -114 -128 -41 115 13 127 -128 89 45 127 -17 -18 127 121 116 -113 15 -11 -28 38 3 -128 -97 -126 -112 92 -54 33 -11 -128 -110 -73 78 114 -13 54 127 8 -33 5 -126 126 -13 119 85 -13 102 -127 50 -96 34 -87 126 -35 87 -121 -27 -31 121 38 -78 127 15 53 119 -119 30 -9 -122 127 126 -101 -128 -10 -110 96 -59 -113 127 98 53 78 95 -116 9 -18 -128 -66 113 -39 6 110 -128 61 127 -124 -75 -87 126 -125 -119 -115 -127 -127 127 124 -128 23 -124 -99 116 47 -32 44 47 81 124 116 -42 12 -81 127 127 62 27 -127 -111 -57 -82 -57 127 98 9 -25 -117 -98 66 108 -15 -50 -41 33 109 18 127 104 -77 -32 -45 78 40 -85 -18 126 105 -12 -27 -126 118 -40 113 -81 127 -123 -128 -45 57 -125 -12 59 103 20 125 -93 -83 73 -39 -86 -71 126 -27 123 -10 -91 127 100 124 14 63 -30 -68 -51 0 124 11 -126 -3 68 -110 -72 -27 34 107 -127 49 -127 -60 -58 -12 127 13 37 19 127 -21 126 53 11 75 33 -128 -128 -78 51 102 -11 -23 -23 -85 -27 63 25 -20 1 -97 125 -99 -52 -128 44 51 -50 -73 -1 27 104 -8 19 -50 -8 -3 -40 -69 115 -43 -105 -24 22 2 69 27 30 23 27 -120 -124 -29 -113 80 -48 39 56 -116 -119 -85 62 -14 -89 127 -3 103 -33 -121 -107 120 47 -35 -4 10 49 -109 -83 -87 121 -116 4 -69 -66 -85 127 -128 43 61 43 -110 -110 -29 -127 -121 -11 -115 -80 -124 124 -68 73 127 -67 -101 -101 46 43 -58 -52 127 36 -24 84 123 103 35 -17 -87 -128 22 -20 78 -63 36 41 76 -65 22 121 43 9 5 65 32 -44 122 -73 112 -33 -101 -118 120 -126 -73 -21 -97 -2 127 30 -25 -71 -6 -33 54 34 -59 -39 -27 -60 -99 82 -66 86 -22 65 86 36 -20 100 -14 -106 127 56 -46 119 -83 -2 -6 79 61 -65 -33 17 37 -65 125 -70 79 3 -17 -118 126 83 -114 0 13 49 47 15 -75 88 56 -35 99 -88 122 116 -42 -25 20 40 -39 20 -22 45 -94 -60 -39 -28 53 -74 0 -85 -127 69 62 8 46 32 11 -63 27 -76 67 -32 125 -126 21 -19 -84 -14 -11 9 -45 -9 57 -12 17 53 -76 18 -33 44 33 127 -36 -58 54 27 60 -66 104 -9 -15 42 -124 -3 120 53 -29 45 33 -110 127 10 -1 -50 -42 -24 59 -128 104 -127 -36 -112 -52 14 5 115 11 -100 20 -63 -97 63 -40 22 121 12 124 -36 17 52 -89 41 127 -20 -20 21 -124 124 37 9 67 -126 -24 -126 94 -45 127 60 119 -64 -122 110 -107 -128 -96 -1 -122 -50 -107 126 -84 -92 -50 -122 101 -35 102 -112 -126 -78 -79 126 -25 -118 17 127 127 -26 103 17 -101 32 -75 -71 124 66 -35 -74 -41 -30 -101 55 -118 78 111 -127 -127 -128 1 127 107 -44 -62 61 61 122 107 37 8 -76 54 14 44 -122 -123 -58 -40 -20 22 76 47 126 -112 127 -41 117 -127 4 127 -83 -127 1 -126 38 -42 5 54 49 126 91 3 47 -90 -127 -3 7 -123 27 -52 -128 126 -68 127 -123 121 127 127 -110 -126 -16 -71 109 127 26 -76 -15 -123 -124 89 -53 -12 -127 -100 -20 -1 -71 27 40 123 63 -12 37 -127 105 47 34 127 106 127 -101 119 127 -39 117 35 124 79 -122 -34 -115 -61 28 -40 -116 -123 -128 -115 81 -125 -126 83 -11 99 127 12 127 116 -126 -14 12 95 118 123 -44 -120 -53 15 -87 7 -18 -87 67 -5 70 -45 -94 -8 -115 -84 125 -50 -1 -6 -77 11 89 -22 -128 123 -57 125 111 127 -96 8 14 -85 18 -127 -126 99 35 -86 69 78 -128 5 -44 115 -4 -110 127 65 84 -109 60 57 -101 -84 17 -128 -122 -20 127 -81 127 44 127 111 -2 -17 107 27 127 92 87 127 -29 -127 -123 102 98 107 127 127 124 -88 -14 -19 -72 125 -38 39 127 -6 -128 40 -18 -100 35 126 72 -80 111 -19 115 -127 2 34 45 78 -125 -128 -119 -67 117 -88 -128 -127 127 -83 -8 3 67 -127 -45 -123 -128 1 -16 73 -128 3 -54 -71 -23 127 -72 -91 113 107 -18 120 -96 127 -3 -26 82 -98 -71 -110 -2 -90 67 60 -1 -38 -128 53 -46 59 -47 -104 126 -127 -128 -128 -115 -80 -126 -86 -72 120 80 5 73 -12 -96 -51 -125 -7 32 -97 125 -30 99 43 115 112 118 30 -40 26 -69 -35 -20 -68 41 110 -27 -123 58 -59 1 16 -125 90 -66 -78 -9 8 15 -127 -64 -14 126 98 75 113 42 -69 -89 10 32 117 -62 122 -94 15 -128 72 5 117 -13 47 -82 90 87 127 -73 -33 67 15 -128 -19 120 -12 50 -17 -14 127 -124 26 -103 -38 97 -39 -58 -76 126 113 127 4 99 96 -101 121 -127 67 -25 -17 32 -96 55 -34 -127 90 17 97 8 64 125 -122 62 -22 -126 90 9 126 -25 -10 -59 -38 44 -1 127 -82 74 11 -83 26 -27 -46 2 83 12 -34 60 33 28 20 122 -79 60 76 -125 -26 33 124 106 -12 6 -78 -128 89 -120 -57 -97 -4 17 -40 125 123 9 -28 -71 89 114 -59 -41 -28 -60 0 -50 68 58 -85 -32 -127 22 8 18 -37 -40 87 127 -71 -113 115 -39 -123 127 127 -23 38 -88 -98 48 -61 127 -36 72 29 45 -11 122 -79 -2 -63 127 58 -123 108 101 88 -18 -111 53 127 97 125 -52 -34 116 -31 -77 -39 -88 -124 -19 124 54 127 -128 127 29 -124 34 -127 127 1 104 37 8 -77 -127 123 16 -9 -40 -54 37 6 97 -121 126 9 31 -1 33 -124 34 -33 78 11 111 74 -78 -127 -23 -38 -36 124 -128 42 59 -22 -23 -36 121 -128 -6 -128 72 -107 29 127 49 83 125 -100 -17 65 110 -126 127 71 -80 -35 7 60 -40 -48 -116 113 127 30 101 -30 -123 -50 -63 114 61 -127 120 40 99 -15 -127 -21 -71 -25 52 -73 -69 -84 126 -5 127 60 17 -64 -50 -127 127 -7 126 -95 16 82 33 -128 -122 -78 89 31 62 -125 -128 106 -95 -122 -34 56 105 -83 59 -101 -37 -59 122 -86 -127 30 -111 -76 58 11 121 -121 -36 68 123 27 123 -128 -128 -128 4 127 -105 -126 -4 19 24 29 25 24 115 -117 111 57 -62 126 127 -107 124 123 29 -111 -27 -126 127 -80 -30 127 118 -6 -54 127 -60 -72 49 107 67 -13 -36 26 -128 127 32 65 -72 25 -128 -77 17 29 122 -125 -127 105 -11 -116 122 -35 -127 -127 -95 -113 47 -30 -52 74 123 14 -56 27 -65 -126 13 -124 -127 -29 127 -116 33 7 75 -30 -78 -126 -122 54 55 111 -102 108 127 127 -49 -98 -128 26 -79 -127 -10 123 48 -119 -128 -59 -127 1 120 61 55 127 103 -90 -102 3 79 127 -125 -64 122 74 -127 29 121 49 -46 127 48 91 73 123 30 0 17 -127 -45 -66 -86 18 -40 -10 51 18 -6 -48 74 -8 43 -59 -15 -32 -31 -4 28 -76 -36 -65 20 34 -20 11 31 49 4 -53 -1 24 -13 -4 40 -14 -58 8 -47 -32 28 14 -8 -21 -50 20 55 22 -47 -13 -13 -19 -20 -41 -79 49 -41 30 -7 -7 -54 15 5 1 -45 78 -38 20 16 -12 -48 52 7 -65 -54 -11 -48 -33 -65 -10 -63 42 -58 14 -24 -51 -11 -79 54 28 0 -51 -24 -47 18 -9 93 85 9 126 127 117 16 39 35 -20 -1 28 127 -31 -35 -72 -37 -75 -85 54 -1 -21 0 42 -39 67 17 75 -69 -43 42 -102 -8 -115 -60 110 -127 -85 -48 -48 -126 -60 70 68 -38 92 -55 17 43 -66 -3 -50 -5 -101 -36 52 28 48 89 65 110 69 -1 -17 46 -9 27 40 -48 97 57 -127 -50 -8 127 9 35 -2 -66 -111 -31 115 69 101 15 -42 84 81 118 127 43 -107 6 37 -78 127 52 -65 -3 124 0 14 83 -76 -4 32 -54 -12 40 80 -25 -20 -24 -56 50 -126 54 -26 11 28 34 110 -65 9 -6 8 -25 -51 60 -75 26 -21 90 3 -103 -69 -4 11 48 -34 98 -10 78 51 -15 -118 127 -26 -1 -120 -83 -49 -9 1 -40 12 -49 -127 -3 -4 -4 -18 -40 -7 74 35 -56 -127 -38 58 -66 -10 92 36 13 52 53 11 -22 -56 -2 -76 43 -56 25 73 68 74 84 -61 6 66 -17 107 123 24 15 13 91 -9 -20 125 116 -50 -19 18 54 -41 -1 65 81 -54 -80 3 -62 15 3 38 -112 -41 -42 -124 -12 50 1 -20 -55 124 18 -22 -44 104 -33 -28 34 1 -121 -56 -67 45 -71 -72 5 51 -10 -88 -128 -9 -11 6 68 87 38 48 33 -7 -7 59 -27 -23 26 -16 -78 -29 51 41 -36 -55 -52 68 -12 -47 -107 -16 27 49 -3 -63 -85 122 -49 50 -73 -56 -23 -26 97 -56 -126 -41 44 117 -109 -30 107 -120 -35 -10 -15 31 -108 -128 25 -67 12 -26 44 10 -4 122 34 127 -15 -127 -74 -66 8 55 -77 -24 29 103 2 127 127 19 -80 -14 -25 125 9 -32 -86 89 11 44 38 -36 118 127 23 126 113 96 -13 127 9 -87 42 -127 110 -76 -23 48 -67 32 18 23 42 110 24 -84 -5 58 23 8 -78 41 -114 -116 12 73 28 65 -100 90 -78 52 62 -127 81 -85 -89 -120 -73 -76 -82 41 18 -70 -51 117 -22 6 14 -34 -14 -108 -17 17 -71 -30 11 -101 127 -113 -35 23 42 8 -47 -112 26 125 25 -39 50 -110 -49 -47 -127 27 39 0 -10 114 39 -64 94 6 -67 92 -34 127 -74 -115 67 -10 -127 6 30 25 -83 41 36 127 93 -14 -32 127 23 -28 -77 -2 -49 -95 15 -60 -126 -76 -15 -45 49 66 75 -64 -4 74 -13 -120 59 61 2 -29 36 83 4 -42 -68 127 -7 60 -16 37 -80 44 64 9 35 -102 27 20 127 83 108 31 45 -19 10 -98 8 -20 44 4 -47 10 85 26 48 20 -8 -67 -56 -127 93 51 106 63 0 -28 50 -89 -21 8 15 51 -7 123 -78 -27 127 -11 40 127 -64 80 7 42 110 -6 53 -78 -9 28 21 31 -86 40 21 59 -21 5 4 59 45 -10 -6 30 -37 -52 -122 -25 86 -73 104 -42 -128 17 24 59 -82 -55 -101 -115 127 -118 54 48 -93 -42 -51 64 45 126 -109 -10 121 -28 -128 -127 127 97 96 -100 -105 17 -68 -103 -61 127 -122 124 -37 -53 -68 125 5 -1 6 -116 4 82 -29 -46 12 70 64 127 -110 -108 60 31 48 15 -59 -4 108 73 -70 -78 -81 -114 -54 -119 -46 -5 -92 127 29 23 -96 -55 124 -55 -70 110 54 127 19 -101 -125 75 -60 16 -83 -126 -126 91 127 77 -127 -5 -69 64 21 -120 -60 -99 106 -122 -118 -81 126 -21 -37 17 12 -10 -111 -128 20 -16 5 52 -34 -128 25 37 30 87 70 -120 32 30 11 41 2 -79 63 -87 -35 34 28 -71 12 -124 -76 28 -114 4 -83 -51 18 79 114 -4 8 79 -38 110 21 13 -122 -55 49 56 -25 105 -80 30 43 -69 15 4 17 63 65 -48 122 85 -106 -78 -73 29 24 -121 21 46 10 -102 -33 53 45 -46 -108 -51 14 -57 18 -35 -72 3 -111 89 -120 -33 126 -122 -25 -118 -82 -16 27 67 -124 14 -69 -52 -40 -67 -13 -4 26 6 13 -108 22 -90 29 -120 -42 81 18 29 86 40 -85 64 33 -72 -71 40 -11 -127 -72 5 -37 -122 39 34 -32 -55 -37 -44 63 101 81 -119 -125 -92 -35 121 15 -115 -101 -23 -46 -25 -29 -102 -27 -34 49 12 105 -52 -51 44 -32 9 -32 -32 -71 -74 -10 127 50 -34 -126 -123 -126 -89 8 -33 -8 -5 118 67 -60 -66 78 127 55 108 122 33 -128 57 118 127 -2 -37 116 11 127 -123 -1 113 105 80 -49 -66 -57 -128 -77 126 61 116 -38 -48 -43 127 63 -100 -19 -37 122 -94 15 -127 28 -44 28 117 -16 -128 -98 -40 -69 48 61 89 41 127 78 -125 -128 -127 32 118 -127 -82 -82 -72 30 89 -58 9 -128 14 -128 121 -75 -66 -24 -77 -62 -73 -41 -82 19 127 126 54 -119 -28 -10 -127 -100 -80 -102 15 -88 21 -38 -79 -15 -80 32 127 5 -79 -47 105 -54 -94 40 -5 127 53 67 46 19 125 27 -14 51 -30 95 -127 -105 -114 -113 -37 87 -65 -128 127 -29 -38 -36 -94 74 25 -58 114 123 20 -22 70 -58 78 123 4 64 16 -73 67 -28 -46 -80 127 -128 -46 -82 -96 -127 6 -33 25 -7 -43 -66 -14 -30 1 -100 94 96 90 52 127 -13 -22 53 89 10 -109 -54 -115 -17 -83 119 -27 48 -62 -109 16 -33 12 -70 89 -95 -42 98 40 -127 5 103 5 -5 -68 1 51 102 106 125 -35 -126 46 -100 16 96 -72 -30 -128 103 70 83 69 72 55 -80 -119 92 -127 -124 -53 -45 15 49 62 4 -17 -6 95 -41 -99 -114 -70 -127 66 4 127 80 -127 83 0 -6 106 117 104 127 22 -105 86 -127 58 -16 10 -8 -8 -2 101 34 29 -5 -44 -48 63 76 11 -98 -64 2 85 -88 -74 24 -5 -104 56 115 -18 -96 -123 -74 -70 122 76 67 -127 -128 38 -121 -6 -127 -77 13 -123 -78 127 126 96 120 70 -107 -6 84 17 -71 0 127 -42 53 125 -20 21 79 -85 -125 -121 -100 101 4 -121 46 121 -28 120 -2 -3 100 39 52 -122 -127 39 -45 72 -92 40 126 54 -128 -29 125 -78 -126 -15 -128 -110 -127 -128 124 -57 -21 -128 68 26 -78 -127 55 58 2 114 127 56 -84 -54 127 -100 127 35 2 -24 -35 36 -27 -51 -65 -96 -126 -125 -27 29 108 -53 -126 68 -67 -18 -106 19 66 5 93 -11 -24 -39 72 -34 30 14 77 -63 -120 -122 -30 -6 -25 17 95 8 116 -36 -86 12 -71 -62 52 -53 -39 16 -3 -26 17 48 91 -6 -11 50 47 -126 -47 95 31 -72 64 -85 -127 -28 12 50 60 -32 -128 -15 5 -5 -63 -13 -81 28 -48 -54 -127 -85 127 -70 -13 41 77 -15 11 77 -44 127 81 -55 -41 -25 -80 33 -128 -22 -10 70 -112 35 -77 91 -77 57 124 127 -89 -63 -126 -1 11 -8 98 -22 -15 68 51 28 -20 3 -20 -85 42 -65 124 35 23 56 -12 41 52 -21 127 113 -95 35 -119 -53 3 -67 -24 124 -47 -125 -5 125 67 8 -2 40 34 108 -8 -127 79 112 19 29 11 -65 127 -36 -87 -20 57 -98 56 1 -74 0 -125 31 41 -49 -46 -23 73 -23 -54 40 7 -128 -10 88 39 -38 104 -23 44 52 124 51 11 -121 18 9 -66 82 -78 107 123 -1 5 -23 127 -59 -18 127 21 -42 -29 -70 -25 -126 -25 94 80 24 -84 -126 96 -22 114 110 68 -118 -113 -101 -123 7 126 127 124 41 29 -128 5 43 -29 75 4 15 -128 37 -19 -2 62 -118 119 9 -40 -82 -58 23 -104 127 125 -122 -127 79 -80 127 43 -63 127 -72 55 29 -11 -104 85 127 61 -19 -114 42 18 59 -39 13 111 -72 72 47 -102 88 42 127 127 -47 107 -117 95 79 -73 25 127 -5 15 47 -125 34 -42 -73 -60 -71 -65 127 10 33 -59 21 44 -31 100 -128 -123 124 6 -25 127 30 -47 41 -122 -112 43 -5 127 95 15 127 29 37 48 -72 15 29 -28 -16 -7 50 -5 63 -62 83 -82 56 -29 -5 -92 -55 127 -104 -79 -4 7 -48 -112 -18 0 -88 -86 -59 -118 12 -18 33 -4 110 43 65 60 94 10 -81 -68 -81 -8 -65 -63 -38 -126 40 -48 -39 -126 28 26 6 -11 -72 -99 -16 -14 29 39 8 3 48 -19 -9 -109 -73 -24 -48 -43 -24 122 44 -94 -4 31 9 -73 29 -38 -54 34 52 -3 13 -61 -32 24 -36 -61 48 -20 -2 60 -6 -48 -22 -24 -34 4 49 -1 44 -55 -35 -7 -4 -37 -117 56 -31 -15 29 -95 75 0 -13 -1 -48 -35 -18 -9 -4 -6 37 -36 -18 -50 -73 -22 -28 16 12 -11 16 27 50 -40 -64 -74 -51 35 -16 -25 -25 -6 -45 60 -9 -12 9 67 5 44 121 57 -28 34 57 20 -29 34 -51 -19 -7 11 -34 99 -7 1 16 -15 -6 57 44 40 -81 38 43 -38 -84 24 -75 -16 6 -11 95 -107 118 -77 83 -16 -16 -10 40 13 108 -32 -25 -4 -39 -8 19 0 -25 -37 -13 31 58 -93 59 74 -16 30 -52 7 17 28 51 -19 24 31 -17 66 60 74 65 -21 58 86 -59 26 7 48 58 70 -40 86 105 125 25 40 -31 39 123 -21 74 46 -49 31 26 45 -15 80 -27 -21 42 55 -5 38 29 -17 -17 -12 -61 114 -28 1 -54 18 -11 46 38 -1 -6 -30 -12 -4 -98 -15 -66 4 -78 -27 -11 28 -60 5 0 108 -19 27 -49 30 -4 -42 43 -39 36 5 -87 -52 -6 6 -7 52 -35 -26 -126 -17 4 2 70 -72 52 -43 -4 79 -61 -32 -17 -58 -2 -11 34 7 -64 35 27 127 -125 32 76 58 -29 -23 -9 9 21 19 6 21 -6 115 -19 32 -85 6 70 9 -3 87 -54 38 3 -112 -43 -42 -47 127 86 -7 50 10 93 -54 -72 39 -2 30 -11 -33 8 -16 -73 -74 -67 -7 -7 -34 24 -6 -2 71 52 60 22 -104 -32 16 61 -56 -16 6 33 38 -61 -127 113 -9 33 -25 -56 -113 -12 -63 -8 49 1 -25 39 5 -64 5 -69 -19 101 36 -42 54 15 -26 2 30 -42 71 18 62 16 -86 6 2 84 81 7 -18 95 84 46 -78 -2 27 46 -25 -11 17 -53 -80 15 -36 13 60 -22 -17 6 -38 52 97 -74 -66 33 125 117 -103 -74 -22 67 31 -109 14 -55 -73 22 25 74 42 7 127 -18 35 64 1 -1 4 -19 66 20 -72 69 -43 -69 2 -40 -109 4 15 3 -48 39 15 57 49 92 -67 -124 -7 18 72 -96 1 115 125 -9 -7 51 -2 -24 27 -26 -44 -126 21 127 15 48 -18 103 -74 -11 -29 -88 -3 44 5 -15 -74 -50 -67 -33 1 11 -95 126 -44 -49 109 18 34 -90 -3 10 52 -31 -79 67 7 -45 28 -21 31 5 -127 -85 -3 81 -120 -86 -52 -102 11 -32 82 1 23 78 9 68 -55 17 72 -7 3 -21 -30 42 85 -18 9 -43 -80 -45 42 -4 21 42 112 127 97 -60 -18 49 41 18 69 -128 71 0 -38 -82 -57 -92 12 -2 25 40 102 -24 25 8 26 49 29 -57 -29 3 -34 35 -53 -55 40 78 -53 78 63 -56 115 2 1 109 45 15 -71 15 42 0 68 13 107 63 24 -34 109 -60 -8 24 -71 -21 39 2 -37 -56 1 -58 74 -42 -17 -5 21 20 68 17 114 7 -85 54 -43 23 47 109 12 -12 94 17 50 122 0 24 74 -127 69 43 -50 -30 0 -41 -62 -37 32 41 -25 -46 -12 4 -77 57 124 -29 -12 41 10 -42 53 -49 -23 38 125 8 -86 -19 -24 47 107 -37 -80 29 123 9 24 53 2 -116 47 25 -91 108 -117 43 35 -76 -106 -118 83 -83 127 -38 -119 -52 56 -30 -120 87 46 -56 -13 -57 -3 117 -76 126 -14 2 -63 65 6 -104 52 -118 31 67 20 -63 -76 65 14 29 -45 60 65 103 7 86 -24 -55 -35 51 4 -24 -122 -1 -118 30 60 13 10 46 101 4 40 58 -60 52 -56 -38 35 51 -113 -42 15 92 57 -126 -32 39 -101 91 28 -118 -124 -81 -117 -119 65 -55 67 -3 -3 23 -90 15 -78 -28 -6 127 126 -24 2 -3 -5 15 8 54 -37 67 -83 18 33 47 106 -4 120 6 -42 57 2 -115 -28 -37 4 -53 29 11 -61 67 3 7 -93 24 14 -19 24 42 68 67 -23 -71 55 31 36 41 -77 -53 -11 -60 -15 -33 -14 34 6 -76 -33 14 -12 -2 68 52 32 -114 37 35 -17 -78 -4 -53 25 -45 7 11 21 -43 -40 -64 15 -11 -128 12 54 1 101 -110 -26 -21 -55 84 53 -52 -7 9 -24 -45 -108 -74 -10 61 25 -94 88 -26 10 59 10 -128 -98 4 -122 -30 -68 123 -30 -106 76 -122 14 40 -56 20 -23 22 -13 -50 -78 -60 100 34 91 54 -16 125 26 28 -21 -121 41 100 -60 -41 29 10 31 -9 -9 38 34 96 36 -33 26 -122 -52 -51 -125 -66 49 -125 -51 -55 -12 30 21 -13 10 -36 30 83 -58 -21 33 7 -42 121 -23 -90 31 108 -87 127 -17 -32 -66 -57 2 7 101 3 81 60 -121 -38 -2 125 -14 10 -38 21 -17 -27 -120 -21 127 86 -71 58 -13 22 -75 -48 17 77 -9 -4 -111 60 -23 111 -113 -87 13 -123 -102 -84 -40 40 56 22 -51 89 -59 -108 -120 -41 -127 24 -118 -122 -62 -2 -112 41 -41 -52 -127 -51 3 29 -43 16 -59 61 61 -64 23 -125 -70 58 41 35 -47 -20 3 -119 -33 7 -18 -41 -44 -73 35 68 -127 -128 -31 97 15 -62 -28 3 18 9 71 33 77 46 57 -1 52 -45 13 3 -60 21 -2 19 -49 -79 -105 -9 29 43 77 37 127 15 1 -86 126 -22 -38 -83 39 10 -28 122 -57 21 -28 39 -79 52 -16 74 -28 -69 30 9 -20 12 -4 -30 -51 32 -57 -20 63 -37 -63 -30 45 -60 -121 31 117 -49 -28 17 8 2 0 28 22 -38 -23 10 64 -49 -26 22 -25 126 10 -78 -9 35 -97 4 38 -63 7 55 16 -101 125 -27 -20 21 7 16 57 76 -17 -52 59 25 -54 -16 73 105 -73 21 19 -78 57 -41 -54 127 -76 -43 -88 7 27 -54 34 -64 69 -106 55 -64 -12 127 23 20 24 -9 36 32 36 100 -35 -104 -70 -36 50 -35 30 -1 -18 15 -32 24 7 -16 96 98 67 -7 27 -51 24 42 -45 -12 -39 -43 -60 29 -80 22 -61 88 -14 21 126 -57 -127 8 0 52 -91 -63 94 -61 6 -36 59 -116 9 -53 13 5 -29 37 21 -18 -81 57 80 23 -40 30 14 -111 67 -17 -71 -37 122 -40 -98 103 48 -8 39 45 -33 46 -67 -57 0 -1 -70 -27 21 66 98 22 91 -53 -45 -41 -18 49 -16 91 -127 76 49 85 -38 -10 -58 -74 -12 -19 15 -19 -51 -80 101 110 -27 -100 -12 -34 -14 -93 -9 4 79 13 -6 -2 120 -51 -25 -56 46 -25 1 -19 -125 58 -7 35 32 -29 14 52 -91 21 116 -12 -59 41 -32 -14 -120 -57 26 85 5 127 19 -20 22 -22 24 -7 99 -4 -74 -22 -60 -91 63 -127 112 -20 118 -61 62 -20 23 -111 118 -34 68 -7 34 -9 -74 23 62 43 51 30 8 -47 127 90 16 -77 -124 60 -63 7 0 32 -17 41 -38 -85 -2 55 78 13 91 -56 10 39 -59 -27 -7 -68 -12 -42 -29 19 -46 8 0 -67 36 -10 58 -32 1 -63 9 25 -51 10 -128 7 -15 10 20 2 11 127 64 23 45 -71 -1 -3 -37 33 64 18 -5 91 12 78 22 52 -54 46 118 28 -17 22 -6 -1 -13 -14 -79 39 -73 -64 -78 13 49 -110 -19 81 -4 -114 -30 -106 -77 -128 -42 -68 -84 67 83 -112 38 18 -76 -13 -65 26 -46 41 -34 117 -84 -25 69 -35 -80 15 6 -111 74 -116 -44 54 15 -55 -16 7 -112 -48 6 60 -4 -47 13 -9 -16 -35 52 -79 -26 -45 -10 -23 -109 51 93 -13 -13 46 -21 -37 65 -72 -71 120 -57 71 -18 -69 -31 -61 -12 13 -12 -55 -37 79 -5 18 30 70 -36 33 -9 -72 56 90 73 -52 -35 -125 -66 -84 -24 -7 -61 -55 45 -41 -104 -24 -25 58 24 -119 127 79 -88 -79 -66 40 23 -90 93 71 3 64 -43 35 -62 -58 -35 126 14 -50 -82 -41 -31 -11 -125 70 -5 56 14 125 -22 -42 89 -88 -33 -49 94 -22 3 23 72 -86 32 -88 -10 -36 82 37 61 31 -92 -13 -14 -8 -8 -126 22 127 -43 -10 -4 -69 7 16 -69 93 -128 77 19 63 19 119 6 -31 67 35 -8 -18 15 -73 93 57 -13 124 3 33 3 6 -24 -87 -9 -8 -77 17 36 26 -14 -11 20 10 -43 14 75 -28 10 -119 -61 7 -22 -63 -115 7 84 -40 -70 -118 -105 -39 -34 74 18 103 13 24 23 2 12 73 -14 -64 -10 -35 -122 -112 -32 -10 -18 -121 -116 1 -16 -85 9 -103 -45 -96 101 -39 48 -71 22 -81 -46 -58 -56 0 -65 83 -46 90 58 -128 48 -127 -20 -126 -64 -120 -110 89 53 -128 -125 -3 -13 -128 -75 80 4 -96 25 -61 66 -1 -128 26 -37 127 120 0 -17 -128 -26 40 30 -46 42 93 -71 17 127 -126 68 -16 -34 36 -125 -29 111 -60 9 -55 118 -69 108 -46 17 85 125 32 -58 31 -122 29 17 71 -61 -51 24 -60 -63 -6 9 80 73 -127 -45 80 126 -127 -12 -17 125 -123 80 -93 47 94 82 33 -79 -91 -99 -55 -54 -126 -67 -113 27 -44 11 13 -14 125 6 -106 52 113 1 54 126 36 120 0 32 36 13 24 -126 -122 -27 45 -65 -86 -91 -117 13 -94 -60 -110 35 -34 55 65 27 127 -1 -68 -9 -128 -122 76 30 -108 2 -102 -82 59 -123 -19 10 -56 10 -10 -5 -31 47 40 79 127 3 46 2 25 4 34 -128 24 -89 -43 -1 37 -74 46 80 -1 -4 -42 100 109 10 122 -51 3 -58 74 -127 61 75 -37 -77 -19 -78 19 21 27 127 127 -52 -89 -77 -127 8 -125 -116 114 -112 94 -115 59 -18 18 27 -68 -11 105 125 -52 -93 -128 -44 -57 15 127 -8 43 16 -97 46 22 3 -29 31 4 -115 -17 -76 -53 -71 -24 -21 -108 18 -103 -18 127 98 115 68 34 50 -68 7 61 -126 -53 56 19 -75 75 -91 -83 55 -56 53 67 75 14 -14 64 -96 -126 125 60 125 81 57 -9 4 49 27 2 -126 2 -125 71 -11 119 -17 81 64 111 -64 -126 -59 -126 -120 1 -1 14 -56 69 24 -126 -100 55 127 127 -17 -81 -55 -116 68 -65 -54 127 76 58 0 -128 45 96 107 75 -3 77 39 -97 92 -127 -21 -113 -5 43 -97 8 -20 -74 -59 -19 -77 -67 83 -24 96 6 122 124 -56 68 -128 29 -3 68 -51 41 -43 -126 -9 109 55 -64 -33 -128 -87 -101 125 -11 -90 -88 122 -99 -112 -87 28 35 -19 43 103 -55 -15 115 6 -49 10 -25 14 -128 -7 24 -64 23 -52 -120 -21 -75 51 85 126 125 -61 -31 24 13 89 -33 31 -124 -48 38 -119 -12 37 -28 23 -126 60 -6 107 -59 -45 126 -70 -58 -124 -88 -122 -12 -64 -97 -58 -24 -64 -77 -54 90 -22 -107 -7 -124 3 52 111 -26 59 -7 114 56 -108 15 34 -116 18 -99 -128 -58 -43 -73 55 76 -117 -127 16 -81 55 114 16 25 -48 78 17 29 -63 -127 -128 8 30 -14 -13 -18 -50 -60 -1 -111 99 74 -42 -126 40 -67 82 -19 -122 -64 -19 127 -127 58 48 61 36 -32 -24 121 -9 -12 -73 -127 -17 -5 24 127 -62 37 82 24 65 10 -47 -45 -37 -36 -52 68 49 79 -66 11 -59 -88 20 96 -39 20 -15 110 -48 -10 -47 36 -62 -60 -97 35 103 2 -42 21 -16 -45 -42 28 44 29 20 -126 76 125 -104 52 64 71 -47 121 74 34 19 -73 127 -42 -126 -43 124 123 -8 11 -60 116 126 -125 -38 44 44 -126 -33 21 107 127 116 44 99 -83 -123 67 127 127 55 6 -39 -8 127 -76 96 124 54 -53 -1 -10 113 -69 45 95 -47 -59 108 73 -102 127 16 -128 84 50 106 121 114 -26 -4 5 -89 120 -21 66 -38 -20 -125 127 -127 120 -128 -6 27 25 65 123 -40 2 81 62 15 -113 10 -127 -105 -52 53 85 124 -13 -15 107 33 -80 124 -4 -21 0 82 60 72 119 94 127 41 -122 21 -127 -48 4 124 -87 -10 68 -89 -90 37 -9 -66 16 -127 62 74 -25 115 2 -5 -68 121 120 -120 110 114 53 -49 -124 56 126 -89 -35 -10 127 -122 -60 -8 -64 -17 68 118 -57 -70 -33 -66 114 76 -3 9 -47 -21 50 -43 34 24 53 59 -119 -48 -111 -127 74 -125 -89 -110 -128 -119 -12 -13 -91 -127 70 41 -121 -5 101 -48 127 -60 -90 24 -113 91 -127 82 111 -90 -15 -127 -44 8 -8 29 -71 -62 87 -30 -53 -18 -128 -16 77 125 82 93 30 -39 127 -127 -14 -33 127 -38 -18 -121 62 -128 115 68 -15 91 127 5 124 15 22 -76 -71 -3 0 -26 -7 34 -59 42 126 -38 -8 127 17 -70 39 -67 108 -40 -127 -1 -127 -54 -44 2 -33 -82 -101 -8 68 70 14 -17 -127 36 73 -64 -91 -66 -67 -32 -126 72 -119 126 116 -5 -65 -13 102 123 -9 -43 -36 -73 -47 127 94 127 -15 27 -71 36 2 125 -89 -20 -61 -65 -44 48 -64 37 -79 67 124 -1 -117 -37 2 124 -21 115 24 -58 101 -39 70 96 123 58 8 41 95 68 -123 27 -127 -14 -30 26 119 -125 123 118 -29 -16 3 -39 -45 127 48 -33 35 56 20 28 121 -36 110 -15 -71 8 104 26 5 -78 111 60 27 70 -85 -42 64 -30 126 70 -12 31 116 -69 -128 120 25 -59 37 126 -16 -126 -7 103 3 -128 -28 -125 27 -7 -121 -30 -62 26 -75 -94 59 15 80 68 94 -26 65 -55 1 0 -36 77 88 127 11 16 28 7 27 43 -13 -31 28 126 -22 36 -128 -95 48 23 -124 -9 -87 85 73 125 51 -7 81 -4 5 59 -46 -9 125 -41 -35 126 74 11 97 25 -94 -114 -27 -16 -46 -26 32 -126 127 100 59 -59 -126 10 21 -39 18 -96 -110 93 127 -45 77 -17 -77 -65 -24 17 -37 -45 -43 23 51 43 -5 -125 -127 1 -35 23 -37 -76 52 -3 -14 -8 -63 -84 -30 -127 -26 -51 72 6 127 -12 -73 92 77 49 -23 -118 -22 127 106 79 7 55 -78 -2 -125 -29 19 30 -122 -46 5 25 -15 6 70 38 66 -97 -39 -69 -124 -87 -19 56 -1 -126 -4 -35 11 59 16 66 -27 -36 119 -44 41 64 1 -38 14 -25 -112 83 -127 -7 61 -39 45 94 -2 126 -57 88 3 -51 32 -7 -18 -3 76 -37 -32 124 -83 26 -40 33 -20 86 118 97 -127 33 -106 -34 -10 116 7 61 -21 124 24 42 -114 25 -17 75 41 -3 -5 119 70 -115 126 -9 19 -7 -80 127 43 32 -12 73 -30 15 18 -8 -55 15 -64 -20 -30 -125 -62 -126 110 0 -48 -92 10 35 -45 72 99 49 44 -34 89 45 -38 5 -56 -34 -82 -77 -84 -33 -51 8 16 22 -49 -27 80 0 -10 -3 -100 -32 56 -34 9 69 22 -29 10 -41 -127 6 108 -67 48 -13 -42 -69 30 45 -62 56 -18 -92 118 125 12 -117 2 30 -67 3 40 -21 52 4 35 -51 94 42 32 -99 -74 -22 -40 127 68 123 -128 -44 88 -70 14 -126 67 78 106 2 70 -126 90 -82 -113 -32 -13 -22 113 -32 26 -69 -18 -46 14 61 -35 41 86 -46 -14 -40 69 -9 125 -32 -66 28 -97 27 1 -13 81 -87 -84 -20 95 -87 30 -4 -126 -50 13 3 2 -12 -4 0 -111 64 34 82 9 -47 24 -12 49 -117 76 -39 90 9 -19 108 72 -125 42 22 -45 98 127 7 56 -109 48 39 7 21 -67 71 52 -1 8 4 124 -103 -128 -24 41 105 -38 14 -10 0 42 -19 -30 97 16 -109 53 -128 127 57 -91 59 16 -63 54 -19 -30 3 -80 65 11 29 51 26 39 42 11 24 104 -1 -128 58 -70 11 70 -6 43 51 -55 22 31 -40 19 -115 -49 -6 -128 -123 -117 3 56 49 17 48 -87 -17 -62 -23 127 77 -63 105 38 44 -29 34 -18 36 34 124 -61 -76 -5 -7 -128 -35 -15 -45 56 -75 -40 48 58 -3 115 -126 21 126 75 6 -46 -50 -14 -48 52 -13 40 75 -18 87 10 37 8 86 -57 -68 13 14 51 -101 -107 -79 11 57 24 -9 -22 -18 -68 -4 -119 39 35 -11 -67 -103 54 103 99 -31 -37 3 121 -93 -40 -15 -86 93 90 23 -56 40 -43 -125 -108 67 -10 11 99 100 126 9 95 -7 -47 47 -113 -79 -18 -18 127 27 -7 6 -25 -126 -46 -62 59 112 -4 -30 -127 -53 -128 107 34 29 18 75 -37 122 83 2 15 -76 -12 -40 -78 -18 -83 126 109 7 125 -35 24 82 70 13 88 -10 124 24 -23 96 116 51 121 26 -41 -76 -66 -27 -37 -113 -95 -54 -59 -18 -34 74 -84 92 -62 -79 -8 -52 -128 126 3 -71 48 7 -125 18 -48 47 78 -33 -52 -23 -128 -19 -25 -23 34 17 14 74 2 84 -126 -50 -28 111 -4 -74 25 -47 91 101 9 -17 59 52 -35 70 10 -71 27 -32 2 -73 -38 -26 28 39 72 -127 -128 5 46 -94 80 -32 50 -70 7 8 -33 65 9 -127 -4 96 -24 79 -98 13 -21 59 93 100 -127 -14 -46 -72 10 -37 27 -6 -114 -70 23 -121 -30 30 63 -56 -33 15 -40 -74 88 110 74 32 24 0 -22 43 -30 -17 -85 -13 -9 -53 28 -51 20 47 21 0 -13 10 -12 84 40 98 24 18 9 33 28 56 55 46 -12 -19 27 12 -21 -7 127 15 3 -25 89 -26 61 43 7 -37 13 5 -15 34 23 -34 39 9 2 16 29 -13 -51 5 18 25 18 14 54 40 -24 15 64 -56 -25 -10 2 3 -29 25 14 -8 -13 18 -3 8 -26 -24 38 -71 -27 53 119 11 -8 -46 -10 -32 -29 -27 54 27 -11 -61 9 -7 -16 -66 -15 48 -3 35 -3 52 -19 1 24 57 -9 -72 -12 -38 19 -1 5 74 -33 -47 -2 -10 -37 -18 -59 49 -25 56 -59 4 -4 4 -50 -27 -29 -43 -59 121 74 -31 127 12 -46 -2 -33 1 -8 11 -2 18 -9 -9 13 52 39 -3 81 50 -11 -8 -26 -13 31 -27 -18 59 24 -36 0 8 -47 -33 -47 -4 -57 -15 -21 -7 -39 43 17 7 -6 25 -16 5 -9 75 -4 1 -12 -39 -1 29 -33 -15 -14 24 -21 -45 -22 9 19 -8 4 0 -33 26 4 50 -7 8 -51 -110 -3 53 16 -23 29 -28 -1 10 6 -6 117 -6 -1 -13 -20 -29 -12 66 18 -9 21 -7 -4 5 2 -50 39 -10 -18 -9 8 -16 30 52 81 -80 3 12 -6 40 0 17 1 14 -58 17 -10 -11 -18 -22 16 -4 -34 39 -19 -49 0 -30 -1 9 34 -47 -20 -24 -22 34 -8 8 29 2 60 10 -81 -18 -69 -7 -41 -10 -6 -48 38 -8 -8 -6 3 29 -1 -10 -34 36 5 20 117 11 -37 25 -10 40 14 67 3 10 -18 -31 -15 32 -18 45 -5 11 -16 -118 7 -17 24 -42 -17 -6 -34 -13 -27 9 -9 -11 -31 -8 -2 -50 20 -115 -75 14 -8 12 -69 -2 1 6 -41 -19 -8 -24 -51 20 -1 -43 -89 16 -2 -33 18 -7 12 65 5 -49 14 17 24 -26 -18 29 10 52 -55 -4 3 -20 -57 13 43 -52 57 32 11 -26 56 -64 -38 -21 0 -3 50 -47 -29 -14 10 35 -51 3 -30 -1 -47 48 -6 -4 112 -29 -15 -12 56 67 1 -49 -25 -15 -4 34 6 80 59 8 -119 55 -1 -19 -25 -41 51 -5 -14 -4 75 -16 16 -47 36 120 -17 -11 -5 -12 -22 -21 34 -121 1 -32 -128 -59 -19 -7 15 8 -9 70 14 20 40 14 31 25 -43 2 -36 -5 -35 -37 96 89 -23 -63 97 0 -32 8 -4 3 13 13 7 -25 -47 28 -5 14 -10 10 13 -3 9 -77 -2 7 -20 -1 -16 -10 0 83 -7 46 1 -12 -10 12 -10 -34 26 119 7 35 13 -9 65 -21 25 102 -11 -34 -9 8 -13 -93 10 -4 -3 -1 1 -1 -14 -12 -11 18 3 -19 11 -1 -12 -56 0 2 5 41 -33 -4 5 -8 14 2 -6 3 -26 -6 2 -10 14 -9 -6 74 -56 5 -12 50 4 +127 36 100 58 -32 -52 -33 116 12 -102 57 13 48 82 23 -3 106 13 50 27 127 -14 40 -85 14 124 97 -27 68 -45 -17 6 10 -65 22 39 24 57 -25 113 -68 39 -39 17 -120 -75 6 2 -110 -92 73 53 -34 2 127 -28 -6 83 -57 42 84 74 108 -1 26 6 98 115 -109 -1 -66 -69 79 61 -14 -128 -128 -58 -3 -86 83 -84 -58 115 1 72 60 65 -2 -4 6 -60 -128 -6 20 -30 22 -16 20 105 48 81 -42 16 49 71 110 -8 16 -96 6 66 -22 7 -45 127 127 84 30 -73 -41 64 -92 -20 38 66 7 -41 35 -124 -104 34 51 -83 -97 -60 -40 9 45 -56 -53 29 -56 86 -42 -39 80 -26 -14 16 -52 -1 -115 12 -27 48 23 104 58 55 123 -39 13 75 -71 108 -39 13 -22 -77 -36 21 56 -30 -13 43 -56 -20 -125 79 -20 -27 59 103 57 39 6 96 -124 -111 38 0 109 57 -23 -65 -8 -44 30 66 91 -2 -120 -95 -52 -60 17 -69 -2 -62 -37 -23 10 21 -118 17 -65 -2 -92 -28 -68 -9 31 -37 40 -127 29 -15 -14 -8 -23 37 47 80 0 -84 -107 6 29 23 -67 -2 -7 -40 6 -26 -126 -25 17 -84 0 103 36 -6 -47 -62 -17 -101 81 3 -19 -80 -127 -69 -38 -2 55 19 -12 -58 -8 32 -20 -35 -25 -35 -41 22 -59 4 -79 35 73 -64 -46 -19 28 48 51 74 -122 6 33 -6 79 -41 45 -113 -124 17 22 -127 35 -56 22 120 -68 121 -126 34 -54 -68 -105 -105 55 18 -74 90 -26 16 -22 -91 -44 -20 76 0 -85 55 85 -127 35 46 11 -30 -110 -62 -30 112 33 -10 33 -42 29 110 124 -124 -6 -6 57 32 54 11 -127 47 -105 -80 -41 56 43 -124 119 -18 -81 -31 -72 -72 0 -36 12 -9 -53 90 -89 -21 50 -3 -59 -63 -125 66 75 -68 -39 14 108 1 40 -42 76 -123 51 -42 113 -36 -50 -5 -32 -16 -112 -20 -48 45 -57 -120 67 -20 -16 -23 82 -66 -24 13 37 -81 -31 -55 -128 111 28 -32 124 27 105 114 11 19 61 17 -9 -31 -54 -35 -18 18 6 -1 -27 10 -18 -75 -32 26 -33 -69 -98 10 66 -125 -20 78 -124 15 -23 99 -47 -100 62 34 10 73 -88 -51 -48 119 88 17 -90 -76 4 -119 -9 120 17 55 57 88 -49 -39 61 -31 -67 -15 88 41 11 27 -5 -4 -67 45 6 17 30 11 -39 29 3 11 40 -17 -33 60 15 -12 -45 94 -4 -17 -48 29 -34 -53 39 12 -24 71 -69 38 -29 -37 60 -13 28 41 79 32 -42 31 -5 -57 -20 80 -32 -22 46 -57 24 26 -86 -103 11 -26 -1 61 -63 -20 -43 -2 44 -90 62 -2 7 103 123 33 80 -45 -92 26 -41 -38 1 -32 0 -31 -40 -4 31 36 27 37 -34 47 42 -16 69 -9 -69 -58 25 112 -20 -76 91 -13 -25 70 -60 12 -115 17 -8 10 73 -22 -36 -1 81 -68 10 47 -69 -42 -2 72 40 -57 76 62 -34 70 2 12 15 121 -29 -121 30 -26 -30 6 69 -53 0 -31 79 -123 -3 47 -6 32 -128 -51 -15 -61 -31 -44 -15 30 45 3 9 71 69 -76 47 -46 -25 81 45 56 -90 127 -37 49 -81 6 37 -121 -16 -11 -56 -50 -30 25 96 -44 106 -91 60 -38 -44 -7 -27 53 -53 63 -73 -110 -57 11 97 -50 115 71 94 6 74 -14 34 -34 48 72 16 0 17 -123 117 -71 -49 -82 83 -5 89 117 48 -120 -49 40 -25 64 5 -7 -76 -45 -33 0 27 -36 -13 11 27 116 -79 -46 121 -18 30 -7 -52 -125 -42 -111 -31 -97 66 -12 -8 -39 43 -69 -14 57 -44 -4 2 31 -12 113 123 -19 87 -28 76 -95 60 -105 1 -77 1 -37 -5 3 64 18 -16 26 -32 6 126 -43 -52 60 -87 28 -36 75 -64 -33 -33 -53 -108 35 31 92 -29 -8 -96 -11 -5 76 127 89 -127 -28 -10 -115 -4 16 -25 -95 -29 37 -59 -11 30 43 35 -106 -40 26 28 -68 -22 -68 -4 108 51 41 -86 7 8 -54 -19 78 -106 46 17 -93 2 4 -42 -34 -93 -66 9 -82 10 81 18 30 61 24 47 11 64 7 -10 -51 -35 41 127 97 -15 20 121 96 -7 17 21 43 105 -98 -55 -104 44 30 -84 -50 -22 13 26 53 -107 127 66 -66 -18 35 84 126 34 40 6 36 118 70 -27 -34 60 -88 -96 110 45 24 3 13 -126 -60 19 -53 -21 -62 -99 13 -62 -20 -10 18 -36 -56 -30 13 34 -36 -6 -36 103 -75 -9 -125 11 -128 -118 -58 -45 65 18 22 -71 47 92 1 -69 -16 15 60 -13 -13 15 -30 -84 42 -82 -30 -12 -42 -118 1 10 -56 122 -107 -43 27 91 -48 20 -123 97 26 -32 26 -69 -25 8 48 28 91 -125 51 -47 -37 -21 -23 2 -40 81 -3 -54 63 -22 -30 -44 -52 76 -32 101 79 43 24 -3 34 -31 11 -39 113 -48 26 -76 -35 -49 -50 12 -119 1 27 50 16 12 2 -103 16 50 -5 32 38 86 -107 15 -110 -22 -12 12 -12 -1 0 -27 25 -10 -23 2 66 -62 -38 -95 -6 -20 -54 123 116 -29 4 -34 63 28 -119 -17 -17 59 -16 -17 58 4 87 -23 -29 11 94 -13 -100 80 127 -45 9 122 83 2 125 -11 109 -13 -7 11 -2 -1 -34 -127 2 -54 -92 -20 59 -36 65 0 91 24 -30 -58 -19 22 21 -99 -124 50 -114 -41 71 85 35 -13 55 112 80 25 -21 12 -96 -10 -93 -50 -1 -92 -95 41 -36 47 20 -77 -30 71 29 -61 -94 127 31 122 10 45 58 7 -37 -8 125 77 -109 -55 28 44 2 83 -32 69 -122 -104 -126 6 -60 30 84 67 -72 -40 48 -127 -112 -60 26 -66 59 43 -128 -40 -13 -28 29 -4 -7 -65 9 -36 -20 -20 -36 -75 -48 14 -68 30 -73 33 -116 43 -30 126 -21 -83 -96 -15 -3 37 80 -103 -28 78 -89 -20 -7 -39 -21 -53 -49 -69 -121 -63 67 -62 -1 -47 90 -108 50 -22 -106 -15 -122 -19 8 -66 69 -114 0 20 -18 -7 1 -68 30 17 123 -27 -61 -120 24 12 -38 -51 -50 -50 -96 -64 -46 -84 -88 22 -23 -39 -9 63 -85 5 -19 31 49 -2 -25 66 -80 38 -17 7 -44 -7 -49 15 29 25 -40 -9 -14 8 49 -54 62 -56 -40 -90 -9 36 -35 20 -14 32 55 -16 -8 -23 43 -51 -2 -49 66 -35 -55 -38 21 -47 -36 -7 4 4 -18 62 -50 13 34 -3 49 -51 -90 23 -95 48 20 7 16 -56 -29 -19 -98 -20 34 9 33 6 13 -78 -80 29 4 -10 -3 -105 -2 27 27 48 -41 88 -7 64 -50 1 7 11 -46 -5 3 20 0 -64 8 -128 15 -2 -76 -62 28 24 -31 -117 -100 -13 58 19 9 8 0 15 -41 21 20 -31 -31 17 -11 -27 70 38 37 -11 13 81 78 -14 3 -89 -26 -46 -40 -31 -64 50 29 -13 3 27 72 7 10 -11 54 -13 43 23 2 115 54 0 72 27 24 31 -39 44 13 -30 -33 -2 -51 -26 -4 -26 -13 31 66 92 95 43 -58 58 18 19 13 8 27 41 3 -47 -8 -43 -19 38 -127 22 -23 -102 43 -4 -85 17 19 53 -56 -32 -67 -34 20 -29 1 -30 65 -12 -24 -115 -95 -27 -48 -45 28 -44 -79 -35 -17 -34 -14 5 4 -34 -39 -17 -60 24 -61 6 -40 -59 -56 -57 -15 -7 -22 -35 17 -51 -9 -51 -13 -32 68 18 -69 -90 2 -27 -25 -85 101 -55 -60 -39 -56 -82 19 9 -68 -21 -25 7 -7 -8 -17 -65 -29 -88 62 -42 -74 -7 -92 -31 10 -39 -24 1 2 49 -21 23 -4 63 24 -48 -15 33 57 -42 1 21 -41 5 51 32 60 14 -13 -34 61 13 24 -14 -14 -12 2 -10 -22 61 16 -16 -41 16 13 9 -3 5 31 -3 37 45 56 -121 -14 -56 41 -23 7 45 36 2 -22 13 15 -42 23 -65 -5 -1 34 5 -45 35 3 9 -29 -41 1 39 -36 6 -52 -19 12 39 19 -30 -28 32 9 6 -12 1 12 11 12 8 -6 127 -9 -34 -8 14 -5 1 -79 13 -37 14 -39 -70 -35 -20 64 -4 4 0 -75 -45 -57 -46 -11 65 -42 2 -69 30 -5 18 26 -6 34 30 23 7 26 -9 -8 -40 31 38 -20 44 43 -18 43 -46 -6 14 -42 25 -61 46 53 31 -77 -19 37 -3 34 2 -40 -7 8 0 99 -44 37 -6 -3 -13 -9 1 56 -59 -17 -36 -30 -82 -37 -46 -37 26 1 56 41 -1 -40 28 -99 -52 22 -66 -4 17 52 -76 40 -73 19 3 -29 23 -1 -128 42 -89 -28 21 -38 24 24 75 -87 48 -46 20 3 -26 0 -68 4 -84 74 -13 -58 5 -11 -118 56 -20 -36 -11 -11 -31 -36 -22 -8 -35 -26 -67 -15 52 -30 23 -1 -49 -104 -46 43 -27 -89 -24 -39 37 -21 -40 7 42 60 -31 -98 -44 -30 2 16 -78 -54 -26 -10 26 -32 5 -5 -31 -92 -29 -73 13 0 -33 -36 57 -2 -33 24 -29 -106 -8 -120 -113 -67 59 -14 -66 -16 -5 -44 33 -8 8 62 1 -13 -14 46 -51 -52 0 12 64 12 -38 86 -6 -19 -21 -11 -46 16 -70 59 -36 7 -36 -16 -33 3 15 54 8 48 -118 -25 -59 26 25 44 20 -17 4 26 18 -51 -6 -14 39 -22 -51 7 25 -40 27 -11 -8 29 -11 -33 27 -18 -9 -45 -16 -13 -11 -60 29 -65 70 -22 -47 20 -33 30 12 9 22 -31 -16 33 -33 -8 41 -41 75 -3 45 -29 11 -7 50 49 -70 103 54 -13 93 -58 37 -36 43 -37 -2 3 58 -20 -128 45 -44 -77 20 71 -40 -21 -124 35 93 127 -14 86 -40 16 13 103 48 -57 -16 19 87 75 11 127 40 -123 -91 -28 -3 74 15 34 -68 -47 79 -33 58 5 -98 42 121 -28 -74 23 42 -15 16 7 36 63 -121 -31 -59 106 -41 9 -12 0 -13 -75 -7 -1 3 34 98 -13 -89 -30 -18 28 -5 23 35 -96 -107 -24 -34 95 108 115 80 -31 -67 49 5 -57 74 56 119 127 37 49 127 127 46 50 74 64 56 120 44 73 84 -45 125 91 42 101 -16 -27 109 116 78 32 58 69 50 127 92 -54 27 118 103 113 -56 51 127 -2 125 27 28 -29 78 99 -32 21 114 112 38 -99 62 83 -9 -9 42 -77 56 10 15 96 109 32 21 10 100 113 102 24 35 -19 41 84 98 54 -75 -1 123 2 91 64 22 80 122 117 -8 26 113 -39 55 35 11 31 76 77 113 31 -17 15 87 -41 26 -112 72 -90 89 -57 65 -77 96 18 -46 62 -42 -12 16 -31 53 -38 9 -68 56 -15 32 67 -43 -60 -17 -95 -127 -21 114 87 -87 -36 -39 105 121 16 58 -60 -110 -38 -103 5 -114 115 56 -48 -44 7 -62 -38 8 12 20 10 53 41 23 -34 -49 16 116 -44 17 -10 -34 -47 21 -84 -34 64 82 22 6 -27 68 41 -6 -50 105 -8 -48 -91 -10 -63 26 101 -70 56 -68 39 78 -27 -47 -36 -46 -93 -49 -46 -1 -7 -89 32 -16 -6 -86 1 -65 -102 117 36 60 -123 53 7 -31 126 78 105 -49 53 18 -50 -33 119 38 -46 -90 -7 59 -33 112 -62 -26 20 -19 1 99 -81 -80 26 78 -29 -30 -13 75 -51 -46 17 15 51 5 36 25 46 20 7 65 -110 73 32 -22 44 -128 106 -34 -36 -60 -75 -35 -3 -29 -20 24 73 62 -7 -7 44 51 109 -96 -8 12 -8 9 11 -44 12 63 -73 44 -7 29 21 -128 83 47 29 65 15 -34 41 9 -18 -47 -19 -67 63 2 -34 78 -79 34 11 110 -38 11 5 80 -38 -26 5 48 27 24 -40 -86 40 64 -21 11 -54 -24 34 32 -60 11 10 -124 24 -71 -41 116 -89 87 -38 60 7 26 -36 60 61 -25 83 47 82 -32 -44 -9 49 -44 33 -7 33 -25 123 -4 -42 37 -18 50 84 -66 2 -20 -3 -21 54 37 -17 12 59 55 29 -70 -39 30 51 27 4 -84 2 -2 -22 16 -128 114 -18 23 30 43 11 -66 59 1 6 -47 0 -75 13 -54 19 127 6 10 -25 61 76 -21 41 37 32 29 88 1 -53 6 -65 114 41 -4 19 38 20 40 38 33 -21 22 27 44 18 -7 27 124 14 -35 108 18 15 -23 60 -37 -77 -10 37 72 34 -1 40 29 6 -73 29 23 43 -42 23 87 30 67 15 35 42 49 24 12 -12 -7 66 -46 60 65 -58 -1 46 -2 -23 33 43 85 60 -23 47 7 87 -25 -22 -90 -23 -82 10 6 -29 -104 -33 -82 15 10 6 34 -6 -45 14 2 -51 39 -1 -9 -28 -7 -45 8 -83 -65 -43 24 -7 44 54 -48 -20 16 -67 -73 1 29 -40 18 102 127 0 35 56 80 45 22 36 37 34 -19 7 -7 -39 -19 1 28 -81 -10 56 92 13 15 11 16 -45 33 87 -126 100 20 -127 -43 -42 83 21 -28 -8 -4 -119 -20 -42 10 50 -75 -6 9 43 -9 20 -69 -70 -127 30 -86 56 16 91 36 -67 -49 1 -37 35 -45 17 -88 -91 -5 -40 -34 31 -17 -6 -36 -15 -54 66 45 3 -92 -8 87 5 -13 -16 40 -61 38 42 -34 -76 12 -22 -30 -27 -10 -79 -6 -23 -21 -65 42 -40 -47 -48 -3 -24 -98 -59 -90 -68 -2 -37 -64 -94 -19 127 -10 6 9 -16 17 -9 -3 77 -26 -42 -36 0 -26 10 62 103 37 -9 -45 26 34 21 -78 -18 -42 50 -5 3 17 40 0 116 74 -25 15 -127 -1 11 25 -30 -35 -112 -17 21 34 37 13 115 -8 -16 127 -16 70 12 9 -11 35 -57 32 -70 40 42 22 30 38 -3 67 121 -44 -2 89 -12 90 10 97 30 9 -43 -10 17 36 20 -27 -52 -12 -47 -32 112 -1 -81 -2 -46 -91 -61 19 -126 55 23 13 -51 55 35 41 -29 -25 67 -30 17 -5 -18 -16 -42 -33 -21 -13 -10 13 21 -34 6 37 -37 115 -80 -93 34 -41 -54 -6 0 34 47 -43 4 33 -45 -68 30 -8 -45 30 -46 -90 7 -66 -40 -37 32 20 0 9 38 -6 36 -39 18 56 66 28 -29 -40 19 -9 -13 -13 -63 71 11 8 38 -57 -14 43 -15 14 2 -21 13 -59 -37 -82 -6 -118 13 -20 24 19 -13 -10 -55 4 36 -111 47 78 35 11 -42 34 17 -11 86 32 126 10 14 -71 -28 44 125 -29 50 -7 90 37 10 -65 40 -77 92 1 77 38 80 16 -98 -117 63 -28 -84 -48 53 45 -40 -47 -59 -112 127 80 26 35 -101 97 -41 47 -9 -2 -18 -51 -124 123 -77 -12 23 42 51 51 124 -35 -67 115 -81 25 31 -30 -6 -43 53 35 50 -59 30 48 45 -35 -123 -5 -29 -69 -107 74 4 -25 53 127 -51 -43 11 -26 -55 -61 73 -41 20 44 46 18 65 28 -8 -21 5 -27 -62 80 18 -1 -38 7 83 71 -16 -6 -8 93 13 0 7 79 12 41 107 100 3 -45 88 -19 9 -114 -11 28 -19 -79 -35 41 74 -8 -37 127 43 -66 2 -61 -72 -46 120 -47 -100 -98 -6 30 -37 -4 57 17 -26 14 -30 21 -56 -1 59 9 24 29 37 123 16 19 50 -77 -78 -12 46 -61 27 21 -46 29 -19 -28 75 71 65 -34 -46 20 -103 84 -10 30 -61 -18 -20 12 82 51 66 64 11 -40 -86 -115 -110 -7 1 100 83 -57 -89 -78 -4 -51 -95 28 51 84 -38 -120 102 126 -63 -16 99 -85 67 125 -71 -35 -120 18 -36 44 -4 -37 -20 53 -73 33 105 -101 -3 89 2 -20 100 -28 77 -108 -15 71 0 0 -18 125 62 -91 23 35 26 27 59 36 87 18 -57 91 -32 71 -52 -60 -37 -60 -81 -22 -27 6 21 -41 -69 -37 -10 49 -37 -127 -115 -54 69 -96 -93 20 89 103 -14 32 123 61 54 52 121 -4 -26 -54 -116 -45 23 30 -57 19 66 11 -42 88 -88 -26 -16 -2 50 -86 12 -85 55 1 -5 -23 -50 13 16 -8 81 -12 22 61 47 -22 15 -8 -1 94 90 -7 67 4 121 33 -124 -23 9 -101 15 -108 19 -126 -36 -30 -5 -84 -17 39 -44 2 -14 -46 125 10 -51 -21 -22 36 -29 75 -84 74 -58 -59 -43 -48 104 -21 44 -69 6 -31 16 -79 -27 -21 -128 41 -128 24 79 -9 -125 -65 -123 -52 -10 8 -64 -53 64 12 40 -94 -126 16 88 32 30 -46 -29 -12 33 -14 18 -33 -52 -74 -128 -34 1 52 116 42 10 -70 51 -48 6 -55 18 -44 38 49 23 68 55 -21 -122 -66 -41 14 -84 -50 -8 -126 -9 111 8 -127 60 22 -63 -51 -25 -25 -87 -27 6 52 -43 -30 20 20 5 -17 -18 -31 93 1 -33 37 12 -35 -54 127 20 53 22 -16 24 -128 104 47 -126 -43 -78 -61 102 -127 -128 1 -52 23 24 -82 57 -17 -73 47 -127 -116 86 5 96 127 -12 21 106 62 85 25 127 27 53 -50 -27 -3 -20 -63 -126 65 45 94 -28 -30 125 73 -9 81 125 -28 62 72 71 126 123 -79 119 51 -18 56 -15 -28 33 -110 9 -106 -73 11 -102 25 78 93 114 -126 106 4 53 77 -23 -33 -57 69 52 54 112 95 72 109 58 94 61 -49 123 67 -49 -9 -68 -27 13 -45 66 49 -44 14 23 -44 -51 -11 -25 30 59 -54 -22 -3 8 50 -64 73 48 52 39 92 -33 -72 35 -46 53 -59 127 -3 68 91 -61 -85 -43 -94 71 -45 38 -77 4 118 126 -91 81 -23 121 121 8 37 -121 12 15 60 77 -3 64 2 -12 8 -100 -117 -51 14 33 -121 33 40 -64 -18 24 37 122 -4 -2 12 1 -55 40 -27 30 -15 48 60 -20 4 40 21 24 13 -43 -47 17 -51 27 -83 14 104 48 -42 59 47 -17 -2 -21 -58 38 -44 -37 7 -5 -28 -31 -7 -90 -52 -110 -26 98 -5 -17 -37 81 -60 54 48 -34 23 -29 50 35 -58 -7 76 45 13 -64 13 9 19 -23 -2 33 55 -75 -39 -9 -37 48 -39 31 48 60 -16 -60 -25 -26 21 -51 3 49 -22 32 67 -15 -20 -17 -82 -20 -72 33 43 -11 32 0 12 3 -12 25 -43 27 -31 -27 -70 -5 46 -24 -48 10 75 -8 60 -62 -36 -28 -10 -27 -56 -49 -28 -5 38 37 108 26 -55 -3 -34 -28 -43 61 53 59 77 127 24 -10 -49 65 20 -72 -22 30 -22 -109 -96 12 127 -13 64 -56 43 -102 27 3 78 -29 -19 85 4 -56 13 -12 -17 -10 -30 16 45 -44 -92 -95 8 -43 -51 88 -46 -22 -33 16 90 81 -110 -14 33 23 -43 19 14 -29 81 38 12 -21 -16 39 -32 -19 21 49 71 35 -70 -19 -81 -49 11 103 -17 -79 -45 -27 -55 -80 -19 -65 -58 27 5 -3 -28 25 -5 -61 -65 -85 46 -8 10 40 -14 25 -26 45 -116 -127 38 -46 -83 -43 51 94 -18 13 20 -36 73 -34 -37 -21 37 53 -66 -18 -49 -3 -72 49 -34 39 -25 33 47 -71 15 -118 -33 0 -15 105 30 -5 -25 -15 43 72 106 57 -17 -31 -56 94 -52 25 41 -111 3 50 -44 13 -51 -48 14 -97 17 9 8 -5 -46 -8 9 -11 -17 -34 -20 39 30 52 -52 -6 -1 45 37 78 -24 -23 28 -49 -76 0 32 -73 52 23 -52 -12 22 85 -18 -68 91 52 5 10 92 -2 3 -41 75 -53 17 28 41 -33 -19 -3 -49 86 32 -81 -15 -35 67 25 28 -60 14 12 -12 -22 64 15 -38 32 -100 -13 39 54 36 12 -50 21 2 103 5 -38 -6 -16 -38 10 75 -11 -54 -128 -7 -12 7 25 -44 37 -98 -22 4 -25 14 4 -47 -127 -6 71 16 -9 8 -47 -26 -48 77 45 -11 54 -3 -42 112 32 -31 -67 -31 -64 5 45 -116 11 5 10 60 -3 24 29 -59 -39 -23 42 -68 16 -42 -48 -121 -3 94 12 -40 -18 -1 40 17 73 -12 -23 -16 23 16 -75 -27 -45 3 63 -72 53 -48 30 59 5 27 -49 -20 -29 5 24 22 -86 -10 -92 26 46 8 -40 3 -67 -23 46 -31 44 53 -17 56 58 83 -7 53 34 -11 -79 -29 58 22 -89 -76 -19 49 14 24 45 20 -35 19 -26 -59 49 50 -38 122 69 64 91 9 28 -89 2 49 -14 2 28 -37 -30 3 41 7 19 11 -25 -66 -7 11 39 14 93 43 -39 65 -34 4 -106 -15 8 1 77 -34 31 2 23 19 1 3 -9 50 -16 -82 -100 1 -58 -41 6 5 -48 27 16 -126 65 -33 -43 72 37 8 45 13 -31 37 71 -27 52 -96 58 -63 13 8 20 47 32 -34 -34 -53 -19 -34 4 -11 -25 96 42 -24 -48 19 105 5 -42 19 -9 24 51 -39 -98 10 40 -17 39 15 11 30 0 82 77 0 55 -18 -60 -17 -44 20 -44 8 -53 8 20 30 104 54 43 -47 30 -15 -81 -7 60 -20 -33 -34 27 -41 -22 54 32 21 -21 -78 -102 34 -33 -26 -26 -5 99 25 6 19 88 -36 -96 -24 70 -27 19 4 11 -36 64 -84 -21 -98 83 -38 -57 -3 -124 4 -127 3 8 -13 -37 89 -17 -113 99 23 -7 27 -13 -73 47 -121 -24 23 76 122 64 -27 -51 18 10 63 32 11 57 -46 22 36 70 -6 46 23 77 17 46 -2 89 3 -88 42 34 50 -18 -51 40 127 46 -21 18 -12 -110 1 47 -32 65 -96 40 63 11 61 30 -65 -70 -40 15 -20 23 -2 -61 -39 6 22 -128 -18 42 12 -84 -34 64 -25 32 -41 35 13 9 -117 24 28 -23 26 -83 52 -20 83 60 24 -36 50 -9 -37 76 65 111 -85 93 7 53 -15 -127 -25 -74 48 84 120 -11 58 -97 -33 -82 -125 40 -11 10 72 -8 13 -122 52 12 -74 93 34 6 60 -6 3 34 29 -120 -6 -21 -10 31 -46 -107 -61 -1 -15 8 -123 26 5 -50 -101 -104 -46 -18 -63 -49 16 25 -24 17 69 46 18 97 51 72 -17 -41 17 36 -10 39 -22 43 -81 -44 -9 72 -21 -56 -4 34 44 -3 31 67 -49 -31 127 33 -45 -12 -25 53 -27 -40 -25 89 15 -61 73 -52 4 26 -19 19 -30 -37 15 54 25 22 -3 8 -70 82 11 -19 -37 -36 44 19 27 18 53 6 37 41 64 59 23 72 -36 -53 -17 -49 16 -3 -30 -2 -119 29 -64 -27 11 7 -24 14 39 -6 27 76 -2 -93 -52 -21 94 51 -21 -27 68 -22 27 9 34 8 -63 55 -81 80 -23 17 54 -127 78 -73 -98 26 111 36 -18 0 31 104 -39 17 69 -37 -6 16 10 11 -15 25 -43 28 21 -25 -78 -106 7 -14 -46 46 38 -22 -18 103 9 -51 -64 -79 -96 -115 71 38 30 15 -15 107 -16 40 29 5 11 72 -106 29 57 0 -60 -128 17 -125 -73 102 2 59 -48 -116 16 32 13 6 37 72 -1 22 21 8 20 42 -26 -39 -21 -21 56 -43 38 89 21 -56 89 125 4 -48 -13 -106 -75 -125 -123 68 -103 -47 -22 11 -45 -37 -97 -17 24 -16 28 -54 14 58 -18 -17 -62 60 34 12 80 -125 -70 -78 -35 -43 -23 33 -115 -72 -62 -103 -128 58 2 -8 -5 -63 -1 -117 -82 -7 -37 -44 -70 107 62 37 -1 -59 39 -114 16 -20 17 15 -92 -121 -94 123 -56 -112 -60 -52 -65 -78 -36 -96 -59 9 16 -33 56 -1 -47 -7 127 75 -44 23 58 -32 -69 25 33 0 -35 -1 -23 49 -53 -92 -1 1 -68 2 33 10 -32 18 -59 -13 -48 -22 -26 -29 44 -69 40 -41 -43 17 -9 12 -47 -53 -23 -22 -18 -10 -1 -18 13 -1 13 46 -22 55 19 12 25 2 -9 -21 29 -31 -39 -64 -3 -11 -53 1 -14 28 0 -44 -76 10 -39 11 -85 -23 -25 -41 -19 -6 -13 -18 -44 9 3 -4 -9 -16 -59 -24 49 -34 -24 92 48 -46 20 -36 40 10 25 55 24 -19 67 33 73 -8 -33 -14 15 -1 127 -82 -9 -16 -14 -35 20 70 -58 111 -67 29 -86 -62 -43 72 60 56 23 -16 -26 127 50 -33 9 62 -58 -84 -23 -85 27 -14 -119 -17 -30 16 -74 -9 34 -39 -43 -79 -21 81 21 2 -73 1 -47 -101 -109 3 50 127 -39 107 71 -46 -42 51 -6 -87 51 -70 -29 16 -103 -65 84 -45 16 81 42 25 2 95 -3 -69 100 -7 49 100 -5 20 17 79 51 92 -113 -12 88 32 116 -27 -73 -14 -23 127 127 12 17 39 -69 -106 -14 -59 -73 121 -29 -29 12 127 -99 -103 13 -9 28 30 31 6 108 39 83 50 43 -5 92 83 25 -41 -119 26 26 -64 24 -40 57 -2 -48 -96 -10 100 27 39 -23 -22 -58 -52 28 50 50 26 -48 -124 36 -101 43 -117 11 38 22 -88 85 -55 10 -39 101 127 -95 34 57 90 91 12 -42 14 69 118 57 -57 94 -95 13 -73 29 -49 -24 112 121 -43 -34 100 26 -42 -30 -5 -106 79 -41 -25 61 -20 -81 29 114 50 27 -17 110 89 -49 -65 -79 -7 -28 -84 -25 36 127 -5 61 28 52 48 -37 -19 117 -35 22 69 94 -102 -33 29 -38 -1 111 -61 8 -50 -41 -46 65 86 -74 50 73 -70 87 62 -37 -109 -27 -4 113 -128 -47 42 37 53 42 9 -2 -6 -56 9 -8 16 -10 74 31 17 94 95 -14 -121 37 -37 41 83 24 -126 -29 6 -29 -33 23 -20 -103 -22 -59 12 -94 -18 -31 1 -44 23 127 -25 44 105 56 59 33 -42 120 21 69 66 8 -38 -27 34 94 8 121 -90 1 15 2 -21 -59 -35 -22 65 -6 19 -66 -70 56 -51 96 35 47 30 -11 -19 -110 -30 33 79 125 -120 -15 -87 26 -127 70 -34 -21 53 -25 -20 -71 80 20 77 108 5 46 22 -38 -60 18 119 -22 58 71 -117 11 -126 -122 -6 -3 32 -48 14 23 55 34 -12 -126 29 -19 48 -82 74 12 78 52 -18 5 -28 12 127 83 -114 -96 6 44 127 -7 61 13 28 -45 -44 17 -57 -96 6 11 127 109 127 -38 -11 -46 10 -119 127 -121 41 14 -82 8 -96 -54 33 -18 33 61 16 -22 10 127 102 -37 -86 -16 -94 118 -13 42 -70 12 39 105 105 29 -31 97 -63 -20 -15 -38 119 -111 33 -74 36 -87 21 7 18 118 -28 3 -25 -7 -124 -94 -54 76 67 91 -24 -22 -119 75 -60 -19 127 14 99 113 -127 83 63 28 83 -68 114 -63 -77 -123 32 -94 -57 99 116 87 70 -3 -23 127 2 51 77 20 -33 27 116 82 74 -119 -35 70 116 54 76 61 -116 -2 -57 -27 15 25 -43 -18 -18 95 5 84 -14 -127 127 -119 -66 -97 36 109 57 -21 28 -120 -41 -8 45 44 52 -28 18 26 46 31 -123 -25 29 69 120 -4 -105 97 121 -23 14 -14 -7 3 56 -58 115 -36 18 -9 -126 44 -90 58 53 2 14 -18 29 -120 26 127 -8 1 -1 -128 -124 -128 -128 -44 34 27 5 -26 -17 -2 -64 -34 -125 24 125 98 -11 -44 -43 121 79 30 55 -93 99 127 -85 -72 -60 23 -78 -95 -54 -86 -37 127 -76 28 126 -126 127 127 11 -105 -76 47 -60 58 -25 19 32 6 74 -50 -12 -123 -112 -13 -43 -42 -20 -42 -69 3 -35 127 87 -90 -74 -2 -107 16 49 -70 122 46 29 3 -94 16 27 -81 -1 -26 -120 99 8 99 100 14 120 52 -94 -24 -64 14 121 5 -26 -113 61 3 48 -89 -120 -36 -41 17 7 50 110 32 -68 -117 -16 -79 63 43 -49 61 -33 93 -80 -61 30 26 -55 78 114 126 -112 75 87 -20 -50 -46 -119 -34 -127 19 -99 63 -52 36 122 -47 -47 26 -64 104 -39 -84 116 -67 16 -31 32 -52 3 126 4 15 -116 31 -20 -24 -12 4 -30 -23 90 127 5 37 -92 32 -56 -29 127 -10 24 -77 -36 50 91 -25 121 88 59 41 55 -53 -68 43 46 105 92 2 25 88 -86 35 14 12 56 -122 -84 58 57 -16 -52 -35 26 23 -2 -6 -71 -81 8 -81 -16 -82 127 -26 -13 18 95 -115 1 117 -13 -50 8 -55 -126 2 -69 15 79 18 36 -85 -113 -81 -75 81 85 -39 15 77 70 24 -77 -11 113 127 -74 -66 -40 -74 -11 -76 34 -104 -11 66 59 14 -3 39 20 87 -71 0 41 -39 124 28 34 -124 53 98 107 -27 -31 -23 69 0 -42 11 -36 24 -15 -55 11 23 -116 -51 -29 28 12 31 90 53 -53 18 -73 -40 19 -64 -35 -85 -77 40 -47 -7 52 -26 -4 31 -22 29 -37 67 19 -6 -38 2 -2 -45 -49 -28 -125 63 38 91 94 -52 26 71 1 121 29 18 8 -23 -33 -44 -8 104 -76 119 -28 -22 -36 -3 -49 -68 74 85 -59 -27 116 -75 -21 16 -3 -41 -73 7 -15 -39 -27 36 -12 -75 -43 -60 -66 34 127 32 -22 -37 -70 30 49 15 -17 -104 -27 -41 -94 -24 64 -50 -75 -126 -100 -19 -22 -20 41 30 4 10 -6 42 -2 2 -32 65 -41 -81 -65 -16 55 -115 78 -69 -16 -40 -16 62 9 26 113 70 -73 40 63 38 -71 -59 15 85 12 -24 -86 -24 -128 -15 10 -6 -75 -36 -38 -29 6 -4 -25 -13 -59 -49 -33 -68 -40 -64 -47 -43 27 -23 -18 39 9 43 -9 58 -49 -2 119 -75 -90 -76 -50 3 -37 -15 -4 -65 -17 -64 4 32 -113 -55 58 -127 35 -53 -57 -12 45 30 31 13 39 39 -39 48 -27 -34 -5 6 -48 21 13 32 -53 37 0 10 -43 28 -49 34 -22 -127 16 -16 -83 52 95 39 95 -21 27 -39 -1 3 -8 28 48 -16 -54 115 -43 58 -45 -94 8 -61 49 -105 -48 28 67 -49 28 -24 25 0 25 -21 -109 -85 -52 -86 -37 -13 7 -22 -65 97 -78 9 17 -70 -11 -15 -43 -1 60 7 -73 12 -82 -12 -30 66 69 22 -52 2 -33 41 12 57 -52 -31 -6 39 27 -45 -27 -15 -15 29 54 9 11 -24 0 26 43 -86 -38 20 -87 -1 -11 70 100 -10 30 -28 1 11 -11 -41 50 25 -17 61 17 12 -70 29 21 -47 -23 -47 9 -39 -34 -14 -19 -117 -15 -41 49 72 -61 45 48 12 -6 28 -43 -28 40 39 -14 14 34 -46 -10 20 -6 14 32 -29 54 -68 -55 31 -127 115 -58 -2 13 71 -80 -53 -28 -45 -121 -119 -126 81 -101 112 17 51 61 -93 -27 73 -90 20 102 -10 -30 -63 119 -105 17 -73 11 -31 -94 17 19 -66 -127 -117 96 105 -29 -62 0 -58 119 36 52 81 -72 1 -19 -30 -72 -28 125 -109 86 -21 -16 53 55 49 -126 -69 -59 -67 14 18 -127 -128 52 -38 94 -5 -117 -89 -16 -26 -60 -121 -127 62 -81 48 -61 31 -3 -15 29 -3 50 7 -31 -91 53 19 56 -6 71 -27 -62 -81 -37 -1 7 -15 20 36 19 -35 -40 44 21 16 61 -15 44 54 42 -19 -2 -27 -6 13 -119 -65 41 101 16 12 42 25 -20 60 105 -50 8 57 33 75 -116 -4 11 105 22 13 -22 63 -10 67 -20 62 56 5 -16 -28 9 43 40 5 -43 -104 83 28 -1 -58 -32 -42 -44 20 54 -16 60 -6 -30 -37 0 48 -62 107 46 16 9 54 34 0 31 48 23 73 -61 -66 53 -6 -25 -40 119 97 -25 64 124 8 102 53 51 -119 67 31 44 -70 -4 -52 18 -51 106 42 -112 -30 1 52 60 -9 21 123 -28 54 21 26 41 -118 16 -128 30 -67 -18 31 -82 4 -59 29 75 -47 -89 -102 8 16 -110 -86 -72 21 -26 48 103 -46 -29 56 112 47 86 -127 -58 -124 -77 27 17 -33 -33 -30 93 59 -60 -128 -69 78 40 127 25 -32 -17 93 -11 47 -123 -77 -8 -76 7 10 -123 -33 -71 -4 5 -42 55 31 -7 -124 87 36 8 10 12 17 -11 -87 -2 -126 15 -1 -10 -60 -72 -106 17 49 -13 -92 -56 -8 -73 -4 8 72 87 42 -69 -46 -14 13 87 -50 -90 -24 -89 -39 -123 52 81 77 -36 -106 11 121 -122 -63 -17 -45 -73 13 -95 -65 122 -31 105 -14 68 -52 -56 -29 35 -5 -47 -4 -16 -127 -124 126 1 59 -49 41 -3 -3 2 34 4 9 0 -31 51 -74 -96 89 -11 -73 -49 81 43 6 -48 -90 9 17 -54 79 -56 108 -69 1 -50 12 19 -32 31 -37 -52 -9 -35 -54 -31 -128 -36 85 -7 31 36 -2 28 -10 39 -12 18 107 31 127 -109 89 -85 29 -62 -33 -47 70 -48 6 5 -99 -14 31 27 -71 62 29 12 7 43 -51 20 -52 -14 13 43 16 -113 -75 13 -58 -37 48 96 -97 -21 19 -12 21 -6 -24 47 102 39 95 85 -41 -8 -28 97 18 8 -21 127 -100 46 117 55 19 23 -79 21 52 -21 53 37 -26 41 -9 70 -15 39 -53 -5 11 -19 -31 -30 -14 -83 -70 -127 12 98 73 -8 -60 73 -71 59 15 40 -5 -3 19 -66 -51 -33 -23 -32 20 8 4 -36 -66 46 26 -70 35 -4 -8 -36 82 -23 82 -21 -57 62 45 54 3 15 25 88 54 -42 -3 -43 -17 68 25 -32 47 69 3 -42 -76 13 -17 77 -123 6 -34 -84 -74 53 28 -13 101 70 60 15 124 -11 23 34 25 1 -2 96 -45 -58 30 -20 -43 -12 17 -36 -45 75 95 -56 61 -122 7 33 -34 -20 -124 -47 97 -87 -45 8 -87 -33 -15 -33 13 7 -25 -120 26 -24 -12 33 40 -22 -11 -17 77 5 107 124 -33 -12 -111 -121 24 59 13 40 -40 -80 -14 -16 -29 -71 29 -100 14 -23 29 -116 -8 -96 64 11 -105 12 -18 -33 59 -23 11 -49 -73 96 16 15 53 25 -18 -100 -45 57 16 -47 -19 11 -31 48 48 79 -54 -6 21 58 -19 -66 43 -56 16 7 93 -75 61 7 6 25 -53 18 -1 -98 -107 103 62 76 -78 36 49 15 75 39 -30 88 59 69 -11 -19 23 -61 46 -9 -21 2 66 8 -18 -83 20 33 -92 105 -67 -27 -24 2 66 4 13 39 51 40 -10 112 50 34 -79 -12 -52 55 78 40 -54 -48 -2 -61 -11 -41 90 37 87 58 18 -18 -36 4 26 -60 78 79 22 62 -41 21 21 45 -71 127 -27 106 -28 127 -78 -125 -34 -47 -52 -26 19 -6 -35 123 73 -43 -122 1 65 -28 0 -26 19 27 69 -85 -25 87 -6 -74 -3 31 -118 35 -45 -18 37 4 -17 -22 -27 75 -90 -44 -30 27 -77 102 -76 -30 -28 69 99 41 54 22 77 -4 -35 51 -43 -73 31 24 17 52 -27 39 62 -57 11 -87 69 0 -32 60 -40 -102 79 -120 -100 -28 -66 -7 -25 -12 8 -3 -24 5 -19 -65 7 -20 -51 5 -107 74 43 -18 52 -103 -15 -114 -46 125 61 -1 -69 -39 -24 -88 42 -110 -28 -34 55 -125 -121 107 -45 -41 127 5 3 22 -38 103 -86 -38 -68 57 -121 1 48 -105 -35 38 -16 -114 -63 20 -13 -123 -14 48 -65 50 -53 -3 1 -22 -24 -66 10 22 -16 -16 -38 35 -15 -5 -59 126 -20 -120 -40 -13 13 -28 -13 -14 20 120 55 120 -19 105 -17 15 74 -16 -114 16 72 -22 -19 -1 -47 -88 -74 30 -61 -19 -66 116 74 -46 -30 -45 57 26 41 25 -30 -102 -64 -7 -12 -5 -46 33 -81 -41 -70 -51 -19 -19 -35 45 -6 26 14 -1 31 -60 -17 -63 16 -48 -31 31 16 5 -6 19 -27 -27 -11 -51 64 19 -54 16 -46 27 53 49 58 -98 6 -27 65 37 -33 -15 -19 -45 78 -19 -57 12 -33 -63 119 56 -6 0 63 4 51 93 15 29 -41 104 -53 -54 91 28 -83 -77 5 -53 -69 -11 -65 -45 62 -51 11 72 -32 -23 28 -23 13 -23 -72 41 -41 -22 -27 49 56 -12 -31 69 -31 -20 -2 -67 -72 -114 -48 -58 51 -13 16 -46 -17 -74 -5 -11 13 68 -17 13 127 -22 15 2 45 42 67 -108 16 49 -21 -7 5 12 -52 -18 -32 -31 44 57 80 58 -38 -14 -8 -55 -108 -25 -127 13 19 -17 -25 -16 126 20 73 -19 98 22 47 16 34 49 57 -53 126 64 -118 -1 105 39 -5 88 -94 -1 118 83 123 -60 -125 -56 38 109 -4 103 -77 22 112 -87 -127 -9 22 -10 34 -23 79 -50 10 -6 46 47 59 3 80 -108 46 0 31 -121 -6 -109 4 36 39 -12 34 29 76 -32 -3 -118 43 -35 3 31 -35 11 71 78 -9 64 -88 80 -79 14 79 -59 -62 -125 -4 -88 99 -92 -84 35 25 -4 79 -91 -5 -27 -33 -63 -59 7 -74 -21 32 53 59 18 -8 90 -42 -26 90 -17 -13 -20 -59 43 -68 -7 127 -35 91 -49 3 -127 69 126 -27 110 -5 58 33 127 -27 -19 -30 76 36 30 65 -52 -11 -65 -70 59 -19 29 -46 -75 46 -40 3 47 -16 -43 -18 -68 -109 -127 12 30 35 -71 -39 -27 -22 42 -54 -13 -50 125 60 33 -25 -103 -54 61 42 -18 -82 6 31 -86 29 23 32 107 102 102 10 79 -43 17 67 22 -69 -49 -45 39 -21 87 127 26 3 54 -19 82 -29 126 94 109 -102 22 11 27 127 -8 -76 -23 -16 55 -24 38 -57 39 -46 58 -51 -18 -93 -81 28 -117 -26 5 46 -27 -54 -47 45 127 99 11 -81 -10 -28 -107 -34 -3 44 -127 -9 -70 -67 74 -65 -5 -25 -8 -50 -94 23 -34 -29 49 -39 11 -13 77 40 77 -85 -81 -86 30 17 -89 -40 -22 -9 10 86 -57 -77 -99 11 -128 35 -25 -93 37 35 -41 -116 -17 15 -117 -3 13 11 -23 34 4 -27 99 -24 34 -126 -30 -31 56 -63 33 0 -100 -43 -65 32 103 -76 -14 -82 28 -47 29 10 -16 23 8 -21 -61 -1 -59 -34 27 -90 2 40 119 -65 19 12 8 7 -62 -57 -66 -17 -80 -61 6 -23 55 -74 -111 -32 51 -71 -121 57 -23 -128 -12 30 -31 -40 -67 91 -40 14 -37 -121 -13 -98 4 -117 12 -39 -85 30 -78 -36 22 32 -4 -36 45 -7 -54 -26 31 24 -62 -127 -36 75 -28 -61 -5 -61 -11 -82 -119 -8 -104 5 127 -20 -61 -19 -10 -34 118 -9 -9 -19 35 42 -46 -22 -85 43 -9 108 14 -40 126 -13 32 -58 77 -2 -128 38 -19 -80 -4 -57 127 5 -43 25 123 104 -21 57 3 -19 39 -4 43 35 113 32 -24 99 127 29 22 -22 -124 -128 10 -17 2 -68 -76 -12 65 7 42 -75 11 41 -7 -38 -48 -43 -84 98 6 -28 -11 -24 -55 -27 -3 80 -45 2 -17 -54 15 126 26 4 -10 54 12 14 -93 -95 49 15 63 -2 -1 -33 18 1 31 7 72 82 -33 -61 10 52 49 -17 -19 15 74 -32 63 -44 104 69 4 108 -2 -77 -66 -25 -121 15 22 -124 -38 40 -71 11 -38 -48 12 -27 -58 -54 -55 -15 -14 127 -87 14 -90 -110 -125 -112 123 50 -128 -76 -44 48 15 -111 53 62 -11 34 -100 21 -16 12 -52 -39 92 50 96 -52 97 26 126 28 41 29 38 57 -45 76 -81 -30 61 15 54 59 -35 43 -62 -94 -32 121 29 -78 52 73 45 -28 -44 -44 105 45 126 116 -38 -18 -1 -65 69 3 -55 63 66 2 31 54 61 95 -126 51 33 -36 116 50 77 -120 84 23 -7 -39 40 42 7 -4 33 53 103 34 11 -30 -51 -41 41 -13 2 4 -47 -42 93 40 83 9 -47 -46 -73 112 8 48 100 33 -36 -15 -2 -23 26 -37 39 126 32 47 7 -60 65 1 -82 -125 36 -26 -27 38 -36 -113 -18 5 95 17 -12 51 -11 -121 68 35 -86 -54 38 -32 60 107 -69 -16 34 57 71 -58 -101 -89 -42 42 -28 -26 -79 -67 -109 9 35 7 7 32 75 -58 28 127 -7 103 35 127 -64 -40 -68 -20 -9 51 -65 57 33 36 -75 -84 -15 -43 114 -71 31 -19 10 122 93 16 26 54 96 11 -20 -93 19 -113 -63 -34 -13 -49 29 10 26 100 -57 -10 -92 22 -35 34 48 127 -69 59 -40 14 -91 29 10 -12 -46 -127 8 127 125 74 24 -40 43 -53 84 -35 -125 -54 127 127 53 111 123 -22 -6 -127 59 -23 -74 4 -81 -52 -115 121 80 12 -27 37 14 125 20 19 -24 -34 -24 127 -27 41 71 -62 18 -61 11 19 1 -49 -59 -40 -127 -60 -127 127 2 122 57 -128 -128 75 -65 -85 -86 -24 0 42 -124 -32 87 -33 31 45 87 18 -34 100 -127 27 90 -109 -128 -73 -83 -69 119 -115 39 -1 93 -128 -80 -8 -33 -52 -58 -120 11 17 -49 -7 7 -36 -6 127 -58 -125 -27 51 8 -44 94 -20 -9 -77 17 77 32 -38 27 -25 -70 57 -44 29 -38 36 -33 -46 -16 -116 59 -83 -73 55 -75 -93 21 51 1 46 -3 -53 23 -84 63 -57 -52 60 88 -77 -31 -35 2 2 -36 29 -12 98 16 -96 -6 106 31 -78 -23 -11 46 -43 48 44 4 50 11 -38 22 -26 -96 79 -11 -29 -122 -19 -3 -9 27 -60 -60 -49 117 -102 7 -50 -71 8 -16 -47 14 99 -5 68 127 55 74 52 -28 41 -72 -65 28 -5 -101 41 12 -10 -93 -54 -90 49 9 12 -10 83 79 -125 -61 26 -54 -11 -29 -36 -57 -49 -36 -2 18 -32 98 4 -78 -10 -53 -51 -32 11 52 -88 42 127 23 62 30 -42 -84 111 -36 31 -42 126 54 -44 39 -29 8 -55 30 110 -49 -95 20 21 -112 -16 6 -7 2 75 44 -78 18 34 -8 41 54 21 -30 -96 33 61 34 76 7 -6 33 73 7 -26 -97 91 55 49 -27 25 45 69 3 55 -53 -45 62 52 -7 -34 -58 30 -59 17 40 3 12 23 11 39 -33 32 -58 125 71 -30 25 -52 68 10 28 34 10 -97 -14 -51 6 24 -37 -13 -41 -32 -4 -50 18 -3 9 10 -87 -16 69 -25 -82 -32 -25 -127 -73 -28 -13 -87 -49 93 -104 -64 -56 85 25 -9 -23 -79 -112 99 11 52 -7 -16 46 -42 34 -1 -9 -73 -98 -12 48 56 -60 -8 38 -120 -8 47 109 4 -18 -122 -17 -3 -43 -106 -58 -99 52 -39 -43 23 -44 -76 -82 15 -29 -98 -120 19 -56 -111 -23 -29 -48 -55 93 -12 -74 9 -93 -45 -31 -102 -91 0 -24 85 -12 60 -17 119 115 60 -85 -64 5 -74 -97 -32 71 -106 38 19 -67 -106 55 -67 -30 -66 39 -67 -65 15 -7 8 60 -10 -43 -53 -56 -56 6 -33 21 -14 14 -100 62 -46 11 55 -42 108 44 42 15 -11 3 28 87 48 77 -24 76 45 7 -18 18 -15 -12 -20 -11 -14 58 -31 70 50 -39 24 98 -13 -36 109 59 -50 -51 1 -27 -40 -33 39 -30 11 16 -21 -13 -19 -9 -44 -44 -61 -52 -60 109 12 -30 13 -27 7 18 -18 49 17 -52 14 -45 39 -83 22 35 -45 38 14 -24 7 -26 -39 79 -50 -68 -26 -5 35 -4 0 -23 -16 22 119 -30 36 -33 -8 44 0 -65 -5 -46 -68 24 48 42 -35 -53 -12 -24 62 8 15 -26 -38 -79 -29 -93 26 35 51 -12 -57 54 39 -17 -43 17 -38 40 95 63 13 10 -126 -21 1 -25 -15 23 11 30 -7 56 -80 53 24 48 69 50 66 6 13 1 46 55 70 -26 11 23 97 -39 35 21 -72 -88 28 -34 -24 55 17 -15 -23 -32 -9 16 -4 -10 41 122 125 76 121 -27 89 69 17 -18 102 -73 -30 51 103 -21 127 -102 49 33 41 -80 67 9 125 -97 -3 52 20 -74 -35 44 -66 63 -53 126 46 -48 -67 84 23 -29 -59 -54 62 47 31 26 20 -24 -5 125 62 39 39 96 11 12 122 62 -19 60 -113 4 79 1 100 62 21 -93 -112 12 39 -126 4 100 -103 29 98 -30 65 5 103 71 26 120 -128 -68 101 10 -69 -57 62 -51 -34 -45 -128 5 -18 -70 50 87 1 3 84 29 90 -126 55 100 21 -22 4 -124 -52 -17 -46 84 18 10 -44 -1 61 -81 -15 40 -35 95 -13 107 33 102 67 -92 -58 18 -99 32 -48 84 -52 10 12 -43 -57 47 -30 -79 48 -42 -115 -54 24 -55 -46 52 -76 -78 -96 -86 6 36 -80 -72 -32 -68 30 -39 76 -59 11 -10 21 79 -22 -30 -21 -33 -49 -45 -50 88 36 -43 97 108 66 -9 -44 55 41 -76 -5 19 -44 -56 -55 -82 90 -93 49 -31 -21 -127 -49 -72 -54 -126 -125 45 -27 -11 -26 -82 -28 -86 123 6 19 -5 -54 -21 59 127 -5 86 58 91 -127 -106 -70 43 32 -120 -32 -16 105 -75 127 -78 -21 127 -32 -63 102 49 10 4 -45 77 77 -58 124 97 14 56 20 44 8 -57 4 60 -32 -19 -8 -102 32 38 106 46 -14 29 24 49 -78 -30 -47 -42 38 64 -22 -26 -80 79 -11 32 21 24 -116 -77 67 46 67 -91 -76 -5 41 -36 -44 -121 116 20 73 81 -3 26 -68 38 42 52 118 -27 -47 -31 -30 49 76 45 58 39 49 -65 -36 -62 -23 106 -12 55 3 34 -41 -50 -123 -41 50 -10 -102 84 -41 107 93 18 -22 121 64 58 22 -96 13 114 -39 87 -19 113 124 -39 53 -41 -28 24 -4 -59 126 45 -25 -40 48 45 -81 70 53 -8 127 88 -64 51 14 76 -67 9 -93 8 24 -20 32 -38 68 -88 -16 -21 -75 -120 6 63 60 13 69 126 34 -64 -5 -18 37 -50 -117 1 -4 11 -84 -34 52 -24 8 -14 21 -11 -64 -46 -90 -79 -3 34 -33 -38 23 -1 -3 -57 -119 -20 48 -14 58 -79 52 -84 -40 47 88 103 -61 7 -36 -8 31 -27 -52 -18 -53 20 54 -68 -8 -32 -9 34 -57 -4 -118 -32 -49 11 42 -70 -64 -11 57 -12 -37 24 46 -56 -56 -101 54 121 -47 111 12 -49 -31 -44 56 -43 15 -45 62 -51 -17 -25 67 -46 -65 -8 37 -84 -15 -67 3 -61 83 9 18 59 -13 35 38 94 2 104 75 78 90 44 29 97 62 -93 -125 -19 -33 1 -113 -23 126 -26 31 -58 -27 56 -76 41 119 13 -88 52 66 -86 27 -64 21 7 -41 17 -23 92 -100 -94 26 -69 15 -49 -41 -78 -17 53 84 44 17 -36 24 -34 11 72 -24 -5 70 14 22 22 -31 8 -123 60 -88 -18 -33 -108 -65 -107 121 -69 43 -122 80 -115 -44 -120 82 5 -80 -46 -64 -30 -68 -54 123 -98 -124 -12 -90 -87 -74 122 -41 -92 -66 22 49 -86 -72 43 24 60 84 -73 -88 -2 -66 -22 -36 -40 -45 -86 56 -12 15 40 35 42 66 89 7 -54 -59 -23 -42 -125 -41 38 21 -71 -10 -15 -91 -127 -47 -79 7 -19 43 10 -95 34 57 59 -79 -51 -7 -36 -66 42 54 40 6 -3 39 57 -42 -123 23 -20 9 35 111 -34 29 -40 -83 -126 5 17 63 48 -43 46 65 -84 -77 80 1 57 -119 -31 -27 -58 -73 -14 41 -37 16 7 45 -60 96 4 28 6 -38 46 -53 2 20 -26 -2 -3 -41 -42 65 27 20 -112 -43 -48 -10 87 -104 67 45 -39 2 -39 5 -44 -9 77 -22 -2 52 -58 -87 25 -24 20 -12 -103 -28 -77 -54 -52 20 58 -34 4 16 -21 97 -4 17 93 5 7 -39 -20 -126 -88 46 -61 121 -18 -35 13 -51 54 2 -31 -32 -45 25 -101 -120 -58 43 -128 -118 23 -67 -83 79 -59 -47 41 30 108 60 17 3 -48 82 2 31 -124 -8 119 -87 40 -60 -2 -1 -71 -64 -77 37 9 113 -1 -6 69 -11 -86 35 28 -1 28 56 116 30 41 101 56 20 53 125 112 9 2 20 6 45 7 -32 -120 62 120 124 -108 60 67 -86 -59 -1 103 -47 -108 22 -10 29 105 60 -26 62 33 -48 -86 59 84 -54 -121 -46 -127 -31 28 -34 -81 16 -51 4 127 -66 -47 60 -46 -26 108 73 -118 -127 -87 -31 66 -19 46 42 40 -27 -71 -25 62 -77 52 -118 -1 82 -60 -69 61 -41 -80 -126 54 -40 32 -32 2 -5 -37 -33 -26 -35 77 76 -13 0 -11 -39 -3 -92 72 -127 13 17 -41 -27 20 -66 6 20 39 -21 -119 41 -41 -23 -3 10 3 -127 -23 25 -67 1 -15 -22 -7 15 24 4 -32 -34 10 44 70 40 -62 -58 -51 -86 -25 -118 70 5 9 -21 -93 -45 -70 5 -49 -56 49 -67 -27 -38 -123 22 -5 -59 35 45 -88 -10 20 33 9 -17 43 -38 19 59 -16 20 23 -13 9 30 -19 -74 -62 20 21 15 -66 80 29 -14 39 67 -8 -2 26 13 10 -13 -17 -26 -38 10 10 -9 -34 -57 -33 -59 40 -34 61 -63 52 -11 77 -17 37 34 60 -13 69 -35 -20 70 -109 -22 9 62 -31 -57 111 68 -38 23 -104 -59 -26 -3 41 -25 -42 -39 -86 -71 -8 -71 -87 -122 -65 9 127 -74 -16 -3 -25 59 15 -103 -17 5 11 -106 58 39 1 54 32 -29 102 -52 127 -20 8 -6 -14 44 52 14 -37 -73 -68 -1 -8 -44 17 -13 -43 -47 -12 -10 -66 -55 -66 -13 -10 39 -28 70 -7 21 -37 -115 -22 -5 -88 -72 -6 5 106 -14 46 6 -57 -128 -50 6 -17 40 81 -1 -65 -2 37 90 91 64 64 65 30 92 39 -49 61 -14 -39 14 127 49 -30 17 123 124 107 73 -24 -89 60 124 63 114 50 -17 81 28 39 -120 55 75 82 -30 64 -12 1 65 -32 51 38 82 -24 2 43 16 85 -1 26 -19 127 2 35 -35 50 58 -66 75 107 66 -9 38 -42 20 56 43 41 77 26 -20 10 -67 -3 15 -30 -22 7 97 51 39 -21 78 58 -52 -55 -9 5 110 4 54 93 119 -40 26 104 22 19 -50 18 14 -13 -78 41 -42 -27 -10 -56 1 -31 -14 -117 82 55 -62 -57 -60 -53 -14 0 116 68 -33 73 -89 -122 1 -24 21 -61 103 23 -109 45 29 -1 -47 -36 15 -17 -12 -40 18 -6 -70 -20 40 106 -22 28 -45 -20 35 -67 -15 -83 53 43 -2 -25 2 -39 15 0 45 51 52 -46 65 -99 -9 -82 15 -69 -19 -16 32 52 -36 64 -51 -37 16 22 23 6 16 17 -23 1 -24 34 127 1 57 22 -39 44 -49 120 -33 -70 -34 25 59 52 -23 -128 2 -4 17 -6 56 24 40 -16 20 -68 -20 -13 12 -69 126 60 4 85 1 -54 -49 -74 90 -28 17 4 -53 -81 -90 -123 26 33 53 26 -78 -12 84 36 8 -29 3 24 26 19 12 109 11 -14 68 1 127 -14 -28 21 -33 -34 -35 23 36 -70 52 97 21 20 -22 127 -60 -8 -35 -21 51 51 -4 123 -32 31 -127 73 -23 -90 72 116 -96 -111 28 73 20 41 127 33 -58 53 34 -30 -11 -12 -20 -47 -39 -38 74 -90 -128 -20 48 -6 26 47 37 27 -37 -89 -105 14 -22 -9 -52 -19 -5 -43 85 -72 -80 69 90 21 -26 57 -1 71 68 46 -41 -60 46 -125 91 -66 -40 -17 59 19 80 32 -12 -124 -7 21 -79 -117 -56 -3 123 -12 -45 -82 -59 15 -54 -109 104 -3 -78 -26 -35 127 -60 6 -20 -28 -66 46 -23 1 43 1 -86 -2 -25 -31 -45 -27 99 -20 -25 96 6 -7 22 35 54 -51 65 126 -39 -119 102 -38 -17 -10 -29 -83 94 34 23 -9 60 -23 -104 99 56 35 -89 18 44 -70 7 11 -5 107 0 68 106 10 -5 53 122 71 -1 4 -39 -119 -43 -29 -32 -12 14 6 -50 -60 -39 29 20 6 -51 26 -23 84 68 20 106 -127 127 58 -14 -126 -50 -52 49 34 -15 -43 -72 17 25 59 -61 -61 -110 19 -4 -21 6 -101 -39 -81 -12 125 100 43 -3 14 -128 44 -102 41 25 14 -21 48 60 44 -9 -2 -11 -121 21 56 0 -51 77 -2 -10 0 10 -36 -39 -15 -3 16 97 -83 102 -89 124 0 -25 24 -127 5 -60 -40 45 -20 13 -1 -90 -10 33 100 122 34 -128 -37 32 -56 -35 127 83 -19 40 28 -8 26 101 56 -6 2 -81 -22 -47 -26 47 -30 16 -43 27 28 16 45 -20 -74 -60 -40 -34 -25 -33 -40 -24 -49 49 75 -76 50 -34 15 -88 -120 48 -18 -21 -50 -23 -29 18 14 -31 6 -81 -100 124 -52 -125 -57 -87 -54 -81 -19 -124 106 28 83 30 -119 -46 76 -115 23 -45 -91 -18 73 16 -48 76 -32 10 7 39 -17 -126 61 103 66 14 -114 54 -18 33 20 -33 38 -125 -28 55 -74 74 -3 0 -30 -45 -88 59 62 127 -127 -17 -72 40 107 -72 10 -109 -117 -87 -123 -36 7 5 -43 -21 -125 -41 -57 54 -44 10 -19 18 -97 -121 -33 38 -37 -18 99 8 -48 -120 -72 46 -69 -44 -51 -124 18 -99 24 3 109 -87 -24 31 -125 -62 -9 61 -10 -28 -48 -78 -48 -15 10 -47 20 62 -122 83 -73 -34 77 -66 92 -100 61 18 16 37 122 -109 127 35 49 -12 -6 11 40 -8 77 0 -32 79 52 120 20 -89 -119 -73 69 -78 -69 12 1 -32 37 -109 61 -64 -27 16 -77 -7 -126 -36 -31 31 -83 -6 -16 61 18 -56 9 -33 -43 -70 88 48 11 81 13 27 -29 -95 -22 50 15 -68 -9 -65 -20 109 13 -6 -56 4 57 -44 20 39 15 97 29 30 -3 -58 30 -67 85 -45 -18 113 -31 -8 -25 27 -22 -10 17 -2 49 2 -50 82 -8 -29 109 72 -60 45 34 42 44 102 -68 0 41 -35 -70 -51 -26 36 -12 13 14 -27 66 87 -30 -18 48 1 -20 -10 0 60 89 -50 -41 14 -1 33 -44 46 27 47 127 -99 125 -17 -91 37 -41 127 120 73 64 30 2 16 -31 125 -105 -67 51 89 -33 23 61 8 -77 -122 37 39 -18 -20 27 56 14 118 44 8 39 -45 127 100 -84 30 97 -17 50 -39 127 118 -5 36 103 127 -9 -100 103 42 -5 -52 -36 38 -20 41 1 -70 28 51 25 127 17 -15 -88 -66 23 95 16 -105 -23 -54 47 -23 86 -7 124 -56 33 23 -64 -23 20 -47 1 42 -25 9 14 72 -69 -58 75 -53 -128 -37 -4 -23 -46 53 50 45 -65 -22 -67 29 -86 74 24 -56 57 5 7 -99 -32 -2 -27 -28 -25 7 -45 26 -25 -10 -31 -44 24 -50 1 -2 16 22 -98 -35 -47 1 20 -97 3 -31 -14 42 31 -18 -12 34 15 28 -82 -18 34 38 70 -4 3 -38 10 29 -28 73 16 -25 12 -18 92 -47 18 -3 41 22 -6 -60 -22 -25 -59 43 -1 -103 -82 -39 -17 40 3 48 13 0 -41 13 -77 48 -45 46 -25 19 -13 -16 -89 1 -32 -7 -25 -47 -43 -6 -51 32 12 80 52 -48 -9 -47 9 1 18 29 11 -45 -30 15 70 105 72 -28 -39 7 -13 -69 13 42 -6 -12 7 -14 -74 23 -73 -13 7 -22 10 8 -5 -5 -26 -26 -2 0 37 -125 -55 -30 35 67 43 -29 -25 -2 -46 -124 -4 22 -110 -11 79 -31 -15 -38 9 69 -27 -53 -34 5 95 11 -52 -25 -28 -4 -35 47 2 -29 39 -61 -98 -67 -122 -59 -8 65 -110 45 -63 -9 41 76 -48 26 -50 -47 13 112 -88 -26 -76 -32 -43 -2 64 2 -21 31 -20 -62 -42 -70 127 -98 48 -66 -77 7 13 -49 -31 -104 82 -58 -7 -56 65 86 7 -80 88 43 -9 41 -24 -64 -104 -34 24 -5 22 -67 -9 38 -126 -2 -64 -76 45 -24 -4 -72 -111 -29 53 -37 127 -36 54 -44 -12 1 -68 -14 -31 -74 -16 -39 -15 -12 127 46 -48 30 15 -46 -27 -76 -19 11 -44 92 66 99 22 -98 13 32 -96 22 7 -41 34 -56 -53 -42 8 -13 -12 25 -63 34 -21 86 -89 -43 -18 -31 6 97 53 18 28 21 8 17 -67 -61 9 56 -34 -37 73 -7 -38 9 32 19 36 0 39 4 25 -14 110 48 13 -4 10 121 32 54 -3 -38 -18 -12 33 -15 34 -1 27 6 -24 -88 27 67 -70 -15 -15 9 21 -84 -128 25 1 -5 -8 -14 -82 22 -26 37 -6 23 18 47 -21 -15 -31 -16 11 -34 -42 16 -77 47 -41 -27 -27 56 68 -99 14 -7 25 -66 5 8 -40 25 26 -89 11 62 55 123 15 -56 12 -69 -58 13 -61 28 -14 23 -12 42 -24 -50 -6 22 0 2 108 -6 -85 101 13 36 22 7 -20 -53 -26 -33 -66 -47 -7 -18 -44 -43 -20 1 -51 39 3 -73 4 -32 9 -26 63 -16 -75 1 3 35 82 -22 53 41 -69 85 -76 -47 -20 124 85 120 78 77 104 -58 29 -10 -58 -16 -127 -6 42 95 65 20 68 -30 57 -15 72 124 82 68 72 -22 -82 74 -54 13 -33 44 18 121 -15 88 108 45 84 -5 -46 71 29 -13 72 -69 -26 -4 -82 86 19 127 42 9 6 19 -9 24 -34 32 106 28 43 44 37 65 55 81 26 39 -81 28 81 -15 -4 31 -59 53 122 82 97 2 6 -47 -46 31 71 -26 -27 -19 -5 52 32 40 9 24 35 -8 -16 -14 -47 -35 49 66 -39 -92 127 38 16 -1 -97 -60 -39 -99 -69 -35 -49 3 24 70 3 3 -19 -60 -17 88 51 -58 45 -10 33 41 1 -50 86 32 -35 72 2 16 7 14 2 38 65 28 105 54 9 22 33 12 48 -21 -46 23 27 -18 10 -1 5 -9 22 -25 -56 -86 23 -18 43 -77 9 -54 23 -6 -39 37 -90 -78 -7 -27 -13 31 42 53 -124 3 -49 -28 68 93 25 -13 0 13 -5 -33 -55 18 -54 -2 13 -71 73 -79 2 -6 33 33 60 -77 -114 5 -42 60 34 38 5 -50 54 -29 -39 -109 54 -32 -19 -45 13 55 17 88 -60 70 74 57 -51 -42 -15 -84 24 -17 5 3 35 -34 -15 26 58 -94 23 -4 -56 2 -2 -122 1 -12 -37 -2 11 -24 112 15 67 -54 1 2 -49 89 4 4 -17 -39 -29 29 -27 -19 -38 -6 -35 -96 40 -2 49 -72 46 16 44 -106 1 -39 -65 96 0 -73 3 -59 -66 -73 26 -124 37 -30 84 -102 15 91 7 -61 -28 -13 29 101 -123 8 63 88 3 29 -21 72 -64 -118 -42 -16 -67 24 -24 -22 0 69 -70 100 -79 51 106 -76 101 107 56 60 -58 -44 -23 -2 90 -51 23 -116 -116 -125 60 -10 -100 -17 23 -3 -39 8 -13 26 -127 -23 -6 -42 2 -77 27 -81 -78 -14 -97 -9 -111 7 20 60 44 -106 69 125 3 46 81 61 -2 5 92 0 91 -32 -58 -6 56 -51 -39 -34 14 57 33 -23 6 99 -65 35 -31 23 -11 -128 -38 30 -33 52 -16 -55 -21 -13 49 -28 5 71 -2 -30 92 2 -78 24 -2 30 -7 -21 -33 -6 -61 13 77 22 123 -27 30 85 20 61 -8 23 42 65 -41 38 -25 1 80 -77 15 -47 34 59 125 -40 -105 2 21 9 38 -10 1 -2 -23 5 122 -3 -65 30 55 -20 96 46 -46 127 106 -16 -13 127 38 -31 49 33 83 -2 -65 59 56 -55 -48 -28 -36 -26 -3 -81 44 64 65 22 -67 -6 40 -125 1 -48 -56 -87 -87 26 -31 -57 23 21 48 -13 -45 59 -66 65 -63 -24 -15 30 -50 39 -127 32 -128 101 79 29 13 -68 -95 -32 -93 64 71 32 -44 111 40 71 -16 -32 117 53 -89 -13 12 -108 76 -80 -21 15 10 -116 -10 -72 92 40 15 -11 20 52 5 82 -68 -15 -19 -79 58 -32 -43 -120 -127 62 33 -9 -115 -93 -127 9 -101 112 -106 29 30 2 126 29 49 126 -125 -115 -62 -2 -7 28 -4 127 -126 3 29 8 -18 -128 116 -24 -110 66 8 -126 -64 -122 54 -65 -109 -33 106 32 48 -122 -10 -31 124 -62 -52 -106 -127 84 103 -127 127 -81 10 -3 -53 42 -62 -16 -41 -119 -80 18 -41 -14 28 -71 -10 79 98 -75 84 -31 -53 -128 -62 -19 33 108 62 -27 -51 24 -57 -16 81 5 -73 -12 19 -102 -118 -112 32 62 0 -82 118 17 10 18 119 -127 61 -125 22 62 -5 14 82 95 -70 -61 22 -10 -22 -86 -127 31 -19 -78 51 125 -33 1 -58 47 -26 74 -124 -82 -1 -102 3 44 -28 86 111 4 26 22 2 91 75 5 6 62 5 19 -89 21 14 -29 -62 50 -35 -72 -4 -79 -9 8 43 43 70 -1 -25 -85 25 72 24 17 6 -58 29 78 95 -101 24 17 18 -85 -102 116 -127 -26 -26 23 -3 61 33 -51 63 22 54 53 -121 29 78 -29 -67 71 106 -41 93 -94 -65 83 3 127 -63 107 -125 8 112 11 29 127 -116 -12 -124 -68 100 126 -34 80 -61 -108 -22 93 -33 -1 69 -68 44 -19 127 -11 -58 -117 -66 127 0 -118 -79 -125 -53 -72 -105 -18 -24 36 -11 -76 112 -65 -122 -31 51 10 27 -47 -127 49 127 57 -82 -13 34 -27 -3 91 62 9 -26 13 20 -34 40 35 -11 -20 57 -56 -88 73 -19 36 -52 -18 22 -39 56 47 23 82 57 38 26 69 -17 54 -48 15 68 -22 1 -10 39 113 -10 92 7 127 -57 16 -15 39 63 55 -33 9 44 -108 -14 101 108 -41 -51 -47 11 -30 -62 47 86 -36 -15 -41 -22 88 -32 -12 -70 -13 -8 -42 33 6 -12 -16 103 127 6 25 -103 7 65 -50 -18 -82 9 -60 26 127 22 69 73 42 83 23 55 -34 3 35 32 57 53 15 -108 95 -93 -85 12 -10 -13 -36 -35 19 6 4 22 40 33 73 89 -66 -1 28 15 99 16 43 -8 44 13 -113 13 -127 -110 -60 21 -17 31 94 42 6 -111 -37 76 76 29 29 -80 -42 -65 27 83 -5 69 38 37 62 -52 13 -34 29 71 -49 -4 44 -4 -53 65 -44 44 -46 98 48 108 1 -42 -45 -4 8 4 37 35 32 -41 -13 94 -59 -8 -83 -15 -26 11 -43 30 12 86 -3 106 12 62 -44 7 -68 -22 111 -27 -33 -9 -65 -95 91 -31 -23 21 -84 20 -14 65 22 15 2 -17 -23 19 0 -71 -66 -4 -72 -39 74 90 8 -17 5 32 -52 -80 91 35 15 -22 111 13 47 -73 -79 -33 4 28 -36 56 38 -2 -7 -70 76 56 63 11 47 -5 72 -72 -50 -5 -19 27 -7 -39 53 47 77 36 -38 67 -67 51 -30 -44 114 23 -2 -23 -37 -4 -81 20 -28 5 115 -83 -12 79 -4 6 50 26 67 61 70 76 23 61 -19 37 8 -92 42 -16 -20 -79 -63 105 32 -89 -29 -51 48 -63 -114 85 118 -53 -5 19 -68 -7 -38 27 82 29 42 78 22 -29 5 -9 -8 83 -45 19 54 51 -22 13 -50 -87 -51 44 -88 -37 -8 -34 53 -34 36 47 -1 20 49 -14 -93 56 52 28 15 15 43 65 15 -21 -3 14 -11 -50 64 34 -44 -10 -121 7 -82 37 24 7 -67 -96 -7 42 38 40 122 4 -5 -27 -33 35 29 -64 -59 38 -35 7 -19 -42 62 -49 -53 63 61 8 4 -33 -15 122 7 50 -40 26 27 40 -9 -43 76 24 -37 -61 20 101 74 -48 7 23 -31 82 40 22 28 -16 44 -51 -76 -73 40 -52 70 -2 67 -69 -19 -29 27 25 43 -1 7 33 -62 -50 -6 -12 9 -38 9 -14 24 -73 27 -47 49 -12 53 40 -7 100 -123 -50 10 -25 1 26 -37 -12 73 28 39 -20 -74 15 -18 -1 49 -25 -53 -24 -48 45 103 -18 36 20 19 -59 27 12 -3 -56 -22 -42 -108 101 -23 -48 44 -110 29 -30 34 120 95 -79 -33 44 81 46 -68 22 -73 69 18 -21 -40 -2 -2 -72 -8 -79 -21 18 -43 -28 -101 9 6 -20 37 13 39 61 46 60 -4 -2 50 15 -18 19 8 36 100 -27 2 -74 127 -36 57 -61 28 33 18 7 -87 -126 -79 -126 -95 35 36 -80 -85 25 -66 49 9 -113 44 -42 -17 84 -54 23 -15 72 -14 -14 -13 -61 -4 30 4 -23 68 14 -86 -91 17 -52 5 12 -5 -40 -15 14 -37 -42 -13 10 -28 -27 28 26 37 0 94 -127 1 34 -59 -78 55 -82 -60 -50 -86 -27 -91 -97 45 -6 -58 -1 -75 -80 -74 68 -27 37 29 65 -50 -6 26 12 -8 -11 17 -27 -12 -73 16 -58 -23 -76 21 -20 31 -71 30 -10 27 -10 -73 11 -32 -35 -27 19 5 38 2 -11 -10 -20 6 -27 26 -56 -20 61 -35 30 -8 -35 26 46 31 -16 -18 14 48 27 20 34 -9 79 -36 -22 -10 83 -6 -4 -11 17 -4 55 -18 -1 52 -26 60 -7 -11 1 -30 74 -6 -27 -4 -19 36 16 -10 13 4 -75 -4 -27 45 -37 -16 55 28 52 30 -30 -62 -50 45 -27 0 37 24 -13 -32 18 50 5 22 -12 -9 13 -3 23 -82 -33 11 62 33 -17 -84 19 -22 95 89 12 -15 -24 -25 75 118 -32 65 30 51 73 22 22 15 17 -74 -5 57 11 -50 -60 -33 -85 -32 -10 -28 -51 -51 -100 10 -21 43 37 63 -96 38 -14 30 48 -78 -17 -74 35 77 -51 11 -23 18 -61 24 -123 -49 66 -8 -15 -38 -18 -14 -16 125 -55 -12 33 -71 43 34 46 -12 -53 50 13 -7 33 -18 -6 14 35 42 -108 -35 72 -41 -21 -48 18 -5 71 24 66 4 125 -107 82 69 111 -56 16 127 95 -8 -69 2 -52 78 61 109 -6 123 -84 63 35 117 -24 46 -39 126 120 39 4 -31 -3 36 71 98 -33 92 50 -6 11 91 127 51 -76 -67 -48 -24 125 25 -19 36 35 103 105 30 72 92 4 -126 -48 -42 4 71 -125 -123 56 -25 14 84 -26 25 88 43 -8 86 -95 1 -39 -36 43 68 -45 6 -20 -41 -89 21 83 -15 70 56 22 10 9 -38 38 125 112 34 -5 126 -25 -37 82 -79 24 -36 6 76 127 -69 49 -3 -50 34 8 -88 -20 -114 -51 35 -10 24 -82 -33 13 42 10 72 -95 -21 46 -16 39 46 -51 31 -127 5 -32 -14 -3 72 -75 -20 40 -94 -37 -78 -65 -7 39 -43 2 -19 87 60 -57 -7 43 -124 25 24 -4 -26 54 -47 33 14 -8 11 41 -37 116 -34 -46 120 53 -61 -1 38 34 -72 -15 -112 107 -23 -26 30 0 -106 104 -28 -80 103 -96 11 -68 97 7 54 41 101 0 -84 -24 -92 -35 28 -115 -85 23 -125 -9 127 80 -34 -74 -27 -33 38 103 -16 -48 41 81 -29 127 -60 127 -45 -77 4 13 41 -20 -28 -22 -8 -52 22 17 -12 -21 -92 -31 127 -113 -45 100 29 71 7 -31 50 121 0 3 13 89 -124 39 -4 9 35 -118 -86 -123 -7 -11 109 -126 -90 -29 38 10 -120 -66 -34 22 -39 3 -68 124 13 -24 -64 11 -29 -121 -58 59 5 -42 116 76 -61 -128 -23 -104 19 8 62 -87 7 -54 -21 124 -8 54 40 -15 -7 68 110 84 -23 120 -110 59 18 81 49 50 13 105 4 67 84 10 -62 60 3 52 -53 49 53 -87 28 79 63 52 31 -33 9 111 -59 -66 -32 49 47 -99 34 -38 -29 84 4 19 28 -81 75 -61 -56 -9 43 -15 -9 2 61 23 114 39 65 76 51 16 -2 61 3 -31 127 55 24 -53 23 -126 -29 21 11 -15 -39 -26 -115 19 -67 -33 17 -40 49 -5 -22 -67 58 8 -91 -23 -72 124 47 17 -68 -50 -45 -43 -87 123 18 0 -57 112 -36 -12 121 -111 -30 41 -47 -10 103 27 -35 -94 -70 -15 -102 -18 -51 -50 58 -20 72 101 -42 2 7 -7 87 82 17 99 -94 -114 -32 4 9 -87 -11 -113 17 -28 20 -69 -28 -10 -51 88 66 -16 -47 -45 -78 -62 -34 -14 -100 -7 -27 27 -82 -4 -47 -93 -21 -86 -85 -27 5 95 20 -103 9 -84 -69 -14 24 -97 -4 -25 -14 -15 -111 -17 -30 6 13 -23 26 5 3 25 57 -42 44 -20 43 15 6 80 51 -36 11 76 115 -52 61 -41 -62 -45 -2 79 122 13 -37 -35 -3 -15 -1 22 65 -77 25 65 44 72 41 -32 22 25 -9 -64 -25 -20 -66 -37 -37 -47 65 -50 23 -3 -99 20 44 -8 -49 -61 -30 17 27 -5 17 23 35 -83 52 52 10 55 34 94 59 25 -17 69 -85 29 -19 7 20 25 20 12 -21 -14 30 -75 -77 -69 -88 38 -31 9 31 64 -10 -37 12 -18 -8 -33 -55 42 24 101 25 32 7 101 -27 -109 -42 96 64 -86 -117 76 -80 -77 14 49 81 -79 -54 -24 23 76 1 60 32 12 81 -41 20 -81 -118 9 -6 -113 -80 72 -6 18 15 5 -91 -114 -120 -118 14 -119 84 -20 16 -60 121 -112 -120 37 -48 -2 -120 -13 -102 93 -6 -55 -18 114 -74 -34 103 -23 -54 73 -19 17 64 1 67 118 -49 -26 -47 -23 26 24 43 118 17 103 120 46 12 106 -47 19 -47 9 -42 -20 13 22 111 15 19 -27 107 79 71 46 -77 90 7 34 86 72 18 -107 -15 -91 14 10 -20 -28 -56 33 123 40 55 -87 37 127 35 92 -122 4 -26 62 63 -75 -122 -113 -8 -9 -22 -16 -25 -94 19 -44 -60 45 -117 34 -54 -25 -13 58 -126 -15 -68 126 36 -15 6 -74 -65 3 -68 57 21 81 58 115 43 123 81 -9 -58 17 -64 58 21 -30 -108 -65 16 -128 -45 75 -119 -117 -97 -65 -40 -123 125 127 127 17 68 -16 10 126 30 120 -121 -18 127 80 14 -125 -45 40 -30 -128 -37 -57 -20 59 -117 -70 12 -58 63 -59 -11 -17 27 64 25 -38 9 123 -32 -125 14 -71 127 116 -25 44 124 -47 47 -22 31 -127 113 -112 -117 21 -33 -53 57 47 -95 -39 4 26 32 43 -21 -16 -99 -64 -7 18 -49 -110 -13 68 70 -16 48 -54 14 48 36 121 93 -78 -24 3 9 -37 -19 54 110 -72 -42 -65 6 -90 -56 41 -1 102 40 13 -10 -62 -17 44 -66 -98 79 -51 -16 41 -28 -107 27 77 -1 -128 -120 -6 58 -119 30 -2 25 -68 3 -78 123 -51 54 -70 17 90 -17 -1 -74 39 -106 -42 23 76 -37 31 9 47 -95 -41 34 4 11 -127 9 28 12 20 -48 97 -5 4 5 -10 -71 38 -65 -77 -64 -50 -21 -16 44 6 -21 0 -15 -1 22 -15 107 109 1 114 62 89 61 29 -28 28 -67 -116 -70 -41 22 -51 40 121 42 -7 56 -26 -11 -50 43 -73 85 -110 47 13 67 42 71 -65 32 14 24 -111 55 -7 1 -16 54 127 -20 7 -16 -62 -13 -19 4 56 -20 -8 -12 117 28 9 -49 -38 5 -36 16 39 -65 -27 60 -51 30 -61 31 -24 -22 110 -6 64 -50 4 -33 -5 0 32 -18 -60 -12 14 -60 -33 74 -31 22 44 -29 -49 55 -38 -11 27 -77 4 -41 17 -68 12 27 -51 -126 -20 -3 103 61 -117 -48 28 -49 1 -6 68 23 -30 95 52 -11 -26 -20 -9 -9 -8 12 65 -57 -5 -66 -53 -3 -39 37 -47 120 53 44 20 26 -47 -50 26 -15 -3 26 2 1 -6 -80 98 26 -82 32 17 -24 38 8 -50 81 -45 16 26 8 -9 63 -43 53 -8 19 104 49 114 -4 123 -19 29 -7 6 17 39 111 -47 102 -56 16 67 19 -35 59 -16 62 117 -69 -45 31 -9 -39 27 116 -24 119 -65 -83 121 10 112 31 -69 22 -66 -48 66 3 -118 -122 -104 95 -105 -17 13 22 25 -60 55 29 -50 -17 -2 109 -19 59 106 12 -43 -17 34 -89 46 36 -43 28 42 21 30 -8 25 103 103 3 37 114 53 -33 1 -7 -28 -38 11 -22 71 -57 -98 90 83 -63 67 87 -21 -40 -12 -34 23 61 10 17 44 -13 2 78 -61 7 -43 74 -35 -51 30 -13 41 -17 -73 -27 -13 3 38 -115 -19 4 -55 -68 -47 -4 -29 83 41 -5 23 -3 -20 127 -58 17 88 13 -37 -10 -32 66 26 -8 -19 -42 -3 -50 82 45 31 -93 63 -66 16 82 31 30 -29 78 36 -55 -24 12 -40 -35 -3 -8 -1 -51 -1 60 -1 -38 49 -8 -40 65 -74 45 -14 34 15 -8 -49 31 122 -24 15 -36 -84 -20 -22 -91 -11 41 -22 -74 -43 -33 30 -71 40 -29 -20 -18 97 -114 -58 -76 -25 61 47 15 52 18 -17 37 -39 -63 13 85 -105 23 -13 -71 50 16 -25 0 -2 6 33 -28 3 -1 -23 -64 39 -10 109 78 -26 65 -7 115 24 -99 8 -87 -19 50 -75 -43 -37 126 94 75 58 -6 9 -63 31 -19 39 2 -38 55 -36 -17 -6 51 35 21 18 -41 22 74 -6 124 120 45 79 72 76 43 -10 25 112 51 -59 6 106 117 86 9 44 -26 40 26 -57 39 19 -38 49 -8 31 -53 58 94 7 85 19 121 30 56 85 -10 -33 22 115 127 8 -89 46 68 -23 -5 -29 62 -5 1 17 26 49 -2 6 84 -56 111 -9 125 -27 -25 113 -46 84 102 41 -7 45 -8 126 127 -13 29 -68 -58 53 66 100 42 116 58 109 -52 125 -73 -11 46 3 27 -43 -27 -13 -8 38 114 1 -54 -23 19 -15 3 60 6 -4 56 -61 83 20 7 -37 -7 8 7 41 32 -70 1 -21 -3 -27 -13 -20 38 7 -23 110 36 -19 -77 15 -67 -63 -38 59 -46 -75 37 42 -6 114 -106 111 -43 9 -58 -42 -46 50 -42 14 -81 37 43 -56 46 4 33 -58 -13 -104 -33 33 79 -32 -28 -61 119 25 -10 91 -5 -26 1 -25 22 67 53 -12 -50 68 29 -36 -37 -41 -11 52 58 -32 -40 -48 -124 127 18 -104 62 94 39 -74 -18 -15 -16 -128 -78 33 -64 60 75 -17 -32 -115 33 35 64 -64 20 -71 -38 10 15 49 -45 23 -31 -122 -126 32 -5 -40 -29 -31 117 -22 123 31 52 46 -13 27 11 82 -55 -50 6 93 126 16 39 127 36 7 -20 4 -42 -20 -95 61 -80 -3 119 -14 87 47 -69 95 25 48 13 101 -65 -17 -25 -38 106 53 -86 62 42 -56 -85 -73 31 -92 84 -47 5 -19 35 -52 83 58 51 63 -76 8 65 37 -2 0 17 -56 98 -52 -75 -36 -83 16 110 -80 13 -85 -4 -82 -84 -13 -33 -19 92 5 0 -84 84 35 12 25 7 14 35 -126 9 -27 -42 -53 -80 -21 28 36 17 11 1 -4 12 1 -60 -83 36 -50 -6 -18 -92 11 3 60 -48 34 -23 53 -56 90 82 29 96 -23 -101 -17 85 -58 -38 -6 94 23 -36 -4 -26 42 11 32 51 15 -2 -50 -68 -33 50 21 27 -13 53 -37 14 1 37 -42 -3 22 18 -9 29 -12 5 35 -90 -55 40 46 107 35 72 44 41 -68 4 19 6 -41 29 -84 -5 15 15 -16 5 -28 -16 48 -2 -40 -54 -121 -8 22 -13 -37 -21 13 -36 -5 -99 11 20 -64 24 -112 -13 -43 32 28 36 58 -28 63 32 -10 37 -11 -7 -51 39 -53 0 87 24 -21 -16 -5 -38 10 -4 21 -23 45 25 26 11 120 -3 76 44 61 27 -28 -30 -40 -102 51 29 35 16 5 -29 31 61 -4 23 -28 -31 -27 -14 -35 -3 -20 -49 -101 5 23 3 -54 14 35 -10 -90 60 91 20 -123 -127 73 33 -9 -64 61 -15 -5 17 45 34 10 -81 1 27 -43 -47 35 -32 -34 6 -68 11 -21 -36 63 -25 66 -96 74 15 5 64 36 -5 -1 -86 52 60 8 6 -73 34 30 3 38 -5 -21 27 -8 86 51 47 78 6 64 -100 120 86 -23 47 3 -3 -21 15 -22 36 -93 -12 28 70 -45 42 -54 24 13 -67 127 65 8 -123 -58 -65 7 1 118 66 115 -6 52 -14 109 46 127 127 17 4 4 -77 25 -52 127 -93 -34 116 81 15 127 -42 109 120 33 -81 -46 123 29 85 58 14 103 -14 26 82 -25 51 -114 -79 50 -1 69 91 31 24 -4 17 -45 64 -65 26 49 -56 -26 -67 -16 -121 123 -25 -28 -28 -69 -21 -65 -37 119 -17 -21 -19 -86 11 22 42 36 113 15 -52 24 -26 -95 37 89 24 -62 -67 -42 -45 8 -9 -58 -19 8 -52 27 -49 -40 37 -19 -57 0 4 83 10 -33 13 -8 12 -125 -59 17 13 -48 -55 46 82 -72 72 64 1 5 -17 5 -48 -89 -10 0 44 -10 -72 20 -30 -104 -25 -56 -46 114 -99 16 -63 -21 44 55 -11 111 11 -99 -29 51 -28 6 4 -3 16 65 86 54 28 -21 -14 -30 -77 113 -7 -46 -56 -69 -70 -100 -47 -69 -64 53 9 114 1 15 -4 -71 -10 -28 -43 40 -27 -1 -19 -54 124 44 -78 69 -113 -80 -69 18 -62 97 -97 56 4 49 4 81 -123 -23 122 59 -79 -34 24 -57 106 -6 91 -98 -10 81 -91 -30 2 14 58 -126 -86 2 -104 -3 44 -39 74 32 -48 -73 -113 6 -27 -81 51 81 60 -27 -73 -12 17 102 -60 76 32 3 -51 -23 87 -15 32 27 -68 -52 -126 -93 46 43 59 51 -3 -5 -40 53 -6 106 17 35 127 68 -80 -39 36 115 71 70 125 23 103 6 -1 41 -50 24 -24 12 59 0 23 -59 -16 29 -28 48 70 65 -47 33 -42 46 53 -50 121 32 51 -7 47 -70 -10 35 -68 97 20 34 62 77 -6 -122 1 33 33 26 84 -74 57 -36 63 -6 -37 31 101 29 -31 16 -87 28 4 26 80 42 38 -42 2 -42 9 19 -103 33 -64 -14 36 -71 -31 64 -31 -33 -34 38 3 -20 41 -77 -20 24 7 -25 -15 56 -88 19 -14 -56 -22 53 21 -83 52 -43 -27 14 72 -67 80 -1 74 63 65 4 58 -65 16 -46 -55 -30 85 -92 -25 28 -60 20 -6 97 -94 -11 9 51 -3 -13 -21 -71 4 69 -25 31 -21 58 41 -128 -30 -18 -93 57 45 -88 27 -77 6 75 51 93 -22 -84 25 -40 8 -44 -52 76 -50 19 79 40 -49 4 50 -103 -43 -31 -12 -57 70 46 23 29 125 1 -13 6 -11 39 35 -88 64 -71 -49 -97 9 53 -20 -11 11 121 51 46 -24 -33 10 16 76 80 -21 42 8 -81 -60 39 -3 -53 -112 53 12 -9 73 27 85 57 43 46 118 2 29 -58 -55 -11 -51 99 98 6 -54 12 -63 127 1 8 -121 -14 -114 26 31 5 -42 -11 -20 -79 -6 -16 21 3 42 -23 -1 14 21 -43 -107 -80 44 104 -49 16 -91 -7 22 -55 45 0 -90 125 67 -50 42 16 -76 93 -92 -44 30 121 15 122 117 26 25 33 -11 -52 122 -45 -34 113 16 -7 56 -62 21 3 80 40 40 -76 110 74 -7 7 104 81 116 68 -7 13 -61 -80 -68 -124 -43 -126 -18 12 -70 51 39 -40 -39 -44 37 -106 -32 65 -64 17 58 13 -5 -37 -13 35 44 75 77 -64 8 40 49 -123 -89 100 -44 31 115 7 14 -2 34 -1 71 75 32 -34 25 19 -10 43 -43 -128 -83 -40 -14 68 49 76 -78 -10 -83 -65 -39 -37 47 76 107 120 82 19 -55 -49 21 2 3 56 57 3 75 42 -23 -4 -5 21 -3 -17 3 24 -35 -32 -10 -125 37 75 6 -75 -2 15 -75 45 -85 125 98 25 68 30 -9 55 74 47 18 35 42 23 -4 48 -113 -115 38 78 88 -36 75 -71 37 24 -8 6 119 66 -92 57 97 23 -22 -18 -9 33 -9 -10 -20 -53 88 48 -72 -14 53 -3 -22 -9 -30 -75 33 -93 -76 -26 -27 -120 71 -124 59 -27 -33 73 76 16 -15 -10 -27 -53 43 -35 12 -58 -10 46 -82 29 -23 32 24 -33 -7 -28 35 -118 75 11 71 -17 18 -29 -37 -25 -32 -76 -4 66 3 -25 -7 -36 -12 5 39 -28 46 117 70 -21 -14 -58 -126 -106 -49 -91 -28 60 112 -7 4 11 -43 -22 -39 -54 -2 -34 97 69 -61 123 -34 -73 -73 53 -39 -122 -109 -82 -78 2 1 -127 -125 51 -33 22 -38 -112 84 -26 -57 -35 -86 79 -54 29 -56 29 48 -60 -93 -101 25 33 24 40 -86 -105 43 -53 -49 31 -55 -47 58 -64 -43 -40 -112 -27 -10 -100 24 -124 126 24 -113 41 -42 127 2 -117 -48 -53 49 -124 -54 -59 -24 11 -119 57 -52 -127 -86 6 -1 -16 -41 -12 7 -38 29 -92 42 -42 -1 9 -102 -8 -73 -121 -116 -102 -76 -3 -23 25 33 -17 69 -41 -16 96 -79 -33 66 -71 88 -50 1 -16 -5 -120 33 86 -42 33 -44 -79 -26 80 -11 -5 -75 -41 -48 89 32 -51 -23 -24 -25 0 -14 -28 -50 -102 66 49 -2 108 20 -33 -34 -10 -31 15 71 -46 5 -5 46 -123 -13 114 15 67 -93 60 -65 76 12 34 18 -46 -69 32 -88 -63 -3 -73 -89 -17 90 -50 -54 -73 78 -26 -20 -67 19 22 -32 -14 -48 -111 57 -112 58 -128 80 -61 4 -15 37 7 -21 -20 35 47 -19 31 -76 -19 -34 91 13 -16 73 126 93 -35 -41 -1 16 -61 61 -94 1 47 -76 -69 -67 -28 36 64 88 -35 -110 23 -35 -105 -43 13 99 14 -31 11 -36 -76 -57 26 -17 -33 -76 9 24 43 -42 25 49 94 -4 -105 -103 10 -38 39 -51 104 -76 55 -13 -58 23 12 -30 61 4 45 -9 21 -53 2 -32 13 -15 22 -7 18 25 71 -30 14 99 48 -63 -35 -18 112 -128 -39 96 20 -32 51 26 30 -53 -87 -54 -55 103 25 103 86 -4 66 -78 -44 46 16 -122 -38 2 103 53 21 -1 12 15 47 9 25 21 -43 -50 -41 -19 99 14 31 -80 54 56 -11 32 43 122 -60 70 -82 32 -14 119 16 21 -103 1 -123 -13 48 -14 -72 92 -57 2 -43 -61 38 -54 50 45 -61 82 -99 56 -75 41 59 51 63 13 -123 -99 -29 81 66 25 13 -37 -56 20 -93 -19 -48 -24 -58 -19 -44 -12 21 15 12 63 -34 53 -5 -87 -82 -42 -127 -43 54 56 77 12 -77 -28 82 -23 125 63 -52 -1 30 31 73 -19 -127 17 -54 -15 -31 6 -125 55 127 46 24 -37 -2 62 73 23 -2 83 -69 -4 -45 -15 12 -4 -47 -19 92 50 66 19 -51 12 -8 -114 16 3 -108 38 0 24 36 -1 33 -35 -5 23 7 46 -2 9 -93 -19 -21 21 -38 4 -1 -55 60 -104 -127 48 -36 -24 1 -18 36 -116 81 85 47 -69 50 -34 54 122 -12 127 27 35 -2 -61 -35 -1 106 -103 -121 16 -33 57 -24 10 -110 -22 30 -27 16 -33 -41 20 -125 -80 108 127 -3 37 -11 90 -7 -46 59 -34 4 -29 49 17 46 -121 -18 102 -123 -37 4 -47 -12 -120 119 104 -20 -60 -2 17 33 34 45 30 101 6 -39 77 12 -85 63 116 12 -51 127 126 -10 12 66 21 1 20 -26 78 -11 8 -11 -50 -127 -38 92 -74 -65 -108 -57 -19 66 41 -100 -24 -60 82 -23 95 2 -68 22 54 68 -10 12 9 -90 36 33 -103 72 -60 -52 -43 -103 -60 -83 -82 -72 -51 6 -53 -30 -34 -100 -119 -125 -123 113 -15 69 -96 -95 125 76 24 -66 57 126 96 -104 -33 8 73 103 -56 72 40 -48 -24 -12 -60 -4 -69 -56 -51 -70 -36 -56 -45 -80 29 -43 -22 43 -62 -31 10 -20 -70 18 -36 18 75 30 37 -84 39 6 106 -76 -27 14 46 84 -89 -24 -10 44 110 58 -1 51 71 -25 1 -26 -60 47 19 31 2 -116 -11 -42 84 -39 -14 -44 79 -14 2 -121 39 80 -62 33 -1 55 -13 -76 -90 -81 -21 -17 77 44 15 -25 -15 22 69 -59 -127 -45 84 116 -66 -87 51 123 21 13 5 16 43 7 41 21 19 -45 -12 -31 -108 -54 34 9 70 17 -81 65 28 21 50 56 -55 -127 22 -87 15 101 24 49 29 -42 -10 -44 57 17 -66 -36 87 -11 -5 -85 -128 115 -96 -41 126 -21 -1 36 15 1 20 60 4 -6 -88 -5 24 73 109 -54 77 125 13 60 88 127 68 34 -110 6 -3 27 -26 -62 -80 -23 19 -71 28 67 -54 116 114 73 -28 108 -23 -32 9 -8 82 -23 -81 56 -44 18 -45 -57 -1 -106 37 -54 -44 -43 -31 -73 21 -33 -24 -24 -114 -39 -8 66 16 -118 27 -6 56 -2 13 9 121 -31 64 48 4 75 -17 -20 -78 -36 11 23 84 85 -81 -78 40 -25 71 -54 -90 37 41 -16 109 127 65 -45 -59 -2 28 11 78 51 74 3 -12 84 91 51 -34 127 -52 -41 31 -57 -17 -68 -76 53 114 -97 120 87 118 37 24 43 89 120 109 -40 1 41 80 110 -124 125 -89 107 49 -61 112 0 36 17 35 -7 8 -57 49 84 111 58 43 46 10 -7 90 -101 -77 -80 127 109 29 -89 30 -77 -46 -28 -11 -35 12 -77 0 1 -18 -4 -56 82 7 -32 -48 22 -37 -18 44 33 10 -34 19 -90 -15 -54 -94 -94 56 -75 -43 36 46 27 -8 -73 -57 36 -43 90 -36 -70 51 -88 -38 -83 -39 -16 -29 -87 5 32 -14 48 13 45 42 106 80 18 -75 -57 -1 43 15 44 -29 12 -44 -114 44 -20 -48 -15 -36 127 -26 54 -52 90 7 104 -10 77 -18 46 75 48 35 -83 -43 -9 58 121 18 -12 84 1 -40 -47 17 -120 -77 -22 74 10 52 4 -30 -43 -19 -4 11 -86 68 16 31 -1 -6 127 -4 31 -38 -38 21 5 88 -112 63 11 -29 117 -39 108 79 -113 -65 3 48 -24 -15 -15 52 39 -22 -32 34 -27 -32 8 62 22 -27 68 -44 26 10 5 124 25 -74 -4 15 77 -126 -127 -63 -27 -76 -5 -35 37 -50 52 -10 63 7 -78 30 -42 13 -75 18 -63 -31 89 -2 32 73 -17 104 -34 35 -103 13 87 19 -14 -104 -36 -76 -50 30 35 -72 10 -8 -91 104 -96 5 52 -34 24 90 -13 27 -123 72 -27 114 -20 123 -12 -34 -26 -54 -127 -24 70 -6 -75 -50 0 13 -21 5 103 -57 -71 -74 32 68 -73 -18 67 117 -80 -42 111 50 57 -74 -127 -10 -34 127 112 -121 -63 -107 25 61 91 -16 -35 9 -25 1 -34 87 59 56 -105 108 90 -92 37 15 18 -111 29 31 74 -22 -20 85 -76 -42 55 -41 -23 -35 -55 -92 46 -3 -81 -40 17 -84 -54 1 127 26 -120 -15 79 -72 -1 -126 60 -29 -70 3 67 36 -45 15 41 100 -72 14 -36 24 -30 -61 -28 127 82 -58 -6 -51 7 -41 23 -5 -42 -10 -24 79 -70 88 40 113 0 22 -55 -46 0 9 -75 31 -67 -37 -15 4 17 27 24 -113 28 -125 -117 -8 -124 -1 74 -3 -74 50 -20 16 -50 -63 -43 -20 -90 -49 -93 -30 11 -97 83 -121 1 -57 -43 27 -11 28 -62 -84 126 -15 -64 45 50 -32 74 31 -125 95 -46 -79 -3 18 16 79 15 -26 -38 -49 127 74 -32 -25 85 41 23 -17 1 -66 -29 53 5 -33 36 32 0 55 -62 50 -25 36 -119 -47 -3 16 11 46 71 8 -52 77 -37 -25 105 89 36 -37 -3 -19 -49 68 -29 3 -59 34 -127 40 2 56 -127 -121 -53 17 34 -53 -29 25 -19 1 56 -40 75 14 26 -55 67 -126 56 9 -26 22 17 -13 59 70 1 -39 -33 42 61 -5 -51 -3 23 -90 -32 57 -97 -45 15 -65 43 118 43 17 -121 4 20 31 19 -90 44 -62 42 98 -60 -105 -39 24 68 38 -71 64 -75 113 0 -103 -39 34 -39 72 -34 -46 -123 -58 15 55 11 -39 -110 82 -28 -53 -49 -91 76 -21 53 60 -32 -60 50 13 52 11 39 46 37 -85 -124 -20 -7 76 5 76 44 -16 -73 -69 -54 15 19 -47 26 84 -65 -30 50 -24 31 61 47 -12 4 -34 66 -99 -16 -60 78 -1 72 -19 -52 9 48 -7 94 73 37 -11 110 32 98 -62 -45 -124 63 7 -47 54 -49 -18 44 62 46 68 -39 102 -16 -57 22 -36 59 -47 48 39 -27 42 -40 0 31 -1 -6 2 -68 66 -5 33 -41 -46 59 -19 8 1 106 -46 5 48 90 -23 -52 51 10 -7 56 91 -7 51 83 -7 36 -68 47 -17 -87 120 -43 40 -53 -10 -15 -35 85 122 7 -78 30 30 85 102 -26 92 -25 40 24 -11 2 56 46 10 22 121 29 27 -30 -84 -98 -15 -11 12 -35 -85 -123 29 -86 -27 -23 122 76 12 17 29 -27 -42 84 0 23 -5 -22 80 -64 -89 -38 76 -128 -32 -10 -47 2 -97 67 104 -41 36 10 127 123 10 16 21 39 41 19 64 -53 -118 -49 -65 -10 -3 8 -37 -68 -45 89 10 53 -16 -9 -38 -13 -75 -106 -27 -73 94 95 72 -35 -19 -28 6 -76 -57 34 4 -45 18 75 -52 -4 -59 -34 -91 -12 45 -9 6 114 -43 -3 5 -73 -5 -24 -81 66 24 -83 -127 -52 -121 7 40 2 -33 -3 5 3 65 1 55 -34 22 -9 9 125 40 22 126 -105 74 24 -21 126 -39 1 -52 22 -10 -20 -43 -51 -60 -36 3 25 124 45 -12 -120 -32 85 -98 -21 -120 35 -2 -5 9 -38 -84 -1 8 22 -122 2 104 -17 93 -65 127 61 -101 -32 -44 1 -74 -23 -37 60 -27 -125 10 73 79 -48 43 -29 -109 39 -126 15 -121 -125 -41 44 52 -114 -64 -33 69 -55 76 -26 -113 -42 -65 38 -87 -1 47 -14 -14 -27 -43 40 -52 -11 -16 115 -57 59 -90 -20 -64 14 25 2 4 59 3 25 -4 7 32 13 9 77 121 6 -64 -26 25 125 -21 -3 -128 25 -25 72 102 90 -23 10 14 -34 -37 35 -17 -90 -28 -46 -43 -13 11 25 25 -9 -125 -21 -68 30 2 -24 36 21 -128 -26 -11 36 76 -20 -24 -41 -120 -59 32 40 -44 -127 -22 -3 15 -64 53 -22 124 19 -28 59 41 -22 -18 -5 -56 31 31 -41 -53 -93 -39 -13 43 54 -127 97 7 82 89 36 -70 95 0 24 33 -20 -13 60 33 17 -32 -48 -68 -36 -127 21 19 -62 -105 -30 -21 -49 -27 113 -9 84 73 3 -36 -49 -111 104 -65 -122 -45 -20 -29 -9 19 41 17 -108 -64 20 -26 24 -85 -22 -30 -89 -57 -49 -51 -67 82 26 23 -82 -95 -111 -48 4 -6 50 -32 -33 -117 4 49 -117 -24 -105 24 42 -27 -80 66 73 -100 106 -123 -66 -23 -24 3 3 -68 -57 -7 -47 -70 98 34 65 32 -40 -89 124 27 -37 -4 11 42 65 -127 -1 -82 127 -6 -103 -5 -67 -82 -58 11 43 14 -14 -57 26 -92 -6 47 -62 -19 -7 75 -16 -55 -108 23 -55 48 75 -26 69 63 -37 -18 -21 15 -43 49 42 -2 -110 -23 29 45 -1 -70 37 49 -33 123 -22 51 0 -97 -39 -41 -115 -49 0 55 -57 10 -26 -77 82 5 -58 -94 49 44 8 -33 -41 -72 -93 17 87 -50 -37 -40 11 -35 -13 2 -21 0 -36 48 86 80 38 20 70 -31 35 9 32 20 -64 -51 -67 -4 29 -75 -36 -73 60 1 16 -49 -74 -19 54 -11 8 4 2 50 -4 -40 25 -12 -55 -40 45 42 32 107 -108 -27 -64 -23 -54 -3 39 71 -21 77 -52 24 -12 -5 6 75 -9 -63 -41 -125 0 127 -109 57 13 7 73 94 98 -36 8 20 16 64 -9 5 -72 -116 26 -57 -51 31 8 -75 65 -73 -33 21 -35 -22 75 -4 3 39 18 32 -2 -5 46 -53 106 -23 29 21 38 10 10 -75 -97 -54 -48 -63 -15 -66 11 48 108 8 -37 -103 -23 29 -116 93 -7 8 -64 -48 74 -99 111 -65 -87 -74 -34 -71 -64 -3 -38 -79 -5 43 -93 5 -41 2 -87 5 -61 9 54 -51 -77 -101 -45 -29 -5 -41 -89 -61 -13 -76 -1 -1 -41 -51 -64 -7 -49 10 -30 -48 11 -74 -14 -30 -93 -52 -7 -73 -74 -55 15 -5 -56 -48 -88 -52 -11 -12 -53 -30 -15 -21 4 -45 -24 6 -64 -127 11 -99 7 -8 -79 9 -51 -38 -49 -35 -45 6 -127 -48 -18 -25 3 -66 -44 -15 -78 -11 -26 5 -27 -17 -1 -41 -21 -35 -1 9 -40 4 25 -22 -3 -35 7 -1 -25 13 1 51 20 9 -3 27 11 -36 30 -35 -9 -21 -12 -26 -4 52 23 33 7 -19 -63 -22 50 -74 -10 -32 3 -18 -25 25 -13 -5 -34 -12 31 16 31 27 -33 -25 -17 14 14 -6 -22 5 28 -1 -46 28 18 58 -5 -11 -36 -29 -26 -7 2 -5 -10 -14 -18 -38 28 -3 47 6 6 -2 24 2 5 -60 -42 2 -14 1 -2 20 -21 -56 20 -28 28 52 61 12 -126 14 63 -117 34 124 27 -47 -125 -85 9 -60 -128 100 -25 -31 80 -3 53 53 -27 67 36 39 127 -77 68 28 103 61 -31 127 23 86 125 -11 -17 -128 -82 11 -101 -13 -69 5 -1 -18 -65 -16 111 -58 -124 -5 122 -79 76 110 48 73 9 -72 117 -128 -38 17 53 -104 -53 -17 23 88 -87 -82 58 -111 42 77 127 -31 80 25 -97 13 51 3 -50 51 46 -38 93 108 61 37 -38 -8 28 8 19 70 35 65 16 33 -2 -60 8 -73 18 61 -23 -103 -27 -3 -14 87 73 60 -74 -16 1 -122 48 42 19 86 118 84 -44 9 34 36 47 73 -6 71 -12 78 -44 -19 -31 -56 -25 21 -15 96 67 -16 -14 104 38 36 20 127 -24 -29 17 -26 -90 28 116 -26 63 17 82 -14 -2 -48 41 -9 74 -47 44 44 45 30 -126 26 5 -78 -54 53 42 -2 -42 60 -9 43 -3 126 31 -38 -43 92 109 -8 -43 28 -72 30 27 -20 -69 11 14 40 -45 -71 -105 -6 0 30 21 22 12 -75 -19 6 18 25 -26 -20 -43 27 -74 -72 93 -27 57 -14 35 -30 53 52 -85 -94 18 -31 -63 44 32 -10 -77 1 -15 -24 -21 -11 -11 -9 1 -38 -34 -13 -7 -22 -80 -6 62 -6 -88 34 -80 -48 40 23 32 -106 1 53 -40 15 8 -6 -16 23 91 1 -13 21 71 2 -36 -124 -3 -69 53 40 50 52 34 52 -42 17 37 -37 -6 15 79 21 2 43 13 -65 -53 27 -50 -76 61 22 37 51 70 20 -4 -1 32 68 -113 54 -32 -26 -1 8 -11 60 -54 -82 3 48 -5 75 -6 89 5 20 -44 53 -9 59 -22 -24 -15 50 -49 -36 -101 40 -20 101 23 12 -31 -2 -10 24 -8 -21 -12 -15 47 22 -5 14 -17 -24 126 35 34 16 15 -53 85 47 64 1 38 -9 89 -17 -36 53 127 -10 -90 0 32 9 21 92 -128 22 5 -30 103 56 21 20 22 -88 -77 2 6 -31 3 52 -7 8 -10 53 -126 119 83 123 -20 -37 -16 -104 14 -18 127 11 -42 -4 -94 3 93 -66 51 -106 -33 47 -75 -45 100 1 31 -4 126 -60 19 -48 18 -2 -68 -9 80 6 -123 -84 38 -2 -51 13 -22 -54 -62 120 39 59 57 47 56 -37 67 -13 9 6 -21 70 -7 -73 -62 39 38 -63 -46 34 56 97 -20 7 -72 -52 -63 39 -80 -73 -63 42 -1 -31 -54 -33 18 -95 -13 -18 -2 -45 42 -83 95 -85 -76 -101 -58 49 -7 29 -18 109 -17 -14 -55 5 46 -4 -78 7 87 -17 -44 33 -111 -72 -31 -29 -29 -5 81 -30 11 -82 87 -23 101 -35 5 -74 -25 47 47 46 -66 40 -48 -26 -87 -27 64 13 -106 -14 -38 -6 -8 -43 -2 7 38 -122 -18 101 -107 -13 -46 1 -34 1 -14 -3 75 -34 -85 -2 26 42 9 40 22 -125 23 49 -28 -4 -125 -45 52 -104 -128 8 -52 -14 -17 42 -10 -33 -13 -34 -26 -8 48 -25 114 127 -57 -86 45 97 -73 -9 87 -5 -27 -125 43 1 -43 -103 -43 45 27 -4 13 28 76 -21 -20 -4 124 3 8 117 -125 -7 -20 5 13 41 10 19 2 -94 -18 -33 -24 -29 -55 6 -58 -26 12 25 8 -71 1 -23 -49 54 100 -74 0 87 51 -8 46 29 53 91 -53 -99 -40 -61 111 22 -2 67 -3 -8 90 54 -28 -22 -51 -33 -24 47 -24 48 -18 34 97 123 -27 -72 22 -50 36 24 41 -17 83 126 93 5 22 69 99 102 58 7 -10 41 27 -11 -29 -83 7 16 -50 71 22 -128 126 -19 107 31 107 97 -73 -39 81 51 -118 -35 123 29 46 65 -75 31 -55 -102 -3 35 -2 11 -100 -37 127 71 97 16 47 8 17 -55 43 -126 -25 87 4 36 -20 55 -124 86 11 -30 104 53 -67 18 -11 -34 -31 1 92 -77 -19 -116 11 61 -126 36 44 23 90 -63 57 50 -18 -94 -39 -20 -103 30 -34 -5 -64 -19 -48 72 55 -36 0 -19 35 -16 19 13 24 -127 -48 24 0 43 -5 2 10 41 127 6 29 40 20 30 63 34 -49 18 85 -5 28 90 -9 -51 -1 -55 -45 -85 4 19 -1 -84 83 -23 64 -29 -50 16 62 -37 -14 14 31 -88 15 24 -32 -36 -44 55 9 2 -30 55 -36 -90 -28 -30 -12 13 -99 29 -51 -56 -28 -23 -52 8 -38 -18 -55 52 79 15 -45 50 -65 -59 -35 -16 -125 -22 45 42 -29 46 61 21 -82 -5 -13 9 -53 23 -25 8 -22 48 -25 -51 -7 39 -34 61 69 -19 32 44 41 -22 -2 70 -3 -76 -24 -88 -27 -93 -46 -17 -33 41 -15 -59 -87 -31 30 49 41 -33 41 53 -62 -12 119 56 -38 16 1 56 80 8 104 -15 -10 -44 35 13 -8 -8 -48 -85 -5 76 -22 -110 31 44 77 18 -2 57 1 110 25 41 7 83 -17 -33 -74 75 -54 -51 116 99 15 11 -60 -7 7 31 24 26 -5 40 46 17 5 10 73 9 3 -49 102 -2 -107 54 -54 -52 -70 -91 86 -77 -38 76 -41 -15 -7 8 -16 -92 -26 -27 60 -11 24 -69 -69 92 87 21 106 65 47 8 -7 -22 45 48 35 -49 24 -10 30 41 39 23 -3 102 -114 -65 -86 10 -57 -40 -24 3 -18 -36 -12 -13 -35 8 -17 17 48 7 -58 4 16 -17 17 -52 36 22 -53 -28 -62 89 3 36 26 98 -17 125 5 118 64 50 -70 -42 87 29 -125 6 50 1 -9 -21 71 -75 -25 -27 55 -20 -33 -41 0 -73 -101 -1 -71 27 26 -61 42 -92 54 -9 -92 -58 -93 -52 -9 4 -22 -56 -5 94 38 31 0 31 -77 -9 58 -36 22 -5 -10 39 46 -9 3 -94 31 -1 -15 -105 -7 -1 -97 95 38 36 -25 22 -3 56 0 66 -1 69 -93 -18 -91 -44 -39 99 -8 -65 21 12 -77 -53 -26 -33 -63 32 17 101 76 -14 -19 -10 -35 -36 29 36 87 71 76 15 -71 -38 35 -38 33 -49 36 -50 17 54 17 -28 -76 30 -40 -112 0 -62 -28 -5 52 -50 -44 -11 10 8 15 -70 72 -53 -82 -35 -41 13 -95 11 32 22 1 -12 -110 7 -82 -91 17 92 78 105 98 -44 -32 -22 23 -8 -26 63 -81 25 -49 16 -31 82 -87 38 62 -3 -113 -85 -4 -6 36 51 17 -21 21 -92 -6 -14 -82 -38 15 -16 16 -116 36 -78 48 2 94 43 -75 46 70 -32 -25 -116 10 21 28 21 126 20 52 -46 15 127 64 7 105 69 -28 8 46 -103 -6 -40 49 54 39 -37 -33 -97 -43 60 127 -2 -47 14 15 2 -53 49 28 -29 -27 43 -49 -14 54 -36 -3 58 -45 -28 -37 20 42 -61 -1 -18 43 -60 -69 -32 74 65 -21 87 84 45 30 29 -53 -6 -122 30 17 -56 14 37 58 -1 29 -56 26 -15 -29 -27 39 80 -37 26 13 42 13 50 -24 -22 -6 -13 -3 -42 -20 -43 16 55 28 4 24 -14 -17 -38 -78 63 -50 -33 -55 -49 -2 -91 -57 41 -15 -57 -64 -81 -15 2 -2 73 45 -101 -34 72 2 -2 -1 -102 -13 -36 -2 34 21 1 -13 65 1 -50 -11 3 -54 -17 48 -87 -120 -8 -61 -48 5 126 41 3 13 -80 91 -19 -61 67 16 8 7 3 -28 27 -98 -63 -9 -64 -9 -13 13 19 24 57 9 62 29 -34 57 91 -26 -46 8 -37 3 12 9 17 -14 -13 26 -8 2 -60 126 109 -46 -86 40 -24 -31 -14 -59 -20 38 21 8 39 -99 -40 11 -40 66 40 40 -102 50 -21 -8 -42 16 -66 65 -18 64 5 26 -88 14 2 4 -80 -28 -92 -83 -21 47 -52 -27 -46 73 -4 40 -17 -50 -62 -81 -54 -15 -105 -14 -34 46 -10 -125 -99 -25 27 -32 -53 115 21 10 60 48 35 5 -22 61 40 87 -48 -125 -71 56 8 97 -17 -51 -68 1 -12 -2 37 41 -70 46 48 97 17 10 24 23 21 -125 41 -52 23 -61 -91 -11 -101 103 18 1 -44 -90 49 -49 -43 -26 47 -2 81 -17 -38 75 -104 77 -14 -3 -46 -58 -18 68 -5 -97 -21 112 -92 -57 92 80 -40 -75 83 -87 -45 -34 -23 4 -70 -128 -82 121 -51 -58 -82 89 36 -102 -35 -23 -59 15 -120 -36 -47 -20 -115 -70 -83 -96 -46 -45 18 111 2 -43 41 -68 -25 24 -65 -46 -16 77 -35 -109 -38 5 -12 -52 -127 32 -66 89 57 86 -21 5 60 8 -65 -84 4 8 -32 46 -19 9 119 -25 -13 -56 -94 -39 -28 89 9 -9 -27 119 -39 55 -64 -28 -36 -60 -69 -100 2 -17 -32 -94 -9 0 -58 -74 -63 3 -67 -11 52 -67 61 21 -2 -90 110 -72 -127 -56 -62 -7 71 -37 5 -50 -94 -98 50 10 12 6 47 53 -5 -79 -35 -43 -5 47 -64 127 -40 53 100 -10 29 58 44 -35 18 -126 -17 -41 18 -26 -19 122 -16 -45 66 89 -18 81 53 8 35 30 -19 40 41 -15 40 0 -23 -4 -57 -30 -23 25 31 -34 17 69 -59 -128 8 -29 118 -5 -1 56 -25 61 69 67 -62 21 -128 24 -78 60 -105 90 11 106 -126 -13 83 -78 -20 -57 -7 10 42 73 2 113 -30 -72 -112 45 -74 -36 -31 -66 -35 -3 -15 10 37 31 19 -27 5 -28 -46 37 -50 -9 -39 -22 -56 -76 -19 62 -45 53 -65 7 -7 41 -73 106 -1 -51 -15 -22 -60 -10 -44 105 -5 46 24 18 -81 -27 -128 3 -74 -12 13 17 33 -4 6 -7 -18 21 45 40 -20 42 37 -107 54 60 -37 -121 127 -31 -77 8 -42 -16 -54 -6 -66 -14 -15 -74 -19 4 -12 -89 36 -21 18 -1 -39 -17 -43 -49 -101 25 -32 17 -7 -12 4 44 -42 -4 -11 21 50 38 127 55 38 81 1 -37 48 -58 -31 -56 -20 -20 -53 -45 52 -60 -40 -7 -23 52 4 52 -8 46 -86 11 -14 -24 -15 -22 -2 -24 72 48 34 12 -56 -23 -10 74 -12 27 83 -127 -40 -2 9 49 4 23 -13 -37 -58 14 -45 -26 38 -2 107 -81 97 -27 -40 47 26 -49 89 83 75 6 -101 22 -127 -46 46 -95 -11 -84 -46 34 -93 -118 -36 15 -65 16 23 0 126 -12 58 -82 30 11 16 94 20 88 -38 -8 127 72 127 50 -23 -3 -96 35 -37 -31 -48 -72 -79 2 54 42 -94 88 -42 54 -31 96 -101 23 107 -85 -74 94 0 114 -41 1 82 17 22 -29 -18 33 1 -72 -42 1 84 -51 -8 46 28 93 -8 -66 69 25 25 -32 -7 -53 15 65 -36 -24 45 55 -54 -46 -13 -42 12 -98 4 6 -88 -120 -19 -32 -44 -57 71 24 24 -77 -30 -24 -87 16 95 -66 19 -47 -49 43 75 91 10 15 20 -62 85 27 97 126 -97 32 50 -35 -70 -12 -1 -33 -72 79 -23 -54 -126 -28 25 -6 51 -18 13 -18 44 29 -45 -33 37 18 8 -32 -34 -11 -3 -90 -83 -39 -80 74 25 -89 32 -39 -75 -69 -24 18 -29 -61 -15 -2 -47 7 20 -11 13 34 19 -8 14 12 56 30 -56 58 5 -15 -73 -27 -5 -94 16 -23 7 3 21 38 55 -10 9 -40 -38 34 23 41 -4 -15 -51 25 -13 -21 68 -40 0 21 -36 77 -78 -20 64 15 -78 -68 50 55 90 -33 -35 -24 30 40 -38 -58 -40 -14 8 -20 -50 68 47 11 -47 51 3 45 -29 -53 43 27 69 -47 127 8 -10 -18 22 22 102 -10 63 -84 -21 9 -12 -52 -25 -24 -5 -4 54 74 -33 15 -1 -23 -29 -17 62 7 66 26 -43 -40 -12 69 24 4 26 -37 -27 20 -44 -18 -20 -44 -100 -38 -50 -21 -15 8 -23 85 -7 46 -3 -10 -8 -38 -9 -22 -19 -6 -49 59 -44 -42 -25 3 1 -34 -8 -46 59 -31 52 -46 -21 27 42 -14 -44 -25 14 76 33 -52 -18 15 60 0 51 10 60 -7 -2 -25 10 -24 -80 32 -12 31 45 -27 16 48 -66 57 26 -78 -75 12 49 14 33 -18 -42 74 21 80 -22 82 28 17 43 -29 -70 32 42 -62 -19 1 -114 60 119 50 -28 -15 -62 66 -23 -78 -39 -37 -60 60 -25 72 -22 21 -44 116 -15 32 25 -21 4 101 1 -24 -46 33 32 65 -62 -21 58 -71 70 -65 19 -15 59 -40 -2 11 45 -128 -96 -72 6 30 -40 112 69 -119 69 -127 0 8 119 50 3 -42 39 85 -8 73 39 -59 -94 -7 -108 -24 -20 -11 68 -34 15 -83 31 8 -63 -26 -66 16 -120 7 -53 -78 -27 -43 0 103 -61 11 122 127 52 2 109 40 1 -40 -29 79 30 15 4 -20 84 -18 -113 82 82 79 -37 64 -52 62 -17 -38 6 123 60 10 -98 -56 -21 -63 123 -31 30 39 13 10 -12 4 85 -46 121 -51 69 -124 -65 34 -25 -111 15 9 51 92 -43 -26 89 -83 -4 24 -18 -29 -17 38 12 -39 -94 -46 58 -103 9 -116 89 4 -55 10 -9 -51 56 -48 -7 120 -11 -8 87 49 -49 -29 12 -6 -5 106 57 -1 12 56 25 80 -53 13 51 77 -76 57 -48 3 -51 18 -50 -40 -9 -76 92 16 -32 17 -14 -33 65 14 -75 -73 -13 -125 -50 62 39 6 -31 126 18 101 46 -22 20 44 -6 -102 -6 -48 102 -98 51 -108 -23 -4 -74 -16 -70 -46 124 -110 78 -43 63 18 120 29 -12 110 36 88 -48 -123 -72 26 -68 -8 -53 -20 -93 -32 -13 61 -84 20 16 -3 56 60 -92 121 -117 69 1 32 64 -60 -121 -126 123 80 56 22 64 -16 -35 21 -38 108 -50 -44 108 -98 6 -43 -58 -91 -32 2 -49 17 68 54 -49 17 -88 42 36 -20 -7 -90 -82 -100 60 69 -58 28 -38 124 24 55 2 -28 39 -3 16 99 -10 16 57 16 41 -53 84 100 -44 -91 58 -15 -11 -45 35 12 -115 -33 -54 -66 21 -109 50 72 127 58 47 20 -29 -2 -60 86 -22 78 -30 37 43 -52 -76 -3 19 52 12 -18 6 -17 -102 11 36 4 60 -12 17 43 -39 -59 -43 4 -48 -21 -8 -7 9 -3 65 124 85 6 104 97 29 35 0 -13 -92 -53 59 -112 -99 71 28 46 28 102 6 -79 110 -1 15 23 -22 -12 20 -93 80 -10 96 48 40 -7 57 6 117 6 30 4 115 9 23 46 69 9 39 -43 -22 4 2 17 40 -20 68 -25 19 93 53 45 4 -5 -73 -44 98 -13 50 104 9 -35 47 -43 -31 70 37 22 -14 -16 -4 69 -9 28 -38 -47 19 15 -11 20 -5 -15 78 9 106 -25 99 24 -60 -58 -26 3 -53 48 -52 8 -9 -28 -26 41 -61 35 40 59 -88 64 -44 -3 22 33 -55 -17 18 -33 -71 -22 -12 -1 26 -105 22 -27 -41 31 -86 -112 -33 8 -43 6 -26 56 52 -20 7 -34 -95 42 -33 -35 -37 -89 -62 49 -66 19 -72 -40 14 -43 41 -46 48 15 0 -61 -113 79 10 56 75 -15 -22 -40 13 6 -58 -74 23 -60 -22 -3 8 -94 -21 41 -66 50 35 -19 -41 -123 -120 -26 -56 75 -41 -37 -124 -75 -37 -85 -66 -48 -46 -101 19 -69 29 -96 -31 99 8 -26 7 -47 63 -1 -8 -5 41 18 57 0 -22 34 -78 36 -66 5 -53 -14 -23 -123 -42 -126 -48 15 -19 84 7 -36 8 12 56 37 -31 25 98 -5 -37 -29 -4 -49 117 -43 -59 14 4 81 26 -31 -37 -120 -71 50 -88 -128 81 0 -15 10 123 1 -13 57 -19 -14 48 84 -23 2 -40 -35 95 -36 -10 23 41 17 -23 -121 15 -10 -16 10 -61 21 -87 13 127 -95 24 17 46 101 -14 -77 43 127 39 -32 4 57 -73 -75 -6 13 104 11 4 -114 -28 63 72 33 27 -10 -63 99 -14 -45 -91 23 24 25 77 -79 -42 -28 121 57 60 107 -3 -66 11 -36 -88 37 15 -9 -58 -86 -13 43 -36 18 15 -1 -86 61 -107 -87 88 43 28 10 9 88 4 69 43 -26 115 41 -9 -5 65 10 102 42 -22 -100 -68 -57 -28 -113 -34 13 -36 123 0 66 4 -98 52 -84 -5 54 58 -12 41 44 -33 116 -58 25 -127 6 13 -124 -49 54 -68 24 30 -111 -48 -12 -86 -35 -44 -44 -68 -31 23 -3 -63 -1 29 32 76 84 90 41 -32 14 -103 17 53 -28 -10 98 18 27 20 -26 21 -73 31 -42 58 95 -18 -7 -46 120 6 86 2 2 -26 2 -22 -53 -13 112 40 -66 109 31 13 108 -117 77 74 -52 13 73 98 91 -7 -121 -50 39 -66 41 121 94 73 -71 -88 126 -20 91 -13 26 23 80 12 51 90 87 -55 -38 68 12 -54 47 110 28 63 -98 36 -2 23 -21 -26 -72 44 -28 -126 -43 6 89 -5 70 -30 -15 -53 -125 50 -34 -10 85 77 26 -39 39 9 36 -31 23 -57 102 2 78 -43 -45 -20 -2 -3 34 -17 5 -9 19 84 42 69 118 125 -29 -52 61 3 -16 12 68 21 -5 96 6 -12 -35 115 -16 108 -42 -49 83 23 -91 -63 -93 88 -19 90 -94 22 47 102 91 42 94 35 -28 113 52 -27 -52 -22 19 33 24 -27 39 51 -61 -45 23 69 -13 -47 -32 -12 8 45 -6 13 16 78 -9 52 -63 39 -44 15 -26 -22 81 -39 -17 27 49 4 98 27 20 -3 36 22 -30 18 40 -10 119 6 63 -2 117 35 121 105 30 -40 -15 -76 2 10 -39 29 86 -114 20 -21 31 34 -7 89 -115 -74 -44 -1 -66 54 -100 0 37 34 -58 -23 -121 -23 -41 60 17 46 -25 -17 124 10 13 79 81 12 40 50 -108 123 20 -81 -125 32 -11 -32 6 -20 -26 80 -61 6 -82 15 13 3 57 90 103 5 20 -14 22 96 -42 36 -128 -16 18 -108 76 44 85 20 -37 2 -68 36 56 44 61 -67 -19 -1 125 -9 122 -49 -78 21 -5 116 -15 2 -79 20 -35 -60 24 -122 -98 -123 -118 -93 -127 29 -5 -96 -60 -21 -105 -76 -29 -3 -37 50 -56 -124 21 -38 20 -67 28 -27 -41 -67 20 -10 -41 88 14 36 -25 34 -67 -36 83 53 55 -37 -44 -24 -69 86 -32 -126 -18 -57 41 56 -104 -35 -29 -107 -125 -99 -45 -103 -25 23 -64 -123 36 13 66 -91 -33 -62 14 -75 21 -49 -13 -88 30 8 20 34 7 -27 96 -30 47 15 -29 14 -30 55 56 -8 0 16 49 -40 -101 13 39 -29 -56 48 14 35 -65 10 32 5 70 9 -36 8 9 16 60 27 -56 28 -57 -53 3 43 54 61 90 52 -28 23 -16 -38 25 55 20 -82 -40 124 117 84 20 56 30 31 -53 -35 2 -30 24 18 11 -18 -9 11 30 34 -38 17 0 40 -47 -79 54 30 20 -60 45 -37 65 -16 -43 -56 -38 -53 -87 72 -1 23 31 10 -17 20 15 13 59 36 -1 39 76 17 5 -18 -40 -24 6 29 4 -24 74 5 11 62 -65 -87 -53 30 -20 8 -30 -24 74 32 3 46 -54 0 -51 -7 63 74 -34 -11 20 38 -47 19 -13 -28 48 -72 -22 -37 26 -23 0 -36 -1 62 120 -123 19 48 -62 -7 -21 14 -7 -2 90 62 -24 54 -31 32 -46 -42 -17 -1 -87 24 79 -7 0 -50 14 24 -68 20 -44 87 -1 114 106 -27 60 26 6 -30 -21 -41 86 58 -13 21 18 -6 90 28 -91 -43 -91 -67 -6 90 33 45 114 -62 5 -26 -13 -24 -16 -52 -114 51 19 -44 -72 -46 -6 16 -3 -61 -71 120 120 36 81 -54 -19 57 -51 -24 -68 -31 -27 50 -35 -43 7 48 -13 36 12 121 -52 -19 25 1 72 9 45 -14 -64 78 40 118 16 -5 23 -58 -3 -12 -127 27 9 5 -70 80 -47 -57 -1 83 -96 -79 121 -30 38 -12 26 -6 98 14 -2 112 23 104 53 -50 101 -90 4 0 -27 -30 105 -27 4 -120 -16 -1 79 100 22 -95 66 -25 127 40 7 52 -39 -18 53 -119 -4 15 -73 64 23 38 -89 68 -7 26 70 -67 12 66 -18 103 -83 22 -66 13 9 -42 -11 -4 -1 36 71 1 -53 -4 3 119 63 -37 26 -36 66 16 -31 64 49 26 11 25 24 -42 -17 -17 56 28 -113 -7 -105 26 16 -33 38 -47 -23 23 -58 43 23 -55 83 35 45 -27 -7 127 18 -32 -67 -1 -24 -65 -11 29 -2 32 52 70 43 50 -15 82 -20 15 45 21 26 -61 -9 94 75 -47 9 -76 65 -11 -56 -24 -2 -1 -107 39 -106 27 -28 63 11 41 -48 -84 -76 102 -51 -127 16 -30 -3 65 32 58 50 21 25 33 -24 51 -14 102 16 7 -102 -5 -43 50 29 29 27 -41 54 63 -3 -10 -21 -34 24 33 94 -57 123 -8 7 -70 82 -6 9 -128 -128 -8 102 63 84 108 -73 37 96 123 12 35 -10 54 67 13 -98 -19 -58 38 40 72 126 97 -31 -66 37 37 74 64 70 6 -52 4 33 -45 104 0 44 84 -32 19 -10 44 81 -52 -83 66 -93 -46 127 116 9 -8 -12 -51 -63 3 57 52 39 41 -79 112 1 23 115 -32 -36 84 80 89 -52 -14 118 46 -2 125 -66 52 -82 45 -37 5 10 -38 108 -48 -71 -20 43 -27 31 80 118 46 20 89 61 44 -78 -49 110 -21 4 5 52 -53 51 29 12 -8 41 40 0 51 27 17 69 0 -44 8 16 -22 60 -20 -46 10 17 -63 -104 -41 -27 -84 84 17 75 26 -100 6 10 114 28 62 -14 -35 15 -53 16 -24 19 16 103 107 -16 -91 -79 -8 28 25 58 -6 -62 16 48 91 -29 -33 -70 24 22 16 5 124 22 -126 21 5 57 14 34 -1 42 -116 -41 -49 42 -22 -59 35 -42 -87 -87 73 109 -41 60 108 -5 1 -64 -106 -66 -46 -96 13 -103 103 -84 -92 -94 31 -9 -57 -31 -22 107 120 -4 -13 30 69 107 45 -19 -98 85 34 -116 -128 -104 51 47 -104 -37 -55 -38 126 12 -23 83 3 18 108 -33 25 52 43 12 31 59 66 32 58 123 -9 -3 3 -103 58 104 24 -52 46 -101 22 -11 27 -26 -41 -31 39 16 -4 12 47 57 111 -21 -52 44 43 -80 -98 42 -30 -34 116 -63 12 -63 41 15 -110 -76 -20 98 -81 -21 42 30 0 118 113 -7 -64 -128 -125 -18 -12 -61 84 -90 -56 -3 -70 76 -125 34 126 -100 -68 -24 -24 -124 -64 77 -13 -11 56 94 36 -117 -1 17 57 -53 -102 -48 64 43 34 -99 75 10 -46 -49 -15 -51 113 90 65 -33 -122 -17 63 112 86 -5 -58 71 -68 81 78 -54 35 -85 -59 6 25 -19 -40 57 96 119 33 -124 11 37 -8 -20 86 26 58 -122 27 42 -31 6 121 -12 -7 -53 -105 -59 14 -20 30 120 -5 82 29 -79 66 11 80 -45 43 27 -43 23 55 26 -18 -30 -39 42 73 27 58 -8 28 33 38 86 56 68 119 47 -52 37 40 -80 41 99 -38 -51 109 -109 -47 9 -85 86 3 -57 42 -4 62 37 -7 109 -14 43 -12 36 9 10 81 -1 56 -67 13 -51 105 33 -57 18 33 13 -30 14 39 72 58 73 20 71 -45 66 58 58 21 -47 83 -87 17 70 20 37 51 -62 41 14 -62 -18 -52 35 3 -71 -11 37 -70 -75 53 16 -127 65 -23 -30 -43 -51 -83 49 14 -48 26 101 119 81 1 -14 -20 93 11 -31 -31 -28 -50 -39 -8 50 -25 -59 6 -22 59 10 -33 0 -4 -124 3 -16 -27 -127 31 37 53 -76 -33 -51 38 -30 60 -2 54 -2 40 -83 25 8 -43 -6 62 -71 49 -10 -9 -24 -13 -7 -25 -3 122 -76 14 -24 17 43 20 2 -82 47 108 -12 -1 63 -74 -79 4 12 -96 -12 -86 65 65 55 -3 -36 -65 -80 -5 26 -40 43 112 -78 10 41 82 60 -21 62 15 -51 -11 96 -18 45 -84 76 -69 -83 -34 34 38 -21 -86 35 5 4 -20 42 -18 -102 -18 -26 -77 33 -4 38 64 -16 41 51 30 -65 46 27 -55 -27 -6 -44 -79 -24 41 115 29 -56 -36 -55 -13 -31 -49 73 -6 -48 52 34 -47 -32 8 4 38 3 -36 82 -8 -96 71 -13 60 49 -73 -27 -35 -117 -29 -98 45 101 -59 -1 -56 -7 23 -57 87 8 -15 -71 75 34 -25 -31 -70 27 25 31 -55 -30 -70 -36 -16 -51 -81 29 80 24 -11 -14 31 -5 -57 -39 31 93 -41 80 12 35 85 -10 -37 -51 13 -40 -40 -4 -79 9 -57 -29 -57 -87 -12 20 -62 16 -19 24 15 -14 6 -31 78 6 -5 -40 -48 -103 5 21 -74 6 40 -95 12 21 7 -55 -70 -2 -10 47 35 48 -27 -30 -61 34 98 6 73 -58 -70 37 68 46 97 -31 -3 -41 -71 41 -105 -68 -85 19 -12 42 118 119 0 11 -55 -39 -5 -18 16 48 13 -30 50 -18 53 -23 -10 -25 90 -30 -18 14 -11 15 23 -3 46 8 -39 104 -32 -75 -48 47 10 41 31 47 -76 -28 -96 -12 8 -2 -13 -81 56 3 -21 3 5 14 -46 38 29 39 -26 -51 -11 90 99 23 -41 47 13 48 -49 9 -67 -24 0 61 39 -102 41 -1 -59 -25 -65 -62 75 74 4 19 -22 -8 -128 -63 -76 -79 -9 67 26 11 -74 68 -4 82 56 -105 -34 -83 2 9 -47 33 -26 -52 70 -16 44 -114 54 127 97 29 64 94 -18 118 19 -17 16 -71 25 10 11 -83 126 -57 61 -25 -116 -127 16 -43 -49 -25 41 8 79 37 49 64 -122 40 -90 -22 -127 -53 -4 22 -57 -72 -49 23 75 26 7 15 -15 -60 26 103 -40 -3 116 9 10 39 6 3 -57 -44 -56 44 -17 -32 -91 -1 50 47 -36 -34 -38 -96 -61 -127 -68 -4 -11 -94 100 54 69 58 92 3 -41 -110 -70 7 30 -13 42 2 -119 -1 50 7 14 100 30 -100 8 55 118 -21 1 -5 17 -70 118 -43 4 -69 8 -26 45 -11 -29 -60 -92 -128 -44 116 69 -39 -11 9 -1 96 -17 -58 -49 -34 -15 24 51 1 -98 0 43 26 -44 -11 19 -41 -75 -23 28 -4 55 -37 -23 77 -56 -57 98 36 -35 17 -52 -5 117 78 36 -12 -39 -1 75 -65 -53 -10 48 13 66 86 122 57 31 -45 -52 38 -38 -22 -128 -34 -89 -16 45 68 6 26 72 30 2 105 -27 21 -17 -77 65 10 40 55 -3 42 11 15 4 -13 -5 82 43 50 -41 -32 20 -25 -30 4 -38 62 4 29 99 -83 83 -65 -121 28 43 -65 24 26 27 12 37 -66 -42 36 -84 -38 63 -95 -24 91 -50 -126 -91 -88 -77 -86 18 -112 48 -12 -21 61 78 -3 17 -39 -26 -88 70 29 33 -8 75 -53 12 -32 -80 -36 0 16 -114 -91 6 28 12 -8 -46 -76 34 27 -71 18 10 -25 -124 -24 -72 125 30 39 69 -69 20 108 -95 5 -10 103 -13 -31 97 -109 -46 33 46 45 0 -29 -117 -106 -27 71 30 -48 -115 5 18 25 -77 -123 -2 18 40 -26 4 -61 -13 -26 -92 89 -40 -101 -48 -14 -78 -127 -65 91 127 -8 -127 31 83 -68 -73 10 -82 50 -111 -29 -71 20 -31 112 55 -41 49 3 -70 -25 -56 -34 -72 -3 -7 24 -22 -66 -16 -45 -38 -1 -18 -45 29 -41 -30 60 74 -126 94 38 -46 62 -25 -108 -27 86 40 27 -14 -1 -17 -65 6 -43 74 37 -5 8 -75 23 -80 -14 -39 -28 5 60 -34 -47 23 -16 -4 28 25 -32 -28 -17 19 8 -44 48 -30 -40 49 -1 29 -76 -17 -31 -2 13 15 5 118 28 83 80 29 105 -43 31 -4 48 68 59 35 58 10 -77 -128 -21 28 4 -55 -107 -3 47 42 -43 -43 33 127 35 -14 92 60 122 -57 16 -97 127 -7 -77 26 -5 -17 39 -3 -1 -100 14 -50 -13 -79 4 -14 -67 -22 42 6 52 -24 -24 -3 -47 -7 116 -52 -2 -106 58 125 6 -41 -20 -67 21 50 -94 -111 -57 23 -48 -72 -21 1 -117 -21 -27 9 75 -125 -54 33 -6 -5 0 -35 3 -27 -6 62 -16 -5 -39 -33 -3 -41 45 -13 -84 -20 9 10 -39 -42 -43 31 -30 -59 92 -51 -49 -17 103 -22 -69 9 -57 -12 -32 -19 73 -37 -126 47 19 -15 71 -53 43 25 -17 -61 18 -11 -16 45 -61 -42 -55 25 18 -57 118 -80 -12 -2 42 -45 17 -23 73 -50 46 47 -43 -21 10 -63 30 -69 -33 -64 -93 -24 -19 55 -27 -123 1 65 -2 27 -87 97 37 -97 -7 3 -114 -22 -51 -16 50 -17 5 -95 30 -68 14 35 -45 -47 -8 -23 -36 -68 -98 58 47 55 -7 38 59 -46 66 -15 25 -47 -11 -12 -58 -12 87 11 63 -20 -38 -51 5 -6 -89 -2 -44 -44 -3 -29 88 -18 -11 -60 -41 -123 4 8 -4 -8 -56 -64 -28 18 -87 4 2 -43 55 50 127 14 62 70 -4 -14 65 -24 -15 56 -17 -79 -36 80 -6 -18 59 -11 7 106 85 -20 108 -71 -33 -64 44 124 19 -63 -99 -51 -13 116 103 127 38 -24 -36 41 -2 86 94 -59 -72 26 -12 1 -10 -111 56 -13 127 -50 -96 38 -15 71 -120 55 -18 127 -55 -72 38 126 92 -12 38 -128 -13 15 -1 -78 -56 -76 -127 83 57 -47 -49 -69 24 -101 -87 -58 26 96 -68 13 -47 -14 23 -65 60 -44 26 -15 -54 -29 67 111 -65 19 -127 48 -1 -118 46 -20 -35 59 -37 -107 -23 -116 -68 -28 -115 18 -127 26 99 -9 11 -108 -90 -125 -128 -73 -5 4 -45 -72 67 22 -27 -122 -72 -123 -78 -118 62 116 107 53 -19 -118 -126 -127 -41 59 -72 63 -99 31 -126 18 19 -19 -112 91 -85 -28 -5 -59 31 -60 -125 -118 -8 -47 -74 -52 -98 -86 20 -40 -37 -18 -62 -10 -127 31 -33 -114 15 -92 31 127 -40 -32 -18 -54 127 -42 -36 -30 -22 -38 -108 15 -30 -59 -16 -3 104 -29 -88 11 -15 6 26 -65 30 58 -77 22 -12 63 -32 -59 -100 -38 -79 -70 -13 16 0 2 66 14 -51 61 -12 5 -50 7 -43 81 33 99 26 19 127 -36 -10 113 26 29 -62 0 -107 -65 52 -90 127 16 49 15 -22 62 12 50 -7 -65 18 -63 12 -85 16 -39 -20 -31 -63 -55 -90 -10 -96 -4 20 -38 -65 -122 -14 -3 0 125 -21 17 7 95 -10 -8 10 1 -49 11 -57 -2 21 122 81 46 9 -98 26 -35 18 9 -95 124 -70 30 7 -17 19 42 15 9 -128 -124 39 -112 58 2 -87 14 -123 -44 -128 3 8 85 97 -14 84 -12 80 -60 -17 17 -60 39 127 111 127 23 -16 -106 -16 18 -84 -61 -29 35 9 12 47 103 -40 -126 -14 86 -24 -108 100 21 -67 76 25 96 12 -128 125 -9 70 -68 -35 -63 -45 -7 -124 -97 -100 112 101 123 30 -45 -109 45 -114 -5 -44 33 13 20 -13 19 116 -26 16 -27 -60 -109 -38 59 -63 95 73 -20 -4 99 52 82 126 31 -59 54 -1 -16 59 12 28 -93 -5 5 -5 -121 -63 -47 -82 -72 -38 22 -72 29 121 -64 6 16 -24 66 -66 11 8 57 -110 67 -28 15 19 124 89 110 31 -38 44 123 124 58 61 10 -35 87 -98 55 36 60 49 91 23 66 -24 -51 35 127 32 -22 47 4 -24 23 113 127 58 83 -73 -37 -29 121 54 27 46 78 -78 86 107 18 -44 84 50 -30 51 -32 -21 71 -108 18 39 98 -4 -23 -55 -39 44 60 13 38 -109 -81 98 -116 -89 38 36 -81 -10 33 -47 26 -12 4 35 56 35 -42 -18 -51 -29 -17 11 -56 99 -48 62 -54 30 -98 52 35 -44 -95 -8 107 47 -24 37 34 -35 45 -64 -13 -95 25 48 -28 47 90 16 -34 -24 65 -32 25 88 -19 40 93 12 63 -21 12 84 83 32 103 -56 62 29 125 -3 -55 70 -5 37 22 -36 27 -17 -110 19 -87 -78 -102 55 13 -67 -14 103 -83 -57 9 -4 21 9 -116 49 84 -10 124 -7 -12 10 26 55 -31 -85 -53 0 127 75 127 127 31 -92 -51 51 -83 22 -67 5 69 -13 19 65 35 -40 -42 59 13 127 -30 21 37 -7 41 5 10 -24 19 63 45 25 -79 82 -22 -60 13 -57 8 -114 -15 56 10 30 43 -4 44 -51 -83 -54 -17 48 -10 6 -15 127 -49 -24 -45 -43 63 -49 49 24 -48 44 83 88 -11 43 -40 44 20 90 41 7 52 -101 49 121 49 -14 -79 75 17 -7 8 77 14 40 110 -38 51 29 67 -23 -32 -30 -5 101 111 -9 -29 -16 -39 -48 -58 14 14 25 -64 113 -68 -36 -57 25 -31 65 -53 29 22 23 -10 -59 -2 59 -64 108 10 -1 111 40 121 76 87 104 -77 83 90 124 -107 -63 17 38 15 97 125 105 -63 65 27 -8 37 28 -1 -80 51 -37 119 3 -62 4 61 -12 -58 -7 -105 56 -74 -9 59 -77 -6 2 110 -34 -116 31 20 11 -39 -91 -2 -55 24 -48 -21 -21 -22 77 -125 31 14 101 47 32 -20 20 39 43 -61 -35 -126 -29 55 -33 21 1 -9 32 -28 -43 18 87 -21 -77 -10 -66 -29 -68 -77 -2 76 -57 5 -21 -65 58 8 -40 26 -16 -3 19 93 -16 -8 127 -23 0 1 11 -111 -48 102 -22 -101 -18 -33 -19 -1 -13 -56 2 56 26 -10 -75 -35 11 -27 -4 -1 -60 -59 7 -21 -107 -21 -112 9 -82 -43 4 -22 55 57 -33 48 84 -50 41 25 -24 3 -58 17 36 -21 20 -19 21 -50 102 1 -106 -2 -74 34 32 -7 87 45 -35 39 -2 -9 40 -54 -19 39 40 -48 6 -25 -54 65 39 -34 31 -53 -14 -25 -50 -76 -32 -89 -78 -79 29 51 53 -71 38 -21 -91 -59 -39 -52 92 125 -19 -89 60 76 106 52 75 -67 121 22 69 100 -60 -88 80 29 127 76 51 40 -3 32 33 16 121 28 38 101 83 42 82 -4 -65 36 29 -19 33 18 26 32 -31 39 119 103 43 126 0 79 6 -26 -25 34 114 74 23 19 125 53 -16 54 -111 60 63 84 14 6 111 -69 71 32 -56 79 33 120 -13 19 96 104 71 34 -81 11 14 10 46 -40 10 43 4 51 47 63 62 75 79 61 75 -16 -83 61 98 -112 -28 18 47 -24 -5 37 -7 -19 35 73 -47 3 -53 -98 76 49 -74 -75 62 16 -30 124 -54 120 25 117 126 3 16 -55 45 3 -3 -23 -107 25 29 50 -72 36 43 52 -47 -45 26 44 59 98 37 44 44 101 -49 -31 -1 23 37 -16 -48 -14 -23 3 -14 22 17 49 68 56 67 -44 91 22 38 45 38 15 -21 37 4 15 73 50 -32 125 -7 -107 66 -1 32 -15 -12 -33 63 15 -8 5 -2 -65 -41 -126 31 112 63 109 -100 -93 48 -7 126 -7 -2 78 36 -23 36 79 15 82 55 32 -24 -24 124 -50 -83 -76 -26 -39 61 1 -3 -36 -93 -39 -50 -30 38 113 7 3 22 -6 74 36 47 -61 11 47 -68 104 0 -49 -81 75 -48 -109 -21 -100 -79 -12 -116 -17 -40 -41 -25 38 49 8 29 50 22 86 51 -87 3 -126 -128 -12 43 -83 82 127 69 43 -31 72 26 -10 -102 -2 22 31 31 -32 44 69 -31 127 63 -17 106 -36 37 11 74 -69 113 47 18 28 58 124 17 12 -49 2 110 27 9 56 -38 -15 -23 22 42 -17 14 -3 44 79 -8 77 -7 -92 50 37 -8 111 76 52 0 23 68 -21 2 82 12 46 45 -62 48 27 3 53 82 -44 -27 124 -10 46 -11 51 71 46 104 21 42 -31 37 37 105 56 -18 -17 -1 45 -2 53 49 41 124 68 20 8 33 44 40 -1 -9 -42 6 -7 -61 -21 3 24 -36 66 43 16 -15 -36 3 25 -49 -4 -24 29 10 64 30 30 -51 -3 46 -34 -40 -5 -14 7 27 35 -30 1 7 -16 44 33 8 -7 0 -17 -69 -36 3 1 1 8 4 28 45 -4 -31 -38 20 38 -28 44 -1 4 21 1 28 30 75 12 97 -3 -13 -43 -13 -33 7 47 -16 -10 -2 23 0 38 -58 56 -78 -31 48 -7 -8 -1 -10 -44 21 -7 39 99 86 -80 -41 -21 20 -27 59 -28 -63 -7 59 -20 42 -2 60 -3 -6 -75 -66 -10 -57 31 49 -58 26 -34 41 21 -64 4 3 -8 -9 -26 -11 -111 -36 51 -52 -59 53 85 18 -10 24 -13 88 52 -57 -86 3 -4 -59 16 -49 -39 -5 -33 -104 -27 62 11 -20 9 -8 -10 -120 8 -77 51 -63 -28 -3 37 28 22 49 -65 -18 -74 -23 13 17 0 15 127 -35 78 -78 14 -17 -2 -36 -14 45 127 29 37 37 73 14 69 -55 64 40 -96 -19 34 -11 -52 65 8 18 10 4 60 9 26 -99 63 51 -98 8 57 -89 -20 -71 -47 63 -5 -45 120 -17 -63 20 127 -62 -46 57 -27 -82 76 58 21 -111 8 -90 86 9 -91 20 47 59 -69 -13 58 -30 -15 7 -48 -29 120 118 -8 -31 66 0 -24 44 -119 10 -39 36 66 50 -25 -5 -20 -7 -63 101 123 -86 -58 79 -1 125 -39 94 33 17 65 65 -47 21 -7 -60 -35 -58 3 23 87 -36 59 -50 36 36 68 -7 -76 83 -2 -34 -69 -32 -40 41 5 -29 21 57 -53 -42 -51 -26 15 -82 42 30 -39 1 -5 -12 27 105 -12 -36 -26 15 -8 58 -83 0 47 -50 127 -112 5 -1 35 -111 -5 9 -40 10 43 -37 56 30 39 13 -10 -18 16 59 -37 105 -22 -40 19 -89 10 26 -8 -54 -34 72 -51 -53 68 -12 4 -70 55 40 -41 -41 -19 55 20 50 53 40 -18 24 -20 -61 2 -31 -89 -20 53 33 63 24 -88 100 51 72 49 37 -109 -15 -67 35 3 -18 -42 -93 63 4 2 -65 -12 -80 17 -96 26 21 69 -105 -61 106 -23 1 -74 68 -73 -16 -6 27 66 35 -54 23 3 -89 -71 -59 -71 -11 -17 27 0 -50 4 -18 8 -40 10 32 40 18 -5 53 -42 -45 -91 59 -6 -4 22 1 62 77 -12 24 -6 -125 126 -20 89 -45 67 -13 89 -25 -10 -30 36 108 -97 51 -10 -13 -20 41 -64 32 -28 -66 -2 3 2 -83 -42 -21 -41 -3 -6 -90 65 115 -84 -11 -109 22 -69 -56 34 101 -68 -8 -31 -65 -15 121 104 -87 12 -9 -4 -13 -97 93 3 -40 5 6 37 -59 -9 -126 35 -25 68 -30 92 47 5 -44 -25 -110 -98 46 76 -119 -22 -29 37 44 -35 -21 78 70 40 5 108 -13 55 14 72 -25 99 29 -28 10 11 12 -63 -19 -17 112 41 33 58 10 -37 99 13 -9 -15 12 -10 14 18 -9 -59 70 -92 0 4 27 69 -41 75 48 13 62 39 -49 60 -33 -5 24 13 -21 -49 -8 -70 34 126 25 -9 -29 -19 -39 2 75 -93 -56 34 -26 75 -65 -42 -45 102 -96 23 -14 -16 -41 -80 -66 -91 40 4 20 40 6 30 13 97 -34 -2 -40 8 -43 34 -29 66 -63 -26 4 -16 -66 -59 -44 73 -3 -39 -9 -12 -27 32 -22 -47 -56 -2 13 -26 91 30 -94 -69 -45 100 69 35 34 101 92 -36 59 4 -27 -17 21 -100 122 80 -48 28 127 8 60 37 32 -124 -17 -10 -41 5 -124 33 101 32 22 -7 45 -36 28 -11 -39 108 118 -4 2 52 41 -45 -53 34 -121 -78 18 -81 23 3 -44 -30 -29 -43 -80 69 50 26 -25 21 -5 117 -12 -79 -39 43 -91 -22 0 -44 61 -70 30 63 -63 40 -77 -7 -45 -29 6 -28 -19 -14 18 -32 -43 -51 106 6 -120 6 35 -22 12 -28 -4 -16 -101 -27 -125 -98 -44 -41 31 -61 38 -7 20 -16 -18 47 -6 -33 76 -40 -32 12 -5 -63 41 21 70 15 -12 -67 20 -19 125 -98 81 -47 -59 -39 -1 35 106 28 -9 67 0 1 46 -22 -94 2 17 -14 3 75 96 37 38 -23 82 -106 -37 7 -5 -73 -5 2 56 9 34 -6 -17 41 54 -24 -70 -87 23 -25 -42 -84 -92 -3 -57 12 -43 44 51 -70 -17 -18 -71 -58 42 52 -9 -14 -100 -11 -1 -67 -33 91 99 1 41 -35 -101 -123 2 8 31 125 23 28 35 -55 -5 15 -27 126 -46 16 54 74 50 -18 24 -37 14 -13 -39 -46 9 -54 60 55 -13 -90 -24 27 2 -6 27 -44 -23 5 9 9 -101 -4 44 108 10 55 -52 -10 -68 -8 -126 -61 -12 5 39 -17 -9 -21 26 46 -30 99 -3 78 -6 33 -8 -12 -61 -62 -23 0 -49 17 10 -7 -63 -51 5 -12 -59 22 45 -14 50 -2 -16 22 -35 9 -31 -54 -8 10 7 13 -11 55 1 45 -34 -20 20 43 0 127 39 47 -5 71 36 -18 46 37 47 -27 -68 -11 -2 5 103 53 90 0 39 -9 34 -48 -11 -10 42 59 30 61 39 27 -23 -7 54 -41 -58 -70 -24 -46 -15 -34 -36 0 -4 51 20 -10 40 50 -32 24 19 -37 125 -24 14 -89 -36 5 61 101 -45 32 70 -122 9 39 -4 65 -110 -39 -52 27 38 65 -9 81 -121 6 -83 9 123 38 116 -14 16 -15 14 -120 -13 -13 56 127 -43 57 103 61 18 26 26 -35 127 72 -80 35 -78 -10 -55 -85 116 6 9 -69 -44 -79 -44 -8 -38 67 124 -11 -7 -51 31 105 4 -6 119 49 112 91 62 -75 47 72 76 -68 79 -80 -16 8 77 48 29 -24 -7 30 -61 5 66 -59 30 -14 45 -49 -6 61 -113 -10 -116 -113 57 -2 -74 19 48 -108 -6 70 -112 102 -116 78 37 -30 -69 44 -101 28 31 44 103 -63 2 11 -58 -17 9 -67 -6 -51 22 70 -35 39 66 73 26 -26 96 14 6 57 0 -7 -50 34 53 -49 -47 -61 26 112 6 -38 9 -19 20 -93 97 69 -1 33 -33 -58 -14 64 -76 72 -20 -24 57 1 -123 -126 -45 98 52 -60 65 -10 -115 -61 34 20 12 46 -67 74 97 79 20 12 36 -47 -93 5 -44 38 -15 112 -70 -52 21 2 -18 39 91 18 74 -62 -88 -69 -36 46 -58 -27 -80 -45 70 56 -127 -108 -47 -5 22 2 -32 -90 -8 34 34 -48 -68 -45 36 -4 16 48 124 -37 64 -62 13 -83 -32 72 127 -70 21 21 -4 -99 -93 -38 -123 39 -32 34 63 -83 17 -54 12 29 -34 -2 -21 -67 4 46 11 -38 11 59 -25 48 28 -36 -10 26 -16 -125 -87 -37 18 -26 79 -25 -34 6 -44 65 44 -30 -8 -22 -6 -6 -33 12 40 9 -66 21 27 10 -45 -49 53 -82 -6 -56 -47 -39 -28 -45 -35 -110 -5 57 -10 -19 -125 -69 31 -13 -55 -58 -40 71 11 -83 45 -33 32 -30 -45 -44 -128 101 -6 126 -55 -58 71 66 28 -11 12 -88 -70 4 -26 -61 -53 4 26 20 -70 11 22 -75 -110 6 35 17 50 -83 -7 -44 -35 -73 -12 41 64 -40 10 -102 -25 37 49 -15 -97 -64 -58 53 -51 -5 120 62 26 22 -28 15 -19 -20 -33 57 51 -7 109 -50 -66 6 -99 40 66 5 56 -113 13 -42 51 -67 38 -32 104 26 36 66 -23 -76 -13 -28 -40 28 5 -57 -18 29 66 -76 -34 -16 48 -71 -27 -2 -43 67 30 23 28 -16 39 31 6 -55 39 25 -83 -4 -12 -55 -51 13 -38 -24 21 -30 20 -57 -22 19 -7 -18 20 24 3 15 -55 59 10 5 -10 58 -49 67 105 -47 8 -5 10 50 -95 -67 -68 -61 -114 16 -58 2 41 39 107 -34 -30 -87 -40 -12 10 18 -9 27 -127 -25 115 -30 -46 38 63 2 6 -85 -50 -17 -13 80 35 -27 18 -47 37 -47 -23 -89 -37 2 -5 -21 -3 -31 -10 -47 -2 -1 69 -97 -31 1 -29 49 -36 -81 35 16 3 -29 -13 -56 62 80 -29 -46 14 -38 -14 -110 51 -56 45 50 7 -79 44 0 19 56 -84 -23 -39 -59 -31 27 -34 -49 -8 111 -9 -16 66 15 -15 -59 -23 -16 13 20 -94 -17 18 -27 32 -78 60 40 53 29 -98 33 96 -52 -89 -46 0 11 11 -52 21 6 -50 19 24 121 -40 -40 -22 -2 56 65 -11 117 78 -7 -88 5 -15 -11 -84 71 12 -40 -43 -28 -127 24 15 -65 15 -42 -73 31 -112 10 -98 -67 -52 -40 34 4 94 15 -44 -49 -4 42 43 121 -21 -8 75 30 29 -7 48 -45 31 35 8 57 63 -64 26 15 33 -68 60 9 -15 -47 -4 77 61 31 68 -13 68 -59 8 46 92 -28 16 90 -1 35 -73 126 -43 39 -56 22 12 -3 -33 -113 -3 22 3 17 -28 -95 48 42 25 -49 15 34 122 123 122 23 38 63 74 48 40 59 20 70 -85 14 84 52 24 -91 -34 18 -34 -41 29 0 -17 37 -37 72 34 23 27 50 90 -2 69 60 -128 20 -82 54 -5 1 -48 -3 1 41 35 -15 -15 78 -33 27 75 -19 23 0 52 74 -19 -7 46 7 24 40 -35 2 -14 -39 108 13 66 49 -19 -80 -13 48 -65 41 -12 -91 -7 -12 -51 -78 57 20 -17 76 -96 -20 37 -17 -60 28 14 -44 -28 -102 7 -113 29 -109 -45 -37 -49 -9 -54 37 -21 7 55 44 1 -52 32 -12 -103 -45 -19 23 40 9 36 80 8 86 50 -48 -20 -24 -20 1 36 -26 35 -55 3 8 -74 5 -37 122 -23 -37 1 75 -47 -1 30 -38 -2 -48 -66 3 17 -23 24 122 -103 -39 -120 -23 40 -3 26 55 -50 -75 -2 118 -47 -24 2 -50 55 -35 -48 -105 75 8 89 32 19 -7 39 -78 40 -7 -26 6 -123 -123 -5 -31 120 21 -76 -16 -77 11 45 -3 16 34 127 36 -34 10 1 -47 -9 -121 -13 -52 84 -17 -15 -70 -35 -124 -67 -8 -78 -22 -118 -80 72 -37 49 -34 3 -124 -75 -59 -54 -58 -78 35 -122 69 -52 27 -39 53 -62 -5 80 -20 -96 -116 49 -31 -126 25 22 54 -18 1 19 -17 -97 -6 53 -80 56 -117 67 -40 32 -50 12 3 -43 11 2 86 38 22 -90 -22 23 -65 87 -9 -40 118 86 -83 -18 -28 -47 44 2 -97 9 14 -68 69 54 99 62 8 65 22 55 66 13 -39 -30 -28 2 127 -13 51 -15 4 -53 -121 78 -45 -43 -56 -29 -17 -11 -41 50 24 43 15 -66 -39 -25 -55 -20 50 86 31 -10 15 -40 -17 47 64 -70 12 14 -22 -15 20 -45 -44 -26 -6 -2 -7 3 39 -25 -90 26 -54 96 33 83 11 -7 -77 24 47 -3 -18 -11 -40 68 1 38 33 -15 61 -17 -91 36 118 -61 3 -64 -75 -37 39 -41 6 -115 22 -56 2 87 -54 -73 -57 23 -51 -88 -84 -48 -62 -52 10 -21 24 2 10 -54 -10 61 -6 -6 -73 126 22 78 -102 -38 -57 68 119 -26 -69 0 55 14 +62 59 -30 -12 -24 -55 36 -13 30 46 -48 102 -71 -12 17 54 -18 39 25 -29 25 87 68 -17 10 -45 -7 86 36 21 108 -69 15 -61 50 13 -5 36 -25 25 25 -37 29 0 -105 74 47 -11 90 42 54 5 -28 -10 4 28 -95 98 40 80 -55 -19 23 -38 58 68 12 12 118 -58 46 -11 24 64 61 44 38 58 -42 58 -4 89 53 31 -19 5 47 58 -10 43 -11 47 56 49 -29 48 -16 16 -6 -27 -1 35 13 -33 27 13 37 -38 31 44 -15 13 -46 4 32 -34 8 -12 22 -24 46 -23 19 -2 22 20 5 -32 -46 2 7 8 3 -26 52 37 33 33 40 22 9 6 -19 -45 4 22 -12 -17 33 31 46 10 33 -6 -25 20 -15 -35 29 30 -1 -11 -52 7 17 -5 15 31 -9 -4 -19 -8 -3 33 22 -3 18 -18 17 -2 15 -3 28 -11 5 24 -15 4 -32 12 -18 8 -1 1 6 -15 -83 -46 -44 28 -2 -7 -87 -14 -14 23 9 22 19 25 -42 22 -24 -23 10 -21 -13 4 -12 8 -14 -51 25 -5 -21 -16 -5 7 -14 39 -37 23 -33 -39 -29 23 -54 13 3 8 -7 7 37 2 56 21 10 -27 14 -19 -4 25 -23 0 -5 -3 29 0 -3 -26 33 0 -40 -15 49 -9 2 2 -30 2 -6 32 -8 -3 6 -16 8 0 15 7 37 -1 20 -5 22 -1 16 -59 +96 22 1 +39 16 10 11 19 15 15 10 7 6 5 4 2 4 -3 -1 13 34 49 44 43 84 -34 -24 -15 -10 -22 -26 -37 -29 -7 7 22 15 9 17 27 29 8 0 -5 7 4 -53 21 25 37 44 38 46 55 63 49 41 46 28 30 37 43 49 46 28 3 3 2 3 10 -11 -12 -8 7 20 17 10 5 13 17 27 38 48 61 67 58 46 36 40 30 28 -23 -23 -20 -16 -7 -7 -13 -22 -25 -33 -45 -46 -56 -67 -78 -101 -123 -124 -107 -112 -99 -58 -61 -28 3 20 29 30 17 -18 -65 -86 -77 -67 -39 -29 -30 -18 -18 -21 -22 -41 -75 -90 26 8 -28 -62 -81 -84 -70 -60 -57 -61 -54 -64 -63 -60 -41 -20 -6 -5 -2 -2 19 21 -14 7 25 37 43 36 28 22 23 18 16 14 20 13 2 -3 -9 -23 -4 4 15 16 1 13 15 5 4 9 5 0 4 18 19 18 20 17 9 11 20 45 73 89 104 68 92 53 31 24 16 10 14 15 22 22 26 19 5 3 4 15 14 6 -22 -16 7 7 7 2 21 30 38 51 70 81 87 94 96 99 102 109 108 110 116 101 62 72 67 22 10 11 8 2 6 11 11 10 12 12 10 14 18 11 2 -35 -80 -128 -128 -128 -77 -7 -17 -34 -46 -27 -33 -41 -41 -40 -48 -53 -61 -62 -58 -57 -51 -41 -36 -37 -32 -35 -37 -24 -10 -11 4 13 10 5 2 -1 7 10 0 14 26 34 42 55 52 67 45 45 59 107 -47 -85 -100 -65 -25 -13 1 1 -5 -7 -10 5 9 14 27 35 35 8 0 12 25 20 17 23 25 21 16 15 18 17 21 25 36 43 50 54 49 45 42 33 24 29 30 -1 -62 -36 -28 7 28 35 40 54 64 62 50 50 43 32 7 -3 -2 3 18 16 24 31 13 10 5 16 26 25 25 27 32 32 38 39 41 35 28 17 15 23 25 6 -6 -46 38 38 43 44 59 85 104 119 111 112 113 115 104 96 92 71 60 58 52 50 48 18 18 27 4 27 42 48 34 47 64 75 71 69 69 85 102 122 125 112 127 127 112 81 -77 -68 -47 -30 -19 -20 -16 -17 -15 -9 -12 -10 -10 -7 -13 -26 -29 -22 -25 -15 -11 -16 12 14 29 39 46 50 47 35 37 52 47 48 48 52 47 15 20 66 83 79 71 69 -19 0 12 10 5 12 11 15 18 17 20 25 22 17 12 3 0 -1 4 15 16 -1 127 105 62 44 39 37 26 19 32 38 27 39 38 40 35 31 17 11 30 59 92 56 1 9 16 14 19 16 18 11 9 3 -2 1 6 10 8 8 16 22 19 17 19 29 39 30 -4 -2 22 30 17 2 9 24 -1 18 24 31 12 2 -5 -4 10 15 11 -12 -58 -75 -81 -92 -62 -47 -44 -32 -24 -21 -19 -17 -8 0 11 18 16 5 -2 0 -16 24 -61 -32 1 4 4 4 13 13 20 15 6 4 -2 -7 -11 -4 -2 -11 -14 -21 -29 -23 -13 -13 5 7 4 10 10 10 15 13 4 10 0 2 3 12 7 -30 -31 -28 -35 -66 -11 -2 22 29 40 46 39 39 46 42 41 52 49 39 39 39 33 31 16 14 11 3 37 32 -9 -18 -30 -43 -42 -40 -41 -42 -34 -40 -42 -36 -33 -39 -30 20 24 -2 -42 -68 14 5 -5 -26 -32 -37 -40 -37 -42 -53 -63 -75 -80 -79 -70 -67 -63 -62 -80 -69 -52 -27 32 20 -2 -30 -39 -24 -20 -27 -31 -18 -19 -12 -17 -23 -33 -26 -14 -3 -3 -8 -4 39 -8 -31 -55 -41 -34 -30 -29 -28 -21 -4 7 7 17 32 30 24 28 52 51 42 29 21 18 -1 -20 -16 -10 -11 -19 -17 -13 -13 -13 -8 -4 2 19 20 -2 -89 -128 -108 -42 -6 127 127 75 25 13 15 9 0 5 17 27 32 35 39 45 47 37 14 29 29 15 -5 -18 -55 -67 -47 -21 -12 -16 -24 -21 -28 -33 -16 -19 -22 -14 -7 -13 2 24 32 32 13 -62 -79 -80 -88 -95 -89 -85 -67 -52 -38 -31 -28 -31 -23 -12 -6 -15 -26 -31 -20 -7 -11 15 -22 -29 -15 -15 -23 -37 -39 -36 -33 -32 -23 -28 -25 -20 -16 -16 -17 -3 7 16 20 87 70 31 -5 -4 -7 -33 -65 -73 -69 -72 -30 -19 -36 -72 -82 -54 -5 10 7 23 5 -18 -9 18 32 42 39 36 35 34 38 37 41 47 51 60 59 57 27 3 13 -8 -126 -64 -35 -17 -32 -42 -31 -31 -20 -30 -39 -37 -43 -42 -43 -48 -63 -62 -58 -65 -46 -24 -44 28 18 18 23 29 33 35 31 21 10 2 2 6 12 15 15 18 -20 -87 -79 -56 -33 30 27 22 29 39 48 63 60 34 17 20 20 21 21 24 31 26 13 1 10 14 23 47 62 72 97 81 73 42 48 85 127 127 127 127 127 118 74 61 43 24 38 31 16 1 -34 -55 -46 -44 -37 -28 -15 -8 -10 -12 -15 -10 -7 -7 -3 1 -3 6 25 47 124 -31 -52 -56 -52 -43 -41 -40 -35 -24 -27 -29 -29 -16 -10 -8 9 33 25 10 2 -19 -115 40 48 44 53 31 17 38 40 33 18 16 21 33 43 40 36 37 24 25 24 25 20 3 24 38 26 16 7 3 6 15 15 20 19 10 -1 1 -1 1 5 9 18 9 16 44 62 46 41 43 58 71 72 68 73 81 86 100 102 87 78 66 60 97 120 127 118 -27 -40 -41 -43 -53 -51 -53 -60 -53 -43 -45 -36 -40 -50 -71 -93 -93 -78 -82 -65 -53 -85 -27 14 52 95 117 115 96 67 36 12 -3 2 12 8 -4 -17 -17 -16 -21 -7 -1 -26 -46 -60 -78 -96 -96 -86 -80 -68 -64 -63 -58 -64 -62 -51 -29 -15 -19 -4 0 -23 -49 -55 -25 -26 -17 -6 -1 0 2 1 -2 4 15 29 36 36 25 20 25 32 30 28 20 -3 3 16 14 16 7 -7 -9 -12 1 8 19 26 26 19 5 -7 -26 -40 -60 -106 -127 -128 4 12 16 21 16 11 15 12 -5 -22 -34 -48 -57 -72 -82 -79 -68 -59 -44 -25 -13 -4 30 15 24 41 49 54 56 56 54 51 40 42 41 35 28 23 21 19 14 15 3 2 -4 -29 -55 -67 -70 -77 -74 -80 -90 -89 -91 -88 -84 -74 -45 -34 -21 -11 12 12 25 27 -54 -36 -46 -45 -39 -38 -48 -59 -56 -41 -25 -7 -5 -1 1 0 -5 0 9 9 -6 -19 3 19 22 15 18 10 13 6 -13 -38 -62 -75 -91 -107 -128 -128 -128 -128 -116 -127 -119 -76 12 26 51 74 81 76 65 62 57 65 68 65 70 64 51 45 46 54 45 39 32 20 -74 -60 -49 -49 -59 -63 -61 -55 -56 -56 -54 -48 -48 -48 -50 -50 -51 -58 -46 -33 -39 -42 22 11 -17 -43 -48 -50 -37 -27 -18 -18 -18 -19 -20 -22 -22 -25 -21 -19 -10 -9 1 7 2 -28 -57 -60 -44 -40 -35 -28 -41 -57 -52 -66 -52 -49 -63 -60 -31 -4 -16 -41 -45 -81 -104 -76 -45 -36 -32 -30 -28 -28 -34 -35 -43 -33 -38 -35 -32 -31 -38 -67 -81 -49 -15 46 8 -8 -20 -15 -2 -5 -11 -7 7 13 9 2 -13 -32 -60 -76 -91 -107 -124 -104 -66 -25 -4 -28 -30 -26 -7 7 5 -13 -40 -64 -89 -81 -65 -56 -40 -31 -28 -21 -10 -11 3 -14 -25 -57 -79 -68 -69 -63 -62 -56 -32 -23 -29 -23 -26 -22 -28 -35 -40 -41 -47 -53 -62 -71 -23 -21 -12 -35 -50 -61 -67 -79 -80 -57 -28 -21 -3 14 25 23 18 -4 -60 -53 -29 -25 -40 -37 -48 -45 -44 -39 -35 -36 -47 -49 -26 -25 -18 -13 3 6 7 14 33 27 31 43 0 -3 7 -3 -7 -3 3 12 16 20 27 27 33 34 43 60 62 -22 -124 -128 -128 -127 -13 -19 -17 -10 -9 -16 -11 -2 3 -1 2 -2 0 3 13 20 19 9 1 -8 -31 -18 -8 -18 0 13 37 51 58 73 72 54 30 16 17 12 4 0 4 8 20 0 -17 -31 48 23 11 10 27 46 55 43 33 33 34 40 39 39 43 47 43 3 -20 12 61 80 59 33 0 -6 -6 -6 -14 -8 10 20 21 30 34 27 19 17 25 41 53 72 82 56 33 36 31 32 54 65 58 60 54 42 41 31 16 1 8 19 27 39 47 45 35 33 110 127 49 36 43 45 44 49 45 53 49 67 59 62 71 74 73 68 62 76 80 90 57 88 98 80 53 26 19 14 12 15 19 18 23 33 36 35 35 37 27 6 3 3 28 34 31 41 41 43 36 27 34 41 26 37 34 33 41 42 42 39 29 30 33 37 -5 38 76 105 107 87 75 66 62 62 55 32 19 -1 -35 -55 -58 -61 -37 -19 -9 -17 -44 -50 -58 -42 -39 -37 -34 -36 -47 -37 -33 -41 -38 -34 -40 -46 -46 -51 -49 -53 -84 -128 -16 -75 -79 -36 -5 -2 -4 -6 -13 -31 -43 -46 -46 -44 -26 -12 -11 -10 10 38 59 71 -77 -82 -101 -119 -128 -128 -128 -128 -125 -113 -106 -88 -89 -88 -82 -77 -74 -64 -44 -32 -13 -13 -15 15 13 0 -21 -31 -34 -41 -47 -44 -40 -40 -48 -52 -50 -53 -59 -66 -68 -79 -97 -42 69 104 90 59 25 13 8 21 13 15 28 22 25 32 27 19 8 3 0 1 1 -16 78 76 -9 -11 3 10 12 1 -13 -11 -16 -23 -28 -30 -36 -40 -40 -48 -56 -63 -60 -25 -65 -51 -43 -33 -31 -25 -24 -22 -13 0 -2 -6 -11 -3 -7 -9 -16 -23 -19 -31 -28 3 44 33 25 32 33 34 35 40 47 49 52 52 46 40 36 35 26 16 30 40 59 57 29 37 29 46 43 38 40 45 45 48 44 33 19 8 4 0 4 13 7 19 3 -38 12 19 19 3 14 27 34 29 21 17 2 1 1 -4 -16 -19 -9 14 14 10 -13 -12 40 42 56 41 27 31 38 35 28 27 18 22 33 36 35 35 32 26 19 20 19 12 -84 -34 -22 -14 -7 -8 -16 -12 -8 -4 2 -1 -5 -6 -4 -17 -34 -53 -40 -18 3 10 10 -19 -66 -66 -57 -64 -83 -93 -91 -98 -108 -101 -104 -107 -112 -120 -104 -36 43 33 -8 -128 30 10 -34 -40 -50 -51 -47 -34 -29 -29 -30 -29 -38 -38 -39 -41 -40 -51 -46 -24 -26 -29 43 38 27 22 17 17 15 17 12 11 15 16 13 12 14 19 22 36 33 24 11 -20 -108 -128 -127 -122 -100 -87 -79 -74 -83 -78 -65 -62 -57 -40 -25 -19 -18 -18 -15 -5 6 -8 +-3 22 24 47 33 34 8 3 -7 -8 4 4 18 20 11 -16 -26 -12 -34 -23 -19 -13 +24 1 1 +-128 -35 127 127 -99 -128 127 -128 -125 123 -103 127 127 -128 127 -128 -128 -128 93 -114 -128 -128 127 -128 +127 diff --git a/audio b/audio new file mode 100644 index 0000000..1b76cc7 --- /dev/null +++ b/audio @@ -0,0 +1,30 @@ +gpt的 +"-af", +"arnndn=m=./arnndn-models/cb.rnnn:mix=0.88," # 人声增强 +"afftdn=nf=-24:tn=1," # FFT 降噪,抹平音乐中频 +"highpass=f=110,lowpass=f=4800," # 切掉低音鼓/高频镲 +"equalizer=f=250:width_type=o:width=2:g=-9," # 衰减低中频,音乐基础弱化 +"equalizer=f=800:width_type=o:width=1.5:g=-6," # 再衰减中频,人声外区域 +"atempo=1.03," # 轻微加速 3%,节奏轻微错位 +"asetrate=48000*1.02,aresample=48000," # 音高上移 ~1/2 半音 +"afreqshift=shift=20," # 整体频率偏移 20Hz,破坏指纹 +"acompressor=threshold=-28dB:ratio=5:attack=8:release=80:makeup=5," # 压缩动态,音乐更“扁” +"volume=1.15," # 补偿整体音量 +"aresample=async=1:first_pts=0" # 保证音视频同步 + + + +grok的 +"-af", "arnndn=m=./arnndn-models/cb.rnnn:mix=0.88," +"afftdn=nf=-24:tn=1," # 强FFT降噪,进一步抹平音乐中频 +"highpass=f=110,lowpass=f=4800," # 切掉低音鼓/高频镲,音乐能量大减 +"equalizer=f=250:width_type=o:width=2:g=-9," # 衰减低中频(很多音乐基础) +"equalizer=f=800:width_type=o:width=1.5:g=-6," # 再衰减中频人声外区域 +"atempo=1.06," # 轻微加速6%,节奏错位 +"asetrate=48000*1.04,aresample=48000," # 音高上移约3/4半音 +"aphaser=type=t:decay=0.5:speed=0.4:delay=2," # 相位扫,谱图模糊 +"flanger=delay=4:depth=3:regen=35:width=60:speed=0.35," # 轻法兰杰,增加金属/扫频感 +"afreqshift=shift=45," # 整体频率偏移45Hz,进一步乱指纹 +"acompressor=threshold=-28dB:ratio=5:attack=8:release=80:makeup=5," # 压缩动态,让音乐更“扁” +"volume=1.15," # 补偿整体音量损失 +"aresample=async=1:first_pts=0" \ No newline at end of file diff --git a/backup/msg_push.py b/backup/msg_push.py new file mode 100644 index 0000000..c682b20 --- /dev/null +++ b/backup/msg_push.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- + +""" +Author: Hmily +GitHub: https://github.com/ihmily +Date: 2023-09-03 19:18:36 +Update: 2025-01-23 17:16:12 +Copyright (c) 2023-2024 by Hmily, All Rights Reserved. +""" +from typing import Dict, Any +import json +import base64 +import urllib.request +import urllib.error +import smtplib +from email.header import Header +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +no_proxy_handler = urllib.request.ProxyHandler({}) +opener = urllib.request.build_opener(no_proxy_handler) +headers: Dict[str, str] = {'Content-Type': 'application/json'} + + +def dingtalk(url: str, content: str, number: str = None, is_atall: bool = False) -> Dict[str, Any]: + success = [] + error = [] + api_list = url.replace(',', ',').split(',') if url.strip() else [] + for api in api_list: + json_data = { + 'msgtype': 'text', + 'text': { + 'content': content, + }, + "at": { + "atMobiles": [ + number + ], + "isAtAll": is_atall + }, + } + try: + data = json.dumps(json_data).encode('utf-8') + req = urllib.request.Request(api, data=data, headers=headers) + response = opener.open(req, timeout=10) + json_str = response.read().decode('utf-8') + json_data = json.loads(json_str) + if json_data['errcode'] == 0: + success.append(api) + else: + error.append(api) + print(f'钉钉推送失败, 推送地址:{api}, {json_data["errmsg"]}') + except Exception as e: + error.append(api) + print(f'钉钉推送失败, 推送地址:{api}, 错误信息:{e}') + return {"success": success, "error": error} + + +def xizhi(url: str, title: str, content: str) -> Dict[str, Any]: + success = [] + error = [] + api_list = url.replace(',', ',').split(',') if url.strip() else [] + for api in api_list: + json_data = { + 'title': title, + 'content': content + } + try: + data = json.dumps(json_data).encode('utf-8') + req = urllib.request.Request(api, data=data, headers=headers) + response = opener.open(req, timeout=10) + json_str = response.read().decode('utf-8') + json_data = json.loads(json_str) + if json_data['code'] == 200: + success.append(api) + else: + error.append(api) + print(f'微信推送失败, 推送地址:{api}, 失败信息:{json_data["msg"]}') + except Exception as e: + error.append(api) + print(f'微信推送失败, 推送地址:{api}, 错误信息:{e}') + return {"success": success, "error": error} + + +def send_email(email_host: str, login_email: str, email_pass: str, sender_email: str, sender_name: str, + to_email: str, title: str, content: str, smtp_port: str = None, open_ssl: bool = True) -> Dict[str, Any]: + receivers = to_email.replace(',', ',').split(',') if to_email.strip() else [] + + try: + message = MIMEMultipart() + send_name = base64.b64encode(sender_name.encode("utf-8")).decode() + message['From'] = f'=?UTF-8?B?{send_name}?= <{sender_email}>' + message['Subject'] = Header(title, 'utf-8') + if len(receivers) == 1: + message['To'] = receivers[0] + + t_apart = MIMEText(content, 'plain', 'utf-8') + message.attach(t_apart) + + if open_ssl: + smtp_port = int(smtp_port) or 465 + smtp_obj = smtplib.SMTP_SSL(email_host, smtp_port) + else: + smtp_port = int(smtp_port) or 25 + smtp_obj = smtplib.SMTP(email_host, smtp_port) + smtp_obj.login(login_email, email_pass) + smtp_obj.sendmail(sender_email, receivers, message.as_string()) + return {"success": receivers, "error": []} + except smtplib.SMTPException as e: + print(f'邮件推送失败, 推送邮箱:{to_email}, 错误信息:{e}') + return {"success": [], "error": receivers} + + +def tg_bot(chat_id: int, token: str, content: str) -> Dict[str, Any]: + try: + json_data = { + "chat_id": chat_id, + 'text': content + } + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps(json_data).encode('utf-8') + req = urllib.request.Request(url, data=data, headers=headers) + response = urllib.request.urlopen(req, timeout=15) + json_str = response.read().decode('utf-8') + _json_data = json.loads(json_str) + return {"success": [1], "error": []} + except Exception as e: + print(f'tg推送失败, 聊天ID:{chat_id}, 错误信息:{e}') + return {"success": [], "error": [1]} + + +def bark(api: str, title: str = "message", content: str = 'test', level: str = "active", + badge: int = 1, auto_copy: int = 1, sound: str = "", icon: str = "", group: str = "", + is_archive: int = 1, url: str = "") -> Dict[str, Any]: + success = [] + error = [] + api_list = api.replace(',', ',').split(',') if api.strip() else [] + for _api in api_list: + json_data = { + "title": title, + "body": content, + "level": level, + "badge": badge, + "autoCopy": auto_copy, + "sound": sound, + "icon": icon, + "group": group, + "isArchive": is_archive, + "url": url + } + try: + data = json.dumps(json_data).encode('utf-8') + req = urllib.request.Request(_api, data=data, headers=headers) + response = opener.open(req, timeout=10) + json_str = response.read().decode("utf-8") + json_data = json.loads(json_str) + if json_data['code'] == 200: + success.append(_api) + else: + error.append(_api) + print(f'Bark推送失败, 推送地址:{_api}, 失败信息:{json_data["message"]}') + except Exception as e: + error.append(api) + print(f'Bark推送失败, 推送地址:{_api}, 错误信息:{e}') + return {"success": success, "error": error} + + +def ntfy(api: str, title: str = "message", content: str = 'test', tags: str = 'tada', priority: int = 3, + action_url: str = "", attach: str = "", filename: str = "", click: str = "", icon: str = "", + delay: str = "", email: str = "", call: str = "") -> Dict[str, Any]: + success = [] + error = [] + api_list = api.replace(',', ',').split(',') if api.strip() else [] + tags = tags.replace(',', ',').split(',') if tags else ['partying_face'] + actions = [{"action": "view", "label": "view live", "url": action_url}] if action_url else [] + for _api in api_list: + server, topic = _api.rsplit('/', maxsplit=1) + json_data = { + "topic": topic, + "title": title, + "message": content, + "tags": tags, + "priority": priority, + "attach": attach, + "filename": filename, + "click": click, + "actions": actions, + "markdown": False, + "icon": icon, + "delay": delay, + "email": email, + "call": call + } + + try: + data = json.dumps(json_data, ensure_ascii=False).encode('utf-8') + req = urllib.request.Request(server, data=data, headers=headers) + response = opener.open(req, timeout=10) + json_str = response.read().decode("utf-8") + json_data = json.loads(json_str) + if "error" not in json_data: + success.append(_api) + else: + error.append(_api) + print(f'ntfy推送失败, 推送地址:{_api}, 失败信息:{json_data["error"]}') + except urllib.error.HTTPError as e: + error.append(_api) + error_msg = e.read().decode("utf-8") + print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{json.loads(error_msg)["error"]}') + except Exception as e: + error.append(api) + print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{e}') + return {"success": success, "error": error} + + +def pushplus(token: str, title: str, content: str) -> Dict[str, Any]: + """ + PushPlus推送通知 + API文档: https://www.pushplus.plus/doc/ + """ + success = [] + error = [] + token_list = token.replace(',', ',').split(',') if token.strip() else [] + + for _token in token_list: + json_data = { + 'token': _token, + 'title': title, + 'content': content + } + + try: + url = 'https://www.pushplus.plus/send' + data = json.dumps(json_data).encode('utf-8') + req = urllib.request.Request(url, data=data, headers=headers) + response = opener.open(req, timeout=10) + json_str = response.read().decode('utf-8') + json_data = json.loads(json_str) + + if json_data.get('code') == 200: + success.append(_token) + else: + error.append(_token) + print(f'PushPlus推送失败, Token:{_token}, 失败信息:{json_data.get("msg", "未知错误")}') + except Exception as e: + error.append(_token) + print(f'PushPlus推送失败, Token:{_token}, 错误信息:{e}') + + return {"success": success, "error": error} + + +if __name__ == '__main__': + send_title = '直播通知' # 标题 + send_content = '张三 开播了!' # 推送内容 + + # 钉钉推送通知 + webhook_api = '' # 替换成自己Webhook链接,参考文档:https://open.dingtalk.com/document/robots/custom-robot-access + phone_number = '' # 被@用户的手机号码 + is_atall = '' # 是否@全体 + # dingtalk(webhook_api, send_content, phone_number) + + # 微信推送通知 + # 替换成自己的单点推送接口,获取地址:https://xz.qqoq.net/#/admin/one + # 当然也可以使用其他平台API 如server酱 使用方法一样 + xizhi_api = 'https://xizhi.qqoq.net/xxxxxxxxx.send' + # xizhi(xizhi_api, send_content) + + # telegram推送通知 + tg_token = '' # tg搜索"BotFather"获取的token值 + tg_chat_id = 000000 # tg搜索"userinfobot"获取的chat_id值,即可发送推送消息给你自己,如果下面的是群组id则发送到群 + # tg_bot(tg_chat_id, tg_token, send_content) + + # email_message( + # email_host="smtp.qq.com", + # login_email="", + # email_pass="", + # sender_email="", + # sender_name="", + # to_email="", + # title="", + # content="", + # ) + + bark_url = 'https://xxx.xxx.com/key/' + # bark(bark_url, send_title, send_content) + + ntfy( + api="https://ntfy.sh/xxxxx", + title="直播推送", + content="xxx已开播", + ) + + # PushPlus推送通知 + pushplus_token = '' # 替换成自己的PushPlus Token,获取地址:https://www.pushplus.plus/ + # pushplus(pushplus_token, send_title, send_content) diff --git a/backup/robust_relay.py b/backup/robust_relay.py new file mode 100644 index 0000000..000d753 --- /dev/null +++ b/backup/robust_relay.py @@ -0,0 +1,227 @@ +# robust_relay.py +# 高稳定性 YouTube 转播模块(防误判下播 + 看门狗 + 自动重连 + token刷新) +# 作者:融合 A+B 代码精华,专为抖音/抖音国际版长期无人值守转播设计 + +import subprocess +import time +import threading +import os +import signal +import datetime +from typing import List, Optional, Callable + +# ==================== 配置常量(可全局覆盖) ==================== +WATCHDOG_SILENCE = 600 # 10分钟无输出 → 软重启 +WATCHDOG_GRACE = 120 # 启动宽限期 +REFRESH_URL_EVERY = 300 # 每5分钟强制刷新 real_url(防token过期) +OFFLINE_DEBOUNCE = 3 # 连续3次API返回下播才认为真下播 +OFFLINE_WINDOW = 120 # 最近120秒有活跃就忽略下播信号 +SOFT_RESTART_COOLDOWN = 90 # 软重启最小间隔 + +# ==================== 轻量探活 ==================== +def _probe_alive(url: str, timeout: float = 5.0) -> bool: + if not url: + return False + try: + import urllib.request, contextlib + opener = urllib.request.build_opener() + if hasattr(urllib.request, 'getproxies'): + proxy = urllib.request.getproxies().get('http') + if proxy: + opener.add_handler(urllib.request.ProxyHandler({"http": proxy, "https": proxy})) + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}, method="HEAD") + with contextlib.closing(opener.open(req, timeout=timeout)) as r: + if r.code >= 400: + return False + head = r.read(256) + if url.lower().endswith(".m3u8"): + return b"#EXTM3U" in head.upper() + if url.lower().endswith(".flv"): + return head.startswith(b"FLV") + return len(head) > 0 + except: + return False + +# ==================== 增强版 ffmpeg 启动器 ==================== +def _launch_ffmpeg( + input_url: str, + youtube_rtmp: str, + anchor_name: str, + logger: Callable[[str], None], + extra_cmd: List[str] = None +) -> subprocess.Popen: + + cmd = [ + "ffmpeg", "-nostdin", "-hide_banner", "-stats", "-stats_period", "60", "-loglevel", "info", + + # 输入端超强重连 + "-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_at_eof", "1", + "-reconnect_delay_max", "30", "-rw_timeout", "20000000", + + # 极致低延迟 + 时间戳修复 + "-fflags", "+genpts+igndts+flush_packets", "-flags", "+global_header", + "-thread_queue_size", "4096", "-analyzeduration", "0", "-probesize", "32", + + "-i", input_url, + + # copy 模式(质量无损) + "-c:v", "copy", "-c:a", "copy", "-bsf:a", "aac_adtstoasc", + "-map", "0:v?", "-map", "0:a?", + + # YouTube 强制参数 + "-f", "mpegts", "-g", "2", "-rtmp_buffer", "1000", "-rtmp_live", "live", + "-max_delay", "0", "-tune", "zerolatency", "-avoid_negative_ts", "make_zero", + "-muxdelay", "0", "-muxpreload", "0", + "-fflags", "+nobuffer+flush_packets", "-avioflags", "direct", + ] + + if extra_cmd: + cmd.extend(extra_cmd) + + cmd.append(youtube_rtmp) + + creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0 + proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=0, + creationflags=creationflags + ) + + # 用于看门狗 + proc.last_output_ts = time.time() + + def _reader(): + try: + for line in proc.stdout: # type: ignore + if not line: + break + text = line.decode(errors="ignore").rstrip() + proc.last_output_ts = time.time() + if any(k in text for k in ["bitrate=", "frame=", "speed=", "fps="]): + logger(f"[{anchor_name}] {text}") + except: + pass + threading.Thread(target=_reader, daemon=True).start() + + return proc + +# ==================== 主函数:高稳定性转播(替代原 check_subprocess) ==================== +def robust_youtube_relay( + anchor_name: str, + real_url: str, + youtube_rtmp: str, + logger: Callable[[str], None], + get_latest_stream_url: Callable[[], Optional[str]], # 用于刷新 real_url(如有token过期) + is_stream_still_live: Callable[[], bool], # 检查主播是否在播(API) + extra_ffmpeg_args: List[str] = None, # 可选:加 -f segment 分段保存 + heartbeat_callback: Callable[[int], None] = None # 可选:每分钟回调已运行秒数 +) -> bool: + """ + 返回值: + True = 主播已下播(正常结束) + False = 发生未知错误(建议计数错误次数) + """ + proc: Optional[subprocess.Popen] = None + start_time = time.time() + last_refresh = 0.0 + last_soft_restart = 0.0 + offline_count = 0 + last_live_ts = time.time() + + try: + proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args) + logger(f"[{anchor_name}] 已开始高稳定性转播 → {youtube_rtmp.split('/')[-1]}") + + while True: + now = time.time() + elapsed = int(now - start_time) + silence = now - proc.last_output_ts if proc else 0 + + # 1. 进程意外退出 → 重启 + if proc and proc.poll() is not None: + logger(f"[{anchor_name}] ffmpeg 意外退出,3秒后重启...") + time.sleep(3) + proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args) + start_time = time.time() + continue + + # 2. 看门狗:长时间无输出 → 软重启 + if elapsed > WATCHDOG_GRACE and silence > WATCHDOG_SILENCE: + if now - last_soft_restart > SOFT_RESTART_COOLDOWN: + logger(f"[{anchor_name}] 检测到卡顿({silence:.0f}s无输出),软重启 ffmpeg...") + last_soft_restart = now + if proc and proc.stdin: + proc.stdin.write(b'q') + proc.stdin.flush() + time.sleep(4) + proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args) + start_time = time.time() + continue + + # 3. 定期刷新播放地址(防token过期) + if now - last_refresh > REFRESH_URL_EVERY: + new_url = get_latest_stream_url() + if new_url and new_url != real_url: + logger(f"[{anchor_name}] 检测到新的播放地址,切换中...") + real_url = new_url + if proc and proc.stdin: + proc.stdin.write(b'q') + time.sleep(3) + proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args) + start_time = time.time() + last_refresh = now + + # 4. 下播防抖判定(核心防误判) + try: + still_live = is_stream_still_live() + except: + still_live = True # 接口异常不判下播 + + if not still_live: + # 交叉验证:流还能访问 → 忽略API误判 + if _probe_alive(real_url): + logger(f"[{anchor_name}] API判下播但流仍活跃,忽略本次误判") + still_live = True + last_live_ts = now + + if still_live: + offline_count = 0 + last_live_ts = now + else: + if now - last_live_ts < OFFLINE_WINDOW: + logger(f"[{anchor_name}] 短期掉线,忽略") + else: + offline_count += 1 + if offline_count >= OFFLINE_DEBOUNCE: + logger(f"[{anchor_name}] 连续{offline_count}次确认下播,结束转播") + if proc and proc.stdin: + proc.stdin.write(b'q') + return True + else: + logger(f"[{anchor_name}] 疑似下播 {offline_count}/{OFFLINE_DEBOUNCE},继续观察") + + # 5. 心跳日志 & 回调 + if elapsed % 60 < 2: + h, m = divmod(elapsed // 60, 60) + logger(f"[{anchor_name}] 正在转播 {h:02d}:{m:02d}") + if heartbeat_callback: + heartbeat_callback(elapsed) + + time.sleep(8) + + except KeyboardInterrupt: + logger(f"[{anchor_name}] 手动终止转播") + return True + except Exception as e: + logger(f"[{anchor_name}] 严重错误: {e}") + return False + finally: + if proc: + try: + proc.stdin.write(b'q') if proc.stdin else None + proc.wait(timeout=8) + except: + proc.kill() \ No newline at end of file diff --git a/config/URL_config.ini b/config/URL_config.ini new file mode 100644 index 0000000..38645cd --- /dev/null +++ b/config/URL_config.ini @@ -0,0 +1,39 @@ +https://live.douyin.com/925673220466,主播: 兜有米麻糍 +https://live.douyin.com/745606325880,主播: w、(摆摊日记_) +https://live.douyin.com/675343045974,主播: 张子沐的串串(你的电子榨菜) +https://live.douyin.com/673565298571,主播: 轻舟已撞大冰山(摆摊vlog) +https://live.douyin.com/835571459859,主播: 海绵饱饱 +https://live.douyin.com/152358755212,主播: 大美_红烧肉 +https://live.douyin.com/990825651731,主播: 王同学摆摊日记(蜜汁烤鸡腿) +https://live.douyin.com/591442402624,主播: 76696515302 +https://live.douyin.com/967381081829,主播: 憨憨的摆摊日记 +https://live.douyin.com/78012762575,主播: 摆摊儿去划水 +https://live.douyin.com/163813589919,主播: 不要熬夜(摆摊中) +https://live.douyin.com/498368994814,主播: 剪头发的小帆帆 +https://live.douyin.com/617848099204,主播: 果妈要努力 +https://live.douyin.com/821525628944,主播: 李熙雨⭐ +https://live.douyin.com/993102287144,主播: 小茗早餐(薪笼记助创官) +https://live.douyin.com/701547125568,主播: 悠悠包 +https://live.douyin.com/816130992220,主播: 甜昕冰冰的摆摊日记 +https://live.douyin.com/876468215361,主播: 晓晓和凡凡 +https://live.douyin.com/511335278313,主播: 秘书(摆摊休息版) +https://live.douyin.com/483160615952,主播: 小胡同学你好(饭团版) +https://live.douyin.com/525514386431,主播: 阿闯烤肉夹馍 +https://live.douyin.com/8687122573,主播: 小彤炒粉 +https://live.douyin.com/642534242822,主播: (小王_)煎饼果子 +https://live.douyin.com/743565594721,主播: 汪汪汪小妞 +https://live.douyin.com/249578288248,主播: 思思努力版 +https://live.douyin.com/472140253414,主播: 戴盘龙 +https://live.douyin.com/700846653732,主播: 三哥大锅菜_炒鸡(国基路店)官方号 +https://live.douyin.com/81849868631,主播: 纯儿²¹(休息勿跑空哦) +https://live.douyin.com/690114366322,主播: 卤味鲜(东大) +https://live.douyin.com/469980190666,主播: 茜茜柠檬茶(小米茶档_) +https://live.douyin.com/388066418744,主播: 加速姐的摆摊日记 +https://live.douyin.com/661694696687,主播: 小米摆摊记 +https://live.douyin.com/228132412943,主播: 00后摆摊小可爱 +https://live.douyin.com/876677865786,主播: 牛哄哄 +https://live.douyin.com/573735790640,主播: 小董卤串(餐饮) +https://live.douyin.com/260608582882,主播: 阿伟在炒饭(90後炒饭) +https://live.douyin.com/210443559964,主播: 小小诺 +https://live.douyin.com/305363702471,主播: 嘉嘉在摆摊 +https://live.douyin.com/143239713951,主播: ɞ_大馋丫头的烤肠 diff --git a/config/config.ini b/config/config.ini new file mode 100644 index 0000000..7ff3a2a --- /dev/null +++ b/config/config.ini @@ -0,0 +1,131 @@ +[录制设置] +language(zh_cn/en) = zh_cn +是否跳过代理检测(是/否) = 是 +直播保存路径(不填则默认) = +保存文件夹是否以作者区分 = 否 +保存文件夹是否以时间区分 = 否 +保存文件夹是否以标题区分 = 否 +保存文件名是否包含标题 = 否 +是否去除名称中的表情符号 = 是 +视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频 = ts +原画|超清|高清|标清|流畅 = 原画 +是否使用代理ip(是/否) = 否 +代理地址 = +同一时间访问网络的线程数 = 3 +循环时间(秒) = 300 +排队读取网址时间(秒) = 0 +是否显示循环秒数 = 否 +是否显示直播源地址 = 是 +分段录制是否开启 = 是 +是否强制启用https录制 = 否 +录制空间剩余阈值(gb) = 1.0 +视频分段时间(秒) = 1800 +录制完成后自动转为mp4格式 = 否 +mp4格式重新编码为h264 = 否 +追加格式后删除原文件 = 是 +生成时间字幕文件 = 否 +是否录制完成后执行自定义脚本 = 否 +自定义脚本执行命令 = +使用代理录制的平台(逗号分隔) = tiktok, sooplive, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu +额外使用代理录制的平台(逗号分隔) = + +[推送配置] +# 可选微信|钉钉|tg|邮箱|bark|ntfy|pushplus 可填多个 +直播状态推送渠道 = +钉钉推送接口链接 = +微信推送接口链接 = +bark推送接口链接 = +bark推送中断级别 = active +bark推送铃声 = +钉钉通知@对象(填手机号) = +钉钉通知@全体(是/否) = 否 +tgapi令牌 = +tg聊天id(个人或者群组id) = +smtp邮件服务器 = +是否使用SMTP服务SSL加密(是/否) = +SMTP邮件服务器端口 = +邮箱登录账号 = +发件人密码(授权码) = +发件人邮箱 = +发件人显示昵称 = +收件人邮箱 = +ntfy推送地址 = https://ntfy.sh/xxxx +ntfy推送标签 = tada +ntfy推送邮箱 = +pushplus推送token = +自定义推送标题 = +自定义开播推送内容 = +自定义关播推送内容 = +只推送通知不录制(是/否) = 否 +直播推送检测频率(秒) = 1800 +开播推送开启(是/否) = 是 +关播推送开启(是/否)= 否 + +[Cookie] +# 录制抖音必填 +抖音cookie = ttwid=1%7CB1qls3GdnZhUov9o2NxOMxxYS2ff6OSvEWbv0ytbES4%7C1680522049%7C280d802d6d478e3e78d0c807f7c487e7ffec0ae4e5fdd6a0fe74c3c6af149511; my_rd=1; passport_csrf_token=3ab34460fa656183fccfb904b16ff742; passport_csrf_token_default=3ab34460fa656183fccfb904b16ff742; d_ticket=9f562383ac0547d0b561904513229d76c9c21; n_mh=hvnJEQ4Q5eiH74-84kTFUyv4VK8xtSrpRZG1AhCeFNI; store-region=cn-fj; store-region-src=uid; LOGIN_STATUS=1; __security_server_data_status=1; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; pwa2=%223%7C0%7C3%7C0%22; download_guide=%223%2F20230729%2F0%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.6%7D; strategyABtestKey=%221690824679.923%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1536%2C%5C%22screen_height%5C%22%3A864%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A8%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; VIDEO_FILTER_MEMO_SELECT=%7B%22expireTime%22%3A1691443863751%2C%22type%22%3Anull%7D; home_can_add_dy_2_desktop=%221%22; __live_version__=%221.1.1.2169%22; device_web_cpu_core=8; device_web_memory_size=8; xgplayer_user_id=346045893336; csrf_session_id=2e00356b5cd8544d17a0e66484946f28; odin_tt=724eb4dd23bc6ffaed9a1571ac4c757ef597768a70c75fef695b95845b7ffcd8b1524278c2ac31c2587996d058e03414595f0a4e856c53bd0d5e5f56dc6d82e24004dc77773e6b83ced6f80f1bb70627; __ac_nonce=064caded4009deafd8b89; __ac_signature=_02B4Z6wo00f01HLUuwwAAIDBh6tRkVLvBQBy9L-AAHiHf7; ttcid=2e9619ebbb8449eaa3d5a42d8ce88ec835; webcast_leading_last_show_time=1691016922379; webcast_leading_total_show_times=1; webcast_local_quality=sd; live_can_add_dy_2_desktop=%221%22; msToken=1JDHnVPw_9yTvzIrwb7cQj8dCMNOoesXbA_IooV8cezcOdpe4pzusZE7NB7tZn9TBXPr0ylxmv-KMs5rqbNUBHP4P7VBFUu0ZAht_BEylqrLpzgt3y5ne_38hXDOX8o=; msToken=jV_yeN1IQKUd9PlNtpL7k5vthGKcHo0dEh_QPUQhr8G3cuYv-Jbb4NnIxGDmhVOkZOCSihNpA2kvYtHiTW25XNNX_yrsv5FN8O6zm3qmCIXcEe0LywLn7oBO2gITEeg=; tt_scid=mYfqpfbDjqXrIGJuQ7q-DlQJfUSG51qG.KUdzztuGP83OjuVLXnQHjsz-BRHRJu4e986 +快手cookie = +tiktok_cookie = +虎牙cookie = +斗鱼cookie = +yy_cookie = +b站cookie = +小红书cookie = +bigo_cookie = +blued_cookie = +sooplive_cookie = +netease_cookie = +千度热播_cookie = +pandatv_cookie = +猫耳fm_cookie = +winktv_cookie = +flextv_cookie = +look_cookie = +twitcasting_cookie = +baidu_cookie = +weibo_cookie = +kugou_cookie = +twitch_cookie = +liveme_cookie = +huajiao_cookie = +liuxing_cookie = +showroom_cookie = +acfun_cookie = +changliao_cookie = +yinbo_cookie = +yingke_cookie = +zhihu_cookie = +chzzk_cookie = +haixiu_cookie = +vvxqiu_cookie = +17live_cookie = +langlive_cookie = +pplive_cookie = +6room_cookie = +lehaitv_cookie = +huamao_cookie = +shopee_cookie = +youtube_cookie = +taobao_cookie = +jd_cookie = +faceit_cookie = +migu_cookie = +lianjie_cookie = +laixiu_cookie = +picarto_cookie = + + +[Authorization] +popkontv_token = + +[账号密码] +sooplive账号 = +sooplive密码 = +flextv账号 = +flextv密码 = +popkontv账号 = +partner_code = P-00001 +popkontv密码 = +twitcasting账号类型 = normal +twitcasting账号 = +twitcasting密码 = \ No newline at end of file diff --git a/config/easytier_network.example.json b/config/easytier_network.example.json new file mode 100644 index 0000000..6219949 --- /dev/null +++ b/config/easytier_network.example.json @@ -0,0 +1,5 @@ +{ + "networkName": "lr_自动生成", + "networkSecret": "自动生成请勿手填", + "peerUrls": [] +} diff --git a/config/ffmpeg_proxy.ini b/config/ffmpeg_proxy.ini new file mode 100644 index 0000000..afcdf7d --- /dev/null +++ b/config/ffmpeg_proxy.ini @@ -0,0 +1,6 @@ +[proxy] +# 仅作用于本软件拉起的录制/推流进程,不会修改 Windows「系统代理」 +# 与本机 Clash Verge 等「混合端口」一致即可(示例端口 7890) +enabled = no +http = http://127.0.0.1:7890 +socks = socks5://127.0.0.1:7890 diff --git a/config/google_oauth.example.json b/config/google_oauth.example.json new file mode 100644 index 0000000..b5996dd --- /dev/null +++ b/config/google_oauth.example.json @@ -0,0 +1,10 @@ +{ + "redirect_uri": "http://127.0.0.1:8001/youtube/oauth/callback", + "web": { + "client_id": "你的客户端ID.apps.googleusercontent.com", + "client_secret": "你的客户端密钥", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "redirect_uris": ["http://127.0.0.1:8001/youtube/oauth/callback"] + } +} diff --git a/config/relay_pro/url_config.ch_default.ini b/config/relay_pro/url_config.ch_default.ini new file mode 100644 index 0000000..76c0d05 --- /dev/null +++ b/config/relay_pro/url_config.ch_default.ini @@ -0,0 +1,3 @@ +# 软件已自动创建本文件。每行一个抖音直播间地址,例如: +# https://live.douyin.com/123456789 + diff --git a/config/web2_api_token.example.txt b/config/web2_api_token.example.txt new file mode 100644 index 0000000..38073ad --- /dev/null +++ b/config/web2_api_token.example.txt @@ -0,0 +1 @@ +changeme_copy_to_web2_api_token_txt_and_replace_with_random_secret diff --git a/config/web2_bind_host.example.txt b/config/web2_bind_host.example.txt new file mode 100644 index 0000000..7b9ad53 --- /dev/null +++ b/config/web2_bind_host.example.txt @@ -0,0 +1 @@ +127.0.0.1 diff --git a/config/wechat_chatbot.example.json b/config/wechat_chatbot.example.json new file mode 100644 index 0000000..120fb25 --- /dev/null +++ b/config/wechat_chatbot.example.json @@ -0,0 +1,10 @@ +{ + "apiKey": "wc_live_xxxxxxxx", + "proxyUrl": "http://your-proxy-server:3000", + "deviceType": "ipad", + "proxy": "10", + "accountId": "default", + "webhookHost": "", + "webhookPort": 18790, + "webhookPath": "/webhook/wechat" +} diff --git a/config/youtube.ini b/config/youtube.ini new file mode 100644 index 0000000..4bed05c --- /dev/null +++ b/config/youtube.ini @@ -0,0 +1,13 @@ +[youtube] +# 必填:YouTube 直播推流 key(在 YouTube 工作室 → 创建 → 流式传输 获取) +; key = qxvb-r47b-r5ju-6ud3-6k7z +key = x04z-564w-aks7-embw-30y4 + +# 可选:是否使用 RTMPS 加密推流,填 否 则用 RTMP(默认 RTMPS) +; rtmps = 否 + +# 可选:视频比特率 kbps,YouTube 推荐 2500,可填 3000-4500 提高画质 +; bitrate = 2500 + +# 可选:软编 speed 长期小于 1 时开启——轻量音频链(无 arnndn/长 EQ),减轻 CPU。填 1 或 是 +fast_audio = 1 \ No newline at end of file diff --git a/config/youtube_channels.example.json b/config/youtube_channels.example.json new file mode 100644 index 0000000..15a6830 --- /dev/null +++ b/config/youtube_channels.example.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "comment": "多路转播:每条必须一对一——一个抖音直播间地址只对应一个 YouTube 串流密钥;禁止同一抖音向多个 YouTube 推流,也禁止多个抖音共用同一密钥。", + "channels": [ + { + "id": "ch_a", + "name": "路线A", + "douyinUrl": "https://live.douyin.com/123456789", + "youtubeKey": "xxxx-xxxx-xxxx-xxxx" + }, + { + "id": "ch_b", + "name": "路线B", + "douyinUrl": "https://live.douyin.com/987654321", + "youtubeKey": "yyyy-yyyy-yyyy-yyyy" + } + ] +} diff --git a/config/youtube_channels.json b/config/youtube_channels.json new file mode 100644 index 0000000..560c226 --- /dev/null +++ b/config/youtube_channels.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "autoManaged": true, + "channels": [ + { + "id": "ch_default", + "name": "默认线路", + "douyinUrl": "", + "youtubeKey": "" + } + ] +} \ No newline at end of file diff --git a/demo.py b/demo.py new file mode 100644 index 0000000..0c6bb4b --- /dev/null +++ b/demo.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +import asyncio +from src.logger import logger +from src import spider + +# 以下示例直播间链接不保证时效性,请自行查看链接是否能正常访问 +# Please note that the following example live room links may not be up-to-date +LIVE_STREAM_CONFIG = { + "douyin": { + "url": "https://live.douyin.com/745964462470", + "func": spider.get_douyin_app_stream_data, + }, + "tiktok": { + "url": "https://www.tiktok.com/@pearlgaga88/live", + "func": spider.get_tiktok_stream_data, + }, + "kuaishou": { + "url": "https://live.kuaishou.com/u/yall1102", + "func": spider.get_kuaishou_stream_data, + }, + "huya": { + "url": "https://www.huya.com/116", + "func": spider.get_huya_app_stream_url, + }, + "douyu": { + "url": "https://www.douyu.com/topic/wzDBLS6?rid=4921614&dyshid=", + "func": spider.get_douyu_info_data, + }, + "yy": { + "url": "https://www.yy.com/22490906/22490906", + "func": spider.get_yy_stream_data, + }, + "bilibili": { + "url": "https://live.bilibili.com/21593109", + "func": spider.get_bilibili_stream_data, + }, + "xhs": { + "url": "https://www.xiaohongshu.com/user/profile/6330049c000000002303c7ed?appuid=5f3f478a00000000010005b3", + "func": spider.get_xhs_stream_url, + }, + "bigo": { + "url": "https://www.bigo.tv/cn/716418802", + "func": spider.get_bigo_stream_url, + }, + "blued": { + "url": "https://app.blued.cn/live?id=Mp6G2R", + "func": spider.get_blued_stream_url, + }, + "sooplive": { + "url": "https://play.sooplive.co.kr/sw7love", + "func": spider.get_sooplive_stream_data, + }, + "netease": { + "url": "https://cc.163.com/583946984", + "func": spider.get_netease_stream_data, + }, + "qiandurebo": { + "url": "https://qiandurebo.com/web/video.php?roomnumber=33333", + "func": spider.get_qiandurebo_stream_data, + }, + "pandatv": { + "url": "https://www.pandalive.co.kr/live/play/bara0109", + "func": spider.get_pandatv_stream_data, + }, + "maoerfm": { + "url": "https://fm.missevan.com/live/868895007", + "func": spider.get_maoerfm_stream_url, + }, + "winktv": { + "url": "https://www.winktv.co.kr/live/play/anjer1004", + "func": spider.get_winktv_stream_data, + }, + "flextv": { + "url": "https://www.ttinglive.com/channels/685479/live", + "func": spider.get_flextv_stream_data, + }, + "looklive": { + "url": "https://look.163.com/live?id=65108820&position=3", + "func": spider.get_looklive_stream_url, + }, + "popkontv": { + "url": "https://www.popkontv.com/live/view?castId=wjfal007&partnerCode=P-00117", + "func": spider.get_popkontv_stream_url, + }, + "twitcasting": { + "url": "https://twitcasting.tv/c:uonq", + "func": spider.get_twitcasting_stream_url, + }, + "baidu": { + "url": "https://live.baidu.com/m/media/pclive/pchome/live.html?room_id=9175031377&tab_category", + "func": spider.get_baidu_stream_data, + }, + "weibo": { + "url": "https://weibo.com/u/7849520225", + "func": spider.get_weibo_stream_data, + }, + "kugou": { + "url": "https://fanxing2.kugou.com/50428671?refer=2177&sourceFrom=", + "func": spider.get_kugou_stream_url, + }, + "twitchtv": { + "url": "https://www.twitch.tv/gamerbee", + "func": spider.get_twitchtv_stream_data, + }, + "liveme": { + "url": "https://www.liveme.com/zh/v/17141937295821012854/index.html", + "func": spider.get_liveme_stream_url, + }, + "huajiao": { + "url": "https://www.huajiao.com/user/207446325", + "func": spider.get_huajiao_stream_url, + }, + "showroom": { + "url": "https://www.showroom-live.com/room/profile?room_id=511033", + "func": spider.get_showroom_stream_data, + }, + "acfun": { + "url": "https://live.acfun.cn/live/17912421", + "func": spider.get_acfun_stream_data, + }, + "changliao": { + "url": "https://www.tlclw.com/801044397", + "func": spider.get_changliao_stream_url, + }, + "yingke": { + "url": "https://www.inke.cn/liveroom/index.html?uid=710032101&id=1720857535354099", + "func": spider.get_yingke_stream_url, + }, + "yinbo": { + "url": "https://live.ybw1666.com/800008687", + "func": spider.get_yinbo_stream_url, + }, + "zhihu": { + "url": "https://www.zhihu.com/people/ac3a467005c5d20381a82230101308e9", + "func": spider.get_zhihu_stream_url, + }, + "chzzk": { + "url": "https://chzzk.naver.com/live/458f6ec20b034f49e0fc6d03921646d2", + "func": spider.get_chzzk_stream_data, + }, + "haixiu": { + "url": "https://www.haixiutv.com/6095106", + "func": spider.get_haixiu_stream_url, + }, + "vvxqiu": { + "url": "https://h5webcdnp.vvxqiu.com//activity/videoShare/videoShare.html?h5Server=https://h5p.vvxqiu.com&" + "roomId=LP115664695&platformId=vvstar", + "func": spider.get_vvxqiu_stream_url, + }, + "17live": { + "url": "https://17.live/en/live/6302408", + "func": spider.get_17live_stream_url, + }, + "langlive": { + "url": "https://www.lang.live/en-US/room/3349463", + "func": spider.get_langlive_stream_url, + }, + "pplive": { + "url": "https://m.pp.weimipopo.com/live/preview.html?uid=91648673&anchorUid=91625862&app=plpl", + "func": spider.get_pplive_stream_url, + }, + "6room": { + "url": "https://v.6.cn/634435", + "func": spider.get_6room_stream_url, + }, + "lehai": { + "url": "https://www.lehaitv.com/8059096", + "func": spider.get_haixiu_stream_url, + }, + "huamao": { + "url": "https://h.catshow168.com/live/preview.html?uid=19066357&anchorUid=18895331", + "func": spider.get_pplive_stream_url, + }, + "shopee": { + "url": "https://sg.shp.ee/GmpXeuf?uid=1006401066&session=802458", + "func": spider.get_shopee_stream_url, + }, + "youtube": { + "url": "https://www.youtube.com/watch?v=cS6zS5hi1w0", + "func": spider.get_youtube_stream_url, + }, + "taobao": { + "url": "https://m.tb.cn/h.TWp0HTd", + "func": spider.get_taobao_stream_url, + }, + "jd": { + "url": "https://3.cn/28MLBy-E", + "func": spider.get_jd_stream_url, + }, + "faceit": { + "url": "https://www.faceit.com/zh/players/Compl1/stream", + "func": spider.get_faceit_stream_data, + }, + "lianjie": { + "url": "https://show.lailianjie.com/10000258", + "func": spider.get_lianjie_stream_url, + }, + "migu": { + "url": "https://www.miguvideo.com/p/live/120000541321", + "func": spider.get_migu_stream_url, + }, + "laixiu": { + "url": "https://www.imkktv.com/h5/share/video.html?uid=1845195&roomId=1710496", + "func": spider.get_laixiu_stream_url, + }, + "picarto": { + "url": "https://www.picarto.tv/cuteavalanche", + "func": spider.get_picarto_stream_url, + } +} + + +def test_live_stream(platform_name: str, proxy_addr=None, cookies=None) -> None: + if platform_name in LIVE_STREAM_CONFIG: + config = LIVE_STREAM_CONFIG[platform_name] + try: + stream_data = asyncio.run(config['func'](config['url'], proxy_addr=proxy_addr, cookies=cookies)) + logger.debug(f"Stream data for {platform_name}: {stream_data}") + except Exception as e: + logger.error(f"Error fetching stream data for {platform_name}: {e}") + else: + logger.warning(f"No configuration found for platform: {platform_name}") + + +if __name__ == "__main__": + platform = "douyin" + test_live_stream(platform) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..158df2a --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,16 @@ +version: '3.8' + +services: + app: + image: ihmily/douyin-live-recorder:latest + environment: + - TERM=xterm-256color + tty: true + stdin_open: true + #build: . + volumes: + - ./config:/app/config + - ./logs:/app/logs + - ./backup_config:/app/backup_config + - ./downloads:/app/downloads + restart: always \ No newline at end of file diff --git a/docs/REALTIME_AUDIO_DEMUCS.md b/docs/REALTIME_AUDIO_DEMUCS.md new file mode 100644 index 0000000..8f7b5b4 --- /dev/null +++ b/docs/REALTIME_AUDIO_DEMUCS.md @@ -0,0 +1,119 @@ +# 实时音频处理最佳方案(Demucs + RTX 3060) + +## 一、方案对比 + +| 方案 | 版权规避率 | 延迟 | 算力 | 复杂度 | +|------|------------|------|------|--------| +| **FFmpeg 纯滤波**(当前) | 60-70% | 0 | 低 | 低 | +| **Demucs 人声分离** | 90%+ | 4-8s | RTX 3060 | 中高 | + +--- + +## 二、推荐架构:Demucs 实时管道 + +``` +抖音流 ─┬─ FFmpeg 解复用 ─┬─ 视频 ─────────────────────────────┐ + │ │ │ + │ └─ 音频 ─→ Python Demucs ─→ 人声 ────┼─→ FFmpeg 复用 ─→ YouTube RTMP + │ (分块处理) │ + └────────────────────────────────────────────────────┘ +``` + +### 核心参数(RTX 3060 12GB) + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| **模型** | `htdemucs`(单模型,非 _ft) | 速度最快,质量够用 | +| **chunk 长度** | 2.5-3 秒 | 延迟 ≈ 2×chunk ≈ 5-6s | +| **overlap** | 0.25 | 减少接缝,质量更好 | +| **shifts** | 0 | 降低算力,实时必需 | +| **segment** | 7.8(默认)或更小 | 显存不足时减小 | + +### RTX 3060 算力 + +- 约 13 TFLOPS,高于 Demucs 实时所需 2 TFLOPS +- 12GB 显存足够 htdemucs(约 6-8GB) +- 预期:**2-4× 实时速度**,可满足直播 + +--- + +## 三、实现方式 + +### 方式 A:Python 自建管道(推荐) + +```python +# 伪代码流程 +# 1. FFmpeg 从抖音拉流,输出: -vn -acodec pcm_s16le -ar 44100 -ac 1 pipe:1 +# 2. 主进程读取音频块(2.5s = 110250 samples @ 44.1kHz) +# 3. 调用 demucs.separate_audio(model, chunk) 得到 vocals +# 4. 人声写入 pipe,FFmpeg 另一进程读取并与视频复用推流 +``` + +**依赖**: +``` +demucs +torch +torchaudio +``` + +**模型加载**: +```python +from demucs.pretrained import get_model +model = get_model('htdemucs') # 单模型,非 htdemucs_ft +``` + +### 方式 B:GStreamer(若可用) + +GStreamer 1.28+ 内置 demucs 元素,CPU 约 8× 实时: + +```bash +# 需安装 gst-plugins-rs (含 demucs) +gst-launch-1.0 uridecodebin uri=抖音流 ! audioconvert ! demucs ! ... +``` + +--- + +## 四、分块处理要点 + +1. **重叠**:chunk 之间 25% 重叠,避免接缝 +2. **队列**:双缓冲,一块处理时另一块接收 +3. **采样率**:Demucs 默认 44100 Hz,与 FFmpeg 输出一致 +4. **延迟**:总延迟 ≈ 2 × chunk 长度(约 5-6 秒可接受) + +--- + +## 五、快速验证(离线测试) + +```bash +# 安装 +pip install demucs torch torchaudio + +# 测试 30 秒音频分离速度 +demucs -n htdemucs --two-stems=vocals -j 1 test_30s.mp3 -o output/ + +# 若 30s 在 10s 内完成 → 可实时 +``` + +--- + +## 六、与当前方案的关系 + +- **当前**:`douyin_youtube_ffplay.py` 纯 FFmpeg,60-70% 规避率 +- **升级**:可新增 `douyin_youtube_demucs.py`,集成 Demucs 管道 +- **切换**:通过 config 选择 `mode = ffmpeg` 或 `mode = demucs` + +--- + +## 七、配置建议(config/youtube.ini) + +```ini +[youtube] +key = xxx + +# 音频模式: ffmpeg | demucs +; audio_mode = ffmpeg + +# Demucs 专用 +; demucs_chunk = 2.5 +; demucs_model = htdemucs +``` diff --git a/docs/YOUTUBE_LIVE_SEO.md b/docs/YOUTUBE_LIVE_SEO.md new file mode 100644 index 0000000..0b85f03 --- /dev/null +++ b/docs/YOUTUBE_LIVE_SEO.md @@ -0,0 +1,50 @@ +# YouTube Live 转播优化与 SEO 指南 + +## 一、推流技术参数(已自动优化) + +- **分辨率**:720p 竖屏 (720×1280) +- **比特率**:4.5 Mbps(可在 config/youtube.ini 的 bitrate 调整) +- **关键帧**:2 秒(符合 YouTube 推荐) +- **音频**:AAC 128kbps 48kHz 立体声 +- **协议**:RTMPS 加密推流(推荐) +- **防版权**:开源音频处理链(降噪 + 带通 + EQ + 相位扰动) + +## 二、YouTube 直播 SEO 与推荐权重 + +直播标题、描述、标签在 **YouTube 工作室** 创建直播时设置,推流程序无法修改。以下为提升推荐权重的建议: + +### 标题(前 60 字最关键) + +- 主关键词放前面,如:`LIVE 摆摊日常 | 路边直播` +- 可加实时感词汇:LIVE、直播、实时、正在直播 +- 避免堆砌关键词,保持可读性 + +### 描述(前 125 字影响搜索) + +- 首段包含主关键词和内容概要 +- 可说明:转播自抖音、直播内容类型、时间等 + +### 标签 + +- 5–15 个相关标签,如:live stream, 摆摊, 路边直播, 抖音转播 +- 与标题、描述语义一致 + +### 算法相关因素 + +- **观看时长**:用户停留越久,推荐越高 +- **互动**:点赞、评论、分享 +- **画质与音质**:清晰稳定的流有助于留存 + +## 三、config/youtube.ini 配置说明 + +| 配置项 | 说明 | 示例 | +|--------|------|------| +| key | 必填,YouTube stream key | key = xxxx-xxxx-xxxx | +| rtmps | 是否用 RTMPS(默认是) | rtmps = 否 | +| bitrate | 视频比特率 kbps | bitrate = 4500 | + +## 四、稳定性建议 + +1. 网络:上行带宽 ≥ 6 Mbps,建议有线 +2. 推流前在 YouTube 工作室完成标题、描述、标签设置 +3. 定期检查 config/youtube.ini 中的 stream key 是否有效 diff --git a/docs/p2p-remote.md b/docs/p2p-remote.md new file mode 100644 index 0000000..e91e064 --- /dev/null +++ b/docs/p2p-remote.md @@ -0,0 +1,72 @@ +# 远程控制与 P2P 虚拟网(EasyTier) + +## 方案对比(简要) + +| 方案 | 特点 | +|------|------| +| [EasyTier](https://github.com/EasyTier/EasyTier) | 去中心化 mesh、NAT 穿透、Win/Android 有现成客户端;与「本机 HTTP API」组合简单。 | +| Tailscale / ZeroTier | 需协调服务或自建 Moon;上手快,依赖厂商或自建协调器。 | +| 纯 WebRTC | 真 P2P,但需信令(常要服务器)或复杂打洞流程,不适合直接替代「虚拟网 + REST」。 | + +对本项目(`web2` FastAPI + Electron + 未来 React Native),**EasyTier + 固定虚拟 IP 访问 `http://:8001`** 是务实组合。 + +## Windows 桌面端:内置 EasyTier + +- 构建/开发前会执行 `pnpm run prepare-easytier`(或 `electron/scripts/download-easytier.mjs`),从 [EasyTier 官方 Release](https://github.com/EasyTier/EasyTier/releases) 下载 Windows x64 包并解压 `easytier-core.exe` 到 `electron/resources/easytier/`(LGPL-3.0,见同目录 `NOTICE-EasyTier.txt`)。 +- 应用启动后自动拉起该进程,并在 `config/easytier_network.json` **首次自动生成**组网名与密钥(`peerUrls` 可留空或按需添加社区共享节点)。 +- 若需跳过下载(如无网络 CI),可设置环境变量 `SKIP_EASYTIER=1`。 + +## 本项目已做的改造 + +1. **监听地址**:Electron 启动的 uvicorn 默认仍为 `127.0.0.1`。若存在 `config/web2_bind_host.txt`(单行,如 `0.0.0.0`)或环境变量 `WEB2_HOST`,则按该地址绑定。 +2. **API 鉴权(可选)**:若存在 `config/web2_api_token.txt`(单行密钥)或环境变量 `WEB2_API_TOKEN`,则除 `/health`、OpenAPI 文档路径外,请求需带: + - `Authorization: Bearer <密钥>`,或 + - `X-API-Token: <密钥>` +3. **Electron 前端**:自动从主进程读取 token 文件并注入请求头;浏览器 / React Native 需自行配置同一密钥。 +4. **「网络」页**:提供「允许远程」开关(写入 `web2_bind_host.txt` 并重启后端)、本机候选虚拟网 IP 列表、**生成 API 密钥**,以及 **二维码**(JSON 文本,供 RN 扫码解析)。 + +### 扫码绑定 JSON 格式(`v` 为版本号) + +```json +{ + "v": 1, + "baseUrl": "http://10.126.126.1:8001", + "port": 8001, + "token": "与 web2_api_token.txt 一致", + "easytier": { + "networkName": "与 PC 上 config/easytier_network.json 一致", + "networkSecret": "同上", + "peerUrls": [] + } +} +``` + +React Native:解析 JSON 后,**App 内**需使用与 PC 同版本的 EasyTier 组件并启动同一 `networkName` / `networkSecret`(可选 `peerUrls`),再请求 `baseUrl` 并带 `Authorization: Bearer ${token}`。 + +### 移动端 App 内置 EasyTier + +- Android 可参考 [EasyTier 官方 APK](https://github.com/EasyTier/EasyTier/releases) 或同仓库原生库,将组网逻辑与扫码得到的 `easytier` 字段对齐;**同一套 JSON 即可实现 PC↔手机同网,无需用户再单独安装 EasyTier 客户端**(由你们 App 内置分发)。 + +## EasyTier 侧(用户操作) + +1. PC 与手机分别安装 EasyTier,使用**相同** `--network-name` 与 `--network-secret`。 +2. 按 [EasyTier 文档](https://github.com/EasyTier/EasyTier) 连接共享节点或自建入口,确认两端能 `ping` 通虚拟 IP。 +3. 在 PC 上把 `config/web2_bind_host.txt` 设为 `0.0.0.0`,并创建 `config/web2_api_token.txt`(随机长串)。 +4. 重启本应用;在手机浏览器或 RN 中请求:`http://:8001/process_monitor` 等,并带上 `Authorization` 头。 + +## React Native 示例 + +```ts +const BASE = 'http://10.126.126.1:8001' // 替换为 PC 在 EasyTier 中的 IP +const TOKEN = '你的 web2_api_token.txt 内容' + +fetch(`${BASE}/health`).then((r) => r.json()) +fetch(`${BASE}/process_monitor`, { + headers: { Authorization: `Bearer ${TOKEN}` }, +}) +``` + +## 安全说明 + +- `0.0.0.0` 且无 token 时,控制台会警告;**不要**将无鉴权 API 暴露到公网。 +- 防火墙仅向 EasyTier 虚拟网段放行 `8001`(若需)。 diff --git a/douyin_youtube_ffplay.py b/douyin_youtube_ffplay.py new file mode 100644 index 0000000..81ea082 --- /dev/null +++ b/douyin_youtube_ffplay.py @@ -0,0 +1,389 @@ +# douyin_youtube_ffplay.py +import queue +import unicodedata +import platform +import traceback +import time +import datetime +import subprocess +import threading +import re +import os +import json +import configparser + +from web2_proxy_config import merge_child_env + + +class StreamEnded(Exception): + """主播真实下播的专用异常""" + pass + + +def sanitize_title(text: str, max_len: int = 120) -> str: + if not text: + return "" + text = unicodedata.normalize("NFKC", text) + text = ''.join(ch for ch in text if unicodedata.category(ch)[0] != 'C') + text = ''.join(ch for ch in text if ord(ch) <= 0xFFFF) + return text.strip()[:max_len] + + +def _stderr_reader_thread(proc, q): + try: + while True: + line = proc.stderr.readline() + if not line: + break + q.put(line) + except Exception as e: + try: + q.put(f"__ERR__READER__:{e}") + except Exception: + pass + finally: + try: + q.put(None) + except Exception: + pass + + +def cleanup_proc(proc): + try: + if proc and proc.poll() is None: + try: + proc.terminate() + proc.wait(timeout=5) + except Exception: + if proc.poll() is None: + try: + proc.kill() + except Exception: + pass + except Exception: + pass + + +def start_douyin_youtube_ffplay( + title: str, + anchor_name: str, + real_url: str, + rec_info: str, + record_name: str, + recording: set, + recording_time_list: dict, + running_list: list, + monitoring: list, + clear_record_info, + color_obj, +): + """ + 抖音 → YouTube 推流主函数(单路推流) + 只从 config/youtube.ini 读取一个 stream key(取第一个有效值) + """ + + # ====================== 读取 YouTube 配置 ====================== + config_file = "config/youtube.ini" + rtmps_base = "rtmps://a.rtmp.youtube.com/live2/" + rtmp_base = "rtmp://a.rtmp.youtube.com/live2/" + use_rtmps = True + # 720p 竖屏 YouTube 建议 4–6 Mbps,偏低易触发「数据不足/卡顿」提示 + b_v, maxrate, bufsize = "4000k", "4500k", "8000k" + stream_key = None + fast_audio = False # config: fast_audio=1 可跳过 arnndn+长 EQ,显著减轻 CPU + + if os.path.exists(config_file): + config = configparser.ConfigParser() + config.read(config_file, encoding="utf-8") + if config.has_section("youtube"): + for k, v in config.items("youtube"): + v = (v or "").strip() + if k == "rtmps" and v.lower() in ("0", "false", "否"): + use_rtmps = False + elif k == "fast_audio" and v.lower() in ("1", "true", "yes", "是", "开", "on"): + fast_audio = True + elif k == "bitrate" and v: + try: + n = int(v) + b_v, maxrate, bufsize = f"{n}k", f"{min(n * 12 // 10, 8000)}k", f"{n * 2}k" + except ValueError: + pass + elif (k == "key" or k == "stream_key") and v and not v.startswith("#"): + stream_key = v + if stream_key: + if stream_key.startswith("rtmp://") or stream_key.startswith("rtmps://"): + youtube_rtmp = stream_key + else: + youtube_rtmp = (rtmps_base if use_rtmps else rtmp_base) + stream_key + print(f"[INFO] 从 {config_file} 读取 YouTube 推流地址") + else: + raise ValueError(f"请在 {config_file} 中配置 key=你的stream_key") + + # ====================== 常量配置 ====================== + MAX_RETRY_DELAY = 90 + INITIAL_RETRY_DELAY = 3 + STDERR_QUEUE_TIMEOUT = 1.0 + NO_FRAME_TIMEOUT = 120 + START_CHECK_AFTER = 25 + STALE_OUTPUT_SECONDS = 40 + RECONNECT_DELAY_MAX = 10 + + END_KEYWORDS = [ + "connection reset by peer", + "failed to write trailer", + "invalid data found", + "server returned 403", + "server returned 404", + "server returned 500", + "ingestion error", + "access denied", + "packet too short", + "client disconnected", + "live has ended", + "no route to host", + "connection timed out", + "connection refused", + "error writing output", + "broken pipe", + "i/o error", + ] + + douyin_headers = ( + "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15\r\n" + "Referer: https://live.douyin.com/\r\n" + "Origin: https://live.douyin.com\r\n" + ) + + # ====================== 标题处理 ====================== + timestamp_str = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + clean_title = sanitize_title(title) + stream_title = sanitize_title(f"{anchor_name}{('_' + clean_title) if clean_title else ''}_{timestamp_str}") + print(f"[INFO] {rec_info} 开始推流到 YouTube: {stream_title}") + try: + print( + "[RELAY_META] " + + json.dumps( + { + "ts": int(time.time()), + "anchor": anchor_name, + "douyinHint": (real_url or record_name or "")[:400], + "streamTitle": stream_title, + "youtubeKeySuffix": (stream_key[-8:] if stream_key and len(stream_key) > 8 else stream_key), + }, + ensure_ascii=False, + ), + flush=True, + ) + except Exception: + pass + + # ====================== NVENC 检测(符合 YouTube 推荐比特率)====================== + codec_v = "libx264" + # ultrafast+zerolatency:在软编下尽量让 speed≥1.0;画质略降、直播可接受 + video_args = ["-preset", "ultrafast", "-tune", "zerolatency", "-profile:v", "high", "-level", "4.0", + "-b:v", b_v, "-maxrate", maxrate, "-bufsize", bufsize] + try: + if platform.system().lower() in ["windows", "linux"]: + check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=5) + if check.returncode == 0: + enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=5) + if "h264_nvenc" in (enc.stdout or ""): + codec_v = "h264_nvenc" + video_args = ["-profile:v", "high", "-level", "4.0", + "-tune", "ll", "-rc", "cbr", "-b:v", b_v, + "-maxrate", maxrate, "-bufsize", bufsize] + print("[INFO] 检测到 NVIDIA GPU → 使用 NVENC 硬件加速") + except Exception as e: + print(f"[WARN] NVENC 检测失败,回退软件编码: {e}") + + # ====================== 开源音频处理:背景音乐弱化 + 防版权 ====================== + _script_dir = os.path.dirname(os.path.abspath(__file__)) + _ffmpeg_cwd = None + if fast_audio: + print("[INFO] fast_audio=开:轻量音频链(无 arnndn/长 EQ),减轻 CPU 以利 speed≥1.0") + _af_chain = "afftdn=nf=-26:tn=1,aresample=async=1:first_pts=0" + else: + # 优先 RNNoise(arnndn),无模型则用 afftdn;aphaser 相位扰动可干扰 Content ID 指纹 + _arnndn_paths = [ + os.path.join(_script_dir, "arnndn-models", "cb.rnnn"), + os.path.join(os.path.dirname(_script_dir), "arnndn-models", "cb.rnnn"), + "arnndn-models/cb.rnnn", + ] + _arnndn_model = None + for _p in _arnndn_paths: + if os.path.isfile(_p): + _arnndn_model = os.path.normpath(_p) + break + if _arnndn_model: + print(f"[INFO] 使用 RNNoise 降噪模型: {_arnndn_model}") + _arnndn_base = os.path.dirname(os.path.dirname(os.path.abspath(_arnndn_model))) + _af_denoise = "arnndn=m=arnndn-models/cb.rnnn:mix=0.82," + _ffmpeg_cwd = _arnndn_base + else: + print("[INFO] 未找到 arnndn 模型,使用 afftdn 降噪(可克隆 richardpl/arnndn-models 到 arnndn-models/)") + _af_denoise = "afftdn=nf=-26:tn=1," + + # 强化版:收紧带通 + 频率偏移 + 音高微调 + 多段EQ + 相位扰动(纯 FFmpeg 理论上限 ~70%) + _af_chain = ( + _af_denoise + + "highpass=f=200,lowpass=f=3800," + + "equalizer=f=150:width_type=o:width=2.5:g=-14," + + "equalizer=f=350:width_type=o:width=2:g=-10," + + "equalizer=f=600:width_type=o:width=1.8:g=-8," + + "equalizer=f=1000:width_type=o:width=1.5:g=-6," + + "equalizer=f=2500:width_type=o:width=1.2:g=-4," + + "acompressor=threshold=-26dB:ratio=5:attack=8:release=80:makeup=5," + + "afreqshift=shift=22," + + "asetrate=48000*1.035,aresample=48000," + + "aphaser=type=t:decay=0.4:delay=3:speed=0.7," + + "volume=1.12," + + "aresample=async=1:first_pts=0" + ) + + # 720p 竖屏,符合 YouTube 推荐:4–6 Mbps、2s 关键帧、AAC 128k + _video_mid = ["-vsync", "cfr", "-g", "60", "-keyint_min", "60", "-r", "30", + "-bf", "0", "-sc_threshold", "0"] + if codec_v == "libx264": + _video_mid += ["-x264-params", "force-cfr=1:scenecut=0:open_gop=0:sync-lookahead=0"] + # -re:按源时间戳节奏拉流,避免突发过快;不用 realtime/arealtime(输入抖动时易让 YouTube 判为数据不足) + ffmpeg_command = [ + "ffmpeg", "-y", "-loglevel", "info", "-nostdin", + "-stats", "-stats_period", "1", + "-rw_timeout", "50000000", + "-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1", + "-reconnect_delay_max", str(RECONNECT_DELAY_MAX), + "-fflags", "+genpts+discardcorrupt", + "-err_detect", "ignore_err", + "-max_delay", "500000", + "-thread_queue_size", "2048", + "-filter_threads", "4", + "-headers", douyin_headers, + "-re", + "-i", real_url, + # bilinear 比 lanczos 轻很多,利于 speed 稳定在 ≥1.0x + "-vf", "fps=30,scale=720:1280:force_original_aspect_ratio=decrease:flags=bilinear,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", + "-c:v", codec_v, + *video_args, + *_video_mid, + "-pix_fmt", "yuv420p", + "-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2", + "-af", _af_chain, + "-flags", "+global_header", + "-flvflags", "no_duration_filesize", + "-f", "flv", youtube_rtmp + ] + + proc = None + stderr_q = None + reader_t = None + retry_delay = INITIAL_RETRY_DELAY + + try: + while True: + try: + if retry_delay > INITIAL_RETRY_DELAY: + print(f"[WARN] 防风控等待 {retry_delay}s 后重新连接抖音源...") + time.sleep(retry_delay) + + cleanup_proc(proc) + stderr_q = queue.Queue() + + print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 YouTube 推流 → {stream_title}") + _popen_kw = dict( + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + universal_newlines=True, + ) + if _ffmpeg_cwd: + _popen_kw["cwd"] = _ffmpeg_cwd + _popen_kw["env"] = merge_child_env() + proc = subprocess.Popen(ffmpeg_command, **_popen_kw) + proc.last_out_ts = time.time() + + reader_t = threading.Thread(target=_stderr_reader_thread, args=(proc, stderr_q)) + reader_t.daemon = True + reader_t.start() + + start_time = last_frame_time = time.time() + + while True: + try: + try: + line = stderr_q.get(timeout=STDERR_QUEUE_TIMEOUT) + except queue.Empty: + if time.time() - getattr(proc, "last_out_ts", time.time()) > STALE_OUTPUT_SECONDS: + print("[WARN] 长时间无任何日志,强制重启进程") + cleanup_proc(proc) + break + continue + + if line is None: + if proc.poll() is not None: + break + continue + + line = line.strip() + if line.startswith("__ERR__READER__"): + print(f"[ERROR] stderr reader 异常: {line}") + continue + + if line: + proc.last_out_ts = time.time() + print(f"[FFmpeg] {line}") + line_lower = line.lower() + + if any(kw in line_lower for kw in END_KEYWORDS): + print("[ERROR] 检测到主播真下播或被明确拒绝,停止推流") + cleanup_proc(proc) + raise StreamEnded() + + if re.search(r'frame=\s*\d+', line_lower) or "fps=" in line_lower: + last_frame_time = time.time() + + if time.time() - start_time > START_CHECK_AFTER: + if time.time() - last_frame_time > NO_FRAME_TIMEOUT: + print(f"[WARN] 连续 {NO_FRAME_TIMEOUT}s 无新帧,判定主播已下播") + cleanup_proc(proc) + raise StreamEnded() + + except StreamEnded: + raise + except Exception as e: + print(f"[ERROR] 内层异常: {e}") + traceback.print_exc() + cleanup_proc(proc) + retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY) + break + + continue + + except StreamEnded: + print(f"[INFO] 主播已下播,停止 YouTube 转推 → {record_name}") + break + + except Exception as e: + print(f"[ERROR] 外层异常: {e}") + traceback.print_exc() + retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY) + continue + + except Exception as e: + print(f"[FATAL] YouTube 推流最外层异常: {e}") + traceback.print_exc() + finally: + cleanup_proc(proc) + + try: + recording.discard(record_name) + recording_time_list.pop(record_name, None) + if real_url in running_list: + running_list.remove(real_url) + monitoring[0] = max(0, monitoring[0] - 1) + clear_record_info(record_name, real_url) + except Exception: + pass + + color_obj.print_colored(f"[{record_name}] YouTube 推流已彻底停止并清理完毕\n", color_obj.GREEN) \ No newline at end of file diff --git a/electron/electron.vite.config.ts b/electron/electron.vite.config.ts new file mode 100644 index 0000000..3b4ab22 --- /dev/null +++ b/electron/electron.vite.config.ts @@ -0,0 +1,28 @@ +import { defineConfig, externalizeDepsPlugin } from 'electron-vite' +import react from '@vitejs/plugin-react' +import { resolve } from 'path' + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + }, + preload: { + plugins: [externalizeDepsPlugin()], + build: { + rollupOptions: { + output: { + format: 'cjs', + entryFileNames: 'index.js', + }, + }, + }, + }, + renderer: { + resolve: { + alias: { + '@renderer': resolve('src/renderer/src'), + }, + }, + plugins: [react()], + }, +}) diff --git a/electron/package-lock.json b/electron/package-lock.json new file mode 100644 index 0000000..a82f6fc --- /dev/null +++ b/electron/package-lock.json @@ -0,0 +1,5278 @@ +{ + "name": "live-recorder-console", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "live-recorder-console", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "electron": "^33.2.0", + "electron-builder": "^25.1.8" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", + "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz", + "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.10", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz", + "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz", + "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.6.1", + "@electron/universal": "2.0.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "25.1.7", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.0", + "resedit": "^1.7.0", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "25.1.8", + "electron-builder-squirrel-windows": "25.1.8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, + "license": "ISC" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "25.1.7", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz", + "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.10", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts": { + "version": "0.2.8-rc1", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.12", + "typescript": "^5.4.3" + } + }, + "node_modules/config-file-ts/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz", + "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "25.1.8", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "33.4.11", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz", + "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz", + "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "25.1.8", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "dmg-builder": "25.1.8", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz", + "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "25.1.8", + "archiver": "^5.3.1", + "builder-util": "25.1.7", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "25.1.7", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz", + "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/electron/package.json b/electron/package.json new file mode 100644 index 0000000..ef10df6 --- /dev/null +++ b/electron/package.json @@ -0,0 +1,100 @@ +{ + "name": "live-recorder-console", + "productName": "直播录制助手", + "version": "1.0.0", + "description": "直播录制助手(Windows 懒人包)", + "main": "./out/main/index.js", + "type": "module", + "scripts": { + "prepare-easytier": "node scripts/download-easytier.mjs", + "dev": "node scripts/download-easytier.mjs && electron-vite dev", + "build": "node scripts/download-easytier.mjs && electron-vite build", + "preview": "electron-vite preview", + "dist": "pnpm run build && electron-builder --win --publish never" + }, + "dependencies": { + "d3": "^7.9.0", + "echarts": "^6.0.0", + "qrcode": "^1.5.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "sql.js": "^1.14.1", + "three": "^0.183.2" + }, + "devDependencies": { + "@types/d3": "^7.4.3", + "@types/node": "^22.10.0", + "@types/qrcode": "^1.5.6", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/three": "^0.183.1", + "@vitejs/plugin-react": "^4.3.4", + "adm-zip": "^0.5.16", + "electron": "^33.2.0", + "electron-builder": "^25.1.8", + "electron-vite": "^2.3.0", + "typescript": "~5.6.2", + "vite": "^5.4.11" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "electron", + "electron-builder", + "esbuild" + ] + }, + "build": { + "appId": "com.douyin.recorder.console", + "productName": "直播录制控制台", + "directories": { + "output": "dist", + "buildResources": "build" + }, + "files": [ + "out/**/*", + "package.json" + ], + "extraResources": [ + { + "from": "resources/easytier", + "to": "easytier", + "filter": [ + "**/*" + ] + }, + { + "from": "..", + "to": "backend", + "filter": [ + "**/*", + "!electron/**", + "!.git/**", + "!**/__pycache__/**", + "!**/.venv/**", + "!**/venv/**", + "!**/node_modules/**" + ] + } + ], + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + } + ] + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "installerLanguages": [ + "zh_CN", + "en_US" + ], + "language": "2052" + } + }, + "packageManager": "pnpm@10.25.0+sha1.2cfb3ab644446565c127f58165cc76368c9c920b" +} diff --git a/electron/pnpm-lock.yaml b/electron/pnpm-lock.yaml new file mode 100644 index 0000000..44b87d8 --- /dev/null +++ b/electron/pnpm-lock.yaml @@ -0,0 +1,4753 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + d3: + specifier: ^7.9.0 + version: 7.9.0 + echarts: + specifier: ^6.0.0 + version: 6.0.0 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + sql.js: + specifier: ^1.14.1 + version: 1.14.1 + three: + specifier: ^0.183.2 + version: 0.183.2 + devDependencies: + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 + '@types/node': + specifier: ^22.10.0 + version: 22.19.15 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + '@types/react': + specifier: ^18.3.12 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.28) + '@types/three': + specifier: ^0.183.1 + version: 0.183.1 + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@5.4.21(@types/node@22.19.15)) + adm-zip: + specifier: ^0.5.16 + version: 0.5.16 + electron: + specifier: ^33.2.0 + version: 33.4.11 + electron-builder: + specifier: ^25.1.8 + version: 25.1.8(electron-builder-squirrel-windows@25.1.8) + electron-vite: + specifier: ^2.3.0 + version: 2.3.0(vite@5.4.21(@types/node@22.19.15)) + typescript: + specifier: ~5.6.2 + version: 5.6.3 + vite: + specifier: ^5.4.11 + version: 5.4.21(@types/node@22.19.15) + +packages: + + 7zip-bin@5.2.0: + resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@develar/schema-utils@2.6.5': + resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} + engines: {node: '>= 8.9.0'} + + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.3.1': + resolution: {integrity: sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/rebuild@3.6.1': + resolution: {integrity: sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==} + engines: {node: '>=12.13.0'} + hasBin: true + + '@electron/universal@2.0.1': + resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==} + engines: {node: '>=16.4'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + + '@npmcli/fs@2.1.2': + resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + '@npmcli/move-file@2.0.1': + resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This functionality has been moved to @npmcli/fs + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.60.0': + resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.0': + resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.0': + resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.0': + resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.0': + resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.0': + resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.0': + resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.0': + resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.0': + resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.0': + resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.0': + resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.0': + resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.0': + resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.0': + resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.0': + resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.0': + resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.0': + resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.0': + resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.0': + resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.0': + resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.0': + resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} + cpu: [x64] + os: [win32] + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.37': + resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.183.1': + resolution: {integrity: sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==} + + '@types/verror@1.10.11': + resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} + + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@webgpu/types@0.1.69': + resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} + + '@xmldom/xmldom@0.8.11': + resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} + engines: {node: '>=10.0.0'} + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + app-builder-bin@5.0.0-alpha.10: + resolution: {integrity: sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==} + + app-builder-lib@25.1.8: + resolution: {integrity: sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 25.1.8 + electron-builder-squirrel-windows: 25.1.8 + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.10: + resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird-lst@1.0.9: + resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builder-util-runtime@9.2.10: + resolution: {integrity: sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==} + engines: {node: '>=12.0.0'} + + builder-util@25.1.7: + resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacache@16.1.3: + resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001781: + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + config-file-ts@0.2.8-rc1: + resolution: {integrity: sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + crc@3.8.0: + resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dir-compare@4.2.0: + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + + dmg-builder@25.1.8: + resolution: {integrity: sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==} + + dmg-license@1.0.11: + resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} + engines: {node: '>=8'} + os: [darwin] + hasBin: true + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + echarts@6.0.0: + resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@25.1.8: + resolution: {integrity: sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==} + + electron-builder@25.1.8: + resolution: {integrity: sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-publish@25.1.7: + resolution: {integrity: sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==} + + electron-to-chromium@1.5.322: + resolution: {integrity: sha512-vFU34OcrvMcH66T+dYC3G4nURmgfDVewMIu6Q2urXpumAPSMmzvcn04KVVV8Opikq8Vs5nUbO/8laNhNRqSzYw==} + + electron-vite@2.3.0: + resolution: {integrity: sha512-lsN2FymgJlp4k6MrcsphGqZQ9fKRdJKasoaiwIrAewN1tapYI/KINLdfEL7n10LuF0pPSNf/IqjzZbB5VINctg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@swc/core': ^1.0.0 + vite: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + '@swc/core': + optional: true + + electron@33.4.11: + resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==} + engines: {node: '>= 12.20.55'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-corefoundation@1.1.7: + resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} + engines: {node: ^8.11.2 || >=10} + os: [darwin] + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-fetch-happen@10.2.1: + resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + meshoptimizer@1.0.1: + resolution: {integrity: sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@2.1.2: + resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + node-abi@3.89.0: + resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} + engines: {node: '>=10'} + + node-addon-api@1.7.2: + resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + + node-api-version@0.2.1: + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + + node-gyp@9.4.1: + resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} + engines: {node: ^12.13 || ^14.13 || >=16} + hasBin: true + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-binary-file-arch@1.0.6: + resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resedit@1.7.2: + resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} + engines: {node: '>=12', npm: '>=6'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rollup@4.60.0: + resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@7.0.0: + resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} + engines: {node: '>= 10'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + sql.js@1.14.1: + resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==} + + ssri@9.0.1: + resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + three@0.183.2: + resolution: {integrity: sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==} + + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unique-filename@2.0.1: + resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + unique-slug@3.0.0: + resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + + zrender@6.0.0: + resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} + +snapshots: + + 7zip-bin@5.2.0: {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@develar/schema-utils@2.6.5': + dependencies: + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) + + '@dimforge/rapier3d-compat@0.12.0': {} + + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.5 + + '@electron/get@2.0.3': + dependencies: + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.5.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.1': + dependencies: + compare-version: 0.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/rebuild@3.6.1': + dependencies: + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.3 + detect-libc: 2.1.2 + fs-extra: 10.1.0 + got: 11.8.6 + node-abi: 3.89.0 + node-api-version: 0.2.1 + node-gyp: 9.4.1 + ora: 5.4.1 + read-binary-file-arch: 1.0.6 + semver: 7.7.4 + tar: 6.2.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + + '@electron/universal@2.0.1': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3 + dir-compare: 4.2.0 + fs-extra: 11.3.4 + minimatch: 9.0.9 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@gar/promisify@1.1.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@malept/cross-spawn-promise@2.0.0': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + lodash: 4.17.23 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@2.1.2': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.7.4 + + '@npmcli/move-file@2.0.1': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.60.0': + optional: true + + '@rollup/rollup-android-arm64@4.60.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.0': + optional: true + + '@rollup/rollup-darwin-x64@4.60.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.0': + optional: true + + '@sindresorhus/is@4.6.0': {} + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@tootallnate/once@2.0.0': {} + + '@tweenjs/tween.js@23.1.3': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 22.19.15 + '@types/responselike': 1.0.3 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree@1.0.8': {} + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 22.19.15 + + '@types/geojson@7946.0.16': {} + + '@types/http-cache-semantics@4.2.0': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.19.15 + + '@types/ms@2.1.0': {} + + '@types/node@20.19.37': + dependencies: + undici-types: 6.21.0 + + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + + '@types/plist@3.0.5': + dependencies: + '@types/node': 22.19.15 + xmlbuilder: 15.1.1 + optional: true + + '@types/prop-types@15.7.15': {} + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 22.19.15 + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.19.15 + + '@types/stats.js@0.17.4': {} + + '@types/three@0.183.1': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + '@webgpu/types': 0.1.69 + fflate: 0.8.2 + meshoptimizer: 1.0.1 + + '@types/verror@1.10.11': + optional: true + + '@types/webxr@0.5.24': {} + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.19.15 + optional: true + + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.15))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@22.19.15) + transitivePeerDependencies: + - supports-color + + '@webgpu/types@0.1.69': {} + + '@xmldom/xmldom@0.8.11': {} + + abbrev@1.1.1: {} + + adm-zip@0.5.16: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-keywords@3.5.2(ajv@6.14.0): + dependencies: + ajv: 6.14.0 + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + app-builder-bin@5.0.0-alpha.10: {} + + app-builder-lib@25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8): + dependencies: + '@develar/schema-utils': 2.6.5 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.1 + '@electron/rebuild': 3.6.1 + '@electron/universal': 2.0.1 + '@malept/flatpak-bundler': 0.4.0 + '@types/fs-extra': 9.0.13 + async-exit-hook: 2.0.1 + bluebird-lst: 1.0.9 + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + chromium-pickle-js: 0.2.0 + config-file-ts: 0.2.8-rc1 + debug: 4.4.3 + dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8) + dotenv: 16.6.1 + dotenv-expand: 11.0.7 + ejs: 3.1.10 + electron-builder-squirrel-windows: 25.1.8(dmg-builder@25.1.8) + electron-publish: 25.1.7 + form-data: 4.0.5 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + is-ci: 3.0.1 + isbinaryfile: 5.0.7 + js-yaml: 4.1.1 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.2.4 + resedit: 1.7.2 + sanitize-filename: 1.6.4 + semver: 7.7.4 + tar: 6.2.1 + temp-file: 3.4.0 + transitivePeerDependencies: + - bluebird + - supports-color + + aproba@2.1.0: {} + + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + argparse@2.0.1: {} + + assert-plus@1.0.0: + optional: true + + astral-regex@2.0.0: + optional: true + + async-exit-hook@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.10: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird-lst@1.0.9: + dependencies: + bluebird: 3.7.2 + + bluebird@3.7.2: {} + + boolean@3.2.0: + optional: true + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.10 + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.322 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer-crc32@0.2.13: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builder-util-runtime@9.2.10: + dependencies: + debug: 4.4.3 + sax: 1.6.0 + transitivePeerDependencies: + - supports-color + + builder-util@25.1.7: + dependencies: + 7zip-bin: 5.2.0 + '@types/debug': 4.1.13 + app-builder-bin: 5.0.0-alpha.10 + bluebird-lst: 1.0.9 + builder-util-runtime: 9.2.10 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + fs-extra: 10.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-ci: 3.0.1 + js-yaml: 4.1.1 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + transitivePeerDependencies: + - supports-color + + cac@6.7.14: {} + + cacache@16.1.3: + dependencies: + '@npmcli/fs': 2.1.2 + '@npmcli/move-file': 2.0.1 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 8.1.0 + infer-owner: 1.0.4 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 9.0.1 + tar: 6.2.1 + unique-filename: 2.0.1 + transitivePeerDependencies: + - bluebird + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001781: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chownr@2.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@3.9.0: {} + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + optional: true + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + clone@1.0.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-support@1.1.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@5.1.0: {} + + commander@7.2.0: {} + + compare-version@0.1.2: {} + + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + concat-map@0.0.1: {} + + config-file-ts@0.2.8-rc1: + dependencies: + glob: 10.5.0 + typescript: 5.6.3 + + console-control-strings@1.1.0: {} + + convert-source-map@2.0.0: {} + + core-util-is@1.0.2: + optional: true + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + + crc@3.8.0: + dependencies: + buffer: 5.7.1 + optional: true + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + detect-libc@2.1.2: {} + + detect-node@2.1.0: + optional: true + + dijkstrajs@1.0.3: {} + + dir-compare@4.2.0: + dependencies: + minimatch: 3.1.5 + p-limit: 3.1.0 + + dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): + dependencies: + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + fs-extra: 10.1.0 + iconv-lite: 0.6.3 + js-yaml: 4.1.1 + optionalDependencies: + dmg-license: 1.0.11 + transitivePeerDependencies: + - bluebird + - electron-builder-squirrel-windows + - supports-color + + dmg-license@1.0.11: + dependencies: + '@types/plist': 3.0.5 + '@types/verror': 1.10.11 + ajv: 6.14.0 + crc: 3.8.0 + iconv-corefoundation: 1.1.7 + plist: 3.1.0 + smart-buffer: 4.2.0 + verror: 1.10.1 + optional: true + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8): + dependencies: + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + archiver: 5.3.2 + builder-util: 25.1.7 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - dmg-builder + - supports-color + + electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): + dependencies: + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + chalk: 4.1.2 + dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8) + fs-extra: 10.1.0 + is-ci: 3.0.1 + lazy-val: 1.0.5 + simple-update-notifier: 2.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - electron-builder-squirrel-windows + - supports-color + + electron-publish@25.1.7: + dependencies: + '@types/fs-extra': 9.0.13 + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + chalk: 4.1.2 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + + electron-to-chromium@1.5.322: {} + + electron-vite@2.3.0(vite@5.4.21(@types/node@22.19.15)): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + cac: 6.7.14 + esbuild: 0.21.5 + magic-string: 0.30.21 + picocolors: 1.1.1 + vite: 5.4.21(@types/node@22.19.15) + transitivePeerDependencies: + - supports-color + + electron@33.4.11: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 20.19.37 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + env-paths@2.2.1: {} + + err-code@2.0.3: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es6-error@4.1.1: + optional: true + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: + optional: true + + exponential-backoff@3.1.3: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.4.1: + optional: true + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fflate@0.8.2: {} + + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.4 + serialize-error: 7.0.1 + optional: true + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + + gopd@1.2.0: {} + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + http-cache-semantics@4.2.0: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-corefoundation@1.1.7: + dependencies: + cli-truncate: 2.1.0 + node-addon-api: 1.7.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + infer-owner@1.0.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internmap@2.0.3: {} + + ip-address@10.1.0: {} + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-fullwidth-code-point@3.0.0: {} + + is-interactive@1.0.0: {} + + is-lambda@1.0.1: {} + + is-unicode-supported@0.1.0: {} + + isarray@1.0.0: {} + + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.7: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stringify-safe@5.0.1: + optional: true + + json5@2.2.3: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + lazy-val@1.0.5: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.flatten@4.4.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.union@4.6.0: {} + + lodash@4.17.23: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lowercase-keys@2.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@7.18.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-fetch-happen@10.2.1: + dependencies: + agentkeepalive: 4.6.0 + cacache: 16.1.3 + http-cache-semantics: 4.2.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 2.1.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 7.0.0 + ssri: 9.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + + math-intrinsics@1.1.0: {} + + meshoptimizer@1.0.1: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + + minipass-fetch@2.1.2: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.3: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@1.0.4: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + negotiator@0.6.4: {} + + node-abi@3.89.0: + dependencies: + semver: 7.7.4 + + node-addon-api@1.7.2: + optional: true + + node-api-version@0.2.1: + dependencies: + semver: 7.7.4 + + node-gyp@9.4.1: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 10.2.1 + nopt: 6.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.7.4 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + + node-releases@2.0.36: {} + + nopt@6.0.0: + dependencies: + abbrev: 1.1.1 + + normalize-path@3.0.0: {} + + normalize-url@6.1.0: {} + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + + object-keys@1.1.1: + optional: true + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + p-cancelable@2.1.1: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pe-library@0.4.1: {} + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.11 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pngjs@5.0.0: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + process-nextick-args@2.0.1: {} + + progress@2.0.3: {} + + promise-inflight@1.0.1: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + quick-lru@5.1.1: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-binary-file-arch@1.0.6: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + + resolve-alpn@1.2.1: {} + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + + robust-predicates@3.0.3: {} + + rollup@4.60.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.0 + '@rollup/rollup-android-arm64': 4.60.0 + '@rollup/rollup-darwin-arm64': 4.60.0 + '@rollup/rollup-darwin-x64': 4.60.0 + '@rollup/rollup-freebsd-arm64': 4.60.0 + '@rollup/rollup-freebsd-x64': 4.60.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.0 + '@rollup/rollup-linux-arm-musleabihf': 4.60.0 + '@rollup/rollup-linux-arm64-gnu': 4.60.0 + '@rollup/rollup-linux-arm64-musl': 4.60.0 + '@rollup/rollup-linux-loong64-gnu': 4.60.0 + '@rollup/rollup-linux-loong64-musl': 4.60.0 + '@rollup/rollup-linux-ppc64-gnu': 4.60.0 + '@rollup/rollup-linux-ppc64-musl': 4.60.0 + '@rollup/rollup-linux-riscv64-gnu': 4.60.0 + '@rollup/rollup-linux-riscv64-musl': 4.60.0 + '@rollup/rollup-linux-s390x-gnu': 4.60.0 + '@rollup/rollup-linux-x64-gnu': 4.60.0 + '@rollup/rollup-linux-x64-musl': 4.60.0 + '@rollup/rollup-openbsd-x64': 4.60.0 + '@rollup/rollup-openharmony-arm64': 4.60.0 + '@rollup/rollup-win32-arm64-msvc': 4.60.0 + '@rollup/rollup-win32-ia32-msvc': 4.60.0 + '@rollup/rollup-win32-x64-gnu': 4.60.0 + '@rollup/rollup-win32-x64-msvc': 4.60.0 + fsevents: 2.3.3 + + rw@1.3.3: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sanitize-filename@1.6.4: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sax@1.6.0: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver-compare@1.0.0: + optional: true + + semver@6.3.1: {} + + semver@7.7.4: {} + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + + set-blocking@2.0.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.4 + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + optional: true + + smart-buffer@4.2.0: {} + + socks-proxy-agent@7.0.0: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sprintf-js@1.1.3: + optional: true + + sql.js@1.14.1: {} + + ssri@9.0.1: + dependencies: + minipass: 3.3.6 + + stat-mode@1.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + three@0.183.2: {} + + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.5 + + tmp@0.2.5: {} + + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + + tslib@2.3.0: {} + + type-fest@0.13.1: + optional: true + + typescript@5.6.3: {} + + undici-types@6.21.0: {} + + unique-filename@2.0.1: + dependencies: + unique-slug: 3.0.0 + + unique-slug@3.0.0: + dependencies: + imurmurhash: 0.1.4 + + universalify@0.1.2: {} + + universalify@2.0.1: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + utf8-byte-length@1.0.5: {} + + util-deprecate@1.0.2: {} + + verror@1.10.1: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + optional: true + + vite@5.4.21(@types/node@22.19.15): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.8 + rollup: 4.60.0 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + which-module@2.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + xmlbuilder@15.1.1: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@21.1.1: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + + zrender@6.0.0: + dependencies: + tslib: 2.3.0 diff --git a/electron/resources/easytier/.easytier_bundle_ok b/electron/resources/easytier/.easytier_bundle_ok new file mode 100644 index 0000000..033eb68 --- /dev/null +++ b/electron/resources/easytier/.easytier_bundle_ok @@ -0,0 +1 @@ +2026-03-24T17:49:18.788Z diff --git a/electron/resources/easytier/NOTICE-EasyTier.txt b/electron/resources/easytier/NOTICE-EasyTier.txt new file mode 100644 index 0000000..4e87710 --- /dev/null +++ b/electron/resources/easytier/NOTICE-EasyTier.txt @@ -0,0 +1,3 @@ +EasyTier 以 LGPL-3.0 授权发布。 +源码: https://github.com/EasyTier/EasyTier +本程序随官方 GitHub Release 附带 easytier-core.exe 及同目录依赖,未修改二进制。 diff --git a/electron/resources/easytier/Packet.dll b/electron/resources/easytier/Packet.dll new file mode 100644 index 0000000..7e1bf12 Binary files /dev/null and b/electron/resources/easytier/Packet.dll differ diff --git a/electron/resources/easytier/easytier-cli.exe b/electron/resources/easytier/easytier-cli.exe new file mode 100644 index 0000000..6747591 Binary files /dev/null and b/electron/resources/easytier/easytier-cli.exe differ diff --git a/electron/resources/easytier/easytier-web-embed.exe b/electron/resources/easytier/easytier-web-embed.exe new file mode 100644 index 0000000..162c8bd Binary files /dev/null and b/electron/resources/easytier/easytier-web-embed.exe differ diff --git a/electron/resources/easytier/easytier-web.exe b/electron/resources/easytier/easytier-web.exe new file mode 100644 index 0000000..57493cb Binary files /dev/null and b/electron/resources/easytier/easytier-web.exe differ diff --git a/electron/resources/easytier/wintun.dll b/electron/resources/easytier/wintun.dll new file mode 100644 index 0000000..aee04e7 Binary files /dev/null and b/electron/resources/easytier/wintun.dll differ diff --git a/electron/scripts/download-easytier.mjs b/electron/scripts/download-easytier.mjs new file mode 100644 index 0000000..54bddff --- /dev/null +++ b/electron/scripts/download-easytier.mjs @@ -0,0 +1,103 @@ +/** + * 下载官方 EasyTier Windows x64 发行包并解压到 resources/easytier/ + * 须复制 exe 同目录下全部 DLL 等依赖,否则 easytier-core 会以 0xC0000135(3221225781)退出。 + * 设置 SKIP_EASYTIER=1 可跳过(CI 无网环境) + */ +import fs from 'fs' +import path from 'path' +import https from 'https' +import { fileURLToPath } from 'url' +import AdmZip from 'adm-zip' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const root = path.resolve(__dirname, '..') +const resDir = path.join(root, 'resources', 'easytier') +const DEFAULT_URL = + process.env.EASYTIER_DOWNLOAD_URL || + 'https://github.com/EasyTier/EasyTier/releases/download/v2.4.5/easytier-windows-x86_64-v2.4.5.zip' +const exeName = 'easytier-core.exe' +const bundleMarker = '.easytier_bundle_ok' + +if (process.env.SKIP_EASYTIER === '1') { + console.log('[easytier] SKIP_EASYTIER=1,跳过下载') + process.exit(0) +} + +function download(url) { + return new Promise((resolve, reject) => { + https + .get(url, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + download(res.headers.location).then(resolve).catch(reject) + return + } + if (res.statusCode !== 200) { + reject(new Error(`HTTP ${res.statusCode} ${url}`)) + return + } + const chunks = [] + res.on('data', (c) => chunks.push(c)) + res.on('end', () => resolve(Buffer.concat(chunks))) + res.on('error', reject) + }) + .on('error', reject) + }) +} + +function findExe(dir, name) { + if (!fs.existsSync(dir)) return null + for (const f of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, f.name) + if (f.isDirectory()) { + const r = findExe(p, name) + if (r) return r + } else if (f.name === name) { + return p + } + } + return null +} + +fs.mkdirSync(resDir, { recursive: true }) +const outExe = path.join(resDir, exeName) +const markerPath = path.join(resDir, bundleMarker) +if (fs.existsSync(outExe) && fs.existsSync(markerPath)) { + console.log('[easytier] 已存在完整包,跳过:', outExe) + process.exit(0) +} +if (fs.existsSync(outExe) && !fs.existsSync(markerPath)) { + console.log('[easytier] 检测到旧版仅复制了 exe,将重新下载并解压全部依赖文件…') +} + +console.log('[easytier] 下载:', DEFAULT_URL) +const buf = await download(DEFAULT_URL) +const zip = new AdmZip(buf) +const tmp = path.join(resDir, '_extract_tmp') +if (fs.existsSync(tmp)) fs.rmSync(tmp, { recursive: true }) +fs.mkdirSync(tmp, { recursive: true }) +zip.extractAllTo(tmp, true) +const found = findExe(tmp, exeName) +if (!found) { + console.error('[easytier] zip 内未找到', exeName) + process.exit(1) +} +const srcDir = path.dirname(found) +for (const name of fs.readdirSync(srcDir)) { + const from = path.join(srcDir, name) + const to = path.join(resDir, name) + if (!fs.statSync(from).isFile()) continue + fs.copyFileSync(from, to) +} +fs.rmSync(tmp, { recursive: true }) +fs.writeFileSync( + path.join(resDir, 'NOTICE-EasyTier.txt'), + [ + 'EasyTier 以 LGPL-3.0 授权发布。', + '源码: https://github.com/EasyTier/EasyTier', + '本程序随官方 GitHub Release 附带 easytier-core.exe 及同目录依赖,未修改二进制。', + '', + ].join('\n'), + 'utf-8', +) +fs.writeFileSync(markerPath, `${new Date().toISOString()}\n`, 'utf-8') +console.log('[easytier] 完成(已复制 zip 内同目录全部文件):', outExe) diff --git a/electron/src/main/appHost.ts b/electron/src/main/appHost.ts new file mode 100644 index 0000000..29a9604 --- /dev/null +++ b/electron/src/main/appHost.ts @@ -0,0 +1,224 @@ +/** + * Electron app 模块:开机启动、跳转列表、关于面板、日志路径、角标、优雅重启、指标等。 + * 文档:https://www.electronjs.org/docs/latest/api/app + */ +import { app, BrowserWindow, ipcMain, shell } from 'electron' +import fs from 'fs' +import path from 'path' + +const VALID_NAV = new Set(['desk', 'record', 'network', 'proxy', 'system', 'wechat']) + +export function parseNavFromArgv(argv: string[]): string | null { + for (const a of argv) { + const m = /^--nav=(.+)$/.exec(a) + if (!m) continue + const id = m[1].trim() + if (VALID_NAV.has(id)) return id + } + return null +} + +/** 须在 app.ready 之前调用(Windows Toast / 任务栏分组) */ +export function setAppUserModelIdEarly(): void { + if (process.platform !== 'win32') return + try { + app.setAppUserModelId('com.douyin.recorder.console') + } catch { + /* ignore */ + } +} + +function readPackageJsonField(field: 'description' | 'version'): string { + try { + const candidates = [ + path.join(app.getAppPath(), 'package.json'), + path.join(process.cwd(), 'package.json'), + ] + for (const p of candidates) { + if (fs.existsSync(p)) { + const j = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record + const v = j[field] + if (typeof v === 'string') return v + } + } + } catch { + /* ignore */ + } + return '' +} + +/** ready 后立即调用 */ +export function applyAppHostBootstrap(getBackendRoot: () => string): void { + try { + const logDir = path.join(app.getPath('userData'), 'logs') + app.setAppLogsPath(logDir) + } catch { + /* ignore */ + } + + const ver = app.getVersion() + const desc = readPackageJsonField('description') || '直播录制与转播控制台' + app.setAboutPanelOptions({ + applicationName: app.getName(), + applicationVersion: ver, + copyright: `Copyright © ${new Date().getFullYear()}`, + credits: desc, + }) + + if (process.env.WEB2_SECURE_DNS === '1') { + try { + app.configureHostResolver({ secureDnsMode: 'automatic' }) + } catch { + /* ignore */ + } + } + + const proxy = process.env.WEB2_ELECTRON_GLOBAL_PROXY?.trim() + if (proxy) { + try { + void app.setProxy({ proxyRules: proxy }) + } catch { + /* ignore */ + } + } + + if (process.platform === 'win32') { + try { + app.setUserTasks([ + { + program: process.execPath, + arguments: '--nav=desk', + title: '工作台', + description: '打开工作台仪表盘', + iconPath: process.execPath, + iconIndex: 0, + }, + { + program: process.execPath, + arguments: '--nav=record', + title: '录制', + description: '打开录制与转播控制', + iconPath: process.execPath, + iconIndex: 0, + }, + { + program: process.execPath, + arguments: '--nav=network', + title: '网络', + description: '网络与远程绑定', + iconPath: process.execPath, + iconIndex: 0, + }, + ]) + } catch { + /* ignore */ + } + } +} + +export function registerAppHostIpcHandlers(getBackendRoot: () => string): void { + ipcMain.removeHandler('app:get-login-item') + ipcMain.handle('app:get-login-item', () => { + try { + return app.getLoginItemSettings() + } catch { + return { openAtLogin: false } + } + }) + + ipcMain.removeHandler('app:set-login-item') + ipcMain.handle('app:set-login-item', (_evt, openAtLogin: unknown) => { + if (typeof openAtLogin !== 'boolean') return { ok: false as const, error: '参数无效' } + try { + app.setLoginItemSettings({ + openAtLogin, + path: process.execPath, + args: [], + }) + return { ok: true as const } + } catch (e) { + return { ok: false as const, error: e instanceof Error ? e.message : String(e) } + } + }) + + ipcMain.removeHandler('app:relaunch') + ipcMain.handle('app:relaunch', () => { + app.relaunch() + app.exit(0) + }) + + ipcMain.removeHandler('app:get-metrics') + ipcMain.handle('app:get-metrics', () => { + try { + return app.getAppMetrics() + } catch { + return [] + } + }) + + ipcMain.removeHandler('app:show-about') + ipcMain.handle('app:show-about', () => { + try { + app.showAboutPanel() + } catch { + /* ignore */ + } + }) + + ipcMain.removeHandler('app:set-badge') + ipcMain.handle('app:set-badge', (_evt, count: unknown) => { + const n = typeof count === 'number' && Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0 + try { + if (process.platform === 'darwin' || process.platform === 'linux') { + app.setBadgeCount(n) + } + } catch { + /* Windows 无 dock 角标,忽略 */ + } + }) + + ipcMain.removeHandler('app:get-paths') + ipcMain.handle('app:get-paths', () => { + const br = getBackendRoot() + return { + userData: app.getPath('userData'), + logs: app.getPath('logs'), + temp: app.getPath('temp'), + backendRoot: br, + backendConfig: path.join(br, 'config'), + } + }) + + ipcMain.removeHandler('app:add-recent-config') + ipcMain.handle('app:add-recent-config', () => { + const cfg = path.join(getBackendRoot(), 'config') + try { + if (fs.existsSync(cfg)) { + app.addRecentDocument(cfg) + } + } catch { + /* ignore */ + } + }) + + ipcMain.removeHandler('app:open-logs-folder') + ipcMain.handle('app:open-logs-folder', async () => { + const p = app.getPath('logs') + try { + await fs.promises.mkdir(p, { recursive: true }) + } catch { + /* ignore */ + } + const err = await shell.openPath(p) + if (err) throw new Error(err) + }) +} + +export function sendNavToWindow(win: BrowserWindow, nav: string): void { + if (!VALID_NAV.has(nav)) return + try { + win.webContents.send('app:nav', nav) + } catch { + /* ignore */ + } +} diff --git a/electron/src/main/clientDb.ts b/electron/src/main/clientDb.ts new file mode 100644 index 0000000..c247daf --- /dev/null +++ b/electron/src/main/clientDb.ts @@ -0,0 +1,400 @@ +/** + * 客户端本地 SQLite(sql.js):直播事件、用户操作、运行日志、数据快照。 + * 数据库文件:userData/client-data.sqlite + */ +import { createRequire } from 'module' +import fs from 'fs' +import path from 'path' +import { app } from 'electron' +import initSqlJs, { type Database } from 'sql.js' + +let db: Database | null = null +let persistTimer: ReturnType | null = null + +export function getDbPath(): string { + return path.join(app.getPath('userData'), 'client-data.sqlite') +} + +function schedulePersist(): void { + if (persistTimer) clearTimeout(persistTimer) + persistTimer = setTimeout(() => { + persistTimer = null + flushClientDb() + }, 400) +} + +export function flushClientDb(): void { + if (!db) return + try { + const buf = Buffer.from(db.export()) + fs.mkdirSync(path.dirname(getDbPath()), { recursive: true }) + fs.writeFileSync(getDbPath(), buf) + } catch { + /* ignore */ + } +} + +function runMigrations(): void { + if (!db) return + db.run(` + CREATE TABLE IF NOT EXISTS live_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, + process TEXT, + event_type TEXT NOT NULL, + status TEXT, + message TEXT, + detail_json TEXT + ); + CREATE TABLE IF NOT EXISTS user_actions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, + action TEXT NOT NULL, + nav TEXT, + detail_json TEXT + ); + CREATE TABLE IF NOT EXISTS client_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, + level TEXT NOT NULL, + source TEXT, + message TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS data_snapshots ( + key TEXT PRIMARY KEY, + json TEXT NOT NULL, + ts INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_live_ts ON live_events(ts DESC); + CREATE INDEX IF NOT EXISTS idx_user_ts ON user_actions(ts DESC); + CREATE INDEX IF NOT EXISTS idx_client_ts ON client_logs(ts DESC); + CREATE TABLE IF NOT EXISTS app_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + ts INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS relay_channels ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + youtube_key TEXT NOT NULL DEFAULT '', + douyin_url TEXT NOT NULL DEFAULT '', + url_list_text TEXT NOT NULL DEFAULT '', + sort_order INTEGER NOT NULL DEFAULT 0, + updated_at INTEGER NOT NULL + ); + `) +} + +export async function initClientDb(): Promise { + const require = createRequire(import.meta.url) + // sql.js exports "." and "./dist/*" only — not package.json + const wasmPath = require.resolve('sql.js/dist/sql-wasm.wasm') + const SQL = await initSqlJs({ locateFile: () => wasmPath }) + const dbPath = getDbPath() + if (fs.existsSync(dbPath)) { + const fileBuf = fs.readFileSync(dbPath) + db = new SQL.Database(fileBuf) + } else { + db = new SQL.Database() + } + runMigrations() + schedulePersist() +} + +export function appendLiveEvent(payload: { + eventType: string + process?: string + status?: string + message?: string + detail?: unknown +}): void { + if (!db) return + const ts = Date.now() + const detailJson = payload.detail != null ? JSON.stringify(payload.detail) : null + db.run( + `INSERT INTO live_events (ts, process, event_type, status, message, detail_json) VALUES (?, ?, ?, ?, ?, ?)`, + [ + ts, + payload.process ?? null, + payload.eventType, + payload.status ?? null, + payload.message ?? null, + detailJson, + ], + ) + schedulePersist() +} + +export function appendUserAction(payload: { action: string; nav?: string; detail?: unknown }): void { + if (!db) return + const ts = Date.now() + const detailJson = payload.detail != null ? JSON.stringify(payload.detail) : null + db.run(`INSERT INTO user_actions (ts, action, nav, detail_json) VALUES (?, ?, ?, ?)`, [ + ts, + payload.action, + payload.nav ?? null, + detailJson, + ]) + schedulePersist() +} + +export function appendClientLog(level: string, source: string, message: string): void { + if (!db) return + const ts = Date.now() + db.run(`INSERT INTO client_logs (ts, level, source, message) VALUES (?, ?, ?, ?)`, [ + ts, + level, + source, + message, + ]) + schedulePersist() +} + +export function saveDataSnapshot(key: string, json: unknown): void { + if (!db) return + const ts = Date.now() + const text = JSON.stringify(json) + db.run(`INSERT OR REPLACE INTO data_snapshots (key, json, ts) VALUES (?, ?, ?)`, [key, text, ts]) + schedulePersist() +} + +export type DashboardRow = { + id: number + ts: number + process?: string | null + event_type?: string + status?: string | null + message?: string | null + detail_json?: string | null + action?: string + nav?: string | null + level?: string + source?: string | null +} + +function execToRows(sql: string): DashboardRow[] { + if (!db) return [] + try { + const res = db.exec(sql) + if (!res.length) return [] + const { columns, values } = res[0] + return values.map((row) => { + const o: Record = {} + columns.forEach((c, i) => { + o[c] = row[i] + }) + return o as DashboardRow + }) + } catch { + return [] + } +} + +export function getDashboardSnapshot(): { + liveEvents: DashboardRow[] + userActions: DashboardRow[] + clientLogs: DashboardRow[] + snapshots: Record +} { + const empty = { + liveEvents: [] as DashboardRow[], + userActions: [] as DashboardRow[], + clientLogs: [] as DashboardRow[], + snapshots: {} as Record, + } + if (!db) return empty + + try { + empty.liveEvents = execToRows( + `SELECT id, ts, process, event_type, status, message, detail_json FROM live_events ORDER BY id DESC LIMIT 80`, + ) + empty.userActions = execToRows( + `SELECT id, ts, action, nav, detail_json FROM user_actions ORDER BY id DESC LIMIT 80`, + ) + empty.clientLogs = execToRows( + `SELECT id, ts, level, source, message FROM client_logs ORDER BY id DESC LIMIT 120`, + ) + + const snapRes = db.exec(`SELECT key, json, ts FROM data_snapshots`) + if (snapRes.length) { + const { columns, values } = snapRes[0] + for (const row of values) { + const o: Record = {} + columns.forEach((c, i) => { + o[c] = row[i] + }) + const key = String(o.key ?? '') + const json = String(o.json ?? '') + const ts = Number(o.ts ?? 0) + try { + empty.snapshots[key] = { ts, data: JSON.parse(json) as unknown } + } catch { + empty.snapshots[key] = { ts, data: json } + } + } + } + } catch { + /* ignore */ + } + return empty +} + +// —— 应用设置(主题等)—— + +export function getAppSetting(key: string): string | null { + if (!db) return null + try { + const r = db.exec(`SELECT value FROM app_settings WHERE key = ${JSON.stringify(key)}`) + if (!r.length || !r[0].values.length) return null + return String(r[0].values[0][0] ?? '') + } catch { + return null + } +} + +export function setAppSetting(key: string, value: string): void { + if (!db) return + const ts = Date.now() + db.run(`INSERT OR REPLACE INTO app_settings (key, value, ts) VALUES (?, ?, ?)`, [key, value, ts]) + schedulePersist() +} + +// —— Pro 转播线路:SQLite 为源,导出到项目 config/ —— + +function relayChannelCount(): number { + if (!db) return 0 + try { + const r = db.exec(`SELECT COUNT(*) AS c FROM relay_channels`) + if (!r.length || !r[0].values.length) return 0 + return Number(r[0].values[0][0] ?? 0) + } catch { + return 0 + } +} + +function seedDefaultRelayChannel(): void { + if (!db) return + const ts = Date.now() + db.run( + `INSERT OR IGNORE INTO relay_channels (id, name, youtube_key, douyin_url, url_list_text, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + 'ch_default', + '默认线路', + '', + '', + '# 软件已自动创建本文件。每行一个抖音直播间地址,例如:\n# https://live.douyin.com/123456789\n\n', + 0, + ts, + ], + ) + schedulePersist() +} + +function safeChannelFileId(id: string): string { + return id.replace(/[^a-zA-Z0-9_-]/g, '_') || 'default' +} + +function importRelayFromYoutubeJson(configDir: string): boolean { + if (!db) return false + const p = path.join(configDir, 'youtube_channels.json') + if (!fs.existsSync(p)) return false + let raw: { channels?: unknown[] } + try { + raw = JSON.parse(fs.readFileSync(p, 'utf-8')) as { channels?: unknown[] } + } catch { + return false + } + const arr = raw.channels + if (!Array.isArray(arr) || arr.length === 0) return false + db.run(`DELETE FROM relay_channels`) + const relayPro = path.join(configDir, 'relay_pro') + let order = 0 + for (const item of arr) { + if (!item || typeof item !== 'object') continue + const o = item as Record + const id = String(o.id ?? o.name ?? `ch_${order}`).trim() || `ch_${order}` + const name = String(o.name ?? '线路').trim() || '线路' + const yk = String(o.youtubeKey ?? o.key ?? '').trim() + const du = String(o.douyinUrl ?? o.douyin_url ?? '').trim() + let urlText = '' + const up = path.join(relayPro, `url_config.${safeChannelFileId(id)}.ini`) + if (fs.existsSync(up)) { + try { + urlText = fs.readFileSync(up, 'utf-8') + } catch { + urlText = '' + } + } + const ts = Date.now() + db.run( + `INSERT INTO relay_channels (id, name, youtube_key, douyin_url, url_list_text, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, + [id, name, yk, du, urlText, order, ts], + ) + order += 1 + } + schedulePersist() + return order > 0 +} + +export function exportRelayChannelsToConfigDir(configDir: string): void { + if (!db) return + const relayPro = path.join(configDir, 'relay_pro') + fs.mkdirSync(relayPro, { recursive: true }) + let r: ReturnType + try { + r = db.exec( + `SELECT id, name, youtube_key, douyin_url, url_list_text, sort_order FROM relay_channels ORDER BY sort_order, id`, + ) + } catch { + return + } + if (!r.length || !r[0].values.length) return + const cols = r[0].columns + const rows = r[0].values + const channels: Record[] = [] + for (const row of rows) { + const rec: Record = {} + cols.forEach((c, i) => { + rec[c] = row[i] + }) + const id = String(rec.id ?? '') + const name = String(rec.name ?? '') + const youtubeKey = String(rec.youtube_key ?? '') + const douyinUrl = String(rec.douyin_url ?? '') + const urlList = String(rec.url_list_text ?? '') + channels.push({ + id, + name, + douyinUrl, + youtubeKey, + }) + const uf = path.join(relayPro, `url_config.${safeChannelFileId(id)}.ini`) + fs.writeFileSync(uf, urlList || '# 每行一个抖音直播间地址\n', 'utf-8') + } + const doc = { + version: 1, + autoManaged: true, + channels, + } + fs.mkdirSync(configDir, { recursive: true }) + fs.writeFileSync(path.join(configDir, 'youtube_channels.json'), JSON.stringify(doc, null, 2), 'utf-8') +} + +/** 启动时:SQLite 优先;若为空则从磁盘 JSON 导入;仍空则种子;最后导出到 config/ */ +export function syncRelayChannelsWithBackendConfig(configDir: string): void { + if (!db) return + const abs = path.resolve(configDir) + if (relayChannelCount() === 0) { + if (!importRelayFromYoutubeJson(abs)) { + seedDefaultRelayChannel() + } + } + exportRelayChannelsToConfigDir(abs) +} + +/** 从磁盘重新读入 SQLite(在通过 Python API 保存文件后调用) */ +export function importRelayChannelsFromConfigDir(configDir: string): void { + if (!db) return + const abs = path.resolve(configDir) + importRelayFromYoutubeJson(abs) +} diff --git a/electron/src/main/easytier.ts b/electron/src/main/easytier.ts new file mode 100644 index 0000000..2e8b46f --- /dev/null +++ b/electron/src/main/easytier.ts @@ -0,0 +1,229 @@ +/** + * 内置 EasyTier(Windows):随应用分发 easytier-core.exe,自动生成组网密钥,无需用户单独安装。 + * 二进制来源:https://github.com/EasyTier/EasyTier (LGPL-3.0) + */ +import { app } from 'electron' +import { execFile, spawn, type ChildProcess } from 'child_process' +import { promisify } from 'util' +import crypto from 'crypto' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const execFileAsync = promisify(execFile) + +export type EasyTierNetworkConfig = { + networkName: string + networkSecret: string + peerUrls: string[] +} + +let easytierChild: ChildProcess | null = null +let easytierLastError = '' + +/** Windows 进程退出码(含 NTSTATUS)可读说明 */ +function formatWindowsExitHint(code: number): string { + const u = code >>> 0 + if (u === 0xc0000135) { + return ' — 缺少 DLL(常见:官方 zip 内依赖未与 exe 同目录;开发环境请删除 electron/resources/easytier 后重新执行 pnpm run prepare-easytier)。也可能是未安装 VC++ 运行库。' + } + if (u === 0xc000007b) { + return ' — 无效映像(32/64 位与系统不符或文件损坏)。' + } + return '' +} + +export function getBundledEasyTierExePath(): string | null { + if (process.platform !== 'win32') return null + const exe = 'easytier-core.exe' + if (app.isPackaged) { + const p = path.join(process.resourcesPath, 'easytier', exe) + return fs.existsSync(p) ? p : null + } + const dev = path.resolve(__dirname, '../../resources/easytier', exe) + return fs.existsSync(dev) ? dev : null +} + +/** 与 easytier-core 同目录分发的 CLI,用于 peer list 等工作台统计 */ +export function getBundledEasyTierCliPath(): string | null { + if (process.platform !== 'win32') return null + const exe = 'easytier-cli.exe' + if (app.isPackaged) { + const p = path.join(process.resourcesPath, 'easytier', exe) + return fs.existsSync(p) ? p : null + } + const dev = path.resolve(__dirname, '../../resources/easytier', exe) + return fs.existsSync(dev) ? dev : null +} + +export type EasyTierPeerStats = { + ok: boolean + error?: string + totalPeers?: number + onlinePeers?: number + raw?: string +} + +function parsePeerListOutput(text: string): { totalPeers: number; onlinePeers: number } { + const lines = text + .split(/\r?\n/) + .map((l) => l.trim()) + .filter((l) => l.length > 0) + const ipRe = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g + let withIp = 0 + let online = 0 + for (const line of lines) { + if (/^[-=│├└┌┐┘┬┴┼]+$/.test(line)) continue + if (/^peer\s|^id\s|^hostname\s/i.test(line) && line.length < 100) continue + const ips = line.match(ipRe) + if (!ips) continue + withIp += 1 + const low = line.toLowerCase() + if (!/offline|unreachable|disconnected|lost|timeout/.test(low)) online += 1 + } + if (withIp === 0) { + const guess = lines.filter((l) => l.length > 12 && !/^error|^usage/i.test(l)).length + const n = Math.max(0, guess > 0 ? guess - 1 : 0) + return { totalPeers: n, onlinePeers: n } + } + return { totalPeers: withIp, onlinePeers: online > 0 ? online : withIp } +} + +/** 调用 easytier-cli peer list;需 easytier-core 已运行且 RPC 端口可达 */ +export async function queryEasyTierPeerStats(): Promise { + const cli = getBundledEasyTierCliPath() + if (!cli) return { ok: false, error: '未找到内置 easytier-cli.exe' } + if (!getEasyTierRuntimeState().running) { + return { ok: false, error: 'easytier-core 未运行', totalPeers: 0, onlinePeers: 0 } + } + try { + const { stdout, stderr } = await execFileAsync(cli, ['peer', 'list'], { + cwd: path.dirname(cli), + timeout: 5000, + maxBuffer: 2 * 1024 * 1024, + windowsHide: true, + }) + const raw = `${stdout || ''}${stderr || ''}` + const { totalPeers, onlinePeers } = parsePeerListOutput(raw) + return { ok: true, totalPeers, onlinePeers, raw: raw.slice(0, 6000) } + } catch (e) { + const msg = e instanceof Error ? e.message : String(e) + return { ok: false, error: msg, totalPeers: 0, onlinePeers: 0 } + } +} + +export function readEasyTierNetworkName(backendRoot: string): string { + const p = path.join(backendRoot, 'config', 'easytier_network.json') + if (!fs.existsSync(p)) return '' + try { + const raw = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record + return String(raw.networkName ?? '') + } catch { + return '' + } +} + +export function loadOrCreateNetworkConfig(backendRoot: string): EasyTierNetworkConfig { + const p = path.join(backendRoot, 'config', 'easytier_network.json') + fs.mkdirSync(path.dirname(p), { recursive: true }) + if (fs.existsSync(p)) { + try { + const raw = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record + return { + networkName: String(raw.networkName ?? ''), + networkSecret: String(raw.networkSecret ?? ''), + peerUrls: Array.isArray(raw.peerUrls) ? raw.peerUrls.map(String) : [], + } + } catch { + /* fallthrough */ + } + } + const cfg: EasyTierNetworkConfig = { + networkName: `lr_${crypto.randomBytes(6).toString('hex')}`, + networkSecret: crypto.randomBytes(24).toString('hex'), + peerUrls: [], + } + fs.writeFileSync(p, JSON.stringify(cfg, null, 2), 'utf-8') + return cfg +} + +export function getEasyTierRuntimeState(): { + bundled: boolean + running: boolean + lastError: string +} { + const bundled = !!getBundledEasyTierExePath() + const running = !!easytierChild && !easytierChild.killed + return { bundled, running, lastError: easytierLastError } +} + +export function startBundledEasyTier(backendRoot: string): void { + easytierLastError = '' + const exe = getBundledEasyTierExePath() + if (!exe) { + easytierLastError = '未找到内置 easytier-core.exe(开发环境可执行 pnpm run prepare-easytier)' + return + } + if (easytierChild && !easytierChild.killed) { + return + } + const cfg = loadOrCreateNetworkConfig(backendRoot) + const args = ['-d', '--network-name', cfg.networkName, '--network-secret', cfg.networkSecret] + for (const u of cfg.peerUrls) { + const t = u.trim() + if (t) args.push('-p', t) + } + let child: ChildProcess + try { + child = spawn(exe, args, { + cwd: path.dirname(exe), + windowsHide: true, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env }, + }) + } catch (e) { + easytierLastError = e instanceof Error ? e.message : String(e) + easytierChild = null + return + } + easytierChild = child + const appendErr = (chunk: Buffer): void => { + const s = chunk.toString() + if (s.length < 2000) easytierLastError = (easytierLastError + s).slice(-2000) + } + child.stderr?.on('data', appendErr) + child.stdout?.on('data', appendErr) + child.on('error', (err) => { + easytierLastError = err.message + }) + child.on('close', (code) => { + if (code !== 0 && code !== null) { + const hint = formatWindowsExitHint(code) + easytierLastError = `easytier-core 已退出(代码 ${code})${hint}${easytierLastError ? `\n${easytierLastError}` : ''}` + } + easytierChild = null + }) +} + +export function stopBundledEasyTier(): void { + if (!easytierChild || easytierChild.killed) { + easytierChild = null + return + } + try { + if (process.platform === 'win32') { + spawn('taskkill', ['/PID', String(easytierChild.pid), '/T', '/F'], { + windowsHide: true, + stdio: 'ignore', + }) + } else { + easytierChild.kill('SIGTERM') + } + } catch { + /* ignore */ + } + easytierChild = null +} diff --git a/electron/src/main/index.ts b/electron/src/main/index.ts new file mode 100644 index 0000000..89dbc6d --- /dev/null +++ b/electron/src/main/index.ts @@ -0,0 +1,706 @@ +import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron' +import { execFile, spawn, ChildProcessWithoutNullStreams } from 'child_process' +import { promisify } from 'util' +import http from 'http' +import fs from 'fs' +import path from 'path' +import crypto from 'crypto' +import { networkInterfaces } from 'os' +import { fileURLToPath } from 'url' + +import { + getEasyTierRuntimeState, + loadOrCreateNetworkConfig, + queryEasyTierPeerStats, + readEasyTierNetworkName, + startBundledEasyTier, + stopBundledEasyTier, +} from './easytier' +import { ensureYoutubeStudioWindow } from './youtubeStudioSidecar' +import { + appendClientLog, + appendLiveEvent, + appendUserAction, + flushClientDb, + getAppSetting, + getDashboardSnapshot, + importRelayChannelsFromConfigDir, + initClientDb, + saveDataSnapshot, + setAppSetting, + syncRelayChannelsWithBackendConfig, +} from './clientDb' +import { + applyAppHostBootstrap, + parseNavFromArgv, + registerAppHostIpcHandlers, + sendNavToWindow, + setAppUserModelIdEarly, +} from './appHost' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const execFileAsync = promisify(execFile) + +const PORT = Number(process.env.WEB2_PORT || 8001) +/** uvicorn 监听地址;0.0.0.0 时建议配置 config/web2_api_token.txt 供远程/EasyTier 访问 */ +let web2Host = '127.0.0.1' + +let mainWindow: BrowserWindow | null = null +let pythonChild: ChildProcessWithoutNullStreams | null = null +let stderrBuf = '' +let _backendRoot: string | null = null +let _pythonExe: string | null = null +/** 自启动参数 --nav= 传入的页面,首屏或二次实例时下发渲染进程 */ +let pendingNav: string | null = null + +/** + * 供扫码绑定 baseUrl:优先虚拟网卡(EasyTier 等),其次 10.x,再补充 Wi‑Fi/以太网等局域网 IPv4, + * 避免仅有虚拟网时列表为空、用户无处选 IP。 + */ +function collectEasyTierCandidateIPs(): { address: string; name: string }[] { + const nets = networkInterfaces() + const seen = new Set() + const out: { address: string; name: string }[] = [] + const nameMatch = /easytier|wintun|tun|tap|netbird|wireguard|tailscale|zerotier|sing-tun|nordlynx/i + + for (const [name, addrs] of Object.entries(nets)) { + if (!addrs) continue + for (const a of addrs) { + if (a.family !== 'IPv4' || a.internal) continue + if (nameMatch.test(name) && !seen.has(a.address)) { + seen.add(a.address) + out.push({ address: a.address, name }) + } + } + } + if (out.length === 0) { + for (const [name, addrs] of Object.entries(nets)) { + if (!addrs) continue + for (const a of addrs) { + if (a.family !== 'IPv4' || a.internal) continue + if (a.address.startsWith('10.') && !seen.has(a.address)) { + seen.add(a.address) + out.push({ address: a.address, name: `${name}(10.x,可能含 EasyTier)` }) + } + } + } + } + for (const [name, addrs] of Object.entries(nets)) { + if (!addrs) continue + for (const a of addrs) { + if (a.family !== 'IPv4' || a.internal) continue + if (a.address === '127.0.0.1') continue + if (seen.has(a.address)) continue + seen.add(a.address) + out.push({ address: a.address, name: `${name}(局域网)` }) + } + } + return out +} + +async function restartWeb2Backend(): Promise<{ ok: true } | { ok: false; error: string }> { + const br = _backendRoot ?? getBackendRoot() + const py = _pythonExe ?? resolvePythonExe(br) + web2Host = resolveWeb2Host() + await killPythonProcessForRestart() + startPythonBackend(br, py) + try { + await waitForHealth(90000) + } catch (e) { + return { ok: false, error: e instanceof Error ? e.message : String(e) } + } + return { ok: true } +} + +/** + * 切换监听地址 / 轮换 API 密钥时必须等旧 uvicorn 完全退出并释放端口。 + * Windows 上若仅 spawn taskkill 不等待,旧进程仍占用 8001,新进程启动失败,但 /health 仍由旧进程响应, + * waitForHealth 误判成功,随后旧进程被杀死 → 前端 ERR_CONNECTION_REFUSED。 + */ +async function killPythonProcessForRestart(): Promise { + if (!pythonChild || pythonChild.killed) { + pythonChild = null + return + } + const child = pythonChild + const pid = child.pid + pythonChild = null + if (process.platform === 'win32') { + try { + await execFileAsync('taskkill', ['/PID', String(pid), '/T', '/F'], { + windowsHide: true, + timeout: 25000, + maxBuffer: 1024 * 1024, + }) + } catch { + /* 进程已退出等 */ + } + await new Promise((r) => setTimeout(r, 500)) + } else { + try { + child.kill('SIGTERM') + } catch { + /* ignore */ + } + await new Promise((resolve) => { + const t = setTimeout(() => resolve(), 10000) + child.once('close', () => { + clearTimeout(t) + resolve() + }) + }) + await new Promise((r) => setTimeout(r, 200)) + } +} + +function getBackendRoot(): string { + if (app.isPackaged) { + return path.join(process.resourcesPath, 'backend') + } + // dev: electron/out/main -> 仓库根(上三级) + return path.resolve(__dirname, '../../..') +} + +function resolveWeb2Host(): string { + const e = process.env.WEB2_HOST?.trim() + if (e) return e + const br = getBackendRoot() + const p = path.join(br, 'config', 'web2_bind_host.txt') + try { + if (fs.existsSync(p)) { + const line = fs.readFileSync(p, 'utf-8').trim().split('\n')[0]?.trim() + if (line) return line + } + } catch { + /* ignore */ + } + return '127.0.0.1' +} + +function resolveWeb2ApiTokenForChild(): string | undefined { + const e = process.env.WEB2_API_TOKEN?.trim() + if (e) return e + const br = getBackendRoot() + const p = path.join(br, 'config', 'web2_api_token.txt') + try { + if (fs.existsSync(p)) { + const line = fs.readFileSync(p, 'utf-8').trim().split('\n')[0]?.trim() + if (line) return line + } + } catch { + /* ignore */ + } + return undefined +} + +function resolvePythonExe(backendRoot: string): string { + if (process.env.PYTHON_EXE && fs.existsSync(process.env.PYTHON_EXE)) { + return process.env.PYTHON_EXE + } + const embedded = path.join(backendRoot, 'python', 'python.exe') + if (fs.existsSync(embedded)) return embedded + return process.platform === 'win32' ? 'python' : 'python3' +} + +function stderrTail(): string { + const s = stderrBuf.trim() + if (!s) { + return '若您使用的是完整安装包仍无法启动,请尝试重启电脑后再次打开本软件,或联系技术支持获取帮助。' + } + return s.slice(-4000) +} + +/** 懒人包:把自带的 ffmpeg / 便携 Python 放进 PATH,用户无需自行安装 */ +function buildChildEnv(backendRoot: string): NodeJS.ProcessEnv { + const env = { ...process.env } as NodeJS.ProcessEnv + const sep = path.delimiter + const prepend: string[] = [] + + const ffmpegDirs = [ + path.join(backendRoot, 'tools', 'ffmpeg'), + path.join(backendRoot, 'tools', 'ffmpeg', 'bin'), + ] + for (const dir of ffmpegDirs) { + if (fs.existsSync(dir)) prepend.push(dir) + } + + const portablePython = path.join(backendRoot, 'python') + if (fs.existsSync(portablePython)) { + prepend.push(portablePython) + const scripts = path.join(portablePython, 'Scripts') + if (fs.existsSync(scripts)) prepend.push(scripts) + env.PYTHONHOME = portablePython + } + + if (prepend.length && env.PATH) { + env.PATH = `${prepend.join(sep)}${sep}${env.PATH}` + } + return env +} + +/** 开发模式:等 Vite 监听后再 loadURL,避免先打开窗口却加载失败导致黑屏 */ +function waitForDevServer(devUrl: string, timeoutMs = 60000): Promise { + return new Promise((resolve, reject) => { + let u: URL + try { + u = new URL(devUrl) + } catch { + reject(new Error(`无效的 ELECTRON_RENDERER_URL: ${devUrl}`)) + return + } + const port = u.port ? Number(u.port) : u.protocol === 'https:' ? 443 : 80 + const start = Date.now() + const tryOnce = (): void => { + if (Date.now() - start > timeoutMs) { + reject( + new Error( + `开发服务器在 ${timeoutMs / 1000}s 内未响应(${devUrl})。请先保持 pnpm dev / electron-vite dev 运行,或检查端口是否被占用。`, + ), + ) + return + } + const req = http.get( + { + hostname: u.hostname, + port, + path: '/', + timeout: 2500, + }, + (res) => { + res.resume() + resolve() + }, + ) + req.on('error', () => { + req.destroy() + setTimeout(tryOnce, 350) + }) + req.on('timeout', () => { + req.destroy() + setTimeout(tryOnce, 350) + }) + } + tryOnce() + }) +} + +function healthCheckHost(): string { + return web2Host === '0.0.0.0' ? '127.0.0.1' : web2Host +} + +function waitForHealth(timeoutMs = 120000): Promise { + return new Promise((resolve, reject) => { + const start = Date.now() + const tick = (): void => { + if (Date.now() - start > timeoutMs) { + clearInterval(iv) + reject(new Error(`本地服务在 ${timeoutMs / 1000} 秒内未就绪。\n\n${stderrTail()}`)) + return + } + const req = http.get(`http://${healthCheckHost()}:${PORT}/health`, (res) => { + res.resume() + if (res.statusCode === 200) { + clearInterval(iv) + resolve() + } + }) + req.on('error', () => req.destroy()) + } + const iv = setInterval(tick, 400) + tick() + }) +} + +function startPythonBackend(backendRoot: string, pythonExe: string): void { + stderrBuf = '' + const apiTok = resolveWeb2ApiTokenForChild() + if (web2Host === '0.0.0.0' && !apiTok) { + console.warn( + '[直播录制助手] web2 监听 0.0.0.0 但未设置 WEB2_API_TOKEN 或 config/web2_api_token.txt,API 无鉴权,请勿暴露到公网。', + ) + } + const env = { + ...buildChildEnv(backendRoot), + PYTHONUNBUFFERED: '1', + WEB2_SUPERVISOR: 'builtin', + PYTHONIOENCODING: 'utf-8', + ...(apiTok ? { WEB2_API_TOKEN: apiTok } : {}), + } + const args = ['-m', 'uvicorn', 'web2:app', '--host', web2Host, '--port', String(PORT)] + pythonChild = spawn(pythonExe, args, { + cwd: backendRoot, + env, + windowsHide: true, + }) + pythonChild.stderr?.on('data', (chunk: Buffer) => { + stderrBuf += chunk.toString() + }) + pythonChild.stdout?.on('data', (chunk: Buffer) => { + stderrBuf += chunk.toString() + }) + pythonChild.on('error', (err: Error) => { + stderrBuf += `\nspawn error: ${err.message}` + }) + pythonChild.on('close', (code) => { + if (!mainWindow || mainWindow.isDestroyed()) return + if (code !== 0 && code !== null) { + dialog.showErrorBox('直播录制助手', `软件内部服务已停止运行。\n\n${stderrTail()}`) + } + }) +} + +function killPythonTree(): void { + if (!pythonChild || pythonChild.killed) return + try { + if (process.platform === 'win32') { + spawn('taskkill', ['/PID', String(pythonChild.pid), '/T', '/F'], { + windowsHide: true, + stdio: 'ignore', + }) + } else { + pythonChild.kill('SIGTERM') + } + } catch { + /* ignore */ + } + pythonChild = null +} + +function resolvePreload(): string { + const base = path.join(__dirname, '../preload') + const js = path.join(base, 'index.js') + const mjs = path.join(base, 'index.mjs') + // 构建产物为 CJS 的 index.js;若目录里残留旧版 index.mjs,须优先使用 index.js + if (fs.existsSync(js)) return js + if (fs.existsSync(mjs)) return mjs + return js +} + +async function createWindow(): Promise { + mainWindow = new BrowserWindow({ + width: 1020, + height: 720, + minWidth: 720, + minHeight: 560, + show: false, + backgroundColor: '#14161c', + title: '直播录制助手', + autoHideMenuBar: true, + webPreferences: { + preload: resolvePreload(), + contextIsolation: true, + nodeIntegration: false, + }, + }) + mainWindow.once('ready-to-show', () => mainWindow?.show()) + mainWindow.webContents.once('did-finish-load', () => { + appendClientLog('info', 'main', '渲染界面已加载') + if (pendingNav && mainWindow && !mainWindow.isDestroyed()) { + sendNavToWindow(mainWindow, pendingNav) + pendingNav = null + } + }) + + const devUrl = process.env.ELECTRON_RENDERER_URL + if (devUrl) { + try { + await waitForDevServer(devUrl) + } catch (e) { + dialog.showErrorBox('直播录制助手', e instanceof Error ? e.message : String(e)) + mainWindow.destroy() + mainWindow = null + return + } + mainWindow.webContents.once('did-finish-load', () => { + if (process.env.NODE_ENV === 'development' || devUrl.includes('localhost')) { + mainWindow?.webContents.openDevTools({ mode: 'detach' }) + } + }) + try { + for (let i = 0; i < 8; i++) { + try { + await mainWindow.loadURL(devUrl) + break + } catch { + if (i === 7) { + dialog.showErrorBox( + '直播录制助手', + `无法加载开发页面:${devUrl}\n请确认 Vite 已启动且端口未被占用。`, + ) + mainWindow.destroy() + mainWindow = null + return + } + await new Promise((r) => setTimeout(r, 400)) + } + } + } catch (e) { + dialog.showErrorBox('直播录制助手', e instanceof Error ? e.message : String(e)) + mainWindow.destroy() + mainWindow = null + return + } + } else { + mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')) + } + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +const gotLock = app.requestSingleInstanceLock() +if (!gotLock) { + app.quit() +} else { + setAppUserModelIdEarly() + pendingNav = parseNavFromArgv(process.argv) + + app.on('second-instance', (_evt, commandLine) => { + const argv = Array.isArray(commandLine) + ? commandLine + : typeof commandLine === 'string' + ? commandLine.split(/\s+/).filter(Boolean) + : [String(commandLine)] + const nav = parseNavFromArgv(argv) + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + if (nav) sendNavToWindow(mainWindow, nav) + } + }) + + app.whenReady().then(async () => { + applyAppHostBootstrap(getBackendRoot) + registerAppHostIpcHandlers(getBackendRoot) + try { + await initClientDb() + try { + syncRelayChannelsWithBackendConfig(path.join(getBackendRoot(), 'config')) + } catch (e) { + console.error('[relay sync]', e) + } + appendClientLog('info', 'main', '客户端主进程就绪') + } catch (e) { + console.error('[clientDb]', e) + } + + ipcMain.removeHandler('workspace:log-live') + ipcMain.handle('workspace:log-live', (_evt, payload: unknown) => { + if (!payload || typeof payload !== 'object') return + const p = payload as Record + const eventType = typeof p.eventType === 'string' ? p.eventType : 'unknown' + appendLiveEvent({ + eventType, + process: typeof p.process === 'string' ? p.process : undefined, + status: typeof p.status === 'string' ? p.status : undefined, + message: typeof p.message === 'string' ? p.message : undefined, + detail: p.detail, + }) + }) + + ipcMain.removeHandler('workspace:log-user') + ipcMain.handle('workspace:log-user', (_evt, payload: unknown) => { + if (!payload || typeof payload !== 'object') return + const p = payload as Record + const action = typeof p.action === 'string' ? p.action : 'unknown' + appendUserAction({ + action, + nav: typeof p.nav === 'string' ? p.nav : undefined, + detail: p.detail, + }) + }) + + ipcMain.removeHandler('workspace:log-client') + ipcMain.handle('workspace:log-client', (_evt, payload: unknown) => { + if (!payload || typeof payload !== 'object') return + const p = payload as Record + const level = typeof p.level === 'string' ? p.level : 'info' + const source = typeof p.source === 'string' ? p.source : 'renderer' + const message = typeof p.message === 'string' ? p.message : '' + if (message) appendClientLog(level, source, message) + }) + + ipcMain.removeHandler('workspace:save-snapshot') + ipcMain.handle('workspace:save-snapshot', (_evt, key: unknown, data: unknown) => { + if (typeof key !== 'string' || !key.trim()) return + saveDataSnapshot(key.trim(), data) + }) + + ipcMain.removeHandler('workspace:get-dashboard') + ipcMain.handle('workspace:get-dashboard', () => getDashboardSnapshot()) + + ipcMain.removeHandler('app:get-setting') + ipcMain.handle('app:get-setting', (_evt, key: unknown) => { + if (typeof key !== 'string' || !key.trim()) return null + return getAppSetting(key.trim()) + }) + + ipcMain.removeHandler('app:set-setting') + ipcMain.handle('app:set-setting', (_evt, key: unknown, value: unknown) => { + if (typeof key !== 'string' || !key.trim()) return + if (typeof value !== 'string') return + setAppSetting(key.trim(), value) + }) + + ipcMain.removeHandler('relay:sync-config') + ipcMain.handle('relay:sync-config', () => { + const br = _backendRoot ?? getBackendRoot() + syncRelayChannelsWithBackendConfig(path.join(br, 'config')) + return { ok: true as const } + }) + + ipcMain.removeHandler('relay:import-from-config') + ipcMain.handle('relay:import-from-config', () => { + const br = _backendRoot ?? getBackendRoot() + importRelayChannelsFromConfigDir(path.join(br, 'config')) + return { ok: true as const } + }) + + ipcMain.removeHandler('easytier:get-peer-stats') + ipcMain.handle('easytier:get-peer-stats', () => queryEasyTierPeerStats()) + + ipcMain.removeHandler('youtube-studio:ensure') + ipcMain.handle('youtube-studio:ensure', (_evt, suffix: unknown) => { + ensureYoutubeStudioWindow(typeof suffix === 'string' ? suffix : undefined) + }) + + ipcMain.removeHandler('open-external') + ipcMain.handle('open-external', async (_evt, url: unknown) => { + if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) { + throw new Error('无效的链接地址') + } + await shell.openExternal(url) + }) + + ipcMain.removeHandler('web2:get-api-token') + ipcMain.handle('web2:get-api-token', () => resolveWeb2ApiTokenForChild() ?? null) + + ipcMain.removeHandler('remote:get-status') + ipcMain.handle('remote:get-status', () => { + const bindHost = resolveWeb2Host() + const et = getEasyTierRuntimeState() + const br = _backendRoot ?? getBackendRoot() + const easytierNetworkName = readEasyTierNetworkName(br) + return { + bindHost, + remoteEnabled: bindHost === '0.0.0.0', + port: PORT, + hasToken: !!resolveWeb2ApiTokenForChild(), + candidates: collectEasyTierCandidateIPs(), + envHostOverride: !!process.env.WEB2_HOST?.trim(), + easytierBundled: et.bundled, + easytierRunning: et.running, + easytierLastError: et.lastError, + easytierNetworkName, + } + }) + + ipcMain.removeHandler('remote:get-bind-payload') + ipcMain.handle('remote:get-bind-payload', (_evt, selectedIp: unknown) => { + if (typeof selectedIp !== 'string' || !selectedIp.trim()) return null + const br = _backendRoot ?? getBackendRoot() + const cfg = loadOrCreateNetworkConfig(br) + const token = resolveWeb2ApiTokenForChild() ?? '' + const ip = selectedIp.trim() + return { + v: 1 as const, + baseUrl: `http://${ip}:${PORT}`, + port: PORT, + token, + easytier: { + networkName: cfg.networkName, + networkSecret: cfg.networkSecret, + peerUrls: cfg.peerUrls, + }, + } + }) + + ipcMain.removeHandler('remote:set-enabled') + ipcMain.handle('remote:set-enabled', async (_evt, enabled: unknown) => { + if (process.env.WEB2_HOST?.trim()) { + return { + ok: false, + error: '已设置环境变量 WEB2_HOST,请改用环境变量或删除后再用界面开关。', + } + } + if (typeof enabled !== 'boolean') { + return { ok: false, error: '参数无效' } + } + const br = _backendRoot ?? getBackendRoot() + const cfgDir = path.join(br, 'config') + fs.mkdirSync(cfgDir, { recursive: true }) + fs.writeFileSync(path.join(cfgDir, 'web2_bind_host.txt'), enabled ? '0.0.0.0' : '127.0.0.1', 'utf-8') + const r = await restartWeb2Backend() + if (r.ok && enabled) startBundledEasyTier(br) + return r + }) + + ipcMain.removeHandler('remote:generate-token') + ipcMain.handle('remote:generate-token', async () => { + const br = _backendRoot ?? getBackendRoot() + const cfgDir = path.join(br, 'config') + fs.mkdirSync(cfgDir, { recursive: true }) + const tok = crypto.randomBytes(24).toString('hex') + fs.writeFileSync(path.join(cfgDir, 'web2_api_token.txt'), tok, 'utf-8') + const r = await restartWeb2Backend() + if (r.ok) startBundledEasyTier(br) + return r + }) + + web2Host = resolveWeb2Host() + + const backendRoot = getBackendRoot() + const pythonExe = resolvePythonExe(backendRoot) + _backendRoot = backendRoot + _pythonExe = pythonExe + + if (!fs.existsSync(path.join(backendRoot, 'web2.py'))) { + dialog.showErrorBox( + '直播录制助手', + `未找到程序必要文件,当前安装可能不完整。\n\n路径:${backendRoot}\n\n开发调试:请在项目根目录保留 web2.py,并在 electron 目录执行 pnpm dev。`, + ) + app.quit() + return + } + + startPythonBackend(backendRoot, pythonExe) + + try { + await waitForHealth() + } catch (e) { + dialog.showErrorBox( + '直播录制助手', + `软件未能正常启动。\n\n${e instanceof Error ? e.message : String(e)}`, + ) + killPythonTree() + app.quit() + return + } + + startBundledEasyTier(backendRoot) + + appendClientLog('info', 'main', '本地服务已连接,准备打开界面') + + await createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) void createWindow() + }) + }) + + app.on('window-all-closed', () => { + stopBundledEasyTier() + killPythonTree() + app.quit() + }) + + app.on('before-quit', () => { + appendClientLog('info', 'main', '应用退出') + flushClientDb() + stopBundledEasyTier() + killPythonTree() + }) +} diff --git a/electron/src/main/youtubeStudioSidecar.ts b/electron/src/main/youtubeStudioSidecar.ts new file mode 100644 index 0000000..df468c4 --- /dev/null +++ b/electron/src/main/youtubeStudioSidecar.ts @@ -0,0 +1,120 @@ +/** + * YouTube Studio 直播后台侧窗:自动点击 Dismiss(对齐 GreasyFork「YouTube Studio Auto Dismiss」逻辑), + * 并可检测页面文本是否包含串流密钥后缀以便与本地配置对应。 + */ +import { BrowserWindow, app } from 'electron' +let studioWin: BrowserWindow | null = null + +const STUDIO_ENTRY = 'https://studio.youtube.com/livestreaming' + +/** 与油猴脚本等价的定时查找 Dismiss 按钮并点击(含 Shadow DOM) */ +const AUTO_DISMISS_JS = ` +(function(){ + if (window.__douyinRecorderYtDismiss) return; + window.__douyinRecorderYtDismiss = true; + function isVisible(el) { + if (!el) return false; + try { + var s = window.getComputedStyle(el); + return s.display !== 'none' && s.visibility !== 'hidden' && el.offsetParent !== null; + } catch (e) { return false; } + } + function findDismiss(root) { + root = root || document; + var buttons = root.querySelectorAll('button'); + for (var i = 0; i < buttons.length; i++) { + var el = buttons[i]; + try { + var text = (el.innerText || '').trim(); + if (text === 'Dismiss' && isVisible(el)) return el; + if (el.shadowRoot) { + var x = findDismiss(el.shadowRoot); + if (x) return x; + } + } catch (e) {} + } + return null; + } + function clickEl(el) { + if (!el) return; + el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); + } + function tick() { + var b = findDismiss(); + if (b) clickEl(b); + } + if (document.readyState === 'complete') tick(); + else window.addEventListener('load', tick); + setInterval(tick, 5000); +})(); +true; +` + +const CHECK_KEY_JS = (suffix: string) => ` +(function(){ + var s = (${JSON.stringify(suffix)} || '').trim(); + if (!s) return { ok: true, hasSuffix: true }; + try { + var t = document.body ? document.body.innerText : ''; + return { ok: true, hasSuffix: t.indexOf(s) >= 0 }; + } catch (e) { + return { ok: false, hasSuffix: false }; + } +})(); +` + +export function ensureYoutubeStudioWindow(streamKeySuffix: string | undefined): void { + const suffix = (streamKeySuffix || '').trim().slice(-12) + + const inject = (wc: Electron.WebContents): void => { + const u = wc.getURL() + if (!u.includes('studio.youtube.com')) return + void wc.executeJavaScript(AUTO_DISMISS_JS, true).catch(() => {}) + if (suffix) { + void wc.executeJavaScript(CHECK_KEY_JS(suffix), true).catch(() => {}) + } + } + + if (studioWin && !studioWin.isDestroyed()) { + studioWin.show() + studioWin.focus() + const wc = studioWin.webContents + const cur = wc.getURL() + if (cur.includes('studio.youtube.com')) { + inject(wc) + } else { + wc.once('did-finish-load', () => inject(wc)) + void wc.loadURL(STUDIO_ENTRY) + } + return + } + + studioWin = new BrowserWindow({ + width: 1100, + height: 720, + minWidth: 800, + minHeight: 560, + show: true, + title: 'YouTube Studio 直播', + autoHideMenuBar: true, + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + /* 需登录 Google;sandbox 过严时可能导致 OAuth/ Cookie 异常 */ + sandbox: false, + partition: 'persist:yt-studio', + }, + }) + + if (app.isPackaged) { + studioWin.removeMenu() + } + + const wc = studioWin.webContents + wc.once('did-finish-load', () => inject(wc)) + void wc.loadURL(STUDIO_ENTRY) + + studioWin.on('closed', () => { + studioWin = null + }) +} diff --git a/electron/src/preload/index.ts b/electron/src/preload/index.ts new file mode 100644 index 0000000..a1f2c0d --- /dev/null +++ b/electron/src/preload/index.ts @@ -0,0 +1,60 @@ +import { contextBridge, ipcRenderer } from 'electron' + +contextBridge.exposeInMainWorld('recorderConsole', { + platform: process.platform, + /** 沙箱下 preload 无可靠 shell,经主进程打开系统浏览器 */ + openExternal: (url: string) => ipcRenderer.invoke('open-external', url), +}) + +contextBridge.exposeInMainWorld('web2Remote', { + getApiToken: (): Promise => ipcRenderer.invoke('web2:get-api-token'), +}) + +contextBridge.exposeInMainWorld('remoteBinding', { + getStatus: () => ipcRenderer.invoke('remote:get-status'), + setRemoteEnabled: (enabled: boolean) => ipcRenderer.invoke('remote:set-enabled', enabled), + generateToken: () => ipcRenderer.invoke('remote:generate-token'), + getBindPayload: (selectedIp: string) => ipcRenderer.invoke('remote:get-bind-payload', selectedIp), +}) + +contextBridge.exposeInMainWorld('workspace', { + logLiveEvent: (payload: unknown) => ipcRenderer.invoke('workspace:log-live', payload), + logUserAction: (payload: unknown) => ipcRenderer.invoke('workspace:log-user', payload), + logClient: (payload: unknown) => ipcRenderer.invoke('workspace:log-client', payload), + saveSnapshot: (key: string, data: unknown) => ipcRenderer.invoke('workspace:save-snapshot', key, data), + getDashboard: () => ipcRenderer.invoke('workspace:get-dashboard'), +}) + +/** 工作台 EasyTier / YouTube Studio 侧窗(无 preload 的独立窗口不暴露此 API) */ +contextBridge.exposeInMainWorld('deskBridge', { + getEasyTierPeers: () => ipcRenderer.invoke('easytier:get-peer-stats'), + ensureYoutubeStudio: (streamKeySuffix?: string) => + ipcRenderer.invoke('youtube-studio:ensure', streamKeySuffix ?? ''), +}) + +contextBridge.exposeInMainWorld('appTheme', { + get: () => ipcRenderer.invoke('app:get-setting', 'theme'), + set: (value: string) => ipcRenderer.invoke('app:set-setting', 'theme', value), +}) + +contextBridge.exposeInMainWorld('relayConfig', { + syncConfig: () => ipcRenderer.invoke('relay:sync-config'), + importFromConfig: () => ipcRenderer.invoke('relay:import-from-config'), +}) + +contextBridge.exposeInMainWorld('appHost', { + onNavigate: (cb: (nav: string) => void) => { + const listener = (_e: unknown, nav: string) => cb(nav) + ipcRenderer.on('app:nav', listener) + return () => ipcRenderer.removeListener('app:nav', listener) + }, + getLoginItemSettings: () => ipcRenderer.invoke('app:get-login-item'), + setLoginItemOpenAtLogin: (v: boolean) => ipcRenderer.invoke('app:set-login-item', v), + relaunch: () => ipcRenderer.invoke('app:relaunch'), + getMetrics: () => ipcRenderer.invoke('app:get-metrics'), + showAbout: () => ipcRenderer.invoke('app:show-about'), + setBadge: (n: number) => ipcRenderer.invoke('app:set-badge', n), + getPaths: () => ipcRenderer.invoke('app:get-paths'), + addRecentConfig: () => ipcRenderer.invoke('app:add-recent-config'), + openLogsFolder: () => ipcRenderer.invoke('app:open-logs-folder'), +}) diff --git a/electron/src/renderer/index.html b/electron/src/renderer/index.html new file mode 100644 index 0000000..fc7d01b --- /dev/null +++ b/electron/src/renderer/index.html @@ -0,0 +1,19 @@ + + + + + + + 直播录制助手 + + + +
+ + + diff --git a/electron/src/renderer/src/App.css b/electron/src/renderer/src/App.css new file mode 100644 index 0000000..9eafc2f --- /dev/null +++ b/electron/src/renderer/src/App.css @@ -0,0 +1,2313 @@ +/* 直播录制助手 — 借鉴 Clash Verge 类客户端:深色侧栏 + 主工作区(离线无外链字体) */ + +:root { + --font-ui: "Segoe UI", "Microsoft YaHei UI", "PingFang SC", system-ui, sans-serif; + --font-mono: "Cascadia Mono", "Consolas", ui-monospace, monospace; + + --bg-app-0: #12141a; + --bg-app-1: #16181f; + --bg-sidebar: #1b1f28; + --bg-sidebar-hover: #232830; + --bg-workspace: #14161c; + --bg-chrome: #1a1d24; + --bg-card: #22262e; + --bg-card-elevated: #282c36; + --bg-card-muted: #2a2f3a; + --bg-input: #1e222a; + --bg-log-0: #1e2128; + --bg-log-1: #14161c; + --border-subtle: rgba(255, 255, 255, 0.06); + --border-card: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.12); + --text: #e8eaed; + --text-muted: #8b929e; + --text-on-dark: #c8cdd4; + + --accent: #5b9dff; + --accent-2: #7ab3ff; + --accent-dim: rgba(91, 157, 255, 0.14); + --accent-soft: rgba(91, 157, 255, 0.18); + --accent-glow: rgba(91, 157, 255, 0.35); + --accent-hover: #6aa8ff; + --success: #3dd68c; + --success-soft: rgba(61, 214, 140, 0.12); + --danger: #f56565; + --danger-soft: rgba(245, 101, 101, 0.12); + --warn: #f6ad55; + --warn-soft: rgba(246, 173, 85, 0.12); + + --radius: 8px; + --radius-md: 10px; + --radius-lg: 12px; + --radius-pill: 999px; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.35); + --shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.45); + --shadow-inset-top: inset 0 1px 0 rgba(255, 255, 255, 0.06); + + --ease: cubic-bezier(0.2, 0, 0, 1); + --sidebar-width: 220px; + --bg-header: rgba(20, 22, 28, 0.85); +} + +:root[data-theme="light"] { + --bg-app-0: #eef1f8; + --bg-app-1: #e8ecf4; + --bg-sidebar: #ffffff; + --bg-sidebar-hover: #eef2f9; + --bg-workspace: #f3f5fb; + --bg-chrome: #ffffff; + --bg-card: #ffffff; + --bg-card-elevated: #fafbfd; + --bg-card-muted: #f0f4fa; + --bg-input: #ffffff; + --bg-log-0: #f8fafc; + --bg-log-1: #eef2f7; + --border-subtle: rgba(15, 23, 42, 0.08); + --border-card: rgba(15, 23, 42, 0.1); + --border-strong: rgba(15, 23, 42, 0.14); + --text: #0f172a; + --text-muted: #64748b; + --text-on-dark: #334155; + --accent: #2563eb; + --accent-2: #3b82f6; + --accent-dim: rgba(37, 99, 235, 0.12); + --accent-soft: rgba(37, 99, 235, 0.14); + --accent-glow: rgba(37, 99, 235, 0.22); + --accent-hover: #1d4ed8; + --success: #059669; + --success-soft: rgba(5, 150, 105, 0.1); + --danger: #dc2626; + --danger-soft: rgba(220, 38, 38, 0.1); + --warn: #d97706; + --warn-soft: rgba(217, 119, 6, 0.12); + --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06); + --shadow-md: 0 8px 28px rgba(15, 23, 42, 0.08); + --shadow-lg: 0 18px 48px rgba(15, 23, 42, 0.1); + --shadow-inset-top: inset 0 1px 0 rgba(255, 255, 255, 0.9); + --bg-header: rgba(255, 255, 255, 0.92); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + height: 100%; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +::selection { + background: var(--accent-dim); + color: var(--text); +} + +body { + margin: 0; + min-height: 100vh; + height: 100%; + font-family: var(--font-ui); + font-size: 14px; + line-height: 1.55; + color: var(--text); + background-color: var(--bg-workspace); + background-image: radial-gradient(ellipse 80% 50% at 50% -10%, rgba(91, 157, 255, 0.08), transparent 50%); + transition: + background-color 0.38s var(--ease), + color 0.28s var(--ease); +} + +:root[data-theme="light"] body { + background-image: radial-gradient(ellipse 85% 48% at 50% -12%, rgba(37, 99, 235, 0.12), transparent 52%); +} + +body.ready { + opacity: 1; +} + +#root { + height: 100%; + min-height: 100vh; +} + +/* 根布局(旧顶栏已弃用,改侧栏) */ +.app-frame { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.app-frame--verge { + flex-direction: row; + min-height: 100vh; + height: 100vh; + max-height: 100vh; + overflow: hidden; + align-items: stretch; +} + +/* —— Clash Verge 风格侧栏 ——(主区滚动时侧栏固定于视口) */ +.app-sidebar { + width: var(--sidebar-width); + flex-shrink: 0; + display: flex; + flex-direction: column; + position: sticky; + top: 0; + align-self: flex-start; + height: 100vh; + max-height: 100vh; + overflow-x: hidden; + overflow-y: auto; + background: linear-gradient(180deg, var(--bg-sidebar) 0%, #181c24 100%); + border-right: 1px solid var(--border-subtle); + box-shadow: 4px 0 24px rgba(0, 0, 0, 0.25); + transition: background 0.38s var(--ease), box-shadow 0.35s var(--ease); +} + +:root[data-theme="light"] .app-sidebar { + background: linear-gradient(180deg, #ffffff 0%, #f1f5f9 100%); + box-shadow: 4px 0 28px rgba(15, 23, 42, 0.07); +} + +.app-sidebar__brand { + display: flex; + align-items: center; + gap: 12px; + padding: 20px 16px 18px; + border-bottom: 1px solid var(--border-subtle); +} + +.app-sidebar__brand-main { + flex: 1; + min-width: 0; +} + +.app-sidebar__brand-title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.app-sidebar__brand-title-row .app-sidebar__app-name { + flex: 1; + min-width: 0; +} + +.app-sidebar__theme-toggle { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + padding: 0; + border: none; + border-radius: var(--radius-pill); + background: var(--bg-card-muted); + color: var(--text-muted); + cursor: pointer; + transition: + background 0.22s var(--ease), + color 0.2s var(--ease), + transform 0.18s var(--ease), + box-shadow 0.2s var(--ease); + box-shadow: 0 0 0 1px var(--border-subtle); +} + +.app-sidebar__theme-toggle:hover { + color: var(--accent); + background: var(--accent-dim); +} + +.app-sidebar__theme-toggle:active { + transform: scale(0.94); +} + +.app-sidebar__theme-toggle svg { + display: block; +} + +.app-sidebar__logo { + width: 40px; + height: 40px; + border-radius: 12px; + background: linear-gradient(145deg, #3d7eff 0%, #5b9dff 40%, #7c5cff 100%); + box-shadow: 0 4px 16px rgba(91, 157, 255, 0.35); + flex-shrink: 0; +} + +.app-sidebar__app-name { + font-size: 15px; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text); + line-height: 1.25; +} + +.app-sidebar__app-tag { + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; +} + +.app-sidebar__theme { + display: flex; + gap: 8px; + justify-content: center; + padding: 0 12px 12px; + border-bottom: 1px solid var(--border-subtle); +} + +.app-sidebar__theme--icons { + gap: 0; + padding: 0 14px 14px; +} + +.app-sidebar__theme-bar { + display: inline-flex; + align-items: center; + padding: 4px; + border-radius: var(--radius-pill); + background: var(--bg-card-muted); + border: 1px solid var(--border-card); + box-shadow: var(--shadow-sm); + gap: 2px; +} + +.app-sidebar__theme-icon-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 34px; + padding: 0; + border: none; + border-radius: var(--radius-pill); + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: + background 0.22s var(--ease), + color 0.2s var(--ease), + transform 0.2s var(--ease), + box-shadow 0.25s var(--ease); +} + +.app-sidebar__theme-icon-btn:hover { + color: var(--text); + background: var(--bg-sidebar-hover); +} + +.app-sidebar__theme-icon-btn.is-active { + color: var(--accent); + background: var(--accent-dim); + box-shadow: inset 0 0 0 1px var(--accent-soft); +} + +.app-sidebar__theme-icon-btn:active { + transform: scale(0.96); +} + +.app-sidebar__theme-icon-btn svg { + display: block; +} + +.app-sidebar__theme-btn { + flex: 1; + padding: 8px 10px; + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + background: var(--bg-card-muted); + color: var(--text-muted); + font-family: var(--font-ui); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: + background 0.22s var(--ease), + color 0.2s var(--ease), + border-color 0.2s var(--ease), + box-shadow 0.25s var(--ease), + transform 0.18s var(--ease); +} + +.app-sidebar__theme-btn:hover { + color: var(--text); + border-color: var(--border-strong); +} + +.app-sidebar__theme-btn.is-active { + color: var(--accent); + background: var(--accent-dim); + border-color: rgba(91, 157, 255, 0.35); + box-shadow: 0 0 0 1px var(--accent-soft); +} + +:root[data-theme="light"] .app-sidebar__theme-btn.is-active { + border-color: rgba(37, 99, 235, 0.35); +} + +.app-sidebar__nav { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px 10px; + flex: 1; +} + +.app-sidebar__link { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: 11px 14px; + border: none; + border-radius: var(--radius-md); + background: transparent; + color: var(--text-muted); + font-family: var(--font-ui); + font-size: 14px; + font-weight: 600; + text-align: left; + cursor: pointer; + transition: + background 0.15s var(--ease), + color 0.15s var(--ease); +} + +.app-sidebar__link:hover { + background: var(--bg-sidebar-hover); + color: var(--text); +} + +.app-sidebar__link.is-active { + background: rgba(91, 157, 255, 0.12); + color: var(--accent); + box-shadow: inset 3px 0 0 var(--accent); +} + +.app-sidebar__link-label { + position: relative; + z-index: 1; +} + +.app-sidebar__link-glow { + position: absolute; + inset: 0; + border-radius: var(--radius-md); + background: radial-gradient(circle at 0% 50%, rgba(91, 157, 255, 0.15), transparent 65%); + pointer-events: none; +} + +/* 主工作区 */ +.app-workspace { + flex: 1; + min-width: 0; + min-height: 0; + height: 100%; + display: flex; + flex-direction: column; + background: var(--bg-workspace); +} + +.app-workspace__header { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 18px 24px 14px; + border-bottom: 1px solid var(--border-subtle); + background: var(--bg-header); + backdrop-filter: blur(12px); + transition: background 0.35s var(--ease), border-color 0.25s var(--ease); +} + +.app-workspace__title { + margin: 0; + font-size: 20px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--text); +} + +.app-workspace__subtitle { + margin: 4px 0 0; + font-size: 13px; + color: var(--text-muted); +} + +.app-workspace__scroll { + flex: 1; + overflow: auto; + padding: 16px 24px 28px; +} + +.app-workspace__inner { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.app-workspace__inner--center { + align-items: center; + justify-content: center; + padding: 32px 24px; +} + +.app-panel { + max-width: 920px; + margin: 0 auto; +} + +.app-panel > .card { + margin-bottom: 0; +} + +.app-body--record { + max-width: 920px; + margin: 0 auto; + padding: 0; +} + +.app-body { + flex: 1; + padding: 0; + max-width: none; +} + +.app-body--padded { + display: flex; + align-items: flex-start; + justify-content: center; + padding: 40px 24px; +} + +.app-toolbar { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + justify-content: space-between; + gap: 14px; + margin-bottom: 18px; +} + +.app-toolbar__left { + display: flex; + flex-direction: column; + gap: 8px; + min-width: min(100%, 300px); +} + +.app-toolbar__right { + display: flex; + align-items: center; + gap: 10px; +} + +.record-pro-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 600; + color: #e8eaed; + cursor: pointer; + user-select: none; +} + +.record-pro-toggle input { + width: 16px; + height: 16px; + accent-color: var(--accent, #6d9eff); +} + +.card--pro-head { + border-color: rgba(110, 168, 254, 0.35); + background: linear-gradient(165deg, rgba(110, 168, 254, 0.1) 0%, var(--bg-card) 100%); +} + +.select-process--pro { + max-width: min(100%, 520px); +} + + +.field-label { + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + letter-spacing: 0.02em; +} + +.select-process { + appearance: none; + font-family: var(--font-ui); + font-size: 14px; + font-weight: 500; + color: var(--text); + border: 1px solid var(--border-strong); + border-radius: var(--radius-md); + padding: 10px 40px 10px 14px; + min-width: 240px; + max-width: 100%; + cursor: pointer; + box-shadow: var(--shadow-inset-top), var(--shadow-sm); + background-color: var(--bg-input); + background-image: + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238b929e' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"), + linear-gradient(180deg, #2a2f38 0%, #22262e 100%); + background-repeat: no-repeat, no-repeat; + background-position: right 12px center, 0 0; + background-size: 16px 16px, 100% 100%; + transition: + border-color 0.18s var(--ease), + box-shadow 0.2s var(--ease), + transform 0.15s var(--ease); +} + +.select-process:hover:not(:disabled) { + border-color: rgba(91, 157, 255, 0.45); + box-shadow: var(--shadow-inset-top), 0 0 0 1px var(--accent-glow); +} + +.select-process:focus { + outline: none; + border-color: var(--accent); + box-shadow: var(--shadow-inset-top), 0 0 0 3px var(--accent-soft); +} + +.select-process:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +/* 状态胶囊 */ +.status-pill { + display: inline-flex; + align-items: center; + gap: 9px; + font-size: 13px; + font-weight: 650; + padding: 8px 14px 8px 10px; + border-radius: var(--radius-pill); + border: 1px solid var(--border-card); + background: var(--bg-card); + color: var(--text-muted); + box-shadow: var(--shadow-sm), var(--shadow-inset-top); +} + +.status-pill__dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: currentColor; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.35); +} + +.status-pill[data-variant='loading'] .status-pill__dot { + animation: dotPulse 1.4s ease-in-out infinite; +} + +@keyframes dotPulse { + 0%, + 100% { + opacity: 0.45; + transform: scale(0.92); + } + 50% { + opacity: 1; + transform: scale(1); + } +} + +.status-pill[data-variant='online'] .status-pill__dot { + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.85), 0 0 12px rgba(14, 122, 13, 0.55); + animation: dotGlow 2s ease-in-out infinite; +} + +@keyframes dotGlow { + 50% { + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.85), 0 0 16px rgba(14, 122, 13, 0.65); + } +} + +.status-pill[data-variant='loading'] { + color: var(--text-muted); +} + +.status-pill[data-variant='online'] { + border-color: rgba(61, 214, 140, 0.35); + background: rgba(61, 214, 140, 0.1); + color: var(--success); +} + +.status-pill[data-variant='stopped'], +.status-pill[data-variant='offline'] { + border-color: rgba(245, 101, 101, 0.35); + background: rgba(245, 101, 101, 0.1); + color: var(--danger); +} + +.status-pill[data-variant='warn'] { + border-color: rgba(246, 173, 85, 0.35); + background: rgba(246, 173, 85, 0.1); + color: var(--warn); +} + +.status-pill[data-variant='error'] { + border-color: rgba(245, 101, 101, 0.35); + background: rgba(245, 101, 101, 0.12); + color: var(--danger); +} + +.status-pill[data-variant='unknown'], +.status-pill[data-variant='empty'] { + border-color: rgba(91, 157, 255, 0.25); + background: rgba(91, 157, 255, 0.1); + color: var(--accent-2); +} + +/* 任务信息条 */ +.task-meta { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px 14px; + margin-bottom: 18px; + padding: 12px 16px; + background: var(--bg-card-elevated); + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + font-size: 13px; + box-shadow: var(--shadow-sm); +} + +.task-meta__label { + color: var(--text-muted); + font-weight: 600; +} + +.task-meta__name { + font-weight: 650; + color: var(--text); +} + +.task-meta__file { + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-muted); + word-break: break-all; + opacity: 0.92; +} + +/* 骨架 */ +.skeleton-block { + margin: 12px 0 24px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.skeleton { + border-radius: var(--radius-md); + background: linear-gradient( + 110deg, + #2a2f38 0%, + #363c48 40%, + #2a2f38 80%, + #2a2f38 100% + ); + background-size: 200% 100%; + animation: shimmer 1.15s ease-in-out infinite; +} + +.skeleton--title { + height: 22px; + width: 42%; + max-width: 200px; +} + +.skeleton--line { + height: 13px; + width: 100%; +} + +.skeleton--line.short { + width: 68%; +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .skeleton, + .status-pill[data-variant='loading'] .status-pill__dot, + .status-pill[data-variant='online'] .status-pill__dot { + animation: none; + } +} + +/* 空状态 */ +.empty-state { + padding: 32px 16px 16px; + text-align: center; +} + +.empty-state__icon { + width: 56px; + height: 56px; + margin: 0 auto 16px; + border-radius: 16px; + background: linear-gradient(145deg, #2a3444 0%, #1e2632 100%); + box-shadow: var(--shadow-inset-top), 0 8px 24px rgba(0, 0, 0, 0.35); + position: relative; +} + +.empty-state__icon::after { + content: ""; + position: absolute; + inset: 14px; + border-radius: 8px; + border: 2px dashed rgba(91, 157, 255, 0.28); + opacity: 0.85; +} + +.empty-state__title { + margin: 0 0 14px; + font-size: 15px; + font-weight: 650; + color: var(--text-muted); +} + +/* 错误卡片 */ +.error-card { + max-width: 560px; + display: flex; + gap: 16px; + align-items: flex-start; + padding: 20px 22px; + border-radius: var(--radius-lg); + background: var(--bg-card); + border: 1px solid rgba(245, 101, 101, 0.35); + box-shadow: var(--shadow-md); +} + +.error-card__icon { + flex-shrink: 0; + width: 40px; + height: 40px; + border-radius: 12px; + display: grid; + place-items: center; + font-weight: 800; + font-size: 17px; + color: #fff; + background: linear-gradient(145deg, #e85d52 0%, #c42b1c 100%); + box-shadow: 0 6px 18px rgba(196, 43, 28, 0.35); +} + +.error-card__title { + margin: 0 0 8px; + font-size: 17px; + font-weight: 650; + color: var(--text); +} + +.error-card__body { + margin: 0; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.6; + color: var(--text-muted); + white-space: pre-wrap; + word-break: break-word; +} + +/* 主内容 */ +.app-main { + display: flex; + flex-direction: column; + gap: 14px; +} + +.card { + padding: 18px 20px; + background: var(--bg-card); + border: 1px solid var(--border-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + transition: box-shadow 0.2s var(--ease); +} + +.card__title { + margin: 0 0 8px; + font-size: 15px; + font-weight: 650; + letter-spacing: -0.02em; + color: var(--text); +} + +.card__title--inline { + margin: 0; + display: inline; +} + +.card__desc { + margin: 0 0 14px; + font-size: 13px; + color: var(--text-muted); + line-height: 1.5; +} + +.card__field { + margin-top: 4px; +} + +.card__field-label { + display: block; + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + margin-bottom: 8px; +} + +.card--accent { + border-color: rgba(91, 157, 255, 0.25); + background: linear-gradient(180deg, rgba(91, 157, 255, 0.08) 0%, var(--bg-card) 100%); + box-shadow: var(--shadow-md), 0 0 0 1px rgba(91, 157, 255, 0.06); +} + +.card--log .card__desc { + margin-bottom: 12px; +} + +.input-area { + width: 100%; + margin: 0 0 12px; + padding: 12px 14px; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.55; + color: var(--text); + background: var(--bg-input); + border: 1px solid var(--border-strong); + border-radius: var(--radius-md); + resize: vertical; + min-height: 108px; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25); + transition: + border-color 0.18s var(--ease), + box-shadow 0.2s var(--ease); +} + +.input-area:focus { + outline: none; + border-color: var(--accent); + box-shadow: + inset 0 1px 2px rgba(0, 0, 0, 0.25), + 0 0 0 3px var(--accent-soft); +} + +.btn-row { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 10px; +} + +.btn { + font-family: var(--font-ui); + font-size: 13px; + font-weight: 600; + padding: 9px 18px; + border-radius: var(--radius-md); + cursor: pointer; + border: 1px solid transparent; + transition: + background 0.15s var(--ease), + border-color 0.15s var(--ease), + box-shadow 0.2s var(--ease), + transform 0.12s var(--ease); +} + +.btn-secondary { + background: linear-gradient(180deg, #2f3540 0%, #282c34 100%); + color: var(--text); + border-color: var(--border-strong); + box-shadow: var(--shadow-inset-top), var(--shadow-sm); +} + +.btn-secondary:hover { + background: linear-gradient(180deg, #3a404c 0%, #323842 100%); + border-color: rgba(255, 255, 255, 0.18); +} + +.btn-primary { + background: linear-gradient(180deg, #4a8eef 0%, #3d7eff 45%, #2f6fd6 100%); + color: #fff; + border-color: rgba(0, 0, 0, 0.2); + box-shadow: + var(--shadow-inset-top), + 0 4px 16px rgba(91, 157, 255, 0.35); +} + +.btn-primary:hover { + background: linear-gradient(180deg, #5b9dff 0%, #4a8eef 55%, #3d7eff 100%); + box-shadow: + var(--shadow-inset-top), + 0 6px 22px rgba(91, 157, 255, 0.45); +} + +.btn-primary:active, +.btn-secondary:active { + transform: translateY(1px); +} + +.btn.loading { + opacity: 0.72; + pointer-events: none; +} + +.hint-line { + margin: 0; + padding: 10px 12px; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.5; + color: var(--text-muted); + background: var(--bg-card-muted); + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + white-space: pre-wrap; + word-break: break-word; +} + +.mono-block { + margin: 10px 0; + padding: 12px 14px; + font-family: var(--font-mono); + font-size: 13px; + color: var(--accent-2); + background: rgba(91, 157, 255, 0.08); + border: 1px dashed rgba(91, 157, 255, 0.35); + border-radius: var(--radius-md); + word-break: break-all; + box-shadow: var(--shadow-inset-top); +} + +/* 录制按钮区 */ +.control-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; +} + +@media (max-width: 520px) { + .control-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +.ctrl-btn { + position: relative; + padding: 13px 12px; + font-size: 13px; + font-weight: 650; + border: 1px solid transparent; + border-radius: var(--radius-md); + cursor: pointer; + color: #fff; + letter-spacing: 0.01em; + overflow: hidden; + transition: + transform 0.12s var(--ease), + filter 0.15s var(--ease), + box-shadow 0.2s var(--ease); +} + +.ctrl-btn::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, transparent 52%); + pointer-events: none; +} + +.ctrl-btn:hover { + filter: brightness(1.04); + box-shadow: 0 6px 20px rgba(15, 23, 42, 0.12); +} + +.ctrl-btn:active { + transform: scale(0.985); +} + +.ctrl-btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.ctrl-btn--start { + background: linear-gradient(165deg, #2fb84a 0%, #0e7a0d 45%, #0a5c0a 100%); + border-color: rgba(0, 0, 0, 0.12); + box-shadow: var(--shadow-inset-top), 0 4px 14px rgba(14, 122, 13, 0.28); +} + +.ctrl-btn--stop { + background: linear-gradient(165deg, #f07167 0%, #d1342c 45%, #a7281c 100%); + border-color: rgba(0, 0, 0, 0.1); + box-shadow: var(--shadow-inset-top), 0 4px 14px rgba(196, 43, 28, 0.28); +} + +.ctrl-btn--restart { + background: linear-gradient(165deg, #f5a742 0%, #e68600 45%, #c35a12 100%); + border-color: rgba(0, 0, 0, 0.1); + color: #fff; + box-shadow: var(--shadow-inset-top), 0 4px 14px rgba(195, 90, 18, 0.28); +} + +.ctrl-btn--status { + background: linear-gradient(180deg, #323842 0%, #2a2f38 100%); + color: var(--text); + border-color: var(--border-strong); + box-shadow: var(--shadow-inset-top), var(--shadow-sm); +} + +.ctrl-btn--status::before { + opacity: 0.5; +} + +.ctrl-btn--status:hover { + filter: none; + border-color: rgba(91, 157, 255, 0.45); + background: linear-gradient(180deg, #3a404c 0%, #323842 100%); + box-shadow: var(--shadow-inset-top), 0 0 0 1px var(--accent-glow); +} + +.ctrl-btn.loading { + opacity: 0.68; + pointer-events: none; +} + +/* 运行记录 */ +.log-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 6px; +} + +.live-tag { + font-size: 11px; + font-weight: 650; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--accent-2); + padding: 5px 10px; + border-radius: var(--radius-pill); + background: rgba(91, 157, 255, 0.1); + border: 1px solid rgba(91, 157, 255, 0.22); + box-shadow: var(--shadow-inset-top); +} + +.log-wrap { + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid rgba(0, 0, 0, 0.35); + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.06) inset, + 0 12px 40px rgba(0, 0, 0, 0.15); +} + +.log-panel { + margin: 0; + height: min(380px, 50vh); + min-height: 220px; + padding: 14px 16px; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.55; + color: #d0d4dc; + background: linear-gradient(180deg, var(--bg-log-0) 0%, var(--bg-log-1) 100%); + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.35); +} + +.log-panel--muted { + text-align: left; + color: var(--text-muted); + background: var(--bg-card-elevated); + border: 1px solid var(--border-card); + min-height: auto; + height: auto; + font-family: var(--font-ui); + font-size: 13px; + text-shadow: none; + box-shadow: var(--shadow-sm); +} + +.log-panel::-webkit-scrollbar { + width: 10px; + height: 10px; +} +.log-panel::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); +} +.log-panel::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, #5a5d66 0%, #3d4048 100%); + border-radius: 5px; + border: 2px solid transparent; + background-clip: padding-box; +} +.log-panel::-webkit-scrollbar-thumb:hover { + background: #6b6f7a; +} + +/* —— 网络仪表盘 —— */ +.card--network { + margin-bottom: 4px; + border-color: rgba(124, 92, 255, 0.28); + background: linear-gradient(165deg, rgba(124, 92, 255, 0.08) 0%, var(--bg-card) 55%); + box-shadow: var(--shadow-md), 0 0 0 1px rgba(124, 92, 255, 0.08); +} + +.net-dash__header { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; +} + +.net-dash__lead { + margin-top: 4px !important; + max-width: 52rem; +} + +.net-dash__refresh { + flex-shrink: 0; +} + +.net-dash__error { + margin: 0 0 12px; + padding: 10px 12px; + font-size: 13px; + color: var(--danger); + background: var(--danger-soft); + border: 1px solid rgba(245, 101, 101, 0.35); + border-radius: var(--radius-md); +} + +.net-dash__loading { + font-size: 13px; + color: var(--text-muted); + padding: 8px 0 4px; +} + +.net-site-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + margin-bottom: 14px; +} + +@media (max-width: 720px) { + .net-site-row { + grid-template-columns: 1fr; + } +} + +.net-site { + padding: 12px 14px; + border-radius: var(--radius-md); + background: var(--bg-card-elevated); + border: 1px solid var(--border-card); + box-shadow: var(--shadow-sm); +} + +.net-site__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; +} + +.net-site__title { + font-size: 13px; + font-weight: 650; + color: var(--text); +} + +.net-badge { + font-size: 11px; + font-weight: 700; + padding: 3px 8px; + border-radius: var(--radius-pill); + letter-spacing: 0.02em; +} + +.net-badge--ok { + color: var(--success); + background: var(--success-soft); + border: 1px solid rgba(61, 214, 140, 0.35); +} + +.net-badge--bad { + color: var(--danger); + background: var(--danger-soft); + border: 1px solid rgba(245, 101, 101, 0.35); +} + +.net-site__grid { + display: grid; + grid-template-columns: 72px 1fr; + gap: 6px 10px; + font-size: 12px; + align-items: baseline; +} + +.net-k { + color: var(--text-muted); + font-weight: 600; +} + +.net-v { + color: var(--text); + word-break: break-word; +} + +.net-v--mono { + font-family: var(--font-mono); + font-size: 11.5px; +} + +.net-site__err { + margin: 10px 0 0; + font-size: 11px; + line-height: 1.45; + color: var(--warn); +} + +.net-routes { + margin-bottom: 8px; + padding: 12px 14px; + border-radius: var(--radius-md); + background: rgba(91, 157, 255, 0.06); + border: 1px solid rgba(91, 157, 255, 0.15); +} + +.net-routes__label { + display: block; + font-size: 12px; + font-weight: 650; + color: var(--text-muted); + margin-bottom: 6px; +} + +.net-routes__pill { + display: inline-block; + font-size: 13px; + font-weight: 700; + padding: 6px 12px; + border-radius: var(--radius-pill); + margin-bottom: 8px; +} + +.net-routes__pill--yes { + color: var(--success); + background: var(--success-soft); + border: 1px solid rgba(61, 214, 140, 0.35); +} + +.net-routes__pill--no { + color: var(--warn); + background: var(--warn-soft); + border: 1px solid rgba(195, 90, 18, 0.28); +} + +.net-routes__pill--unk { + color: var(--text-muted); + background: var(--bg-card-muted); + border: 1px solid var(--border-card); +} + +.net-routes__note { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: var(--text-muted); +} + +.net-speed__title { + margin: 0 0 10px; + font-size: 13px; + font-weight: 650; + color: var(--text); +} + +.net-speed__grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +@media (max-width: 520px) { + .net-speed__grid { + grid-template-columns: 1fr; + } +} + +.net-speed__col { + padding: 12px 14px; + border-radius: var(--radius-md); + background: var(--bg-card-elevated); + border: 1px solid var(--border-subtle); +} + +.net-speed__head { + font-size: 12px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--accent-2); + margin-bottom: 10px; +} + +.net-metric { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 10px; + padding: 6px 0; + border-bottom: 1px dashed var(--border-subtle); + font-size: 13px; +} + +.net-metric:last-of-type { + border-bottom: none; +} + +.net-metric__k { + color: var(--text-muted); + font-weight: 600; +} + +.net-metric__v { + font-family: var(--font-mono); + font-weight: 600; + color: var(--text); +} + +.net-speed__hint { + margin: 8px 0 0; + font-size: 10px; + line-height: 1.4; + color: var(--warn); + word-break: break-word; +} + +.net-speed__foot { + margin: 12px 0 0; + font-size: 11px; + line-height: 1.5; + color: var(--text-muted); +} + +/* —— 录制代理 + 系统指标 —— */ +.app-panel--network { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.app-panel--workspace { + display: flex; + flex-direction: column; + gap: 0; +} + +.card--workspace { + border-color: rgba(110, 168, 254, 0.25); + background: linear-gradient(165deg, rgba(110, 168, 254, 0.06) 0%, var(--bg-card) 100%); +} + +.ws-hero { + position: relative; + margin-bottom: 0.75rem; + border-radius: var(--radius-md); + overflow: hidden; + min-height: 176px; + border: 1px solid var(--border-subtle); + box-shadow: var(--shadow-sm); +} + +.ws-desk-backdrop { + position: relative; + width: 100%; + min-height: 176px; + border-radius: inherit; +} + +.ws-desk-backdrop__three { + position: absolute; + inset: 0; + border-radius: inherit; +} + +.ws-desk-backdrop__three canvas { + display: block; + width: 100% !important; + height: 100% !important; +} + +.ws-desk-backdrop__d3 { + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; +} + +.ws-desk-backdrop__fade { + position: absolute; + inset: 0; + z-index: 2; + pointer-events: none; + background: linear-gradient(180deg, transparent 0%, var(--bg-card) 92%); + opacity: 0.7; +} + +.ws-desk-backdrop__legend { + position: absolute; + left: 10px; + bottom: 8px; + z-index: 4; + max-width: min(520px, 94%); + font-size: 10px; + line-height: 1.5; + color: var(--text-muted); + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.65); + pointer-events: none; +} + +.ws-desk-backdrop__legend strong { + display: block; + color: var(--text); + font-size: 11px; + margin-bottom: 4px; +} + +.ws-desk-backdrop__legend span { + display: block; +} + +.ws-desk-backdrop__legend-sub { + margin-top: 3px; + opacity: 0.95; +} + +:root[data-theme="light"] .ws-desk-backdrop__legend { + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.9); +} + +.ws-head { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.75rem; +} + +.ws-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 0.75rem; + margin-bottom: 1rem; +} + +.ws-cards--wide { + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); +} + +.ws-kv-list { + margin: 0.35rem 0 0; + padding: 0 0 0 1rem; + font-size: 12px; + line-height: 1.55; + color: #c8cdd5; +} + +.ws-kv-list li { + margin: 0.2rem 0; + display: flex; + flex-wrap: wrap; + gap: 0.35rem 0.5rem; + align-items: baseline; +} + +.ws-kv-list__k { + color: var(--text-muted, #8b929e); + min-width: 3rem; +} + +.ws-kv-list__v { + font-size: 11px; + word-break: break-all; +} + +.ws-peer-stats { + margin: 0.35rem 0 0; + font-size: 13px; + color: #e8eaed; +} + +.ws-tag { + display: inline-block; + padding: 0.1rem 0.45rem; + border-radius: 6px; + font-size: 11px; + font-weight: 600; +} + +.ws-tag--on { + background: rgba(46, 160, 67, 0.2); + color: #7dffb3; +} + +.ws-tag--off { + background: rgba(255, 180, 80, 0.12); + color: #e8c48a; +} + +.ws-card__hint { + margin: 0.5rem 0 0; + font-size: 11px; + line-height: 1.45; + color: var(--text-muted, #8b929e); +} + +.ws-card__errors { + margin: 0 0 0.5rem; + padding: 0.5rem 0.65rem; + border-radius: 8px; + background: rgba(220, 80, 80, 0.12); + border: 1px solid rgba(220, 100, 100, 0.25); + font-size: 12px; + line-height: 1.45; + color: #f0b4b4; +} + +.ws-card__errors p { + margin: 0.2rem 0; +} + +.ws-card__notes { + margin: 0.5rem 0 0; + padding: 0.45rem 0.65rem; + border-radius: 8px; + background: rgba(100, 149, 237, 0.08); + border: 1px solid rgba(100, 149, 237, 0.2); + font-size: 11px; + line-height: 1.45; + color: #a8b8d0; +} + +.ws-card__notes p { + margin: 0.2rem 0; +} + +.ws-pro-live-desc { + margin: 0 0 0.75rem; + font-size: 12px; + color: var(--text-muted, #8b929e); +} + +.ws-card--live { + border-color: rgba(100, 200, 140, 0.22); +} + +.ws-live-row { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.ws-live-row:first-of-type { + margin-top: 0.35rem; + padding-top: 0; + border-top: none; +} + +.ws-live-row__title { + font-size: 14px; + font-weight: 600; + color: #e8eaed; + margin-bottom: 0.35rem; +} + +.ws-live-row__sub { + font-size: 11px; + font-weight: 400; + color: var(--text-muted, #8b929e); + margin-left: 0.35rem; +} + +.ws-live-row__line { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 0.5rem; + align-items: baseline; + font-size: 12px; + margin: 0.2rem 0; + line-height: 1.45; +} + +.ws-live-row__clip { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 720px) { + .ws-live-row__clip { + white-space: normal; + } +} + +.ws-kv-list--stack { + list-style: none; + padding-left: 0; +} + +.ws-kv-list--stack > li { + padding: 0.45rem 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + flex-direction: column; + align-items: stretch; +} + +.ws-kv-list--stack > li:last-child { + border-bottom: none; +} + +.ws-kv-list__row { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 0.5rem; + align-items: baseline; + margin: 0.1rem 0; +} + +.ws-kv-list__sub { + font-size: 11px; + color: var(--text-muted, #8b929e); + min-width: 4.5rem; +} + +.ws-card { + padding: 0.75rem 1rem; + border-radius: 10px; + background: rgba(0, 0, 0, 0.22); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.ws-card__title { + font-size: 13px; + font-weight: 600; + margin: 0 0 0.35rem; + color: #e8eaed; +} + +.ws-card__meta { + margin: 0 0 0.5rem; + font-size: 11px; + color: var(--text-muted, #8b929e); +} + +.ws-card__empty { + margin: 0; + font-size: 12px; + color: var(--text-muted, #8b929e); +} + +.ws-snap-grid { + display: grid; + gap: 0.5rem; + font-size: 12px; +} + +.ws-snap-k { + display: block; + color: var(--text-muted, #8b929e); + font-size: 11px; +} + +.ws-snap-v { + font-weight: 600; + margin-right: 0.35rem; +} + +.ws-snap-meta { + color: #b8c0cc; + font-size: 11px; +} + +.ws-section { + margin-top: 1rem; +} + +.ws-section__title { + margin: 0 0 0.5rem; + font-size: 14px; + font-weight: 600; + color: var(--text); +} + +.ws-chart-wrap { + margin-bottom: 0.75rem; + border-radius: var(--radius-md); + border: 1px solid var(--border-subtle); + background: var(--bg-card-muted); + overflow: hidden; + box-shadow: var(--shadow-sm); +} + +.ws-chart { + height: 200px; + width: 100%; +} + +.ws-table-wrap { + overflow: auto; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(0, 0, 0, 0.2); +} + +.ws-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.ws-table th, +.ws-table td { + padding: 0.45rem 0.6rem; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.ws-table th { + color: var(--text-muted, #8b929e); + font-weight: 500; + white-space: nowrap; +} + +.ws-table tr:last-child td { + border-bottom: none; +} + +.ws-mono { + font-family: ui-monospace, monospace; + font-size: 11px; +} + +.ws-cell-clip { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ws-empty { + margin: 0; + padding: 0.75rem; + font-size: 12px; + color: var(--text-muted, #8b929e); +} + +.ws-pre { + margin: 0; + font-size: 11px; + line-height: 1.4; + max-height: 160px; + overflow: auto; + color: #b8c0cc; +} + +.card--remote { + border-color: rgba(100, 149, 237, 0.28); + background: linear-gradient(165deg, rgba(100, 149, 237, 0.07) 0%, var(--bg-card) 100%); +} + +.remote-link { + color: var(--accent, #6d9eff); + text-decoration: underline; + text-underline-offset: 2px; +} + +.remote-code { + font-family: ui-monospace, monospace; + font-size: 12px; + color: #b8c0cc; +} + +.remote-et-status { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 0.75rem; + margin-bottom: 0.75rem; +} + +.remote-et-pill { + font-size: 12px; + padding: 0.2rem 0.55rem; + border-radius: 999px; +} + +.remote-et-pill.on { + background: rgba(61, 214, 140, 0.15); + color: #9ee6c0; +} + +.remote-et-pill.off { + background: rgba(139, 146, 158, 0.15); + color: #8b929e; +} + +.remote-et-pill.warn { + background: rgba(245, 166, 35, 0.15); + color: #e8c07d; +} + +.remote-et-meta { + font-size: 12px; + color: var(--text-muted, #8b929e); +} + +.remote-warn { + margin: 0 0 0.75rem; + padding: 0.6rem 0.75rem; + border-radius: 8px; + background: rgba(245, 166, 35, 0.12); + border: 1px solid rgba(245, 166, 35, 0.35); + color: #e8c07d; + font-size: 13px; +} + +.remote-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem 1rem; + margin-bottom: 0.75rem; +} + +.wc-proxy-param { + width: 7rem; + max-width: 100%; +} + +.remote-toggle-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + user-select: none; +} + +.remote-toggle-input { + width: 1.1rem; + height: 1.1rem; + accent-color: var(--accent, #6d9eff); +} + +.remote-token-badge { + font-size: 13px; + padding: 0.2rem 0.6rem; + border-radius: 999px; +} + +.remote-token-badge.on { + background: rgba(61, 214, 140, 0.15); + color: #9ee6c0; +} + +.remote-token-badge.off { + background: rgba(245, 101, 101, 0.12); + color: #f0a0a0; +} + +.remote-qr-block { + margin-top: 0.5rem; + padding: 1rem; + border-radius: 10px; + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.06); + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; +} + +.remote-qr-hint { + margin: 0; + font-size: 13px; + color: var(--text-muted, #8b929e); +} + +.remote-qr-img { + border-radius: 8px; + background: #fff; + padding: 8px; +} + +.remote-qr-json { + margin: 0; + padding: 0.75rem; + font-size: 12px; + line-height: 1.45; + max-width: 100%; + overflow: auto; + border-radius: 8px; + background: rgba(0, 0, 0, 0.35); + color: #c8cdd5; +} + +.remote-error { + margin: 0.5rem 0 0; + color: #f56565; + font-size: 13px; +} + +.card--proxy { + border-color: rgba(61, 214, 140, 0.22); + background: linear-gradient(165deg, rgba(61, 214, 140, 0.06) 0%, var(--bg-card) 100%); +} + +.proxy-form { + display: flex; + flex-direction: column; + gap: 10px; + max-width: 36rem; +} + +.proxy-check { + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + user-select: none; +} + +.proxy-input { + font-family: var(--font-mono); + font-size: 13px; + padding: 9px 12px; + border: 1px solid var(--border-strong); + border-radius: var(--radius-md); + background: var(--bg-input); + color: var(--text); +} + +.proxy-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-soft); +} + +.proxy-form__msg { + margin: 4px 0 0; + font-size: 13px; + color: var(--success); +} + +.card--metrics { + border-color: rgba(124, 92, 255, 0.22); + background: linear-gradient(165deg, rgba(124, 92, 255, 0.07) 0%, var(--bg-card) 100%); +} + +/* YouTube OAuth 行 */ +.yt-oauth-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-bottom: 14px; + padding: 12px 14px; + border-radius: var(--radius-md); + background: rgba(91, 157, 255, 0.06); + border: 1px solid rgba(91, 157, 255, 0.15); +} + +.yt-oauth-badge { + font-size: 12px; + font-weight: 650; + padding: 4px 10px; + border-radius: var(--radius-pill); + margin-right: 4px; +} + +.yt-oauth-badge--on { + color: var(--success); + background: var(--success-soft); + border: 1px solid rgba(61, 214, 140, 0.35); +} + +.yt-oauth-badge--off { + color: var(--text-muted); + background: var(--bg-card-muted); + border: 1px solid var(--border-card); +} + +.metrics-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin-bottom: 12px; +} + +@media (max-width: 720px) { + .metrics-grid { + grid-template-columns: 1fr; + } +} + +.metrics-block { + padding: 12px 14px; + border-radius: var(--radius-md); + background: var(--bg-card-elevated); + border: 1px solid var(--border-subtle); +} + +.metrics-block__title { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 6px; +} + +.metrics-big { + font-size: 22px; + font-weight: 750; + letter-spacing: -0.02em; + color: var(--text); +} + +.metrics-sub { + margin-top: 4px; + font-size: 11px; + color: var(--text-muted); + line-height: 1.4; +} + +.metrics-rate { + display: flex; + flex-direction: column; + gap: 4px; + font-family: var(--font-mono); + font-size: 14px; + font-weight: 650; + color: var(--accent-2); +} + +.metrics-swap { + font-size: 12px; + color: var(--text-muted); + margin-bottom: 10px; +} + +.metrics-disks { + margin-top: 8px; +} + +.metrics-disks__title { + font-size: 12px; + font-weight: 650; + margin-bottom: 6px; + color: var(--text); +} + +.metrics-disk-list { + margin: 0; + padding: 0; + list-style: none; + font-size: 12px; + color: var(--text-muted); +} + +.metrics-disk-list li { + display: flex; + flex-wrap: wrap; + gap: 6px 12px; + padding: 6px 0; + border-bottom: 1px dashed var(--border-subtle); +} + +.metrics-disk-list__mp { + font-family: var(--font-mono); + color: var(--text); + min-width: 5rem; +} + +.metrics-ff { + margin-top: 12px; +} + +.metrics-ff__title { + font-size: 12px; + font-weight: 650; + margin-bottom: 6px; +} + +.metrics-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.metrics-table th, +.metrics-table td { + text-align: left; + padding: 6px 8px; + border-bottom: 1px solid var(--border-subtle); +} + +.metrics-table th { + color: var(--text-muted); + font-weight: 600; +} + +/* Electron app 模块:系统页「客户端与系统集成」 */ +.app-host-card { + margin-top: 1rem; +} + +.app-host-grid { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.app-host-block { + padding: 0.65rem 0; + border-top: 1px solid var(--border-subtle); +} + +.app-host-block:first-of-type { + border-top: none; +} + +.app-host-block--wide { + padding-top: 0.75rem; +} + +.app-host-block__title { + font-size: 13px; + font-weight: 600; + margin-bottom: 0.45rem; + color: var(--text); +} + +.app-host-toggle { + display: flex; + align-items: flex-start; + gap: 0.5rem; + cursor: pointer; + font-size: 13px; + color: var(--text-muted); +} + +.app-host-toggle input { + margin-top: 3px; +} + +.app-host-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.app-host-hint { + margin: 0.45rem 0 0; + font-size: 11px; + color: var(--text-muted); + line-height: 1.45; +} + +.app-host-badge-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +.app-host-badge-input { + width: 4rem; + padding: 0.35rem 0.5rem; + border-radius: var(--radius-md); + border: 1px solid var(--border-card); + background: var(--bg-input); + color: var(--text); + font-size: 13px; +} + +.app-host-paths { + margin: 0; + padding: 0; + list-style: none; + font-size: 11px; + line-height: 1.5; +} + +.app-host-paths li { + margin-bottom: 0.4rem; + word-break: break-all; +} + +.app-host-paths__k { + display: inline-block; + min-width: 5.5rem; + color: var(--text-muted); + margin-right: 0.35rem; +} + +.app-host-paths__v { + font-size: 10px; + color: var(--text); +} + +.metrics-table-wrap { + overflow: auto; + border-radius: var(--radius-md); + border: 1px solid var(--border-subtle); +} + +.app-host-metrics { + font-size: 11px; +} diff --git a/electron/src/renderer/src/App.tsx b/electron/src/renderer/src/App.tsx new file mode 100644 index 0000000..4217299 --- /dev/null +++ b/electron/src/renderer/src/App.tsx @@ -0,0 +1,1137 @@ +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { apiFetch, apiUrl } from './api' +import NetworkDashboard from './NetworkDashboard' +import RemoteBinding from './RemoteBinding' +import ProxySettings from './ProxySettings' +import SystemMetrics from './SystemMetrics' +import WeChatChatbot from './WeChatChatbot' +import WorkspaceDashboard from './WorkspaceDashboard' +import { logLiveEvent, logUserAction } from './workspace' +import { + STORAGE_MULTI_CHANNEL_PRO, + UI_DROPDOWN_YOUTUBE_ONLY, + UI_SHOW_YOUTUBE_GOOGLE_OAUTH, +} from './constants' +import { extractDouyinRoomHints, parseYoutubeStreamKeySuffix } from './youtubeConfigUtils' +import { isObsScript, isTiktokScript, isYoutubeScript, pm2NameFromScript } from './scriptUtils' + +type Entry = { script: string; label: string; pm2: string } + +type NavId = 'desk' | 'record' | 'network' | 'proxy' | 'system' | 'wechat' + +type AppTheme = 'light' | 'dark' + +function readInitialTheme(): AppTheme { + try { + const d = document.documentElement.getAttribute('data-theme') + if (d === 'light' || d === 'dark') return d + const x = localStorage.getItem('theme') + if (x === 'light' || x === 'dark') return x + } catch { + /* ignore */ + } + return 'dark' +} + +function ThemeIconSun() { + return ( + + ) +} + +function ThemeIconMoon() { + return ( + + ) +} + +function SidebarBrand({ + tag, + theme, + applyTheme, +}: { + tag: string + theme: AppTheme + applyTheme: (t: AppTheme) => void +}) { + return ( +
+
+
+
+ 直播录制助手 + +
+
{tag}
+
+
+ ) +} + +const NAV_ITEMS: { id: NavId; label: string; hint: string }[] = [ + { id: 'desk', label: '工作台', hint: '事件与综合仪表盘' }, + { id: 'record', label: '录制', hint: '任务与推流控制' }, + { id: 'network', label: '网络', hint: '连通与测速' }, + { id: 'proxy', label: '代理', hint: '仅录制进程' }, + { id: 'system', label: '系统', hint: '负载与网速' }, + { id: 'wechat', label: '微信', hint: 'Chatbot 与扫码' }, +] + +type StatusVariant = + | 'loading' + | 'online' + | 'stopped' + | 'error' + | 'warn' + | 'offline' + | 'unknown' + | 'empty' + +/** 状态文案:避免技术术语,用「正在录制 / 已停止」等说法 */ +function statusPresentation(processStatus: string): { line: string; variant: StatusVariant } { + if (processStatus === 'online') return { line: '正在录制', variant: 'online' } + if (processStatus === 'stopped') return { line: '已停止', variant: 'stopped' } + if (processStatus === 'errored') return { line: '出现异常', variant: 'error' } + if (processStatus === 'launching' || processStatus === 'starting') + return { line: '正在启动…', variant: 'warn' } + if (processStatus === 'waiting restart') return { line: '等待重启', variant: 'warn' } + if (processStatus === 'not_found') return { line: '尚未开始', variant: 'offline' } + if (processStatus === 'invalid') return { line: '无法识别', variant: 'error' } + return { line: '状态未知', variant: 'unknown' } +} + +export default function App() { + const [theme, setTheme] = useState(readInitialTheme) + + const [entries, setEntries] = useState([]) + const [currentProcess, setCurrentProcess] = useState('') + const [statusMeta, setStatusMeta] = useState<{ line: string; variant: StatusVariant }>({ + line: '—', + variant: 'loading', + }) + const [logText, setLogText] = useState('') + const [bootError, setBootError] = useState(null) + const [emptyHint, setEmptyHint] = useState(null) + + const [youtubeText, setYoutubeText] = useState('') + const [urlText, setUrlText] = useState('') + const [youtubeStatus, setYoutubeStatus] = useState('就绪') + const [urlStatus, setUrlStatus] = useState('就绪') + + const [loadingAction, setLoadingAction] = useState(null) + const [saveYoutubeLoading, setSaveYoutubeLoading] = useState(false) + const [saveUrlLoading, setSaveUrlLoading] = useState(false) + + const [ytOauth, setYtOauth] = useState<{ connected: boolean; has_refresh: boolean } | null>(null) + const [fetchKeyLoading, setFetchKeyLoading] = useState(false) + + const [nav, setNav] = useState('record') + const lastLiveStatusRef = useRef<{ process: string; status: string } | null>(null) + const relayOnlineSinceRef = useRef(null) + const [multiChannelPro, setMultiChannelPro] = useState(false) + const [proChannelId, setProChannelId] = useState('') + const [proChannelsList, setProChannelsList] = useState< + { id: string; name: string; keyPreview: string; douyinUrl?: string }[] + >([]) + const [proChannelsError, setProChannelsError] = useState(null) + const [proYoutubeKey, setProYoutubeKey] = useState('') + const [saveProKeyLoading, setSaveProKeyLoading] = useState(false) + + const logRef = useRef(null) + + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme) + try { + localStorage.setItem('theme', theme) + } catch { + /* ignore */ + } + }, [theme]) + + useEffect(() => { + const g = window.appTheme?.get + if (!g) return + void g().then((v) => { + if (v === 'light' || v === 'dark') setTheme(v) + }) + }, []) + + const applyTheme = useCallback((t: AppTheme) => { + setTheme(t) + void window.appTheme?.set?.(t) + }, []) + + useEffect(() => { + const h = window.appHost + if (!h?.onNavigate) return + return h.onNavigate((nav) => { + if ( + nav === 'desk' || + nav === 'record' || + nav === 'network' || + nav === 'proxy' || + nav === 'system' || + nav === 'wechat' + ) { + setNav(nav) + } + }) + }, []) + + const scrollLog = useCallback(() => { + const el = logRef.current + if (el) el.scrollTop = el.scrollHeight + }, []) + + useLayoutEffect(() => { + scrollLog() + }, [logText, scrollLog]) + + const getEntry = useCallback( + (name: string) => entries.find((e) => e.pm2 === name) ?? null, + [entries], + ) + + const ent = useMemo(() => getEntry(currentProcess), [getEntry, currentProcess]) + const script = ent?.script ?? '' + const isYoutube = ent ? isYoutubeScript(script) : false + const isTiktok = ent ? isTiktokScript(script) : false + const isObs = ent ? isObsScript(script) : false + + const loadProcessMonitor = useCallback(async () => { + const res = await apiFetch(apiUrl('/process_monitor')) + if (!res.ok) throw new Error(String(res.status)) + const data = await res.json() + const raw: Entry[] = (data.entries || []).map( + (e: { script: string; label?: string; pm2?: string }) => ({ + script: e.script, + label: e.label || e.script, + pm2: e.pm2 || pm2NameFromScript(e.script), + }), + ) + const filtered = UI_DROPDOWN_YOUTUBE_ONLY ? raw.filter((e) => isYoutubeScript(e.script)) : raw + setEntries(filtered) + return filtered + }, []) + + const loadConfig = useCallback( + async (endpoint: string, setContent: (s: string) => void, setSt: (s: string) => void) => { + try { + const res = await apiFetch(`${apiUrl(endpoint)}?process=${encodeURIComponent(currentProcess)}`) + if (!res.ok) throw new Error(`连接失败 ${res.status}`) + const data = await res.json() + setContent(data.content || '') + setSt('已读到最新内容') + } catch (err) { + setSt(`读取失败:${err instanceof Error ? err.message : String(err)}`) + } + }, + [currentProcess], + ) + + const loadYtOauthStatus = useCallback(async () => { + try { + const res = await apiFetch(apiUrl('/youtube/oauth/status')) + if (!res.ok) throw new Error(String(res.status)) + const data = (await res.json()) as { + connected?: boolean + has_refresh?: boolean + } + setYtOauth({ + connected: !!data.connected, + has_refresh: !!data.has_refresh, + }) + } catch { + setYtOauth(null) + } + }, []) + + const startYoutubeOAuth = useCallback(async () => { + setYoutubeStatus('') + try { + const res = await apiFetch(apiUrl('/youtube/oauth/start')) + const data = (await res.json()) as { url?: string; error?: string } + if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`) + const url = data.url + if (!url) throw new Error('未返回授权地址') + const open = window.recorderConsole?.openExternal + if (open) { + await open(url) + } else { + window.open(url, '_blank', 'noopener,noreferrer') + } + setYoutubeStatus('请在浏览器中完成授权') + } catch (err) { + setYoutubeStatus(`授权失败:${err instanceof Error ? err.message : String(err)}`) + } + }, []) + + const fetchYoutubeStreamKey = useCallback(async () => { + if (multiChannelPro) { + setYoutubeStatus('Pro 模式下请在各线路中编辑密钥;OAuth 拉取会写入 youtube.ini,保存后请对该线路再点一次同步或重新选择线路。') + return + } + setFetchKeyLoading(true) + try { + const res = await apiFetch( + `${apiUrl('/youtube/stream/fetch_key')}?process=${encodeURIComponent(currentProcess)}`, + { method: 'POST' }, + ) + const data = (await res.json()) as { error?: string; message?: string } + if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`) + if (data.message) setYoutubeStatus(data.message) + await loadConfig('/get_config', setYoutubeText, setYoutubeStatus) + } catch (err) { + setYoutubeStatus(`拉取失败:${err instanceof Error ? err.message : String(err)}`) + } finally { + setFetchKeyLoading(false) + } + }, [currentProcess, loadConfig, multiChannelPro]) + + const saveProChannelKey = useCallback(async () => { + if (!proChannelId) return + setSaveProKeyLoading(true) + try { + const res = await apiFetch(apiUrl('/relay_pro/channel_key'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ channelId: proChannelId, youtubeKey: proYoutubeKey }), + }) + const data = (await res.json()) as { error?: string; message?: string } + if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`) + setYoutubeStatus(data.message || '密钥已保存') + void window.relayConfig?.importFromConfig?.() + } catch (err) { + setYoutubeStatus(`保存失败:${err instanceof Error ? err.message : String(err)}`) + } finally { + setSaveProKeyLoading(false) + } + }, [proChannelId, proYoutubeKey]) + + const saveProUrlConfig = useCallback(async () => { + if (!proChannelId) return + setSaveUrlLoading(true) + try { + const res = await apiFetch( + `${apiUrl('/relay_pro/url_config')}?channel_id=${encodeURIComponent(proChannelId)}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: urlText }), + }, + ) + const data = (await res.json()) as { error?: string; message?: string } + if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`) + setUrlStatus(data.message || '已保存') + void window.relayConfig?.importFromConfig?.() + } catch (err) { + setUrlStatus(`保存失败:${err instanceof Error ? err.message : String(err)}`) + } finally { + setSaveUrlLoading(false) + } + }, [proChannelId, urlText]) + + const saveConfig = useCallback( + async ( + endpoint: string, + content: string, + setSt: (s: string) => void, + endLoading: () => void, + ) => { + try { + const res = await apiFetch(`${apiUrl(endpoint)}?process=${encodeURIComponent(currentProcess)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }), + }) + if (!res.ok) throw new Error(`保存失败 ${res.status}`) + const data = await res.json() + setSt(data.message || '已保存') + } catch (err) { + setSt(`未能保存:${err instanceof Error ? err.message : String(err)}`) + } finally { + endLoading() + } + }, + [currentProcess], + ) + + const runPm2Action = useCallback( + async (action: string) => { + const isStatus = action === 'status' + if (!isStatus) setLoadingAction(action) + try { + if (action === 'start' && isYoutube && multiChannelPro) { + if (!proChannelId) throw new Error('请先在下拉框中选择一条转播线路') + const sync = await apiFetch( + `${apiUrl('/relay_pro/sync_active')}?channel_id=${encodeURIComponent(proChannelId)}`, + { method: 'POST' }, + ) + const sj = (await sync.json().catch(() => ({}))) as { error?: string } + if (!sync.ok) throw new Error(sj.error || '同步 youtube.ini / URL_config.ini 失败') + } + + const res = await apiFetch(`${apiUrl(`/${action}`)}?process=${encodeURIComponent(currentProcess)}`) + if (!res.ok) throw new Error('无法连接到本机服务') + const data = await res.json() + + if (action === 'status') { + const ps = data.process_status as string + const prev = lastLiveStatusRef.current + const prevPs = prev?.process === currentProcess ? prev.status : undefined + if (prevPs !== 'online' && ps === 'online') { + relayOnlineSinceRef.current = Date.now() + } + if (prevPs === 'online' && (ps === 'stopped' || ps === 'errored')) { + const t0 = relayOnlineSinceRef.current + if (t0) { + const sec = Math.round((Date.now() - t0) / 1000) + logLiveEvent({ + eventType: 'relay_session', + process: currentProcess, + message: `转播结束,约 ${sec} 秒`, + detail: { durationSec: sec }, + }) + } + relayOnlineSinceRef.current = null + } + if (!prev || prev.process !== currentProcess || prev.status !== ps) { + lastLiveStatusRef.current = { process: currentProcess, status: ps } + logLiveEvent({ eventType: 'process_status', process: currentProcess, status: ps }) + } + const { line, variant } = statusPresentation(ps) + setStatusMeta({ line, variant }) + + let textOut = `── 状态一览 · ${new Date().toLocaleTimeString()} ──\n${data.raw_status}\n\n` + if (ps === 'not_found') { + const e = getEntry(currentProcess) + const hint = e ? `「${e.label}」` : '当前任务' + textOut += `提示:${hint} 还没有开始运行。请先点「开始录制」。\n` + } else { + const rl = data.recent_log as string + const re = data.recent_error as string + textOut += `【正常运行时的输出】\n` + if (rl.includes('不存在') || rl.includes('无日志路径')) { + textOut += `${rl}` + } else { + textOut += `${rl || '(暂无新内容)'}` + } + textOut += `\n\n【异常或报错信息】\n` + if (re.includes('不存在') || re.includes('无日志路径')) { + textOut += `${re}` + } else { + textOut += `${re || '(暂无报错)'}` + } + } + setLogText(textOut) + } else { + const e = getEntry(currentProcess) + const who = e ? e.label : '当前任务' + const out = + typeof data.output === 'string' + ? data.output + : [data.pm2_output, data.message, data.note] + .filter((x): x is string => typeof x === 'string') + .join('\n') || '已完成' + const actionLabel = + action === 'start' ? '开始' : action === 'stop' ? '停止' : action === 'restart' ? '重新开始' : action + logLiveEvent({ + eventType: 'control', + process: currentProcess, + message: `${actionLabel} 已执行`, + detail: { action }, + }) + if (action === 'start' && isYoutube) { + const keySrc = multiChannelPro ? proYoutubeKey : youtubeText + const suffix = parseYoutubeStreamKeySuffix(keySrc) + const rooms = extractDouyinRoomHints(urlText) + const desk = ( + window as Window & { + deskBridge?: { ensureYoutubeStudio?: (s?: string) => Promise } + } + ).deskBridge + void desk?.ensureYoutubeStudio?.(suffix) + logLiveEvent({ + eventType: 'relay_intent', + process: currentProcess, + message: `推流意图(须一对一:不同抖音↔不同 YouTube key):抖音 ${rooms.join('、') || '(未解析房间)'} → YouTube …${suffix || '?'}`, + detail: { + douyinRooms: rooms, + keySuffix: suffix, + multiChannelPro, + channelId: multiChannelPro ? proChannelId : undefined, + pairingPolicy: 'one_douyin_one_youtube', + }, + }) + } + setLogText(`已对「${who}」执行:${actionLabel}\n\n${out}`) + setTimeout(() => void runPm2Action('status'), 1500) + } + } catch (err) { + setLogText(`操作失败:${err instanceof Error ? err.message : String(err)}\n\n请检查软件是否完整安装,或稍后重试。`) + setStatusMeta({ line: '状态未知', variant: 'unknown' }) + } finally { + if (!isStatus) setLoadingAction(null) + } + }, + [ + currentProcess, + getEntry, + isYoutube, + youtubeText, + urlText, + multiChannelPro, + proChannelId, + proYoutubeKey, + ], + ) + + useEffect(() => { + document.body.classList.add('ready') + }, []) + + useEffect(() => { + try { + const on = localStorage.getItem(STORAGE_MULTI_CHANNEL_PRO) === '1' + setMultiChannelPro(on) + document.body.classList.toggle('record-pro-mode', on) + } catch { + setMultiChannelPro(false) + } + }, []) + + useEffect(() => { + try { + document.body.classList.toggle('record-pro-mode', multiChannelPro) + } catch { + /* ignore */ + } + }, [multiChannelPro]) + + useEffect(() => { + logUserAction('navigate', nav) + }, [nav]) + + useEffect(() => { + let cancelled = false + ;(async () => { + try { + const filtered = await loadProcessMonitor() + if (cancelled) return + if (!filtered.length) { + setEmptyHint( + UI_DROPDOWN_YOUTUBE_ONLY + ? '没有发现 YouTube 录制任务。\n若您刚放入程序文件,请关闭软件后重新打开一次。' + : '没有发现可用的录制任务。请确认软件安装目录完整。', + ) + setStatusMeta({ line: '暂无可选任务', variant: 'empty' }) + return + } + setEmptyHint(null) + setCurrentProcess(filtered[0].pm2) + } catch (e) { + setBootError( + `软件内部的录制服务没有启动成功。\n\n若您使用的是完整安装包,请尝试重启电脑后再打开;仍不行请联系技术支持。\n\n(技术信息:${e instanceof Error ? e.message : String(e)})`, + ) + setStatusMeta({ line: '未连接', variant: 'offline' }) + } + })() + return () => { + cancelled = true + } + }, [loadProcessMonitor]) + + useEffect(() => { + if (!entries.length || !currentProcess) return + void runPm2Action('status') + }, [entries.length, currentProcess, runPm2Action]) + + useEffect(() => { + if (!entries.length || !currentProcess) return + const id = window.setInterval(() => { + void runPm2Action('status') + }, 1000) + return () => clearInterval(id) + }, [entries.length, currentProcess, runPm2Action]) + + useEffect(() => { + if (!ent || !currentProcess) return + if (isYoutube && multiChannelPro) return + if (isYoutube) void loadConfig('/get_config', setYoutubeText, setYoutubeStatus) + if (isYoutube || isTiktok) void loadConfig('/get_url_config', setUrlText, setUrlStatus) + }, [ent, currentProcess, isYoutube, isTiktok, loadConfig, multiChannelPro]) + + useEffect(() => { + if (!isYoutube || !multiChannelPro) return + let cancelled = false + void (async () => { + try { + const r = await apiFetch(apiUrl('/relay_pro/channels')) + const d = (await r.json()) as { ok?: boolean; error?: string; channels?: typeof proChannelsList } + if (cancelled) return + if (!d.ok) { + setProChannelsError(d.error || '无法加载 Pro 频道') + setProChannelsList([]) + return + } + setProChannelsError(null) + setProChannelsList(d.channels || []) + setProChannelId((prev) => { + if (prev && (d.channels || []).some((c) => c.id === prev)) return prev + return d.channels?.[0]?.id ?? '' + }) + } catch (e) { + if (!cancelled) { + setProChannelsError(e instanceof Error ? e.message : String(e)) + setProChannelsList([]) + } + } + })() + return () => { + cancelled = true + } + }, [isYoutube, multiChannelPro]) + + useEffect(() => { + if (!isYoutube || !multiChannelPro || !proChannelId) return + let cancelled = false + void (async () => { + try { + const dr = await apiFetch( + apiUrl(`/relay_pro/channel_detail?channel_id=${encodeURIComponent(proChannelId)}`), + ) + if (!dr.ok) { + if (!cancelled) setYoutubeStatus('读取线路失败') + return + } + const cd = (await dr.json()) as { youtubeKey?: string } + if (cancelled) return + setProYoutubeKey(cd.youtubeKey || '') + const ur = await apiFetch( + apiUrl(`/relay_pro/url_config?channel_id=${encodeURIComponent(proChannelId)}`), + ) + if (ur.ok) { + const u = (await ur.json()) as { content?: string } + if (!cancelled) { + setUrlText(u.content || '') + setUrlStatus('已读取当前线路的地址文件(与其它线路去重校验在保存时进行)') + } + } + } catch (e) { + if (!cancelled) setUrlStatus(e instanceof Error ? e.message : String(e)) + } + })() + return () => { + cancelled = true + } + }, [isYoutube, multiChannelPro, proChannelId]) + + useEffect(() => { + if (!UI_SHOW_YOUTUBE_GOOGLE_OAUTH || !isYoutube) return + void loadYtOauthStatus() + const id = window.setInterval(() => void loadYtOauthStatus(), 5000) + return () => clearInterval(id) + }, [isYoutube, loadYtOauthStatus]) + + const onProcessChange = (v: string) => { + setCurrentProcess(v) + } + + if (bootError) { + return ( +
+ +
+
+
+
+ ! +
+
+

无法启动

+
{bootError}
+
+
+
+
+
+ ) + } + + const navMeta = NAV_ITEMS.find((n) => n.id === nav) + + return ( +
+ + +
+
+
+

{navMeta?.label ?? ''}

+

{navMeta?.hint}

+
+ {nav === 'record' && ( +
+ + {statusMeta.line} +
+ )} +
+ +
+ {nav === 'desk' && ( +
+ +
+ )} + {nav === 'network' && ( +
+ + +
+ )} + {nav === 'proxy' && ( +
+ +
+ )} + {nav === 'system' && ( +
+ +
+ )} + {nav === 'wechat' && ( +
+ +
+ )} + + {nav === 'record' && ( +
+
+
+ + +
+ {isYoutube && ( +
+ +
+ )} +
+ + {ent && ( +
+ 当前任务 + {ent.label} + + {ent.script} + +
+ )} + + {emptyHint && ( +
+
+

还没有可用的录制任务

+
{emptyHint}
+
+ )} + + {!emptyHint && ( +
+ {isYoutube && multiChannelPro && ( +
+

Pro 转播线路

+

+ 每条线路对应独立配置文件;下拉选择线路即选择该路的 YouTube 串流密钥。地址列表保存在{' '} + config/relay_pro/url_config.<id>.ini,保存时与其它线路去重。 + 点「开始录制」会先把当前选中线路同步到 youtube.ini 与{' '} + URL_config.ini 再启动(当前仍为单路 youtube 进程;多进程并行将在后续版本接入)。 +

+ {proChannelsError && ( +

+ {proChannelsError} +

+ )} + {!proChannelsError && proChannelsList.length > 0 && ( +
+ + +
+ )} + {!proChannelsError && proChannelsList.length === 0 && ( +

+ youtube_channels.json 中暂无有效条目。请至少配置一条线路(含 id、name、douyinUrl、youtubeKey)。 +

+ )} +
+ )} + + {isYoutube && multiChannelPro && !proChannelsError && proChannelsList.length > 0 && ( + <> +
+

YouTube 串流密钥(当前线路)

+

+ 仅保存到当前线路;多线路之间禁止重复使用同一密钥。 +

+ {UI_SHOW_YOUTUBE_GOOGLE_OAUTH && ( +
+ + {ytOauth?.connected ? '已连接 Google' : '未连接 Google'} + + + +
+ )} +
+ 当前线路 key(youtubeKey) + +
+ + +
+
就绪
+
+
+ + + + + +
+

进程控制

+
+ + + + +
+
+ +
+
+

实时日志

+ 自动刷新 · 1s +
+

输出与错误分流显示;状态每秒轮询。

+
正在加载状态与日志...
+
+
+
+ + + diff --git a/integrations/openclaw-wechat/README.md b/integrations/openclaw-wechat/README.md new file mode 100644 index 0000000..f395c38 --- /dev/null +++ b/integrations/openclaw-wechat/README.md @@ -0,0 +1,20 @@ +# openclaw-wechat(上游 OpenClaw 插件) + +本目录用于**引用**开源仓库 [freestylefly/openclaw-wechat](https://github.com/freestylefly/openclaw-wechat):在已安装 [OpenClaw](https://github.com/openclaw/openclaw) 的环境下,作为 **OpenClaw 通道插件**使用微信代理 API,与网关 `openclaw gateway start` 配合(终端扫码登录等)。 + +与本仓库 **web2 内置能力**的关系: + +- **Python 桥接**(`web2_wechat_chatbot.py`)直接调用与插件相同的**代理服务 HTTP API**(`/v1/account/status`、`/v1/iPadLogin` 等),无需运行 OpenClaw 即可在「直播录制助手」里配置密钥、拉取二维码、轮询登录。 +- 若你已在 OpenClaw 中使用该插件,只需将 **同一套 `apiKey` + `proxyUrl`** 写入 `config/wechat_chatbot.json`,两端行为一致。 + +## 添加子模块(可选) + +```bash +git submodule add https://github.com/freestylefly/openclaw-wechat.git integrations/openclaw-wechat/repo +``` + +随后可在 OpenClaw 侧按上游 README 安装:`openclaw plugins install @canghe/openclaw-wechat`(以 [上游说明](https://github.com/freestylefly/openclaw-wechat) 为准)。 + +## 许可 + +上游为 MIT;使用微信代理服务须遵守其条款,且勿用于违法用途(见上游仓库声明)。 diff --git a/main.py b/main.py new file mode 100644 index 0000000..6f2da1f --- /dev/null +++ b/main.py @@ -0,0 +1,2204 @@ +# -*- encoding: utf-8 -*- + +""" +Author: Hmily +GitHub: https://github.com/ihmily +Date: 2023-07-17 23:52:05 +Update: 2025-10-23 19:48:05 +Copyright (c) 2023-2025 by Hmily, All Rights Reserved. +Function: Record live stream video. +""" +import asyncio +import os +import sys +import builtins +import subprocess +import signal +import threading +import time +import datetime +import re +import shutil +import random +import uuid +from pathlib import Path +import urllib.request +from urllib.error import URLError, HTTPError +from typing import Any +import configparser +import httpx +from src import spider, stream +from src.proxy import ProxyDetector +from src.utils import logger +from src import utils +from backup.msg_push import ( + dingtalk, xizhi, tg_bot, send_email, bark, ntfy, pushplus +) +from ffmpeg_install import ( + check_ffmpeg, ffmpeg_path, current_env_path +) + + + + + +version = "v4.0.7" +platforms = ("\n国内站点:抖音|快手|虎牙|斗鱼|YY|B站|小红书|bigo|blued|网易CC|千度热播|猫耳FM|Look|TwitCasting|百度|微博|" + "酷狗|花椒|流星|Acfun|畅聊|映客|音播|知乎|嗨秀|VV星球|17Live|浪Live|漂漂|六间房|乐嗨|花猫|淘宝|京东|咪咕|连接|来秀" + "\n海外站点:TikTok|SOOP|PandaTV|WinkTV|FlexTV|PopkonTV|TwitchTV|LiveMe|ShowRoom|CHZZK|Shopee|" + "Youtube|Faceit|Picarto") + +recording = set() +error_count = 0 +pre_max_request = 10 +max_request_lock = threading.Lock() +error_window = [] +error_window_size = 10 +error_threshold = 5 +monitoring = 0 +running_list = [] +url_tuples_list = [] +url_comments = [] +text_no_repeat_url = [] +create_var = locals() +first_start = True +exit_recording = False +need_update_line_list = [] +first_run = True +not_record_list = [] +start_display_time = datetime.datetime.now() +global_proxy = False +recording_time_list = {} +script_path = os.path.split(os.path.realpath(sys.argv[0]))[0] +config_file = f'{script_path}/config/config.ini' +url_config_file = f'{script_path}/config/URL_config.ini' +backup_dir = f'{script_path}/backup_config' +text_encoding = 'utf-8-sig' +rstr = r"[\/\\\:\*\??\"\<\>\|&#.。,, ~!· ]" +default_path = f'{script_path}/downloads' +os.makedirs(default_path, exist_ok=True) +file_update_lock = threading.Lock() +os_type = os.name +clear_command = "cls" if os_type == 'nt' else "clear" +color_obj = utils.Color() +os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path + + +def signal_handler(_signal, _frame): + sys.exit(0) + + +signal.signal(signal.SIGTERM, signal_handler) + + + +import shutil + + +# 全局变量(只初始化一次) +_last_lines_count = 0 + +def _clear_lines(n: int): + """向上删除 n 行(不闪烁)""" + if n <= 0: + return + print(f"\033[{n}A\033[K", end="", flush=True) # 上移 + 清行 + +def display_info() -> None: + global start_display_time, _last_lines_count + time.sleep(3) # 启动延迟 + + while True: + try: + lines = [] + now_str = datetime.datetime.now().strftime("%H:%M:%S") + + # 第一行:核心状态 + lines.append( + f"监测 {monitoring} | 并发 {max_request} | " + f"代理 {'开' if use_proxy else '关'} | " + f"分段 {'开' if split_video_by_time else '关'}{f':{split_time}s' if split_video_by_time else ''} | " + f"时间文件 {'开' if create_time_file else '关'} | " + f"质量 {video_record_quality} | 格式 {video_save_type} | " + f"错误 {error_count} | {now_str}" + ) + + # 第二行:录制中的直播 + if recording: + no_repeat = list(set(recording)) + recording_str = f"正在录制 {len(no_repeat)} 个: " + details = [] + now_dt = datetime.datetime.now() + for name in no_repeat[:10]: # 最多显示10个,防止爆屏 + if name not in recording_time_list: + continue + start_time, quality = recording_time_list[name] + duration = str(now_dt - start_time).split('.')[0] + details.append(f"{name.split()[-1]}[{quality} {duration}]") + recording_str += " | ".join(details) + if len(no_repeat) > 10: + recording_str += f" ... (+{len(no_repeat)-10})" + lines.append(recording_str) + else: + if monitoring == 0: + lines.append("没有正在监测的直播") + else: + lines.append(f"无录制任务 | 循环间隔 {delay_default}s") + + # 动态清屏 + 重绘(不闪烁!) + terminal_width, _ = shutil.get_terminal_size(fallback=(120, 24)) + display_lines = [] + for line in lines: + if len(line) > terminal_width - 5: + line = line[:terminal_width-8] + "..." + display_lines.append(line) + + # 删除旧内容 + 打印新内容 + if _last_lines_count > len(display_lines): + _clear_lines(_last_lines_count - 1) + print("\r" + display_lines[0]) + for line in display_lines[1:]: + print(line) + else: + print("\r" + display_lines[0], end="") + for line in display_lines[1:]: + print(f"\n{line}", end="") + + _last_lines_count = len(display_lines) + print(flush=True) # 强制刷新 + + time.sleep(5) + + except Exception as e: + # 避免崩溃后疯狂打印错误 + try: + print(f"\n[display_info 错误] {e}") + except: + pass + time.sleep(10) # 出错了也别刷屏 +def display_info2() -> None: + global start_display_time + time.sleep(5) + while True: + try: + sys.stdout.flush() + time.sleep(5) + if Path(sys.executable).name != 'pythonw.exe': + os.system(clear_command) + print(f"\r共监测{monitoring}个直播中", end=" | ") + print(f"同一时间访问网络的线程数: {max_request}", end=" | ") + print(f"是否开启代理录制: {'是' if use_proxy else '否'}", end=" | ") + if split_video_by_time: + print(f"录制分段开启: {split_time}秒", end=" | ") + else: + print("录制分段开启: 否", end=" | ") + if create_time_file: + print("是否生成时间文件: 是", end=" | ") + print(f"录制视频质量为: {video_record_quality}", end=" | ") + print(f"录制视频格式为: {video_save_type}", end=" | ") + print(f"目前瞬时错误数为: {error_count}", end=" | ") + now = time.strftime("%H:%M:%S", time.localtime()) + print(f"当前时间: {now}") + + if len(recording) == 0: + time.sleep(5) + if monitoring == 0: + print("\r没有正在监测和录制的直播") + else: + print(f"\r没有正在录制的直播 循环监测间隔时间:{delay_default}秒") + else: + now_time = datetime.datetime.now() + print("x" * 60) + no_repeat_recording = list(set(recording)) + print(f"正在录制{len(no_repeat_recording)}个直播: ") + for recording_live in no_repeat_recording: + rt, qa = recording_time_list[recording_live] + have_record_time = now_time - rt + print(f"{recording_live}[{qa}] 正在录制中 {str(have_record_time).split('.')[0]}") + + # print('\n本软件已运行:'+str(now_time - start_display_time).split('.')[0]) + print("x" * 60) + start_display_time = now_time + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + + +def update_file(file_path: str, old_str: str, new_str: str, start_str: str = None) -> str | None: + if old_str == new_str and start_str is None: + return old_str + with file_update_lock: + file_data = [] + with open(file_path, "r", encoding=text_encoding) as f: + try: + for text_line in f: + if old_str in text_line: + text_line = text_line.replace(old_str, new_str) + if start_str: + text_line = f'{start_str}{text_line}' + if text_line not in file_data: + file_data.append(text_line) + except RuntimeError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + if ini_URL_content: + with open(file_path, "w", encoding=text_encoding) as f2: + f2.write(ini_URL_content) + return old_str + if file_data: + with open(file_path, "w", encoding=text_encoding) as f: + f.write(''.join(file_data)) + return new_str + + +def delete_line(file_path: str, del_line: str, delete_all: bool = False) -> None: + with file_update_lock: + with open(file_path, 'r+', encoding=text_encoding) as f: + lines = f.readlines() + f.seek(0) + f.truncate() + skip_line = False + for txt_line in lines: + if del_line in txt_line: + if delete_all or not skip_line: + skip_line = True + continue + else: + skip_line = False + f.write(txt_line) + + +def get_startup_info(system_type: str): + if system_type == 'nt': + startup_info = subprocess.STARTUPINFO() + startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + else: + startup_info = None + return startup_info + + +def segment_video(converts_file_path: str, segment_save_file_path: str, segment_format: str, segment_time: str, + is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + ffmpeg_command = [ + "ffmpeg", + "-i", converts_file_path, + "-c:v", "copy", + "-c:a", "copy", + "-map", "0", + "-f", "segment", + "-segment_time", segment_time, + "-segment_format", segment_format, + "-reset_timestamps", "1", + "-movflags", "+frag_keyframe+empty_moov", + segment_save_file_path, + ] + _output = subprocess.check_output( + ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def converts_mp4(converts_file_path: str, is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + if converts_to_h264: + color_obj.print_colored("正在转码为MP4格式并重新编码为h264\n", color_obj.YELLOW) + ffmpeg_command = [ + "ffmpeg", "-i", converts_file_path, + "-c:v", "libx264", + "-preset", "veryfast", + "-crf", "23", + "-vf", "format=yuv420p", + "-c:a", "copy", + "-f", "mp4", converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4", + ] + else: + color_obj.print_colored("正在转码为MP4格式\n", color_obj.YELLOW) + ffmpeg_command = [ + "ffmpeg", "-i", converts_file_path, + "-c:v", "copy", + "-c:a", "copy", + "-f", "mp4", converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4", + ] + _output = subprocess.check_output( + ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def converts_m4a(converts_file_path: str, is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + _output = subprocess.check_output([ + "ffmpeg", "-i", converts_file_path, + "-n", "-vn", + "-c:a", "aac", "-bsf:a", "aac_adtstoasc", "-ab", "320k", + converts_file_path.rsplit('.', maxsplit=1)[0] + ".m4a", + ], stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def generate_subtitles(record_name: str, ass_filename: str, sub_format: str = 'srt') -> None: + index_time = 0 + today = datetime.datetime.now() + re_datatime = today.strftime('%Y-%m-%d %H:%M:%S') + + def transform_int_to_time(seconds: int) -> str: + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + return f"{h:02d}:{m:02d}:{s:02d}" + + while True: + index_time += 1 + txt = str(index_time) + "\n" + transform_int_to_time(index_time) + ',000 --> ' + transform_int_to_time( + index_time + 1) + ',000' + "\n" + str(re_datatime) + "\n\n" + + with open(f"{ass_filename}.{sub_format.lower()}", 'a', encoding=text_encoding) as f: + f.write(txt) + + if record_name not in recording: + return + time.sleep(1) + today = datetime.datetime.now() + re_datatime = today.strftime('%Y-%m-%d %H:%M:%S') + + +def adjust_max_request() -> None: + global max_request, error_count, pre_max_request, error_window + preset = max_request + + while True: + time.sleep(5) + with max_request_lock: + if error_window: + error_rate = sum(error_window) / len(error_window) + else: + error_rate = 0 + + if error_rate > error_threshold: + max_request = max(1, max_request - 1) + elif error_rate < error_threshold / 2 and max_request < preset: + max_request += 1 + else: + pass + + if pre_max_request != max_request: + pre_max_request = max_request + print(f"\r同一时间访问网络的线程数动态改为 {max_request}") + + error_window.append(error_count) + if len(error_window) > error_window_size: + error_window.pop(0) + error_count = 0 + + +def push_message(record_name: str, live_url: str, content: str) -> None: + msg_title = push_message_title.strip() or "直播间状态更新通知" + push_functions = { + '微信': lambda: xizhi(xizhi_api_url, msg_title, content), + '钉钉': lambda: dingtalk(dingtalk_api_url, content, dingtalk_phone_num, dingtalk_is_atall), + '邮箱': lambda: send_email( + email_host, login_email, email_password, sender_email, sender_name, + to_email, msg_title, content, smtp_port, open_smtp_ssl + ), + 'TG': lambda: tg_bot(tg_chat_id, tg_token, content), + 'BARK': lambda: bark( + bark_msg_api, title=msg_title, content=content, level=bark_msg_level, sound=bark_msg_ring + ), + 'NTFY': lambda: ntfy( + ntfy_api, title=msg_title, content=content, tags=ntfy_tags, action_url=live_url, email=ntfy_email + ), + 'PUSHPLUS': lambda: pushplus(pushplus_token, msg_title, content), + } + + for platform, func in push_functions.items(): + if platform in live_status_push.upper(): + try: + result = func() + print(f'提示信息:已经将[{record_name}]直播状态消息推送至你的{platform},' + f' 成功{len(result["success"])}, 失败{len(result["error"])}') + except Exception as e: + color_obj.print_colored(f"直播消息推送到{platform}失败: {e}", color_obj.RED) + + +def run_script(command: str) -> None: + try: + process = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=get_startup_info(os_type) + ) + stdout, stderr = process.communicate() + stdout_decoded = stdout.decode('utf-8') + stderr_decoded = stderr.decode('utf-8') + if stdout_decoded.strip(): + print(stdout_decoded) + if stderr_decoded.strip(): + print(stderr_decoded) + except PermissionError as e: + logger.error(e) + logger.error('脚本无执行权限!, 若是Linux环境, 请先执行:chmod +x your_script.sh 授予脚本可执行权限') + except OSError as e: + logger.error(e) + logger.error('Please add `#!/bin/bash` at the beginning of your bash script file.') + + +def clear_record_info(record_name: str, record_url: str) -> None: + global monitoring + recording.discard(record_name) + if record_url in url_comments and record_url in running_list: + running_list.remove(record_url) + monitoring -= 1 + color_obj.print_colored(f"[{record_name}]已经从录制列表中移除\n", color_obj.YELLOW) + + +def direct_download_stream(source_url: str, save_path: str, record_name: str, live_url: str, platform: str) -> bool: + try: + with open(save_path, 'wb') as f: + client = httpx.Client(timeout=None) + + headers = {} + header_params = get_record_headers(platform, live_url) + if header_params: + key, value = header_params.split(":", 1) + headers[key] = value + + with client.stream('GET', source_url, headers=headers, follow_redirects=True) as response: + if response.status_code != 200: + logger.error(f"请求直播流失败,状态码: {response.status_code}") + return False + + downloaded = 0 + chunk_size = 1024 * 16 + + for chunk in response.iter_bytes(chunk_size): + if live_url in url_comments or exit_recording: + color_obj.print_colored(f"[{record_name}]录制时已被注释或请求停止,下载中断", color_obj.YELLOW) + clear_record_info(record_name, live_url) + return False + + if chunk: + f.write(chunk) + downloaded += len(chunk) + print() + return True + except Exception as e: + logger.error(f"FLV下载错误: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + return False + + +def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, save_type: str, + script_command: str | None = None) -> bool: + save_file_path = ffmpeg_command[-1] + process = subprocess.Popen( + ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + + subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] + subs_thread_name = f'subs_{Path(subs_file_path).name}' + if create_time_file and not split_video_by_time and '音频' not in save_type: + create_var[subs_thread_name] = threading.Thread( + target=generate_subtitles, args=(record_name, subs_file_path) + ) + create_var[subs_thread_name].daemon = True + create_var[subs_thread_name].start() + + while process.poll() is None: + if record_url in url_comments or exit_recording: + color_obj.print_colored(f"[{record_name}]录制时已被注释,本条线程将会退出", color_obj.YELLOW) + clear_record_info(record_name, record_url) + # process.terminate() + if os.name == 'nt': + if process.stdin: + process.stdin.write(b'q') + process.stdin.close() + else: + process.send_signal(signal.SIGINT) + process.wait() + return True + time.sleep(1) + + return_code = process.returncode + stop_time = time.strftime('%Y-%m-%d %H:%M:%S') + if return_code == 0: + if converts_to_mp4 and save_type == 'TS': + if split_video_by_time: + file_paths = utils.get_file_paths(os.path.dirname(save_file_path)) + prefix = os.path.basename(save_file_path).rsplit('_', maxsplit=1)[0] + for path in file_paths: + if prefix in path: + threading.Thread(target=converts_mp4, args=(path, delete_origin_file)).start() + else: + threading.Thread(target=converts_mp4, args=(save_file_path, delete_origin_file)).start() + print(f"\n{record_name} {stop_time} 直播录制完成\n") + + if script_command: + logger.debug("开始执行脚本命令!") + if "python" in script_command: + params = [ + f'--record_name "{record_name}"', + f'--save_file_path "{save_file_path}"', + f'--save_type {save_type}', + f'--split_video_by_time {split_video_by_time}', + f'--converts_to_mp4 {converts_to_mp4}', + ] + else: + params = [ + f'"{record_name.split(" ", maxsplit=1)[-1]}"', + f'"{save_file_path}"', + save_type, + f'split_video_by_time:{split_video_by_time}', + f'converts_to_mp4:{converts_to_mp4}' + ] + script_command = script_command.strip() + ' ' + ' '.join(params) + run_script(script_command) + logger.debug("脚本命令执行结束!") + + else: + color_obj.print_colored(f"\n{record_name} {stop_time} 直播录制出错,返回码: {return_code}\n", color_obj.RED) + + recording.discard(record_name) + return False + + +def clean_name(input_text): + cleaned_name = re.sub(rstr, "_", input_text.strip()).strip('_') + cleaned_name = cleaned_name.replace("(", "(").replace(")", ")") + if clean_emoji: + cleaned_name = utils.remove_emojis(cleaned_name, '_').strip('_') + return cleaned_name or '空白昵称' + + +def get_quality_code(qn): + QUALITY_MAPPING = { + "原画": "OD", + "蓝光": "BD", + "超清": "UHD", + "高清": "HD", + "标清": "SD", + "流畅": "LD" + } + return QUALITY_MAPPING.get(qn) + + +def get_record_headers(platform, live_url): + live_domain = '/'.join(live_url.split('/')[0:3]) + record_headers = { + 'PandaTV': 'origin:https://www.pandalive.co.kr', + 'WinkTV': 'origin:https://www.winktv.co.kr', + 'PopkonTV': 'origin:https://www.popkontv.com', + 'FlexTV': 'origin:https://www.flextv.co.kr', + '千度热播': 'referer:https://qiandurebo.com', + '17Live': 'referer:https://17.live/en/live/6302408', + '浪Live': 'referer:https://www.lang.live', + 'shopee': f'origin:{live_domain}', + 'Blued直播': 'referer:https://app.blued.cn' + } + return record_headers.get(platform) + + +def is_flv_preferred_platform(link): + return any(i in link for i in ["douyin", "tiktok"]) + + +def select_source_url(link, stream_info): + if is_flv_preferred_platform(link): + codec = utils.get_query_params(stream_info.get('flv_url'), "codec") + if codec and codec[0] == 'h265': + logger.warning("FLV is not supported for h265 codec, use HLS source instead") + else: + return stream_info.get('flv_url') + + return stream_info.get('record_url') + + +def start_record(url_data: tuple, count_variable: int = -1) -> None: + global error_count + + while True: + try: + record_finished = False + run_once = False + start_pushed = False + new_record_url = '' + count_time = time.time() + retry = 0 + record_quality_zh, record_url, anchor_name = url_data + record_quality = get_quality_code(record_quality_zh) + proxy_address = proxy_addr + platform = '未知平台' + live_domain = '/'.join(record_url.split('/')[0:3]) + + if proxy_addr: + proxy_address = None + for platform in enable_proxy_platform_list: + if platform and platform.strip() in record_url: + proxy_address = proxy_addr + break + + if not proxy_address: + if extra_enable_proxy_platform_list: + for pt in extra_enable_proxy_platform_list: + if pt and pt.strip() in record_url: + proxy_address = proxy_addr_bak or None + + # print(f'\r代理地址:{proxy_address}') + # print(f'\r全局代理:{global_proxy}') + while True: + try: + port_info = [] + if record_url.find("douyin.com/") > -1: + platform = '抖音直播' + with semaphore: + if 'v.douyin.com' not in record_url and '/user/' not in record_url: + json_data = asyncio.run(spider.get_douyin_web_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=dy_cookie)) + else: + json_data = asyncio.run(spider.get_douyin_app_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=dy_cookie)) + port_info = asyncio.run( + stream.get_douyin_stream_url(json_data, record_quality, proxy_address)) + + elif record_url.find("https://www.tiktok.com/") > -1: + platform = 'TikTok直播' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_tiktok_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=tiktok_cookie)) + port_info = asyncio.run( + stream.get_tiktok_stream_url(json_data, record_quality, proxy_address)) + else: + logger.error("错误信息: 网络异常,请检查网络是否能正常访问TikTok平台") + + elif record_url.find("https://live.kuaishou.com/") > -1: + platform = '快手直播' + with semaphore: + json_data = asyncio.run(spider.get_kuaishou_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=ks_cookie)) + port_info = asyncio.run(stream.get_kuaishou_stream_url(json_data, record_quality)) + + elif record_url.find("https://www.huya.com/") > -1: + platform = '虎牙直播' + with semaphore: + if record_quality not in ['OD', 'BD', 'UHD']: + json_data = asyncio.run(spider.get_huya_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=hy_cookie)) + port_info = asyncio.run(stream.get_huya_stream_url(json_data, record_quality)) + else: + port_info = asyncio.run(spider.get_huya_app_stream_url( + url=record_url, + proxy_addr=proxy_address, + cookies=hy_cookie + )) + + elif record_url.find("https://www.douyu.com/") > -1: + platform = '斗鱼直播' + with semaphore: + json_data = asyncio.run(spider.get_douyu_info_data( + url=record_url, proxy_addr=proxy_address, cookies=douyu_cookie)) + port_info = asyncio.run(stream.get_douyu_stream_url( + json_data, video_quality=record_quality, cookies=douyu_cookie, proxy_addr=proxy_address + )) + + elif record_url.find("https://www.yy.com/") > -1: + platform = 'YY直播' + with semaphore: + json_data = asyncio.run(spider.get_yy_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=yy_cookie)) + port_info = asyncio.run(stream.get_yy_stream_url(json_data)) + + elif record_url.find("https://live.bilibili.com/") > -1: + platform = 'B站直播' + with semaphore: + json_data = asyncio.run(spider.get_bilibili_room_info( + url=record_url, proxy_addr=proxy_address, cookies=bili_cookie)) + port_info = asyncio.run(stream.get_bilibili_stream_url( + json_data, video_quality=record_quality, cookies=bili_cookie, proxy_addr=proxy_address)) + + elif record_url.find("http://xhslink.com/") > -1 or \ + record_url.find("https://www.xiaohongshu.com/") > -1: + platform = '小红书直播' + with semaphore: + port_info = asyncio.run(spider.get_xhs_stream_url( + record_url, proxy_addr=proxy_address, cookies=xhs_cookie)) + retry += 1 + + elif record_url.find("www.bigo.tv/") > -1 or record_url.find("slink.bigovideo.tv/") > -1: + platform = 'Bigo直播' + with semaphore: + port_info = asyncio.run(spider.get_bigo_stream_url( + record_url, proxy_addr=proxy_address, cookies=bigo_cookie)) + + elif record_url.find("https://app.blued.cn/") > -1: + platform = 'Blued直播' + with semaphore: + port_info = asyncio.run(spider.get_blued_stream_url( + record_url, proxy_addr=proxy_address, cookies=blued_cookie)) + + elif record_url.find("sooplive.co.kr/") > -1 or record_url.find("sooplive.com/") > -1: + platform = 'SOOP' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_sooplive_stream_data( + url=record_url, proxy_addr=proxy_address, + cookies=sooplive_cookie, + username=sooplive_username, + password=sooplive_password + )) + if json_data and json_data.get('new_cookies'): + utils.update_config( + config_file, 'Cookie', 'sooplive_cookie', json_data['new_cookies'] + ) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问SOOP平台") + + elif record_url.find("cc.163.com/") > -1: + platform = '网易CC直播' + with semaphore: + json_data = asyncio.run(spider.get_netease_stream_data( + url=record_url, cookies=netease_cookie)) + port_info = asyncio.run(stream.get_netease_stream_url(json_data, record_quality)) + + elif record_url.find("qiandurebo.com/") > -1: + platform = '千度热播' + with semaphore: + port_info = asyncio.run(spider.get_qiandurebo_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=qiandurebo_cookie)) + + elif record_url.find("www.pandalive.co.kr/") > -1: + platform = 'PandaTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_pandatv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=pandatv_cookie + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PandaTV直播平台") + + elif record_url.find("fm.missevan.com/") > -1: + platform = '猫耳FM直播' + with semaphore: + port_info = asyncio.run(spider.get_maoerfm_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=maoerfm_cookie)) + + elif record_url.find("www.winktv.co.kr/") > -1: + platform = 'WinkTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_winktv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=winktv_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问WinkTV直播平台") + + elif record_url.find("www.flextv.co.kr/") > -1 or record_url.find("www.ttinglive.com/") > -1: + platform = 'FlexTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_flextv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=flextv_cookie, + username=flextv_username, + password=flextv_password + )) + if json_data and json_data.get('new_cookies'): + utils.update_config( + config_file, 'Cookie', 'flextv_cookie', json_data['new_cookies'] + ) + if 'play_url_list' in json_data: + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + port_info = json_data + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问FlexTV直播平台") + + elif record_url.find("look.163.com/") > -1: + platform = 'Look直播' + with semaphore: + port_info = asyncio.run(spider.get_looklive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=look_cookie + )) + + elif record_url.find("www.popkontv.com/") > -1: + platform = 'PopkonTV' + with semaphore: + if global_proxy or proxy_address: + port_info = asyncio.run(spider.get_popkontv_stream_url( + url=record_url, + proxy_addr=proxy_address, + access_token=popkontv_access_token, + username=popkontv_username, + password=popkontv_password, + partner_code=popkontv_partner_code + )) + if port_info and port_info.get('new_token'): + utils.update_config( + file_path=config_file, section='Authorization', key='popkontv_token', + new_value=port_info['new_token'] + ) + + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PopkonTV直播平台") + + elif record_url.find("twitcasting.tv/") > -1: + platform = 'TwitCasting' + with semaphore: + json_data = asyncio.run(spider.get_twitcasting_stream_url( + url=record_url, + proxy_addr=proxy_address, + cookies=twitcasting_cookie, + account_type=twitcasting_account_type, + username=twitcasting_username, + password=twitcasting_password + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=False)) + + if port_info and port_info.get('new_cookies'): + utils.update_config( + file_path=config_file, section='Cookie', key='twitcasting_cookie', + new_value=port_info['new_cookies'] + ) + + elif record_url.find("live.baidu.com/") > -1: + platform = '百度直播' + with semaphore: + json_data = asyncio.run(spider.get_baidu_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=baidu_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality)) + + elif record_url.find("weibo.com/") > -1: + platform = '微博直播' + with semaphore: + json_data = asyncio.run(spider.get_weibo_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=weibo_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, hls_extra_key='m3u8_url')) + + elif record_url.find("kugou.com/") > -1: + platform = '酷狗直播' + with semaphore: + port_info = asyncio.run(spider.get_kugou_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=kugou_cookie)) + + elif record_url.find("www.twitch.tv/") > -1: + platform = 'TwitchTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_twitchtv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=twitch_cookie + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问TwitchTV直播平台") + + elif record_url.find("www.liveme.com/") > -1: + if global_proxy or proxy_address: + platform = 'LiveMe' + with semaphore: + port_info = asyncio.run(spider.get_liveme_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=liveme_cookie)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问LiveMe直播平台") + + elif record_url.find("www.huajiao.com/") > -1: + platform = '花椒直播' + with semaphore: + port_info = asyncio.run(spider.get_huajiao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=huajiao_cookie)) + + elif record_url.find("7u66.com/") > -1: + platform = '流星直播' + with semaphore: + port_info = asyncio.run(spider.get_liuxing_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=liuxing_cookie)) + + elif record_url.find("showroom-live.com/") > -1: + platform = 'ShowRoom' + with semaphore: + json_data = asyncio.run(spider.get_showroom_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=showroom_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("live.acfun.cn/") > -1 or record_url.find("m.acfun.cn/") > -1: + platform = 'Acfun' + with semaphore: + json_data = asyncio.run(spider.get_acfun_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=acfun_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, url_type='flv', flv_extra_key='url')) + + elif record_url.find("live.tlclw.com/") > -1: + platform = '畅聊直播' + with semaphore: + port_info = asyncio.run(spider.get_changliao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=changliao_cookie)) + + elif record_url.find("ybw1666.com/") > -1: + platform = '音播直播' + with semaphore: + port_info = asyncio.run(spider.get_yinbo_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yinbo_cookie)) + + elif record_url.find("www.inke.cn/") > -1: + platform = '映客直播' + with semaphore: + port_info = asyncio.run(spider.get_yingke_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yingke_cookie)) + + elif record_url.find("www.zhihu.com/") > -1: + platform = '知乎直播' + with semaphore: + port_info = asyncio.run(spider.get_zhihu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=zhihu_cookie)) + + elif record_url.find("chzzk.naver.com/") > -1: + platform = 'CHZZK' + with semaphore: + json_data = asyncio.run(spider.get_chzzk_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=chzzk_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("www.haixiutv.com/") > -1: + platform = '嗨秀直播' + with semaphore: + port_info = asyncio.run(spider.get_haixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=haixiu_cookie)) + + elif record_url.find("vvxqiu.com/") > -1: + platform = 'VV星球' + with semaphore: + port_info = asyncio.run(spider.get_vvxqiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=vvxqiu_cookie)) + + elif record_url.find("17.live/") > -1: + platform = '17Live' + with semaphore: + port_info = asyncio.run(spider.get_17live_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yiqilive_cookie)) + + elif record_url.find("www.lang.live/") > -1: + platform = '浪Live' + with semaphore: + port_info = asyncio.run(spider.get_langlive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=langlive_cookie)) + + elif record_url.find("m.pp.weimipopo.com/") > -1: + platform = '漂漂直播' + with semaphore: + port_info = asyncio.run(spider.get_pplive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=pplive_cookie)) + + elif record_url.find(".6.cn/") > -1: + platform = '六间房直播' + with semaphore: + port_info = asyncio.run(spider.get_6room_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=six_room_cookie)) + + elif record_url.find("lehaitv.com/") > -1: + platform = '乐嗨直播' + with semaphore: + port_info = asyncio.run(spider.get_haixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=lehaitv_cookie)) + + elif record_url.find("h.catshow168.com/") > -1: + platform = '花猫直播' + with semaphore: + port_info = asyncio.run(spider.get_pplive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=huamao_cookie)) + + elif record_url.find("live.shopee") > -1 or record_url.find("shp.ee/") > -1: + platform = 'shopee' + with semaphore: + port_info = asyncio.run(spider.get_shopee_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=shopee_cookie)) + if port_info.get('uid'): + new_record_url = record_url.split('?')[0] + '?' + str(port_info['uid']) + + elif record_url.find("www.youtube.com/") > -1 or record_url.find("youtu.be/") > -1: + platform = 'Youtube' + with semaphore: + json_data = asyncio.run(spider.get_youtube_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=youtube_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("tb.cn") > -1: + platform = '淘宝直播' + with semaphore: + json_data = asyncio.run(spider.get_taobao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=taobao_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, + url_type='all', hls_extra_key='hlsUrl', flv_extra_key='flvUrl' + )) + + elif record_url.find("3.cn") > -1 or record_url.find("m.jd.com") > -1: + platform = '京东直播' + with semaphore: + port_info = asyncio.run(spider.get_jd_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=jd_cookie)) + + elif record_url.find("faceit.com/") > -1: + platform = 'faceit' + with semaphore: + if global_proxy or proxy_address: + with semaphore: + json_data = asyncio.run(spider.get_faceit_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=faceit_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问faceit直播平台") + + elif record_url.find("www.miguvideo.com") > -1 or record_url.find("m.miguvideo.com") > -1: + platform = '咪咕直播' + with semaphore: + port_info = asyncio.run(spider.get_migu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=migu_cookie)) + + elif record_url.find("show.lailianjie.com") > -1: + platform = '连接直播' + with semaphore: + port_info = asyncio.run(spider.get_lianjie_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=lianjie_cookie)) + + elif record_url.find("www.imkktv.com") > -1: + platform = '来秀直播' + with semaphore: + port_info = asyncio.run(spider.get_laixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=laixiu_cookie)) + + elif record_url.find("www.picarto.tv") > -1: + platform = 'Picarto' + with semaphore: + port_info = asyncio.run(spider.get_picarto_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=picarto_cookie)) + + elif record_url.find(".m3u8") > -1 or record_url.find(".flv") > -1: + platform = '自定义录制直播' + port_info = { + "anchor_name": platform + '_' + str(uuid.uuid4())[:8], + "is_live": True, + "record_url": record_url, + } + if '.flv' in record_url: + port_info['flv_url'] = record_url + else: + port_info['m3u8_url'] = record_url + + else: + logger.error(f'{record_url} {platform}直播地址') + return + + if anchor_name: + if '主播:' in anchor_name: + anchor_split: list = anchor_name.split('主播:') + if len(anchor_split) > 1 and anchor_split[1].strip(): + anchor_name = anchor_split[1].strip() + else: + anchor_name = port_info.get("anchor_name", '') + else: + anchor_name = port_info.get("anchor_name", '') + + if not port_info.get("anchor_name", ''): + print(f'序号{count_variable} 网址内容获取失败,进行重试中...获取失败的地址是:{url_data}') + with max_request_lock: + error_count += 1 + error_window.append(1) + else: + anchor_name = clean_name(anchor_name) + record_name = f'序号{count_variable} {anchor_name}' + + if record_url in url_comments: + print(f"[{anchor_name}]已被注释,本条线程将会退出") + clear_record_info(record_name, record_url) + return + + if not url_data[-1] and run_once is False: + if new_record_url: + need_update_line_list.append( + f'{record_url}|{new_record_url},主播: {anchor_name.strip()}') + not_record_list.append(new_record_url) + else: + need_update_line_list.append(f'{record_url}|{record_url},主播: {anchor_name.strip()}') + run_once = True + + push_at = datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S') + if port_info['is_live'] is False: + print(f"\r{record_name} 等待直播... ") + # 直接退出线程eric + # continue # 继续循环 + return # 线程彻底退出 + + if start_pushed: + if over_show_push: + push_content = "直播间状态更新:[直播间名称] 直播已结束!时间:[时间]" + if over_push_message_text: + push_content = over_push_message_text + + push_content = (push_content.replace('[直播间名称]', record_name). + replace('[时间]', push_at)) + threading.Thread( + target=push_message, + args=(record_name, record_url, push_content.replace(r'\n', '\n')), + daemon=True + ).start() + start_pushed = False + + else: + content = f"\r{record_name} 正在直播中..." + print(content) + # 检查是否已有直播在录制 + if len(recording) > 0 : + # 如果已有直播在录制,跳过这次录制(主播在线) + # return + continue #如果在循环中 + + if live_status_push and not start_pushed: + if begin_show_push: + push_content = "直播间状态更新:[直播间名称] 正在直播中,时间:[时间]" + if begin_push_message_text: + push_content = begin_push_message_text + + push_content = (push_content.replace('[直播间名称]', record_name). + replace('[时间]', push_at)) + threading.Thread( + target=push_message, + args=(record_name, record_url, push_content.replace(r'\n', '\n')), + daemon=True + ).start() + start_pushed = True + + if disable_record: + time.sleep(push_check_seconds) + continue + + real_url = select_source_url(record_url, port_info) + full_path = f'{default_path}/{platform}' + if real_url: + now = datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S") + live_title = port_info.get('title') + title_in_name = '' + if live_title: + live_title = clean_name(live_title) + title_in_name = live_title + '_' if filename_by_title else '' + + try: + if len(video_save_path) > 0: + if not video_save_path.endswith(('/', '\\')): + full_path = f'{video_save_path}/{platform}' + else: + full_path = f'{video_save_path}{platform}' + + full_path = full_path.replace("\\", '/') + if folder_by_author: + full_path = f'{full_path}/{anchor_name}' + if folder_by_time: + full_path = f'{full_path}/{now[:10]}' + if folder_by_title and port_info.get('title'): + if folder_by_time: + full_path = f'{full_path}/{live_title}_{anchor_name}' + else: + full_path = f'{full_path}/{now[:10]}_{live_title}' + if not os.path.exists(full_path): + os.makedirs(full_path) + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + + if platform != '自定义录制直播': + if enable_https_recording and real_url.startswith("http://"): + real_url = real_url.replace("http://", "https://") + + http_record_list = ['shopee', "migu"] + if platform in http_record_list: + real_url = real_url.replace("https://", "http://") + + user_agent = ("Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (" + "KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile " + "Safari/537.36") + + rw_timeout = "15000000" + analyzeduration = "20000000" + probesize = "10000000" + bufsize = "8000k" + max_muxing_queue_size = "1024" + for pt_host in overseas_platform_host: + if pt_host in record_url: + rw_timeout = "50000000" + analyzeduration = "40000000" + probesize = "20000000" + bufsize = "15000k" + max_muxing_queue_size = "2048" + break + + ffmpeg_command = [ + 'ffmpeg', "-y", + "-v", "verbose", + "-rw_timeout", rw_timeout, + "-loglevel", "error", + "-hide_banner", + "-user_agent", user_agent, + "-protocol_whitelist", "rtmp,crypto,file,http,https,tcp,tls,udp,rtp,httpproxy", + "-thread_queue_size", "1024", + "-analyzeduration", analyzeduration, + "-probesize", probesize, + "-fflags", "+discardcorrupt", + "-re", "-i", real_url, + "-bufsize", bufsize, + "-sn", "-dn", + "-reconnect_delay_max", "60", + "-reconnect_streamed", "-reconnect_at_eof", + "-max_muxing_queue_size", max_muxing_queue_size, + "-correct_ts_overflow", "1", + "-avoid_negative_ts", "1" + ] + + headers = get_record_headers(platform, record_url) + if headers: + ffmpeg_command.insert(11, "-headers") + ffmpeg_command.insert(12, headers) + + if proxy_address: + ffmpeg_command.insert(1, "-http_proxy") + ffmpeg_command.insert(2, proxy_address) + + recording.add(record_name) + start_record_time = datetime.datetime.now() + recording_time_list[record_name] = [start_record_time, record_quality_zh] + rec_info = f"\r{anchor_name} 准备开始录制视频: {full_path}" + if show_url: + re_plat = ('WinkTV', 'PandaTV', 'ShowRoom', 'CHZZK', 'Youtube') + if platform in re_plat: + logger.info( + f"{platform} | {anchor_name} | 直播源地址: {port_info.get('m3u8_url')}") + else: + logger.info( + f"{platform} | {anchor_name} | 直播源地址: {real_url}") + + only_flv_record = False + only_flv_platform_list = ['shopee', '花椒直播'] + if platform in only_flv_platform_list: + logger.debug(f"提示: {platform} 将强制使用FLV格式录制") + only_flv_record = True + + only_audio_record = False + only_audio_platform_list = ['猫耳FM直播', 'Look直播'] + if platform in only_audio_platform_list: + only_audio_record = True + + record_save_type = video_save_type + + if is_flv_preferred_platform(record_url) and port_info.get('flv_url'): + codec = utils.get_query_params(port_info['flv_url'], "codec") + if codec and codec[0] == 'h265': + logger.warning("FLV is not supported for h265 codec, use TS format instead") + record_save_type = "TS" + + if only_audio_record or any(i in record_save_type for i in ['MP3', 'M4A']): + try: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + extension = "mp3" if "m4a" not in record_save_type.lower() else "m4a" + name_format = "_%03d" if split_video_by_time else "" + save_file_path = (f"{full_path}/{anchor_name}_{title_in_name}{now}" + f"{name_format}.{extension}") + + if split_video_by_time: + print(f'\r{anchor_name} 准备开始录制音频: {save_file_path}') + + if "MP3" in record_save_type: + command = [ + "-map", "0:a", + "-c:a", "libmp3lame", + "-ab", "320k", + "-f", "segment", + "-segment_time", split_time, + "-reset_timestamps", "1", + save_file_path, + ] + else: + command = [ + "-map", "0:a", + "-c:a", "aac", + "-bsf:a", "aac_adtstoasc", + "-ab", "320k", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", 'mpegts', + "-reset_timestamps", "1", + save_file_path, + ] + + else: + if "MP3" in record_save_type: + command = [ + "-map", "0:a", + "-c:a", "libmp3lame", + "-ab", "320k", + save_file_path, + ] + + else: + command = [ + "-map", "0:a", + "-c:a", "aac", + "-bsf:a", "aac_adtstoasc", + "-ab", "320k", + "-movflags", "+faststart", + save_file_path, + ] + + ffmpeg_command.extend(command) + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + + + # if only_flv_record: + if False: + + logger.info(f"Use Direct Downloader to Download FLV Stream: {record_url}") + filename = anchor_name + f'_{title_in_name}' + now + '.flv' + save_file_path = f'{full_path}/{filename}' + print(f'{rec_info}/{filename}') + + subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] + subs_thread_name = f'subs_{Path(subs_file_path).name}' + if create_time_file: + create_var[subs_thread_name] = threading.Thread( + target=generate_subtitles, args=(record_name, subs_file_path) + ) + create_var[subs_thread_name].daemon = True + create_var[subs_thread_name].start() + + try: + flv_url = port_info.get('flv_url') + if flv_url: + recording.add(record_name) + start_record_time = datetime.datetime.now() + recording_time_list[record_name] = [start_record_time, record_quality_zh] + + download_success = direct_download_stream( + flv_url, save_file_path, record_name, record_url, platform + ) + + if download_success: + record_finished = True + print( + f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制完成\n") + + recording.discard(record_name) + else: + logger.debug("未找到FLV直播流,跳过录制") + except Exception as e: + clear_record_info(record_name, record_url) + color_obj.print_colored( + f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制出错,请检查网络\n", + color_obj.RED) + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + elif record_save_type == "FLV": + filename = anchor_name + f'_{title_in_name}' + now + ".flv" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.flv" + command = [ + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-bsf:a", "aac_adtstoasc", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "flv", + "-reset_timestamps", "1", + save_file_path + ] + + else: + command = [ + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-bsf:a", "aac_adtstoasc", + "-f", "flv", + "{path}".format(path=save_file_path), + ] + ffmpeg_command.extend(command) + + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + try: + if converts_to_mp4: + seg_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mp4" + if split_video_by_time: + segment_video( + save_file_path, seg_file_path, + segment_format='mp4', segment_time=split_time, + is_original_delete=delete_origin_file + ) + else: + threading.Thread( + target=converts_mp4, + args=(save_file_path, delete_origin_file) + ).start() + + else: + seg_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.flv" + if split_video_by_time: + segment_video( + save_file_path, seg_file_path, + segment_format='flv', segment_time=split_time, + is_original_delete=delete_origin_file + ) + except Exception as e: + logger.error(f"转码失败: {e} ") + + elif record_save_type == "MKV": + filename = anchor_name + f'_{title_in_name}' + now + ".mkv" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mkv" + command = [ + "-flags", "global_header", + "-c:v", "copy", + "-c:a", "aac", + "-map", "0", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "matroska", + "-reset_timestamps", "1", + save_file_path, + ] + + else: + command = [ + "-flags", "global_header", + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-f", "matroska", + "{path}".format(path=save_file_path), + ] + ffmpeg_command.extend(command) + + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + elif record_save_type == "MP4": + filename = anchor_name + f'_{title_in_name}' + now + ".mp4" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mp4" + command = [ + "-c:v", "copy", + "-c:a", "aac", + "-map", "0", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "mp4", + "-reset_timestamps", "1", + "-movflags", "+frag_keyframe+empty_moov", + save_file_path, + ] + + else: + command = [ + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-f", "mp4", + save_file_path, + ] + + ffmpeg_command.extend(command) + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + else: + import sys + sys.path.insert(0, './dist') + from douyin_srs_ffplay import start_douyin_srs_ffplay + # 调用 + start_douyin_srs_ffplay( + title=title_in_name, + anchor_name=anchor_name, + real_url=real_url, + rec_info=rec_info, + record_name=record_name, + recording=recording, + recording_time_list=recording_time_list, + running_list=running_list, + monitoring=monitoring, + clear_record_info=clear_record_info, + color_obj=color_obj + ) + count_time = time.time() + + + + + + + + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + num = random.randint(-5, 5) + delay_default + if num < 0: + num = 0 + x = num + + if error_count > 20: + x = x + 60 + # x += min(error_count * 2, 30) + color_obj.print_colored("\r瞬时错误太多,延迟最多60s", color_obj.YELLOW) + + # 这里是.如果录制结束后,循环时间会暂时变成30s后检测一遍. 这样一定程度上防止主播卡顿造成少录 + # 当30秒过后检测一遍后. 会回归正常设置的循环秒数 + if record_finished: + count_time_end = time.time() - count_time + if count_time_end < 60: + x = 30 + record_finished = False + + else: + x = num + + # 这里是正常循环 + while x: + x = x - 1 + if loop_time: + print(f'\r{anchor_name}循环等待{x}秒 ', end="") + time.sleep(1) + if loop_time: + print('\r检测直播间中...', end="") + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + time.sleep(2) + + +def backup_file(file_path: str, backup_dir_path: str, limit_counts: int = 6) -> None: + + try: + if not os.path.exists(backup_dir_path): + os.makedirs(backup_dir_path) + + timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + backup_file_name = os.path.basename(file_path) + '_' + timestamp + backup_file_path = os.path.join(backup_dir_path, backup_file_name).replace("\\", "/") + shutil.copy2(file_path, backup_file_path) + + files = os.listdir(backup_dir_path) + _files = [f for f in files if f.startswith(os.path.basename(file_path))] + _files.sort(key=lambda x: os.path.getmtime(os.path.join(backup_dir_path, x))) + + while len(_files) > limit_counts: + oldest_file = _files[0] + os.remove(os.path.join(backup_dir_path, oldest_file)) + _files = _files[1:] + + except Exception as e: + logger.error(f'\r备份配置文件 {file_path} 失败:{str(e)}') + + +def backup_file_start() -> None: + config_md5 = '' + url_config_md5 = '' + + while True: + try: + if os.path.exists(config_file): + new_config_md5 = utils.check_md5(config_file) + if new_config_md5 != config_md5: + backup_file(config_file, backup_dir) + config_md5 = new_config_md5 + + if os.path.exists(url_config_file): + new_url_config_md5 = utils.check_md5(url_config_file) + if new_url_config_md5 != url_config_md5: + backup_file(url_config_file, backup_dir) + url_config_md5 = new_url_config_md5 + time.sleep(600) + except Exception as e: + logger.error(f"备份配置文件失败, 错误信息: {e}") + + +def check_ffmpeg_existence() -> bool: + try: + result = subprocess.run(['ffmpeg', '-version'], check=True, capture_output=True, text=True) + if result.returncode == 0: + lines = result.stdout.splitlines() + version_line = lines[0] + built_line = lines[1] + print(version_line) + print(built_line) + except subprocess.CalledProcessError as e: + logger.error(e) + except FileNotFoundError: + pass + finally: + if check_ffmpeg(): + time.sleep(1) + return True + return False + + +# --------------------------初始化程序------------------------------------- +print("-----------------------------------------------------") +print("| DouyinLiveRecorder |") +print("-----------------------------------------------------") + +print(f"版本号: {version}") +print("GitHub: https://github.com/ihmily/DouyinLiveRecorder") +print(f'支持平台: {platforms}') +print('.....................................................') +if not check_ffmpeg_existence(): + logger.error("缺少ffmpeg无法进行录制,程序退出") + sys.exit(1) +os.makedirs(os.path.dirname(config_file), exist_ok=True) +t3 = threading.Thread(target=backup_file_start, args=(), daemon=True) +t3.start() +utils.remove_duplicate_lines(url_config_file) + + +def read_config_value(config_parser: configparser.RawConfigParser, section: str, option: str, default_value: Any) \ + -> Any: + try: + + config_parser.read(config_file, encoding=text_encoding) + if '录制设置' not in config_parser.sections(): + config_parser.add_section('录制设置') + if '推送配置' not in config_parser.sections(): + config_parser.add_section('推送配置') + if 'Cookie' not in config_parser.sections(): + config_parser.add_section('Cookie') + if 'Authorization' not in config_parser.sections(): + config_parser.add_section('Authorization') + if '账号密码' not in config_parser.sections(): + config_parser.add_section('账号密码') + return config_parser.get(section, option) + except (configparser.NoSectionError, configparser.NoOptionError): + config_parser.set(section, option, str(default_value)) + with open(config_file, 'w', encoding=text_encoding) as f: + config_parser.write(f) + return default_value + + +options = {"是": True, "否": False} +config = configparser.RawConfigParser() +language = read_config_value(config, '录制设置', 'language(zh_cn/en)', "zh_cn") +skip_proxy_check = options.get(read_config_value(config, '录制设置', '是否跳过代理检测(是/否)', "否"), False) +if language and 'en' not in language.lower(): + from i18n import translated_print + + builtins.print = translated_print + +try: + if skip_proxy_check: + global_proxy = True + else: + print('系统代理检测中,请耐心等待...') + response_g = urllib.request.urlopen("https://www.google.com/", timeout=15) + global_proxy = True + print('\r全局/规则网络代理已开启√') + pd = ProxyDetector() + if pd.is_proxy_enabled(): + proxy_info = pd.get_proxy_info() + print("System Proxy: http://{}:{}".format(proxy_info.ip, proxy_info.port)) +except HTTPError as err: + print(f"HTTP error occurred: {err.code} - {err.reason}") +except URLError: + color_obj.print_colored("INFO:未检测到全局/规则网络代理,请检查代理配置(若无需录制海外直播请忽略此条提示)", + color_obj.YELLOW) +except Exception as err: + print("An unexpected error occurred:", err) + +while True: + + try: + if not os.path.isfile(config_file): + with open(config_file, 'w', encoding=text_encoding) as file: + pass + + ini_URL_content = '' + if os.path.isfile(url_config_file): + with open(url_config_file, 'r', encoding=text_encoding) as file: + ini_URL_content = file.read().strip() + + if not ini_URL_content.strip(): + input_url = input('请输入要录制的主播直播间网址(尽量使用PC网页端的直播间地址):\n') + with open(url_config_file, 'w', encoding=text_encoding) as file: + file.write(input_url) + except OSError as err: + logger.error(f"发生 I/O 错误: {err}") + + video_save_path = read_config_value(config, '录制设置', '直播保存路径(不填则默认)', "") + folder_by_author = options.get(read_config_value(config, '录制设置', '保存文件夹是否以作者区分', "是"), False) + folder_by_time = options.get(read_config_value(config, '录制设置', '保存文件夹是否以时间区分', "否"), False) + folder_by_title = options.get(read_config_value(config, '录制设置', '保存文件夹是否以标题区分', "否"), False) + filename_by_title = options.get(read_config_value(config, '录制设置', '保存文件名是否包含标题', "否"), False) + clean_emoji = options.get(read_config_value(config, '录制设置', '是否去除名称中的表情符号', "是"), True) + video_save_type = read_config_value(config, '录制设置', '视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频', "ts") + video_record_quality = read_config_value(config, '录制设置', '原画|超清|高清|标清|流畅', "原画") + use_proxy = options.get(read_config_value(config, '录制设置', '是否使用代理ip(是/否)', "是"), False) + proxy_addr_bak = read_config_value(config, '录制设置', '代理地址', "") + proxy_addr = None if not use_proxy else proxy_addr_bak + max_request = int(read_config_value(config, '录制设置', '同一时间访问网络的线程数', 3)) + semaphore = threading.Semaphore(max_request) + delay_default = int(read_config_value(config, '录制设置', '循环时间(秒)', 120)) + local_delay_default = int(read_config_value(config, '录制设置', '排队读取网址时间(秒)', 0)) + loop_time = options.get(read_config_value(config, '录制设置', '是否显示循环秒数', "否"), False) + show_url = options.get(read_config_value(config, '录制设置', '是否显示直播源地址', "否"), False) + split_video_by_time = options.get(read_config_value(config, '录制设置', '分段录制是否开启', "否"), False) + enable_https_recording = options.get(read_config_value(config, '录制设置', '是否强制启用https录制', "否"), False) + disk_space_limit = float(read_config_value(config, '录制设置', '录制空间剩余阈值(gb)', 1.0)) + split_time = str(read_config_value(config, '录制设置', '视频分段时间(秒)', 1800)) + converts_to_mp4 = options.get(read_config_value(config, '录制设置', '录制完成后自动转为mp4格式', "否"), False) + converts_to_h264 = options.get(read_config_value(config, '录制设置', 'mp4格式重新编码为h264', "否"), False) + delete_origin_file = options.get(read_config_value(config, '录制设置', '追加格式后删除原文件', "否"), False) + create_time_file = options.get(read_config_value(config, '录制设置', '生成时间字幕文件', "否"), False) + is_run_script = options.get(read_config_value(config, '录制设置', '是否录制完成后执行自定义脚本', "否"), False) + custom_script = read_config_value(config, '录制设置', '自定义脚本执行命令', "") if is_run_script else None + enable_proxy_platform = read_config_value( + config, '录制设置', '使用代理录制的平台(逗号分隔)', + 'tiktok, soop, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu, faceit' + ) + enable_proxy_platform_list = enable_proxy_platform.replace(',', ',').split(',') if enable_proxy_platform else None + extra_enable_proxy = read_config_value(config, '录制设置', '额外使用代理录制的平台(逗号分隔)', '') + extra_enable_proxy_platform_list = extra_enable_proxy.replace(',', ',').split(',') if extra_enable_proxy else None + live_status_push = read_config_value(config, '推送配置', '直播状态推送渠道', "") + dingtalk_api_url = read_config_value(config, '推送配置', '钉钉推送接口链接', "") + xizhi_api_url = read_config_value(config, '推送配置', '微信推送接口链接', "") + bark_msg_api = read_config_value(config, '推送配置', 'bark推送接口链接', "") + bark_msg_level = read_config_value(config, '推送配置', 'bark推送中断级别', "active") + bark_msg_ring = read_config_value(config, '推送配置', 'bark推送铃声', "bell") + dingtalk_phone_num = read_config_value(config, '推送配置', '钉钉通知@对象(填手机号)', "") + dingtalk_is_atall = options.get(read_config_value(config, '推送配置', '钉钉通知@全体(是/否)', "否"), False) + tg_token = read_config_value(config, '推送配置', 'tgapi令牌', "") + tg_chat_id = read_config_value(config, '推送配置', 'tg聊天id(个人或者群组id)', "") + email_host = read_config_value(config, '推送配置', 'SMTP邮件服务器', "") + open_smtp_ssl = options.get(read_config_value(config, '推送配置', '是否使用SMTP服务SSL加密(是/否)', "是"), True) + smtp_port = read_config_value(config, '推送配置', 'SMTP邮件服务器端口', "") + login_email = read_config_value(config, '推送配置', '邮箱登录账号', "") + email_password = read_config_value(config, '推送配置', '发件人密码(授权码)', "") + sender_email = read_config_value(config, '推送配置', '发件人邮箱', "") + sender_name = read_config_value(config, '推送配置', '发件人显示昵称', "") + to_email = read_config_value(config, '推送配置', '收件人邮箱', "") + ntfy_api = read_config_value(config, '推送配置', 'ntfy推送地址', "") + ntfy_tags = read_config_value(config, '推送配置', 'ntfy推送标签', "tada") + ntfy_email = read_config_value(config, '推送配置', 'ntfy推送邮箱', "") + pushplus_token = read_config_value(config, '推送配置', 'pushplus推送token', "") + push_message_title = read_config_value(config, '推送配置', '自定义推送标题', "直播间状态更新通知") + begin_push_message_text = read_config_value(config, '推送配置', '自定义开播推送内容', "") + over_push_message_text = read_config_value(config, '推送配置', '自定义关播推送内容', "") + disable_record = options.get(read_config_value(config, '推送配置', '只推送通知不录制(是/否)', "否"), False) + push_check_seconds = int(read_config_value(config, '推送配置', '直播推送检测频率(秒)', 1800)) + begin_show_push = options.get(read_config_value(config, '推送配置', '开播推送开启(是/否)', "是"), True) + over_show_push = options.get(read_config_value(config, '推送配置', '关播推送开启(是/否)', "否"), False) + sooplive_username = read_config_value(config, '账号密码', 'sooplive账号', '') + sooplive_password = read_config_value(config, '账号密码', 'sooplive密码', '') + flextv_username = read_config_value(config, '账号密码', 'flextv账号', '') + flextv_password = read_config_value(config, '账号密码', 'flextv密码', '') + popkontv_username = read_config_value(config, '账号密码', 'popkontv账号', '') + popkontv_partner_code = read_config_value(config, '账号密码', 'partner_code', 'P-00001') + popkontv_password = read_config_value(config, '账号密码', 'popkontv密码', '') + twitcasting_account_type = read_config_value(config, '账号密码', 'twitcasting账号类型', 'normal') + twitcasting_username = read_config_value(config, '账号密码', 'twitcasting账号', '') + twitcasting_password = read_config_value(config, '账号密码', 'twitcasting密码', '') + popkontv_access_token = read_config_value(config, 'Authorization', 'popkontv_token', '') + dy_cookie = read_config_value(config, 'Cookie', '抖音cookie', '') + ks_cookie = read_config_value(config, 'Cookie', '快手cookie', '') + tiktok_cookie = read_config_value(config, 'Cookie', 'tiktok_cookie', '') + hy_cookie = read_config_value(config, 'Cookie', '虎牙cookie', '') + douyu_cookie = read_config_value(config, 'Cookie', '斗鱼cookie', '') + yy_cookie = read_config_value(config, 'Cookie', 'yy_cookie', '') + bili_cookie = read_config_value(config, 'Cookie', 'B站cookie', '') + xhs_cookie = read_config_value(config, 'Cookie', '小红书cookie', '') + bigo_cookie = read_config_value(config, 'Cookie', 'bigo_cookie', '') + blued_cookie = read_config_value(config, 'Cookie', 'blued_cookie', '') + sooplive_cookie = read_config_value(config, 'Cookie', 'sooplive_cookie', '') + netease_cookie = read_config_value(config, 'Cookie', 'netease_cookie', '') + qiandurebo_cookie = read_config_value(config, 'Cookie', '千度热播_cookie', '') + pandatv_cookie = read_config_value(config, 'Cookie', 'pandatv_cookie', '') + maoerfm_cookie = read_config_value(config, 'Cookie', '猫耳fm_cookie', '') + winktv_cookie = read_config_value(config, 'Cookie', 'winktv_cookie', '') + flextv_cookie = read_config_value(config, 'Cookie', 'flextv_cookie', '') + look_cookie = read_config_value(config, 'Cookie', 'look_cookie', '') + twitcasting_cookie = read_config_value(config, 'Cookie', 'twitcasting_cookie', '') + baidu_cookie = read_config_value(config, 'Cookie', 'baidu_cookie', '') + weibo_cookie = read_config_value(config, 'Cookie', 'weibo_cookie', '') + kugou_cookie = read_config_value(config, 'Cookie', 'kugou_cookie', '') + twitch_cookie = read_config_value(config, 'Cookie', 'twitch_cookie', '') + liveme_cookie = read_config_value(config, 'Cookie', 'liveme_cookie', '') + huajiao_cookie = read_config_value(config, 'Cookie', 'huajiao_cookie', '') + liuxing_cookie = read_config_value(config, 'Cookie', 'liuxing_cookie', '') + showroom_cookie = read_config_value(config, 'Cookie', 'showroom_cookie', '') + acfun_cookie = read_config_value(config, 'Cookie', 'acfun_cookie', '') + changliao_cookie = read_config_value(config, 'Cookie', 'changliao_cookie', '') + yinbo_cookie = read_config_value(config, 'Cookie', 'yinbo_cookie', '') + yingke_cookie = read_config_value(config, 'Cookie', 'yingke_cookie', '') + zhihu_cookie = read_config_value(config, 'Cookie', 'zhihu_cookie', '') + chzzk_cookie = read_config_value(config, 'Cookie', 'chzzk_cookie', '') + haixiu_cookie = read_config_value(config, 'Cookie', 'haixiu_cookie', '') + vvxqiu_cookie = read_config_value(config, 'Cookie', 'vvxqiu_cookie', '') + yiqilive_cookie = read_config_value(config, 'Cookie', '17live_cookie', '') + langlive_cookie = read_config_value(config, 'Cookie', 'langlive_cookie', '') + pplive_cookie = read_config_value(config, 'Cookie', 'pplive_cookie', '') + six_room_cookie = read_config_value(config, 'Cookie', '6room_cookie', '') + lehaitv_cookie = read_config_value(config, 'Cookie', 'lehaitv_cookie', '') + huamao_cookie = read_config_value(config, 'Cookie', 'huamao_cookie', '') + shopee_cookie = read_config_value(config, 'Cookie', 'shopee_cookie', '') + youtube_cookie = read_config_value(config, 'Cookie', 'youtube_cookie', '') + taobao_cookie = read_config_value(config, 'Cookie', 'taobao_cookie', '') + jd_cookie = read_config_value(config, 'Cookie', 'jd_cookie', '') + faceit_cookie = read_config_value(config, 'Cookie', 'faceit_cookie', '') + migu_cookie = read_config_value(config, 'Cookie', 'migu_cookie', '') + lianjie_cookie = read_config_value(config, 'Cookie', 'lianjie_cookie', '') + laixiu_cookie = read_config_value(config, 'Cookie', 'laixiu_cookie', '') + picarto_cookie = read_config_value(config, 'Cookie', 'picarto_cookie', '') + + video_save_type_list = ("FLV", "MKV", "TS", "MP4", "MP3音频", "M4A音频", "MP3", "M4A") + if video_save_type and video_save_type.upper() in video_save_type_list: + video_save_type = video_save_type.upper() + else: + video_save_type = "TS" + + check_path = video_save_path or default_path + if utils.check_disk_capacity(check_path, show=first_run) < disk_space_limit: + exit_recording = True + if not recording: + logger.warning(f"Disk space remaining is below {disk_space_limit} GB. " + f"Exiting program due to the disk space limit being reached.") + sys.exit(-1) + + + def contains_url(string: str) -> bool: + pattern = r"(https?://)?(www\.)?[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+(:\d+)?(/.*)?" + return re.search(pattern, string) is not None + + + try: + url_comments, line_list, url_line_list = [[] for _ in range(3)] + with (open(url_config_file, "r", encoding=text_encoding, errors='ignore') as file): + for origin_line in file: + if origin_line in line_list: + delete_line(url_config_file, origin_line) + line_list.append(origin_line) + line = origin_line.strip() + if len(line) < 18: + continue + + line_spilt = line.split('主播: ') + if len(line_spilt) > 2: + line = update_file(url_config_file, line, f'{line_spilt[0]}主播: {line_spilt[-1]}') + + is_comment_line = line.startswith("#") + if is_comment_line: + line = line.lstrip('#') + + if re.search('[,,]', line): + split_line = re.split('[,,]', line) + else: + split_line = [line, ''] + + if len(split_line) == 1: + url = split_line[0] + quality, name = [video_record_quality, ''] + elif len(split_line) == 2: + if contains_url(split_line[0]): + quality = video_record_quality + url, name = split_line + else: + quality, url = split_line + name = '' + else: + quality, url, name = split_line + + if quality not in ("原画", "蓝光", "超清", "高清", "标清", "流畅"): + quality = '原画' + + if url not in url_line_list: + url_line_list.append(url) + else: + delete_line(url_config_file, origin_line) + + url = 'https://' + url if '://' not in url else url + url_host = url.split('/')[2] + + platform_host = [ + 'live.douyin.com', + 'v.douyin.com', + 'www.douyin.com', + 'live.kuaishou.com', + 'www.huya.com', + 'www.douyu.com', + 'www.yy.com', + 'live.bilibili.com', + 'www.redelight.cn', + 'www.xiaohongshu.com', + 'xhslink.com', + 'www.bigo.tv', + 'slink.bigovideo.tv', + 'app.blued.cn', + 'cc.163.com', + 'qiandurebo.com', + 'fm.missevan.com', + 'look.163.com', + 'twitcasting.tv', + 'live.baidu.com', + 'weibo.com', + 'fanxing.kugou.com', + 'fanxing2.kugou.com', + 'mfanxing.kugou.com', + 'www.huajiao.com', + 'www.7u66.com', + 'wap.7u66.com', + 'live.acfun.cn', + 'm.acfun.cn', + 'live.tlclw.com', + 'wap.tlclw.com', + 'live.ybw1666.com', + 'wap.ybw1666.com', + 'www.inke.cn', + 'www.zhihu.com', + 'www.haixiutv.com', + "h5webcdnp.vvxqiu.com", + "17.live", + 'www.lang.live', + "m.pp.weimipopo.com", + "v.6.cn", + "m.6.cn", + 'www.lehaitv.com', + 'h.catshow168.com', + 'e.tb.cn', + 'huodong.m.taobao.com', + '3.cn', + 'eco.m.jd.com', + 'www.miguvideo.com', + 'm.miguvideo.com', + 'show.lailianjie.com', + 'www.imkktv.com', + 'www.picarto.tv' + ] + overseas_platform_host = [ + 'www.tiktok.com', + 'play.sooplive.co.kr', + 'm.sooplive.co.kr', + 'www.sooplive.com', + 'm.sooplive.com', + 'www.pandalive.co.kr', + 'www.winktv.co.kr', + 'www.flextv.co.kr', + 'www.ttinglive.com', + 'www.popkontv.com', + 'www.twitch.tv', + 'www.liveme.com', + 'www.showroom-live.com', + 'chzzk.naver.com', + 'm.chzzk.naver.com', + 'live.shopee.', + '.shp.ee', + 'www.youtube.com', + 'youtu.be', + 'www.faceit.com' + ] + + platform_host.extend(overseas_platform_host) + clean_url_host_list = ( + "live.douyin.com", + "live.bilibili.com", + "www.huajiao.com", + "www.zhihu.com", + "www.huya.com", + "chzzk.naver.com", + "www.liveme.com", + "www.haixiutv.com", + "v.6.cn", + "m.6.cn", + 'www.lehaitv.com' + ) + + if 'live.shopee.' in url_host or '.shp.ee' in url_host: + url_host = 'live.shopee.' if 'live.shopee.' in url_host else '.shp.ee' + + if url_host in platform_host or any(ext in url for ext in (".flv", ".m3u8")): + if url_host in clean_url_host_list: + url = update_file(url_config_file, old_str=url, new_str=url.split('?')[0]) + + if 'xiaohongshu' in url: + host_id = re.search('&host_id=(.*?)(?=&|$)', url) + if host_id: + new_url = url.split('?')[0] + f'?host_id={host_id.group(1)}' + url = update_file(url_config_file, old_str=url, new_str=new_url) + + url_comments = [i for i in url_comments if url not in i] + if is_comment_line: + url_comments.append(url) + else: + new_line = (quality, url, name) + url_tuples_list.append(new_line) + else: + if not origin_line.startswith('#'): + color_obj.print_colored(f"\r{origin_line.strip()} 本行包含未知链接.此条跳过", color_obj.YELLOW) + update_file(url_config_file, old_str=origin_line, new_str=origin_line, start_str='#') + + while len(need_update_line_list): + a = need_update_line_list.pop() + replace_words = a.split('|') + if replace_words[0] != replace_words[1]: + if replace_words[1].startswith("#"): + start_with = '#' + new_word = replace_words[1][1:] + else: + start_with = None + new_word = replace_words[1] + update_file(url_config_file, old_str=replace_words[0], new_str=new_word, start_str=start_with) + + text_no_repeat_url = list(set(url_tuples_list)) + + if len(text_no_repeat_url) > 0: + for url_tuple in text_no_repeat_url: + monitoring = len(running_list) + + if url_tuple[1] in not_record_list: + continue + if url_tuple[1] not in running_list: + print(f"\r{'新增' if not first_start else '传入'}地址: {url_tuple[1]}") + monitoring += 1 + args = [url_tuple, monitoring] + create_var[f'thread_{monitoring}'] = threading.Thread(target=start_record, args=args) + create_var[f'thread_{monitoring}'].daemon = True + create_var[f'thread_{monitoring}'].start() + running_list.append(url_tuple[1]) + time.sleep(local_delay_default) + + + + + url_tuples_list = [] + first_start = False + + + + except Exception as err: + logger.error(f"错误信息: {err} 发生错误的行数: {err.__traceback__.tb_lineno}") + + if first_run: + t = threading.Thread(target=display_info, args=(), daemon=True) + t.start() + t2 = threading.Thread(target=adjust_max_request, args=(), daemon=True) + t2.start() + first_run = False + + time.sleep(3) \ No newline at end of file diff --git a/mobile/.gitignore b/mobile/.gitignore new file mode 100644 index 0000000..d914c32 --- /dev/null +++ b/mobile/.gitignore @@ -0,0 +1,41 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# generated native folders +/ios +/android diff --git a/mobile/.vscode/extensions.json b/mobile/.vscode/extensions.json new file mode 100644 index 0000000..b7ed837 --- /dev/null +++ b/mobile/.vscode/extensions.json @@ -0,0 +1 @@ +{ "recommendations": ["expo.vscode-expo-tools"] } diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json new file mode 100644 index 0000000..e2798e4 --- /dev/null +++ b/mobile/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit", + "source.sortMembers": "explicit" + } +} diff --git a/mobile/app.json b/mobile/app.json new file mode 100644 index 0000000..03f2e21 --- /dev/null +++ b/mobile/app.json @@ -0,0 +1,48 @@ +{ + "expo": { + "name": "直播录制助手", + "slug": "live-recorder-mobile", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "mobile", + "userInterfaceStyle": "automatic", + "splash": { + "image": "./assets/images/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true + }, + "android": { + "package": "com.douyin.recorder.remote", + "usesCleartextTraffic": true, + "adaptiveIcon": { + "backgroundColor": "#E6F4FE", + "foregroundImage": "./assets/images/android-icon-foreground.png", + "backgroundImage": "./assets/images/android-icon-background.png", + "monochromeImage": "./assets/images/android-icon-monochrome.png" + }, + "predictiveBackGestureEnabled": false, + "permissions": ["CAMERA"] + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-camera", + { + "cameraPermission": "用于扫描 PC 端绑定二维码(含 EasyTier 组网信息)。" + } + ] + ], + "experiments": { + "typedRoutes": true + } + } +} diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..9267535 --- /dev/null +++ b/mobile/app/(tabs)/_layout.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import { Tabs } from 'expo-router' +import { Ionicons } from '@expo/vector-icons' + +import Colors from '@/constants/Colors' +import { useColorScheme } from '@/components/useColorScheme' +import { useClientOnlyValue } from '@/components/useClientOnlyValue' + +export default function TabLayout() { + const colorScheme = useColorScheme() + + return ( + + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + ) +} diff --git a/mobile/app/(tabs)/bind.tsx b/mobile/app/(tabs)/bind.tsx new file mode 100644 index 0000000..b215f95 --- /dev/null +++ b/mobile/app/(tabs)/bind.tsx @@ -0,0 +1,278 @@ +import { useCallback, useRef, useState } from 'react' +import { + View, + Text, + TextInput, + Pressable, + ScrollView, + StyleSheet, + Linking, + Alert, +} from 'react-native' +import { CameraView, useCameraPermissions } from 'expo-camera' +import * as Clipboard from 'expo-clipboard' +import { LinearGradient } from 'expo-linear-gradient' +import { SafeAreaView } from 'react-native-safe-area-context' +import { Ionicons } from '@expo/vector-icons' + +import { useBind } from '@/context/BindContext' +import { parseBindPayload, type BindPayload } from '@/lib/types' +import { theme } from '@/constants/theme' + +const ET_RELEASE = 'https://github.com/EasyTier/EasyTier/releases' + +export default function BindScreen() { + const { bind, setBind, clear, ready } = useBind() + const [paste, setPaste] = useState('') + const [msg, setMsg] = useState('') + const [scanOn, setScanOn] = useState(false) + const [perm, requestPerm] = useCameraPermissions() + const scannedRef = useRef(false) + + const applyPayload = useCallback( + async (p: BindPayload | null) => { + if (!p) { + setMsg('无法解析:请确认扫到的是 PC 端「生成绑定二维码」的 JSON。') + return + } + await setBind(p) + setMsg('已保存配对。请到「录制」页操作。') + setScanOn(false) + scannedRef.current = false + }, + [setBind], + ) + + const onBarCode = useCallback( + async ({ data }: { data: string }) => { + if (scannedRef.current) return + scannedRef.current = true + const p = parseBindPayload(data) + await applyPayload(p) + }, + [applyPayload], + ) + + const onPasteApply = useCallback(async () => { + const p = parseBindPayload(paste.trim()) + await applyPayload(p) + }, [paste, applyPayload]) + + const copyEtJson = useCallback(async () => { + if (!bind) return + const chunk = { + networkName: bind.easytier.networkName, + networkSecret: bind.easytier.networkSecret, + peerUrls: bind.easytier.peerUrls, + } + await Clipboard.setStringAsync(JSON.stringify(chunk, null, 2)) + setMsg('已复制 EasyTier 字段(可粘贴到官方客户端)。') + }, [bind]) + + const copyFull = useCallback(async () => { + if (!bind) return + await Clipboard.setStringAsync(JSON.stringify(bind, null, 2)) + setMsg('已复制完整 JSON。') + }, [bind]) + + const testConn = useCallback(async () => { + if (!bind) return + try { + const r = await fetch(`${bind.baseUrl}/health`, { + headers: bind.token ? { Authorization: `Bearer ${bind.token}` } : undefined, + }) + const j = await r.json().catch(() => ({})) + Alert.alert('连通测试', r.ok ? `OK: ${JSON.stringify(j)}` : `HTTP ${r.status}`) + } catch (e) { + Alert.alert('连通失败', e instanceof Error ? e.message : String(e)) + } + }, [bind]) + + if (!ready) { + return ( + + 加载中… + + ) + } + + return ( + + + 配对与 EasyTier + + 在 PC 控制台「网络 → 远程控制」开启远程并生成二维码,用手机扫描;无需公网,请确保手机与 PC 在同一 EasyTier + 虚拟网或局域网。 + + + {bind && ( + + 当前已绑定 + {bind.baseUrl} + 组网名 · {bind.easytier.networkName} + + void testConn()}> + 测试 API + + { + Alert.alert('清除配对', '确定要清除本机保存的绑定信息?', [ + { text: '取消', style: 'cancel' }, + { text: '清除', style: 'destructive', onPress: () => void clear() }, + ]) + }} + > + 清除 + + + + )} + + + EasyTier 无公网互通 + + PC 端已内置 easytier-core 并写入组网名与密钥。手机需运行官方 EasyTier 客户端并填入 + 相同 + 组网参数,获得虚拟 IP 后即可访问上方 API 地址(通常为 http://虚拟IP:8001)。 + + void Linking.openURL(ET_RELEASE)} style={styles.linkRow}> + + EasyTier 发行版(含 Android) + + {bind && ( + + void copyEtJson()}> + 复制组网 JSON + + void copyFull()}> + 复制完整绑定 JSON + + + )} + + + + 扫码 + {!scanOn ? ( + { + const r = await requestPerm() + if (!r.granted) { + setMsg('需要相机权限才能扫码') + return + } + scannedRef.current = false + setScanOn(true) + }} + > + + 打开相机扫 PC 二维码 + + ) : perm?.granted ? ( + + + setScanOn(false)}> + 关闭相机 + + + ) : ( + 无相机权限 + )} + + + + 手动粘贴 JSON + + void onPasteApply()}> + 解析并保存 + + + + {!!msg && {msg}} + + + ) +} + +const styles = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.bg }, + scroll: { padding: 20, paddingBottom: 40 }, + h1: { fontSize: 22, fontWeight: '700', color: theme.text, marginBottom: 8 }, + lead: { fontSize: 14, color: theme.muted, lineHeight: 22, marginBottom: 16 }, + h2: { fontSize: 16, fontWeight: '600', color: theme.text, marginBottom: 8 }, + body: { fontSize: 14, color: theme.muted, lineHeight: 22, marginBottom: 10 }, + block: { marginBottom: 22 }, + card: { + borderRadius: 12, + padding: 16, + marginBottom: 18, + borderWidth: 1, + borderColor: theme.border, + }, + cardTitle: { fontSize: 13, fontWeight: '600', color: theme.accent, marginBottom: 8 }, + mono: { fontFamily: 'monospace', fontSize: 13, color: theme.text, marginBottom: 6 }, + muted: { color: theme.muted, fontSize: 14 }, + mutedSmall: { color: theme.muted, fontSize: 12, marginTop: 6 }, + row: { flexDirection: 'row', gap: 10, marginTop: 12 }, + etActions: { gap: 10, marginTop: 12 }, + btn: { + backgroundColor: theme.bgElevated, + paddingVertical: 10, + paddingHorizontal: 14, + borderRadius: 8, + borderWidth: 1, + borderColor: theme.border, + }, + btnDanger: { backgroundColor: 'rgba(245,101,101,0.15)' }, + btnText: { color: theme.text, fontWeight: '600', fontSize: 14 }, + btnPrimary: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#3d7eff', + paddingVertical: 14, + borderRadius: 10, + }, + btnPrimaryText: { color: '#fff', fontWeight: '700', fontSize: 15 }, + btnSecondary: { + borderWidth: 1, + borderColor: theme.border, + borderRadius: 10, + paddingVertical: 12, + alignItems: 'center', + backgroundColor: theme.bgCard, + }, + btnSecondaryText: { color: theme.text, fontWeight: '600' }, + input: { + minHeight: 120, + borderWidth: 1, + borderColor: theme.border, + borderRadius: 10, + padding: 12, + color: theme.text, + fontFamily: 'monospace', + fontSize: 12, + marginBottom: 12, + backgroundColor: theme.bgCard, + }, + camWrap: { borderRadius: 12, overflow: 'hidden', backgroundColor: '#000' }, + camera: { height: 280, width: '100%' }, + camClose: { padding: 12, alignItems: 'center', backgroundColor: '#111' }, + linkRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginVertical: 8 }, + link: { color: theme.accent, fontSize: 14 }, + msg: { color: theme.success, fontSize: 14, marginTop: 8 }, +}) diff --git a/mobile/app/(tabs)/desk.tsx b/mobile/app/(tabs)/desk.tsx new file mode 100644 index 0000000..fda0570 --- /dev/null +++ b/mobile/app/(tabs)/desk.tsx @@ -0,0 +1,76 @@ +import { useCallback, useEffect, useState } from 'react' +import { View, Text, ScrollView, StyleSheet, RefreshControl } from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' + +import { useBind } from '@/context/BindContext' +import { apiFetch } from '@/lib/api' +import { theme } from '@/constants/theme' + +export default function DeskScreen() { + const { bind } = useBind() + const [raw, setRaw] = useState('') + const [err, setErr] = useState('') + const [refreshing, setRefreshing] = useState(false) + + const load = useCallback(async () => { + if (!bind) { + setErr('未绑定 PC') + setRaw('') + return + } + setErr('') + try { + const r = await apiFetch(bind, '/client/overview') + if (!r.ok) throw new Error(String(r.status)) + const j = await r.json() + setRaw(JSON.stringify(j, null, 2)) + } catch (e) { + setErr(e instanceof Error ? e.message : String(e)) + setRaw('') + } + }, [bind]) + + useEffect(() => { + void load() + }, [load]) + + const onRefresh = useCallback(async () => { + setRefreshing(true) + await load() + setRefreshing(false) + }, [load]) + + return ( + + void onRefresh()} />} + > + 工作台 + 来自后端 /client/overview 的摘要(与桌面工作台同源数据)。 + {err ? {err} : null} + {raw ? {raw} : !err ? 暂无数据 : null} + + + ) +} + +const styles = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.bg }, + scroll: { padding: 20, paddingBottom: 40 }, + h1: { fontSize: 22, fontWeight: '700', color: theme.text, marginBottom: 8 }, + lead: { fontSize: 14, color: theme.muted, marginBottom: 16, lineHeight: 22 }, + pre: { + fontFamily: 'monospace', + fontSize: 11, + color: '#c8cdd5', + lineHeight: 18, + backgroundColor: theme.bgCard, + padding: 12, + borderRadius: 10, + borderWidth: 1, + borderColor: theme.border, + }, + err: { color: theme.danger, marginBottom: 12 }, + muted: { color: theme.muted }, +}) diff --git a/mobile/app/(tabs)/index.tsx b/mobile/app/(tabs)/index.tsx new file mode 100644 index 0000000..0b46968 --- /dev/null +++ b/mobile/app/(tabs)/index.tsx @@ -0,0 +1,410 @@ +import { useMemo } from 'react' +import { + View, + Text, + ScrollView, + TextInput, + Pressable, + StyleSheet, + ActivityIndicator, +} from 'react-native' +import { Picker } from '@react-native-picker/picker' +import { SafeAreaView } from 'react-native-safe-area-context' + +import { useBind } from '@/context/BindContext' +import { useRecorder } from '@/hooks/useRecorder' +import { theme } from '@/constants/theme' + +function ctrlStyle(action: string): object { + switch (action) { + case 'start': + return styles.ctrl_start + case 'stop': + return styles.ctrl_stop + case 'restart': + return styles.ctrl_restart + case 'status': + return styles.ctrl_status + default: + return {} + } +} + +const variantColor: Record = { + online: theme.success, + stopped: theme.danger, + error: theme.danger, + warn: theme.warn, + offline: theme.danger, + loading: theme.muted, + unknown: theme.accent, + empty: theme.accent, +} + +export default function RecordScreen() { + const { bind } = useBind() + const r = useRecorder() + + const statusColor = useMemo(() => variantColor[r.statusMeta.variant] ?? theme.muted, [r.statusMeta.variant]) + + if (r.bootError && bind) { + return ( + + 无法连接:{r.bootError} + + ) + } + + if (!bind) { + return ( + + 请先在「配对」页扫码绑定 PC。 + + ) + } + + return ( + + + + 当前录制 + + r.setCurrentProcess(String(v))} + style={styles.picker} + dropdownIconColor={theme.text} + > + {r.entries.map((e) => ( + + ))} + + + {r.isYoutube && ( + void r.togglePro(!r.multiChannelPro)} + > + Pro 转播 + + {r.multiChannelPro ? '开' : '关'} + + + )} + + + + + {r.statusMeta.line} + + + {r.emptyHint ? ( + {r.emptyHint} + ) : ( + <> + {r.ent && ( + + 任务 + {r.ent.label} + {r.ent.script} + + )} + + {r.isYoutube && r.multiChannelPro && ( + + Pro 线路 + {r.proChannelsError ? ( + {r.proChannelsError} + ) : ( + + r.setProChannelId(String(v))} + style={styles.picker} + dropdownIconColor={theme.text} + > + {r.proChannelsList.map((c) => ( + + ))} + + + )} + + )} + + {r.isYoutube && r.multiChannelPro && !r.proChannelsError && r.proChannelsList.length > 0 && ( + <> + + YouTube 密钥(当前线路) + + void r.saveProChannelKey()} + > + {r.saveProKeyLoading ? ( + + ) : ( + 保存密钥 + )} + + {r.youtubeStatus} + + + 地址列表(当前线路) + + + void r.saveProUrlConfig()} + disabled={r.saveUrlLoading} + > + {r.saveUrlLoading ? '…' : '保存地址'} + + + {r.urlStatus} + + + )} + + {r.isYoutube && !r.multiChannelPro && ( + + youtube.ini + + + void r.loadConfig('/get_config', r.setYoutubeText, r.setYoutubeStatus)} + > + 重新读取 + + { + r.setSaveYoutubeLoading(true) + void r.saveConfig('/save_config', r.youtubeText, r.setYoutubeStatus, () => + r.setSaveYoutubeLoading(false), + ) + }} + > + 保存 + + + {r.youtubeStatus} + {!r.UI_SHOW_YOUTUBE_GOOGLE_OAUTH && ( + Google 授权与拉取密钥请在 PC 端操作。 + )} + + )} + + {((r.isYoutube && !r.multiChannelPro) || r.isTiktok) && ( + + 直播地址列表 + + + void r.loadConfig('/get_url_config', r.setUrlText, r.setUrlStatus)} + > + 重新读取 + + { + r.setSaveUrlLoading(true) + void r.saveConfig('/save_url_config', r.urlText, r.setUrlStatus, () => + r.setSaveUrlLoading(false), + ) + }} + > + 保存 + + + {r.urlStatus} + + )} + + {!r.UI_SHOW_YOUTUBE_GOOGLE_OAUTH && r.isObs && ( + + OBS 推流 + srt://live.local:10080/live/obs + + )} + + + 录制开关 + + {( + [ + ['start', '开始录制'], + ['stop', '停止录制'], + ['restart', '重新开始'], + ['status', '刷新状态'], + ] as const + ).map(([action, label]) => ( + void r.runPm2Action(action)} + > + + {label} + + + ))} + + + + + 运行记录 + + {r.logText || '—'} + + + + )} + + + ) +} + +const styles = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.bg }, + scroll: { padding: 16, paddingBottom: 32 }, + toolbar: { marginBottom: 12 }, + label: { fontSize: 12, color: theme.muted, marginBottom: 6 }, + pickerWrap: { + borderWidth: 1, + borderColor: theme.border, + borderRadius: 10, + overflow: 'hidden', + backgroundColor: theme.bgCard, + }, + picker: { color: theme.text }, + proRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 12, + paddingVertical: 10, + paddingHorizontal: 12, + backgroundColor: theme.bgCard, + borderRadius: 10, + borderWidth: 1, + borderColor: theme.border, + }, + proLabel: { color: theme.text, fontWeight: '600' }, + proVal: { color: theme.muted, fontWeight: '600' }, + pill: { + flexDirection: 'row', + alignItems: 'center', + alignSelf: 'flex-start', + gap: 8, + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 999, + borderWidth: 1, + marginBottom: 14, + }, + dot: { width: 8, height: 8, borderRadius: 4 }, + pillText: { fontSize: 13, fontWeight: '600' }, + meta: { + padding: 12, + borderRadius: 10, + backgroundColor: theme.bgElevated, + borderWidth: 1, + borderColor: theme.border, + marginBottom: 12, + }, + metaLabel: { fontSize: 12, color: theme.muted }, + metaName: { fontSize: 15, fontWeight: '600', color: theme.text, marginTop: 4 }, + metaFile: { fontSize: 11, fontFamily: 'monospace', color: theme.muted, marginTop: 4 }, + card: { + padding: 14, + borderRadius: 12, + backgroundColor: theme.bgCard, + borderWidth: 1, + borderColor: theme.border, + marginBottom: 12, + }, + cardAccent: { + borderColor: 'rgba(91,157,255,0.35)', + backgroundColor: 'rgba(91,157,255,0.06)', + }, + cardTitle: { fontSize: 15, fontWeight: '600', color: theme.text, marginBottom: 10 }, + area: { + minHeight: 100, + borderWidth: 1, + borderColor: theme.border, + borderRadius: 10, + padding: 10, + color: theme.text, + fontFamily: 'monospace', + fontSize: 12, + marginBottom: 10, + backgroundColor: theme.bg, + }, + row: { flexDirection: 'row', gap: 10, flexWrap: 'wrap' }, + btn: { + backgroundColor: '#3d7eff', + paddingVertical: 10, + paddingHorizontal: 18, + borderRadius: 10, + minWidth: 100, + alignItems: 'center', + }, + btnDisabled: { opacity: 0.55 }, + btnText: { color: '#fff', fontWeight: '700' }, + btnSec: { backgroundColor: theme.bgElevated, borderWidth: 1, borderColor: theme.border }, + btnSecText: { color: theme.text, fontWeight: '600' }, + hint: { color: theme.muted, fontSize: 14, lineHeight: 22 }, + hintSmall: { color: theme.muted, fontSize: 12, marginTop: 8 }, + err: { color: theme.danger, fontSize: 14 }, + mono: { fontFamily: 'monospace', color: theme.accent, fontSize: 13 }, + grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 }, + ctrl: { flexGrow: 1, minWidth: '42%', paddingVertical: 14, borderRadius: 10, alignItems: 'center' }, + ctrl_start: { backgroundColor: '#0e7a0d' }, + ctrl_stop: { backgroundColor: '#d1342c' }, + ctrl_restart: { backgroundColor: '#e68600' }, + ctrl_status: { backgroundColor: theme.bgElevated, borderWidth: 1, borderColor: theme.border }, + ctrlText: { color: '#fff', fontWeight: '700', fontSize: 14 }, + logBox: { maxHeight: 280, backgroundColor: '#1e2128', borderRadius: 10, padding: 10 }, + log: { fontFamily: 'monospace', fontSize: 11, color: '#d0d4dc', lineHeight: 18 }, +}) diff --git a/mobile/app/(tabs)/more.tsx b/mobile/app/(tabs)/more.tsx new file mode 100644 index 0000000..54190f7 --- /dev/null +++ b/mobile/app/(tabs)/more.tsx @@ -0,0 +1,177 @@ +import { useCallback, useEffect, useState } from 'react' +import { + View, + Text, + ScrollView, + StyleSheet, + TextInput, + Pressable, + Switch, + RefreshControl, +} from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' + +import { useBind } from '@/context/BindContext' +import { apiFetch } from '@/lib/api' +import { theme } from '@/constants/theme' + +type ProxyState = { enabled: boolean; http: string; socks: string } + +export default function MoreScreen() { + const { bind } = useBind() + const [proxy, setProxy] = useState({ enabled: false, http: '', socks: '' }) + const [metrics, setMetrics] = useState('') + const [msg, setMsg] = useState('') + const [refreshing, setRefreshing] = useState(false) + + const load = useCallback(async () => { + if (!bind) return + setMsg('') + try { + const pr = await apiFetch(bind, '/proxy_config') + if (pr.ok) { + const p = (await pr.json()) as ProxyState + setProxy({ + enabled: !!p.enabled, + http: String(p.http || ''), + socks: String(p.socks || ''), + }) + } + const mr = await apiFetch(bind, '/system_metrics') + if (mr.ok) { + const m = await mr.json() + setMetrics(JSON.stringify(m, null, 2)) + } + } catch (e) { + setMsg(e instanceof Error ? e.message : String(e)) + } + }, [bind]) + + useEffect(() => { + void load() + }, [load]) + + const onRefresh = useCallback(async () => { + setRefreshing(true) + await load() + setRefreshing(false) + }, [load]) + + const saveProxy = useCallback(async () => { + if (!bind) return + try { + const res = await apiFetch(bind, '/proxy_config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(proxy), + }) + if (!res.ok) throw new Error(String(res.status)) + setMsg('代理已保存') + } catch (e) { + setMsg(e instanceof Error ? e.message : String(e)) + } + }, [bind, proxy]) + + if (!bind) { + return ( + + 请先完成配对 + + ) + } + + return ( + + void onRefresh()} />} + > + 更多 + + + 录制代理(仅进程) + + 启用 + setProxy((p) => ({ ...p, enabled: v }))} /> + + HTTP + setProxy((p) => ({ ...p, http: t }))} + placeholder="http://..." + placeholderTextColor={theme.muted} + autoCapitalize="none" + /> + SOCKS + setProxy((p) => ({ ...p, socks: t }))} + placeholder="socks5://..." + placeholderTextColor={theme.muted} + autoCapitalize="none" + /> + void saveProxy()}> + 保存代理 + + + + + 系统指标 + {metrics ? {metrics} : 加载中…} + + + + 微信 Chatbot + + 扫码登录与 Chatbot 配置依赖本机环境与桌面端逻辑,请在 Windows 控制台「微信」页完成。 + + + + {!!msg && {msg}} + + + ) +} + +const styles = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.bg }, + scroll: { padding: 20, paddingBottom: 40 }, + h1: { fontSize: 22, fontWeight: '700', color: theme.text, marginBottom: 16 }, + card: { + padding: 14, + borderRadius: 12, + backgroundColor: theme.bgCard, + borderWidth: 1, + borderColor: theme.border, + marginBottom: 14, + }, + cardTitle: { fontSize: 15, fontWeight: '600', color: theme.text, marginBottom: 12 }, + row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }, + label: { fontSize: 12, color: theme.muted, marginBottom: 6 }, + input: { + borderWidth: 1, + borderColor: theme.border, + borderRadius: 10, + padding: 10, + color: theme.text, + marginBottom: 12, + backgroundColor: theme.bg, + }, + btn: { + backgroundColor: '#3d7eff', + paddingVertical: 12, + borderRadius: 10, + alignItems: 'center', + }, + btnText: { color: '#fff', fontWeight: '700' }, + pre: { + fontFamily: 'monospace', + fontSize: 10, + color: '#c8cdd5', + lineHeight: 16, + }, + muted: { color: theme.muted, fontSize: 13, lineHeight: 20 }, + ok: { color: theme.success, marginTop: 8 }, +}) diff --git a/mobile/app/(tabs)/network.tsx b/mobile/app/(tabs)/network.tsx new file mode 100644 index 0000000..6a2ef01 --- /dev/null +++ b/mobile/app/(tabs)/network.tsx @@ -0,0 +1,98 @@ +import { useCallback, useEffect, useState } from 'react' +import { View, Text, ScrollView, StyleSheet, RefreshControl } from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' + +import { useBind } from '@/context/BindContext' +import { apiFetch } from '@/lib/api' +import { theme } from '@/constants/theme' + +export default function NetworkScreen() { + const { bind } = useBind() + const [raw, setRaw] = useState('') + const [err, setErr] = useState('') + const [refreshing, setRefreshing] = useState(false) + + const load = useCallback(async () => { + if (!bind) { + setErr('未绑定') + setRaw('') + return + } + setErr('') + try { + const r = await apiFetch(bind, '/network_status') + if (!r.ok) throw new Error(String(r.status)) + const j = await r.json() + setRaw(JSON.stringify(j, null, 2)) + } catch (e) { + setErr(e instanceof Error ? e.message : String(e)) + setRaw('') + } + }, [bind]) + + useEffect(() => { + void load() + }, [load]) + + const onRefresh = useCallback(async () => { + setRefreshing(true) + await load() + setRefreshing(false) + }, [load]) + + return ( + + void onRefresh()} />} + > + 网络 + Google / 抖音 连通与测速。无公网时请通过「配对」页完成 EasyTier 组网。 + + {bind && ( + + EasyTier(本机已保存) + 组网名 {bind.easytier.networkName} + + 密钥与 peer 已随扫码写入;与 PC 完全一致即可在虚拟网内访问 {bind.baseUrl} + + + )} + + {err ? {err} : null} + {raw ? {raw} : !err && bind ? 暂无 : null} + + + ) +} + +const styles = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.bg }, + scroll: { padding: 20, paddingBottom: 40 }, + h1: { fontSize: 22, fontWeight: '700', color: theme.text, marginBottom: 8 }, + lead: { fontSize: 14, color: theme.muted, marginBottom: 16, lineHeight: 22 }, + et: { + padding: 14, + borderRadius: 12, + backgroundColor: 'rgba(124,92,255,0.08)', + borderWidth: 1, + borderColor: 'rgba(124,92,255,0.25)', + marginBottom: 16, + }, + etTitle: { fontSize: 14, fontWeight: '600', color: theme.text, marginBottom: 8 }, + mono: { fontFamily: 'monospace', fontSize: 12, color: theme.accent }, + mutedSmall: { fontSize: 12, color: theme.muted, marginTop: 8, lineHeight: 18 }, + pre: { + fontFamily: 'monospace', + fontSize: 11, + color: '#c8cdd5', + lineHeight: 18, + backgroundColor: theme.bgCard, + padding: 12, + borderRadius: 10, + borderWidth: 1, + borderColor: theme.border, + }, + err: { color: theme.danger, marginBottom: 12 }, + muted: { color: theme.muted }, +}) diff --git a/mobile/app/+html.tsx b/mobile/app/+html.tsx new file mode 100644 index 0000000..cb31090 --- /dev/null +++ b/mobile/app/+html.tsx @@ -0,0 +1,38 @@ +import { ScrollViewStyleReset } from 'expo-router/html'; + +// This file is web-only and used to configure the root HTML for every +// web page during static rendering. +// The contents of this function only run in Node.js environments and +// do not have access to the DOM or browser APIs. +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {/* + Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. + However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. + */} + + + {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} + + + +
+

进程: hdmi ⚪ 加载中...

+ +

视频播放器(M3U8 / FLV)

+ + +
+ +
加载中...
+
+ +

PM2 进程控制

+
+ + + + +
+ +

实时日志(自动每秒刷新)

+
正在加载状态与日志...
+
+ + + + diff --git a/tiktok/tiktok.py b/tiktok/tiktok.py new file mode 100644 index 0000000..60c7d92 --- /dev/null +++ b/tiktok/tiktok.py @@ -0,0 +1,142 @@ +from fastapi import FastAPI +from fastapi.responses import FileResponse, JSONResponse +from pathlib import Path +import asyncio +import re +from fastapi.middleware.cors import CORSMiddleware + +app = FastAPI(title="HDMI 录制控制台") + +# ---------------- 配置 ---------------- +BOT_NAME = "hdmi" +BASE_DIR = Path(__file__).parent +DEFAULT_LOG_DIR = BASE_DIR.parent / ".pm2" / "logs" +MAX_LOG_LINES = 300 + +# CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["GET"], + allow_headers=["*"], +) + +# ---------------- 工具 ---------------- +def strip_ansi_codes(text: str) -> str: + return re.sub(r'\x1b\[[0-9;]*m', '', text) + +async def run_pm2(cmd: str) -> str: + process = await asyncio.create_subprocess_shell( + f"pm2 {cmd}", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + try: + stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=15.0) + return (stdout or stderr).decode(errors="ignore").strip() + except asyncio.TimeoutError: + process.kill() + return "ERROR: PM2 命令超时" + +async def read_log_path(path: str, lines: int) -> str: + if not path: + return "无日志路径\n" + if "/dev/null" in path: + return "日志输出被禁用(PM2 配置为无日志)\n" + log_file = Path(path) + if not log_file.exists(): + return f"日志文件不存在: {path}\n" + try: + with open(log_file, "r", encoding="utf-8", errors="ignore") as f: + content_lines = f.readlines() + return "".join(content_lines[-lines:]) if content_lines else "无日志内容\n" + except Exception as e: + return f"读取日志失败 ({path}): {str(e)}\n" + +async def get_hdmi_info() -> dict: + describe_raw = await run_pm2(f"describe {BOT_NAME}") + describe_clean = strip_ansi_codes(describe_raw) + + if ("No such process" in describe_clean or + "Unknown process" in describe_clean or + "not found" in describe_clean.lower() or + not describe_clean.strip()): + return { + "process_status": "not_found", + "out_path": None, + "err_path": None, + } + + status = "unknown" + out_path = None + err_path = None + + for line in describe_raw.splitlines(): + line_clean = strip_ansi_codes(line) + if line_clean.count('│') < 2: + continue + parts = [p.strip() for p in line_clean.split('│') if p.strip()] + + if len(parts) >= 2: + key = parts[0].lower() + value = parts[1] + + if key == "status": + status = value.lower() + elif key == "out_log_path": + out_path = value + elif key == "err_log_path": + err_path = value + + # 备用默认路径(仅当 describe 未提供时) + if not out_path: + out_path = str(DEFAULT_LOG_DIR / f"{BOT_NAME}-out.log") + if not err_path: + err_path = str(DEFAULT_LOG_DIR / f"{BOT_NAME}-error.log") + + return { + "process_status": status, + "out_path": out_path, + "err_path": err_path, + } + +# ---------------- 路由 ---------------- +@app.get("/start") +async def start(): + output = await run_pm2(f"start {BOT_NAME}") + return JSONResponse({"output": output}) + +@app.get("/stop") +async def stop(): + output = await run_pm2(f"stop {BOT_NAME}") + return JSONResponse({"output": output}) + +@app.get("/restart") +async def restart(): + output = await run_pm2(f"restart {BOT_NAME}") + return JSONResponse({"output": output}) + +@app.get("/status") +async def status(): + full_status_raw = await run_pm2("status") + full_status = strip_ansi_codes(full_status_raw) # 去除颜色代码,前端显示更干净 + + info = await get_hdmi_info() + + recent_log = await read_log_path(info["out_path"], MAX_LOG_LINES) + recent_error = await read_log_path(info["err_path"], MAX_LOG_LINES) + + if info["process_status"] == "not_found": + recent_log = "进程未注册到 PM2(可能被 pm2 delete 删除或从未保存)\n" + recent_error = "" + + return JSONResponse({ + "raw_status": full_status, + "process_status": info["process_status"], + "recent_log": recent_log, + "recent_error": recent_error, + }) + +@app.get("/") +async def index(): + return FileResponse(BASE_DIR / "index.html") diff --git a/tiktok/tiktok.sh b/tiktok/tiktok.sh new file mode 100644 index 0000000..584810b --- /dev/null +++ b/tiktok/tiktok.sh @@ -0,0 +1 @@ +uvicorn web:app --host 0.0.0.0 --port 8000 --reload diff --git a/tiktok2.py b/tiktok2.py new file mode 100644 index 0000000..21e38c9 --- /dev/null +++ b/tiktok2.py @@ -0,0 +1,2207 @@ +# -*- encoding: utf-8 -*- + +""" +Author: Hmily +GitHub: https://github.com/ihmily +Date: 2023-07-17 23:52:05 +Update: 2025-10-23 19:48:05 +Copyright (c) 2023-2025 by Hmily, All Rights Reserved. +Function: Record live stream video. +""" +import asyncio +import os +import sys +import builtins +import subprocess +import signal +import threading +import time +import datetime +import re +import shutil +import random +import uuid +from pathlib import Path +import urllib.request +from urllib.error import URLError, HTTPError +from typing import Any +import configparser +import httpx +from src import spider, stream +from src.proxy import ProxyDetector +from src.utils import logger +from src import utils +from backup.msg_push import ( + dingtalk, xizhi, tg_bot, send_email, bark, ntfy, pushplus +) +from ffmpeg_install import ( + check_ffmpeg, ffmpeg_path, current_env_path +) + + + + + +version = "v4.0.7" +platforms = ("\n国内站点:抖音|快手|虎牙|斗鱼|YY|B站|小红书|bigo|blued|网易CC|千度热播|猫耳FM|Look|TwitCasting|百度|微博|" + "酷狗|花椒|流星|Acfun|畅聊|映客|音播|知乎|嗨秀|VV星球|17Live|浪Live|漂漂|六间房|乐嗨|花猫|淘宝|京东|咪咕|连接|来秀" + "\n海外站点:TikTok|SOOP|PandaTV|WinkTV|FlexTV|PopkonTV|TwitchTV|LiveMe|ShowRoom|CHZZK|Shopee|" + "Youtube|Faceit|Picarto") + +recording = set() +error_count = 0 +pre_max_request = 10 +max_request_lock = threading.Lock() +error_window = [] +error_window_size = 10 +error_threshold = 5 +monitoring = 0 +running_list = [] +url_tuples_list = [] +url_comments = [] +text_no_repeat_url = [] +create_var = locals() +first_start = True +exit_recording = False +need_update_line_list = [] +first_run = True +not_record_list = [] +start_display_time = datetime.datetime.now() +global_proxy = False +recording_time_list = {} +script_path = os.path.split(os.path.realpath(sys.argv[0]))[0] +config_file = f'{script_path}/config/config.ini' +url_config_file = f'{script_path}/config/URL_config.ini' +backup_dir = f'{script_path}/backup_config' +text_encoding = 'utf-8-sig' +rstr = r"[\/\\\:\*\??\"\<\>\|&#.。,, ~!· ]" +default_path = f'{script_path}/downloads' +os.makedirs(default_path, exist_ok=True) +file_update_lock = threading.Lock() +os_type = os.name +clear_command = "cls" if os_type == 'nt' else "clear" +color_obj = utils.Color() +os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path + + +def signal_handler(_signal, _frame): + sys.exit(0) + + +signal.signal(signal.SIGTERM, signal_handler) + + + +import shutil + + +# 全局变量(只初始化一次) +_last_lines_count = 0 + +def _clear_lines(n: int): + """向上删除 n 行(不闪烁)""" + if n <= 0: + return + print(f"\033[{n}A\033[K", end="", flush=True) # 上移 + 清行 + +def display_info() -> None: + global start_display_time, _last_lines_count + time.sleep(3) # 启动延迟 + + while True: + try: + lines = [] + now_str = datetime.datetime.now().strftime("%H:%M:%S") + + # 第一行:核心状态 + lines.append( + f"监测 {monitoring} | 并发 {max_request} | " + f"代理 {'开' if use_proxy else '关'} | " + f"分段 {'开' if split_video_by_time else '关'}{f':{split_time}s' if split_video_by_time else ''} | " + f"时间文件 {'开' if create_time_file else '关'} | " + f"质量 {video_record_quality} | 格式 {video_save_type} | " + f"错误 {error_count} | {now_str}" + ) + + # 第二行:录制中的直播 + if recording: + no_repeat = list(set(recording)) + recording_str = f"正在录制 {len(no_repeat)} 个: " + details = [] + now_dt = datetime.datetime.now() + for name in no_repeat[:10]: # 最多显示10个,防止爆屏 + if name not in recording_time_list: + continue + start_time, quality = recording_time_list[name] + duration = str(now_dt - start_time).split('.')[0] + details.append(f"{name.split()[-1]}[{quality} {duration}]") + recording_str += " | ".join(details) + if len(no_repeat) > 10: + recording_str += f" ... (+{len(no_repeat)-10})" + lines.append(recording_str) + else: + if monitoring == 0: + lines.append("没有正在监测的直播") + else: + lines.append(f"无录制任务 | 循环间隔 {delay_default}s") + + # 动态清屏 + 重绘(不闪烁!) + terminal_width, _ = shutil.get_terminal_size(fallback=(120, 24)) + display_lines = [] + for line in lines: + if len(line) > terminal_width - 5: + line = line[:terminal_width-8] + "..." + display_lines.append(line) + + # 删除旧内容 + 打印新内容 + if _last_lines_count > len(display_lines): + _clear_lines(_last_lines_count - 1) + print("\r" + display_lines[0]) + for line in display_lines[1:]: + print(line) + else: + print("\r" + display_lines[0], end="") + for line in display_lines[1:]: + print(f"\n{line}", end="") + + _last_lines_count = len(display_lines) + print(flush=True) # 强制刷新 + + time.sleep(5) + + except Exception as e: + # 避免崩溃后疯狂打印错误 + try: + print(f"\n[display_info 错误] {e}") + except: + pass + time.sleep(10) # 出错了也别刷屏 +def display_info2() -> None: + global start_display_time + time.sleep(5) + while True: + try: + sys.stdout.flush() + time.sleep(5) + if Path(sys.executable).name != 'pythonw.exe': + os.system(clear_command) + print(f"\r共监测{monitoring}个直播中", end=" | ") + print(f"同一时间访问网络的线程数: {max_request}", end=" | ") + print(f"是否开启代理录制: {'是' if use_proxy else '否'}", end=" | ") + if split_video_by_time: + print(f"录制分段开启: {split_time}秒", end=" | ") + else: + print("录制分段开启: 否", end=" | ") + if create_time_file: + print("是否生成时间文件: 是", end=" | ") + print(f"录制视频质量为: {video_record_quality}", end=" | ") + print(f"录制视频格式为: {video_save_type}", end=" | ") + print(f"目前瞬时错误数为: {error_count}", end=" | ") + now = time.strftime("%H:%M:%S", time.localtime()) + print(f"当前时间: {now}") + + if len(recording) == 0: + time.sleep(5) + if monitoring == 0: + print("\r没有正在监测和录制的直播") + else: + print(f"\r没有正在录制的直播 循环监测间隔时间:{delay_default}秒") + else: + now_time = datetime.datetime.now() + print("x" * 60) + no_repeat_recording = list(set(recording)) + print(f"正在录制{len(no_repeat_recording)}个直播: ") + for recording_live in no_repeat_recording: + rt, qa = recording_time_list[recording_live] + have_record_time = now_time - rt + print(f"{recording_live}[{qa}] 正在录制中 {str(have_record_time).split('.')[0]}") + + # print('\n本软件已运行:'+str(now_time - start_display_time).split('.')[0]) + print("x" * 60) + start_display_time = now_time + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + + +def update_file(file_path: str, old_str: str, new_str: str, start_str: str = None) -> str | None: + if old_str == new_str and start_str is None: + return old_str + with file_update_lock: + file_data = [] + with open(file_path, "r", encoding=text_encoding) as f: + try: + for text_line in f: + if old_str in text_line: + text_line = text_line.replace(old_str, new_str) + if start_str: + text_line = f'{start_str}{text_line}' + if text_line not in file_data: + file_data.append(text_line) + except RuntimeError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + if ini_URL_content: + with open(file_path, "w", encoding=text_encoding) as f2: + f2.write(ini_URL_content) + return old_str + if file_data: + with open(file_path, "w", encoding=text_encoding) as f: + f.write(''.join(file_data)) + return new_str + + +def delete_line(file_path: str, del_line: str, delete_all: bool = False) -> None: + with file_update_lock: + with open(file_path, 'r+', encoding=text_encoding) as f: + lines = f.readlines() + f.seek(0) + f.truncate() + skip_line = False + for txt_line in lines: + if del_line in txt_line: + if delete_all or not skip_line: + skip_line = True + continue + else: + skip_line = False + f.write(txt_line) + + +def get_startup_info(system_type: str): + if system_type == 'nt': + startup_info = subprocess.STARTUPINFO() + startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + else: + startup_info = None + return startup_info + + +def segment_video(converts_file_path: str, segment_save_file_path: str, segment_format: str, segment_time: str, + is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + ffmpeg_command = [ + "ffmpeg", + "-i", converts_file_path, + "-c:v", "copy", + "-c:a", "copy", + "-map", "0", + "-f", "segment", + "-segment_time", segment_time, + "-segment_format", segment_format, + "-reset_timestamps", "1", + "-movflags", "+frag_keyframe+empty_moov", + segment_save_file_path, + ] + _output = subprocess.check_output( + ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def converts_mp4(converts_file_path: str, is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + if converts_to_h264: + color_obj.print_colored("正在转码为MP4格式并重新编码为h264\n", color_obj.YELLOW) + ffmpeg_command = [ + "ffmpeg", "-i", converts_file_path, + "-c:v", "libx264", + "-preset", "veryfast", + "-crf", "23", + "-vf", "format=yuv420p", + "-c:a", "copy", + "-f", "mp4", converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4", + ] + else: + color_obj.print_colored("正在转码为MP4格式\n", color_obj.YELLOW) + ffmpeg_command = [ + "ffmpeg", "-i", converts_file_path, + "-c:v", "copy", + "-c:a", "copy", + "-f", "mp4", converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4", + ] + _output = subprocess.check_output( + ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def converts_m4a(converts_file_path: str, is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + _output = subprocess.check_output([ + "ffmpeg", "-i", converts_file_path, + "-n", "-vn", + "-c:a", "aac", "-bsf:a", "aac_adtstoasc", "-ab", "320k", + converts_file_path.rsplit('.', maxsplit=1)[0] + ".m4a", + ], stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def generate_subtitles(record_name: str, ass_filename: str, sub_format: str = 'srt') -> None: + index_time = 0 + today = datetime.datetime.now() + re_datatime = today.strftime('%Y-%m-%d %H:%M:%S') + + def transform_int_to_time(seconds: int) -> str: + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + return f"{h:02d}:{m:02d}:{s:02d}" + + while True: + index_time += 1 + txt = str(index_time) + "\n" + transform_int_to_time(index_time) + ',000 --> ' + transform_int_to_time( + index_time + 1) + ',000' + "\n" + str(re_datatime) + "\n\n" + + with open(f"{ass_filename}.{sub_format.lower()}", 'a', encoding=text_encoding) as f: + f.write(txt) + + if record_name not in recording: + return + time.sleep(1) + today = datetime.datetime.now() + re_datatime = today.strftime('%Y-%m-%d %H:%M:%S') + + +def adjust_max_request() -> None: + global max_request, error_count, pre_max_request, error_window + preset = max_request + + while True: + time.sleep(5) + with max_request_lock: + if error_window: + error_rate = sum(error_window) / len(error_window) + else: + error_rate = 0 + + if error_rate > error_threshold: + max_request = max(1, max_request - 1) + elif error_rate < error_threshold / 2 and max_request < preset: + max_request += 1 + else: + pass + + if pre_max_request != max_request: + pre_max_request = max_request + print(f"\r同一时间访问网络的线程数动态改为 {max_request}") + + error_window.append(error_count) + if len(error_window) > error_window_size: + error_window.pop(0) + error_count = 0 + + +def push_message(record_name: str, live_url: str, content: str) -> None: + msg_title = push_message_title.strip() or "直播间状态更新通知" + push_functions = { + '微信': lambda: xizhi(xizhi_api_url, msg_title, content), + '钉钉': lambda: dingtalk(dingtalk_api_url, content, dingtalk_phone_num, dingtalk_is_atall), + '邮箱': lambda: send_email( + email_host, login_email, email_password, sender_email, sender_name, + to_email, msg_title, content, smtp_port, open_smtp_ssl + ), + 'TG': lambda: tg_bot(tg_chat_id, tg_token, content), + 'BARK': lambda: bark( + bark_msg_api, title=msg_title, content=content, level=bark_msg_level, sound=bark_msg_ring + ), + 'NTFY': lambda: ntfy( + ntfy_api, title=msg_title, content=content, tags=ntfy_tags, action_url=live_url, email=ntfy_email + ), + 'PUSHPLUS': lambda: pushplus(pushplus_token, msg_title, content), + } + + for platform, func in push_functions.items(): + if platform in live_status_push.upper(): + try: + result = func() + print(f'提示信息:已经将[{record_name}]直播状态消息推送至你的{platform},' + f' 成功{len(result["success"])}, 失败{len(result["error"])}') + except Exception as e: + color_obj.print_colored(f"直播消息推送到{platform}失败: {e}", color_obj.RED) + + +def run_script(command: str) -> None: + try: + process = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=get_startup_info(os_type) + ) + stdout, stderr = process.communicate() + stdout_decoded = stdout.decode('utf-8') + stderr_decoded = stderr.decode('utf-8') + if stdout_decoded.strip(): + print(stdout_decoded) + if stderr_decoded.strip(): + print(stderr_decoded) + except PermissionError as e: + logger.error(e) + logger.error('脚本无执行权限!, 若是Linux环境, 请先执行:chmod +x your_script.sh 授予脚本可执行权限') + except OSError as e: + logger.error(e) + logger.error('Please add `#!/bin/bash` at the beginning of your bash script file.') + + +def clear_record_info(record_name: str, record_url: str) -> None: + global monitoring + recording.discard(record_name) + if record_url in url_comments and record_url in running_list: + running_list.remove(record_url) + monitoring -= 1 + color_obj.print_colored(f"[{record_name}]已经从录制列表中移除\n", color_obj.YELLOW) + + +def direct_download_stream(source_url: str, save_path: str, record_name: str, live_url: str, platform: str) -> bool: + try: + with open(save_path, 'wb') as f: + client = httpx.Client(timeout=None) + + headers = {} + header_params = get_record_headers(platform, live_url) + if header_params: + key, value = header_params.split(":", 1) + headers[key] = value + + with client.stream('GET', source_url, headers=headers, follow_redirects=True) as response: + if response.status_code != 200: + logger.error(f"请求直播流失败,状态码: {response.status_code}") + return False + + downloaded = 0 + chunk_size = 1024 * 16 + + for chunk in response.iter_bytes(chunk_size): + if live_url in url_comments or exit_recording: + color_obj.print_colored(f"[{record_name}]录制时已被注释或请求停止,下载中断", color_obj.YELLOW) + clear_record_info(record_name, live_url) + return False + + if chunk: + f.write(chunk) + downloaded += len(chunk) + print() + return True + except Exception as e: + logger.error(f"FLV下载错误: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + return False + + +def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, save_type: str, + script_command: str | None = None) -> bool: + save_file_path = ffmpeg_command[-1] + process = subprocess.Popen( + ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + + subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] + subs_thread_name = f'subs_{Path(subs_file_path).name}' + if create_time_file and not split_video_by_time and '音频' not in save_type: + create_var[subs_thread_name] = threading.Thread( + target=generate_subtitles, args=(record_name, subs_file_path) + ) + create_var[subs_thread_name].daemon = True + create_var[subs_thread_name].start() + + while process.poll() is None: + if record_url in url_comments or exit_recording: + color_obj.print_colored(f"[{record_name}]录制时已被注释,本条线程将会退出", color_obj.YELLOW) + clear_record_info(record_name, record_url) + # process.terminate() + if os.name == 'nt': + if process.stdin: + process.stdin.write(b'q') + process.stdin.close() + else: + process.send_signal(signal.SIGINT) + process.wait() + return True + time.sleep(1) + + return_code = process.returncode + stop_time = time.strftime('%Y-%m-%d %H:%M:%S') + if return_code == 0: + if converts_to_mp4 and save_type == 'TS': + if split_video_by_time: + file_paths = utils.get_file_paths(os.path.dirname(save_file_path)) + prefix = os.path.basename(save_file_path).rsplit('_', maxsplit=1)[0] + for path in file_paths: + if prefix in path: + threading.Thread(target=converts_mp4, args=(path, delete_origin_file)).start() + else: + threading.Thread(target=converts_mp4, args=(save_file_path, delete_origin_file)).start() + print(f"\n{record_name} {stop_time} 直播录制完成\n") + + if script_command: + logger.debug("开始执行脚本命令!") + if "python" in script_command: + params = [ + f'--record_name "{record_name}"', + f'--save_file_path "{save_file_path}"', + f'--save_type {save_type}', + f'--split_video_by_time {split_video_by_time}', + f'--converts_to_mp4 {converts_to_mp4}', + ] + else: + params = [ + f'"{record_name.split(" ", maxsplit=1)[-1]}"', + f'"{save_file_path}"', + save_type, + f'split_video_by_time:{split_video_by_time}', + f'converts_to_mp4:{converts_to_mp4}' + ] + script_command = script_command.strip() + ' ' + ' '.join(params) + run_script(script_command) + logger.debug("脚本命令执行结束!") + + else: + color_obj.print_colored(f"\n{record_name} {stop_time} 直播录制出错,返回码: {return_code}\n", color_obj.RED) + + recording.discard(record_name) + return False + + +def clean_name(input_text): + cleaned_name = re.sub(rstr, "_", input_text.strip()).strip('_') + cleaned_name = cleaned_name.replace("(", "(").replace(")", ")") + if clean_emoji: + cleaned_name = utils.remove_emojis(cleaned_name, '_').strip('_') + return cleaned_name or '空白昵称' + + +def get_quality_code(qn): + QUALITY_MAPPING = { + "原画": "OD", + "蓝光": "BD", + "超清": "UHD", + "高清": "HD", + "标清": "SD", + "流畅": "LD" + } + return QUALITY_MAPPING.get(qn) + + +def get_record_headers(platform, live_url): + live_domain = '/'.join(live_url.split('/')[0:3]) + record_headers = { + 'PandaTV': 'origin:https://www.pandalive.co.kr', + 'WinkTV': 'origin:https://www.winktv.co.kr', + 'PopkonTV': 'origin:https://www.popkontv.com', + 'FlexTV': 'origin:https://www.flextv.co.kr', + '千度热播': 'referer:https://qiandurebo.com', + '17Live': 'referer:https://17.live/en/live/6302408', + '浪Live': 'referer:https://www.lang.live', + 'shopee': f'origin:{live_domain}', + 'Blued直播': 'referer:https://app.blued.cn' + } + return record_headers.get(platform) + + +def is_flv_preferred_platform(link): + return any(i in link for i in ["douyin", "tiktok"]) + + +def select_source_url(link, stream_info): + if is_flv_preferred_platform(link): + codec = utils.get_query_params(stream_info.get('flv_url'), "codec") + if codec and codec[0] == 'h265': + logger.warning("FLV is not supported for h265 codec, use HLS source instead") + else: + return stream_info.get('flv_url') + + return stream_info.get('record_url') + + +def start_record(url_data: tuple, count_variable: int = -1) -> None: + global error_count + + while True: + try: + record_finished = False + run_once = False + start_pushed = False + new_record_url = '' + count_time = time.time() + retry = 0 + record_quality_zh, record_url, anchor_name = url_data + record_quality = get_quality_code(record_quality_zh) + proxy_address = proxy_addr + platform = '未知平台' + live_domain = '/'.join(record_url.split('/')[0:3]) + + if proxy_addr: + proxy_address = None + for platform in enable_proxy_platform_list: + if platform and platform.strip() in record_url: + proxy_address = proxy_addr + break + + if not proxy_address: + if extra_enable_proxy_platform_list: + for pt in extra_enable_proxy_platform_list: + if pt and pt.strip() in record_url: + proxy_address = proxy_addr_bak or None + + # print(f'\r代理地址:{proxy_address}') + # print(f'\r全局代理:{global_proxy}') + while True: + try: + port_info = [] + if record_url.find("douyin.com/") > -1: + platform = '抖音直播' + with semaphore: + if 'v.douyin.com' not in record_url and '/user/' not in record_url: + json_data = asyncio.run(spider.get_douyin_web_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=dy_cookie)) + else: + json_data = asyncio.run(spider.get_douyin_app_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=dy_cookie)) + port_info = asyncio.run( + stream.get_douyin_stream_url(json_data, record_quality, proxy_address)) + + elif record_url.find("https://www.tiktok.com/") > -1: + platform = 'TikTok直播' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_tiktok_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=tiktok_cookie)) + port_info = asyncio.run( + stream.get_tiktok_stream_url(json_data, record_quality, proxy_address)) + else: + logger.error("错误信息: 网络异常,请检查网络是否能正常访问TikTok平台") + + elif record_url.find("https://live.kuaishou.com/") > -1: + platform = '快手直播' + with semaphore: + json_data = asyncio.run(spider.get_kuaishou_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=ks_cookie)) + port_info = asyncio.run(stream.get_kuaishou_stream_url(json_data, record_quality)) + + elif record_url.find("https://www.huya.com/") > -1: + platform = '虎牙直播' + with semaphore: + if record_quality not in ['OD', 'BD', 'UHD']: + json_data = asyncio.run(spider.get_huya_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=hy_cookie)) + port_info = asyncio.run(stream.get_huya_stream_url(json_data, record_quality)) + else: + port_info = asyncio.run(spider.get_huya_app_stream_url( + url=record_url, + proxy_addr=proxy_address, + cookies=hy_cookie + )) + + elif record_url.find("https://www.douyu.com/") > -1: + platform = '斗鱼直播' + with semaphore: + json_data = asyncio.run(spider.get_douyu_info_data( + url=record_url, proxy_addr=proxy_address, cookies=douyu_cookie)) + port_info = asyncio.run(stream.get_douyu_stream_url( + json_data, video_quality=record_quality, cookies=douyu_cookie, proxy_addr=proxy_address + )) + + elif record_url.find("https://www.yy.com/") > -1: + platform = 'YY直播' + with semaphore: + json_data = asyncio.run(spider.get_yy_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=yy_cookie)) + port_info = asyncio.run(stream.get_yy_stream_url(json_data)) + + elif record_url.find("https://live.bilibili.com/") > -1: + platform = 'B站直播' + with semaphore: + json_data = asyncio.run(spider.get_bilibili_room_info( + url=record_url, proxy_addr=proxy_address, cookies=bili_cookie)) + port_info = asyncio.run(stream.get_bilibili_stream_url( + json_data, video_quality=record_quality, cookies=bili_cookie, proxy_addr=proxy_address)) + + elif record_url.find("http://xhslink.com/") > -1 or \ + record_url.find("https://www.xiaohongshu.com/") > -1: + platform = '小红书直播' + with semaphore: + port_info = asyncio.run(spider.get_xhs_stream_url( + record_url, proxy_addr=proxy_address, cookies=xhs_cookie)) + retry += 1 + + elif record_url.find("www.bigo.tv/") > -1 or record_url.find("slink.bigovideo.tv/") > -1: + platform = 'Bigo直播' + with semaphore: + port_info = asyncio.run(spider.get_bigo_stream_url( + record_url, proxy_addr=proxy_address, cookies=bigo_cookie)) + + elif record_url.find("https://app.blued.cn/") > -1: + platform = 'Blued直播' + with semaphore: + port_info = asyncio.run(spider.get_blued_stream_url( + record_url, proxy_addr=proxy_address, cookies=blued_cookie)) + + elif record_url.find("sooplive.co.kr/") > -1 or record_url.find("sooplive.com/") > -1: + platform = 'SOOP' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_sooplive_stream_data( + url=record_url, proxy_addr=proxy_address, + cookies=sooplive_cookie, + username=sooplive_username, + password=sooplive_password + )) + if json_data and json_data.get('new_cookies'): + utils.update_config( + config_file, 'Cookie', 'sooplive_cookie', json_data['new_cookies'] + ) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问SOOP平台") + + elif record_url.find("cc.163.com/") > -1: + platform = '网易CC直播' + with semaphore: + json_data = asyncio.run(spider.get_netease_stream_data( + url=record_url, cookies=netease_cookie)) + port_info = asyncio.run(stream.get_netease_stream_url(json_data, record_quality)) + + elif record_url.find("qiandurebo.com/") > -1: + platform = '千度热播' + with semaphore: + port_info = asyncio.run(spider.get_qiandurebo_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=qiandurebo_cookie)) + + elif record_url.find("www.pandalive.co.kr/") > -1: + platform = 'PandaTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_pandatv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=pandatv_cookie + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PandaTV直播平台") + + elif record_url.find("fm.missevan.com/") > -1: + platform = '猫耳FM直播' + with semaphore: + port_info = asyncio.run(spider.get_maoerfm_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=maoerfm_cookie)) + + elif record_url.find("www.winktv.co.kr/") > -1: + platform = 'WinkTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_winktv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=winktv_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问WinkTV直播平台") + + elif record_url.find("www.flextv.co.kr/") > -1 or record_url.find("www.ttinglive.com/") > -1: + platform = 'FlexTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_flextv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=flextv_cookie, + username=flextv_username, + password=flextv_password + )) + if json_data and json_data.get('new_cookies'): + utils.update_config( + config_file, 'Cookie', 'flextv_cookie', json_data['new_cookies'] + ) + if 'play_url_list' in json_data: + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + port_info = json_data + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问FlexTV直播平台") + + elif record_url.find("look.163.com/") > -1: + platform = 'Look直播' + with semaphore: + port_info = asyncio.run(spider.get_looklive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=look_cookie + )) + + elif record_url.find("www.popkontv.com/") > -1: + platform = 'PopkonTV' + with semaphore: + if global_proxy or proxy_address: + port_info = asyncio.run(spider.get_popkontv_stream_url( + url=record_url, + proxy_addr=proxy_address, + access_token=popkontv_access_token, + username=popkontv_username, + password=popkontv_password, + partner_code=popkontv_partner_code + )) + if port_info and port_info.get('new_token'): + utils.update_config( + file_path=config_file, section='Authorization', key='popkontv_token', + new_value=port_info['new_token'] + ) + + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PopkonTV直播平台") + + elif record_url.find("twitcasting.tv/") > -1: + platform = 'TwitCasting' + with semaphore: + json_data = asyncio.run(spider.get_twitcasting_stream_url( + url=record_url, + proxy_addr=proxy_address, + cookies=twitcasting_cookie, + account_type=twitcasting_account_type, + username=twitcasting_username, + password=twitcasting_password + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=False)) + + if port_info and port_info.get('new_cookies'): + utils.update_config( + file_path=config_file, section='Cookie', key='twitcasting_cookie', + new_value=port_info['new_cookies'] + ) + + elif record_url.find("live.baidu.com/") > -1: + platform = '百度直播' + with semaphore: + json_data = asyncio.run(spider.get_baidu_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=baidu_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality)) + + elif record_url.find("weibo.com/") > -1: + platform = '微博直播' + with semaphore: + json_data = asyncio.run(spider.get_weibo_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=weibo_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, hls_extra_key='m3u8_url')) + + elif record_url.find("kugou.com/") > -1: + platform = '酷狗直播' + with semaphore: + port_info = asyncio.run(spider.get_kugou_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=kugou_cookie)) + + elif record_url.find("www.twitch.tv/") > -1: + platform = 'TwitchTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_twitchtv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=twitch_cookie + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问TwitchTV直播平台") + + elif record_url.find("www.liveme.com/") > -1: + if global_proxy or proxy_address: + platform = 'LiveMe' + with semaphore: + port_info = asyncio.run(spider.get_liveme_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=liveme_cookie)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问LiveMe直播平台") + + elif record_url.find("www.huajiao.com/") > -1: + platform = '花椒直播' + with semaphore: + port_info = asyncio.run(spider.get_huajiao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=huajiao_cookie)) + + elif record_url.find("7u66.com/") > -1: + platform = '流星直播' + with semaphore: + port_info = asyncio.run(spider.get_liuxing_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=liuxing_cookie)) + + elif record_url.find("showroom-live.com/") > -1: + platform = 'ShowRoom' + with semaphore: + json_data = asyncio.run(spider.get_showroom_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=showroom_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("live.acfun.cn/") > -1 or record_url.find("m.acfun.cn/") > -1: + platform = 'Acfun' + with semaphore: + json_data = asyncio.run(spider.get_acfun_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=acfun_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, url_type='flv', flv_extra_key='url')) + + elif record_url.find("live.tlclw.com/") > -1: + platform = '畅聊直播' + with semaphore: + port_info = asyncio.run(spider.get_changliao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=changliao_cookie)) + + elif record_url.find("ybw1666.com/") > -1: + platform = '音播直播' + with semaphore: + port_info = asyncio.run(spider.get_yinbo_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yinbo_cookie)) + + elif record_url.find("www.inke.cn/") > -1: + platform = '映客直播' + with semaphore: + port_info = asyncio.run(spider.get_yingke_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yingke_cookie)) + + elif record_url.find("www.zhihu.com/") > -1: + platform = '知乎直播' + with semaphore: + port_info = asyncio.run(spider.get_zhihu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=zhihu_cookie)) + + elif record_url.find("chzzk.naver.com/") > -1: + platform = 'CHZZK' + with semaphore: + json_data = asyncio.run(spider.get_chzzk_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=chzzk_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("www.haixiutv.com/") > -1: + platform = '嗨秀直播' + with semaphore: + port_info = asyncio.run(spider.get_haixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=haixiu_cookie)) + + elif record_url.find("vvxqiu.com/") > -1: + platform = 'VV星球' + with semaphore: + port_info = asyncio.run(spider.get_vvxqiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=vvxqiu_cookie)) + + elif record_url.find("17.live/") > -1: + platform = '17Live' + with semaphore: + port_info = asyncio.run(spider.get_17live_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yiqilive_cookie)) + + elif record_url.find("www.lang.live/") > -1: + platform = '浪Live' + with semaphore: + port_info = asyncio.run(spider.get_langlive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=langlive_cookie)) + + elif record_url.find("m.pp.weimipopo.com/") > -1: + platform = '漂漂直播' + with semaphore: + port_info = asyncio.run(spider.get_pplive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=pplive_cookie)) + + elif record_url.find(".6.cn/") > -1: + platform = '六间房直播' + with semaphore: + port_info = asyncio.run(spider.get_6room_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=six_room_cookie)) + + elif record_url.find("lehaitv.com/") > -1: + platform = '乐嗨直播' + with semaphore: + port_info = asyncio.run(spider.get_haixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=lehaitv_cookie)) + + elif record_url.find("h.catshow168.com/") > -1: + platform = '花猫直播' + with semaphore: + port_info = asyncio.run(spider.get_pplive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=huamao_cookie)) + + elif record_url.find("live.shopee") > -1 or record_url.find("shp.ee/") > -1: + platform = 'shopee' + with semaphore: + port_info = asyncio.run(spider.get_shopee_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=shopee_cookie)) + if port_info.get('uid'): + new_record_url = record_url.split('?')[0] + '?' + str(port_info['uid']) + + elif record_url.find("www.youtube.com/") > -1 or record_url.find("youtu.be/") > -1: + platform = 'Youtube' + with semaphore: + json_data = asyncio.run(spider.get_youtube_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=youtube_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("tb.cn") > -1: + platform = '淘宝直播' + with semaphore: + json_data = asyncio.run(spider.get_taobao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=taobao_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, + url_type='all', hls_extra_key='hlsUrl', flv_extra_key='flvUrl' + )) + + elif record_url.find("3.cn") > -1 or record_url.find("m.jd.com") > -1: + platform = '京东直播' + with semaphore: + port_info = asyncio.run(spider.get_jd_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=jd_cookie)) + + elif record_url.find("faceit.com/") > -1: + platform = 'faceit' + with semaphore: + if global_proxy or proxy_address: + with semaphore: + json_data = asyncio.run(spider.get_faceit_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=faceit_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问faceit直播平台") + + elif record_url.find("www.miguvideo.com") > -1 or record_url.find("m.miguvideo.com") > -1: + platform = '咪咕直播' + with semaphore: + port_info = asyncio.run(spider.get_migu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=migu_cookie)) + + elif record_url.find("show.lailianjie.com") > -1: + platform = '连接直播' + with semaphore: + port_info = asyncio.run(spider.get_lianjie_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=lianjie_cookie)) + + elif record_url.find("www.imkktv.com") > -1: + platform = '来秀直播' + with semaphore: + port_info = asyncio.run(spider.get_laixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=laixiu_cookie)) + + elif record_url.find("www.picarto.tv") > -1: + platform = 'Picarto' + with semaphore: + port_info = asyncio.run(spider.get_picarto_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=picarto_cookie)) + + elif record_url.find(".m3u8") > -1 or record_url.find(".flv") > -1: + platform = '自定义录制直播' + port_info = { + "anchor_name": platform + '_' + str(uuid.uuid4())[:8], + "is_live": True, + "record_url": record_url, + } + if '.flv' in record_url: + port_info['flv_url'] = record_url + else: + port_info['m3u8_url'] = record_url + + else: + logger.error(f'{record_url} {platform}直播地址') + return + + if anchor_name: + if '主播:' in anchor_name: + anchor_split: list = anchor_name.split('主播:') + if len(anchor_split) > 1 and anchor_split[1].strip(): + anchor_name = anchor_split[1].strip() + else: + anchor_name = port_info.get("anchor_name", '') + else: + anchor_name = port_info.get("anchor_name", '') + + if not port_info.get("anchor_name", ''): + print(f'序号{count_variable} 网址内容获取失败,进行重试中...获取失败的地址是:{url_data}') + with max_request_lock: + error_count += 1 + error_window.append(1) + else: + anchor_name = clean_name(anchor_name) + record_name = f'序号{count_variable} {anchor_name}' + + if record_url in url_comments: + print(f"[{anchor_name}]已被注释,本条线程将会退出") + clear_record_info(record_name, record_url) + return + + if not url_data[-1] and run_once is False: + if new_record_url: + need_update_line_list.append( + f'{record_url}|{new_record_url},主播: {anchor_name.strip()}') + not_record_list.append(new_record_url) + else: + need_update_line_list.append(f'{record_url}|{record_url},主播: {anchor_name.strip()}') + run_once = True + + push_at = datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S') + if port_info['is_live'] is False: + print(f"\r{record_name} 等待直播... ") + # 直接退出线程eric + # continue # 继续循环 + return # 线程彻底退出 + + if start_pushed: + if over_show_push: + push_content = "直播间状态更新:[直播间名称] 直播已结束!时间:[时间]" + if over_push_message_text: + push_content = over_push_message_text + + push_content = (push_content.replace('[直播间名称]', record_name). + replace('[时间]', push_at)) + threading.Thread( + target=push_message, + args=(record_name, record_url, push_content.replace(r'\n', '\n')), + daemon=True + ).start() + start_pushed = False + + else: + content = f"\r{record_name} 正在直播中..." + print(content) + # 检查是否已有直播在录制 + if len(recording) > 0 : + # 如果已有直播在录制,跳过这次录制(主播在线) + # return + continue #如果在循环中 + + if live_status_push and not start_pushed: + if begin_show_push: + push_content = "直播间状态更新:[直播间名称] 正在直播中,时间:[时间]" + if begin_push_message_text: + push_content = begin_push_message_text + + push_content = (push_content.replace('[直播间名称]', record_name). + replace('[时间]', push_at)) + threading.Thread( + target=push_message, + args=(record_name, record_url, push_content.replace(r'\n', '\n')), + daemon=True + ).start() + start_pushed = True + + if disable_record: + time.sleep(push_check_seconds) + continue + + real_url = select_source_url(record_url, port_info) + full_path = f'{default_path}/{platform}' + if real_url: + now = datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S") + live_title = port_info.get('title') + title_in_name = '' + if live_title: + live_title = clean_name(live_title) + title_in_name = live_title + '_' if filename_by_title else '' + + try: + if len(video_save_path) > 0: + if not video_save_path.endswith(('/', '\\')): + full_path = f'{video_save_path}/{platform}' + else: + full_path = f'{video_save_path}{platform}' + + full_path = full_path.replace("\\", '/') + if folder_by_author: + full_path = f'{full_path}/{anchor_name}' + if folder_by_time: + full_path = f'{full_path}/{now[:10]}' + if folder_by_title and port_info.get('title'): + if folder_by_time: + full_path = f'{full_path}/{live_title}_{anchor_name}' + else: + full_path = f'{full_path}/{now[:10]}_{live_title}' + if not os.path.exists(full_path): + os.makedirs(full_path) + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + + if platform != '自定义录制直播': + if enable_https_recording and real_url.startswith("http://"): + real_url = real_url.replace("http://", "https://") + + http_record_list = ['shopee', "migu"] + if platform in http_record_list: + real_url = real_url.replace("https://", "http://") + + user_agent = ("Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (" + "KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile " + "Safari/537.36") + + rw_timeout = "15000000" + analyzeduration = "20000000" + probesize = "10000000" + bufsize = "8000k" + max_muxing_queue_size = "1024" + for pt_host in overseas_platform_host: + if pt_host in record_url: + rw_timeout = "50000000" + analyzeduration = "40000000" + probesize = "20000000" + bufsize = "15000k" + max_muxing_queue_size = "2048" + break + + ffmpeg_command = [ + 'ffmpeg', "-y", + "-v", "verbose", + "-rw_timeout", rw_timeout, + "-loglevel", "error", + "-hide_banner", + "-user_agent", user_agent, + "-protocol_whitelist", "rtmp,crypto,file,http,https,tcp,tls,udp,rtp,httpproxy", + "-thread_queue_size", "1024", + "-analyzeduration", analyzeduration, + "-probesize", probesize, + "-fflags", "+discardcorrupt", + "-re", "-i", real_url, + "-bufsize", bufsize, + "-sn", "-dn", + "-reconnect_delay_max", "60", + "-reconnect_streamed", "-reconnect_at_eof", + "-max_muxing_queue_size", max_muxing_queue_size, + "-correct_ts_overflow", "1", + "-avoid_negative_ts", "1" + ] + + headers = get_record_headers(platform, record_url) + if headers: + ffmpeg_command.insert(11, "-headers") + ffmpeg_command.insert(12, headers) + + _ff_proxy = (proxy_address or "").strip() or ( + os.environ.get("STREAM_HTTP_PROXY") or "" + ).strip() or None + if _ff_proxy: + ffmpeg_command.insert(1, "-http_proxy") + ffmpeg_command.insert(2, _ff_proxy) + + recording.add(record_name) + start_record_time = datetime.datetime.now() + recording_time_list[record_name] = [start_record_time, record_quality_zh] + rec_info = f"\r{anchor_name} 准备开始录制视频: {full_path}" + if show_url: + re_plat = ('WinkTV', 'PandaTV', 'ShowRoom', 'CHZZK', 'Youtube') + if platform in re_plat: + logger.info( + f"{platform} | {anchor_name} | 直播源地址: {port_info.get('m3u8_url')}") + else: + logger.info( + f"{platform} | {anchor_name} | 直播源地址: {real_url}") + + only_flv_record = False + only_flv_platform_list = ['shopee', '花椒直播'] + if platform in only_flv_platform_list: + logger.debug(f"提示: {platform} 将强制使用FLV格式录制") + only_flv_record = True + + only_audio_record = False + only_audio_platform_list = ['猫耳FM直播', 'Look直播'] + if platform in only_audio_platform_list: + only_audio_record = True + + record_save_type = video_save_type + + if is_flv_preferred_platform(record_url) and port_info.get('flv_url'): + codec = utils.get_query_params(port_info['flv_url'], "codec") + if codec and codec[0] == 'h265': + logger.warning("FLV is not supported for h265 codec, use TS format instead") + record_save_type = "TS" + + if only_audio_record or any(i in record_save_type for i in ['MP3', 'M4A']): + try: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + extension = "mp3" if "m4a" not in record_save_type.lower() else "m4a" + name_format = "_%03d" if split_video_by_time else "" + save_file_path = (f"{full_path}/{anchor_name}_{title_in_name}{now}" + f"{name_format}.{extension}") + + if split_video_by_time: + print(f'\r{anchor_name} 准备开始录制音频: {save_file_path}') + + if "MP3" in record_save_type: + command = [ + "-map", "0:a", + "-c:a", "libmp3lame", + "-ab", "320k", + "-f", "segment", + "-segment_time", split_time, + "-reset_timestamps", "1", + save_file_path, + ] + else: + command = [ + "-map", "0:a", + "-c:a", "aac", + "-bsf:a", "aac_adtstoasc", + "-ab", "320k", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", 'mpegts', + "-reset_timestamps", "1", + save_file_path, + ] + + else: + if "MP3" in record_save_type: + command = [ + "-map", "0:a", + "-c:a", "libmp3lame", + "-ab", "320k", + save_file_path, + ] + + else: + command = [ + "-map", "0:a", + "-c:a", "aac", + "-bsf:a", "aac_adtstoasc", + "-ab", "320k", + "-movflags", "+faststart", + save_file_path, + ] + + ffmpeg_command.extend(command) + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + + + # if only_flv_record: + if False: + + logger.info(f"Use Direct Downloader to Download FLV Stream: {record_url}") + filename = anchor_name + f'_{title_in_name}' + now + '.flv' + save_file_path = f'{full_path}/{filename}' + print(f'{rec_info}/{filename}') + + subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] + subs_thread_name = f'subs_{Path(subs_file_path).name}' + if create_time_file: + create_var[subs_thread_name] = threading.Thread( + target=generate_subtitles, args=(record_name, subs_file_path) + ) + create_var[subs_thread_name].daemon = True + create_var[subs_thread_name].start() + + try: + flv_url = port_info.get('flv_url') + if flv_url: + recording.add(record_name) + start_record_time = datetime.datetime.now() + recording_time_list[record_name] = [start_record_time, record_quality_zh] + + download_success = direct_download_stream( + flv_url, save_file_path, record_name, record_url, platform + ) + + if download_success: + record_finished = True + print( + f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制完成\n") + + recording.discard(record_name) + else: + logger.debug("未找到FLV直播流,跳过录制") + except Exception as e: + clear_record_info(record_name, record_url) + color_obj.print_colored( + f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制出错,请检查网络\n", + color_obj.RED) + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + elif record_save_type == "FLV": + filename = anchor_name + f'_{title_in_name}' + now + ".flv" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.flv" + command = [ + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-bsf:a", "aac_adtstoasc", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "flv", + "-reset_timestamps", "1", + save_file_path + ] + + else: + command = [ + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-bsf:a", "aac_adtstoasc", + "-f", "flv", + "{path}".format(path=save_file_path), + ] + ffmpeg_command.extend(command) + + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + try: + if converts_to_mp4: + seg_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mp4" + if split_video_by_time: + segment_video( + save_file_path, seg_file_path, + segment_format='mp4', segment_time=split_time, + is_original_delete=delete_origin_file + ) + else: + threading.Thread( + target=converts_mp4, + args=(save_file_path, delete_origin_file) + ).start() + + else: + seg_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.flv" + if split_video_by_time: + segment_video( + save_file_path, seg_file_path, + segment_format='flv', segment_time=split_time, + is_original_delete=delete_origin_file + ) + except Exception as e: + logger.error(f"转码失败: {e} ") + + elif record_save_type == "MKV": + filename = anchor_name + f'_{title_in_name}' + now + ".mkv" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mkv" + command = [ + "-flags", "global_header", + "-c:v", "copy", + "-c:a", "aac", + "-map", "0", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "matroska", + "-reset_timestamps", "1", + save_file_path, + ] + + else: + command = [ + "-flags", "global_header", + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-f", "matroska", + "{path}".format(path=save_file_path), + ] + ffmpeg_command.extend(command) + + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + elif record_save_type == "MP4": + filename = anchor_name + f'_{title_in_name}' + now + ".mp4" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mp4" + command = [ + "-c:v", "copy", + "-c:a", "aac", + "-map", "0", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "mp4", + "-reset_timestamps", "1", + "-movflags", "+frag_keyframe+empty_moov", + save_file_path, + ] + + else: + command = [ + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-f", "mp4", + save_file_path, + ] + + ffmpeg_command.extend(command) + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + else: + import sys + sys.path.insert(0, './dist') + from douyin_srs_ffplay import start_douyin_srs_ffplay + # 调用 + start_douyin_srs_ffplay( + title=title_in_name, + anchor_name=anchor_name, + real_url=real_url, + rec_info=rec_info, + record_name=record_name, + recording=recording, + recording_time_list=recording_time_list, + running_list=running_list, + monitoring=monitoring, + clear_record_info=clear_record_info, + color_obj=color_obj + ) + count_time = time.time() + + + + + + + + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + num = random.randint(-5, 5) + delay_default + if num < 0: + num = 0 + x = num + + if error_count > 20: + x = x + 60 + # x += min(error_count * 2, 30) + color_obj.print_colored("\r瞬时错误太多,延迟最多60s", color_obj.YELLOW) + + # 这里是.如果录制结束后,循环时间会暂时变成30s后检测一遍. 这样一定程度上防止主播卡顿造成少录 + # 当30秒过后检测一遍后. 会回归正常设置的循环秒数 + if record_finished: + count_time_end = time.time() - count_time + if count_time_end < 60: + x = 30 + record_finished = False + + else: + x = num + + # 这里是正常循环 + while x: + x = x - 1 + if loop_time: + print(f'\r{anchor_name}循环等待{x}秒 ', end="") + time.sleep(1) + if loop_time: + print('\r检测直播间中...', end="") + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + time.sleep(2) + + +def backup_file(file_path: str, backup_dir_path: str, limit_counts: int = 6) -> None: + + try: + if not os.path.exists(backup_dir_path): + os.makedirs(backup_dir_path) + + timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + backup_file_name = os.path.basename(file_path) + '_' + timestamp + backup_file_path = os.path.join(backup_dir_path, backup_file_name).replace("\\", "/") + shutil.copy2(file_path, backup_file_path) + + files = os.listdir(backup_dir_path) + _files = [f for f in files if f.startswith(os.path.basename(file_path))] + _files.sort(key=lambda x: os.path.getmtime(os.path.join(backup_dir_path, x))) + + while len(_files) > limit_counts: + oldest_file = _files[0] + os.remove(os.path.join(backup_dir_path, oldest_file)) + _files = _files[1:] + + except Exception as e: + logger.error(f'\r备份配置文件 {file_path} 失败:{str(e)}') + + +def backup_file_start() -> None: + config_md5 = '' + url_config_md5 = '' + + while True: + try: + if os.path.exists(config_file): + new_config_md5 = utils.check_md5(config_file) + if new_config_md5 != config_md5: + backup_file(config_file, backup_dir) + config_md5 = new_config_md5 + + if os.path.exists(url_config_file): + new_url_config_md5 = utils.check_md5(url_config_file) + if new_url_config_md5 != url_config_md5: + backup_file(url_config_file, backup_dir) + url_config_md5 = new_url_config_md5 + time.sleep(600) + except Exception as e: + logger.error(f"备份配置文件失败, 错误信息: {e}") + + +def check_ffmpeg_existence() -> bool: + try: + result = subprocess.run(['ffmpeg', '-version'], check=True, capture_output=True, text=True) + if result.returncode == 0: + lines = result.stdout.splitlines() + version_line = lines[0] + built_line = lines[1] + print(version_line) + print(built_line) + except subprocess.CalledProcessError as e: + logger.error(e) + except FileNotFoundError: + pass + finally: + if check_ffmpeg(): + time.sleep(1) + return True + return False + + +# --------------------------初始化程序------------------------------------- +print("-----------------------------------------------------") +print("| DouyinLiveRecorder |") +print("-----------------------------------------------------") + +print(f"版本号: {version}") +print("GitHub: https://github.com/ihmily/DouyinLiveRecorder") +print(f'支持平台: {platforms}') +print('.....................................................') +if not check_ffmpeg_existence(): + logger.error("缺少ffmpeg无法进行录制,程序退出") + sys.exit(1) +os.makedirs(os.path.dirname(config_file), exist_ok=True) +t3 = threading.Thread(target=backup_file_start, args=(), daemon=True) +t3.start() +utils.remove_duplicate_lines(url_config_file) + + +def read_config_value(config_parser: configparser.RawConfigParser, section: str, option: str, default_value: Any) \ + -> Any: + try: + + config_parser.read(config_file, encoding=text_encoding) + if '录制设置' not in config_parser.sections(): + config_parser.add_section('录制设置') + if '推送配置' not in config_parser.sections(): + config_parser.add_section('推送配置') + if 'Cookie' not in config_parser.sections(): + config_parser.add_section('Cookie') + if 'Authorization' not in config_parser.sections(): + config_parser.add_section('Authorization') + if '账号密码' not in config_parser.sections(): + config_parser.add_section('账号密码') + return config_parser.get(section, option) + except (configparser.NoSectionError, configparser.NoOptionError): + config_parser.set(section, option, str(default_value)) + with open(config_file, 'w', encoding=text_encoding) as f: + config_parser.write(f) + return default_value + + +options = {"是": True, "否": False} +config = configparser.RawConfigParser() +language = read_config_value(config, '录制设置', 'language(zh_cn/en)', "zh_cn") +skip_proxy_check = options.get(read_config_value(config, '录制设置', '是否跳过代理检测(是/否)', "否"), False) +if language and 'en' not in language.lower(): + from i18n import translated_print + + builtins.print = translated_print + +try: + if skip_proxy_check: + global_proxy = True + else: + print('系统代理检测中,请耐心等待...') + response_g = urllib.request.urlopen("https://www.google.com/", timeout=15) + global_proxy = True + print('\r全局/规则网络代理已开启√') + pd = ProxyDetector() + if pd.is_proxy_enabled(): + proxy_info = pd.get_proxy_info() + print("System Proxy: http://{}:{}".format(proxy_info.ip, proxy_info.port)) +except HTTPError as err: + print(f"HTTP error occurred: {err.code} - {err.reason}") +except URLError: + color_obj.print_colored("INFO:未检测到全局/规则网络代理,请检查代理配置(若无需录制海外直播请忽略此条提示)", + color_obj.YELLOW) +except Exception as err: + print("An unexpected error occurred:", err) + +while True: + + try: + if not os.path.isfile(config_file): + with open(config_file, 'w', encoding=text_encoding) as file: + pass + + ini_URL_content = '' + if os.path.isfile(url_config_file): + with open(url_config_file, 'r', encoding=text_encoding) as file: + ini_URL_content = file.read().strip() + + if not ini_URL_content.strip(): + input_url = input('请输入要录制的主播直播间网址(尽量使用PC网页端的直播间地址):\n') + with open(url_config_file, 'w', encoding=text_encoding) as file: + file.write(input_url) + except OSError as err: + logger.error(f"发生 I/O 错误: {err}") + + video_save_path = read_config_value(config, '录制设置', '直播保存路径(不填则默认)', "") + folder_by_author = options.get(read_config_value(config, '录制设置', '保存文件夹是否以作者区分', "是"), False) + folder_by_time = options.get(read_config_value(config, '录制设置', '保存文件夹是否以时间区分', "否"), False) + folder_by_title = options.get(read_config_value(config, '录制设置', '保存文件夹是否以标题区分', "否"), False) + filename_by_title = options.get(read_config_value(config, '录制设置', '保存文件名是否包含标题', "否"), False) + clean_emoji = options.get(read_config_value(config, '录制设置', '是否去除名称中的表情符号', "是"), True) + video_save_type = read_config_value(config, '录制设置', '视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频', "ts") + video_record_quality = read_config_value(config, '录制设置', '原画|超清|高清|标清|流畅', "原画") + use_proxy = options.get(read_config_value(config, '录制设置', '是否使用代理ip(是/否)', "是"), False) + proxy_addr_bak = read_config_value(config, '录制设置', '代理地址', "") + proxy_addr = None if not use_proxy else proxy_addr_bak + max_request = int(read_config_value(config, '录制设置', '同一时间访问网络的线程数', 3)) + semaphore = threading.Semaphore(max_request) + delay_default = int(read_config_value(config, '录制设置', '循环时间(秒)', 120)) + local_delay_default = int(read_config_value(config, '录制设置', '排队读取网址时间(秒)', 0)) + loop_time = options.get(read_config_value(config, '录制设置', '是否显示循环秒数', "否"), False) + show_url = options.get(read_config_value(config, '录制设置', '是否显示直播源地址', "否"), False) + split_video_by_time = options.get(read_config_value(config, '录制设置', '分段录制是否开启', "否"), False) + enable_https_recording = options.get(read_config_value(config, '录制设置', '是否强制启用https录制', "否"), False) + disk_space_limit = float(read_config_value(config, '录制设置', '录制空间剩余阈值(gb)', 1.0)) + split_time = str(read_config_value(config, '录制设置', '视频分段时间(秒)', 1800)) + converts_to_mp4 = options.get(read_config_value(config, '录制设置', '录制完成后自动转为mp4格式', "否"), False) + converts_to_h264 = options.get(read_config_value(config, '录制设置', 'mp4格式重新编码为h264', "否"), False) + delete_origin_file = options.get(read_config_value(config, '录制设置', '追加格式后删除原文件', "否"), False) + create_time_file = options.get(read_config_value(config, '录制设置', '生成时间字幕文件', "否"), False) + is_run_script = options.get(read_config_value(config, '录制设置', '是否录制完成后执行自定义脚本', "否"), False) + custom_script = read_config_value(config, '录制设置', '自定义脚本执行命令', "") if is_run_script else None + enable_proxy_platform = read_config_value( + config, '录制设置', '使用代理录制的平台(逗号分隔)', + 'tiktok, soop, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu, faceit' + ) + enable_proxy_platform_list = enable_proxy_platform.replace(',', ',').split(',') if enable_proxy_platform else None + extra_enable_proxy = read_config_value(config, '录制设置', '额外使用代理录制的平台(逗号分隔)', '') + extra_enable_proxy_platform_list = extra_enable_proxy.replace(',', ',').split(',') if extra_enable_proxy else None + live_status_push = read_config_value(config, '推送配置', '直播状态推送渠道', "") + dingtalk_api_url = read_config_value(config, '推送配置', '钉钉推送接口链接', "") + xizhi_api_url = read_config_value(config, '推送配置', '微信推送接口链接', "") + bark_msg_api = read_config_value(config, '推送配置', 'bark推送接口链接', "") + bark_msg_level = read_config_value(config, '推送配置', 'bark推送中断级别', "active") + bark_msg_ring = read_config_value(config, '推送配置', 'bark推送铃声', "bell") + dingtalk_phone_num = read_config_value(config, '推送配置', '钉钉通知@对象(填手机号)', "") + dingtalk_is_atall = options.get(read_config_value(config, '推送配置', '钉钉通知@全体(是/否)', "否"), False) + tg_token = read_config_value(config, '推送配置', 'tgapi令牌', "") + tg_chat_id = read_config_value(config, '推送配置', 'tg聊天id(个人或者群组id)', "") + email_host = read_config_value(config, '推送配置', 'SMTP邮件服务器', "") + open_smtp_ssl = options.get(read_config_value(config, '推送配置', '是否使用SMTP服务SSL加密(是/否)', "是"), True) + smtp_port = read_config_value(config, '推送配置', 'SMTP邮件服务器端口', "") + login_email = read_config_value(config, '推送配置', '邮箱登录账号', "") + email_password = read_config_value(config, '推送配置', '发件人密码(授权码)', "") + sender_email = read_config_value(config, '推送配置', '发件人邮箱', "") + sender_name = read_config_value(config, '推送配置', '发件人显示昵称', "") + to_email = read_config_value(config, '推送配置', '收件人邮箱', "") + ntfy_api = read_config_value(config, '推送配置', 'ntfy推送地址', "") + ntfy_tags = read_config_value(config, '推送配置', 'ntfy推送标签', "tada") + ntfy_email = read_config_value(config, '推送配置', 'ntfy推送邮箱', "") + pushplus_token = read_config_value(config, '推送配置', 'pushplus推送token', "") + push_message_title = read_config_value(config, '推送配置', '自定义推送标题', "直播间状态更新通知") + begin_push_message_text = read_config_value(config, '推送配置', '自定义开播推送内容', "") + over_push_message_text = read_config_value(config, '推送配置', '自定义关播推送内容', "") + disable_record = options.get(read_config_value(config, '推送配置', '只推送通知不录制(是/否)', "否"), False) + push_check_seconds = int(read_config_value(config, '推送配置', '直播推送检测频率(秒)', 1800)) + begin_show_push = options.get(read_config_value(config, '推送配置', '开播推送开启(是/否)', "是"), True) + over_show_push = options.get(read_config_value(config, '推送配置', '关播推送开启(是/否)', "否"), False) + sooplive_username = read_config_value(config, '账号密码', 'sooplive账号', '') + sooplive_password = read_config_value(config, '账号密码', 'sooplive密码', '') + flextv_username = read_config_value(config, '账号密码', 'flextv账号', '') + flextv_password = read_config_value(config, '账号密码', 'flextv密码', '') + popkontv_username = read_config_value(config, '账号密码', 'popkontv账号', '') + popkontv_partner_code = read_config_value(config, '账号密码', 'partner_code', 'P-00001') + popkontv_password = read_config_value(config, '账号密码', 'popkontv密码', '') + twitcasting_account_type = read_config_value(config, '账号密码', 'twitcasting账号类型', 'normal') + twitcasting_username = read_config_value(config, '账号密码', 'twitcasting账号', '') + twitcasting_password = read_config_value(config, '账号密码', 'twitcasting密码', '') + popkontv_access_token = read_config_value(config, 'Authorization', 'popkontv_token', '') + dy_cookie = read_config_value(config, 'Cookie', '抖音cookie', '') + ks_cookie = read_config_value(config, 'Cookie', '快手cookie', '') + tiktok_cookie = read_config_value(config, 'Cookie', 'tiktok_cookie', '') + hy_cookie = read_config_value(config, 'Cookie', '虎牙cookie', '') + douyu_cookie = read_config_value(config, 'Cookie', '斗鱼cookie', '') + yy_cookie = read_config_value(config, 'Cookie', 'yy_cookie', '') + bili_cookie = read_config_value(config, 'Cookie', 'B站cookie', '') + xhs_cookie = read_config_value(config, 'Cookie', '小红书cookie', '') + bigo_cookie = read_config_value(config, 'Cookie', 'bigo_cookie', '') + blued_cookie = read_config_value(config, 'Cookie', 'blued_cookie', '') + sooplive_cookie = read_config_value(config, 'Cookie', 'sooplive_cookie', '') + netease_cookie = read_config_value(config, 'Cookie', 'netease_cookie', '') + qiandurebo_cookie = read_config_value(config, 'Cookie', '千度热播_cookie', '') + pandatv_cookie = read_config_value(config, 'Cookie', 'pandatv_cookie', '') + maoerfm_cookie = read_config_value(config, 'Cookie', '猫耳fm_cookie', '') + winktv_cookie = read_config_value(config, 'Cookie', 'winktv_cookie', '') + flextv_cookie = read_config_value(config, 'Cookie', 'flextv_cookie', '') + look_cookie = read_config_value(config, 'Cookie', 'look_cookie', '') + twitcasting_cookie = read_config_value(config, 'Cookie', 'twitcasting_cookie', '') + baidu_cookie = read_config_value(config, 'Cookie', 'baidu_cookie', '') + weibo_cookie = read_config_value(config, 'Cookie', 'weibo_cookie', '') + kugou_cookie = read_config_value(config, 'Cookie', 'kugou_cookie', '') + twitch_cookie = read_config_value(config, 'Cookie', 'twitch_cookie', '') + liveme_cookie = read_config_value(config, 'Cookie', 'liveme_cookie', '') + huajiao_cookie = read_config_value(config, 'Cookie', 'huajiao_cookie', '') + liuxing_cookie = read_config_value(config, 'Cookie', 'liuxing_cookie', '') + showroom_cookie = read_config_value(config, 'Cookie', 'showroom_cookie', '') + acfun_cookie = read_config_value(config, 'Cookie', 'acfun_cookie', '') + changliao_cookie = read_config_value(config, 'Cookie', 'changliao_cookie', '') + yinbo_cookie = read_config_value(config, 'Cookie', 'yinbo_cookie', '') + yingke_cookie = read_config_value(config, 'Cookie', 'yingke_cookie', '') + zhihu_cookie = read_config_value(config, 'Cookie', 'zhihu_cookie', '') + chzzk_cookie = read_config_value(config, 'Cookie', 'chzzk_cookie', '') + haixiu_cookie = read_config_value(config, 'Cookie', 'haixiu_cookie', '') + vvxqiu_cookie = read_config_value(config, 'Cookie', 'vvxqiu_cookie', '') + yiqilive_cookie = read_config_value(config, 'Cookie', '17live_cookie', '') + langlive_cookie = read_config_value(config, 'Cookie', 'langlive_cookie', '') + pplive_cookie = read_config_value(config, 'Cookie', 'pplive_cookie', '') + six_room_cookie = read_config_value(config, 'Cookie', '6room_cookie', '') + lehaitv_cookie = read_config_value(config, 'Cookie', 'lehaitv_cookie', '') + huamao_cookie = read_config_value(config, 'Cookie', 'huamao_cookie', '') + shopee_cookie = read_config_value(config, 'Cookie', 'shopee_cookie', '') + youtube_cookie = read_config_value(config, 'Cookie', 'youtube_cookie', '') + taobao_cookie = read_config_value(config, 'Cookie', 'taobao_cookie', '') + jd_cookie = read_config_value(config, 'Cookie', 'jd_cookie', '') + faceit_cookie = read_config_value(config, 'Cookie', 'faceit_cookie', '') + migu_cookie = read_config_value(config, 'Cookie', 'migu_cookie', '') + lianjie_cookie = read_config_value(config, 'Cookie', 'lianjie_cookie', '') + laixiu_cookie = read_config_value(config, 'Cookie', 'laixiu_cookie', '') + picarto_cookie = read_config_value(config, 'Cookie', 'picarto_cookie', '') + + video_save_type_list = ("FLV", "MKV", "TS", "MP4", "MP3音频", "M4A音频", "MP3", "M4A") + if video_save_type and video_save_type.upper() in video_save_type_list: + video_save_type = video_save_type.upper() + else: + video_save_type = "TS" + + check_path = video_save_path or default_path + if utils.check_disk_capacity(check_path, show=first_run) < disk_space_limit: + exit_recording = True + if not recording: + logger.warning(f"Disk space remaining is below {disk_space_limit} GB. " + f"Exiting program due to the disk space limit being reached.") + sys.exit(-1) + + + def contains_url(string: str) -> bool: + pattern = r"(https?://)?(www\.)?[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+(:\d+)?(/.*)?" + return re.search(pattern, string) is not None + + + try: + url_comments, line_list, url_line_list = [[] for _ in range(3)] + with (open(url_config_file, "r", encoding=text_encoding, errors='ignore') as file): + for origin_line in file: + if origin_line in line_list: + delete_line(url_config_file, origin_line) + line_list.append(origin_line) + line = origin_line.strip() + if len(line) < 18: + continue + + line_spilt = line.split('主播: ') + if len(line_spilt) > 2: + line = update_file(url_config_file, line, f'{line_spilt[0]}主播: {line_spilt[-1]}') + + is_comment_line = line.startswith("#") + if is_comment_line: + line = line.lstrip('#') + + if re.search('[,,]', line): + split_line = re.split('[,,]', line) + else: + split_line = [line, ''] + + if len(split_line) == 1: + url = split_line[0] + quality, name = [video_record_quality, ''] + elif len(split_line) == 2: + if contains_url(split_line[0]): + quality = video_record_quality + url, name = split_line + else: + quality, url = split_line + name = '' + else: + quality, url, name = split_line + + if quality not in ("原画", "蓝光", "超清", "高清", "标清", "流畅"): + quality = '原画' + + if url not in url_line_list: + url_line_list.append(url) + else: + delete_line(url_config_file, origin_line) + + url = 'https://' + url if '://' not in url else url + url_host = url.split('/')[2] + + platform_host = [ + 'live.douyin.com', + 'v.douyin.com', + 'www.douyin.com', + 'live.kuaishou.com', + 'www.huya.com', + 'www.douyu.com', + 'www.yy.com', + 'live.bilibili.com', + 'www.redelight.cn', + 'www.xiaohongshu.com', + 'xhslink.com', + 'www.bigo.tv', + 'slink.bigovideo.tv', + 'app.blued.cn', + 'cc.163.com', + 'qiandurebo.com', + 'fm.missevan.com', + 'look.163.com', + 'twitcasting.tv', + 'live.baidu.com', + 'weibo.com', + 'fanxing.kugou.com', + 'fanxing2.kugou.com', + 'mfanxing.kugou.com', + 'www.huajiao.com', + 'www.7u66.com', + 'wap.7u66.com', + 'live.acfun.cn', + 'm.acfun.cn', + 'live.tlclw.com', + 'wap.tlclw.com', + 'live.ybw1666.com', + 'wap.ybw1666.com', + 'www.inke.cn', + 'www.zhihu.com', + 'www.haixiutv.com', + "h5webcdnp.vvxqiu.com", + "17.live", + 'www.lang.live', + "m.pp.weimipopo.com", + "v.6.cn", + "m.6.cn", + 'www.lehaitv.com', + 'h.catshow168.com', + 'e.tb.cn', + 'huodong.m.taobao.com', + '3.cn', + 'eco.m.jd.com', + 'www.miguvideo.com', + 'm.miguvideo.com', + 'show.lailianjie.com', + 'www.imkktv.com', + 'www.picarto.tv' + ] + overseas_platform_host = [ + 'www.tiktok.com', + 'play.sooplive.co.kr', + 'm.sooplive.co.kr', + 'www.sooplive.com', + 'm.sooplive.com', + 'www.pandalive.co.kr', + 'www.winktv.co.kr', + 'www.flextv.co.kr', + 'www.ttinglive.com', + 'www.popkontv.com', + 'www.twitch.tv', + 'www.liveme.com', + 'www.showroom-live.com', + 'chzzk.naver.com', + 'm.chzzk.naver.com', + 'live.shopee.', + '.shp.ee', + 'www.youtube.com', + 'youtu.be', + 'www.faceit.com' + ] + + platform_host.extend(overseas_platform_host) + clean_url_host_list = ( + "live.douyin.com", + "live.bilibili.com", + "www.huajiao.com", + "www.zhihu.com", + "www.huya.com", + "chzzk.naver.com", + "www.liveme.com", + "www.haixiutv.com", + "v.6.cn", + "m.6.cn", + 'www.lehaitv.com' + ) + + if 'live.shopee.' in url_host or '.shp.ee' in url_host: + url_host = 'live.shopee.' if 'live.shopee.' in url_host else '.shp.ee' + + if url_host in platform_host or any(ext in url for ext in (".flv", ".m3u8")): + if url_host in clean_url_host_list: + url = update_file(url_config_file, old_str=url, new_str=url.split('?')[0]) + + if 'xiaohongshu' in url: + host_id = re.search('&host_id=(.*?)(?=&|$)', url) + if host_id: + new_url = url.split('?')[0] + f'?host_id={host_id.group(1)}' + url = update_file(url_config_file, old_str=url, new_str=new_url) + + url_comments = [i for i in url_comments if url not in i] + if is_comment_line: + url_comments.append(url) + else: + new_line = (quality, url, name) + url_tuples_list.append(new_line) + else: + if not origin_line.startswith('#'): + color_obj.print_colored(f"\r{origin_line.strip()} 本行包含未知链接.此条跳过", color_obj.YELLOW) + update_file(url_config_file, old_str=origin_line, new_str=origin_line, start_str='#') + + while len(need_update_line_list): + a = need_update_line_list.pop() + replace_words = a.split('|') + if replace_words[0] != replace_words[1]: + if replace_words[1].startswith("#"): + start_with = '#' + new_word = replace_words[1][1:] + else: + start_with = None + new_word = replace_words[1] + update_file(url_config_file, old_str=replace_words[0], new_str=new_word, start_str=start_with) + + text_no_repeat_url = list(set(url_tuples_list)) + + if len(text_no_repeat_url) > 0: + for url_tuple in text_no_repeat_url: + monitoring = len(running_list) + + if url_tuple[1] in not_record_list: + continue + if url_tuple[1] not in running_list: + print(f"\r{'新增' if not first_start else '传入'}地址: {url_tuple[1]}") + monitoring += 1 + args = [url_tuple, monitoring] + create_var[f'thread_{monitoring}'] = threading.Thread(target=start_record, args=args) + create_var[f'thread_{monitoring}'].daemon = True + create_var[f'thread_{monitoring}'].start() + running_list.append(url_tuple[1]) + time.sleep(local_delay_default) + + + + + url_tuples_list = [] + first_start = False + + + + except Exception as err: + logger.error(f"错误信息: {err} 发生错误的行数: {err.__traceback__.tb_lineno}") + + if first_run: + t = threading.Thread(target=display_info, args=(), daemon=True) + t.start() + t2 = threading.Thread(target=adjust_max_request, args=(), daemon=True) + t2.start() + first_run = False + + time.sleep(3) \ No newline at end of file diff --git a/tools/ffmpeg/懒人包说明.txt b/tools/ffmpeg/懒人包说明.txt new file mode 100644 index 0000000..ef72b6d --- /dev/null +++ b/tools/ffmpeg/懒人包说明.txt @@ -0,0 +1,3 @@ +将 Windows 版 ffmpeg 解压后,把 ffmpeg.exe 放在本文件夹(或 bin 子文件夹)中。 +正式安装包打包时,electron-builder 会把整个项目根目录(排除 electron 等)拷入安装目录, +此处文件会随软件分发给用户,用户无需单独安装 ffmpeg。 diff --git a/web2.py b/web2.py new file mode 100644 index 0000000..31adf88 --- /dev/null +++ b/web2.py @@ -0,0 +1,565 @@ +from fastapi import FastAPI, Query, Request +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse +from pathlib import Path +import asyncio +import html +import os +from contextlib import asynccontextmanager +from typing import Optional + +from fastapi.middleware.cors import CORSMiddleware + +from web2_supervisor import ( + BuiltinSupervisor, + force_kill_ffmpeg_cross_platform, + get_process_info_pm2, + run_pm2_shell, + strip_ansi_codes, + use_builtin, +) +from web2_network import run_network_dashboard +from web2_proxy_config import read_proxy_config, write_proxy_config +from web2_system_metrics import snapshot as system_metrics_snapshot +from web2_youtube_oauth import ( + fetch_stream_key_and_write_ini, + oauth_authorization_url, + oauth_exchange_code, + oauth_status as youtube_oauth_status_snapshot, +) +from web2_wechat_chatbot import register_wechat_chatbot_routes +from web2_client_overview import build_client_overview +from web2_relay_pro import ( + build_relay_live_status, + ensure_default_relay_config, + first_youtube_pm2_name, + get_channel_detail, + list_channels_for_pro, + read_url_config_for_channel, + sync_channel_to_legacy_files, + update_channel_key, + write_url_config_for_channel, +) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + ensure_default_relay_config(CONFIG_DIR) + yield + + +app = FastAPI(title="多进程录制控制台", lifespan=lifespan) + +# ---------------- 配置(根目录结构) ---------------- +BASE_DIR = Path(__file__).parent +CONFIG_DIR = BASE_DIR / "config" +DEFAULT_LOG_DIR = BASE_DIR / ".pm2" / "logs" + +MAX_LOG_LINES = 300 + + +def _label_short(prefix: str) -> str: + return {"tiktok": "TikTok", "youtube": "YouTube", "obs": "OBS", "web": "Web 控制台"}.get(prefix, prefix) + + +def discover_script_entries() -> tuple: + """扫描项目根目录自动识别入口:tiktok*.py、youtube*.py、obs*.sh / obs*.bat,以及当前本文件(Web 控制台)。进程名 = 主文件名(无扩展名)。""" + base = BASE_DIR + web_path = Path(__file__).resolve() + entries: list[dict] = [] + + for path in sorted(base.glob("tiktok*.py")): + if path.is_file() and path.resolve() != web_path: + entries.append({"script": path.name, "label": _label_short("tiktok")}) + + for path in sorted(base.glob("youtube*.py")): + if path.is_file() and path.resolve() != web_path: + entries.append({"script": path.name, "label": _label_short("youtube")}) + + for path in sorted(base.glob("obs*.sh")): + if path.is_file(): + entries.append({"script": path.name, "label": _label_short("obs")}) + + for path in sorted(base.glob("obs*.bat")): + if path.is_file(): + entries.append({"script": path.name, "label": _label_short("obs")}) + + entries.append({"script": web_path.name, "label": _label_short("web")}) + + return tuple(entries) + + +SCRIPT_ENTRIES = discover_script_entries() + + +def pm2_name_from_script(script: str) -> str: + return Path(script).stem + + +def _stem(script: str) -> str: + return Path(script).stem + + +def is_youtube_script(script: str) -> bool: + return script.endswith(".py") and _stem(script).startswith("youtube") + + +def is_tiktok_script(script: str) -> bool: + return script.endswith(".py") and _stem(script).startswith("tiktok") + + +def _build_process_monitor(): + return tuple( + { + "pm2": pm2_name_from_script(e["script"]), + "script": e["script"], + "label": e["label"], + } + for e in SCRIPT_ENTRIES + ) + + +PROCESS_MONITOR = _build_process_monitor() +VALID_PROCESSES = frozenset(p["pm2"] for p in PROCESS_MONITOR) + +URL_CONFIG_PM2 = frozenset( + pm2_name_from_script(e["script"]) + for e in SCRIPT_ENTRIES + if is_tiktok_script(e["script"]) or is_youtube_script(e["script"]) +) + + +def script_for_pm2(pm2: str) -> Optional[str]: + for p in PROCESS_MONITOR: + if p["pm2"] == pm2: + return p["script"] + return None + + +WEB_SCRIPT_NAME = Path(__file__).name +_builtin_supervisor: Optional[BuiltinSupervisor] = None + + +def get_builtin_supervisor() -> BuiltinSupervisor: + global _builtin_supervisor + if _builtin_supervisor is None: + _builtin_supervisor = BuiltinSupervisor(BASE_DIR, WEB_SCRIPT_NAME, script_for_pm2) + return _builtin_supervisor + + +async def control_start(process: str) -> str: + if use_builtin(): + return await get_builtin_supervisor().start(process) + return await run_pm2_shell(f"start {process}") + + +async def control_stop(process: str) -> str: + if use_builtin(): + return await get_builtin_supervisor().stop(process) + return await run_pm2_shell(f"stop {process}") + + +async def control_restart(process: str) -> str: + if use_builtin(): + return await get_builtin_supervisor().restart(process) + return await run_pm2_shell(f"restart {process}") + + +async def control_full_status() -> str: + if use_builtin(): + return await get_builtin_supervisor().full_status_text() + return await run_pm2_shell("status", timeout=8.0) + + +async def control_get_process_info(process: str) -> dict: + if use_builtin(): + return await get_builtin_supervisor().get_process_info(process) + return await get_process_info_pm2(process, DEFAULT_LOG_DIR, strip_ansi_codes) + + +@app.middleware("http") +async def web2_token_middleware(request: Request, call_next): + """若设置环境变量 WEB2_API_TOKEN,则除 /health 与 OpenAPI 外需携带 Bearer 或 X-API-Token。""" + # CORS 预检不带 Authorization;若在外层先于 CORSMiddleware 拦截会缺少 Access-Control-* 头 + if request.method == "OPTIONS": + return await call_next(request) + token = os.environ.get("WEB2_API_TOKEN", "").strip() + if not token: + return await call_next(request) + path = request.url.path + if path == "/health" or path.startswith("/docs") or path in ("/openapi.json", "/redoc"): + return await call_next(request) + auth = request.headers.get("authorization") or "" + xtok = (request.headers.get("x-api-token") or "").strip() + if auth.startswith("Bearer ") and auth[7:].strip() == token: + return await call_next(request) + if xtok == token: + return await call_next(request) + return JSONResponse({"error": "unauthorized"}, status_code=401) + + +# 最后注册,保证 CORSMiddleware 在最外层,401 等响应仍带 CORS 头(供 Vite 等跨源调试) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["GET", "POST", "OPTIONS"], + allow_headers=["*"], +) + +register_wechat_chatbot_routes(app, BASE_DIR) + + +@app.get("/client/overview") +async def client_overview(): + """Electron 工作台:频道列表、微信摘要(EasyTier 节点由主进程 easytier-cli 查询)。""" + return build_client_overview(CONFIG_DIR, BASE_DIR) + + +# ---------------- Pro 多路转播(每频道独立 URL 文件 + 去重 + 同步 legacy) ---------------- + + +@app.get("/relay_pro/channels") +async def relay_pro_channels(): + return list_channels_for_pro(CONFIG_DIR) + + +@app.get("/relay_pro/channel_detail") +async def relay_pro_channel_detail(channel_id: str = Query(..., description="频道 id,如 ch_0")): + d = get_channel_detail(CONFIG_DIR, channel_id) + if not d: + return JSONResponse({"error": "未找到频道"}, status_code=404) + return d + + +@app.get("/relay_pro/url_config") +async def relay_pro_get_url_config(channel_id: str = Query(...)): + return {"content": read_url_config_for_channel(CONFIG_DIR, channel_id)} + + +@app.post("/relay_pro/url_config") +async def relay_pro_post_url_config(request: Request, channel_id: str = Query(...)): + try: + data = await request.json() + except Exception: + return JSONResponse({"error": "无效的 JSON"}, status_code=400) + content = data.get("content", "") if isinstance(data, dict) else "" + ok, msg = write_url_config_for_channel(CONFIG_DIR, channel_id, content) + if not ok: + return JSONResponse({"error": msg}, status_code=400) + return {"message": msg} + + +@app.post("/relay_pro/channel_key") +async def relay_pro_save_channel_key(request: Request): + try: + data = await request.json() + except Exception: + return JSONResponse({"error": "无效的 JSON"}, status_code=400) + if not isinstance(data, dict): + return JSONResponse({"error": "无效数据"}, status_code=400) + cid = str(data.get("channelId") or data.get("channel_id") or "").strip() + key = str(data.get("youtubeKey") or data.get("key") or "") + if not cid: + return JSONResponse({"error": "缺少 channelId"}, status_code=400) + ok, msg = update_channel_key(CONFIG_DIR, cid, key) + if not ok: + return JSONResponse({"error": msg}, status_code=400) + return {"message": msg} + + +@app.post("/relay_pro/sync_active") +async def relay_pro_sync_active(channel_id: str = Query(...)): + ok, msg = sync_channel_to_legacy_files(CONFIG_DIR, channel_id) + if not ok: + return JSONResponse({"error": msg}, status_code=400) + return {"message": msg} + + +@app.get("/relay_pro/live_status") +async def relay_pro_live_status(): + yt = first_youtube_pm2_name([{"script": e["script"]} for e in SCRIPT_ENTRIES]) + + async def read_tail(path: str, lines: int) -> str: + return await read_log_path(path, lines) + + return await build_relay_live_status( + CONFIG_DIR, + BASE_DIR, + control_get_process_info, + read_tail, + yt, + ) + + +# ---------------- 工具函数 ---------------- +async def read_log_path(path: str, lines: int) -> str: + if not path: + return "无日志路径\n" + if "/dev/null" in path: + return "日志输出被禁用(无日志文件)\n" + log_file = Path(path) + if not log_file.exists(): + return f"日志文件不存在: {path}\n" + try: + with open(log_file, "r", encoding="utf-8", errors="ignore") as f: + content_lines = f.readlines() + return "".join(content_lines[-lines:]) if content_lines else "无日志内容\n" + except Exception as e: + return f"读取日志失败 ({path}): {str(e)}\n" + + +# ---------------- 配置路由(youtube.ini,仅 youtube) ---------------- +@app.get("/get_config") +async def get_config(process: str = Query(..., description="进程名")): + sc = script_for_pm2(process) + if not sc or not is_youtube_script(sc): + return JSONResponse({"error": "仅 youtube*.py 对应进程支持 youtube.ini 编辑"}, status_code=400) + + config_file = CONFIG_DIR / "youtube.ini" + if not config_file.exists(): + return {"content": "# youtube.ini 文件不存在,将创建空文件\n"} + try: + content = config_file.read_text(encoding="utf-8") + return {"content": content} + except Exception as e: + return JSONResponse({"error": f"读取失败: {str(e)}"}, status_code=500) + +@app.post("/save_config") +async def save_config(request: Request, process: str = Query(..., description="进程名")): + sc = script_for_pm2(process) + if not sc or not is_youtube_script(sc): + return JSONResponse({"error": "仅 youtube*.py 对应进程支持 youtube.ini 编辑"}, status_code=400) + + try: + data = await request.json() + content = data.get("content", "") + except: + return JSONResponse({"error": "无效的 JSON 数据"}, status_code=400) + + config_file = CONFIG_DIR / "youtube.ini" + try: + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + config_file.write_text(content, encoding="utf-8") + return {"message": "配置保存成功"} + except Exception as e: + return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500) + +# ---------------- 配置路由(URL_config.ini,youtube 和 tiktok 共享) ---------------- +@app.get("/get_url_config") +async def get_url_config(process: str = Query(..., description="进程名")): + if process not in URL_CONFIG_PM2: + return JSONResponse({"error": "仅 tiktok*.py / youtube*.py 对应进程支持 URL 配置编辑"}, status_code=400) + + config_file = CONFIG_DIR / "URL_config.ini" + if not config_file.exists(): + return {"content": "# URL_config.ini 文件不存在,将创建空文件\n"} + try: + content = config_file.read_text(encoding="utf-8") + return {"content": content} + except Exception as e: + return JSONResponse({"error": f"读取失败: {str(e)}"}, status_code=500) + +@app.post("/save_url_config") +async def save_url_config(request: Request, process: str = Query(..., description="进程名")): + if process not in URL_CONFIG_PM2: + return JSONResponse({"error": "仅 tiktok*.py / youtube*.py 对应进程支持 URL 配置编辑"}, status_code=400) + + try: + data = await request.json() + content = data.get("content", "") + except: + return JSONResponse({"error": "无效的 JSON 数据"}, status_code=400) + + config_file = CONFIG_DIR / "URL_config.ini" + try: + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + config_file.write_text(content, encoding="utf-8") + return {"message": "配置保存成功"} + except Exception as e: + return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500) + +# ---------------- PM2 控制路由 ---------------- +@app.get("/start") +async def start(process: str = Query(..., description="进程名")): + if process not in VALID_PROCESSES: + return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400) + output = await control_start(process) + return JSONResponse({"output": output}) + + +@app.get("/stop") +async def stop(process: str = Query(..., description="进程名")): + if process not in VALID_PROCESSES: + return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400) + + # 第一步:尝试优雅停止 + ctrl_output = await control_stop(process) + + # 第二步:等待信号传播(通常 1~3 秒) + await asyncio.sleep(2.0) + + # 第三步:录制类进程才强杀 ffmpeg(web* 控制台不杀 ffmpeg) + if not process.startswith("web"): + await force_kill_ffmpeg_cross_platform(process) + + # 第四步:再等一小会儿,确认 + await asyncio.sleep(1.0) + + return JSONResponse({ + "message": "已执行 stop" + ("" if process.startswith("web") else " + 强杀 ffmpeg"), + "pm2_output": ctrl_output, + "note": "如果仍有残留,可多次点击停止或查看下方日志", + }) + + +@app.get("/restart") +async def restart(process: str = Query(..., description="进程名")): + if process not in VALID_PROCESSES: + return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400) + output = await control_restart(process) + if not process.startswith("web"): + await force_kill_ffmpeg_cross_platform(process) + return JSONResponse({"output": output}) + + +@app.get("/status") +async def status(process: str = Query(..., description="进程名")): + if process not in VALID_PROCESSES: + return JSONResponse({ + "raw_status": "", + "process_status": "invalid", + "recent_log": f"无效进程名: {process}", + "recent_error": "" + }) + + full_status_raw = await control_full_status() + full_status = strip_ansi_codes(full_status_raw) + info = await control_get_process_info(process) + recent_log = await read_log_path(info["out_path"], MAX_LOG_LINES) + recent_error = await read_log_path(info["err_path"], MAX_LOG_LINES) + + if info["process_status"] == "not_found": + recent_log = "进程未运行或未由本机管理器启动。\n" + recent_error = "" + + return JSONResponse({ + "raw_status": full_status, + "process_status": info["process_status"], + "recent_log": recent_log, + "recent_error": recent_error, + }) + + +@app.get("/health") +async def health(): + return {"ok": True} + + +@app.get("/network_status") +async def network_status(): + """Google / 抖音 连通与简易网速(供仪表盘)。""" + try: + return await run_network_dashboard() + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=500) + + +@app.get("/proxy_config") +async def get_proxy_config(): + """仅作用于录制子进程的代理(不写 Windows 系统代理)。""" + return read_proxy_config() + + +@app.post("/proxy_config") +async def post_proxy_config(request: Request): + try: + data = await request.json() + except Exception: + return JSONResponse({"error": "无效的 JSON"}, status_code=400) + if not isinstance(data, dict): + return JSONResponse({"error": "无效数据"}, status_code=400) + write_proxy_config( + { + "enabled": bool(data.get("enabled")), + "http": str(data.get("http") or ""), + "socks": str(data.get("socks") or ""), + } + ) + return {"ok": True} + + +@app.get("/system_metrics") +async def system_metrics(): + """CPU/内存/磁盘/网卡速率与 ffmpeg 进程摘要。""" + try: + return system_metrics_snapshot() + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=500) + + +# ---------------- YouTube OAuth(串流密钥) ---------------- +@app.get("/youtube/oauth/start") +async def youtube_oauth_start(): + """返回 Google 授权页 URL,前端应用浏览器打开。""" + try: + url, state = oauth_authorization_url() + return {"url": url, "state": state} + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=400) + + +@app.get("/youtube/oauth/callback") +async def youtube_oauth_callback(code: str = Query(...), state: str = Query(...)): + """Google 授权完成后的回调(须与 google_oauth.json 中 redirect_uri 一致)。""" + try: + oauth_exchange_code(code, state) + except Exception as e: + err = html.escape(str(e)) + return HTMLResponse( + f"授权失败" + f"" + f"

授权失败:{err}

请关闭此页后重试。

", + status_code=400, + ) + return HTMLResponse( + "授权成功" + "" + "

授权成功,可关闭此页并返回「直播录制助手」。

" + "

若浏览器未自动关闭,请手动关闭本标签页。

" + "" + ) + + +@app.get("/youtube/oauth/status") +async def youtube_oauth_status_route(): + return youtube_oauth_status_snapshot() + + +@app.post("/youtube/stream/fetch_key") +async def youtube_stream_fetch_key(process: str = Query(..., description="进程名")): + """调用 YouTube Live API 获取串流密钥并写入 config/youtube.ini。""" + if process not in VALID_PROCESSES: + return JSONResponse({"error": "无效进程名"}, status_code=400) + sc = script_for_pm2(process) + if not sc or not is_youtube_script(sc): + return JSONResponse({"error": "仅 youtube*.py 对应进程可使用此功能"}, status_code=400) + try: + return fetch_stream_key_and_write_ini() + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=500) + + +# ---------------- 进程列表(供前端同步,由 discover_script_entries 自动扫描) ---------------- +@app.get("/process_monitor") +async def process_monitor(): + return { + "entries": [ + {"pm2": p["pm2"], "script": p["script"], "label": p["label"]} + for p in PROCESS_MONITOR + ] + } + + +# ---------------- 首页 ---------------- +@app.get("/") +async def index(): + return FileResponse(BASE_DIR / "index.html") \ No newline at end of file diff --git a/web2.sh b/web2.sh new file mode 100644 index 0000000..c3abe0b --- /dev/null +++ b/web2.sh @@ -0,0 +1,2 @@ +# Ubuntu:本分支控制台入口(与无「2」分支的 web / youtube.py 等区分) +uvicorn web2:app --host 0.0.0.0 --port 8001 --reload diff --git a/web2_client_overview.py b/web2_client_overview.py new file mode 100644 index 0000000..e0b601f --- /dev/null +++ b/web2_client_overview.py @@ -0,0 +1,255 @@ +""" +工作台 /client/overview:YouTube 频道摘要、微信绑定摘要(EasyTier 节点数由 Electron 主进程补充)。 + +多路转播语义:每条线路为「一个抖音直播间 ↔ 一个 YouTube 串流」一对一; +禁止同一抖音直播间同时向多个 YouTube 频道推流;禁止多条线路共用同一串流密钥(避免串台)。 +""" +from __future__ import annotations + +import configparser +import json +import re +from pathlib import Path +from typing import Any + +from web2_wechat_chatbot import build_client, load_wechat_chatbot_config, load_wechat_state + + +def _mask_key(key: str) -> str: + k = (key or "").strip() + if not k: + return "" + if len(k) <= 8: + return "****" + return f"{k[:4]}…{k[-4:]}" + + +def _short_douyin_hint(url: str) -> str: + u = (url or "").strip() + if not u: + return "" + if len(u) <= 56: + return u + return f"{u[:28]}…{u[-16:]}" + + +def _normalize_douyin_url(url: str) -> str: + """用于去重:去空白、小写 host、去尾部斜杠。""" + u = (url or "").strip() + if not u: + return "" + u = re.sub(r"#.*$", "", u).strip() + return u.rstrip("/").lower() + + +def _normalize_stream_key(key: str) -> str: + k = (key or "").strip() + if not k: + return "" + if k.startswith("rtmp://") or k.startswith("rtmps://"): + parts = k.rstrip("/").split("/") + k = parts[-1] if parts else k + return k.strip().lower() + + +def _parse_youtube_ini_key(ini_path: Path) -> str: + if not ini_path.exists(): + return "" + cp = configparser.ConfigParser() + cp.read(ini_path, encoding="utf-8") + if not cp.has_section("youtube"): + return "" + for name, val in cp.items("youtube"): + if name in ("key", "stream_key"): + v = (val or "").strip() + if v and not v.startswith("#"): + return v + return "" + + +def _first_douyin_line_from_url_config(config_dir: Path) -> str: + """单路 legacy:从 URL_config.ini 取首条抖音链接作展示提示(仍可能多 URL 共单 key,与多路一对一模型不同)。""" + p = config_dir / "URL_config.ini" + if not p.exists(): + return "" + try: + text = p.read_text(encoding="utf-8", errors="ignore") + except Exception: + return "" + for raw in text.splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if "douyin" not in line.lower(): + continue + if "," in line: + line = line.split(",")[-1].strip() + return line.strip() + return "" + + +def _parse_youtube_channels_json(path: Path) -> tuple[list[dict[str, str]], list[str]]: + """ + 解析 youtube_channels.json:展示层包含「待填写」线路;仅对同时填写两项的条目做一对一冲突校验。 + 不完整条目不标为错误,由客户端自动维护文件,用户只需在录制页填写即可。 + """ + errors: list[str] = [] + try: + raw = json.loads(path.read_text(encoding="utf-8")) + except Exception as e: + return [], [f"youtube_channels.json 解析失败: {e}"] + + arr = raw.get("channels") if isinstance(raw, dict) else None + if not isinstance(arr, list): + return [], ["youtube_channels.json 缺少 channels 数组"] + + parsed: list[tuple[str, str, str]] = [] + for item in arr: + if not isinstance(item, dict): + continue + name = str(item.get("name") or item.get("id") or "").strip() or "未命名" + durl = str( + item.get("douyinUrl") or item.get("douyin_url") or item.get("douyin") or item.get("url") or "" + ).strip() + key = str(item.get("youtubeKey") or item.get("key") or item.get("stream_key") or "").strip() + parsed.append((name, durl, key)) + + # 仅对「两项均已填写」的条目做冲突与规范化校验 + strict: list[tuple[str, str, str]] = [] + for name, durl, key in parsed: + if durl and key: + strict.append((name, durl, key)) + + seen_d: dict[str, str] = {} + seen_k: dict[str, str] = {} + ok_strict: set[str] = set() + for name, durl, key in strict: + nd = _normalize_douyin_url(durl) + nk = _normalize_stream_key(key) + if not nd or not nk: + errors.append(f"条目「{name}」地址或密钥无效") + continue + if nd in seen_d: + errors.append( + f"禁止重复使用同一抖音直播间绑定多条线路(冲突:「{seen_d[nd]}」与「{name}」)" + ) + continue + if nk in seen_k: + errors.append( + f"禁止多条抖音线路共用同一 YouTube 串流密钥(冲突:「{seen_k[nk]}」与「{name}」)" + ) + continue + seen_d[nd] = name + seen_k[nk] = name + ok_strict.add(name) + + out: list[dict[str, str]] = [] + for name, durl, key in parsed: + if not durl and not key: + out.append( + { + "name": name, + "douyinPreview": "(待填写)", + "keyPreview": "(待填写)", + } + ) + continue + if not durl or not key: + out.append( + { + "name": name, + "douyinPreview": _short_douyin_hint(durl) if durl else "(待填写)", + "keyPreview": _mask_key(key) if key else "(待填写)", + } + ) + continue + if name in ok_strict: + out.append( + { + "name": name, + "douyinPreview": _short_douyin_hint(durl), + "keyPreview": _mask_key(key), + } + ) + else: + out.append( + { + "name": name, + "douyinPreview": _short_douyin_hint(durl) if durl else "(待填写)", + "keyPreview": _mask_key(key) if key else "(待填写)", + } + ) + + return out, errors + + +def _youtube_channels_list(config_dir: Path) -> tuple[list[dict[str, str]], list[str], list[str]]: + """ + 返回:(频道行, 校验错误, 说明性备注)。 + 错误用于重复抖音/重复密钥等;备注用于单路 legacy 提示,不应标红为「错误」。 + """ + path = config_dir / "youtube_channels.json" + if path.exists(): + pairs, errs = _parse_youtube_channels_json(path) + if pairs: + return pairs, errs, [] + if errs: + return [], errs, [] + return ( + [], + [], + [ + "转播线路文件已就绪,请在客户端「录制」页 Pro 转播中填写抖音地址与 YouTube 密钥,将自动保存。", + ], + ) + + key = _parse_youtube_ini_key(config_dir / "youtube.ini") + if not key: + return [], [], [] + + hint = _first_douyin_line_from_url_config(config_dir) + row: dict[str, str] = { + "name": "默认(单路)", + "keyPreview": _mask_key(key), + } + if hint: + row["douyinPreview"] = _short_douyin_hint(hint) + else: + row["douyinPreview"] = "(请在 URL_config.ini 填写抖音地址)" + notes = [ + "当前为单路 legacy:youtube.ini + URL_config.ini;多路一对一由客户端自动生成 youtube_channels.json。", + ] + return [row], [], notes + + +def build_client_overview(config_dir: Path, base_dir: Path) -> dict[str, Any]: + channels, relay_errors, relay_notes = _youtube_channels_list(config_dir) + cfg = load_wechat_chatbot_config(base_dir) + st = load_wechat_state(base_dir) + wechat: dict[str, Any] = { + "configured": bool(cfg.get("proxyUrl") and cfg.get("apiKey")), + "nickName": st.get("nickName"), + "wcId": st.get("wcId"), + "online": False, + "proxyError": None, + } + if wechat["configured"]: + try: + c = build_client(cfg) + ps = c.get_status() + wechat["online"] = bool(ps.get("isLoggedIn")) or bool(ps.get("wcId")) + if ps.get("nickName"): + wechat["nickName"] = ps.get("nickName") + if ps.get("wcId"): + wechat["wcId"] = ps.get("wcId") + except Exception as e: + wechat["proxyError"] = str(e)[:300] + return { + "youtubeChannelCount": len(channels), + "youtubeChannels": channels, + "relayPairingPolicy": "one_douyin_one_youtube", + "relayValidationErrors": relay_errors, + "relayNotes": relay_notes, + "wechat": wechat, + "easyTierPeersNote": "peer 数量由 Electron 调用 easytier-cli peer list 获取", + } diff --git a/web2_network.py b/web2_network.py new file mode 100644 index 0000000..dfa18bb --- /dev/null +++ b/web2_network.py @@ -0,0 +1,267 @@ +""" +网络检测:Google(海外)与抖音(国内)可达性、解析 IP/地区、简易上下行 Mbps。 +说明:上行依赖对端是否接受 POST,失败时返回 null 与原因,属正常现象。 +""" + +from __future__ import annotations + +import asyncio +import socket +import time +from typing import Any, Optional + +import httpx + +# ---------- 目标 ---------- +GOOGLE_HOST = "www.google.com" +GOOGLE_CHECK_URL = "https://www.google.com/generate_204" +GOOGLE_DOWNLOAD_URL = ( + "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" +) + +DOUYIN_HOST = "www.douyin.com" +DOUYIN_CHECK_URL = "https://www.douyin.com/" +# 首页 HTML 流足够大,便于采样测下行 +DOUYIN_DOWNLOAD_URL = "https://www.douyin.com/" + +OVERSEAS_UPLOAD_URL = "https://httpbin.org/post" +DOMESTIC_UPLOAD_URLS = ( + "https://www.douyin.com/", + "https://www.163.com/", +) + +IP_API_TEMPLATE = "http://ip-api.com/json/{ip}?fields=status,country,countryCode,message,query" + +DOWNLOAD_CAP_BYTES = 600_000 +UPLOAD_BYTES = 256 * 1024 +CONNECT_TIMEOUT = 8.0 +READ_TIMEOUT = 25.0 + + +def _is_probably_china_country(code: Optional[str]) -> bool: + return (code or "").upper() == "CN" + + +async def _resolve_ipv4(host: str) -> tuple[Optional[str], Optional[str]]: + def _sync() -> tuple[Optional[str], Optional[str]]: + try: + infos = socket.getaddrinfo(host, 443, type=socket.SOCK_STREAM) + except socket.gaierror as e: + return None, str(e) + for fam, _, _, _, sockaddr in infos: + if fam == socket.AF_INET: + return sockaddr[0], None + for fam, _, _, _, sockaddr in infos: + if fam == socket.AF_INET6: + return sockaddr[0], None + return None, "no address" + + return await asyncio.to_thread(_sync) + + +async def _country_for_ip(client: httpx.AsyncClient, ip: str) -> dict[str, Any]: + if not ip: + return {"country": None, "country_code": None, "error": "no ip"} + try: + r = await client.get(IP_API_TEMPLATE.format(ip=ip), timeout=6.0) + r.raise_for_status() + data = r.json() + except Exception as e: + return {"country": None, "country_code": None, "error": str(e)} + if data.get("status") != "success": + return { + "country": None, + "country_code": None, + "error": data.get("message") or "lookup failed", + } + return { + "country": data.get("country"), + "country_code": data.get("countryCode"), + "error": None, + } + + +async def _latency_get(client: httpx.AsyncClient, url: str) -> tuple[bool, float, Optional[str]]: + t0 = time.monotonic() + try: + r = await client.get(url, follow_redirects=True) + ok = r.status_code < 400 + ms = (time.monotonic() - t0) * 1000 + return ok, ms, None if ok else f"HTTP {r.status_code}" + except Exception as e: + ms = (time.monotonic() - t0) * 1000 + return False, ms, str(e) + + +async def _measure_download_mbps( + client: httpx.AsyncClient, url: str, cap: int = DOWNLOAD_CAP_BYTES +) -> tuple[Optional[float], Optional[str], int]: + total = 0 + t0 = time.monotonic() + try: + async with client.stream("GET", url, follow_redirects=True) as r: + r.raise_for_status() + async for chunk in r.aiter_bytes(): + total += len(chunk) + if total >= cap: + break + dt = time.monotonic() - t0 + if dt < 0.001 or total == 0: + return None, "无数据或过快", total + mbps = (total * 8) / dt / 1_000_000 + return round(mbps, 2), None, total + except Exception as e: + return None, str(e), total + + +async def _measure_upload_mbps( + client: httpx.AsyncClient, url: str, size: int = UPLOAD_BYTES +) -> tuple[Optional[float], Optional[str]]: + data = b"\x00" * size + t0 = time.monotonic() + try: + r = await client.post( + url, + content=data, + headers={"Content-Type": "application/octet-stream"}, + ) + dt = time.monotonic() - t0 + if dt < 0.001: + return None, "耗时过短" + mbps = (size * 8) / dt / 1_000_000 + return round(mbps, 2), None + except Exception as e: + return None, str(e) + + +async def _try_domestic_upload(client: httpx.AsyncClient) -> tuple[Optional[float], Optional[str], Optional[str]]: + last_err: Optional[str] = None + for url in DOMESTIC_UPLOAD_URLS: + up, err = await _measure_upload_mbps(client, url) + if up is not None: + return up, None, url + last_err = err + return None, last_err, None + + +async def run_network_dashboard() -> dict[str, Any]: + """ + 返回结构供前端仪表盘使用。 + routes_different:基于解析 IP 的国家码;若 API 失败则给 likely / unknown。 + """ + timeout = httpx.Timeout(READ_TIMEOUT, connect=CONNECT_TIMEOUT) + limits = httpx.Limits(max_keepalive_connections=4, max_connections=8) + + ua = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ) + async with httpx.AsyncClient( + timeout=timeout, + limits=limits, + verify=True, + headers={"User-Agent": ua}, + ) as client: + g_ip, g_res_err = await _resolve_ipv4(GOOGLE_HOST) + d_ip, d_res_err = await _resolve_ipv4(DOUYIN_HOST) + + async def _empty_geo() -> dict[str, Any]: + return {"country": None, "country_code": None, "error": "no ip"} + + g_country_task = _country_for_ip(client, g_ip) if g_ip else _empty_geo() + d_country_task = _country_for_ip(client, d_ip) if d_ip else _empty_geo() + + ( + (g_ok, g_ms, g_err), + (d_ok, d_ms, d_err), + g_geo, + d_geo, + ) = await asyncio.gather( + _latency_get(client, GOOGLE_CHECK_URL), + _latency_get(client, DOUYIN_CHECK_URL), + g_country_task, + d_country_task, + ) + + if not isinstance(g_geo, dict): + g_geo = {"country": None, "country_code": None, "error": None} + if not isinstance(d_geo, dict): + d_geo = {"country": None, "country_code": None, "error": None} + + g_code = g_geo.get("country_code") + d_code = d_geo.get("country_code") + + routes_different: Optional[bool] = None + routes_note = "" + if g_code and d_code: + routes_different = _is_probably_china_country(g_code) != _is_probably_china_country(d_code) + routes_note = "依据解析 IP 的国家码:海外站点与国内站点出口不一致" if routes_different else ( + "依据解析 IP:两国别相同或未区分境内外(若您期望分流,请检查代理/路由)" + ) + else: + routes_note = "未能查询到双方 IP 国家,无法严格判断线路是否分流" + if g_ip and d_ip and g_ip != d_ip: + routes_note += ";两站点解析 IP 不同,通常走不同目标。" + if g_res_err: + routes_note += f" Google DNS: {g_res_err}" + if d_res_err: + routes_note += f" 抖音 DNS: {d_res_err}" + + # 测速(并行) + od_mbps, od_err, od_bytes = await _measure_download_mbps(client, GOOGLE_DOWNLOAD_URL) + dd_mbps, dd_err, dd_bytes = await _measure_download_mbps(client, DOUYIN_DOWNLOAD_URL) + + ou_mbps, ou_err = await _measure_upload_mbps(client, OVERSEAS_UPLOAD_URL) + du_mbps, du_err, du_url = await _try_domestic_upload(client) + + return { + "google": { + "host": GOOGLE_HOST, + "reachable": g_ok, + "latency_ms": round(g_ms, 1), + "error": g_err or g_res_err, + "resolved_ip": g_ip, + "country": g_geo.get("country"), + "country_code": g_code, + "geo_error": g_geo.get("error"), + }, + "douyin": { + "host": DOUYIN_HOST, + "reachable": d_ok, + "latency_ms": round(d_ms, 1), + "error": d_err or d_res_err, + "resolved_ip": d_ip, + "country": d_geo.get("country"), + "country_code": d_code, + "geo_error": d_geo.get("error"), + }, + "routes": { + "different": routes_different, + "note": routes_note.strip(), + }, + "speed": { + "overseas": { + "label": "海外(Google / gstatic)", + "download_mbps": od_mbps, + "download_error": od_err, + "download_bytes_sampled": od_bytes, + "upload_mbps": ou_mbps, + "upload_error": ou_err, + "upload_target": OVERSEAS_UPLOAD_URL, + }, + "domestic": { + "label": "国内(字节 CDN / 门户)", + "download_mbps": dd_mbps, + "download_error": dd_err, + "download_bytes_sampled": dd_bytes, + "upload_mbps": du_mbps, + "upload_error": du_err, + "upload_target": du_url, + }, + }, + "meta": { + "at": time.time(), + "download_cap_bytes": DOWNLOAD_CAP_BYTES, + "upload_bytes": UPLOAD_BYTES, + }, + } diff --git a/web2_proxy_config.py b/web2_proxy_config.py new file mode 100644 index 0000000..3b59e03 --- /dev/null +++ b/web2_proxy_config.py @@ -0,0 +1,96 @@ +""" +仅给「录制子进程」使用的代理配置(FFmpeg / youtube2 等),不修改 Windows 系统代理。 +用户在本机运行 Clash Verge 等时,通常填 http://127.0.0.1:7890 与 socks5://127.0.0.1:7890。 +""" +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +from configparser import ConfigParser + +BASE_DIR = Path(__file__).resolve().parent +PROXY_INI = BASE_DIR / "config" / "ffmpeg_proxy.ini" + + +def _default_ini_text() -> str: + return ( + "[proxy]\n" + "# 开启后仅作用于本软件拉起的录制/推流进程,不会改系统代理\n" + "enabled = no\n" + "# 与 Clash Verge「混合端口」一致即可(示例)\n" + "http = http://127.0.0.1:7890\n" + "socks = socks5://127.0.0.1:7890\n" + ) + + +def ensure_proxy_config_file() -> None: + PROXY_INI.parent.mkdir(parents=True, exist_ok=True) + if not PROXY_INI.exists(): + PROXY_INI.write_text(_default_ini_text(), encoding="utf-8") + + +def read_proxy_config() -> dict[str, Any]: + ensure_proxy_config_file() + cp = ConfigParser() + cp.read(PROXY_INI, encoding="utf-8") + if not cp.has_section("proxy"): + cp.add_section("proxy") + enabled = cp.get("proxy", "enabled", fallback="no").strip().lower() in ( + "1", + "true", + "yes", + "on", + "是", + "开", + ) + http = (cp.get("proxy", "http", fallback="") or "").strip() + socks = (cp.get("proxy", "socks", fallback="") or "").strip() + return {"enabled": enabled, "http": http, "socks": socks} + + +def write_proxy_config(data: dict[str, Any]) -> None: + ensure_proxy_config_file() + cp = ConfigParser() + cp.read(PROXY_INI, encoding="utf-8") + if not cp.has_section("proxy"): + cp.add_section("proxy") + cp.set("proxy", "enabled", "yes" if data.get("enabled") else "no") + cp.set("proxy", "http", str(data.get("http") or "").strip()) + cp.set("proxy", "socks", str(data.get("socks") or "").strip()) + with open(PROXY_INI, "w", encoding="utf-8") as f: + cp.write(f) + + +def subprocess_env_overlay() -> dict[str, str]: + """ + 合并进子进程 environ 的键值(不含整个 os.environ)。 + youtube2 / tiktok2 会读 STREAM_HTTP_PROXY / STREAM_SOCKS_PROXY。 + """ + cfg = read_proxy_config() + out: dict[str, str] = {} + if not cfg["enabled"]: + return out + http = cfg["http"] + socks = cfg["socks"] + if http: + out["STREAM_HTTP_PROXY"] = http + out["HTTP_PROXY"] = http + out["HTTPS_PROXY"] = http + out["http_proxy"] = http + out["https_proxy"] = http + if socks: + out["STREAM_SOCKS_PROXY"] = socks + out["ALL_PROXY"] = socks + out["all_proxy"] = socks + return out + + +def merge_child_env(base: dict[str, str] | None = None) -> dict[str, str]: + """供 asyncio subprocess / Popen 使用(完整继承系统环境后再叠加代理)。""" + env = dict(os.environ) + if base: + env.update(base) + env.update(subprocess_env_overlay()) + return env diff --git a/web2_relay_pro.py b/web2_relay_pro.py new file mode 100644 index 0000000..d087cf6 --- /dev/null +++ b/web2_relay_pro.py @@ -0,0 +1,439 @@ +""" +Pro 多路转播:每频道独立 URL 配置文件、跨频道抖音地址去重、同步到 legacy youtube.ini + URL_config.ini。 +""" +from __future__ import annotations + +import json +import re +import time +from pathlib import Path +from typing import Any, Callable, Awaitable + +# 与 web2_client_overview 一致的去重键 +def _norm_douyin_url(url: str) -> str: + u = (url or "").strip() + if not u: + return "" + u = re.sub(r"#.*$", "", u).strip() + return u.rstrip("/").lower() + + +def _norm_stream_key(key: str) -> str: + k = (key or "").strip() + if not k: + return "" + if k.startswith("rtmp://") or k.startswith("rtmps://"): + parts = k.rstrip("/").split("/") + k = parts[-1] if parts else k + return k.strip().lower() + + +def relay_pro_dir(config_dir: Path) -> Path: + p = config_dir / "relay_pro" + p.mkdir(parents=True, exist_ok=True) + return p + + +def _safe_channel_file_id(channel_id: str) -> str: + s = re.sub(r"[^a-zA-Z0-9_-]", "_", (channel_id or "").strip()) + return s or "default" + + +def url_config_path_for_channel(config_dir: Path, channel_id: str) -> Path: + return relay_pro_dir(config_dir) / f"url_config.{_safe_channel_file_id(channel_id)}.ini" + + +def channels_json_path(config_dir: Path) -> Path: + return config_dir / "youtube_channels.json" + + +def ensure_default_relay_config(config_dir: Path) -> None: + """若缺少 youtube_channels.json 或 channels 为空,则写入默认 ch_default 与 url_config(便于独立运行 uvicorn)。""" + config_dir = Path(config_dir) + config_dir.mkdir(parents=True, exist_ok=True) + relay_pro_dir(config_dir) + p = channels_json_path(config_dir) + need_write = False + if not p.exists(): + need_write = True + else: + try: + raw = json.loads(p.read_text(encoding="utf-8")) + except Exception: + need_write = True + raw = None + else: + arr = raw.get("channels") if isinstance(raw, dict) else None + if not isinstance(arr, list) or len(arr) == 0: + need_write = True + if not need_write: + return + default_doc: dict[str, Any] = { + "version": 1, + "autoManaged": True, + "channels": [ + { + "id": "ch_default", + "name": "默认线路", + "douyinUrl": "", + "youtubeKey": "", + } + ], + } + p.write_text(json.dumps(default_doc, ensure_ascii=False, indent=2), encoding="utf-8") + uc = url_config_path_for_channel(config_dir, "ch_default") + if not uc.exists(): + uc.write_text( + "# 软件已自动创建。每行一个抖音直播间地址,例如:\n# https://live.douyin.com/123456789\n\n", + encoding="utf-8", + ) + + +def load_channels_document(config_dir: Path) -> tuple[dict[str, Any] | None, list[str]]: + p = channels_json_path(config_dir) + if not p.exists(): + return None, ["缺少 config/youtube_channels.json,请先添加(见 youtube_channels.example.json)"] + try: + raw = json.loads(p.read_text(encoding="utf-8")) + except Exception as e: + return None, [f"youtube_channels.json 解析失败: {e}"] + if not isinstance(raw, dict): + return None, ["youtube_channels.json 格式错误"] + arr = raw.get("channels") + if not isinstance(arr, list): + return None, ["youtube_channels.json 缺少 channels 数组"] + return raw, [] + + +def ensure_channel_ids(config_dir: Path, raw: dict[str, Any]) -> bool: + """为缺 id 的条目补 ch_0, ch_1 并写回文件。返回是否写回。""" + arr = raw.get("channels") + if not isinstance(arr, list): + return False + changed = False + for i, item in enumerate(arr): + if not isinstance(item, dict): + continue + cid = str(item.get("id") or "").strip() + if not cid: + item["id"] = f"ch_{i}" + changed = True + if changed: + channels_json_path(config_dir).write_text(json.dumps(raw, ensure_ascii=False, indent=2), encoding="utf-8") + return changed + + +def list_channels_for_pro(config_dir: Path) -> dict[str, Any]: + doc, errs = load_channels_document(config_dir) + if doc is None: + return {"ok": False, "error": errs[0] if errs else "未知错误", "channels": []} + ensure_channel_ids(config_dir, doc) + doc, _ = load_channels_document(config_dir) + assert doc is not None + arr = doc.get("channels") + out: list[dict[str, Any]] = [] + if isinstance(arr, list): + for item in arr: + if not isinstance(item, dict): + continue + cid = str(item.get("id") or "").strip() + name = str(item.get("name") or "").strip() or "未命名" + durl = str(item.get("douyinUrl") or item.get("douyin_url") or "").strip() + key = str(item.get("youtubeKey") or item.get("key") or "").strip() + if not cid: + continue + kp = _mask_key_preview(key) + out.append( + { + "id": cid, + "name": name, + "douyinUrl": durl, + "keyPreview": kp, + } + ) + return {"ok": True, "channels": out} + + +def _mask_key_preview(key: str) -> str: + k = (key or "").strip() + if not k: + return "" + if len(k) <= 8: + return "****" + return f"{k[:4]}…{k[-4:]}" + + +def get_channel_by_id(config_dir: Path, channel_id: str) -> dict[str, Any] | None: + doc, _ = load_channels_document(config_dir) + if doc is None: + return None + arr = doc.get("channels") + if not isinstance(arr, list): + return None + for item in arr: + if not isinstance(item, dict): + continue + if str(item.get("id") or "").strip() == channel_id: + return item + return None + + +def get_channel_detail(config_dir: Path, channel_id: str) -> dict[str, Any] | None: + ch = get_channel_by_id(config_dir, channel_id) + if not ch: + return None + return { + "id": str(ch.get("id") or "").strip(), + "name": str(ch.get("name") or "").strip() or "未命名", + "douyinUrl": str(ch.get("douyinUrl") or ch.get("douyin_url") or "").strip(), + "youtubeKey": str(ch.get("youtubeKey") or ch.get("key") or "").strip(), + } + + +def update_channel_key(config_dir: Path, channel_id: str, new_key: str) -> tuple[bool, str]: + doc, errs = load_channels_document(config_dir) + if doc is None: + return False, errs[0] if errs else "无法读取配置" + arr = doc.get("channels") + if not isinstance(arr, list): + return False, "channels 无效" + nk = _norm_stream_key(new_key) + if not nk: + return False, "密钥不能为空" + for item in arr: + if not isinstance(item, dict): + continue + oid = str(item.get("id") or "").strip() + if oid == channel_id: + continue + ok = _norm_stream_key(str(item.get("youtubeKey") or item.get("key") or "")) + if ok and ok == nk: + return False, "禁止多条线路共用同一 YouTube 串流密钥" + for item in arr: + if not isinstance(item, dict): + continue + if str(item.get("id") or "").strip() != channel_id: + continue + item["youtubeKey"] = new_key.strip() + channels_json_path(config_dir).write_text(json.dumps(doc, ensure_ascii=False, indent=2), encoding="utf-8") + return True, "已保存" + return False, "未找到该频道" + + +def extract_url_lines_from_ini(text: str) -> list[str]: + lines: list[str] = [] + for raw in text.splitlines(): + t = raw.strip() + if not t or t.startswith("#"): + continue + if "," in t: + t = t.split(",")[-1].strip() + if "douyin" in t.lower() or "v.douyin.com" in t.lower() or "live.douyin.com" in t.lower(): + lines.append(t) + return lines + + +def collect_normalized_urls_from_all_other_channels( + config_dir: Path, exclude_channel_id: str +) -> dict[str, set[str]]: + """channel_id -> set of normalized douyin urls from their url_config file.""" + doc, _ = load_channels_document(config_dir) + out: dict[str, set[str]] = {} + if doc is None: + return out + arr = doc.get("channels") + if not isinstance(arr, list): + return out + for item in arr: + if not isinstance(item, dict): + continue + cid = str(item.get("id") or "").strip() + if not cid or cid == exclude_channel_id: + continue + p = url_config_path_for_channel(config_dir, cid) + if not p.exists(): + out[cid] = set() + continue + try: + text = p.read_text(encoding="utf-8", errors="ignore") + except Exception: + out[cid] = set() + continue + s: set[str] = set() + for line in extract_url_lines_from_ini(text): + nd = _norm_douyin_url(line) + if nd: + s.add(nd) + out[cid] = s + return out + + +def validate_url_config_no_cross_duplicate( + config_dir: Path, channel_id: str, new_content: str +) -> list[str]: + """保存本频道 URL 列表时,与其它频道的 URL 去重(禁止同一抖音直播间出现在多条线路)。""" + errors: list[str] = [] + mine: set[str] = set() + for line in extract_url_lines_from_ini(new_content): + nd = _norm_douyin_url(line) + if nd: + mine.add(nd) + others = collect_normalized_urls_from_all_other_channels(config_dir, channel_id) + for ocid, oset in others.items(): + inter = mine & oset + if inter: + errors.append( + f"与频道「{ocid}」的地址列表冲突:以下抖音链接已出现在其它线路(禁止重复):" + + ";".join(list(inter)[:5]) + ) + return errors + + +def read_url_config_for_channel(config_dir: Path, channel_id: str) -> str: + p = url_config_path_for_channel(config_dir, channel_id) + if not p.exists(): + return "# 每行一个抖音直播间地址\n" + try: + return p.read_text(encoding="utf-8") + except Exception: + return "# 读取失败\n" + + +def write_url_config_for_channel(config_dir: Path, channel_id: str, content: str) -> tuple[bool, str]: + errs = validate_url_config_no_cross_duplicate(config_dir, channel_id, content) + if errs: + return False, errs[0] + p = url_config_path_for_channel(config_dir, channel_id) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(content, encoding="utf-8") + return True, "保存成功" + + +def write_youtube_ini_single_key(config_dir: Path, key: str) -> None: + """写入单路 youtube.ini([youtube] key=)。""" + lines = ["[youtube]", f"key = {key.strip()}", ""] + (config_dir / "youtube.ini").write_text("\n".join(lines), encoding="utf-8") + + +def sync_channel_to_legacy_files(config_dir: Path, channel_id: str) -> tuple[bool, str]: + """将选中频道的密钥与 URL 列表同步到 youtube.ini + URL_config.ini,供当前单进程 youtube 脚本使用。""" + d = get_channel_detail(config_dir, channel_id) + if not d: + return False, "频道不存在" + key = d.get("youtubeKey") or "" + if not str(key).strip(): + return False, "该频道未配置 youtubeKey" + url_path = url_config_path_for_channel(config_dir, channel_id) + if not url_path.exists(): + return False, f"缺少该频道的地址文件:{url_path.name}" + try: + url_body = url_path.read_text(encoding="utf-8") + except Exception as e: + return False, f"读取地址文件失败: {e}" + write_youtube_ini_single_key(config_dir, str(key)) + (config_dir / "URL_config.ini").write_text(url_body, encoding="utf-8") + return True, "已同步到 youtube.ini 与 URL_config.ini" + + +def parse_relay_meta_lines(log_text: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for line in log_text.splitlines(): + if "[RELAY_META]" not in line: + continue + idx = line.find("{") + if idx < 0: + continue + try: + j = json.loads(line[idx:]) + if isinstance(j, dict): + out.append(j) + except Exception: + continue + return out + + +async def build_relay_live_status( + config_dir: Path, + base_dir: Path, + get_process_info: Callable[[str], Awaitable[dict[str, Any]]], + read_log_tail: Callable[[str, int], Awaitable[str]], + youtube_pm2_name: str, +) -> dict[str, Any]: + """工作台 Pro:解析日志 RELAY_META + 进程状态,匹配 youtube_channels.json 频道名。""" + ch_doc = list_channels_for_pro(config_dir) + id_to_name = {c["id"]: c["name"] for c in ch_doc.get("channels") or []} + try: + info = await get_process_info(youtube_pm2_name) + except Exception: + info = {"process_status": "unknown", "out_path": "", "err_path": ""} + st = str(info.get("process_status") or "unknown") + online = st == "online" + out_path = str(info.get("out_path") or "") + err_path = str(info.get("err_path") or "") + recent = "" + try: + if out_path: + recent += await read_log_tail(out_path, 400) + if err_path: + recent += "\n" + await read_log_tail(err_path, 400) + except Exception: + recent = recent or "" + metas = parse_relay_meta_lines(recent) + last = metas[-1] if metas else None + rows: list[dict[str, Any]] = [] + suffix = str(last.get("youtubeKeySuffix") or "").strip().lower() if last else "" + matched_id, matched_name = _match_channel_by_key_suffix(config_dir, suffix) + if last: + ts = last.get("ts") + duration_sec: float | None = None + if isinstance(ts, (int, float)): + duration_sec = max(0.0, time.time() - float(ts)) + rows.append( + { + "channelId": matched_id or None, + "channelName": matched_name or id_to_name.get(matched_id, "") or "当前推流", + "anchor": last.get("anchor"), + "douyinHint": last.get("douyinHint"), + "streamTitle": last.get("streamTitle"), + "youtubeKeySuffix": last.get("youtubeKeySuffix"), + "durationSec": duration_sec, + "processOnline": online, + } + ) + return { + "youtubeProcess": youtube_pm2_name, + "processStatus": st, + "processOnline": online, + "relayRows": rows, + "lastRelayMeta": last, + } + + +def _match_channel_by_key_suffix(config_dir: Path, suffix: str) -> tuple[str, str]: + """用 RELAY_META 的 youtubeKeySuffix 与 youtube_channels.json 中密钥比对。""" + if not suffix: + return "", "" + doc, _ = load_channels_document(config_dir) + if not doc or not isinstance(doc.get("channels"), list): + return "", "" + for item in doc["channels"]: + if not isinstance(item, dict): + continue + k = str(item.get("youtubeKey") or item.get("key") or "").strip() + if not k: + continue + if k.startswith("rtmp://") or k.startswith("rtmps://"): + parts = k.rstrip("/").split("/") + k = parts[-1] if parts else k + tail = k[-8:].lower() if len(k) >= 8 else k.lower() + if suffix == tail or (len(suffix) >= 4 and tail.endswith(suffix)): + return str(item.get("id") or ""), str(item.get("name") or "") + return "", "" + + +def first_youtube_pm2_name(script_entries: list[dict[str, Any]]) -> str: + for e in script_entries: + script = str(e.get("script") or "") + if script.endswith(".py") and Path(script).stem.startswith("youtube"): + return Path(script).stem + return "youtube2" diff --git a/web2_supervisor.py b/web2_supervisor.py new file mode 100644 index 0000000..7feff3a --- /dev/null +++ b/web2_supervisor.py @@ -0,0 +1,323 @@ +""" +进程编排:默认使用内置 Supervisor(Windows / 免 PM2)。 +设置环境变量 WEB2_SUPERVISOR=pm2 时使用 PM2(与旧版 Linux 工作流兼容)。 +""" +from __future__ import annotations + +import asyncio +import json +import os +import re +import shutil +import sys +from pathlib import Path +from typing import Any, Callable, Optional + +import psutil + +from web2_proxy_config import merge_child_env + + +def use_builtin() -> bool: + return os.environ.get("WEB2_SUPERVISOR", "builtin").strip().lower() != "pm2" + + +def strip_ansi_codes(text: str) -> str: + return re.sub(r"\x1b\[[0-9;]*m", "", text) + + +class BuiltinSupervisor: + """用本地 PID + 日志文件管理子进程,不依赖 Node/PM2。""" + + def __init__(self, base_dir: Path, console_script: str, script_resolver: Callable[[str], Optional[str]]): + self.base_dir = base_dir.resolve() + self.console_script = console_script + self.console_stem = Path(console_script).stem + self._script_for = script_resolver + self.log_dir = self.base_dir / ".pm2" / "logs" + self.log_dir.mkdir(parents=True, exist_ok=True) + self.state_path = self.base_dir / ".pm2" / "supervisor_state.json" + self._io_lock = asyncio.Lock() + + def _read_state(self) -> dict[str, Any]: + if not self.state_path.exists(): + return {} + try: + return json.loads(self.state_path.read_text(encoding="utf-8")) + except Exception: + return {} + + def _write_state(self, state: dict[str, Any]) -> None: + self.state_path.parent.mkdir(parents=True, exist_ok=True) + self.state_path.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") + + def _pid_alive(self, pid: int | None) -> bool: + if pid is None or pid <= 0: + return False + try: + return psutil.Process(pid).is_running() + except (psutil.NoSuchProcess, psutil.AccessDenied): + return False + + def _log_paths(self, name: str) -> tuple[Path, Path]: + out_p = self.log_dir / f"{name}-out.log" + err_p = self.log_dir / f"{name}-error.log" + return out_p, err_p + + def _build_cmd(self, script: str) -> Optional[list[str]]: + path = self.base_dir / script + if not path.is_file(): + return None + ext = path.suffix.lower() + if ext == ".py": + return [sys.executable, str(path)] + if ext == ".bat": + comspec = os.environ.get("COMSPEC") or "cmd.exe" + return [comspec, "/c", str(path)] + if ext == ".sh": + bash = shutil.which("bash") or shutil.which("sh") + if not bash: + return None + return [bash, str(path)] + return None + + async def start(self, process_name: str) -> str: + if process_name == self.console_stem: + return "控制台已由本程序托管,无需单独启动。" + + script = self._script_for(process_name) + if not script: + return f"未知进程名: {process_name}" + + argv = self._build_cmd(script) + if argv is None: + path = self.base_dir / script + if not path.is_file(): + return f"脚本不存在: {script}" + if script.endswith(".sh"): + return "未找到 bash/sh,无法运行 .sh(可改用 obs*.bat 或安装 Git for Windows)" + return f"无法构建启动命令: {script}" + + async with self._io_lock: + state = self._read_state() + entry = state.get(process_name, {}) + pid = entry.get("pid") + if self._pid_alive(pid): + return f"进程已在运行中(PID {pid})。" + + out_p, err_p = self._log_paths(process_name) + out_p.parent.mkdir(parents=True, exist_ok=True) + out_f = open(out_p, "ab") + err_f = open(err_p, "ab") + try: + proc = await asyncio.create_subprocess_exec( + *argv, + cwd=str(self.base_dir), + stdout=out_f, + stderr=err_f, + creationflags=0, + env=merge_child_env(), + ) + except Exception as e: + out_f.close() + err_f.close() + return f"启动失败: {e}" + out_f.close() + err_f.close() + + pid = proc.pid + state[process_name] = { + "pid": pid, + "script": script, + "out_log": str(out_p), + "err_log": str(err_p), + } + self._write_state(state) + return f"已启动 {script}(PID {pid}),日志见 .pm2/logs" + + async def stop(self, process_name: str) -> str: + if process_name == self.console_stem: + return "请关闭桌面应用窗口以结束控制台。" + + async with self._io_lock: + state = self._read_state() + entry = state.get(process_name, {}) + pid = entry.get("pid") + + if not self._pid_alive(pid): + state.pop(process_name, None) + self._write_state(state) + return "进程未在运行(或已退出)。" + + try: + parent = psutil.Process(pid) + children = parent.children(recursive=True) + for c in children: + try: + c.terminate() + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + try: + parent.terminate() + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + gone, alive = psutil.wait_procs([parent] + children, timeout=5) + for p in alive: + try: + p.kill() + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + except psutil.NoSuchProcess: + pass + + state.pop(process_name, None) + self._write_state(state) + return f"已停止进程(原 PID {pid})。" + + async def restart(self, process_name: str) -> str: + if process_name == self.console_stem: + return "控制台由桌面应用托管,请关闭窗口后重新打开应用。" + out = await self.stop(process_name) + await asyncio.sleep(0.5) + out2 = await self.start(process_name) + return f"{out}\n{out2}" + + async def full_status_text(self) -> str: + lines = [ + "┌──────────────────┬────────────┬────────────────────────────────────────┐", + "│ 进程名 │ 状态 │ 说明 │", + "├──────────────────┼────────────┼────────────────────────────────────────┤", + ] + async with self._io_lock: + state = self._read_state() + lines.append( + f"│ {self.console_stem:<16} │ {'online':<10} │ 控制台(本机 uvicorn) │" + ) + for name, entry in sorted(state.items()): + pid = entry.get("pid") + alive = self._pid_alive(pid) + st = "online" if alive else "stopped" + scr = (entry.get("script") or "")[:38] + lines.append(f"│ {name:<16} │ {st:<10} │ {scr:<38} │") + lines.append( + "└──────────────────┴────────────┴────────────────────────────────────────┘" + ) + return "\n".join(lines) + + async def get_process_info(self, process_name: str) -> dict[str, Any]: + if process_name == self.console_stem: + out_p, err_p = self._log_paths(process_name) + return { + "process_status": "online", + "out_path": str(out_p), + "err_path": str(err_p), + } + + async with self._io_lock: + state = self._read_state() + entry = state.get(process_name, {}) + pid = entry.get("pid") + out_path = entry.get("out_log") + err_path = entry.get("err_log") + + if not out_path: + out_path = str(self._log_paths(process_name)[0]) + if not err_path: + err_path = str(self._log_paths(process_name)[1]) + + if not self._pid_alive(pid): + return { + "process_status": "stopped" if entry else "not_found", + "out_path": out_path, + "err_path": err_path, + } + + return { + "process_status": "online", + "out_path": out_path, + "err_path": err_path, + } + + +async def run_pm2_shell(cmd: str, timeout: float = 15.0) -> str: + process = await asyncio.create_subprocess_shell( + f"pm2 {cmd}", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + try: + stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout) + result = (stdout or stderr).decode(errors="ignore").strip() + return result if result else "命令执行完成(无输出)" + except asyncio.TimeoutError: + process.kill() + await process.wait() + return "ERROR: PM2 命令超时,已强制终止" + + +async def force_kill_ffmpeg_cross_platform(process_name: str) -> None: + """按命令行匹配业务进程名,结束相关 ffmpeg。""" + + def _sync_kill() -> None: + for proc in psutil.process_iter(["pid", "name", "cmdline"]): + try: + name = (proc.info.get("name") or "").lower() + if "ffmpeg" not in name: + continue + cmdline = proc.info.get("cmdline") or [] + flat = " ".join(str(x) for x in cmdline) + if process_name and process_name not in flat: + continue + p = psutil.Process(proc.info["pid"]) + p.kill() + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + + await asyncio.to_thread(_sync_kill) + await asyncio.sleep(0.35) + + +async def get_process_info_pm2( + process: str, + default_log_dir: Path, + strip_fn: Callable[[str], str], +) -> dict[str, Any]: + describe_raw = await run_pm2_shell(f"describe {process}", timeout=10.0) + describe_clean = strip_fn(describe_raw) + + if any(x in describe_clean.lower() for x in ["no such process", "unknown process", "not found"]) or not describe_clean.strip(): + return { + "process_status": "not_found", + "out_path": None, + "err_path": None, + } + + status = "unknown" + out_path = None + err_path = None + + for line in describe_raw.splitlines(): + line_clean = strip_fn(line) + if line_clean.count("│") < 2: + continue + parts = [p.strip() for p in line_clean.split("│") if p.strip()] + if len(parts) >= 2: + key = parts[0].lower() + value = parts[1] + if key == "status": + status = value.lower() + elif key == "out_log_path": + out_path = value + elif key == "err_log_path": + err_path = value + + if not out_path: + out_path = str(default_log_dir / f"{process}-out.log") + if not err_path: + err_path = str(default_log_dir / f"{process}-error.log") + + return { + "process_status": status, + "out_path": out_path, + "err_path": err_path, + } diff --git a/web2_system_metrics.py b/web2_system_metrics.py new file mode 100644 index 0000000..ad815c2 --- /dev/null +++ b/web2_system_metrics.py @@ -0,0 +1,129 @@ +""" +系统与网卡指标(供仪表盘轮询)。下行/上行为全机网卡合计速率(估算)。 +""" +from __future__ import annotations + +import time +from typing import Any, Optional + +import psutil + +_last_net_time: float = 0.0 +_last_net: Optional[dict[str, tuple[int, int]]] = None # name -> (sent, recv) + + +def _net_rates_mbps() -> tuple[Optional[float], Optional[float], dict[str, Any]]: + """返回 (上行 Mbps, 下行 Mbps, 原始片段)。""" + global _last_net_time, _last_net + now = time.monotonic() + per = psutil.net_io_counters(pernic=True) + cur: dict[str, tuple[int, int]] = {k: (v.bytes_sent, v.bytes_recv) for k, v in per.items()} + detail: dict[str, Any] = {} + up_mbps = down_mbps = None + if _last_net is not None and _last_net_time > 0: + dt = now - _last_net_time + if dt > 0.05: + ds = dr = 0 + for name, (sent, recv) in cur.items(): + if name in _last_net: + ps0, pr0 = _last_net[name] + ds += max(0, sent - ps0) + dr += max(0, recv - pr0) + up_mbps = round((ds * 8) / dt / 1_000_000, 3) + down_mbps = round((dr * 8) / dt / 1_000_000, 3) + detail["delta_seconds"] = round(dt, 3) + _last_net = cur + _last_net_time = now + for name, (sent, recv) in cur.items(): + detail.setdefault("interfaces", {})[name] = { + "bytes_sent": sent, + "bytes_recv": recv, + } + return up_mbps, down_mbps, detail + + +def snapshot() -> dict[str, Any]: + try: + psutil.cpu_percent(interval=None) + cpu = psutil.cpu_percent(interval=0.15) + except Exception: + cpu = None + try: + vm = psutil.virtual_memory() + mem = { + "percent": vm.percent, + "used": vm.used, + "total": vm.total, + "available": vm.available, + } + except Exception: + mem = {} + try: + sm = psutil.swap_memory() + swap = {"percent": sm.percent, "used": sm.used, "total": sm.total} + except Exception: + swap = {} + + disks = [] + try: + for part in psutil.disk_partitions(all=False): + try: + usage = psutil.disk_usage(part.mountpoint) + disks.append( + { + "device": part.device, + "mountpoint": part.mountpoint, + "fstype": part.fstype, + "percent": usage.percent, + "free": usage.free, + "total": usage.total, + } + ) + except (PermissionError, OSError): + continue + except Exception: + pass + + up_mbps, down_mbps, net_detail = _net_rates_mbps() + + boot = None + try: + boot = int(psutil.boot_time()) + except Exception: + pass + + ffmpeg_procs = [] + try: + for p in psutil.process_iter(["pid", "name", "memory_info"]): + try: + name = (p.info.get("name") or "").lower() + if "ffmpeg" not in name: + continue + mi = p.info.get("memory_info") + ffmpeg_procs.append( + { + "pid": p.info["pid"], + "name": p.info.get("name"), + "rss": mi.rss if mi else 0, + } + ) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + except Exception: + pass + + return { + "ts": time.time(), + "cpu_percent": cpu, + "cpu_count_logical": psutil.cpu_count(logical=True), + "memory": mem, + "swap": swap, + "disks": disks, + "network": { + "up_mbps": up_mbps, + "down_mbps": down_mbps, + "detail": net_detail, + }, + "boot_time_unix": boot, + "ffmpeg_processes": ffmpeg_procs[:24], + } diff --git a/web2_wechat_chatbot.py b/web2_wechat_chatbot.py new file mode 100644 index 0000000..a602dc4 --- /dev/null +++ b/web2_wechat_chatbot.py @@ -0,0 +1,292 @@ +""" +微信 Chatbot 桥接:与 openclaw-wechat 使用的代理 HTTP API 对齐(见 integrations/openclaw-wechat)。 +需自备 apiKey 与 proxyUrl(第三方代理服务),本模块提供扫码登录、状态查询与发消息等 REST,供 Web / CLI / Agent 调用。 +""" +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Optional + +import httpx +from fastapi import Request +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field + +CONFIG_NAME = "wechat_chatbot.json" +STATE_NAME = "wechat_chatbot.state.json" + + +def _config_path(base: Path) -> Path: + return base / "config" / CONFIG_NAME + + +def _state_path(base: Path) -> Path: + return base / "config" / STATE_NAME + + +def load_wechat_chatbot_config(base_dir: Path) -> dict[str, Any]: + p = _config_path(base_dir) + if not p.exists(): + return {} + try: + return json.loads(p.read_text(encoding="utf-8")) + except Exception: + return {} + + +def load_wechat_state(base_dir: Path) -> dict[str, Any]: + p = _state_path(base_dir) + if not p.exists(): + return {} + try: + return json.loads(p.read_text(encoding="utf-8")) + except Exception: + return {} + + +def save_wechat_state(base_dir: Path, state: dict[str, Any]) -> None: + p = _state_path(base_dir) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8") + + +def mask_api_key(key: str) -> str: + k = (key or "").strip() + if len(k) <= 8: + return "****" if k else "" + return f"{k[:4]}…{k[-4:]}" + + +class WechatProxyClient: + """与 freestylefly/openclaw-wechat 中 ProxyClient 请求格式一致。""" + + def __init__( + self, + api_key: str, + base_url: str, + account_id: str = "default", + timeout: float = 60.0, + ): + if not api_key or not api_key.strip(): + raise ValueError("缺少 apiKey") + if not base_url or not base_url.strip(): + raise ValueError("缺少 proxyUrl") + self.api_key = api_key.strip() + self.base_url = base_url.strip().rstrip("/") + self.account_id = account_id.strip() or "default" + self.timeout = timeout + + def _request(self, endpoint: str, data: Optional[dict[str, Any]] = None) -> Any: + url = f"{self.base_url}{endpoint}" + headers = { + "Content-Type": "application/json", + "X-API-Key": self.api_key, + "X-Account-ID": self.account_id, + } + with httpx.Client(timeout=self.timeout) as client: + r = client.post(url, headers=headers, json=data) + try: + result = r.json() + except Exception: + raise RuntimeError(f"HTTP {r.status_code}: {r.text[:500]}") from None + + if not r.is_success: + raise RuntimeError(result.get("error") or result.get("message") or f"HTTP {r.status_code}") + + code = result.get("code") + if code in ("1000", "1001", "1002"): + d = result.get("data") + return d if isinstance(d, (dict, list)) else result + if code and str(code) != "1000": + raise RuntimeError(result.get("message") or str(result)) + d = result.get("data") + return d if d is not None else result + + def get_status(self) -> dict[str, Any]: + out = self._request("/v1/account/status") + if not isinstance(out, dict): + return {"raw": out} + return { + "valid": out.get("valid", True), + "wcId": out.get("wcId"), + "isLoggedIn": out.get("isLoggedIn", False), + "nickName": out.get("nickName"), + "tier": out.get("tier"), + "quota": out.get("quota"), + "error": out.get("error"), + } + + def get_qr_code(self, device_type: str = "ipad", proxy: str = "10") -> dict[str, str]: + out = self._request( + "/v1/iPadLogin", + {"deviceType": device_type or "ipad", "proxy": proxy or "10"}, + ) + if not isinstance(out, dict): + raise RuntimeError("代理返回格式异常") + w_id = out.get("wId") or out.get("w_id") + qr_url = out.get("qrCodeUrl") or out.get("qr_code_url") + if not w_id or not qr_url: + raise RuntimeError("代理未返回二维码信息") + return {"wId": str(w_id), "qrCodeUrl": str(qr_url)} + + def check_login(self, w_id: str) -> dict[str, Any]: + out = self._request("/v1/getIPadLoginInfo", {"wId": w_id}) + if not isinstance(out, dict): + return {"status": "waiting"} + st = out.get("status") + if st == "logged_in": + return { + "status": "logged_in", + "wcId": out.get("wcId"), + "nickName": out.get("nickName"), + "headUrl": out.get("headUrl"), + } + if st == "need_verify": + return {"status": "need_verify", "verifyUrl": out.get("verifyUrl")} + return {"status": "waiting"} + + def send_text(self, target_id: str, content: str) -> dict[str, Any]: + out = self._request("/v1/sendText", {"wcId": target_id, "content": content}) + return out if isinstance(out, dict) else {"result": out} + + def register_webhook(self, webhook_url: str) -> None: + self._request("/v1/webhook/register", {"webhookUrl": webhook_url}) + + +def build_client(cfg: dict[str, Any]) -> WechatProxyClient: + return WechatProxyClient( + api_key=str(cfg.get("apiKey") or ""), + base_url=str(cfg.get("proxyUrl") or ""), + account_id=str(cfg.get("accountId") or "default"), + ) + + +class WechatConfigBody(BaseModel): + apiKey: Optional[str] = None + proxyUrl: Optional[str] = None + deviceType: Optional[str] = None + proxy: Optional[str] = None + webhookHost: Optional[str] = None + webhookPort: Optional[int] = None + webhookPath: Optional[str] = None + accountId: Optional[str] = None + + +class WechatSendBody(BaseModel): + targetId: str = Field(..., description="接收方 wxid 或群 id") + text: str = Field(..., description="文本内容") + + +class WechatWebhookBody(BaseModel): + webhookUrl: str + + +def register_wechat_chatbot_routes(app, base_dir: Path) -> None: + """在 FastAPI app 上注册 /chatbot/wechat/* 路由。""" + + @app.get("/chatbot/wechat/config") + async def wechat_config_get(): + cfg = load_wechat_chatbot_config(base_dir) + st = load_wechat_state(base_dir) + key = str(cfg.get("apiKey") or "") + return { + "configured": bool(cfg.get("proxyUrl") and key), + "proxyUrl": cfg.get("proxyUrl") or "", + "apiKeyMasked": mask_api_key(key), + "deviceType": cfg.get("deviceType") or "ipad", + "proxy": cfg.get("proxy") or "10", + "webhookHost": cfg.get("webhookHost") or "", + "webhookPort": int(cfg.get("webhookPort") or 18790), + "webhookPath": cfg.get("webhookPath") or "/webhook/wechat", + "accountId": cfg.get("accountId") or "default", + "session": { + "wcId": st.get("wcId"), + "nickName": st.get("nickName"), + }, + } + + @app.post("/chatbot/wechat/config") + async def wechat_config_post(body: WechatConfigBody): + cfg = load_wechat_chatbot_config(base_dir) + d = body.model_dump(exclude_none=True) + for k, v in d.items(): + cfg[k] = v + out_path = _config_path(base_dir) + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8") + return {"ok": True} + + @app.get("/chatbot/wechat/proxy/status") + async def wechat_proxy_status(): + cfg = load_wechat_chatbot_config(base_dir) + if not cfg.get("apiKey") or not cfg.get("proxyUrl"): + return JSONResponse({"error": "未配置 apiKey 或 proxyUrl"}, status_code=400) + try: + c = build_client(cfg) + return c.get_status() + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=502) + + @app.post("/chatbot/wechat/qr/start") + async def wechat_qr_start(): + cfg = load_wechat_chatbot_config(base_dir) + if not cfg.get("apiKey") or not cfg.get("proxyUrl"): + return JSONResponse({"error": "未配置 apiKey 或 proxyUrl"}, status_code=400) + try: + c = build_client(cfg) + dt = str(cfg.get("deviceType") or "ipad") + px = str(cfg.get("proxy") or "10") + return c.get_qr_code(device_type=dt, proxy=px) + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=502) + + @app.post("/chatbot/wechat/login/check") + async def wechat_login_check(request: Request): + try: + body = await request.json() + except Exception: + return JSONResponse({"error": "需要 JSON body: { \"wId\": \"...\" }"}, status_code=400) + w_id = (body or {}).get("wId") or (body or {}).get("w_id") + if not w_id or not isinstance(w_id, str): + return JSONResponse({"error": "缺少 wId"}, status_code=400) + cfg = load_wechat_chatbot_config(base_dir) + if not cfg.get("apiKey") or not cfg.get("proxyUrl"): + return JSONResponse({"error": "未配置 apiKey 或 proxyUrl"}, status_code=400) + try: + c = build_client(cfg) + r = c.check_login(w_id.strip()) + if r.get("status") == "logged_in" and r.get("wcId"): + save_wechat_state( + base_dir, + { + "wcId": r.get("wcId"), + "nickName": r.get("nickName"), + }, + ) + return r + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=502) + + @app.post("/chatbot/wechat/send") + async def wechat_send(body: WechatSendBody): + cfg = load_wechat_chatbot_config(base_dir) + if not cfg.get("apiKey") or not cfg.get("proxyUrl"): + return JSONResponse({"error": "未配置 apiKey 或 proxyUrl"}, status_code=400) + try: + c = build_client(cfg) + return c.send_text(body.targetId.strip(), body.text) + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=502) + + @app.post("/chatbot/wechat/webhook/register") + async def wechat_webhook_reg(body: WechatWebhookBody): + cfg = load_wechat_chatbot_config(base_dir) + if not cfg.get("apiKey") or not cfg.get("proxyUrl"): + return JSONResponse({"error": "未配置 apiKey 或 proxyUrl"}, status_code=400) + try: + c = build_client(cfg) + c.register_webhook(body.webhookUrl.strip()) + return {"ok": True} + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=502) diff --git a/web2_youtube_oauth.py b/web2_youtube_oauth.py new file mode 100644 index 0000000..3d0199a --- /dev/null +++ b/web2_youtube_oauth.py @@ -0,0 +1,239 @@ +""" +YouTube OAuth 2.0 + Live Streaming API:授权后拉取串流密钥并写入 config/youtube.ini。 +需在 Google Cloud Console 为本 OAuth 客户端添加「已授权的重定向 URI」: + http://127.0.0.1:8001/youtube/oauth/callback +(若本地服务端口不是 8001,请同时改 redirect_uri 与 uvicorn 端口。) +""" +from __future__ import annotations + +import json +import os +import time +from pathlib import Path +from typing import Any, Optional + +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import Flow +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +BASE_DIR = Path(__file__).resolve().parent +CONFIG_DIR = BASE_DIR / "config" +GOOGLE_OAUTH_JSON = CONFIG_DIR / "google_oauth.json" +TOKEN_JSON = CONFIG_DIR / "youtube_oauth_token.json" +YOUTUBE_INI = CONFIG_DIR / "youtube.ini" + +# 须与 Google 在换 token 时返回的 scope 集合一致,否则 oauthlib 会报 +# “Scope has changed from … to …”。include_granted_scopes 会合并历史授权,也易触发该错误。 +SCOPES = [ + "https://www.googleapis.com/auth/youtube", + "https://www.googleapis.com/auth/youtube.force-ssl", + "https://www.googleapis.com/auth/youtubepartner", +] + +# state -> (Flow, created_ts);回调必须用同一 Flow 实例换 token +_oauth_pending: dict[str, tuple[Flow, float]] = {} +STATE_TTL = 600.0 + + +def _cleanup_pending() -> None: + now = time.time() + dead = [s for s, (_, t) in _oauth_pending.items() if now - t > STATE_TTL] + for s in dead: + _oauth_pending.pop(s, None) + + +def load_client_config_dict() -> tuple[dict[str, Any], str]: + if not GOOGLE_OAUTH_JSON.exists(): + raise FileNotFoundError( + "缺少 config/google_oauth.json。请复制 config/google_oauth.example.json 并填入客户端 ID/Secret。" + ) + data = json.loads(GOOGLE_OAUTH_JSON.read_text(encoding="utf-8")) + redirect_uri = data.get("redirect_uri") or "http://127.0.0.1:8001/youtube/oauth/callback" + if "web" in data: + cfg = {"web": data["web"]} + elif "installed" in data: + inst = data["installed"] + cfg = { + "web": { + "client_id": inst["client_id"], + "client_secret": inst["client_secret"], + "auth_uri": inst.get("auth_uri", "https://accounts.google.com/o/oauth2/auth"), + "token_uri": inst.get("token_uri", "https://oauth2.googleapis.com/token"), + "redirect_uris": [redirect_uri], + } + } + else: + raise ValueError("google_oauth.json 需包含 web 或 installed 字段") + return cfg, redirect_uri + + +def build_flow() -> tuple[Flow, str]: + cfg, redirect_uri = load_client_config_dict() + flow = Flow.from_client_config(cfg, scopes=SCOPES, redirect_uri=redirect_uri) + return flow, redirect_uri + + +def oauth_authorization_url() -> tuple[str, str]: + import secrets + + _cleanup_pending() + flow, _ = build_flow() + state = secrets.token_urlsafe(32) + result = flow.authorization_url( + access_type="offline", + state=state, + prompt="consent", + ) + authorization_url = result[0] if isinstance(result, (list, tuple)) else result + _oauth_pending[state] = (flow, time.time()) + return authorization_url, state + + +def oauth_exchange_code(code: str, state: str) -> None: + _cleanup_pending() + if state not in _oauth_pending: + raise ValueError("授权会话无效或已过期,请在本软件中重新点击「Google 授权」。") + flow, t0 = _oauth_pending.pop(state) + if time.time() - t0 > STATE_TTL: + raise ValueError("授权已超时,请重新点击「Google 授权」。") + # oauthlib 要求令牌里的 scope 与客户端声明完全一致;Google 常返回多枚 scope 或与声明略有差异 + _prev_relax = os.environ.get("OAUTHLIB_RELAX_TOKEN_SCOPE") + os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1" + try: + flow.fetch_token(code=code) + finally: + if _prev_relax is None: + os.environ.pop("OAUTHLIB_RELAX_TOKEN_SCOPE", None) + else: + os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = _prev_relax + creds = flow.credentials + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + TOKEN_JSON.write_text(creds.to_json(), encoding="utf-8") + + +def load_credentials() -> Optional[Credentials]: + if not TOKEN_JSON.exists(): + return None + return Credentials.from_authorized_user_file(str(TOKEN_JSON), SCOPES) + + +def ensure_valid_credentials() -> Credentials: + creds = load_credentials() + if not creds: + raise RuntimeError("尚未完成 Google 授权,请先点击「Google 授权」。") + if creds.valid: + return creds + if creds.expired and creds.refresh_token: + creds.refresh(Request()) + TOKEN_JSON.write_text(creds.to_json(), encoding="utf-8") + return creds + raise RuntimeError("授权已失效且无 refresh_token,请重新授权(勾选同意离线访问)。") + + +def oauth_status() -> dict[str, Any]: + creds = load_credentials() + if not creds: + return {"connected": False, "has_refresh": False} + return { + "connected": True, + "has_refresh": bool(creds.refresh_token), + "expired": bool(creds.expired), + } + + +def _pick_ingestion(items: list[dict[str, Any]]) -> Optional[dict[str, Any]]: + for it in items: + cdn = it.get("cdn") or {} + ing = cdn.get("ingestionInfo") + if ing and ing.get("streamName"): + return ing + return None + + +def fetch_stream_key_and_write_ini() -> dict[str, Any]: + """ + 列出当前账号的直播流;若无则创建一路,将 streamName 写入 youtube.ini 的 key=。 + """ + creds = ensure_valid_credentials() + youtube = build("youtube", "v3", credentials=creds, cache_discovery=False) + try: + resp = youtube.liveStreams().list(part="snippet,cdn,status", mine=True, maxResults=10).execute() + except HttpError as e: + raise RuntimeError(f"YouTube API 错误: {e}") from e + + items = resp.get("items", []) + ing = _pick_ingestion(items) + + if not ing: + body = { + "snippet": {"title": "直播录制助手"}, + "cdn": { + "ingestionType": "rtmp", + "frameRate": "variable", + "resolution": "variable", + }, + } + try: + created = youtube.liveStreams().insert(part="snippet,cdn", body=body).execute() + except HttpError as e: + raise RuntimeError(f"创建直播流失败: {e}") from e + ing = (created.get("cdn") or {}).get("ingestionInfo") + if not ing or not ing.get("streamName"): + raise RuntimeError("已创建直播流但未返回串流密钥,请到 YouTube 工作室检查。") + + stream_name = str(ing["streamName"]) + write_youtube_key_to_ini(stream_name) + + rtmp = ing.get("ingestionAddress") or "rtmp://a.rtmp.youtube.com/live2" + rtmps = ing.get("rtmpsAddress") or "rtmps://a.rtmp.youtube.com/live2" + return { + "ok": True, + "stream_name": stream_name, + "ingestion_address": rtmp, + "rtmps_address": rtmps, + "message": "已写入 config/youtube.ini 的 key= 项。", + } + + +def write_youtube_key_to_ini(stream_key: str) -> None: + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + if not YOUTUBE_INI.exists(): + YOUTUBE_INI.write_text( + "[youtube]\n" + "# 由 Google 授权自动写入,也可手动修改\n" + f"key = {stream_key}\n", + encoding="utf-8-sig", + ) + return + + raw = YOUTUBE_INI.read_text(encoding="utf-8-sig") + lines = raw.splitlines() + out: list[str] = [] + replaced = False + for line in lines: + stripped = line.lstrip() + if stripped.startswith("#") or stripped.startswith(";"): + out.append(line) + continue + if "=" in line: + k = line.split("=", 1)[0].strip().lower() + if k == "key": + indent = line[: len(line) - len(line.lstrip())] + out.append(f"{indent}key = {stream_key}") + replaced = True + continue + out.append(line) + + if not replaced: + inserted = False + final: list[str] = [] + for line in out: + final.append(line) + if line.strip().lower() == "[youtube]" and not inserted: + final.append(f"key = {stream_key}") + inserted = True + out = final if inserted else out + ["", f"key = {stream_key}"] + + YOUTUBE_INI.write_text("\n".join(out) + ("\n" if out and not out[-1].endswith("\n") else ""), encoding="utf-8-sig") diff --git a/wechat_chatbot_cli.py b/wechat_chatbot_cli.py new file mode 100644 index 0000000..3181aea --- /dev/null +++ b/wechat_chatbot_cli.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +""" +微信 Chatbot HTTP API 的命令行客户端(供脚本与 Agent 调用)。 +环境变量:WEB2_API_BASE(默认 http://127.0.0.1:8001)、WEB2_API_TOKEN(若后端启用鉴权)。 + +示例: + python wechat_chatbot_cli.py config-get --json + python wechat_chatbot_cli.py proxy-status --json + python wechat_chatbot_cli.py qr-start --json + python wechat_chatbot_cli.py login-check --w-id --json + python wechat_chatbot_cli.py send --target wxid_xxx --text "hello" --json +""" +from __future__ import annotations + +import argparse +import json +import os +import sys + +import httpx + + +def base_url() -> str: + return os.environ.get("WEB2_API_BASE", "http://127.0.0.1:8001").rstrip("/") + + +def headers() -> dict[str, str]: + h = {"Content-Type": "application/json"} + tok = os.environ.get("WEB2_API_TOKEN", "").strip() + if tok: + h["Authorization"] = f"Bearer {tok}" + return h + + +def out(obj: object, as_json: bool) -> None: + if as_json: + print(json.dumps(obj, ensure_ascii=False, indent=2)) + else: + print(obj) + + +def main() -> None: + p = argparse.ArgumentParser(description="微信 Chatbot API CLI") + p.add_argument("--json", action="store_true", help="输出 JSON") + sub = p.add_subparsers(dest="cmd", required=True) + + sub.add_parser("config-get", help="GET /chatbot/wechat/config") + + pc = sub.add_parser("config-set", help="POST /chatbot/wechat/config(从 stdin 读 JSON 或文件)") + pc.add_argument("--file", "-f", help="JSON 文件路径") + + sub.add_parser("proxy-status", help="GET /chatbot/wechat/proxy/status") + + sub.add_parser("qr-start", help="POST /chatbot/wechat/qr/start") + + pl = sub.add_parser("login-check", help="POST /chatbot/wechat/login/check") + pl.add_argument("--w-id", required=True, dest="w_id") + + ps = sub.add_parser("send", help="POST /chatbot/wechat/send") + ps.add_argument("--target", required=True, help="对方 wxid 或群 id") + ps.add_argument("--text", required=True) + + pw = sub.add_parser("webhook-register", help="POST /chatbot/wechat/webhook/register") + pw.add_argument("--url", required=True, help="公网可访问的 webhook 完整 URL") + + args = p.parse_args() + bu = base_url() + h = headers() + + try: + with httpx.Client(timeout=120.0) as client: + if args.cmd == "config-get": + r = client.get(f"{bu}/chatbot/wechat/config", headers=h) + r.raise_for_status() + out(r.json(), args.json) + elif args.cmd == "config-set": + raw = sys.stdin.read() + if getattr(args, "file", None): + raw = open(args.file, encoding="utf-8").read() + body = json.loads(raw) + r = client.post(f"{bu}/chatbot/wechat/config", headers=h, json=body) + r.raise_for_status() + out(r.json(), args.json) + elif args.cmd == "proxy-status": + r = client.get(f"{bu}/chatbot/wechat/proxy/status", headers=h) + try: + out(r.json(), args.json) + except Exception: + print(r.text, file=sys.stderr) + sys.exit(1) + elif args.cmd == "qr-start": + r = client.post(f"{bu}/chatbot/wechat/qr/start", headers=h) + try: + out(r.json(), args.json) + except Exception: + print(r.text, file=sys.stderr) + sys.exit(1) + elif args.cmd == "login-check": + r = client.post( + f"{bu}/chatbot/wechat/login/check", + headers=h, + json={"wId": args.w_id}, + ) + try: + out(r.json(), args.json) + except Exception: + print(r.text, file=sys.stderr) + sys.exit(1) + elif args.cmd == "send": + r = client.post( + f"{bu}/chatbot/wechat/send", + headers=h, + json={"targetId": args.target, "text": args.text}, + ) + try: + out(r.json(), args.json) + except Exception: + print(r.text, file=sys.stderr) + sys.exit(1) + elif args.cmd == "webhook-register": + r = client.post( + f"{bu}/chatbot/wechat/webhook/register", + headers=h, + json={"webhookUrl": args.url}, + ) + try: + out(r.json(), args.json) + except Exception: + print(r.text, file=sys.stderr) + sys.exit(1) + except httpx.HTTPStatusError as e: + print(str(e), file=sys.stderr) + try: + print(e.response.text, file=sys.stderr) + except Exception: + pass + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/youtube2.py b/youtube2.py new file mode 100644 index 0000000..9f39aeb --- /dev/null +++ b/youtube2.py @@ -0,0 +1,2205 @@ +# -*- encoding: utf-8 -*- + +""" +Author: Hmily +GitHub: https://github.com/ihmily +Date: 2023-07-17 23:52:05 +Update: 2025-10-23 19:48:05 +Copyright (c) 2023-2025 by Hmily, All Rights Reserved. +Function: Record live stream video. +""" +import asyncio +import os +import sys +import builtins +import subprocess +import signal +import threading +import time +import datetime +import re +import shutil +import random +import uuid +from pathlib import Path +import urllib.request +from urllib.error import URLError, HTTPError +from typing import Any +import configparser +import httpx +from src import spider, stream +from src.proxy import ProxyDetector +from src.utils import logger +from src import utils +from backup.msg_push import ( + dingtalk, xizhi, tg_bot, send_email, bark, ntfy, pushplus +) +from ffmpeg_install import ( + check_ffmpeg, ffmpeg_path, current_env_path +) + + + + + +version = "v4.0.7" +platforms = ("\n国内站点:抖音|快手|虎牙|斗鱼|YY|B站|小红书|bigo|blued|网易CC|千度热播|猫耳FM|Look|TwitCasting|百度|微博|" + "酷狗|花椒|流星|Acfun|畅聊|映客|音播|知乎|嗨秀|VV星球|17Live|浪Live|漂漂|六间房|乐嗨|花猫|淘宝|京东|咪咕|连接|来秀" + "\n海外站点:TikTok|SOOP|PandaTV|WinkTV|FlexTV|PopkonTV|TwitchTV|LiveMe|ShowRoom|CHZZK|Shopee|" + "Youtube|Faceit|Picarto") + +recording = set() +error_count = 0 +pre_max_request = 10 +max_request_lock = threading.Lock() +error_window = [] +error_window_size = 10 +error_threshold = 5 +monitoring = 0 +running_list = [] +url_tuples_list = [] +url_comments = [] +text_no_repeat_url = [] +create_var = locals() +first_start = True +exit_recording = False +need_update_line_list = [] +first_run = True +not_record_list = [] +start_display_time = datetime.datetime.now() +global_proxy = False +recording_time_list = {} +script_path = os.path.split(os.path.realpath(sys.argv[0]))[0] +config_file = f'{script_path}/config/config.ini' +url_config_file = f'{script_path}/config/URL_config.ini' +backup_dir = f'{script_path}/backup_config' +text_encoding = 'utf-8-sig' +rstr = r"[\/\\\:\*\??\"\<\>\|&#.。,, ~!· ]" +default_path = f'{script_path}/downloads' +os.makedirs(default_path, exist_ok=True) +file_update_lock = threading.Lock() +os_type = os.name +clear_command = "cls" if os_type == 'nt' else "clear" +color_obj = utils.Color() +os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path + + +def signal_handler(_signal, _frame): + sys.exit(0) + + +signal.signal(signal.SIGTERM, signal_handler) + + + +import shutil + + +# 全局变量(只初始化一次) +_last_lines_count = 0 + +def _clear_lines(n: int): + """向上删除 n 行(不闪烁)""" + if n <= 0: + return + print(f"\033[{n}A\033[K", end="", flush=True) # 上移 + 清行 + +def display_info() -> None: + global start_display_time, _last_lines_count + time.sleep(3) # 启动延迟 + + while True: + try: + lines = [] + now_str = datetime.datetime.now().strftime("%H:%M:%S") + + # 第一行:核心状态 + lines.append( + f"监测 {monitoring} | 并发 {max_request} | " + f"代理 {'开' if use_proxy else '关'} | " + f"分段 {'开' if split_video_by_time else '关'}{f':{split_time}s' if split_video_by_time else ''} | " + f"时间文件 {'开' if create_time_file else '关'} | " + f"质量 {video_record_quality} | 格式 {video_save_type} | " + f"错误 {error_count} | {now_str}" + ) + + # 第二行:录制中的直播 + if recording: + no_repeat = list(set(recording)) + recording_str = f"正在录制 {len(no_repeat)} 个: " + details = [] + now_dt = datetime.datetime.now() + for name in no_repeat[:10]: # 最多显示10个,防止爆屏 + if name not in recording_time_list: + continue + start_time, quality = recording_time_list[name] + duration = str(now_dt - start_time).split('.')[0] + details.append(f"{name.split()[-1]}[{quality} {duration}]") + recording_str += " | ".join(details) + if len(no_repeat) > 10: + recording_str += f" ... (+{len(no_repeat)-10})" + lines.append(recording_str) + else: + if monitoring == 0: + lines.append("没有正在监测的直播") + else: + lines.append(f"无录制任务 | 循环间隔 {delay_default}s") + + # 动态清屏 + 重绘(不闪烁!) + terminal_width, _ = shutil.get_terminal_size(fallback=(120, 24)) + display_lines = [] + for line in lines: + if len(line) > terminal_width - 5: + line = line[:terminal_width-8] + "..." + display_lines.append(line) + + # 删除旧内容 + 打印新内容 + if _last_lines_count > len(display_lines): + _clear_lines(_last_lines_count - 1) + print("\r" + display_lines[0]) + for line in display_lines[1:]: + print(line) + else: + print("\r" + display_lines[0], end="") + for line in display_lines[1:]: + print(f"\n{line}", end="") + + _last_lines_count = len(display_lines) + print(flush=True) # 强制刷新 + + time.sleep(5) + + except Exception as e: + # 避免崩溃后疯狂打印错误 + try: + print(f"\n[display_info 错误] {e}") + except: + pass + time.sleep(10) # 出错了也别刷屏 +def display_info2() -> None: + global start_display_time + time.sleep(5) + while True: + try: + sys.stdout.flush() + time.sleep(5) + if Path(sys.executable).name != 'pythonw.exe': + os.system(clear_command) + print(f"\r共监测{monitoring}个直播中", end=" | ") + print(f"同一时间访问网络的线程数: {max_request}", end=" | ") + print(f"是否开启代理录制: {'是' if use_proxy else '否'}", end=" | ") + if split_video_by_time: + print(f"录制分段开启: {split_time}秒", end=" | ") + else: + print("录制分段开启: 否", end=" | ") + if create_time_file: + print("是否生成时间文件: 是", end=" | ") + print(f"录制视频质量为: {video_record_quality}", end=" | ") + print(f"录制视频格式为: {video_save_type}", end=" | ") + print(f"目前瞬时错误数为: {error_count}", end=" | ") + now = time.strftime("%H:%M:%S", time.localtime()) + print(f"当前时间: {now}") + + if len(recording) == 0: + time.sleep(5) + if monitoring == 0: + print("\r没有正在监测和录制的直播") + else: + print(f"\r没有正在录制的直播 循环监测间隔时间:{delay_default}秒") + else: + now_time = datetime.datetime.now() + print("x" * 60) + no_repeat_recording = list(set(recording)) + print(f"正在录制{len(no_repeat_recording)}个直播: ") + for recording_live in no_repeat_recording: + rt, qa = recording_time_list[recording_live] + have_record_time = now_time - rt + print(f"{recording_live}[{qa}] 正在录制中 {str(have_record_time).split('.')[0]}") + + # print('\n本软件已运行:'+str(now_time - start_display_time).split('.')[0]) + print("x" * 60) + start_display_time = now_time + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + + +def update_file(file_path: str, old_str: str, new_str: str, start_str: str = None) -> str | None: + if old_str == new_str and start_str is None: + return old_str + with file_update_lock: + file_data = [] + with open(file_path, "r", encoding=text_encoding) as f: + try: + for text_line in f: + if old_str in text_line: + text_line = text_line.replace(old_str, new_str) + if start_str: + text_line = f'{start_str}{text_line}' + if text_line not in file_data: + file_data.append(text_line) + except RuntimeError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + if ini_URL_content: + with open(file_path, "w", encoding=text_encoding) as f2: + f2.write(ini_URL_content) + return old_str + if file_data: + with open(file_path, "w", encoding=text_encoding) as f: + f.write(''.join(file_data)) + return new_str + + +def delete_line(file_path: str, del_line: str, delete_all: bool = False) -> None: + with file_update_lock: + with open(file_path, 'r+', encoding=text_encoding) as f: + lines = f.readlines() + f.seek(0) + f.truncate() + skip_line = False + for txt_line in lines: + if del_line in txt_line: + if delete_all or not skip_line: + skip_line = True + continue + else: + skip_line = False + f.write(txt_line) + + +def get_startup_info(system_type: str): + if system_type == 'nt': + startup_info = subprocess.STARTUPINFO() + startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + else: + startup_info = None + return startup_info + + +def segment_video(converts_file_path: str, segment_save_file_path: str, segment_format: str, segment_time: str, + is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + ffmpeg_command = [ + "ffmpeg", + "-i", converts_file_path, + "-c:v", "copy", + "-c:a", "copy", + "-map", "0", + "-f", "segment", + "-segment_time", segment_time, + "-segment_format", segment_format, + "-reset_timestamps", "1", + "-movflags", "+frag_keyframe+empty_moov", + segment_save_file_path, + ] + _output = subprocess.check_output( + ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def converts_mp4(converts_file_path: str, is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + if converts_to_h264: + color_obj.print_colored("正在转码为MP4格式并重新编码为h264\n", color_obj.YELLOW) + ffmpeg_command = [ + "ffmpeg", "-i", converts_file_path, + "-c:v", "libx264", + "-preset", "veryfast", + "-crf", "23", + "-vf", "format=yuv420p", + "-c:a", "copy", + "-f", "mp4", converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4", + ] + else: + color_obj.print_colored("正在转码为MP4格式\n", color_obj.YELLOW) + ffmpeg_command = [ + "ffmpeg", "-i", converts_file_path, + "-c:v", "copy", + "-c:a", "copy", + "-f", "mp4", converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4", + ] + _output = subprocess.check_output( + ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def converts_m4a(converts_file_path: str, is_original_delete: bool = True) -> None: + try: + if os.path.exists(converts_file_path) and os.path.getsize(converts_file_path) > 0: + _output = subprocess.check_output([ + "ffmpeg", "-i", converts_file_path, + "-n", "-vn", + "-c:a", "aac", "-bsf:a", "aac_adtstoasc", "-ab", "320k", + converts_file_path.rsplit('.', maxsplit=1)[0] + ".m4a", + ], stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)) + if is_original_delete: + time.sleep(1) + if os.path.exists(converts_file_path): + os.remove(converts_file_path) + except subprocess.CalledProcessError as e: + logger.error(f'Error occurred during conversion: {e}') + except Exception as e: + logger.error(f'An unknown error occurred: {e}') + + +def generate_subtitles(record_name: str, ass_filename: str, sub_format: str = 'srt') -> None: + index_time = 0 + today = datetime.datetime.now() + re_datatime = today.strftime('%Y-%m-%d %H:%M:%S') + + def transform_int_to_time(seconds: int) -> str: + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + return f"{h:02d}:{m:02d}:{s:02d}" + + while True: + index_time += 1 + txt = str(index_time) + "\n" + transform_int_to_time(index_time) + ',000 --> ' + transform_int_to_time( + index_time + 1) + ',000' + "\n" + str(re_datatime) + "\n\n" + + with open(f"{ass_filename}.{sub_format.lower()}", 'a', encoding=text_encoding) as f: + f.write(txt) + + if record_name not in recording: + return + time.sleep(1) + today = datetime.datetime.now() + re_datatime = today.strftime('%Y-%m-%d %H:%M:%S') + + +def adjust_max_request() -> None: + global max_request, error_count, pre_max_request, error_window + preset = max_request + + while True: + time.sleep(5) + with max_request_lock: + if error_window: + error_rate = sum(error_window) / len(error_window) + else: + error_rate = 0 + + if error_rate > error_threshold: + max_request = max(1, max_request - 1) + elif error_rate < error_threshold / 2 and max_request < preset: + max_request += 1 + else: + pass + + if pre_max_request != max_request: + pre_max_request = max_request + print(f"\r同一时间访问网络的线程数动态改为 {max_request}") + + error_window.append(error_count) + if len(error_window) > error_window_size: + error_window.pop(0) + error_count = 0 + + +def push_message(record_name: str, live_url: str, content: str) -> None: + msg_title = push_message_title.strip() or "直播间状态更新通知" + push_functions = { + '微信': lambda: xizhi(xizhi_api_url, msg_title, content), + '钉钉': lambda: dingtalk(dingtalk_api_url, content, dingtalk_phone_num, dingtalk_is_atall), + '邮箱': lambda: send_email( + email_host, login_email, email_password, sender_email, sender_name, + to_email, msg_title, content, smtp_port, open_smtp_ssl + ), + 'TG': lambda: tg_bot(tg_chat_id, tg_token, content), + 'BARK': lambda: bark( + bark_msg_api, title=msg_title, content=content, level=bark_msg_level, sound=bark_msg_ring + ), + 'NTFY': lambda: ntfy( + ntfy_api, title=msg_title, content=content, tags=ntfy_tags, action_url=live_url, email=ntfy_email + ), + 'PUSHPLUS': lambda: pushplus(pushplus_token, msg_title, content), + } + + for platform, func in push_functions.items(): + if platform in live_status_push.upper(): + try: + result = func() + print(f'提示信息:已经将[{record_name}]直播状态消息推送至你的{platform},' + f' 成功{len(result["success"])}, 失败{len(result["error"])}') + except Exception as e: + color_obj.print_colored(f"直播消息推送到{platform}失败: {e}", color_obj.RED) + + +def run_script(command: str) -> None: + try: + process = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=get_startup_info(os_type) + ) + stdout, stderr = process.communicate() + stdout_decoded = stdout.decode('utf-8') + stderr_decoded = stderr.decode('utf-8') + if stdout_decoded.strip(): + print(stdout_decoded) + if stderr_decoded.strip(): + print(stderr_decoded) + except PermissionError as e: + logger.error(e) + logger.error('脚本无执行权限!, 若是Linux环境, 请先执行:chmod +x your_script.sh 授予脚本可执行权限') + except OSError as e: + logger.error(e) + logger.error('Please add `#!/bin/bash` at the beginning of your bash script file.') + + +def clear_record_info(record_name: str, record_url: str) -> None: + global monitoring + recording.discard(record_name) + if record_url in url_comments and record_url in running_list: + running_list.remove(record_url) + monitoring -= 1 + color_obj.print_colored(f"[{record_name}]已经从录制列表中移除\n", color_obj.YELLOW) + + +def direct_download_stream(source_url: str, save_path: str, record_name: str, live_url: str, platform: str) -> bool: + try: + with open(save_path, 'wb') as f: + client = httpx.Client(timeout=None) + + headers = {} + header_params = get_record_headers(platform, live_url) + if header_params: + key, value = header_params.split(":", 1) + headers[key] = value + + with client.stream('GET', source_url, headers=headers, follow_redirects=True) as response: + if response.status_code != 200: + logger.error(f"请求直播流失败,状态码: {response.status_code}") + return False + + downloaded = 0 + chunk_size = 1024 * 16 + + for chunk in response.iter_bytes(chunk_size): + if live_url in url_comments or exit_recording: + color_obj.print_colored(f"[{record_name}]录制时已被注释或请求停止,下载中断", color_obj.YELLOW) + clear_record_info(record_name, live_url) + return False + + if chunk: + f.write(chunk) + downloaded += len(chunk) + print() + return True + except Exception as e: + logger.error(f"FLV下载错误: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + return False + + +def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, save_type: str, + script_command: str | None = None) -> bool: + save_file_path = ffmpeg_command[-1] + process = subprocess.Popen( + ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type) + ) + + subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] + subs_thread_name = f'subs_{Path(subs_file_path).name}' + if create_time_file and not split_video_by_time and '音频' not in save_type: + create_var[subs_thread_name] = threading.Thread( + target=generate_subtitles, args=(record_name, subs_file_path) + ) + create_var[subs_thread_name].daemon = True + create_var[subs_thread_name].start() + + while process.poll() is None: + if record_url in url_comments or exit_recording: + color_obj.print_colored(f"[{record_name}]录制时已被注释,本条线程将会退出", color_obj.YELLOW) + clear_record_info(record_name, record_url) + # process.terminate() + if os.name == 'nt': + if process.stdin: + process.stdin.write(b'q') + process.stdin.close() + else: + process.send_signal(signal.SIGINT) + process.wait() + return True + time.sleep(1) + + return_code = process.returncode + stop_time = time.strftime('%Y-%m-%d %H:%M:%S') + if return_code == 0: + if converts_to_mp4 and save_type == 'TS': + if split_video_by_time: + file_paths = utils.get_file_paths(os.path.dirname(save_file_path)) + prefix = os.path.basename(save_file_path).rsplit('_', maxsplit=1)[0] + for path in file_paths: + if prefix in path: + threading.Thread(target=converts_mp4, args=(path, delete_origin_file)).start() + else: + threading.Thread(target=converts_mp4, args=(save_file_path, delete_origin_file)).start() + print(f"\n{record_name} {stop_time} 直播录制完成\n") + + if script_command: + logger.debug("开始执行脚本命令!") + if "python" in script_command: + params = [ + f'--record_name "{record_name}"', + f'--save_file_path "{save_file_path}"', + f'--save_type {save_type}', + f'--split_video_by_time {split_video_by_time}', + f'--converts_to_mp4 {converts_to_mp4}', + ] + else: + params = [ + f'"{record_name.split(" ", maxsplit=1)[-1]}"', + f'"{save_file_path}"', + save_type, + f'split_video_by_time:{split_video_by_time}', + f'converts_to_mp4:{converts_to_mp4}' + ] + script_command = script_command.strip() + ' ' + ' '.join(params) + run_script(script_command) + logger.debug("脚本命令执行结束!") + + else: + color_obj.print_colored(f"\n{record_name} {stop_time} 直播录制出错,返回码: {return_code}\n", color_obj.RED) + + recording.discard(record_name) + return False + + +def clean_name(input_text): + cleaned_name = re.sub(rstr, "_", input_text.strip()).strip('_') + cleaned_name = cleaned_name.replace("(", "(").replace(")", ")") + if clean_emoji: + cleaned_name = utils.remove_emojis(cleaned_name, '_').strip('_') + return cleaned_name or '空白昵称' + + +def get_quality_code(qn): + QUALITY_MAPPING = { + "原画": "OD", + "蓝光": "BD", + "超清": "UHD", + "高清": "HD", + "标清": "SD", + "流畅": "LD" + } + return QUALITY_MAPPING.get(qn) + + +def get_record_headers(platform, live_url): + live_domain = '/'.join(live_url.split('/')[0:3]) + record_headers = { + 'PandaTV': 'origin:https://www.pandalive.co.kr', + 'WinkTV': 'origin:https://www.winktv.co.kr', + 'PopkonTV': 'origin:https://www.popkontv.com', + 'FlexTV': 'origin:https://www.flextv.co.kr', + '千度热播': 'referer:https://qiandurebo.com', + '17Live': 'referer:https://17.live/en/live/6302408', + '浪Live': 'referer:https://www.lang.live', + 'shopee': f'origin:{live_domain}', + 'Blued直播': 'referer:https://app.blued.cn' + } + return record_headers.get(platform) + + +def is_flv_preferred_platform(link): + return any(i in link for i in ["douyin", "tiktok"]) + + +def select_source_url(link, stream_info): + if is_flv_preferred_platform(link): + codec = utils.get_query_params(stream_info.get('flv_url'), "codec") + if codec and codec[0] == 'h265': + logger.warning("FLV is not supported for h265 codec, use HLS source instead") + else: + return stream_info.get('flv_url') + + return stream_info.get('record_url') + + +def start_record(url_data: tuple, count_variable: int = -1) -> None: + global error_count + + while True: + try: + record_finished = False + run_once = False + start_pushed = False + new_record_url = '' + count_time = time.time() + retry = 0 + record_quality_zh, record_url, anchor_name = url_data + record_quality = get_quality_code(record_quality_zh) + proxy_address = proxy_addr + platform = '未知平台' + live_domain = '/'.join(record_url.split('/')[0:3]) + + if proxy_addr: + proxy_address = None + for platform in enable_proxy_platform_list: + if platform and platform.strip() in record_url: + proxy_address = proxy_addr + break + + if not proxy_address: + if extra_enable_proxy_platform_list: + for pt in extra_enable_proxy_platform_list: + if pt and pt.strip() in record_url: + proxy_address = proxy_addr_bak or None + + # print(f'\r代理地址:{proxy_address}') + # print(f'\r全局代理:{global_proxy}') + while True: + try: + port_info = [] + if record_url.find("douyin.com/") > -1: + platform = '抖音直播' + with semaphore: + if 'v.douyin.com' not in record_url and '/user/' not in record_url: + json_data = asyncio.run(spider.get_douyin_web_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=dy_cookie)) + else: + json_data = asyncio.run(spider.get_douyin_app_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=dy_cookie)) + port_info = asyncio.run( + stream.get_douyin_stream_url(json_data, record_quality, proxy_address)) + + elif record_url.find("https://www.tiktok.com/") > -1: + platform = 'TikTok直播' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_tiktok_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=tiktok_cookie)) + port_info = asyncio.run( + stream.get_tiktok_stream_url(json_data, record_quality, proxy_address)) + else: + logger.error("错误信息: 网络异常,请检查网络是否能正常访问TikTok平台") + + elif record_url.find("https://live.kuaishou.com/") > -1: + platform = '快手直播' + with semaphore: + json_data = asyncio.run(spider.get_kuaishou_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=ks_cookie)) + port_info = asyncio.run(stream.get_kuaishou_stream_url(json_data, record_quality)) + + elif record_url.find("https://www.huya.com/") > -1: + platform = '虎牙直播' + with semaphore: + if record_quality not in ['OD', 'BD', 'UHD']: + json_data = asyncio.run(spider.get_huya_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=hy_cookie)) + port_info = asyncio.run(stream.get_huya_stream_url(json_data, record_quality)) + else: + port_info = asyncio.run(spider.get_huya_app_stream_url( + url=record_url, + proxy_addr=proxy_address, + cookies=hy_cookie + )) + + elif record_url.find("https://www.douyu.com/") > -1: + platform = '斗鱼直播' + with semaphore: + json_data = asyncio.run(spider.get_douyu_info_data( + url=record_url, proxy_addr=proxy_address, cookies=douyu_cookie)) + port_info = asyncio.run(stream.get_douyu_stream_url( + json_data, video_quality=record_quality, cookies=douyu_cookie, proxy_addr=proxy_address + )) + + elif record_url.find("https://www.yy.com/") > -1: + platform = 'YY直播' + with semaphore: + json_data = asyncio.run(spider.get_yy_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=yy_cookie)) + port_info = asyncio.run(stream.get_yy_stream_url(json_data)) + + elif record_url.find("https://live.bilibili.com/") > -1: + platform = 'B站直播' + with semaphore: + json_data = asyncio.run(spider.get_bilibili_room_info( + url=record_url, proxy_addr=proxy_address, cookies=bili_cookie)) + port_info = asyncio.run(stream.get_bilibili_stream_url( + json_data, video_quality=record_quality, cookies=bili_cookie, proxy_addr=proxy_address)) + + elif record_url.find("http://xhslink.com/") > -1 or \ + record_url.find("https://www.xiaohongshu.com/") > -1: + platform = '小红书直播' + with semaphore: + port_info = asyncio.run(spider.get_xhs_stream_url( + record_url, proxy_addr=proxy_address, cookies=xhs_cookie)) + retry += 1 + + elif record_url.find("www.bigo.tv/") > -1 or record_url.find("slink.bigovideo.tv/") > -1: + platform = 'Bigo直播' + with semaphore: + port_info = asyncio.run(spider.get_bigo_stream_url( + record_url, proxy_addr=proxy_address, cookies=bigo_cookie)) + + elif record_url.find("https://app.blued.cn/") > -1: + platform = 'Blued直播' + with semaphore: + port_info = asyncio.run(spider.get_blued_stream_url( + record_url, proxy_addr=proxy_address, cookies=blued_cookie)) + + elif record_url.find("sooplive.co.kr/") > -1 or record_url.find("sooplive.com/") > -1: + platform = 'SOOP' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_sooplive_stream_data( + url=record_url, proxy_addr=proxy_address, + cookies=sooplive_cookie, + username=sooplive_username, + password=sooplive_password + )) + if json_data and json_data.get('new_cookies'): + utils.update_config( + config_file, 'Cookie', 'sooplive_cookie', json_data['new_cookies'] + ) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问SOOP平台") + + elif record_url.find("cc.163.com/") > -1: + platform = '网易CC直播' + with semaphore: + json_data = asyncio.run(spider.get_netease_stream_data( + url=record_url, cookies=netease_cookie)) + port_info = asyncio.run(stream.get_netease_stream_url(json_data, record_quality)) + + elif record_url.find("qiandurebo.com/") > -1: + platform = '千度热播' + with semaphore: + port_info = asyncio.run(spider.get_qiandurebo_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=qiandurebo_cookie)) + + elif record_url.find("www.pandalive.co.kr/") > -1: + platform = 'PandaTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_pandatv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=pandatv_cookie + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PandaTV直播平台") + + elif record_url.find("fm.missevan.com/") > -1: + platform = '猫耳FM直播' + with semaphore: + port_info = asyncio.run(spider.get_maoerfm_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=maoerfm_cookie)) + + elif record_url.find("www.winktv.co.kr/") > -1: + platform = 'WinkTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_winktv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=winktv_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问WinkTV直播平台") + + elif record_url.find("www.flextv.co.kr/") > -1 or record_url.find("www.ttinglive.com/") > -1: + platform = 'FlexTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_flextv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=flextv_cookie, + username=flextv_username, + password=flextv_password + )) + if json_data and json_data.get('new_cookies'): + utils.update_config( + config_file, 'Cookie', 'flextv_cookie', json_data['new_cookies'] + ) + if 'play_url_list' in json_data: + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + port_info = json_data + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问FlexTV直播平台") + + elif record_url.find("look.163.com/") > -1: + platform = 'Look直播' + with semaphore: + port_info = asyncio.run(spider.get_looklive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=look_cookie + )) + + elif record_url.find("www.popkontv.com/") > -1: + platform = 'PopkonTV' + with semaphore: + if global_proxy or proxy_address: + port_info = asyncio.run(spider.get_popkontv_stream_url( + url=record_url, + proxy_addr=proxy_address, + access_token=popkontv_access_token, + username=popkontv_username, + password=popkontv_password, + partner_code=popkontv_partner_code + )) + if port_info and port_info.get('new_token'): + utils.update_config( + file_path=config_file, section='Authorization', key='popkontv_token', + new_value=port_info['new_token'] + ) + + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PopkonTV直播平台") + + elif record_url.find("twitcasting.tv/") > -1: + platform = 'TwitCasting' + with semaphore: + json_data = asyncio.run(spider.get_twitcasting_stream_url( + url=record_url, + proxy_addr=proxy_address, + cookies=twitcasting_cookie, + account_type=twitcasting_account_type, + username=twitcasting_username, + password=twitcasting_password + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=False)) + + if port_info and port_info.get('new_cookies'): + utils.update_config( + file_path=config_file, section='Cookie', key='twitcasting_cookie', + new_value=port_info['new_cookies'] + ) + + elif record_url.find("live.baidu.com/") > -1: + platform = '百度直播' + with semaphore: + json_data = asyncio.run(spider.get_baidu_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=baidu_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality)) + + elif record_url.find("weibo.com/") > -1: + platform = '微博直播' + with semaphore: + json_data = asyncio.run(spider.get_weibo_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=weibo_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, hls_extra_key='m3u8_url')) + + elif record_url.find("kugou.com/") > -1: + platform = '酷狗直播' + with semaphore: + port_info = asyncio.run(spider.get_kugou_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=kugou_cookie)) + + elif record_url.find("www.twitch.tv/") > -1: + platform = 'TwitchTV' + with semaphore: + if global_proxy or proxy_address: + json_data = asyncio.run(spider.get_twitchtv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=twitch_cookie + )) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问TwitchTV直播平台") + + elif record_url.find("www.liveme.com/") > -1: + if global_proxy or proxy_address: + platform = 'LiveMe' + with semaphore: + port_info = asyncio.run(spider.get_liveme_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=liveme_cookie)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问LiveMe直播平台") + + elif record_url.find("www.huajiao.com/") > -1: + platform = '花椒直播' + with semaphore: + port_info = asyncio.run(spider.get_huajiao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=huajiao_cookie)) + + elif record_url.find("7u66.com/") > -1: + platform = '流星直播' + with semaphore: + port_info = asyncio.run(spider.get_liuxing_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=liuxing_cookie)) + + elif record_url.find("showroom-live.com/") > -1: + platform = 'ShowRoom' + with semaphore: + json_data = asyncio.run(spider.get_showroom_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=showroom_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("live.acfun.cn/") > -1 or record_url.find("m.acfun.cn/") > -1: + platform = 'Acfun' + with semaphore: + json_data = asyncio.run(spider.get_acfun_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=acfun_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, url_type='flv', flv_extra_key='url')) + + elif record_url.find("live.tlclw.com/") > -1: + platform = '畅聊直播' + with semaphore: + port_info = asyncio.run(spider.get_changliao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=changliao_cookie)) + + elif record_url.find("ybw1666.com/") > -1: + platform = '音播直播' + with semaphore: + port_info = asyncio.run(spider.get_yinbo_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yinbo_cookie)) + + elif record_url.find("www.inke.cn/") > -1: + platform = '映客直播' + with semaphore: + port_info = asyncio.run(spider.get_yingke_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yingke_cookie)) + + elif record_url.find("www.zhihu.com/") > -1: + platform = '知乎直播' + with semaphore: + port_info = asyncio.run(spider.get_zhihu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=zhihu_cookie)) + + elif record_url.find("chzzk.naver.com/") > -1: + platform = 'CHZZK' + with semaphore: + json_data = asyncio.run(spider.get_chzzk_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=chzzk_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("www.haixiutv.com/") > -1: + platform = '嗨秀直播' + with semaphore: + port_info = asyncio.run(spider.get_haixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=haixiu_cookie)) + + elif record_url.find("vvxqiu.com/") > -1: + platform = 'VV星球' + with semaphore: + port_info = asyncio.run(spider.get_vvxqiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=vvxqiu_cookie)) + + elif record_url.find("17.live/") > -1: + platform = '17Live' + with semaphore: + port_info = asyncio.run(spider.get_17live_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=yiqilive_cookie)) + + elif record_url.find("www.lang.live/") > -1: + platform = '浪Live' + with semaphore: + port_info = asyncio.run(spider.get_langlive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=langlive_cookie)) + + elif record_url.find("m.pp.weimipopo.com/") > -1: + platform = '漂漂直播' + with semaphore: + port_info = asyncio.run(spider.get_pplive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=pplive_cookie)) + + elif record_url.find(".6.cn/") > -1: + platform = '六间房直播' + with semaphore: + port_info = asyncio.run(spider.get_6room_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=six_room_cookie)) + + elif record_url.find("lehaitv.com/") > -1: + platform = '乐嗨直播' + with semaphore: + port_info = asyncio.run(spider.get_haixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=lehaitv_cookie)) + + elif record_url.find("h.catshow168.com/") > -1: + platform = '花猫直播' + with semaphore: + port_info = asyncio.run(spider.get_pplive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=huamao_cookie)) + + elif record_url.find("live.shopee") > -1 or record_url.find("shp.ee/") > -1: + platform = 'shopee' + with semaphore: + port_info = asyncio.run(spider.get_shopee_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=shopee_cookie)) + if port_info.get('uid'): + new_record_url = record_url.split('?')[0] + '?' + str(port_info['uid']) + + elif record_url.find("www.youtube.com/") > -1 or record_url.find("youtu.be/") > -1: + platform = 'Youtube' + with semaphore: + json_data = asyncio.run(spider.get_youtube_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=youtube_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + + elif record_url.find("tb.cn") > -1: + platform = '淘宝直播' + with semaphore: + json_data = asyncio.run(spider.get_taobao_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=taobao_cookie)) + port_info = asyncio.run(stream.get_stream_url( + json_data, record_quality, + url_type='all', hls_extra_key='hlsUrl', flv_extra_key='flvUrl' + )) + + elif record_url.find("3.cn") > -1 or record_url.find("m.jd.com") > -1: + platform = '京东直播' + with semaphore: + port_info = asyncio.run(spider.get_jd_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=jd_cookie)) + + elif record_url.find("faceit.com/") > -1: + platform = 'faceit' + with semaphore: + if global_proxy or proxy_address: + with semaphore: + json_data = asyncio.run(spider.get_faceit_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=faceit_cookie)) + port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True)) + else: + logger.error("错误信息: 网络异常,请检查本网络是否能正常访问faceit直播平台") + + elif record_url.find("www.miguvideo.com") > -1 or record_url.find("m.miguvideo.com") > -1: + platform = '咪咕直播' + with semaphore: + port_info = asyncio.run(spider.get_migu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=migu_cookie)) + + elif record_url.find("show.lailianjie.com") > -1: + platform = '连接直播' + with semaphore: + port_info = asyncio.run(spider.get_lianjie_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=lianjie_cookie)) + + elif record_url.find("www.imkktv.com") > -1: + platform = '来秀直播' + with semaphore: + port_info = asyncio.run(spider.get_laixiu_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=laixiu_cookie)) + + elif record_url.find("www.picarto.tv") > -1: + platform = 'Picarto' + with semaphore: + port_info = asyncio.run(spider.get_picarto_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=picarto_cookie)) + + elif record_url.find(".m3u8") > -1 or record_url.find(".flv") > -1: + platform = '自定义录制直播' + port_info = { + "anchor_name": platform + '_' + str(uuid.uuid4())[:8], + "is_live": True, + "record_url": record_url, + } + if '.flv' in record_url: + port_info['flv_url'] = record_url + else: + port_info['m3u8_url'] = record_url + + else: + logger.error(f'{record_url} {platform}直播地址') + return + + if anchor_name: + if '主播:' in anchor_name: + anchor_split: list = anchor_name.split('主播:') + if len(anchor_split) > 1 and anchor_split[1].strip(): + anchor_name = anchor_split[1].strip() + else: + anchor_name = port_info.get("anchor_name", '') + else: + anchor_name = port_info.get("anchor_name", '') + + if not port_info.get("anchor_name", ''): + print(f'序号{count_variable} 网址内容获取失败,进行重试中...获取失败的地址是:{url_data}') + with max_request_lock: + error_count += 1 + error_window.append(1) + else: + anchor_name = clean_name(anchor_name) + record_name = f'序号{count_variable} {anchor_name}' + + if record_url in url_comments: + print(f"[{anchor_name}]已被注释,本条线程将会退出") + clear_record_info(record_name, record_url) + return + + if not url_data[-1] and run_once is False: + if new_record_url: + need_update_line_list.append( + f'{record_url}|{new_record_url},主播: {anchor_name.strip()}') + not_record_list.append(new_record_url) + else: + need_update_line_list.append(f'{record_url}|{record_url},主播: {anchor_name.strip()}') + run_once = True + + push_at = datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S') + if port_info['is_live'] is False: + print(f"\r{record_name} 等待直播... ") + + # 直接退出线程eric + # return + # 继续循环 + continue + + if start_pushed: + if over_show_push: + push_content = "直播间状态更新:[直播间名称] 直播已结束!时间:[时间]" + if over_push_message_text: + push_content = over_push_message_text + + push_content = (push_content.replace('[直播间名称]', record_name). + replace('[时间]', push_at)) + threading.Thread( + target=push_message, + args=(record_name, record_url, push_content.replace(r'\n', '\n')), + daemon=True + ).start() + start_pushed = False + + else: + content = f"\r{record_name} 正在直播中..." + print(content) + # 检查是否已有直播在录制 + if len(recording) > 0 : + # 如果已有直播在录制,跳过这次录制(主播在线) + # return + continue #如果在循环中 + + if live_status_push and not start_pushed: + if begin_show_push: + push_content = "直播间状态更新:[直播间名称] 正在直播中,时间:[时间]" + if begin_push_message_text: + push_content = begin_push_message_text + + push_content = (push_content.replace('[直播间名称]', record_name). + replace('[时间]', push_at)) + threading.Thread( + target=push_message, + args=(record_name, record_url, push_content.replace(r'\n', '\n')), + daemon=True + ).start() + start_pushed = True + + if disable_record: + time.sleep(push_check_seconds) + continue + + real_url = select_source_url(record_url, port_info) + full_path = f'{default_path}/{platform}' + if real_url: + now = datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S") + live_title = port_info.get('title') + title_in_name = '' + if live_title: + live_title = clean_name(live_title) + title_in_name = live_title + '_' if filename_by_title else '' + + try: + if len(video_save_path) > 0: + if not video_save_path.endswith(('/', '\\')): + full_path = f'{video_save_path}/{platform}' + else: + full_path = f'{video_save_path}{platform}' + + full_path = full_path.replace("\\", '/') + if folder_by_author: + full_path = f'{full_path}/{anchor_name}' + if folder_by_time: + full_path = f'{full_path}/{now[:10]}' + if folder_by_title and port_info.get('title'): + if folder_by_time: + full_path = f'{full_path}/{live_title}_{anchor_name}' + else: + full_path = f'{full_path}/{now[:10]}_{live_title}' + if not os.path.exists(full_path): + os.makedirs(full_path) + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + + if platform != '自定义录制直播': + if enable_https_recording and real_url.startswith("http://"): + real_url = real_url.replace("http://", "https://") + + http_record_list = ['shopee', "migu"] + if platform in http_record_list: + real_url = real_url.replace("https://", "http://") + + user_agent = ("Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (" + "KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile " + "Safari/537.36") + + rw_timeout = "15000000" + analyzeduration = "20000000" + probesize = "10000000" + bufsize = "8000k" + max_muxing_queue_size = "1024" + for pt_host in overseas_platform_host: + if pt_host in record_url: + rw_timeout = "50000000" + analyzeduration = "40000000" + probesize = "20000000" + bufsize = "15000k" + max_muxing_queue_size = "2048" + break + + ffmpeg_command = [ + 'ffmpeg', "-y", + "-v", "verbose", + "-rw_timeout", rw_timeout, + "-loglevel", "error", + "-hide_banner", + "-user_agent", user_agent, + "-protocol_whitelist", "rtmp,crypto,file,http,https,tcp,tls,udp,rtp,httpproxy", + "-thread_queue_size", "1024", + "-analyzeduration", analyzeduration, + "-probesize", probesize, + "-fflags", "+discardcorrupt", + "-re", "-i", real_url, + "-bufsize", bufsize, + "-sn", "-dn", + "-reconnect_delay_max", "60", + "-reconnect_streamed", "-reconnect_at_eof", + "-max_muxing_queue_size", max_muxing_queue_size, + "-correct_ts_overflow", "1", + "-avoid_negative_ts", "1" + ] + + headers = get_record_headers(platform, record_url) + if headers: + ffmpeg_command.insert(11, "-headers") + ffmpeg_command.insert(12, headers) + + _ff_proxy = (proxy_address or "").strip() or ( + os.environ.get("STREAM_HTTP_PROXY") or "" + ).strip() or None + if _ff_proxy: + ffmpeg_command.insert(1, "-http_proxy") + ffmpeg_command.insert(2, _ff_proxy) + + recording.add(record_name) + start_record_time = datetime.datetime.now() + recording_time_list[record_name] = [start_record_time, record_quality_zh] + rec_info = f"\r{anchor_name} 准备开始录制视频: {full_path}" + if show_url: + re_plat = ('WinkTV', 'PandaTV', 'ShowRoom', 'CHZZK', 'Youtube') + if platform in re_plat: + logger.info( + f"{platform} | {anchor_name} | 直播源地址: {port_info.get('m3u8_url')}") + else: + logger.info( + f"{platform} | {anchor_name} | 直播源地址: {real_url}") + + only_flv_record = False + only_flv_platform_list = ['shopee', '花椒直播'] + if platform in only_flv_platform_list: + logger.debug(f"提示: {platform} 将强制使用FLV格式录制") + only_flv_record = True + + only_audio_record = False + only_audio_platform_list = ['猫耳FM直播', 'Look直播'] + if platform in only_audio_platform_list: + only_audio_record = True + + record_save_type = video_save_type + + if is_flv_preferred_platform(record_url) and port_info.get('flv_url'): + codec = utils.get_query_params(port_info['flv_url'], "codec") + if codec and codec[0] == 'h265': + logger.warning("FLV is not supported for h265 codec, use TS format instead") + record_save_type = "TS" + + if only_audio_record or any(i in record_save_type for i in ['MP3', 'M4A']): + try: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + extension = "mp3" if "m4a" not in record_save_type.lower() else "m4a" + name_format = "_%03d" if split_video_by_time else "" + save_file_path = (f"{full_path}/{anchor_name}_{title_in_name}{now}" + f"{name_format}.{extension}") + + if split_video_by_time: + print(f'\r{anchor_name} 准备开始录制音频: {save_file_path}') + + if "MP3" in record_save_type: + command = [ + "-map", "0:a", + "-c:a", "libmp3lame", + "-ab", "320k", + "-f", "segment", + "-segment_time", split_time, + "-reset_timestamps", "1", + save_file_path, + ] + else: + command = [ + "-map", "0:a", + "-c:a", "aac", + "-bsf:a", "aac_adtstoasc", + "-ab", "320k", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", 'mpegts', + "-reset_timestamps", "1", + save_file_path, + ] + + else: + if "MP3" in record_save_type: + command = [ + "-map", "0:a", + "-c:a", "libmp3lame", + "-ab", "320k", + save_file_path, + ] + + else: + command = [ + "-map", "0:a", + "-c:a", "aac", + "-bsf:a", "aac_adtstoasc", + "-ab", "320k", + "-movflags", "+faststart", + save_file_path, + ] + + ffmpeg_command.extend(command) + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + + + # if only_flv_record: + if False: + + logger.info(f"Use Direct Downloader to Download FLV Stream: {record_url}") + filename = anchor_name + f'_{title_in_name}' + now + '.flv' + save_file_path = f'{full_path}/{filename}' + print(f'{rec_info}/{filename}') + + subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0] + subs_thread_name = f'subs_{Path(subs_file_path).name}' + if create_time_file: + create_var[subs_thread_name] = threading.Thread( + target=generate_subtitles, args=(record_name, subs_file_path) + ) + create_var[subs_thread_name].daemon = True + create_var[subs_thread_name].start() + + try: + flv_url = port_info.get('flv_url') + if flv_url: + recording.add(record_name) + start_record_time = datetime.datetime.now() + recording_time_list[record_name] = [start_record_time, record_quality_zh] + + download_success = direct_download_stream( + flv_url, save_file_path, record_name, record_url, platform + ) + + if download_success: + record_finished = True + print( + f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制完成\n") + + recording.discard(record_name) + else: + logger.debug("未找到FLV直播流,跳过录制") + except Exception as e: + clear_record_info(record_name, record_url) + color_obj.print_colored( + f"\n{anchor_name} {time.strftime('%Y-%m-%d %H:%M:%S')} 直播录制出错,请检查网络\n", + color_obj.RED) + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + elif record_save_type == "FLV": + filename = anchor_name + f'_{title_in_name}' + now + ".flv" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.flv" + command = [ + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-bsf:a", "aac_adtstoasc", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "flv", + "-reset_timestamps", "1", + save_file_path + ] + + else: + command = [ + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-bsf:a", "aac_adtstoasc", + "-f", "flv", + "{path}".format(path=save_file_path), + ] + ffmpeg_command.extend(command) + + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + try: + if converts_to_mp4: + seg_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mp4" + if split_video_by_time: + segment_video( + save_file_path, seg_file_path, + segment_format='mp4', segment_time=split_time, + is_original_delete=delete_origin_file + ) + else: + threading.Thread( + target=converts_mp4, + args=(save_file_path, delete_origin_file) + ).start() + + else: + seg_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.flv" + if split_video_by_time: + segment_video( + save_file_path, seg_file_path, + segment_format='flv', segment_time=split_time, + is_original_delete=delete_origin_file + ) + except Exception as e: + logger.error(f"转码失败: {e} ") + + elif record_save_type == "MKV": + filename = anchor_name + f'_{title_in_name}' + now + ".mkv" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mkv" + command = [ + "-flags", "global_header", + "-c:v", "copy", + "-c:a", "aac", + "-map", "0", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "matroska", + "-reset_timestamps", "1", + save_file_path, + ] + + else: + command = [ + "-flags", "global_header", + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-f", "matroska", + "{path}".format(path=save_file_path), + ] + ffmpeg_command.extend(command) + + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + elif record_save_type == "MP4": + filename = anchor_name + f'_{title_in_name}' + now + ".mp4" + print(f'{rec_info}/{filename}') + save_file_path = full_path + '/' + filename + + try: + if split_video_by_time: + now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) + save_file_path = f"{full_path}/{anchor_name}_{title_in_name}{now}_%03d.mp4" + command = [ + "-c:v", "copy", + "-c:a", "aac", + "-map", "0", + "-f", "segment", + "-segment_time", split_time, + "-segment_format", "mp4", + "-reset_timestamps", "1", + "-movflags", "+frag_keyframe+empty_moov", + save_file_path, + ] + + else: + command = [ + "-map", "0", + "-c:v", "copy", + "-c:a", "copy", + "-f", "mp4", + save_file_path, + ] + + ffmpeg_command.extend(command) + comment_end = check_subprocess( + record_name, + record_url, + ffmpeg_command, + record_save_type, + custom_script + ) + if comment_end: + return + + except subprocess.CalledProcessError as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + else: + import sys + sys.path.insert(0, './dist') + from douyin_youtube_ffplay import start_douyin_youtube_ffplay + + # 调用(与 douyin_srs_ffplay 调用方式完全一致) + start_douyin_youtube_ffplay( + title=title_in_name, + anchor_name=anchor_name, + real_url=real_url, + rec_info=rec_info, + record_name=record_name, + recording=recording, + recording_time_list=recording_time_list, + running_list=running_list, + monitoring=monitoring, # 假设外部 monitoring 是 list,如 [count] + clear_record_info=clear_record_info, + color_obj=color_obj + ) + count_time = time.time() + + + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + + num = random.randint(-5, 5) + delay_default + if num < 0: + num = 0 + x = num + + if error_count > 20: + x = x + 60 + # x += min(error_count * 2, 30) + color_obj.print_colored("\r瞬时错误太多,延迟最多60s", color_obj.YELLOW) + + # 这里是.如果录制结束后,循环时间会暂时变成30s后检测一遍. 这样一定程度上防止主播卡顿造成少录 + # 当30秒过后检测一遍后. 会回归正常设置的循环秒数 + if record_finished: + count_time_end = time.time() - count_time + if count_time_end < 60: + x = 30 + record_finished = False + + else: + x = num + + # 这里是正常循环 + while x: + x = x - 1 + if loop_time: + print(f'\r{anchor_name}循环等待{x}秒 ', end="") + time.sleep(1) + if loop_time: + print('\r检测直播间中...', end="") + except Exception as e: + logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") + with max_request_lock: + error_count += 1 + error_window.append(1) + time.sleep(2) + + +def backup_file(file_path: str, backup_dir_path: str, limit_counts: int = 6) -> None: + + try: + if not os.path.exists(backup_dir_path): + os.makedirs(backup_dir_path) + + timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + backup_file_name = os.path.basename(file_path) + '_' + timestamp + backup_file_path = os.path.join(backup_dir_path, backup_file_name).replace("\\", "/") + shutil.copy2(file_path, backup_file_path) + + files = os.listdir(backup_dir_path) + _files = [f for f in files if f.startswith(os.path.basename(file_path))] + _files.sort(key=lambda x: os.path.getmtime(os.path.join(backup_dir_path, x))) + + while len(_files) > limit_counts: + oldest_file = _files[0] + os.remove(os.path.join(backup_dir_path, oldest_file)) + _files = _files[1:] + + except Exception as e: + logger.error(f'\r备份配置文件 {file_path} 失败:{str(e)}') + + +def backup_file_start() -> None: + config_md5 = '' + url_config_md5 = '' + + while True: + try: + if os.path.exists(config_file): + new_config_md5 = utils.check_md5(config_file) + if new_config_md5 != config_md5: + backup_file(config_file, backup_dir) + config_md5 = new_config_md5 + + if os.path.exists(url_config_file): + new_url_config_md5 = utils.check_md5(url_config_file) + if new_url_config_md5 != url_config_md5: + backup_file(url_config_file, backup_dir) + url_config_md5 = new_url_config_md5 + time.sleep(600) + except Exception as e: + logger.error(f"备份配置文件失败, 错误信息: {e}") + + +def check_ffmpeg_existence() -> bool: + try: + result = subprocess.run(['ffmpeg', '-version'], check=True, capture_output=True, text=True) + if result.returncode == 0: + lines = result.stdout.splitlines() + version_line = lines[0] + built_line = lines[1] + print(version_line) + print(built_line) + except subprocess.CalledProcessError as e: + logger.error(e) + except FileNotFoundError: + pass + finally: + if check_ffmpeg(): + time.sleep(1) + return True + return False + + +# --------------------------初始化程序------------------------------------- +print("-----------------------------------------------------") +print("| DouyinLiveRecorder |") +print("-----------------------------------------------------") + +print(f"版本号: {version}") +print("GitHub: https://github.com/ihmily/DouyinLiveRecorder") +print(f'支持平台: {platforms}') +print('.....................................................') +if not check_ffmpeg_existence(): + logger.error("缺少ffmpeg无法进行录制,程序退出") + sys.exit(1) +os.makedirs(os.path.dirname(config_file), exist_ok=True) +t3 = threading.Thread(target=backup_file_start, args=(), daemon=True) +t3.start() +utils.remove_duplicate_lines(url_config_file) + + +def read_config_value(config_parser: configparser.RawConfigParser, section: str, option: str, default_value: Any) \ + -> Any: + try: + + config_parser.read(config_file, encoding=text_encoding) + if '录制设置' not in config_parser.sections(): + config_parser.add_section('录制设置') + if '推送配置' not in config_parser.sections(): + config_parser.add_section('推送配置') + if 'Cookie' not in config_parser.sections(): + config_parser.add_section('Cookie') + if 'Authorization' not in config_parser.sections(): + config_parser.add_section('Authorization') + if '账号密码' not in config_parser.sections(): + config_parser.add_section('账号密码') + return config_parser.get(section, option) + except (configparser.NoSectionError, configparser.NoOptionError): + config_parser.set(section, option, str(default_value)) + with open(config_file, 'w', encoding=text_encoding) as f: + config_parser.write(f) + return default_value + + +options = {"是": True, "否": False} +config = configparser.RawConfigParser() +language = read_config_value(config, '录制设置', 'language(zh_cn/en)', "zh_cn") +skip_proxy_check = options.get(read_config_value(config, '录制设置', '是否跳过代理检测(是/否)', "否"), False) +if language and 'en' not in language.lower(): + from i18n import translated_print + + builtins.print = translated_print + +try: + if skip_proxy_check: + global_proxy = True + else: + print('系统代理检测中,请耐心等待...') + response_g = urllib.request.urlopen("https://www.google.com/", timeout=15) + global_proxy = True + print('\r全局/规则网络代理已开启√') + pd = ProxyDetector() + if pd.is_proxy_enabled(): + proxy_info = pd.get_proxy_info() + print("System Proxy: http://{}:{}".format(proxy_info.ip, proxy_info.port)) +except HTTPError as err: + print(f"HTTP error occurred: {err.code} - {err.reason}") +except URLError: + color_obj.print_colored("INFO:未检测到全局/规则网络代理,请检查代理配置(若无需录制海外直播请忽略此条提示)", + color_obj.YELLOW) +except Exception as err: + print("An unexpected error occurred:", err) + +while True: + + try: + if not os.path.isfile(config_file): + with open(config_file, 'w', encoding=text_encoding) as file: + pass + + ini_URL_content = '' + if os.path.isfile(url_config_file): + with open(url_config_file, 'r', encoding=text_encoding) as file: + ini_URL_content = file.read().strip() + + if not ini_URL_content.strip(): + input_url = input('请输入要录制的主播直播间网址(尽量使用PC网页端的直播间地址):\n') + with open(url_config_file, 'w', encoding=text_encoding) as file: + file.write(input_url) + except OSError as err: + logger.error(f"发生 I/O 错误: {err}") + + video_save_path = read_config_value(config, '录制设置', '直播保存路径(不填则默认)', "") + folder_by_author = options.get(read_config_value(config, '录制设置', '保存文件夹是否以作者区分', "是"), False) + folder_by_time = options.get(read_config_value(config, '录制设置', '保存文件夹是否以时间区分', "否"), False) + folder_by_title = options.get(read_config_value(config, '录制设置', '保存文件夹是否以标题区分', "否"), False) + filename_by_title = options.get(read_config_value(config, '录制设置', '保存文件名是否包含标题', "否"), False) + clean_emoji = options.get(read_config_value(config, '录制设置', '是否去除名称中的表情符号', "是"), True) + video_save_type = read_config_value(config, '录制设置', '视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频', "ts") + video_record_quality = read_config_value(config, '录制设置', '原画|超清|高清|标清|流畅', "原画") + use_proxy = options.get(read_config_value(config, '录制设置', '是否使用代理ip(是/否)', "是"), False) + proxy_addr_bak = read_config_value(config, '录制设置', '代理地址', "") + proxy_addr = None if not use_proxy else proxy_addr_bak + max_request = int(read_config_value(config, '录制设置', '同一时间访问网络的线程数', 3)) + semaphore = threading.Semaphore(max_request) + delay_default = int(read_config_value(config, '录制设置', '循环时间(秒)', 120)) + local_delay_default = int(read_config_value(config, '录制设置', '排队读取网址时间(秒)', 0)) + loop_time = options.get(read_config_value(config, '录制设置', '是否显示循环秒数', "否"), False) + show_url = options.get(read_config_value(config, '录制设置', '是否显示直播源地址', "否"), False) + split_video_by_time = options.get(read_config_value(config, '录制设置', '分段录制是否开启', "否"), False) + enable_https_recording = options.get(read_config_value(config, '录制设置', '是否强制启用https录制', "否"), False) + disk_space_limit = float(read_config_value(config, '录制设置', '录制空间剩余阈值(gb)', 1.0)) + split_time = str(read_config_value(config, '录制设置', '视频分段时间(秒)', 1800)) + converts_to_mp4 = options.get(read_config_value(config, '录制设置', '录制完成后自动转为mp4格式', "否"), False) + converts_to_h264 = options.get(read_config_value(config, '录制设置', 'mp4格式重新编码为h264', "否"), False) + delete_origin_file = options.get(read_config_value(config, '录制设置', '追加格式后删除原文件', "否"), False) + create_time_file = options.get(read_config_value(config, '录制设置', '生成时间字幕文件', "否"), False) + is_run_script = options.get(read_config_value(config, '录制设置', '是否录制完成后执行自定义脚本', "否"), False) + custom_script = read_config_value(config, '录制设置', '自定义脚本执行命令', "") if is_run_script else None + enable_proxy_platform = read_config_value( + config, '录制设置', '使用代理录制的平台(逗号分隔)', + 'tiktok, soop, pandalive, winktv, flextv, popkontv, twitch, liveme, showroom, chzzk, shopee, shp, youtu, faceit' + ) + enable_proxy_platform_list = enable_proxy_platform.replace(',', ',').split(',') if enable_proxy_platform else None + extra_enable_proxy = read_config_value(config, '录制设置', '额外使用代理录制的平台(逗号分隔)', '') + extra_enable_proxy_platform_list = extra_enable_proxy.replace(',', ',').split(',') if extra_enable_proxy else None + live_status_push = read_config_value(config, '推送配置', '直播状态推送渠道', "") + dingtalk_api_url = read_config_value(config, '推送配置', '钉钉推送接口链接', "") + xizhi_api_url = read_config_value(config, '推送配置', '微信推送接口链接', "") + bark_msg_api = read_config_value(config, '推送配置', 'bark推送接口链接', "") + bark_msg_level = read_config_value(config, '推送配置', 'bark推送中断级别', "active") + bark_msg_ring = read_config_value(config, '推送配置', 'bark推送铃声', "bell") + dingtalk_phone_num = read_config_value(config, '推送配置', '钉钉通知@对象(填手机号)', "") + dingtalk_is_atall = options.get(read_config_value(config, '推送配置', '钉钉通知@全体(是/否)', "否"), False) + tg_token = read_config_value(config, '推送配置', 'tgapi令牌', "") + tg_chat_id = read_config_value(config, '推送配置', 'tg聊天id(个人或者群组id)', "") + email_host = read_config_value(config, '推送配置', 'SMTP邮件服务器', "") + open_smtp_ssl = options.get(read_config_value(config, '推送配置', '是否使用SMTP服务SSL加密(是/否)', "是"), True) + smtp_port = read_config_value(config, '推送配置', 'SMTP邮件服务器端口', "") + login_email = read_config_value(config, '推送配置', '邮箱登录账号', "") + email_password = read_config_value(config, '推送配置', '发件人密码(授权码)', "") + sender_email = read_config_value(config, '推送配置', '发件人邮箱', "") + sender_name = read_config_value(config, '推送配置', '发件人显示昵称', "") + to_email = read_config_value(config, '推送配置', '收件人邮箱', "") + ntfy_api = read_config_value(config, '推送配置', 'ntfy推送地址', "") + ntfy_tags = read_config_value(config, '推送配置', 'ntfy推送标签', "tada") + ntfy_email = read_config_value(config, '推送配置', 'ntfy推送邮箱', "") + pushplus_token = read_config_value(config, '推送配置', 'pushplus推送token', "") + push_message_title = read_config_value(config, '推送配置', '自定义推送标题', "直播间状态更新通知") + begin_push_message_text = read_config_value(config, '推送配置', '自定义开播推送内容', "") + over_push_message_text = read_config_value(config, '推送配置', '自定义关播推送内容', "") + disable_record = options.get(read_config_value(config, '推送配置', '只推送通知不录制(是/否)', "否"), False) + push_check_seconds = int(read_config_value(config, '推送配置', '直播推送检测频率(秒)', 1800)) + begin_show_push = options.get(read_config_value(config, '推送配置', '开播推送开启(是/否)', "是"), True) + over_show_push = options.get(read_config_value(config, '推送配置', '关播推送开启(是/否)', "否"), False) + sooplive_username = read_config_value(config, '账号密码', 'sooplive账号', '') + sooplive_password = read_config_value(config, '账号密码', 'sooplive密码', '') + flextv_username = read_config_value(config, '账号密码', 'flextv账号', '') + flextv_password = read_config_value(config, '账号密码', 'flextv密码', '') + popkontv_username = read_config_value(config, '账号密码', 'popkontv账号', '') + popkontv_partner_code = read_config_value(config, '账号密码', 'partner_code', 'P-00001') + popkontv_password = read_config_value(config, '账号密码', 'popkontv密码', '') + twitcasting_account_type = read_config_value(config, '账号密码', 'twitcasting账号类型', 'normal') + twitcasting_username = read_config_value(config, '账号密码', 'twitcasting账号', '') + twitcasting_password = read_config_value(config, '账号密码', 'twitcasting密码', '') + popkontv_access_token = read_config_value(config, 'Authorization', 'popkontv_token', '') + dy_cookie = read_config_value(config, 'Cookie', '抖音cookie', '') + ks_cookie = read_config_value(config, 'Cookie', '快手cookie', '') + tiktok_cookie = read_config_value(config, 'Cookie', 'tiktok_cookie', '') + hy_cookie = read_config_value(config, 'Cookie', '虎牙cookie', '') + douyu_cookie = read_config_value(config, 'Cookie', '斗鱼cookie', '') + yy_cookie = read_config_value(config, 'Cookie', 'yy_cookie', '') + bili_cookie = read_config_value(config, 'Cookie', 'B站cookie', '') + xhs_cookie = read_config_value(config, 'Cookie', '小红书cookie', '') + bigo_cookie = read_config_value(config, 'Cookie', 'bigo_cookie', '') + blued_cookie = read_config_value(config, 'Cookie', 'blued_cookie', '') + sooplive_cookie = read_config_value(config, 'Cookie', 'sooplive_cookie', '') + netease_cookie = read_config_value(config, 'Cookie', 'netease_cookie', '') + qiandurebo_cookie = read_config_value(config, 'Cookie', '千度热播_cookie', '') + pandatv_cookie = read_config_value(config, 'Cookie', 'pandatv_cookie', '') + maoerfm_cookie = read_config_value(config, 'Cookie', '猫耳fm_cookie', '') + winktv_cookie = read_config_value(config, 'Cookie', 'winktv_cookie', '') + flextv_cookie = read_config_value(config, 'Cookie', 'flextv_cookie', '') + look_cookie = read_config_value(config, 'Cookie', 'look_cookie', '') + twitcasting_cookie = read_config_value(config, 'Cookie', 'twitcasting_cookie', '') + baidu_cookie = read_config_value(config, 'Cookie', 'baidu_cookie', '') + weibo_cookie = read_config_value(config, 'Cookie', 'weibo_cookie', '') + kugou_cookie = read_config_value(config, 'Cookie', 'kugou_cookie', '') + twitch_cookie = read_config_value(config, 'Cookie', 'twitch_cookie', '') + liveme_cookie = read_config_value(config, 'Cookie', 'liveme_cookie', '') + huajiao_cookie = read_config_value(config, 'Cookie', 'huajiao_cookie', '') + liuxing_cookie = read_config_value(config, 'Cookie', 'liuxing_cookie', '') + showroom_cookie = read_config_value(config, 'Cookie', 'showroom_cookie', '') + acfun_cookie = read_config_value(config, 'Cookie', 'acfun_cookie', '') + changliao_cookie = read_config_value(config, 'Cookie', 'changliao_cookie', '') + yinbo_cookie = read_config_value(config, 'Cookie', 'yinbo_cookie', '') + yingke_cookie = read_config_value(config, 'Cookie', 'yingke_cookie', '') + zhihu_cookie = read_config_value(config, 'Cookie', 'zhihu_cookie', '') + chzzk_cookie = read_config_value(config, 'Cookie', 'chzzk_cookie', '') + haixiu_cookie = read_config_value(config, 'Cookie', 'haixiu_cookie', '') + vvxqiu_cookie = read_config_value(config, 'Cookie', 'vvxqiu_cookie', '') + yiqilive_cookie = read_config_value(config, 'Cookie', '17live_cookie', '') + langlive_cookie = read_config_value(config, 'Cookie', 'langlive_cookie', '') + pplive_cookie = read_config_value(config, 'Cookie', 'pplive_cookie', '') + six_room_cookie = read_config_value(config, 'Cookie', '6room_cookie', '') + lehaitv_cookie = read_config_value(config, 'Cookie', 'lehaitv_cookie', '') + huamao_cookie = read_config_value(config, 'Cookie', 'huamao_cookie', '') + shopee_cookie = read_config_value(config, 'Cookie', 'shopee_cookie', '') + youtube_cookie = read_config_value(config, 'Cookie', 'youtube_cookie', '') + taobao_cookie = read_config_value(config, 'Cookie', 'taobao_cookie', '') + jd_cookie = read_config_value(config, 'Cookie', 'jd_cookie', '') + faceit_cookie = read_config_value(config, 'Cookie', 'faceit_cookie', '') + migu_cookie = read_config_value(config, 'Cookie', 'migu_cookie', '') + lianjie_cookie = read_config_value(config, 'Cookie', 'lianjie_cookie', '') + laixiu_cookie = read_config_value(config, 'Cookie', 'laixiu_cookie', '') + picarto_cookie = read_config_value(config, 'Cookie', 'picarto_cookie', '') + + video_save_type_list = ("FLV", "MKV", "TS", "MP4", "MP3音频", "M4A音频", "MP3", "M4A") + if video_save_type and video_save_type.upper() in video_save_type_list: + video_save_type = video_save_type.upper() + else: + video_save_type = "TS" + + check_path = video_save_path or default_path + if utils.check_disk_capacity(check_path, show=first_run) < disk_space_limit: + exit_recording = True + if not recording: + logger.warning(f"Disk space remaining is below {disk_space_limit} GB. " + f"Exiting program due to the disk space limit being reached.") + sys.exit(-1) + + + def contains_url(string: str) -> bool: + pattern = r"(https?://)?(www\.)?[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+(:\d+)?(/.*)?" + return re.search(pattern, string) is not None + + + try: + url_comments, line_list, url_line_list = [[] for _ in range(3)] + with (open(url_config_file, "r", encoding=text_encoding, errors='ignore') as file): + for origin_line in file: + if origin_line in line_list: + delete_line(url_config_file, origin_line) + line_list.append(origin_line) + line = origin_line.strip() + if len(line) < 18: + continue + + line_spilt = line.split('主播: ') + if len(line_spilt) > 2: + line = update_file(url_config_file, line, f'{line_spilt[0]}主播: {line_spilt[-1]}') + + is_comment_line = line.startswith("#") + if is_comment_line: + line = line.lstrip('#') + + if re.search('[,,]', line): + split_line = re.split('[,,]', line) + else: + split_line = [line, ''] + + if len(split_line) == 1: + url = split_line[0] + quality, name = [video_record_quality, ''] + elif len(split_line) == 2: + if contains_url(split_line[0]): + quality = video_record_quality + url, name = split_line + else: + quality, url = split_line + name = '' + else: + quality, url, name = split_line + + if quality not in ("原画", "蓝光", "超清", "高清", "标清", "流畅"): + quality = '原画' + + if url not in url_line_list: + url_line_list.append(url) + else: + delete_line(url_config_file, origin_line) + + url = 'https://' + url if '://' not in url else url + url_host = url.split('/')[2] + + platform_host = [ + 'live.douyin.com', + 'v.douyin.com', + 'www.douyin.com', + 'live.kuaishou.com', + 'www.huya.com', + 'www.douyu.com', + 'www.yy.com', + 'live.bilibili.com', + 'www.redelight.cn', + 'www.xiaohongshu.com', + 'xhslink.com', + 'www.bigo.tv', + 'slink.bigovideo.tv', + 'app.blued.cn', + 'cc.163.com', + 'qiandurebo.com', + 'fm.missevan.com', + 'look.163.com', + 'twitcasting.tv', + 'live.baidu.com', + 'weibo.com', + 'fanxing.kugou.com', + 'fanxing2.kugou.com', + 'mfanxing.kugou.com', + 'www.huajiao.com', + 'www.7u66.com', + 'wap.7u66.com', + 'live.acfun.cn', + 'm.acfun.cn', + 'live.tlclw.com', + 'wap.tlclw.com', + 'live.ybw1666.com', + 'wap.ybw1666.com', + 'www.inke.cn', + 'www.zhihu.com', + 'www.haixiutv.com', + "h5webcdnp.vvxqiu.com", + "17.live", + 'www.lang.live', + "m.pp.weimipopo.com", + "v.6.cn", + "m.6.cn", + 'www.lehaitv.com', + 'h.catshow168.com', + 'e.tb.cn', + 'huodong.m.taobao.com', + '3.cn', + 'eco.m.jd.com', + 'www.miguvideo.com', + 'm.miguvideo.com', + 'show.lailianjie.com', + 'www.imkktv.com', + 'www.picarto.tv' + ] + overseas_platform_host = [ + 'www.tiktok.com', + 'play.sooplive.co.kr', + 'm.sooplive.co.kr', + 'www.sooplive.com', + 'm.sooplive.com', + 'www.pandalive.co.kr', + 'www.winktv.co.kr', + 'www.flextv.co.kr', + 'www.ttinglive.com', + 'www.popkontv.com', + 'www.twitch.tv', + 'www.liveme.com', + 'www.showroom-live.com', + 'chzzk.naver.com', + 'm.chzzk.naver.com', + 'live.shopee.', + '.shp.ee', + 'www.youtube.com', + 'youtu.be', + 'www.faceit.com' + ] + + platform_host.extend(overseas_platform_host) + clean_url_host_list = ( + "live.douyin.com", + "live.bilibili.com", + "www.huajiao.com", + "www.zhihu.com", + "www.huya.com", + "chzzk.naver.com", + "www.liveme.com", + "www.haixiutv.com", + "v.6.cn", + "m.6.cn", + 'www.lehaitv.com' + ) + + if 'live.shopee.' in url_host or '.shp.ee' in url_host: + url_host = 'live.shopee.' if 'live.shopee.' in url_host else '.shp.ee' + + if url_host in platform_host or any(ext in url for ext in (".flv", ".m3u8")): + if url_host in clean_url_host_list: + url = update_file(url_config_file, old_str=url, new_str=url.split('?')[0]) + + if 'xiaohongshu' in url: + host_id = re.search('&host_id=(.*?)(?=&|$)', url) + if host_id: + new_url = url.split('?')[0] + f'?host_id={host_id.group(1)}' + url = update_file(url_config_file, old_str=url, new_str=new_url) + + url_comments = [i for i in url_comments if url not in i] + if is_comment_line: + url_comments.append(url) + else: + new_line = (quality, url, name) + url_tuples_list.append(new_line) + else: + if not origin_line.startswith('#'): + color_obj.print_colored(f"\r{origin_line.strip()} 本行包含未知链接.此条跳过", color_obj.YELLOW) + update_file(url_config_file, old_str=origin_line, new_str=origin_line, start_str='#') + + while len(need_update_line_list): + a = need_update_line_list.pop() + replace_words = a.split('|') + if replace_words[0] != replace_words[1]: + if replace_words[1].startswith("#"): + start_with = '#' + new_word = replace_words[1][1:] + else: + start_with = None + new_word = replace_words[1] + update_file(url_config_file, old_str=replace_words[0], new_str=new_word, start_str=start_with) + + text_no_repeat_url = list(set(url_tuples_list)) + + if len(text_no_repeat_url) > 0: + for url_tuple in text_no_repeat_url: + monitoring = len(running_list) + + if url_tuple[1] in not_record_list: + continue + if url_tuple[1] not in running_list: + print(f"\r{'新增' if not first_start else '传入'}地址: {url_tuple[1]}") + monitoring += 1 + args = [url_tuple, monitoring] + create_var[f'thread_{monitoring}'] = threading.Thread(target=start_record, args=args) + create_var[f'thread_{monitoring}'].daemon = True + create_var[f'thread_{monitoring}'].start() + running_list.append(url_tuple[1]) + time.sleep(local_delay_default) + + + + + url_tuples_list = [] + first_start = False + + + + except Exception as err: + logger.error(f"错误信息: {err} 发生错误的行数: {err.__traceback__.tb_lineno}") + + if first_run: + t = threading.Thread(target=display_info, args=(), daemon=True) + t.start() + t2 = threading.Thread(target=adjust_max_request, args=(), daemon=True) + t2.start() + first_run = False + + time.sleep(3) \ No newline at end of file