docs: 新增文档分支

This commit is contained in:
ikun 2024-04-10 23:35:16 +08:00
parent a769b23cdf
commit bd10b7ee9c
102 changed files with 3309 additions and 9261 deletions

View File

@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@ -1,66 +0,0 @@
name: build beta
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- '.gitignore'
pull_request:
branches: [main]
jobs:
build:
strategy:
matrix:
runs-on: [windows-latest, macos-latest, ubuntu-latest, ubuntu-20.04]
runs-on: ${{ matrix.runs-on }}
steps:
- name: Checkout git repo
uses: actions/checkout@v2
- name: Setup Python 3.11
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install pyinstaller
run: python -m pip install pyinstaller
- name: Install dependencies
run: python -m pip install -r ./requirements.txt
- name: Build
run: python build.py build test
- name: Upload
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.runs-on }}
path: ./dist
post:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout git repo
uses: actions/checkout@v2
- name: Download Build Files
uses: actions/download-artifact@v4
with:
path: dist
pattern: dist-*
merge-multiple: false
- name: Get File Address
run: |
echo "windowsFile=./dist/dist-windows-latest/lx-music-api-server_$(git rev-parse --short HEAD).exe" >> $GITHUB_ENV
echo "macosFile=./dist/dist-macos-latest/lx-music-api-server_$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "linuxFile=./dist/dist-ubuntu-latest/lx-music-api-server_$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Post to Channel
run: |
echo ${{ secrets.SCRIPT_BASE64 }} | base64 -d > test.py
ESCAPED=`python test.py`
curl -s -v "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMediaGroup?chat_id=${{ secrets.TELEGRAM_ID }}&media=%5B%7B%22type%22:%22document%22,%20%22media%22:%22attach://windows%22,%22parse_mode%22:%22MarkdownV2%22,%22caption%22:${ESCAPED}%7D%5D" -F windows=@${{ env.windowsFile }}

View File

@ -1,102 +0,0 @@
name: Build Release
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- '.gitignore'
permissions:
contents: write
pull-requests: write
jobs:
build:
if: contains(github.event.head_commit.message, '[release]')
strategy:
matrix:
runs-on: [windows-latest, macos-latest, ubuntu-latest]
runs-on: ${{ matrix.runs-on }}
steps:
- name: Checkout git repo
uses: actions/checkout@v2
- name: Setup Python 3.11
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install pyinstaller
run: python -m pip install pyinstaller
- name: Install dependencies
run: python -m pip install -r ./requirements.txt
- name: Build
run: python build.py build release
- name: Upload
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.runs-on }}
path: ./dist
release:
if: contains(github.event.head_commit.message, '[release]')
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Setup Python 3.11
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Get version
run: |
python -m pip install toml
echo PACKAGE_VERSION=`python -c 'import toml; print(toml.load("./pyproject.toml")["tool"]["poetry"]["version"])'` >> $GITHUB_ENV
- name: Generate Changelog
run: |
git fetch --prune --unshallow --tags
python build.py changelog >> ./changelog.md
- name: Create git tag
uses: pkgdeps/git-tag-action@v2
with:
github_token: ${{ github.token }}
github_repo: ${{ github.repository }}
version: ${{ env.PACKAGE_VERSION }}
git_commit_sha: ${{ github.sha }}
git_tag_prefix: "v"
- name: Download Build Files
uses: actions/download-artifact@v4
with:
path: dist
pattern: dist-*
merge-multiple: false
- name: Rename Files
run: |
mv ./dist/dist-windows-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }}.exe ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_windows.exe
mv ./dist/dist-macos-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }} ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_macos
mv ./dist/dist-ubuntu-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }} ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_ubuntu
- name: Release
uses: softprops/action-gh-release@v1
with:
body_path: ./changelog.md
prerelease: false
draft: false
tag_name: v${{ env.PACKAGE_VERSION }}
files: |
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_windows.exe
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_macos
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_ubuntu
env:
GITHUB_TOKEN: ${{ github.token }}

52
.github/workflows/deploy-docs.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: 部署文档
on:
push:
branches:
- docs
permissions:
contents: write
jobs:
deploy-gh-pages:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
# 如果你文档需要 Git 子模块,取消注释下一行
# submodules: true
- name: 设置 pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: 设置 Node.js
uses: actions/setup-node@v3
with:
node-version: 20
cache: pnpm
- name: 安装依赖
run: |
corepack enable
pnpm install --frozen-lockfile
- name: 构建文档
env:
NODE_OPTIONS: --max_old_space_size=8192
run: |-
pnpm run docs:build
> src/.vuepress/dist/.nojekyll
- name: 部署文档
uses: JamesIves/github-pages-deploy-action@v4
with:
# 部署文档
branch: gh-pages
folder: src/.vuepress/dist

46
.gitignore vendored
View File

@ -1,43 +1,5 @@
# Git Ignore for lx-music-api-server
# python
*.pyc
*.pyo
*.pyd
*.pyz
*/__pycache__/*
__pycache__/
*.egg-info
*.egg
# pyinstaller
*.spec
dist
build
# project
cache.db
data.db
test.*
*/test.*
logs
config.json
/config/config.json
/config/data.db
*.log
*.bak
*.tmp
# VSCode
.history
# Vim
*.swp
*.swp~
# GVim
*.un~
*.un
# temp script
lx-music-source-example.js
node_modules/
src/.vuepress/.cache/
src/.vuepress/.temp/
src/.vuepress/dist/

View File

@ -1,13 +0,0 @@
FROM python:3.10-alpine
WORKDIR /app
COPY ./main.py .
COPY ./common ./common
COPY ./modules ./modules
COPY ./requirements.txt .
# 指定源, 如果后期源挂了, 更换个源就可以.
RUN pip install --no-cache -i https://pypi.mirrors.ustc.edu.cn/simple/ -r requirements.txt
CMD [ "python", "main.py" ]

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 LX Music
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.

282
README.md
View File

@ -1,282 +0,0 @@
简体中文 | [English](README_EN.md)
<div align="center">
![lx-music-api-server-python](https://socialify.git.ci/lxmusics/lx-music-api-server-python/image?description=1&forks=1&issues=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2Flxmusics%2Flx-music-api-server-python%2Fmain%2Ficon.png&owner=1&pulls=1&stargazers=1&theme=Auto)
![GitHub Repo Size](https://img.shields.io/github/repo-size/lxmusics/lx-music-api-server-python?style=for-the-badge)
[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/lxmusics/lx-music-api-server-python/build_beta.yml?style=for-the-badge)](https://github.com/lxmusics/lx-music-api-server-python/actions/workflows/build_beta.yml)
[![GitHub Release (with filter)](https://img.shields.io/github/v/release/lxmusics/lx-music-api-server-python?style=for-the-badge)](https://github.com/lxmusics/lx-music-api-server-python/releases/latest)
[![GitHub All Releases](https://img.shields.io/github/downloads/lxmusics/lx-music-api-server-python/total?style=for-the-badge&color=violet)](https://github.com/lxmusics/lx-music-api-server-python/releases)
[![GitHub License](https://img.shields.io/github/license/lxmusics/lx-music-api-server-python?style=for-the-badge)](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE)
</div>
原仓库:[lx-music-api-server](https://github.com/lxmusics/lx-music-api-server)
你可以在原仓库中找到对应的可用源脚本
由于使用此项目导致的**封号**等情况**与本项目无关**
## 💡特点
- [ ] 功能
- [ ] 完整性API歌单搜索
- [ ] 网易云刷新登录
- [ ] 服务器向webdav
- [ ] WebUI
- [x] 客户端播放服务器上的本地音乐
- [x] Cookie池
- [x] https监听多端口监听
- [x] 反代兼容性
- [x] 获取更高的音质
- [x] QRC解密
- [ ] 本地化支持(目前仅支持简体中文)
- [x] 多端部署(`Windows` `Linux` `MacOS`
## 💻部署方法
### Release 部署(推荐)
1. 从 [Releases](https://github.com/lxmusics/lx-music-api-server-python/releases)
或 [Actions](https://github.com/lxmusics/lx-music-api-server-python/actions)
下载对应你系统的可执行文件 (从 GitHub Actions 下载需要登录 GitHub 账号)
2. 运行可执行文件(如果下载的文件是压缩包请先解压)
---
### Poetry 部署
环境要求: Python 3.8+
1. 安装 poetry
```bash
pip install poetry
```
2. clone本项目并进入项目目录
```bash
git clone https://github.com/lxmusics/lx-music-api-server-python.git
cd lx-music-api-server-python
```
3. 安装依赖
```bash
poetry install
```
4. 启动
```bash
poetry shell # 进入poetry环境
python main.py # 运行项目
```
---
### 直接部署
环境要求Python 3.6 - 3.11, 建议Python 3.8+
Python 3.12 及以上会装不上依赖
没有其他限制,能用 Python 理论上就能跑起来
linux 命令如果为 python3 请自行替换
1. clone本项目并进入项目目录
```bash
git clone https://github.com/lxmusics/lx-music-api-server-python.git
cd lx-music-api-server-python
```
2. 安装依赖
```bash
python -m pip install -r ./requirements.txt
```
3. 启动
```bash
python main.py
```
---
### Docker 部署
环境要求Docker
不知道需要的 Docker 版本,可以自己尝试现有的 Docker 版本是否可以使用
1. 更新软件包
```bash
sudo apt-get update
```
2. 安装Docker已有跳过
```bash
sudo apt-get install -y docker.io
```
3. 创建容器
```bash
docker run --name lx-music-api-server-python -p 9763:9763 -v /root/lx-music-api-server-python/config:/work/config -v /root/lx-music-api-server-python/logs:/work/logs --restart always -d ikun0014/lx-music-api-docker
```
4. 填写配置
前往/root/lx-music-api-server-python/config填写 (权限不足可修改创建容器命令)
## 📖返回码说明
接口返回值中`body.code`字段值中的代码含义
| 内容 | 含义 |
| ---- | ------------------------------------- |
| 0 | 成功 |
| 1 | IP 被封禁或不支持反代 |
| 2 | 获取失败 |
| 4 | 服务器内部错误(对应 statuscode 500 |
| 5 | 请求过于频繁 |
| 6 | 参数错误 |
接口返回的`statuscode`对应的代码含义
| 内容 | 含义 |
| ---- | ---------------------------------- |
| 200 | 成功 |
| 403 | IP 被封禁 |
| 400 | 参数错误 |
| 429 | 请求过于频繁 |
| 500 | 服务器内部错误(对应 body.code 4 |
## 🔖备注
### 本项目中可能会出现以下优秀代码
1. 三角形具有稳定性
```python
for a in xxx:
if (xxx):
if (xxx):
if (xxx):
for b in xxx:
if (xxx):
while (xxx):
pass
pass
pass
pass
pass
pass
pass
```
2. 能一行写完那就坚决不多行
```python
sys.stdout.write('\r|'+'=' * (int(dd['pares'].index(ds) / total * 50)) + ' ' * (49 - int(dd['pares'].index(ds) / total * 50)) + f'''|{int(dd['pares'].index(ds) / total * 100)}% xx''' + ds['title']+' ' * 20)
```
3. 不复用重复部分
```python
async def other(method, source, songid, _):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(songid)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
async def other_with_query(method, source, t, _, query):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(t, query)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
```
4. 模块不拆分
详见[config.py](https://github.com/lxmusics/lx-music-api-server-python/tree/main/common/config.py)
5. 不明所以的变量名
```python
a = '小明'
b = 1
c = 2
d = b''
def e(a, b, c):
c = xxx
d = xxx
f = e(c, b, a)
```
## 📄项目协议
本项目基于 [MIT](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE) 许可证发行,以下协议是对于 MIT 原协议的补充,如有冲突,以以下协议为准。
词语约定:本协议中的“本项目”指本音源项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。
1. 本项目的数据来源原理是从各官方音乐平台的公开服务器中拉取数据,经过对数据简单地筛选与合并后进行展示,因此本项目不对数据的准确性负责。
2. 使用本项目的过程中可能会产生版权数据,对于这些版权数据,本项目不拥有它们的所有权,为了避免造成侵权,使用者务必在**24 小时**内清除使用本项目的过程中所产生的版权数据。
3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。
4. 本项目完全免费,且开源发布于 GitHub 面向全世界人用作对技术的学习交流,本项目不对项目内的技术可能存在违反当地法律法规的行为作保证,**禁止在违反当地法律法规的情况下使用本项目**,对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。
若你使用了本项目,将代表你接受以上协议。
音乐平台不易,请尊重版权,支持正版。
本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。
若对此有疑问请 mail to:
helloplhm-qwq+outlook.com
folltoshe+foxmail.com
(请将`+`替换成`@`)
## ✨Star 趋势图
[![Stargazers over time](https://starchart.cc/lxmusics/lx-music-api-server-python.svg)](https://starchart.cc/lxmusics/lx-music-api-server-python)
## ⚙️贡献者
[![Contributor](https://contrib.rocks/image?repo=lxmusics/lx-music-api-server-python)](https://github.com/lxmusics/lx-music-api-server-python/graphs/contributors)

View File

@ -1,276 +0,0 @@
English | [简体中文](README.md)
<div align="center">
![lx-music-api-server-python](https://socialify.git.ci/lxmusics/lx-music-api-server-python/image?description=1&forks=1&issues=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2Flxmusics%2Flx-music-api-server-python%2Fmain%2Ficon.png&owner=1&pulls=1&stargazers=1&theme=Auto)
![GitHub Repo Size](https://img.shields.io/github/repo-size/lxmusics/lx-music-api-server-python?style=for-the-badge)
[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/lxmusics/lx-music-api-server-python/build_beta.yml?style=for-the-badge)](https://github.com/lxmusics/lx-music-api-server-python/actions/workflows/build_beta.yml)
[![GitHub Release (with filter)](https://img.shields.io/github/v/release/lxmusics/lx-music-api-server-python?style=for-the-badge)](https://github.com/lxmusics/lx-music-api-server-python/releases/latest)
[![GitHub All Releases](https://img.shields.io/github/downloads/lxmusics/lx-music-api-server-python/total?style=for-the-badge&color=violet)](https://github.com/lxmusics/lx-music-api-server-python/releases)
[![GitHub License](https://img.shields.io/github/license/lxmusics/lx-music-api-server-python?style=for-the-badge)](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE)
</div>
Original Repo: [lx-music-api-server](https://github.com/lxmusics/lx-music-api-server)
You can find the corresponding available source scripts in the original repository.
**Ban** and other situations caused by the use of this project have **nothing** to do with this project.
## 💡Feature
- [ ] Functions
- [ ] All APIs (playlist, search)
- [ ] Netease Cloud Refresh Login
- [ ] Client plays music on webdav
- [ ] WebUI
- [x] Client plays local music on the server
- [x] Cookie pool
- [x] HTTPS listening, multi port listening
- [x] Reverse proxy compatibility
- [x] Get higher quality
- [x] QRC decrypt
- [ ] Localization (currently only Simplified Chinese is supported)
- [x] Platform support: `Windows` `Linux` `MacOS`
## 💻How to deploy
### Use Release (recommended)
1. Download the executable file corresponding to your system from [Releases](https://github.com/lxmusics/lx-music-api-server-python/releases) or [Actions](https://github.com/lxmusics/lx-music-api-server-python/actions)
2. Run the downloaded executable file (maybe you need to unzip the downloaded file if it is a compressed files)
---
### Use Poetry
Required environment: Python 3.8+
1. Install poetry
```bash
pip install poetry
```
2. Clone this project and enter the project directory
```bash
git clone https://github.com/lxmusics/lx-music-api-server-python.git
cd lx-music-api-server-python
```
3. Install requirements
```bash
poetry install --no-root
```
4. Run it
```bash
poetry shell # enter poetry environment
python main.py # run project
```
---
### Directly deploy
Required environment: Python 3.6 - 3.11, Python 3.8+ is better.
Python 3.12 or higher maybe install requirements failed.
Without other restrictions, you can run only with Python.
If you are using linux, you command maybe python3, please replace it yourself.
1. Clone this project and enter the project directory
```bash
git clone https://github.com/lxmusics/lx-music-api-server-python.git
cd lx-music-api-server-python
```
2. Install requirements
```bash
python -m pip install -r ./requirements.txt
```
3. Run it
```bash
python main.py
```
---
### Use Docker
Required environment: Docker
We don't know the Docker version you need, you can try whether the existing Docker version can be used by yourself.
1. Update package
```bash
sudo apt-get update
```
2. install Docker (skip if you already have it)
```bash
sudo apt-get install -y docker.io
```
3. Create container
```bash
docker run --name lx-music-api-server-python -p 9763:9763 -v /root/lx-music-api-server-python/config:/work/config -v /root/lx-music-api-server-python/logs:/work/logs --restart always -d ikun0014/lx-music-api-docker
```
## 📖Return code description
The code meaning in the `body.code` field value in the interface return value.
| Value | Meaning |
| ---- | --------------------------------------------------- |
| 0 | Success |
| 1 | IP is banned or does not support anti-generation |
| 2 | Fail to obtain. |
| 4 | Server internal error (corresponding to statuscode 500) |
| 5 | Too frequent requests |
| 6 | Parameter error |
The code meaning of `statuscode` returned by the interface.
| Value | Meaning |
| ---- | -------------------------------------------------- |
| 200 | Success |
| 403 | IP is banned |
| 400 | Parameter error |
| 429 | Too frequent requests |
| 500 | Server internal error (corresponding to body.code 4) |
## 🔖Remarks
### The following excellent codes may appear in this project.
1. Triangle has stability.
```python
for a in xxx:
if (xxx):
if (xxx):
if (xxx):
for b in xxx:
if (xxx):
while (xxx):
pass
pass
pass
pass
pass
pass
pass
```
2. If you can finish it in one line, then don't write many lines.
```python
sys.stdout.write('\r|'+'=' * (int(dd['pares'].index(ds) / total * 50)) + ' ' * (49 - int(dd['pares'].index(ds) / total * 50)) + f'''|{int(dd['pares'].index(ds) / total * 100)}% xx''' + ds['title']+' ' * 20)
```
3. Do not reuse duplicate parts
```python
async def other(method, source, songid, _):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(songid)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
async def other_with_query(method, source, t, _, query):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(t, query)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
```
4. Module does not split
Details at [config.py](https://github.com/lxmusics/lx-music-api-server-python/tree/main/common/config.py)
5. Unknown variable name
```python
a = '小明'
b = 1
c = 2
d = b''
def e(a, b, c):
c = xxx
d = xxx
f = e(c, b, a)
```
## 📄Project agreement
This project is issued under [MIT](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE) license. The following agreement is a supplement to the original MIT agreement. In case of conflict, the following agreement shall prevail.
Word agreement: "this project" in this agreement refers to this audio source project; "User" means the user who signed this Agreement; "Official Music Platform" refers to the official platforms built in this project, including Cool Me, Cool Dog, Mi Gu and other music sources; "Copyright data" refers to data of which others have copyright, including but not limited to images, audio, names, etc.
1. The data source principle of this project is to pull data from the public servers of official music platforms, and display the data after simple screening and merging, so this project is not responsible for the accuracy of the data.
2. Copyright data may be generated during the use of this project, and this project does not own the copyright data. In order to avoid infringement, users must clear the copyright data generated during the use of this project within **24 hours**.
3. Any direct, indirect, special, accidental or consequential damages of any nature arising from the use of this project (including but not limited to damages caused by loss of goodwill, shutdown, computer failure or malfunction, or any and all other commercial damages or losses) shall be borne by the user.
4. This project is completely free of charge, and the open source is published on GitHub for people all over the world to learn and exchange technology. This project does not guarantee that the technology in the project may violate local laws and regulations. **It is forbidden to use this project in violation of local laws and regulations.** The user shall bear any illegal acts caused by the user knowing or not knowing that the local laws and regulations do not allow it, and this project will not bear any direct, indirect, special, accidental or consequential responsibilities.
If you use this project, you will accept the above agreement on your behalf.
Music platform is not easy, please respect copyright and support genuine.
This project is only used for the exploration and research of technical feasibility, and does not accept any commercial (including but not limited to advertising) cooperation and donation.
If you have any questions about this, please mail to:
helloplhm-qwq+outlook.com
folltoshe+foxmail.com
(please replace `+` to `@`)
## ✨Star trend chart
[![Stargazers over time](https://starchart.cc/lxmusics/lx-music-api-server-python.svg)](https://starchart.cc/lxmusics/lx-music-api-server-python)
## ⚙Contributor
[![Contributor](https://contrib.rocks/image?repo=lxmusics/lx-music-api-server-python)](https://github.com/lxmusics/lx-music-api-server-python/graphs/contributors)

234
build.py
View File

@ -1,234 +0,0 @@
import subprocess
import toml
import sys
import re
import os
def get_latest_tag():
t = subprocess.check_output(['git', 'tag']).decode('utf-8').strip().split("\n")
return t[-1] if (t[-1] != toml.load("./pyproject.toml")["tool"]["poetry"]["version"]) else t[-2]
def get_specified_tag(index):
r = subprocess.check_output(['git', 'tag']).decode('utf-8').strip().split("\n")
n = []
for i in r:
if (i):
n.append(i.strip())
return n[index]
def get_changelog():
cmd = ['git', 'log', f'{get_specified_tag(-1)}..HEAD', '--pretty=format:"%h %s"']
# print(cmd)
res = subprocess.check_output(cmd).decode('utf-8').strip()
res = res.split('\n')
featMsg = []
fixMsg = []
docsMsg = []
buildMsg = []
otherMsg = []
noticeMsg = []
unknownMsg = []
for msg in res:
if (re.match('[a-f0-9]*.(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|notice)\:', msg[1:-1])):
msg = msg[1:-1]
if msg[8:].startswith('notice:'):
noticeMsg.append(msg)
elif msg[8:].startswith('feat:'):
featMsg.append(msg)
elif msg[8:].startswith('fix:'):
fixMsg.append(msg)
elif msg[8:].startswith('docs:'):
docsMsg.append(msg)
elif msg[8:].startswith('build:'):
buildMsg.append(msg)
else:
otherMsg.append(msg)
else:
msg = msg[1:-1]
unknownMsg.append(msg)
# final
Nres = ''
if (len(noticeMsg) > 0):
Nres += '## 公告\n'
for msg in noticeMsg:
Nres += f'- {msg}\n'
if (len(featMsg) > 0):
Nres += '## 功能更新\n'
for msg in featMsg:
Nres += f'- {msg}\n'
if (len(fixMsg) > 0):
Nres += '## bug修复\n'
for msg in fixMsg:
Nres += f'- {msg}\n'
if (len(docsMsg) > 0):
Nres += '## 文档更新\n'
for msg in docsMsg:
Nres += f'- {msg}\n'
if (len(unknownMsg) > 0):
Nres += '## 未知类型的提交\n'
for msg in unknownMsg:
Nres += f'- {msg}\n'
if (len(buildMsg) > 0):
Nres += '## 构建配置\n'
for msg in buildMsg:
Nres += f'- {msg}\n'
if (len(otherMsg) > 0):
Nres += '## 其他\n'
for msg in otherMsg:
Nres += f'- {msg}\n'
return Nres.strip()
def get_python_path():
return sys.executable
def get_latest_commit_sha():
return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip()[0:7]
def build_test(fileName):
os.environ['build'] = 'T'
try:
import PyInstaller as UNUSED
except ModuleNotFoundError:
print('[ERROR]: Please install PyInstaller module first.',
'If you have the module installed,',
'Please check if you forgetting to activate the virtualenv.', sep='\n')
sys.exit(1)
sha = get_latest_commit_sha()
popen = subprocess.Popen([get_python_path(),
'-m',
'PyInstaller',
'-F',
'-i',
'icon.ico',
'--name',
fileName if fileName else f'lx-music-api-server_{sha}',
'main.py'])
print('PyInstaller process started, PID: ' + str(popen.pid))
print('Please wait for a while...')
popen.wait()
if (popen.returncode != 0):
print(f'[ERROR]: PyInstaller build with code {popen.returncode}.',
'Please check the output log,',
'this may inculde errors or warnings.', sep='\n')
sys.exit(popen.returncode)
else:
print('[SUCCESS]: PyInstaller build success.')
print('FilePath: ' + os.getcwd() + os.sep + os.listdir(os.getcwd() + '/dist')[0])
def build_release(fileName = ''):
os.environ['build'] = 'R'
try:
import PyInstaller as UNUSED
except ModuleNotFoundError:
print('[ERROR]: Please install PyInstaller module first.',
'If you have the module installed,',
'Please check if you forgetting to activate the virtualenv.', sep='\n')
sys.exit(1)
vername = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
popen = subprocess.Popen([get_python_path(),
'-m',
'PyInstaller',
'-F',
'-i',
'icon.ico',
'--name',
fileName if fileName else f'lx-music-api-server_{vername}',
'main.py'])
print('PyInstaller process started, PID: ' + str(popen.pid))
print('Please wait for a while...')
popen.wait()
if (popen.returncode != 0):
print(f'[ERROR]: PyInstaller build with code {popen.returncode}.',
'Please check the output log,',
'this may inculde errors or warnings.', sep='\n')
sys.exit(popen.returncode)
else:
print('[SUCCESS]: PyInstaller build success.')
print('FilePath: ' + os.getcwd() + os.sep + os.listdir(os.getcwd() + '/dist')[0])
argv = sys.argv
argv.pop(0)
commands = []
options = []
further_info_required_options = ['-f', '--fileName']
for arg in argv:
if (arg.startswith('-')):
options.append(arg)
if (arg in further_info_required_options):
options.append(argv[argv.index(arg) + 1])
else:
if (arg not in options):
commands.append(arg)
def main():
fileName = ''
for o in options:
if (o == '-f' or o == '--fileName'):
try:
fileName = options[options.index(o) + 1]
except:
print('[ERROR] No fileName specified')
sys.exit(1)
elif (o == '-h' or o == '--help'):
print('Usage: build.py [options] <command>')
print('Options:')
print(' -f, --filename <fileName> Specify the fileName of the executable.')
print(' -h, --help Show this help message and exit.')
print('Commands:')
print(' build test Build test executable.')
print(' build release Build release executable.')
print(' changelog Show changelog.')
sys.exit(0)
elif (o.startswith('-')):
print(f'[ERROR] Invalid option "{o}" specified.')
sys.exit(1)
if (len(commands) == 0):
print('[ERROR] No command specified')
sys.exit(0)
try:
if (commands[0] == 'build'):
if (len(commands) == 1):
print('[WARNING] No build command specified, defaulting to build test.')
build_test(fileName)
sys.exit(0)
elif (commands[1] == 'test'):
build_test(fileName)
sys.exit(0)
elif (commands[1] == 'release'):
build_release(fileName)
sys.exit(0)
else:
print('[ERROR] Invalid build command specified.')
sys.exit(1)
elif (commands[0] == 'changelog'):
print(get_changelog())
sys.exit(0)
else:
print('[ERROR] Invalid command specified.')
sys.exit(1)
except IndexError:
print('[ERROR] Invalid command specified.')
sys.exit(1)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('[INFO] Aborting...')
sys.exit(0)

View File

@ -1,40 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: EncryptUtils.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from Crypto.Cipher import AES, DES
import binascii
import base64
def createAesEncrypt(plainText, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv)
if isinstance(plainText, str):
plainText = plainText.encode('utf-8')
return cipher.encrypt(pad(plainText))
def createAesDecrypt(cipherText, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(cipherText))
def createAesEncryptByHex(cipherText, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv)
if isinstance(cipherText, str):
cipherText = cipherText.encode('utf-8')
return unpad(cipher.decrypt(binascii.unhexlify(cipherText)))
def createAesEncryptByBase64(cipherText, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv)
if isinstance(cipherText, str):
cipherText = cipherText.encode('utf-8')
return unpad(cipher.decrypt(base64.b64decode(cipherText)))
def pad(s):
return s + (16 - len(s) % 16) * chr(16 - len(s) % 16)
def unpad(s):
return s[:-ord(s[len(s) - 1:])]

View File

@ -1,322 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: Httpx.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import aiohttp
import requests
import random
import traceback
import zlib
import ujson as json
import re
import time
import pickle
from . import log
from . import config
from . import utils
from . import variable
def is_valid_utf8(text) -> bool:
try:
if isinstance(text, bytes):
text = text.decode('utf-8')
# 判断是否为有效的utf-8字符串
if "\ufffe" in text:
return False
try:
text.encode('utf-8').decode('utf-8')
return True
except UnicodeDecodeError:
return False
except:
logger.error(traceback.format_exc())
return False
def is_plain_text(text) -> bool:
# 判断是否为纯文本
pattern = re.compile(r'[^\x00-\x7F]')
return not bool(pattern.search(text))
def convert_dict_to_form_string(dic: dict) -> str:
# 将字典转换为表单字符串
return '&'.join([f'{k}={v}' for k, v in dic.items()])
def log_plaintext(text: str) -> str:
if (text.startswith('{') and text.endswith('}')):
try:
text = json.loads(text)
except:
pass
elif (text.startswith('<xml') and text.endswith('>')): # xml data
try:
text = f'xml: {utils.load_xml(text)}'
except:
pass
return text
# 内置的UA列表
ua_list = [ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0 uacq',
'Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 uacq',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'
]
# 日志记录器
logger = log.log('http_utils')
def request(url: str, options = {}) -> requests.Response:
'''
Http请求主函数, 用于发送网络请求
- url: 需要请求的URL地址(必填)
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
- method: 请求方法
- headers: 请求头
- body: 请求体(也可使用python原生requests库的data参数)
- form: 提交的表单数据
- cache: 缓存设置
- no-cache: 不缓存
- <int>: 缓存可用秒数
- cache-ignore: <list> 缓存忽略关键字
@ return: requests.Response类型的响应数据
'''
# 缓存读取
cache_key = f'{url}{options}'
if (isinstance(options.get('cache-ignore'), list)):
for i in options.get('cache-ignore'):
cache_key = cache_key.replace(str(i), '')
options.pop('cache-ignore')
cache_key = utils.createMD5(cache_key)
if options.get("cache") and options["cache"] != "no-cache":
cache = config.getCache("httpx", cache_key)
if cache:
logger.debug(f"请求 {url} 有可用缓存")
return pickle.loads(utils.createBase64Decode(cache["data"]))
if "cache" in list(options.keys()):
cache_info = options.get("cache")
options.pop("cache")
else:
cache_info = None
# 获取请求方法没有则默认为GET请求
try:
method = options['method'].upper()
options.pop('method')
except Exception as e:
method = 'GET'
# 获取User-Agent没有则从ua_list中随机选择一个
try:
d_lower = {k.lower(): v for k, v in options['headers'].items()}
useragent = d_lower['user-agent']
except:
try:
options['headers']['User-Agent'] = random.choice(ua_list)
except:
options['headers'] = {}
options['headers']['User-Agent'] = random.choice(ua_list)
# 检查是否在国内
if ((not variable.iscn) and (not options["headers"].get("X-Forwarded-For"))):
options["headers"]["X-Forwarded-For"] = variable.fakeip
# 获取请求主函数
try:
reqattr = getattr(requests, method.lower())
except AttributeError:
raise AttributeError('Unsupported method: '+method)
# 请求前记录
logger.debug(f'HTTP Request: {url}\noptions: {options}')
# 转换body/form参数为原生的data参数并为form请求追加Content-Type头
if (method == 'POST') or (method == 'PUT'):
if options.get('body'):
options['data'] = options['body']
options.pop('body')
if options.get('form'):
options['data'] = convert_dict_to_form_string(options['form'])
options.pop('form')
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
if (isinstance(options['data'], dict)):
options['data'] = json.dumps(options['data'])
# 进行请求
try:
logger.info("-----start----- " + url)
req = reqattr(url, **options)
except Exception as e:
logger.error(f'HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}')
raise e
# 请求后记录
logger.debug(f'Request to {url} succeed with code {req.status_code}')
if (req.content.startswith(b'\x78\x9c') or req.content.startswith(b'\x78\x01')): # zlib headers
try:
decompressed = zlib.decompress(req.content)
if (is_valid_utf8(decompressed)):
logger.debug(log_plaintext(decompressed.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
except:
logger.debug('response is not text binary, ignore logging it')
else:
if (is_valid_utf8(req.content)):
logger.debug(log_plaintext(req.content.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
# 缓存写入
if (cache_info and cache_info != "no-cache"):
cache_data = pickle.dumps(req)
expire_time = (cache_info if isinstance(cache_info, int) else 3600) + int(time.time())
config.updateCache("httpx", cache_key, {"expire": True, "time": expire_time, "data": utils.createBase64Encode(cache_data)})
logger.debug("缓存已更新: " + url)
def _json():
return json.loads(req.content)
setattr(req, 'json', _json)
# 返回请求
return req
def checkcn():
try:
req = request("https://mips.kugou.com/check/iscn?&format=json")
body = utils.CreateObject(req.json())
variable.iscn = bool(body.flag)
if (not variable.iscn):
variable.fakeip = config.read_config('common.fakeip')
logger.info(f"您在非中国大陆服务器({body.country})上启动了项目已自动开启ip伪装")
logger.warning("此方式无法解决咪咕音乐的链接获取问题,您可以配置代理,服务器地址可在下方链接中找到\nhttps://hidemy.io/cn/proxy-list/?country=CN#list")
except Exception as e:
logger.warning('检查服务器位置失败,已忽略')
logger.warning(traceback.format_exc())
class ClientResponse:
# 这个类为了方便aiohttp响应与requests响应的跨类使用也为了解决pickle无法缓存的问题
def __init__(self, status, content, headers):
self.status = status
self.content = content
self.headers = headers
self.text = content.decode("utf-8", errors='ignore')
def json(self):
return json.loads(self.content)
async def convert_to_requests_response(aiohttp_response) -> ClientResponse:
content = await aiohttp_response.content.read() # 从aiohttp响应中读取字节数据
status_code = aiohttp_response.status # 获取状态码
headers = dict(aiohttp_response.headers.items()) # 获取标头信息并转换为字典
return ClientResponse(status_code, content, headers)
async def AsyncRequest(url, options = {}) -> ClientResponse:
'''
Http异步请求主函数, 用于发送网络请求
- url: 需要请求的URL地址(必填)
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
- method: 请求方法
- headers: 请求头
- body: 请求体(也可使用python原生requests库的data参数)
- form: 提交的表单数据
- cache: 缓存设置
- no-cache: 不缓存
- <int>: 缓存可用秒数
- cache-ignore: <list> 缓存忽略关键字
@ return: common.Httpx.ClientResponse类型的响应数据
'''
if (not variable.aioSession):
variable.aioSession = aiohttp.ClientSession(trust_env=True)
# 缓存读取
cache_key = f'{url}{options}'
if (isinstance(options.get('cache-ignore'), list)):
for i in options.get('cache-ignore'):
cache_key = cache_key.replace(str(i), '')
options.pop('cache-ignore')
cache_key = utils.createMD5(cache_key)
if options.get("cache") and options["cache"] != "no-cache":
cache = config.getCache("httpx_async", cache_key)
if cache:
logger.debug(f"请求 {url} 有可用缓存")
c = pickle.loads(utils.createBase64Decode(cache["data"]))
return c
if "cache" in list(options.keys()):
cache_info = options.get("cache")
options.pop("cache")
else:
cache_info = None
# 获取请求方法没有则默认为GET请求
try:
method = options['method']
options.pop('method')
except Exception as e:
method = 'GET'
# 获取User-Agent没有则从ua_list中随机选择一个
try:
d_lower = {k.lower(): v for k, v in options['headers'].items()}
useragent = d_lower['user-agent']
except:
try:
options['headers']['User-Agent'] = random.choice(ua_list)
except:
options['headers'] = {}
options['headers']['User-Agent'] = random.choice(ua_list)
# 检查是否在国内
if ((not variable.iscn) and (not options["headers"].get("X-Forwarded-For"))):
options["headers"]["X-Forwarded-For"] = variable.fakeip
# 获取请求主函数
try:
reqattr = getattr(variable.aioSession, method.lower())
except AttributeError:
raise AttributeError('Unsupported method: '+method)
# 请求前记录
logger.debug(f'HTTP Request: {url}\noptions: {options}')
# 转换body/form参数为原生的data参数并为form请求追加Content-Type头
if (method == 'POST') or (method == 'PUT'):
if (options.get('body') is not None):
options['data'] = options['body']
options.pop('body')
if (options.get('form') is not None):
options['data'] = convert_dict_to_form_string(options['form'])
options.pop('form')
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
if (isinstance(options.get('data'), dict)):
options['data'] = json.dumps(options['data'])
# 进行请求
try:
logger.info("-----start----- " + url)
req_ = await reqattr(url, **options)
except Exception as e:
logger.error(f'HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}')
raise e
# 请求后记录
logger.debug(f'Request to {url} succeed with code {req_.status}')
# 为懒人提供的不用改代码移植的方法
# 才不是梓澄呢
req = await convert_to_requests_response(req_)
if (req.content.startswith(b'\x78\x9c') or req.content.startswith(b'\x78\x01')): # zlib headers
try:
decompressed = zlib.decompress(req.content)
if (is_valid_utf8(decompressed)):
logger.debug(log_plaintext(decompressed.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
except:
logger.debug('response is not text binary, ignore logging it')
else:
if (is_valid_utf8(req.content)):
logger.debug(log_plaintext(req.content.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
# 缓存写入
if (cache_info and cache_info != "no-cache"):
cache_data = pickle.dumps(req)
expire_time = (cache_info if isinstance(cache_info, int) else 3600) + int(time.time())
config.updateCache("httpx_async", cache_key, {"expire": True, "time": expire_time, "data": utils.createBase64Encode(cache_data)})
logger.debug("缓存已更新: " + url)
# 返回请求
return req

View File

View File

@ -1,797 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: config.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import ujson as json
import time
import os
import traceback
import sys
import sqlite3
import shutil
from . import variable
from .log import log
import threading
logger = log('config_manager')
# 创建线程本地存储对象
local_data = threading.local()
def get_data_connection():
# 检查线程本地存储对象是否存在连接对象,如果不存在则创建一个新的连接对象
if (not hasattr(local_data, 'connection')):
local_data.connection = sqlite3.connect('./config/data.db')
return local_data.connection
# 创建线程本地存储对象
local_cache = threading.local()
def get_cache_connection():
# 检查线程本地存储对象是否存在连接对象,如果不存在则创建一个新的连接对象
if not hasattr(local_cache, 'connection'):
local_cache.connection = sqlite3.connect('./cache.db')
return local_cache.connection
class ConfigReadException(Exception):
pass
default = {
"common": {
"host": "0.0.0.0",
"_host-desc": "服务器启动时所使用的HOST地址",
"ports": [ 9763 ],
"_ports-desc": "服务器启动时所使用的端口",
"ssl_info": {
"desc": "服务器https配置is_https是这个服务器是否是https服务器如果你使用了反向代理来转发这个服务器如果它使用了https也请将它设置为true",
"is_https": False,
"enable": False,
"ssl_ports": [ 443 ],
"path": {
"desc": "ssl证书的文件地址",
"cert": "/path/to/your/cer",
"privkey": "/path/to/your/private/key",
},
},
"reverse_proxy": {
"desc": "针对类似于nginx一类的反代的配置",
"allow_proxy": True,
"_allow_proxy-desc": "是否允许反代",
"proxy_whitelist_remote": [
"反代时允许的ip来源列表通常为127.0.0.1",
"127.0.0.1"
],
"real_ip_header": 'X-Real-IP',
"_real_ip_header-desc": "反代来源ip的来源头不懂请保持默认",
},
"debug_mode": False,
"_debug_mode-desc": "是否开启调试模式",
"log_length_limit": 500,
"_log_length_limit-desc": "单条日志长度限制",
"fakeip": "1.0.1.114",
"_fakeip-desc": "服务器在海外时的IP伪装值",
"proxy": {
"enable": False,
"http_value": "http://127.0.0.1:7890",
"https_value": "http://127.0.0.1:7890",
},
"_proxy-desc": "代理配置HTTP与HTTPS协议需分开配置",
"log_file": True,
"_log_file-desc": "是否开启日志文件",
'cookiepool': False,
'_cookiepool-desc': '是否开启cookie池这将允许用户配置多个cookie并在请求时随机使用一个启用后请在module.cookiepool中配置cookie在user处配置的cookie会被忽略cookiepool中格式统一为列表嵌套user处的cookie的字典',
"allow_download_script": True,
'_allow_download_script-desc': '是否允许直接从服务端下载脚本,开启后可以直接访问 /script?key=你的请求key 下载脚本',
"download_config": {
"desc": "源脚本的相关配置dev为是否启用开发模式",
"name": "修改为你的源脚本名称",
"intro": "修改为你的源脚本描述",
"author": "修改为你的源脚本作者",
"version": "修改为你的源版本",
"filename": "lx-music-source.js",
"_filename-desc": "客户端保存脚本时的文件名(可能因浏览器不同出现不一样的情况)",
"dev": True,
"quality": {
"kw": ["128k"],
"kg": ["128k"],
"tx": ["128k"],
"wy": ["128k"],
"mg": ["128k"],
}
},
"local_music": {
"desc": "服务器侧本地音乐相关配置,请确保你的带宽足够",
"audio_path": "./audio",
"temp_path": "./temp",
}
},
"security": {
"rate_limit": {
"global": 0,
"ip": 0,
"desc": "请求速率限制global为全局ip为单个ip填入的值为至少间隔多久才能进行一次请求单位不限制请填为0"
},
"key": {
"enable": False,
"_enable-desc": "是否开启请求key开启后只有请求头中包含key且值一样时可以访问API",
"ban": True,
"values": ["114514"],
},
"whitelist_host": [
"localhost",
"0.0.0.0",
"127.0.0.1",
],
"_whitelist_host-desc": "强制白名单HOST不需要加端口号即不受其他安全设置影响的HOST",
"check_lxm": False,
"_check_lxm-desc": "是否检查lxm请求头正常的LX Music请求时都会携带这个请求头",
"lxm_ban": True,
"_lxm_ban-desc": "lxm请求头不存在或不匹配时是否将用户IP加入黑名单",
"allowed_host": {
"desc": "HOST允许列表启用后只允许列表内的HOST访问服务器不需要加端口号",
"enable": False,
"blacklist": {
"desc": "当用户访问的HOST并不在允许列表中时是否将请求IP加入黑名单长度单位",
"enable": False,
"length": 0,
},
"list": [
"localhost",
"0.0.0.0",
"127.0.0.1",
],
},
"banlist": {
"desc": "是否启用黑名单(全局设置,关闭后已存储的值并不受影响,但不会再检查)",
"enable": True,
"expire": {
"desc": "是否启用黑名单IP过期关闭后其他地方的配置会失效",
"enable": True,
"length": 86400 * 7, # 七天
},
}
},
"module": {
"kg": {
"desc": "酷狗音乐相关配置",
"client": {
"desc": "客户端请求配置,不懂请保持默认,修改请统一为字符串格式",
"appid": "1005",
"_appid-desc": "酷狗音乐的appid官方安卓为1005官方PC为1001",
"signatureKey": "OIlwieks28dk2k092lksi2UIkp",
"_signatureKey-desc": "客户端signature采用的key值需要与appid对应",
"clientver": "12029",
"_clientver-desc": "客户端versioncodepidversionsecret可能随此值而变化",
"pidversionsecret": "57ae12eb6890223e355ccfcb74edf70d",
"_pidversionsecret-desc": "获取URL时所用的key值计算验证值",
"pid": "2",
},
"tracker": {
"desc": "trackerapi请求配置不懂请保持默认修改请统一为字符串格式",
"host": "https://gateway.kugou.com",
"path": "/v5/url",
"version": "v5",
"x-router": {
"desc": "当host为gateway.kugou.com时需要追加此头为tracker类地址时则不需要",
"enable": True,
"value": "tracker.kugou.com",
},
"extra_params": {},
"_extra_params-desc": "自定义添加的param优先级大于默认填写类型为普通的JSON数据会自动转换为请求param",
},
"user": {
"desc": "此处内容请统一抓包获取需要vip账号来获取会员歌曲如果没有请留为空值mid必填可以瞎填一段数字",
"token": "",
"userid": "0",
"mid": "114514",
"lite_sign_in": {
"desc": "是否启用概念版自动签到仅在appid=3116时运行",
"enable": False,
"interval": 86400,
"mixsongmid": {
"desc": "mix_songmid的获取方式, 默认auto, 可以改成一个数字手动",
"value": "auto"
}
},
"refresh_token": {
"desc": "酷狗token保活相关配置30天不刷新token会失效enable是否启动interval刷新间隔。默认appid=1005时有效3116需要更换signatureKey",
"enable": False,
"interval": 86000,
"login_url": "http://login.user.kugou.com/v4/login_by_token"
}
}
},
"tx": {
"desc": "QQ音乐相关配置",
"vkeyserver": {
"desc": "请求官方api时使用的guiduin等信息不需要与cookie中信息一致",
"guid": "114514",
"uin": "10086",
},
"user": {
"desc": "用户数据可以通过浏览器获取需要vip账号来获取会员歌曲如果没有请留为空值qqmusic_key可以从Cookie中/客户端的请求体中comm.authst获取",
"qqmusic_key": "",
"uin": "",
"_uin-desc": "key对应的QQ号",
'refresh_login': {
'desc': '刷新登录相关配置enable是否启动interval刷新间隔',
'enable': False,
'interval': 86000
}
},
"cdnaddr": "http://ws.stream.qqmusic.qq.com/",
},
"wy": {
"desc": "网易云音乐相关配置",
"user": {
"desc": "账号cookie数据可以通过浏览器获取需要vip账号来获取会员歌曲如果没有请留为空值",
"cookie": ""
},
},
"mg": {
"desc": "咪咕音乐相关配置",
"user": {
"desc": "研究不深后两项自行抓包获取网页端cookie",
"by": "",
"session": "",
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36",
"refresh_login": {
"enable": False,
"interval": 86400,
"desc": "进行cookie保活"
}
},
},
"kw": {
"desc": "酷我音乐相关配置proto支持值['bd-api', 'kuwodes']",
"proto": "bd-api",
"user": {
"uid": "0",
"token": "",
"device_id": "0",
},
"des": {
"desc": "kuwodes接口mobi, nmobi一类的加密相关配置",
"f": "kuwo",
"need_encrypt": True,
"param填写注释": "{songId}为歌曲id, {map_quality}为map后的歌曲音质酷我规范, {raw_quality}为请求时的歌曲音质LX规范, {ext}为歌曲文件扩展名",
"params": "type=convert_url_with_sign&rid={songId}&quality={map_quality}&ext={ext}",
"host": "nmobi.kuwo.cn",
"path": "mobi.s",
"response_types": ['这里是reponse_type的所有支持值当设置为json时会使用到下面的两个值来获取url/bitrate如果为text则为传统的逐行解析方式', 'json', 'text'],
"response_type": "json",
"url_json_path": "data.url",
"bitrate_json_path": "data.bitrate",
"headers": {
"User-Agent": 'okhttp/3.10.0'
}
}
},
'cookiepool': {
'kg': [
{
'userid': '0',
'token': '',
'mid': '114514',
"lite_sign_in": {
"desc": "是否启用概念版自动签到仅在appid=3116时运行",
"enable": False,
"interval": 86400,
"mixsongmid": {
"desc": "mix_songmid的获取方式, 默认auto, 可以改成一个数字手动",
"value": "auto"
}
}
},
],
'tx': [
{
'qqmusic_key': '',
'uin': '',
'refresh_login': {
'desc': 'cookie池中对于此账号刷新登录的配置账号间互不干扰',
'enable': False,
'interval': 86000,
}
}
],
'wy': [
{
'cookie': '',
}
],
'mg': [
{
'by': '',
'session': '',
'useragent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36',
"refresh_login": {
"enable": False,
"interval": 86400
}
}
],
'kw': [
{
"uid": "0",
"token": "",
"device_id": "0",
},
]
},
},
}
def handle_default_config():
with open("./config/config.json", "w", encoding="utf-8") as f:
f.write(json.dumps(default, indent=2, ensure_ascii=False,
escape_forward_slashes=False))
f.close()
if (not os.getenv('build')):
logger.info('首次启动或配置文件被删除,已创建默认配置文件')
logger.info(
f'\n建议您到{variable.workdir + os.path.sep}config.json修改配置后重新启动服务器')
return default
class ConfigReadException(Exception):
pass
def load_data():
config_data = {}
try:
# Connect to the database
conn = get_data_connection()
cursor = conn.cursor()
# Retrieve all configuration data from the 'config' table
cursor.execute("SELECT key, value FROM data")
rows = cursor.fetchall()
for row in rows:
key, value = row
config_data[key] = json.loads(value)
except Exception as e:
logger.error(f"Error loading config: {str(e)}")
logger.error(traceback.format_exc())
return config_data
def save_data(config_data):
try:
# Connect to the database
conn = get_data_connection()
cursor = conn.cursor()
# Clear existing data in the 'data' table
cursor.execute("DELETE FROM data")
# Insert the new configuration data into the 'data' table
for key, value in config_data.items():
cursor.execute(
"INSERT INTO data (key, value) VALUES (?, ?)", (key, json.dumps(value)))
conn.commit()
except Exception as e:
logger.error(f"Error saving config: {str(e)}")
logger.error(traceback.format_exc())
def getCache(module, key):
try:
# 连接到数据库(如果数据库不存在,则会自动创建)
conn = get_cache_connection()
# 创建一个游标对象
cursor = conn.cursor()
cursor.execute("SELECT data FROM cache WHERE module=? AND key=?",
(module, key))
result = cursor.fetchone()
if result:
cache_data = json.loads(result[0])
cache_data["time"] = int(cache_data["time"])
if (not cache_data['expire']):
return cache_data
if (int(time.time()) < int(cache_data['time'])):
return cache_data
except:
pass
# traceback.print_exc()
return False
def updateCache(module, key, data):
try:
# 连接到数据库(如果数据库不存在,则会自动创建)
conn = get_cache_connection()
# 创建一个游标对象
cursor = conn.cursor()
cursor.execute(
"SELECT data FROM cache WHERE module=? AND key=?", (module, key))
result = cursor.fetchone()
if result:
cursor.execute(
"UPDATE cache SET data = ? WHERE module = ? AND key = ?", (json.dumps(data), module, key))
else:
cursor.execute(
"INSERT INTO cache (module, key, data) VALUES (?, ?, ?)", (module, key, json.dumps(data)))
conn.commit()
except:
logger.error('缓存写入遇到错误…')
logger.error(traceback.format_exc())
def resetRequestTime(ip):
config_data = load_data()
try:
try:
config_data['requestTime'][ip] = 0
except KeyError:
config_data['requestTime'] = {}
config_data['requestTime'][ip] = 0
save_data(config_data)
except:
logger.error('配置写入遇到错误…')
logger.error(traceback.format_exc())
def updateRequestTime(ip):
try:
config_data = load_data()
try:
config_data['requestTime'][ip] = time.time()
except KeyError:
config_data['requestTime'] = {}
config_data['requestTime'][ip] = time.time()
save_data(config_data)
except:
logger.error('配置写入遇到错误...')
logger.error(traceback.format_exc())
def getRequestTime(ip):
config_data = load_data()
try:
value = config_data['requestTime'][ip]
except:
value = 0
return value
def read_data(key):
config = load_data()
keys = key.split('.')
value = config
for k in keys:
if k not in value and keys.index(k) != len(keys) - 1:
value[k] = {}
elif k not in value and keys.index(k) == len(keys) - 1:
value = None
value = value[k]
return value
def write_data(key, value):
config = load_data()
keys = key.split('.')
current = config
for k in keys[:-1]:
if k not in current:
current[k] = {}
current = current[k]
current[keys[-1]] = value
save_data(config)
def push_to_list(key, obj):
config = load_data()
keys = key.split('.')
current = config
for k in keys[:-1]:
if k not in current:
current[k] = {}
current = current[k]
if keys[-1] not in current:
current[keys[-1]] = []
current[keys[-1]].append(obj)
save_data(config)
def write_config(key, value):
config = None
with open('./config/config.json', 'r', encoding='utf-8') as f:
config = json.load(f)
keys = key.split('.')
current = config
for k in keys[:-1]:
if k not in current:
current[k] = {}
current = current[k]
current[keys[-1]] = value
variable.config = config
with open('./config/config.json', 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False,
escape_forward_slashes=False)
f.close()
def read_default_config(key):
try:
config = default
keys = key.split('.')
value = config
for k in keys:
if isinstance(value, dict):
if k not in value and keys.index(k) != len(keys) - 1:
value[k] = {}
elif k not in value and keys.index(k) == len(keys) - 1:
value = None
value = value[k]
else:
value = None
break
return value
except:
return None
def _read_config(key):
try:
config = variable.config
keys = key.split('.')
value = config
for k in keys:
if isinstance(value, dict):
if k not in value and keys.index(k) != len(keys) - 1:
value[k] = None
elif k not in value and keys.index(k) == len(keys) - 1:
value = None
value = value[k]
else:
value = None
break
return value
except (KeyError, TypeError):
return None
def read_config(key):
try:
config = variable.config
keys = key.split('.')
value = config
for k in keys:
if isinstance(value, dict):
if k not in value and keys.index(k) != len(keys) - 1:
value[k] = {}
elif k not in value and keys.index(k) == len(keys) - 1:
value = None
value = value[k]
else:
value = None
break
return value
except:
default_value = read_default_config(key)
if (isinstance(default_value, type(None))):
logger.warning(f'配置文件{key}不存在')
else:
for i in range(len(keys)):
tk = '.'.join(keys[:(i + 1)])
tkvalue = _read_config(tk)
logger.debug(f'configfix: 读取配置文件{tk}的值:{tkvalue}')
if ((tkvalue is None) or (tkvalue == {})):
write_config(tk, read_default_config(tk))
logger.info(f'配置文件{tk}不存在,已创建')
return default_value
def write_data(key, value):
config = load_data()
keys = key.split('.')
current = config
for k in keys[:-1]:
if k not in current:
current[k] = {}
current = current[k]
current[keys[-1]] = value
save_data(config)
def initConfig():
if (not os.path.exists('./config')):
os.mkdir('config')
if (os.path.exists('./config.json')):
shutil.move('config.json','./config')
if (os.path.exists('./data.db')):
shutil.move('./data.db','./config')
try:
with open("./config/config.json", "r", encoding="utf-8") as f:
try:
variable.config = json.loads(f.read())
if (not isinstance(variable.config, dict)):
logger.warning('配置文件并不是一个有效的字典,使用默认值')
variable.config = default
with open("./config/config.json", "w", encoding="utf-8") as f:
f.write(json.dumps(variable.config, indent=2,
ensure_ascii=False, escape_forward_slashes=False))
f.close()
except:
if os.path.getsize("./config/config.json") != 0:
logger.error("配置文件加载失败请检查是否遵循JSON语法规范")
sys.exit(1)
else:
variable.config = handle_default_config()
except FileNotFoundError:
variable.config = handle_default_config()
# print(variable.config)
variable.log_length_limit = read_config('common.log_length_limit')
variable.debug_mode = read_config('common.debug_mode')
logger.debug("配置文件加载成功")
conn = sqlite3.connect('./cache.db')
# 创建一个游标对象
cursor = conn.cursor()
# 创建一个表来存储缓存数据
cursor.execute('''CREATE TABLE IF NOT EXISTS cache
(id INTEGER PRIMARY KEY AUTOINCREMENT,
module TEXT NOT NULL,
key TEXT NOT NULL,
data TEXT NOT NULL)''')
conn.close()
conn2 = sqlite3.connect('./config/data.db')
# 创建一个游标对象
cursor2 = conn2.cursor()
cursor2.execute('''CREATE TABLE IF NOT EXISTS data
(key TEXT PRIMARY KEY,
value TEXT)''')
conn2.close()
logger.debug('数据库初始化成功')
# handle data
all_data_keys = {'banList': [], 'requestTime': {}, 'banListRaw': []}
data = load_data()
if (data == {}):
write_data('banList', [])
write_data('requestTime', {})
logger.info('数据库内容为空,已写入默认值')
for k, v in all_data_keys.items():
if (k not in data):
write_data(k, v)
logger.info(f'数据库中不存在{k},已创建')
# 处理代理配置
if (read_config('common.proxy.enable')):
if (read_config('common.proxy.http_value')):
os.environ['http_proxy'] = read_config('common.proxy.http_value')
logger.info('HTTP协议代理地址: ' +
read_config('common.proxy.http_value'))
if (read_config('common.proxy.https_value')):
os.environ['https_proxy'] = read_config('common.proxy.https_value')
logger.info('HTTPS协议代理地址: ' +
read_config('common.proxy.https_value'))
logger.info('代理功能已开启,请确保代理地址正确,否则无法连接网络')
# cookie池
if (read_config('common.cookiepool')):
logger.info('已启用cookie池功能请确定配置的cookie都能正确获取链接')
logger.info('传统的源 - 单用户cookie配置将被忽略')
logger.info('所以即使某个源你只有一个cookie也请填写到cookiepool对应的源中否则将无法使用该cookie')
variable.use_cookie_pool = True
# 移除已经过期的封禁数据
banlist = read_data('banList')
banlistRaw = read_data('banListRaw')
count = 0
for b in banlist:
if (b['expire'] and (time.time() > b['expire_time'])):
count += 1
banlist.remove(b)
if (b['ip'] in banlistRaw):
banlistRaw.remove(b['ip'])
write_data('banList', banlist)
write_data('banListRaw', banlistRaw)
if (count != 0):
logger.info(f'已移除{count}条过期封禁数据')
# 处理旧版数据库的banListRaw
banlist = read_data('banList')
banlistRaw = read_data('banListRaw')
if (banlist != [] and banlistRaw == []):
for b in banlist:
banlistRaw.append(b['ip'])
return
def ban_ip(ip_addr, ban_time=-1):
if read_config('security.banlist.enable'):
banList = read_data('banList')
banList.append({
'ip': ip_addr,
'expire': read_config('security.banlist.expire.enable'),
'expire_time': read_config('security.banlist.expire.length') if (ban_time == -1) else ban_time,
})
write_data('banList', banList)
banListRaw = read_data('banListRaw')
if (ip_addr not in banListRaw):
banListRaw.append(ip_addr)
write_data('banListRaw', banListRaw)
else:
if (variable.banList_suggest < 10):
variable.banList_suggest += 1
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
def check_ip_banned(ip_addr):
if read_config('security.banlist.enable'):
banList = read_data('banList')
banlistRaw = read_data('banListRaw')
if (ip_addr in banlistRaw):
for b in banList:
if (b['ip'] == ip_addr):
if (b['expire']):
if (b['expire_time'] > int(time.time())):
return True
else:
banList.remove(b)
banlistRaw.remove(b['ip'])
write_data('banListRaw', banlistRaw)
write_data('banList', banList)
return False
else:
return True
else:
return False
return False
else:
return False
else:
if (variable.banList_suggest <= 10):
variable.banList_suggest += 1
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
return False
initConfig()

View File

@ -1,12 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: exceptions.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
class FailedException(Exception):
# 此错误用于处理代理API请求失败的情况
pass

View File

@ -1,396 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: localMusic.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import platform
import subprocess
import sys
from PIL import Image
import aiohttp
from common.utils import createFileMD5, createMD5, timeLengthFormat
from . import log, config
import ujson as json
import traceback
import mutagen
import os
logger = log.log('local_music_handler')
audios = []
map = {}
AUDIO_PATH = config.read_config("common.local_music.audio_path")
TEMP_PATH = config.read_config("common.local_music.temp_path")
FFMPEG_PATH = None
def convertCover(input_bytes):
if (input_bytes.startswith(b'\xff\xd8\xff\xe0')): # jpg object do not need convert
return input_bytes
temp = TEMP_PATH + '/' + createMD5(input_bytes) + '.img'
with open(temp, 'wb') as f:
f.write(input_bytes)
f.close()
img = Image.open(temp)
img = img.convert('RGB')
with open(temp + 'crt', 'wb') as f:
img.save(f, format='JPEG')
f.close()
data = None
with open(temp + 'crt', 'rb') as f:
data = f.read()
f.close()
try:
os.remove(temp)
except:
pass
try:
os.remove(temp + 'crt')
except:
pass
return data
def check_ffmpeg():
logger.info('正在检查ffmpeg')
devnull = open(os.devnull, 'w')
linux_bin_path = '/usr/bin/ffmpeg'
environ_ffpmeg_path = os.environ.get('FFMPEG_PATH')
if (platform.system() == 'Windows' or platform.system() == 'Cygwin'):
if (environ_ffpmeg_path and (not environ_ffpmeg_path.endswith('.exe'))):
environ_ffpmeg_path += '/ffmpeg.exe'
else:
if (environ_ffpmeg_path and os.path.isdir(environ_ffpmeg_path)):
environ_ffpmeg_path += '/ffmpeg'
if (environ_ffpmeg_path):
try:
subprocess.Popen([environ_ffpmeg_path, '-version'], stdout=devnull, stderr=devnull)
devnull.close()
return environ_ffpmeg_path
except:
pass
if (os.path.isfile(linux_bin_path)):
try:
subprocess.Popen([linux_bin_path, '-version'], stdout=devnull, stderr=devnull)
devnull.close()
return linux_bin_path
except:
pass
try:
subprocess.Popen(['ffmpeg', '-version'], stdout=devnull, stderr=devnull)
return 'ffmpeg'
except:
logger.warning('无法找到ffmpeg对于本地音乐的一些扩展功能无法使用如果您不需要请忽略本条提示')
logger.warning('如果您已经安装,请将 FFMPEG_PATH 环境变量设置为您的ffmpeg安装路径或者将其添加到PATH中')
return None
def getAudioCoverFromFFMpeg(path):
if (not FFMPEG_PATH):
return None
cmd = [FFMPEG_PATH, '-i', path, TEMP_PATH + '/_tmp.jpg']
popen = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stdout)
popen.wait()
if (os.path.exists(TEMP_PATH + '/_tmp.jpg')):
with open(TEMP_PATH + '/_tmp.jpg', 'rb') as f:
data = f.read()
f.close()
try:
os.remove(TEMP_PATH + '/_tmp.jpg')
except:
pass
return data
def readFileCheckCover(path):
with open(path, 'rb') as f: # read the first 1MB audio
data = f.read(1024 * 1024)
return b'image/' in data
def checkLyricValid(lyric_content):
if (lyric_content is None):
return False
if (lyric_content == ''):
return False
lines = lyric_content.split('\n')
for line in lines:
line = line.strip()
if (line == ''):
continue
if (line.startswith('[')):
continue
if (not line.startswith('[')):
return False
return True
def filterLyricLine(lyric_content: str) -> str:
lines = lyric_content.split('\n')
completed = []
for line in lines:
line = line.strip()
if (line.startswith('[')):
completed.append(line)
continue
return '\n'.join(completed)
def getAudioMeta(filepath):
if not os.path.exists(filepath):
return None
try:
audio = mutagen.File(filepath)
if not audio:
return None
logger.debug(audio.items())
if (filepath.lower().endswith('.mp3')):
cover = audio.get('APIC:')
if (cover):
cover = convertCover(cover.data)
lrc_key = None
for k in list(audio.keys()):
if (k.startswith('USLT')):
lrc_key = k
break
title = audio.get('TIT2')
artist = audio.get('TPE1')
album = audio.get('TALB')
if (lrc_key):
lyric = audio.get(lrc_key)
else:
lyric = None
if (title):
title = title.text
if (artist):
artist = artist.text
if (album):
album = album.text
if (lyric):
lyric = [lyric.text]
if (not lyric):
if (os.path.isfile(os.path.splitext(filepath)[0] + '.lrc')):
with open(os.path.splitext(filepath)[0] + '.lrc', 'r', encoding='utf-8') as f:
t = f.read().replace('\ufeff', '')
logger.debug(t)
lyric = filterLyricLine(t)
logger.debug(lyric)
if (not checkLyricValid(lyric)):
lyric = [None]
else:
lyric = [lyric]
f.close()
else:
lyric = [None]
else:
cover = audio.get('cover')
if (cover):
cover = convertCover(cover[0])
else:
if (readFileCheckCover(filepath)):
cover = getAudioCoverFromFFMpeg(filepath)
else:
cover = None
title = audio.get('title')
artist = audio.get('artist')
album = audio.get('album')
lyric = audio.get('lyrics')
if (not lyric):
if (os.path.isfile(os.path.splitext(filepath)[0] + '.lrc')):
with open(os.path.splitext(filepath)[0] + '.lrc', 'r', encoding='utf-8') as f:
lyric = filterLyricLine(f.read())
if (not checkLyricValid(lyric)):
lyric = [None]
else:
lyric = [lyric]
f.close()
else:
lyric = [None]
return {
"filepath": filepath,
"title": title[0] if title else '',
"artist": ''.join(artist) if artist else '',
"album": album[0] if album else '',
"cover_path": extractCover({
"filepath": filepath,
"cover": cover,
}, TEMP_PATH),
"lyrics": lyric[0],
'length': audio.info.length,
'format_length': timeLengthFormat(audio.info.length),
'md5': createFileMD5(filepath),
}
except:
logger.error(f"get audio meta error: {filepath}")
logger.error(traceback.format_exc())
return None
def checkAudioValid(path):
if not os.path.exists(path):
return False
try:
audio = mutagen.File(path)
if not audio:
return False
return True
except:
logger.error(f"check audio valid error: {path}")
logger.error(traceback.format_exc())
return False
def extractCover(audio_info, temp_path):
if (not audio_info['cover']):
return None
path = os.path.join(temp_path + '/' + createMD5(audio_info['filepath']) + '_cover.jpg')
with open(path, 'wb') as f:
f.write(audio_info['cover'])
return path
def findAudios(cache):
available_exts = [
'mp3',
'wav',
'flac',
'ogg',
'm4a',
]
files = os.listdir(AUDIO_PATH)
if (files == []):
return []
audios = []
_map = {}
for c in cache:
_map[c['filepath']] = c
for file in files:
if (not file.endswith(tuple(available_exts))):
continue
path = os.path.join(AUDIO_PATH, file)
if (not checkAudioValid(path)):
continue
logger.info(f"found audio: {path}")
if (not (_map.get(path) and _map[path]['md5'] == createFileMD5(path))):
meta = getAudioMeta(path)
audios = audios + [meta]
else:
audios = audios + [_map[path]]
return audios
def getAudioCover(filepath):
if not os.path.exists(filepath):
return None
try:
audio = mutagen.File(filepath)
if not audio:
return None
if (filepath.lower().endswith('mp3')):
return audio.get('APIC:').data
else:
if (readFileCheckCover(filepath)):
return getAudioCoverFromFFMpeg(filepath)
else:
return None
except:
logger.error(f"get audio cover error: {filepath}")
logger.error(traceback.format_exc())
return None
def writeAudioCover(filepath):
s = getAudioCover(filepath)
path = os.path.join(TEMP_PATH + '/' + createMD5(filepath) + '_cover.jpg')
with open(path, 'wb') as f:
f.write(s)
f.close()
return path
def writeLocalCache(audios):
with open(TEMP_PATH + '/meta.json', 'w', encoding='utf-8') as f:
f.write(json.dumps({
"file_list": os.listdir(AUDIO_PATH),
"audios": audios
}, ensure_ascii = False, indent = 2))
f.close()
def dumpLocalCache():
try:
TEMP_PATH = config.read_config("common.local_music.temp_path")
with open(TEMP_PATH + '/meta.json', 'r', encoding='utf-8') as f:
d = json.loads(f.read())
return d
except:
return {
"file_list": [],
"audios": []
}
def initMain():
global FFMPEG_PATH
FFMPEG_PATH = check_ffmpeg()
logger.debug('找到的ffmpeg命令: ' + str(FFMPEG_PATH))
if (not os.path.exists(AUDIO_PATH)):
os.mkdir(AUDIO_PATH)
logger.info(f"创建本地音乐文件夹 {AUDIO_PATH}")
if (not os.path.exists(TEMP_PATH)):
os.mkdir(TEMP_PATH)
logger.info(f"创建本地音乐临时文件夹 {TEMP_PATH}")
global audios
cache = dumpLocalCache()
if (cache['file_list'] == os.listdir(AUDIO_PATH)):
audios = cache['audios']
else:
audios = findAudios(cache['audios'])
writeLocalCache(audios)
for a in audios:
map[a['filepath']] = a
logger.info("初始化本地音乐成功")
logger.debug(f'本地音乐列表: {audios}')
logger.debug(f'本地音乐map: {map}')
async def generateAudioFileResonse(path):
try:
w = map[path]
return aiohttp.web.FileResponse(w['filepath'])
except:
return {
'code': 2,
'msg': '未找到文件',
'data': None
}, 404
async def generateAudioCoverResonse(path):
try:
w = map[path]
if (not os.path.exists(w['cover_path'])):
p = writeAudioCover(w['filepath'])
logger.debug(f"生成音乐封面文件 {w['cover_path']} 成功")
return aiohttp.web.FileResponse(p)
return aiohttp.web.FileResponse(w['cover_path'])
except:
logger.debug(traceback.format_exc())
return {
'code': 2,
'msg': '未找到封面',
'data': None
}, 404
async def generateAudioLyricResponse(path):
try:
w = map[path]
return w['lyrics']
except:
return {
'code': 2,
'msg': '未找到歌词',
'data': None
}, 404
def checkLocalMusic(path):
return {
'file': os.path.exists(path),
'cover': os.path.exists(map[path]['cover_path']),
'lyric': bool(map[path]['lyrics'])
}

View File

@ -1,283 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: log.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import logging
import colorlog
import os
import sys
import re
import traceback
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from .utils import filterFileName, addToGlobalNamespace
from .variable import debug_mode, log_length_limit, log_file
from colorama import Fore, Back, Style
from colorama import init as clinit
clinit() # 初始化 colorama
if ((not os.path.exists("logs")) and log_file):
try:
os.mkdir("logs")
except:
pass
class Color:
"""
彩色文字处理器
"""
def __getattr__(self, k):
return lambda x: f"{getattr(Fore, k.upper())}{x}{Style.RESET_ALL}"
color = Color()
def is_rubbish(input_string):
return bool(re.match(r'^(\~|\^)*$', input_string))
def stack_error(exception):
stack_trace = traceback.format_exception(type(exception), exception, exception.__traceback__)
return ''.join(stack_trace)
def find_function(file_name, line_number):
with open(file_name, 'r') as file:
lines = file.readlines()
target_line = lines[line_number - 1] # 获取目标行内容
for name, obj in inspect.getmembers(inspect.currentframe().f_back.f_globals):
if (inspect.isfunction(obj)):# and target_line.lstrip().startswith('def '+obj.__name__)):
return obj.__name__, inspect.getsourcelines(obj)[1]
def python_highlight(code):
return highlight(code, PythonLexer(), TerminalFormatter())
def read_code(file_path, target_line_number):
try:
with open(file_path, 'r', encoding='utf-8') as file:
lines = file.readlines()
start = max(0, target_line_number - 4)
end = min(target_line_number + 4, len(lines))
lineMap = {
'current': lines[target_line_number - 1],
'result': lines[start:end]
}
return lineMap
except FileNotFoundError:
sys.stderr.write("日志模块出错,本次日志可能无法记录,请报告给开发者: 处理错误语法高亮时找不到源文件")
except Exception as e:
sys.stderr.write(f"日志模块出错,本次日志可能无法记录,请报告给开发者: \n" + traceback.format_exc())
def stack_info(stack_trace_line):
try:
parts = stack_trace_line.split(', ')
file_path = ' '.join(parts[0].split(' ')[1:]) # 提取文件路径
line_number = int(parts[1].split(' ')[1]) # 提取行号
function_name = parts[2].split(' ')[1] # 提取函数名
return file_path, line_number, function_name
except Exception as e:
sys.stderr.write(f"日志模块出错,本次日志可能无法记录,请报告给开发者: \n" + traceback.format_exc())
def highlight_error(e):
try:
if (isinstance(e, Exception)):
error = stack_error(e)
else:
error = e
lines = [i.strip() for i in error.split("\n") if i.strip()]
final = []
ign = False
for i in lines:
if (ign):
ign = False
continue
if (i.startswith("Traceback (most recent call last):")):
final.append(color.cyan(i))
elif (i.startswith("During handling of the above exception, another exception occurred:")):
final.append(color.cyan(i))
elif (i.startswith("The above exception was the direct cause of the following exception:")):
final.append(color.cyan(i))
elif (i.startswith("File")):
ign = True
p, l, f = stack_info(i)
p = p[1:-1]
if (p.startswith('<') or not os.path.isfile(p)):
final.append(i)
final.append(" " if (lines[lines.index(l) + 1].startswith("File")) else (" " + lines[lines.index(l) + 1]))
continue
code = read_code(p, l)
cc = []
viewed = False
for i in range(len(code['result'])):
c = code["result"][i]
if (c.startswith(code['current']) and (i <= 3)):
viewed = True
cc.append((' ' * (10 - len(str(l))) + f'{color.red(str(l))} >|' + python_highlight(c)))
else:
line_number = l + (code["result"].index(c) - 3)
cc.append((' ' * (10 - len(str(line_number))) + f'{color.blue(str(line_number))} |' + python_highlight(c)))
code = "\n".join(cc)
p = '"' + p + '"'
final.append(f" File {color.yellow(f'{p}')} in {color.cyan(f)}()\n\n\n{code}\n")
else:
try:
if (is_rubbish(i)):
continue
if (issubclass(require(("builtins." if ("." not in i.split(":")[0]) else "") + i.split(":")[0]), Exception)):
exc = i.split(":")[0]
desc = "" if (len(i.split(":")) == 1) else ':'.join(i.split(":")[1:]).strip()
final.append(color.red(exc) + (": " + color.yellow(desc)) if (desc) else "")
else:
final.append(color.cyan(i))
except:
# traceback.print_exc()
final.append(i)
return "\n".join(final).replace('\n\n', '\n')
except:
sys.stderr.write('格式化错误失败,使用默认格式\n' + traceback.format_exc())
if (isinstance(e, Exception)):
return stack_error(e)
else:
return e
class LogHelper(logging.Handler):
# 日志转接器
def __init__(self, custom_logger):
super().__init__()
self.custom_logger = custom_logger
def emit(self, record):
# print(record)
log_message = self.format(record)
self.custom_logger.info(log_message)
class log:
# 主类
def __init__(self, module_name='Not named logger', output_level='INFO', filename=''):
self._logger = logging.getLogger(module_name)
if not output_level.upper() in dir(logging):
raise NameError('Unknown loglevel: '+output_level)
if not debug_mode:
self._logger.setLevel(getattr(logging, output_level.upper()))
else:
self._logger.setLevel(logging.DEBUG)
formatter = colorlog.ColoredFormatter(
'%(log_color)s%(asctime)s|[%(name)s/%(levelname)s]|%(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
log_colors={
'DEBUG': 'cyan',
'INFO': 'white',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
})
if log_file:
file_formatter = logging.Formatter(
'%(asctime)s|[%(name)s/%(levelname)s]|%(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
if filename:
filename = filterFileName(filename)
else:
filename = './logs/' + module_name + '.log'
file_handler = logging.FileHandler(filename, encoding="utf-8")
file_handler.setFormatter(file_formatter)
file_handler_ = logging.FileHandler(
"./logs/console_full.log", encoding="utf-8")
file_handler_.setFormatter(file_formatter)
self._logger.addHandler(file_handler_)
self._logger.addHandler(file_handler)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
self.module_name = module_name
self._logger.addHandler(console_handler)
debug_handler = logging.StreamHandler()
debug_handler.setFormatter(formatter)
def debug(self, message, allow_hidden=True):
if self.module_name == "flask" and "\n" in message:
if message.startswith("Error"):
return self._logger.error(message)
for m in message.split("\n"):
if "WARNING" in m:
self._logger.warning(m)
else:
self._logger.info(m)
return
if len(str(message)) > log_length_limit and allow_hidden:
message = str(message)[:log_length_limit] + " ..."
self._logger.debug(message)
def log(self, message, allow_hidden=True):
if self.module_name == "flask" and "\n" in message:
if message.startswith("Error"):
return self._logger.error(message)
for m in message.split("\n"):
if "WARNING" in m:
self._logger.warning(m)
else:
self._logger.info(m)
return
if len(str(message)) > log_length_limit and allow_hidden:
message = str(message)[:log_length_limit] + " ..."
self._logger.info(message)
def info(self, message, allow_hidden=True):
if self.module_name == "flask" and "\n" in message:
if message.startswith("Error"):
return self._logger.error(message)
for m in message.split("\n"):
if "WARNING" in m:
self._logger.warning(m)
else:
self._logger.info(m)
return
if len(str(message)) > log_length_limit and allow_hidden:
message = str(message)[:log_length_limit] + "..."
self._logger.info(message)
def warning(self, message):
if (message.strip().startswith('Traceback')):
self._logger.warning('\n' + highlight_error(message))
else:
self._logger.warning(message)
def error(self, message):
if (message.startswith('Traceback')):
self._logger.error('\n' + highlight_error(message))
else:
self._logger.error(message)
def critical(self, message):
self._logger.critical(message)
def set_level(self, loglevel):
loglevel_upper = loglevel.upper()
if not loglevel_upper in dir(logging):
raise NameError('Unknown loglevel: ' + loglevel)
self._logger.setLevel(getattr(logging, loglevel_upper))
def getLogger(self):
return self._logger
def addHandler(self, handler):
self._logger.addHandler(handler)
printlogger = log('print')
def logprint(*args, sep=' ', end='', file=None, flush=None):
printlogger.info(sep.join(str(arg) for arg in args), allow_hidden=False)
addToGlobalNamespace('print', logprint)

View File

@ -1,112 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: lx.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from . import Httpx
from . import config
from . import scheduler
from .log import log
from aiohttp.web import Response
import ujson as json
import re
logger = log('lx_script')
jsd_mirror_list = [
'https://cdn.jsdelivr.net',
'https://gcore.jsdelivr.net',
'https://fastly.jsdelivr.net',
'https://jsd.cdn.zzko.cn',
'https://jsdelivr.b-cdn.net',
]
github_raw_mirror_list = [
'https://raw.githubusercontent.com',
'https://mirror.ghproxy.com/https://raw.githubusercontent.com',
'https://ghraw.gkcoll.xyz',
'https://raw.fgit.mxtrans.net',
'https://github.moeyy.xyz/https://raw.githubusercontent.com',
'https://raw.fgit.cf',
]
async def get_response(retry = 0):
if (retry > 21):
logger.warning('请求源脚本内容失败')
return
baseurl = '/lxmusics/lx-music-api-server/main/lx-music-source-example.js'
jsdbaseurl = '/gh/lxmusics/lx-music-api-server@main/lx-music-source-example.js'
try:
i = retry
if (i > 10):
i = i - 11
if (i < 5):
req = await Httpx.AsyncRequest(jsd_mirror_list[retry] + jsdbaseurl)
elif (i < 11):
req = await Httpx.AsyncRequest(github_raw_mirror_list[retry - 5] + baseurl)
if (not req.text.startswith('/*!')):
logger.info('疑似请求到了无效的内容,忽略')
raise Exception from None
except Exception as e:
if (isinstance(e, RuntimeError)):
if ('Session is closed' in str(e)):
logger.error('脚本更新失败clientSession已被关闭')
return
return await get_response(retry + 1)
return req
async def get_script():
req = await get_response()
if (req.status == 200):
with open('./lx-music-source-example.js', 'w', encoding='utf-8') as f:
f.write(req.text)
f.close()
logger.info('更新源脚本成功')
else:
logger.warning('请求源脚本内容失败')
async def generate_script_response(request):
if (request.query.get('key') not in config.read_config('security.key.values') and config.read_config('security.key.enable')):
return {'code': 6, 'msg': 'key验证失败', 'data': None}, 403
try:
with open('./lx-music-source-example.js', 'r', encoding='utf-8') as f:
script = f.read()
except:
return {'code': 4, 'msg': '本地无源脚本', 'data': None}, 400
scriptLines = script.split('\n')
newScriptLines = []
for line in scriptLines:
oline = line
line = line.strip()
if (line.startswith('const API_URL')):
newScriptLines.append(f'''const API_URL = "{'https' if config.read_config('common.ssl_info.is_https') else 'http'}://{request.host}"''')
elif (line.startswith('const API_KEY')):
newScriptLines.append(f"""const API_KEY = `{request.query.get("key") if request.query.get("key") else ''''''}`""")
elif (line.startswith("* @name")):
newScriptLines.append(" * @name " + config.read_config("common.download_config.name"))
elif (line.startswith("* @description")):
newScriptLines.append(" * @description " + config.read_config("common.download_config.intro"))
elif (line.startswith("* @author")):
newScriptLines.append(" * @author " + config.read_config("common.download_config.author"))
elif (line.startswith("* @version")):
newScriptLines.append(" * @version " + config.read_config("common.download_config.version"))
elif (line.startswith("const DEV_ENABLE ")):
newScriptLines.append("const DEV_ENABLE = " + str(config.read_config("common.download_config.dev")).lower())
else:
newScriptLines.append(oline)
r = '\n'.join(newScriptLines)
r = re.sub(r'const MUSIC_QUALITY = {[^}]+}', f'const MUSIC_QUALITY = JSON.parse(\'{json.dumps(config.read_config("common.download_config.quality"))}\')', r)
return Response(text = r, content_type = 'text/javascript',
headers = {
'Content-Disposition': f'''attachment; filename={
config.read_config("common.download_config.filename")
if config.read_config("common.download_config.filename").endswith(".js")
else (config.read_config("common.download_config.filename") + ".js")}'''
})
if (config.read_config('common.allow_download_script')):
scheduler.append('update_script', get_script)

View File

@ -1,34 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: lxsecurity.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from . import utils
import ujson as json
import binascii
import re
def checklxmheader(lxm, url):
try:
path = url.replace(re.findall(r'(?:https?:\/\/[\w.:]+\/)', url)[0], '/').replace('//', '/')
retvalue = re.findall(r'(?:\d\w)+', path)[0]
cop, version = tuple(lxm.split('&'))
version = (3 - len(version) % 3) * '0' + version
cop = utils.handleInflateRawSync(binascii.unhexlify(cop.encode('utf-8'))).decode('utf-8')
cop = utils.createBase64Decode(cop).decode('utf-8')
arr, outsideversion = tuple([cop.split(']')[0] + ']', cop.split(']')[1]])
arr = json.loads(arr)
version = re.findall("\\d+", version)[0]
if not outsideversion.startswith(version):
return False
if (not (version) in ("".join(arr))) and (not (retvalue) in "".join(arr)):
return False
return True
except:
return False

View File

@ -1,10 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: __init__.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from . import qdes

View File

@ -1,32 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: qdes.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from .log import log
from . import variable
import binascii
import zlib
logger = log('qdes')
try:
from .natives import qdes
variable.qdes_lib_loaded = True
except:
try:
import qdes
variable.qdes_lib_loaded = True
except:
logger.warning('QRC解密库qdes加载失败, 可能为不支持当前系统, QRC相关的逐字歌词获取将无法使用')
def qdes_decrypt(qrc):
if variable.qdes_lib_loaded:
decoded = zlib.decompress(qdes.LyricDecode(binascii.unhexlify(qrc.encode('utf-8')))).decode('utf-8')
return decoded
else:
raise ModuleNotFoundError('qdes解密库未被加载')

View File

@ -1,65 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: scheduler.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
# 一个简单的循环任务调度器
import time
import asyncio
import traceback
from .utils import timestamp_format
from . import log
logger = log.log("scheduler")
running_event = asyncio.Event()
global tasks
tasks = []
class taskWrapper:
def __init__(self, name, function, interval = 86400, args = {}, latest_execute = 0):
self.function = function
self.interval = interval
self.name = name
self.latest_execute = latest_execute
self.args = args
def check_available(self):
return (time.time() - self.latest_execute) >= self.interval
async def run(self):
try:
logger.info(f"task {self.name} run start")
await self.function(**self.args)
logger.info(f'task {self.name} run success, next execute: {timestamp_format(self.interval + self.latest_execute)}')
except Exception as e:
logger.error(f"task {self.name} run failed, waiting for next execute...")
logger.error(traceback.format_exc())
def append(name, task, interval = 86400, args = {}):
global tasks
wrapper = taskWrapper(name, task, interval, args)
logger.debug(f"new task ({name}) registered")
return tasks.append(wrapper)
# 在 thread_runner 函数中修改循环逻辑
async def thread_runner():
global tasks, running_event
while not running_event.is_set():
tasks_runner = []
for t in tasks:
if (t.check_available() and not running_event.is_set()):
t.latest_execute = int(time.time())
tasks_runner.append(t.run())
if (tasks_runner):
await asyncio.gather(*tasks_runner)
await asyncio.sleep(1)
async def run():
logger.debug("scheduler thread starting...")
task = asyncio.create_task(thread_runner())
logger.debug("schedluer thread load success")

View File

@ -1,202 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: utils.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import hashlib
import platform
import binascii
import builtins
import base64
import zlib
import time
import re
import xmltodict
from urllib.parse import quote, unquote, urlparse
def createBase64Encode(data_bytes):
encoded_data = base64.b64encode(data_bytes)
return encoded_data.decode('utf-8')
def createHexEncode(data_bytes):
hex_encoded = binascii.hexlify(data_bytes)
return hex_encoded.decode('utf-8')
def createBase64Decode(data):
decoded_data = base64.b64decode(data)
return decoded_data
def createHexDecode(data):
decoded_data = binascii.unhexlify(data.decode('utf-8'))
return decoded_data
def handleInflateRawSync(data):
decompress_obj = zlib.decompressobj(-zlib.MAX_WBITS)
decompressed_data = decompress_obj.decompress(data) + decompress_obj.flush()
return decompressed_data
def require(module):
index = 0
module_array = module.split('.')
for m in module_array:
if index == 0:
_module = __import__(m)
index += 1
else:
_module = getattr(_module, m)
index += 1
return _module
def addToGlobalNamespace(key, data):
setattr(builtins, key, data)
def filterFileName(filename):
if platform.system() == 'Windows' or platform.system() == 'Cygwin':
# Windows不合法文件名字符
illegal_chars = r'[<>:"/\\|?*\x00-\x1f]'
else:
# 不合法文件名字符
illegal_chars = r'[/\x00-\x1f]'
# 将不合法字符替换为下划线
return re.sub(illegal_chars, '_', filename)
def createMD5(s: (str, bytes)):
if (isinstance(s, str)):
s = s.encode("utf-8")
return hashlib.md5(s).hexdigest()
def createFileMD5(path):
with open(path, 'rb') as f:
md5 = hashlib.md5()
for chunk in iter(lambda: f.read(4096), b""):
md5.update(chunk)
return md5.hexdigest()
def readFile(path, mode = "text"):
try:
fileObj = open(path, "rb")
except FileNotFoundError:
return "file not found"
content = fileObj.read()
if mode == "base64":
return createBase64Encode(content)
elif mode == "hex":
return createHexEncode(content)
elif mode == "text":
return content.decode("utf-8")
else:
return "unsupported mode"
def unique_list(list_in):
unique_list = []
[unique_list.append(x) for x in list_in if x not in unique_list]
return unique_list
def encodeURIComponent(component):
if (isinstance(component, str)):
component = component.encode('utf-8')
elif (not isinstance(component, bytes)):
raise TypeError('component must be str or bytes')
return quote(component)
def decodeURIComponent(component):
return unquote(component)
def encodeURI(uri):
parse_result = urlparse(uri)
params = {}
for q in parse_result.query.split('&'):
k, v = q.split('=')
v = encodeURIComponent(v)
params[k] = v
query = '&'.join([f'{k}={v}' for k, v in params.items()])
return parse_result._replace(query=query).geturl()
def decodeURI(uri):
parse_result = urlparse(uri)
params = {}
for q in parse_result.query.split('&'):
k, v = q.split('=')
v = decodeURIComponent(v)
params[k] = v
query = '&'.join([f'{k}={v}' for k, v in params.items()])
return parse_result._replace(query=query).geturl()
def sortDict(dictionary):
sorted_items = sorted(dictionary.items())
sorted_dict = {k: v for k, v in sorted_items}
return sorted_dict
def mergeDict(dict1, dict2):
merged_dict = dict2.copy()
merged_dict.update(dict1)
return merged_dict
class CreateObject(dict):
def __init__(self, d):
super().__init__(d)
self._raw = d
for key, value in d.items():
if isinstance(value, dict):
setattr(self, key, CreateObject(value))
else:
setattr(self, key, value)
def __setattr__(self, key, value):
super().__setattr__(key, value)
if key != "_raw":
self._raw[key] = value
def to_dict(self):
result = {}
for key, value in self.items():
if isinstance(value, CreateObject):
result[key] = value.to_dict()
else:
result[key] = value
return result
def __getattr__(self, UNUSED):
return None
def dump_xml(data):
return xmltodict.unparse(data)
def load_xml(data):
return xmltodict.parse(data)
def sizeFormat(size):
if size < 1024:
return f"{size}B"
elif size < 1024**2:
return f"{round(size / 1024, 2)}KB"
elif size < 1024**3:
return f"{round(size / 1024**2, 2)}MB"
elif size < 1024**4:
return f"{round(size / 1024**3, 2)}GB"
elif size < 1024**5:
return f"{round(size / 1024**4, 2)}TB"
else:
return f"{round(size / 1024**5, 2)}PB"
def timeLengthFormat(t):
try:
t = int(t)
except:
return '//'
hour = t // 3600
minute = (t % 3600) // 60
second = t % 60
return f"{((('0' + str(hour)) if (len(str(hour)) == 1) else str(hour)) + ':') if (hour > 0) else ''}{minute:02}:{second:02}"
def timestamp_format(t):
if (not isinstance(t, int)):
t = int(t)
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t))
addToGlobalNamespace('require', require)

View File

@ -1,57 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: variable.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import os as _os
import ujson as _json
def _read_config_file():
try:
with open("./config/config.json", "r", encoding = "utf-8") as f:
return _json.load(f)
except:
return {}
def _read_config(key):
try:
config = _read_config_file()
keys = key.split('.')
value = config
for k in keys:
if isinstance(value, dict):
if k not in value and keys.index(k) != len(keys) - 1:
value[k] = {}
elif k not in value and keys.index(k) == len(keys) - 1:
value = None
value = value[k]
else:
value = None
break
return value
except:
return None
_dm = _read_config("common.debug_mode")
_lm = _read_config("common.log_file")
_ll = _read_config("common.log_length_limit")
debug_mode = _dm if (_dm) else False
log_length_limit = _ll if (_ll) else 500
log_file = _lm if (isinstance(_lm , bool)) else True
running = True
config = {}
workdir = _os.getcwd()
banList_suggest = 0
iscn = True
fake_ip = None
aioSession = None
qdes_lib_loaded = False
use_cookie_pool = False
running_ports = []
use_proxy = False
http_proxy = ''
https_proxy = ''

View File

@ -1,15 +0,0 @@
version: "3.8"
services:
lx:
container_name: lx-server
build: .
ports:
- "9763:9763"
volumes:
- .:/app
environment:
TZ: 'Asia/Shanghai'
restart: always
networks:
default:

BIN
icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,261 +0,0 @@
/*!
* @name 替换为你的音乐源名称
* @description 替换为你的音乐源介绍
* @version v2.0.0
* @author Folltoshe & helloplhm-qwq
* @repository https://github.com/lxmusics/lx-music-api-server
*/
// 是否开启开发模式
const DEV_ENABLE = true
// 服务端地址
const API_URL = 'http://127.0.0.1:9763'
// 服务端配置的请求key
const API_KEY = ''
// 音质配置(key为音源名称,不要乱填.如果你账号为VIP可以填写到hires)
// 全部的支持值: ['128k', '320k', 'flac', 'flac24bit']
const MUSIC_QUALITY = {
kw: ['128k'],
kg: ['128k'],
tx: ['128k'],
wy: ['128k'],
mg: ['128k'],
}
// 音源配置(默认为自动生成,可以修改为手动)
const MUSIC_SOURCE = Object.keys(MUSIC_QUALITY)
MUSIC_SOURCE.push('local')
/**
* 下面的东西就不要修改了
*/
const { EVENT_NAMES, request, on, send, utils, env, version } = globalThis.lx
/**
* URL请求
*
* @param {string} url - 请求的地址
* @param {object} options - 请求的配置文件
* @return {Promise} 携带响应体的Promise对象
*/
const httpFetch = (url, options = { method: 'GET' }) => {
return new Promise((resolve, reject) => {
console.log('--- start --- ' + url)
request(url, options, (err, resp) => {
if (err) return reject(err)
console.log('API Response: ', resp)
resolve(resp)
})
})
}
/**
* Encodes the given data to base64.
*
* @param {type} data - the data to be encoded
* @return {string} the base64 encoded string
*/
const handleBase64Encode = (data) => {
var data = utils.buffer.from(data, 'utf-8')
return utils.buffer.bufToString(data, 'base64')
}
/**
*
* @param {string} source - 音源
* @param {object} musicInfo - 歌曲信息
* @param {string} quality - 音质
* @returns {Promise<string>} 歌曲播放链接
* @throws {Error} - 错误消息
*/
const handleGetMusicUrl = async (source, musicInfo, quality) => {
if (source == 'local') {
if (!musicInfo.songmid.startsWith('server_')) throw new Error('upsupported local file')
const songId = musicInfo.songmid
const requestBody = {
p: songId.replace('server_', ''),
}
var t = 'c'
var b = handleBase64Encode(JSON.stringify(requestBody)) /* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
const targetUrl = `${API_URL}/local/${t}?q=${b}`
const request = await httpFetch(targetUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`,
'X-Request-Key': API_KEY,
},
follow_max: 5,
})
const { body } = request
if (body.code == 0 && body.data && body.data.file) {
var t = 'u'
var b = handleBase64Encode(JSON.stringify(requestBody)) /* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
return `${API_URL}/local/${t}?q=${b}`
}
throw new Error('404 Not Found')
}
const songId = musicInfo.hash ?? musicInfo.songmid
const request = await httpFetch(`${API_URL}/url/${source}/${songId}/${quality}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`,
'X-Request-Key': API_KEY,
},
follow_max: 5,
})
const { body } = request
if (!body || isNaN(Number(body.code))) throw new Error('unknow error')
if (env != 'mobile') console.groupEnd()
switch (body.code) {
case 0:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) success, URL: ${body.data}`)
return body.data
case 1:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed: ip被封禁`)
throw new Error('block ip')
case 2:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed, ${body.msg}`)
throw new Error('get music url failed')
case 4:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed, 远程服务器错误`)
throw new Error('internal server error')
case 5:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed, 请求过于频繁,请休息一下吧`)
throw new Error('too many requests')
case 6:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed, 请求参数错误`)
throw new Error('param error')
default:
console.log(`handleGetMusicUrl(${source}_${musicInfo.songmid}, ${quality}) failed, ${body.msg ? body.msg : 'unknow error'}`)
throw new Error(body.msg ?? 'unknow error')
}
}
const handleGetMusicPic = async (source, musicInfo) => {
switch (source) {
case 'local':
// 先从服务器检查是否有对应的类型,再响应链接
if (!musicInfo.songmid.startsWith('server_')) throw new Error('upsupported local file')
const songId = musicInfo.songmid
const requestBody = {
p: songId.replace('server_', ''),
}
var t = 'c'
var b = handleBase64Encode(JSON.stringify(requestBody))/* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
const targetUrl = `${API_URL}/local/${t}?q=${b}`
const request = await httpFetch(targetUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`
},
follow_max: 5,
})
const { body } = request
if (body.code === 0 && body.data.cover) {
var t = 'p'
var b = handleBase64Encode(JSON.stringify(requestBody))/* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
return `${API_URL}/local/${t}?q=${b}`
}
throw new Error('get music pic failed')
default:
throw new Error('action(pic) does not support source(' + source + ')')
}
}
const handleGetMusicLyric = async (source, musicInfo) => {
switch (source) {
case 'local':
if (!musicInfo.songmid.startsWith('server_')) throw new Error('upsupported local file')
const songId = musicInfo.songmid
const requestBody = {
p: songId.replace('server_', ''),
}
var t = 'c'
var b = handleBase64Encode(JSON.stringify(requestBody))/* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
const targetUrl = `${API_URL}/local/${t}?q=${b}`
const request = await httpFetch(targetUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`
},
follow_max: 5,
})
const { body } = request
if (body.code === 0 && body.data.lyric) {
var t = 'l'
var b = handleBase64Encode(JSON.stringify(requestBody))/* url safe*/.replace(/\+/g, '-').replace(/\//g, '_')
const request2 = await httpFetch(`${API_URL}/local/${t}?q=${b}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`
},
follow_max: 5,
})
if (request2.body.code === 0) {
return {
lyric: request2.body.data ?? "",
tlyric: "",
rlyric: "",
lxlyric: ""
}
}
throw new Error('get music lyric failed')
}
throw new Error('get music lyric failed')
default:
throw new Error('action(lyric) does not support source(' + source + ')')
}
}
// 生成歌曲信息
const musicSources = {}
MUSIC_SOURCE.forEach(item => {
musicSources[item] = {
name: item,
type: 'music',
actions: (item == 'local') ? ['musicUrl', 'pic', 'lyric'] : ['musicUrl'],
qualitys: (item == 'local') ? [] : MUSIC_QUALITY[item],
}
})
// 监听 LX Music 请求事件
on(EVENT_NAMES.request, ({ action, source, info }) => {
switch (action) {
case 'musicUrl':
if (env != 'mobile') {
console.group(`Handle Action(musicUrl)`)
console.log('source', source)
console.log('quality', info.type)
console.log('musicInfo', info.musicInfo)
} else {
console.log(`Handle Action(musicUrl)`)
console.log('source', source)
console.log('quality', info.type)
console.log('musicInfo', info.musicInfo)
}
return handleGetMusicUrl(source, info.musicInfo, info.type)
.then(data => Promise.resolve(data))
.catch(err => Promise.reject(err))
case 'pic':
return handleGetMusicPic(source, info.musicInfo)
.then(data => Promise.resolve(data))
.catch(err => Promise.reject(err))
case 'lyric':
return handleGetMusicLyric(source, info.musicInfo)
.then(data => Promise.resolve(data))
.catch(err => Promise.reject(err))
default:
console.error(`action(${action}) not support`)
return Promise.reject('action not support')
}
})
// 向 LX Music 发送初始化成功事件
send(EVENT_NAMES.inited, { status: true, openDevTools: DEV_ENABLE, sources: musicSources })

309
main.py
View File

@ -1,309 +0,0 @@
#!/usr/bin/env python3
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: main.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import sys
from common.utils import createBase64Decode
if ((sys.version_info.major == 3 and sys.version_info.minor < 6) or sys.version_info.major == 2):
print('Python版本过低请使用Python 3.6+ ')
sys.exit(1)
from common import config, localMusic
from common import lxsecurity
from common import log
from common import Httpx
from common import variable
from common import scheduler
from common import lx_script
from aiohttp.web import Response, FileResponse, StreamResponse
import ujson as json
import threading
import traceback
import modules
import asyncio
import aiohttp
import time
import os
def handleResult(dic, status = 200) -> Response:
if (not isinstance(dic, dict)):
dic = {
'code': 0,
'msg': 'success',
'data': dic
}
return Response(body = json.dumps(dic, indent=2, ensure_ascii=False), content_type='application/json', status = status)
logger = log.log("main")
aiologger = log.log('aiohttp_web')
stopEvent = None
if (sys.version_info.minor < 8 and sys.version_info.major == 3):
logger.warning('您使用的Python版本已经停止更新不建议继续使用')
import concurrent
stopEvent = concurrent.futures._base.CancelledError
else:
stopEvent = asyncio.exceptions.CancelledError
def start_checkcn_thread() -> None:
threading.Thread(target=Httpx.checkcn).start()
# check request info before start
async def handle_before_request(app, handler):
async def handle_request(request):
try:
if (config.read_config('common.reverse_proxy.allow_proxy')):
if (request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))):
# proxy header
if (request.remote in config.read_config('common.reverse_proxy.proxy_whitelist_remote')):
request.remote_addr = request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))
else:
return handleResult({"code": 1, "msg": "反代客户端远程地址不在反代ip白名单中", "data": None}, 403)
else:
request.remote_addr = request.remote
else:
request.remote_addr = request.remote
# check ip
if (config.check_ip_banned(request.remote_addr)):
return handleResult({"code": 1, "msg": "您的IP已被封禁", "data": None}, 403)
# check global rate limit
if (
(time.time() - config.getRequestTime('global'))
<
(config.read_config("security.rate_limit.global"))
):
return handleResult({"code": 5, "msg": "全局限速", "data": None}, 429)
if (
(time.time() - config.getRequestTime(request.remote_addr))
<
(config.read_config("security.rate_limit.ip"))
):
return handleResult({"code": 5, "msg": "IP限速", "data": None}, 429)
# update request time
config.updateRequestTime('global')
config.updateRequestTime(request.remote_addr)
# check host
if (config.read_config("security.allowed_host.enable")):
if request.host.split(":")[0] not in config.read_config("security.allowed_host.list"):
if config.read_config("security.allowed_host.blacklist.enable"):
config.ban_ip(request.remote_addr, int(config.read_config("security.allowed_host.blacklist.length")))
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
resp = await handler(request)
if (isinstance(resp, (str, list, dict))):
resp = handleResult(resp)
elif (isinstance(resp, tuple) and len(resp) == 2): # flask like response
body, status = resp
if (isinstance(body, (str, list, dict))):
resp = handleResult(body, status)
else:
resp = Response(body = str(body), content_type='text/plain', status = status)
elif (not isinstance(resp, (Response, FileResponse, StreamResponse))):
resp = Response(body = str(resp), content_type='text/plain', status = 200)
aiologger.info(f'{request.remote_addr + ("" if (request.remote == request.remote_addr) else f"|proxy@{request.remote}")} - {request.method} "{request.path}", {resp.status}')
return resp
except:
logger.error(traceback.format_exc())
return {"code": 4, "msg": "内部服务器错误", "data": None}
return handle_request
async def main(request):
return handleResult({"code": 0, "msg": "success", "data": None})
async def handle(request):
method = request.match_info.get('method')
source = request.match_info.get('source')
songId = request.match_info.get('songId')
quality = request.match_info.get('quality')
if (config.read_config("security.key.enable") and request.host.split(':')[0] not in config.read_config('security.whitelist_host')):
if (request.headers.get("X-Request-Key")) not in config.read_config("security.key.values"):
if (config.read_config("security.key.ban")):
config.ban_ip(request.remote_addr)
return handleResult({"code": 1, "msg": "key验证失败", "data": None}, 403)
if (config.read_config('security.check_lxm.enable') and request.host.split(':')[0] not in config.read_config('security.whitelist_host')):
lxm = request.headers.get('lxm')
if (not lxsecurity.checklxmheader(lxm, request.url)):
if (config.read_config('security.lxm_ban.enable')):
config.ban_ip(request.remote_addr)
return handleResult({"code": 1, "msg": "lxm请求头验证失败", "data": None}, 403)
try:
query = dict(request.query)
if (method in dir(modules)):
return handleResult(await getattr(modules, method)(source, songId, quality, query))
else:
return handleResult(await modules.other(method, source, songId, quality, query))
except:
logger.error(traceback.format_exc())
return handleResult({'code': 4, 'msg': '内部服务器错误', 'data': None}, 500)
async def handle_404(request):
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
async def handle_local(request):
try:
query = dict(request.query)
data = query.get('q')
data = createBase64Decode(data.replace('-', '+').replace('_', '/'))
data = json.loads(data)
t = request.match_info.get('type')
data['t'] = t
except:
logger.info(traceback.format_exc())
return handleResult({'code': 6, 'msg': '请求参数有错', 'data': None}, 404)
if (data['t'] == 'u'):
if (data['p'] in list(localMusic.map.keys())):
return await localMusic.generateAudioFileResonse(data['p'])
else:
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
if (data['t'] == 'l'):
if (data['p'] in list(localMusic.map.keys())):
return await localMusic.generateAudioLyricResponse(data['p'])
else:
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
if (data['t'] == 'p'):
if (data['p'] in list(localMusic.map.keys())):
return await localMusic.generateAudioCoverResonse(data['p'])
else:
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
if (data['t'] == 'c'):
if (not data['p'] in list(localMusic.map.keys())):
return {
'code': 0,
'msg': 'success',
'data': {
'file': False,
'cover': False,
'lyric': False
}
}
return {
'code': 0,
'msg': 'success',
'data': localMusic.checkLocalMusic(data['p'])
}
app = aiohttp.web.Application(middlewares=[handle_before_request])
# mainpage
app.router.add_get('/', main)
# api
app.router.add_get('/{method}/{source}/{songId}/{quality}', handle)
app.router.add_get('/{method}/{source}/{songId}', handle)
app.router.add_get('/local/{type}', handle_local)
if (config.read_config('common.allow_download_script')):
app.router.add_get('/script', lx_script.generate_script_response)
# 404
app.router.add_route('*', '/{tail:.*}', handle_404)
async def run_app():
retries = 0
while True:
if (retries > 4):
logger.warning("重试次数已达上限,但仍有部分端口未能完成监听,已自动进行忽略")
return
try:
host = config.read_config('common.host')
ports = [int(port) for port in config.read_config('common.ports')]
ssl_ports = [int(port) for port in config.read_config('common.ssl_info.ssl_ports')]
final_ssl_ports = []
final_ports = []
for p in ports:
if (p not in ssl_ports and p not in variable.running_ports):
final_ports.append(p)
else:
if (p not in variable.running_ports):
final_ssl_ports.append(p)
# 读取证书和私钥路径
cert_path = config.read_config('common.ssl_info.path.cert')
privkey_path = config.read_config('common.ssl_info.path.privkey')
# 创建 HTTP AppRunner
http_runner = aiohttp.web.AppRunner(app)
await http_runner.setup()
# 启动 HTTP 端口监听
for port in final_ports:
if (port not in variable.running_ports):
http_site = aiohttp.web.TCPSite(http_runner, host, port)
await http_site.start()
variable.running_ports.append(port)
logger.info(f"监听 -> http://{host}:{port}")
if (config.read_config("common.ssl_info.enable") and final_ssl_ports != []):
if (os.path.exists(cert_path) and os.path.exists(privkey_path)):
import ssl
# 创建 SSL 上下文,加载配置文件中指定的证书和私钥
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(cert_path, privkey_path)
# 创建 HTTPS AppRunner
https_runner = aiohttp.web.AppRunner(app)
await https_runner.setup()
# 启动 HTTPS 端口监听
for port in ssl_ports:
if (port not in variable.running_ports):
https_site = aiohttp.web.TCPSite(https_runner, host, port, ssl_context=ssl_context)
await https_site.start()
variable.running_ports.append(port)
logger.info(f"监听 -> https://{host}:{port}")
return
except OSError as e:
if (str(e).startswith("[Errno 98]") or str(e).startswith('[Errno 10048]')):
logger.error("端口已被占用,请检查\n" + str(e))
logger.info('服务器将在10s后再次尝试启动...')
await asyncio.sleep(10)
logger.info('重新尝试启动...')
retries += 1
else:
raise
async def initMain():
await scheduler.run()
variable.aioSession = aiohttp.ClientSession(trust_env=True)
localMusic.initMain()
try:
await run_app()
logger.info("服务器启动成功请按下Ctrl + C停止")
await asyncio.Event().wait() # 等待停止事件
except (KeyboardInterrupt, stopEvent):
pass
except OSError as e:
logger.error("遇到未知错误,请查看日志")
logger.error(traceback.format_exc())
except:
logger.error("遇到未知错误,请查看日志")
logger.error(traceback.format_exc())
finally:
logger.info('wating for sessions to complete...')
if variable.aioSession:
await variable.aioSession.close()
variable.running = False
logger.info("Server stopped")
if __name__ == "__main__":
try:
start_checkcn_thread()
asyncio.run(initMain())
except KeyboardInterrupt:
pass
except:
logger.error('初始化出错,请检查日志')
logger.error(traceback.format_exc())

View File

@ -1,213 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: __init__.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common.exceptions import FailedException
from common.utils import require
from common import log
from common import config
# 从.引入的包并没有在代码中直接使用但是是用require在请求时进行引入的不要动
from . import kw
from . import mg
from . import kg
from . import tx
from . import wy
import traceback
import time
logger = log.log('api_handler')
sourceExpirationTime = {
'tx': {
"expire": True,
"time": 80400, # 不知道tx为什么要取一个这么不对劲的数字当过期时长
},
'kg': {
"expire": True,
"time": 24 * 60 * 60, # 24 hours
},
'kw': {
"expire": True,
"time": 60 * 60 # 60 minutes
},
'wy': {
"expire": True,
"time": 20 * 60, # 20 minutes
},
'mg': {
"expire": False,
"time": 0,
}
}
async def url(source, songId, quality, query):
if (not quality):
return {
'code': 2,
'msg': '需要参数"quality"',
'data': None,
}
if (source == "kg"):
songId = songId.lower()
try:
cache = config.getCache('urls', f'{source}_{songId}_{quality}')
if cache:
logger.debug(f'使用缓存的{source}_{songId}_{quality}数据URL{cache["url"]}')
return {
'code': 0,
'msg': 'success',
'data': cache['url'],
'extra': {
'cache': True,
'quality': {
'target': quality,
'result': quality,
},
'expire': {
# 在更新缓存的时候把有效期的75%作为链接可用时长,现在加回来
'time': int(cache['time'] + (sourceExpirationTime[source]['time'] * 0.25)) if cache['expire'] else None,
'canExpire': cache['expire'],
}
},
}
except:
logger.error(traceback.format_exc())
try:
func = require('modules.' + source + '.url')
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(songId, quality)
logger.info(f'获取{source}_{songId}_{quality}成功URL{result["url"]}')
canExpire = sourceExpirationTime[source]['expire']
expireTime = sourceExpirationTime[source]['time'] + int(time.time())
config.updateCache('urls', f'{source}_{songId}_{quality}', {
"expire": canExpire,
# 取有效期的75%作为链接可用时长
"time": int(expireTime - sourceExpirationTime[source]['time'] * 0.25),
"url": result['url'],
})
logger.debug(f'缓存已更新:{source}_{songId}_{quality}, URL{result["url"]}, expire: {expireTime}')
return {
'code': 0,
'msg': 'success',
'data': result['url'],
'extra': {
'cache': False,
'quality': {
'target': quality,
'result': result['quality'],
},
'expire': {
'time': expireTime if canExpire else None,
'canExpire': canExpire,
},
},
}
except FailedException as e:
logger.info(f'获取{source}_{songId}_{quality}失败,原因:' + e.args[0])
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
async def lyric(source, songId, _, query):
cache = config.getCache('lyric', f'{source}_{songId}')
if cache:
return {
'code': 0,
'msg': 'success',
'data': cache['data']
}
try:
func = require('modules.' + source + '.lyric')
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(songId)
config.updateCache('lyric', f'{source}_{songId}', {
"data": result,
"time": int(time.time() + (86400 * 3)), # 歌词缓存3天
"expire": True,
})
logger.debug(f'缓存已更新:{source}_{songId}, lyric: {result}')
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
async def search(source, songid, _, query):
try:
func = require('modules.' + source + '.search')
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(songid, query)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
async def other(method, source, songid, _, query):
try:
func = require('modules.' + source + '.' + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
}
try:
result = await func(songid)
return {
'code': 0,
'msg': 'success',
'data': result
}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
}
async def info_with_query(source, songid, _, query):
return await other('info', source, songid, None)

View File

@ -1,87 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: __init__.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from .musicInfo import getMusicMVHash as _getMVHash
from .musicInfo import getMusicSingerInfo as _getInfo2
from .musicInfo import getMusicInfo as _getInfo
from .utils import tools
from .player import url
from .lyric import getLyric as _getLyric
from .lyric import lyricSearchByHash as _lyricSearch
from .mv import getMvInfo as _getMvInfo
from .mv import getMvPlayURL as _getMvUrl
from .search import getSongSearchResult as _songsearch
from . import lite_signin
from common.exceptions import FailedException
from common import Httpx
from common import utils
import asyncio
from . import refresh_token
async def info(hash_):
tasks = []
tasks.append(_getInfo(hash_))
tasks.append(_getInfo2(hash_))
tasks.append(_getMVHash(hash_))
res = await asyncio.gather(*tasks)
res1 = res[0]
res2 = res[1]
file_info = {}
for k, v in tools['qualityHashMap'].items():
if (res1['audio_info'][v] and k != 'master'):
file_info[k] = {
'hash': res1['audio_info'][v],
'size': utils.sizeFormat(int(res1['audio_info'][v.replace('hash', 'filesize')])),
}
if (isinstance(res1, type(None))):
raise FailedException('获取歌曲信息失败,请检查歌曲是否存在')
return {
'name': res1['songname'],
'name_ori': res1['ori_audio_name'],
'name_extra': res1['songname'].replace(res1['ori_audio_name'], '').strip(),
'singer': res1['author_name'],
'singer_list': res2,
'format_length': utils.timeLengthFormat(int(res1['audio_info']['timelength']) / 1000),
'length': int(res1['audio_info']['timelength']) / 1000,
'hash': res1['audio_info']['hash'],
'file_info': file_info,
'songmid': res1['audio_id'],
'album_id': res1['album_info']['album_id'],
'album': res1['album_info']['album_name'],
'bpm': int(res1['bpm']),
'language': res1['language'],
'cover': res1['album_info']['sizable_cover'].format(size = 1080),
'sizable_cover': res1['album_info']['sizable_cover'],
'publish_date': res1['publish_date'],
'mvid': res[2],
'genre': []
}
async def mv(hash_):
tasks = []
tasks.append(_getMvInfo(hash_))
tasks.append(_getMvUrl(hash_))
res = await asyncio.gather(*tasks)
res1 = res[0]
res2 = res[1]
res1['play_info'] = res2
return res1
async def lyric(hash_):
lyric_search_result = await _lyricSearch(hash_)
choosed_lyric = lyric_search_result[0]
return await _getLyric(choosed_lyric['id'], choosed_lyric['accesskey'])
async def search(type, params):
if (type == 'song'):
return await _songsearch(**params)
else:
raise FailedException('暂不支持该类型搜索')

View File

@ -1,168 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: lite_signin.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common.exceptions import FailedException
from .utils import buildRequestParams, sign
from common import Httpx, config, utils, variable, scheduler, log
import random
import binascii
import time
logger = log.log('kugou_lite_sign_in')
async def randomMixSongMid():
'''
通过TOP500榜单获取随机歌曲的mixsongmid
'''
# 声明榜单url
rankUrl = 'http://mobilecdnbj.kugou.com/api/v3/rank/song?version=9108&ranktype=1&plat=0&pagesize=100&area_code=1&page=1&rankid=8888&with_res_tag=0&show_portrait_mv=1'
# 请求
res = await Httpx.AsyncRequest(rankUrl, {
"method": 'GET'
})
data = res.json()
if (data.get('status') != 1):
raise FailedException('排行榜获取失败')
# 随机选择一首歌曲
randomSong = random.choice(data['data']['info'])
# 因为排行榜api不会返回mixsongmid
# 所以需要进行一次搜索接口来获取
search_req = await Httpx.AsyncRequest(utils.encodeURI(f'https://songsearch.kugou.com/song_search_v2?' + buildRequestParams({
"keyword": randomSong['filename'],
"area_code": 1,
"page": 1,
"pagesize": 1,
"userid": 0,
"clientver": "",
"platform": "WebFilter",
"filter": 2,
"iscorrection": 1,
"privilege_filter": 0
})), {
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.142.86 Safari/537.36",
"Referer": "https://www.kugou.com",
}
})
body = search_req.json()
if (body.get('status') != 1):
raise FailedException('歌曲搜索失败')
if (body['data']['total'] == 0 or body['data']['lists'] == []):
raise FailedException('歌曲搜索失败')
return body['data']['lists'][0]['MixSongID']
async def do_account_signin(user_info):
'''
签到主函数传入userinfo响应None就是成功报错即为不成功
'''
# 检查用户配置文件获取mixsongmid
mixid = user_info['lite_sign_in']['mixsongmid']['value']
if (mixid == 'auto'):
mixid = await randomMixSongMid()
# 声明变量
headers = {
'User-Agent': f'Android712-AndroidPhone-{config.read_config("module.kg.client.clientver")}-18-0-NetMusic-wifi',
'KG-THash': '3e5ec6b',
'KG-Rec': '1',
'KG-RC': '1',
"x-router": "youth.kugou.com"
}
body = """{"mixsongid":__id__}""".replace("__id__", str(mixid))
# params = "userid={}&token={}&appid=3116&clientver=10518&clienttime={}&mid={}&uuid={}&dfid=-".format(read_config("common.kg.userid"), read_config("common.kg.token"), int(time.time()), read_config("common.kg.mid"), str(binascii.hexlify(random.randbytes(16)), encoding = "utf-8"))
params = {
"userid": user_info['userid'],
"token": user_info['token'],
"appid": 3116,
"clientver": config.read_config('module.kg.client.clientver'),
"clienttime": int(time.time()),
"mid": user_info['mid'],
"uuid": str(binascii.hexlify(random.randbytes(16)), encoding="utf-8"),
"dfid": "-"
}
params['signature'] = sign(
params, body, config.read_config('module.kg.client.signatureKey'))
# 发送请求
req = await Httpx.AsyncRequest(f"https://gateway.kugou.com/v2/report/listen_song?" +
buildRequestParams(params), {
"method": "POST",
"body": body,
"headers": headers
})
req = req.json()
if req['status'] == 1:
return
else:
raise FailedException(req['error_msg'])
def task_handler():
# not lite client configure
if (int(config.read_config('module.kg.client.appid')) != 3116):
return
# no user
if ((not variable.use_cookie_pool) and (not config.read_config('module.kg.user.token'))):
return
# devide cookiepool
if (variable.use_cookie_pool):
pool = config.read_config('module.cookiepool.kg')
for user in pool:
index = pool.index(user)
if (user.get('lite_sign_in') is None):
user['lite_sign_in'] = {
"desc": "是否启用概念版自动签到仅在appid=3116时运行",
"enable": False,
"interval": 86400,
"mixsongmid": {
"desc": "mix_songmid的获取方式, 默认auto, 可以改成一个数字手动",
"value": "auto"
}
}
pool[index] = user
config.write_config('module.cookiepool.kg', pool)
logger.info(f'用户池用户(index = {index})配置缺失lite_sign_in字段已自动写入')
# refresh
pool = config.read_config('module.cookiepool.kg')
# add signin schedule task
for user in pool:
if (user.get('lite_sign_in').get('enable')):
scheduler.append(f'kugou_lite_sign_in_{user["userid"]}', do_account_signin, user['lite_sign_in']['interval'], {'user_info': user})
else:
user_info = config.read_config('module.kg.user')
if (user_info.get('lite_sign_in') is None):
user_info['lite_sign_in'] = {
"desc": "是否启用概念版自动签到仅在appid=3116时运行",
"enable": False,
"interval": 86400,
"mixsongmid": {
"desc": "mix_songmid的获取方式, 默认auto, 可以改成一个数字手动",
"value": "auto"
}
}
config.write_config('module.kg.user', user_info)
logger.info('用户配置缺失lite_sign_in字段已自动写入')
if (user_info.get('lite_sign_in').get('enable')):
scheduler.append(f'kugou_lite_sign_in', do_account_signin, user_info['lite_sign_in']['interval'], {'user_info': user_info})
task_handler()

View File

@ -1,123 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: lyric.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common.exceptions import FailedException
from common.utils import encodeURI, createBase64Decode
from .musicInfo import getMusicInfo
from common import Httpx
import ujson as json
import zlib
import re
class ParseTools:
def __init__(self):
self.head_exp = r'^.*\[id:\$\w+\]\n'
def parse(self, string):
string = string.replace('\r', '')
if re.match(self.head_exp, string):
string = re.sub(self.head_exp, '', string)
trans = re.search(r'\[language:([\w=\\/+]+)\]', string)
lyric = None
rlyric = None
tlyric = None
if trans:
string = re.sub(r'\[language:[\w=\\/+]+\]\n', '', string)
decoded_trans = createBase64Decode(trans.group(1)).decode('utf-8')
trans_json = json.loads(decoded_trans)
for item in trans_json['content']:
if item['type'] == 0:
rlyric = item['lyricContent']
elif item['type'] == 1:
tlyric = item['lyricContent']
self.i = 0
lxlyric = re.sub(r'\[((\d+),\d+)\].*', lambda x: self.process_lyric_match(x, rlyric, tlyric, self.i), string)
rlyric = '\n'.join(rlyric) if rlyric else ''
tlyric = '\n'.join(tlyric) if tlyric else ''
lxlyric = re.sub(r'<(\d+,\d+),\d+>', r'<\1>', lxlyric)
lyric = re.sub(r'<\d+,\d+>', '', lxlyric)
return {
'lyric': lyric,
'tlyric': tlyric,
'rlyric': rlyric,
'lxlyric': lxlyric
}
def process_lyric_match(self, match, rlyric, tlyric, i):
result = re.match(r'\[((\d+),\d+)\].*', match.group(0))
time = int(result.group(2))
ms = time % 1000
time /= 1000
m = str(int(time / 60)).zfill(2)
time %= 60
s = str(int(time)).zfill(2)
time_string = f'{m}:{s}.{ms}'
transformed_t = ''
if (tlyric):
for t in tlyric[i]:
transformed_t += t
tlyric[i] = transformed_t
if (rlyric):
nr = []
for r in rlyric[i]:
nr.append(r)
_tnr = ''.join(nr)
if (' ' in _tnr):
rlyric[i] = _tnr
else:
nr = []
for r in rlyric[i]:
nr.append(r.strip())
rlyric[i] = ' '.join(nr)
if rlyric:
rlyric[i] = f'[{time_string}]{rlyric[i] if rlyric[i] else ""}'.replace(' ', ' ')
if tlyric:
tlyric[i] = f'[{time_string}]{tlyric[i] if tlyric[i] else ""}'
self.i += 1
return re.sub(result.group(1), time_string, match.group(0))
global_parser = ParseTools()
def krcDecode(a:bytes):
encrypt_key = (64, 71, 97, 119, 94, 50, 116, 71, 81, 54, 49, 45, 206, 210, 110, 105)
content = a[4:] # krc1
compress_content = bytes(content[i] ^ encrypt_key[i % len(encrypt_key)] for i in range(len(content)))
text_bytes = zlib.decompress(bytes(compress_content))
text = text_bytes.decode("utf-8")
return text
async def lyricSearchByHash(hash_):
musicInfo = await getMusicInfo(hash_)
if (not musicInfo):
raise FailedException('歌曲信息获取失败')
hash_new = musicInfo['audio_info']['hash']
name = musicInfo['songname']
timelength = int(musicInfo['audio_info']['timelength']) // 1000
req = await Httpx.AsyncRequest(encodeURI(f'https://lyrics.kugou.com/search?ver=1&man=yes&client=pc&keyword=' +
name + '&hash=' + hash_new + '&timelength=' + str(timelength)), {
'method': 'GET',
})
body = req.json()
if (body['status'] != 200):
raise FailedException('歌词获取失败')
if (not body['candidates']):
raise FailedException('歌词获取失败: 当前歌曲无歌词')
return body['candidates']
async def getLyric(lyric_id, accesskey):
req = await Httpx.AsyncRequest(f'https://lyrics.kugou.com/download?ver=1&client=pc&id={lyric_id}&accesskey={accesskey}', {
'method': 'GET',
})
body = req.json()
if (body['status'] != 200 or body['error_code'] != 0 or (not body['content'])):
raise FailedException('歌词获取失败')
content = createBase64Decode(body['content'])
content = krcDecode(content)
return global_parser.parse(content)

View File

@ -1,104 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: musicInfo.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common.utils import createMD5
from common import Httpx
from .utils import tools, signRequest
import random
import ujson as json
import time
async def getMusicInfo(hash_, use_cache = True):
tn = int(time.time())
url = "http://gateway.kugou.com/v3/album_audio/audio"
options = {
"method": "POST",
"headers": {
"KG-THash": "13a3164",
"KG-RC": "1",
"KG-Fake": "0",
"KG-RF": "00869891",
"User-Agent": "Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi",
"x-router": "kmr.service.kugou.com",
},
"body": {
"area_code": "1",
"show_privilege": "1",
"show_album_info": "1",
"is_publish": "",
"appid": 1005,
"clientver": 11451,
"mid": '114514',
"dfid": "-",
"clienttime": tn,
"key": 'OIlwlieks28dk2k092lksi2UIkp',
"data": [
{
"hash": hash_
}
]
},
'cache': 86400 * 30 if use_cache else 'no-cache',
'cache-ignore': [tn]
}
body = await Httpx.AsyncRequest(url, dict(options))
body = body.json()
return body['data'][0][0] if (body['data'] and body['data'][0]) else {}
async def getMusicSingerInfo(hash_, use_cache = True):
# https://expendablekmrcdn.kugou.com/container/v2/image?album_image_type=-3&appid=1005&author_image_type=4%2C5&clientver=12029&count=5&data=%5B%7B%22mixSongId%22%3A452960726%2C%22album_id%22%3A62936873%2C%22hash%22%3A%2241f45664e8235b786990cbf213cd4725%22%2C%22filename%22%3A%22%E8%A2%81%E5%B0%8F%E8%91%B3%E3%80%81%E9%98%BF%E8%BE%B0%EF%BC%88%E9%98%8E%E8%BE%B0%EF%BC%89%20-%20%E5%8C%96%E4%BD%9C%E7%83%9F%E7%81%AB%E4%B8%BA%E4%BD%A0%E5%9D%A0%E8%90%BD%22%2C%22album_audio_id%22%3A452960726%7D%5D&isCdn=1&publish_time=1&signature=b6670b9d81ca1a4e52e186c4db74c7f2
url = "https://expendablekmrcdn.kugou.com/container/v2/image"
params = {
"album_image_type": -3,
"appid": 1005,
"author_image_type": "4,5",
"clientver": 12029,
"count": 5,
"data": json.dumps([
{
"hash": hash_.lower()
}
]),
"isCdn": 1,
"publish_time": 1
}
uuid = createMD5(str(random.randint(100000, 999999)) + '114514')
req = await signRequest(url, params, {
'method': 'GET',
'headers': {
'User-Agent': 'Android712-AndroidPhone-11451-18-0-Avatar-wifi',
'KG-THash': '2a2624f',
'KG-RC': '1',
'KG-Fake': '0',
'KG-RF': '0074c2c4',
'appid': '1005',
'clientver': '11451',
'uuid': uuid,
},
'cache': 86400 * 30 if use_cache else 'no-cache',
'cache-ignore': [uuid]
}, 'OIlwieks28dk2k092lksi2UIkp')
authors = req.json()['data'][0]['author']
res = []
for a in authors:
res.append({
'name': a['author_name'],
'id': a['author_id'],
'avatar': a['sizable_avatar'].format(size = 1080),
'sizable_avatar': a['sizable_avatar'],
})
return res
async def getMusicMVHash(hash_, use_cache = True):
req = await Httpx.AsyncRequest('http://mobilecdnbj.kugou.com/api/v3/song/info?hash=' + hash_, {
'method': 'GET',
'cache': 86400 * 30 if use_cache else 'no-cache',
})
body = req.json()
return body['data']['mvhash'] if (body['data']) else ''

View File

@ -1,86 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: mv.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common import Httpx
from common import utils
from common.exceptions import FailedException
async def getMvInfo(mvhash, use_cache = True):
req = await Httpx.AsyncRequest(f'http://mobilecdnbj.kugou.com/api/v3/mv/detail?mvhash={mvhash}', {
'method': 'GET',
'cache': 86400 * 30 if use_cache else 'no-cache',
})
body = req.json()
if (body['status'] != 1):
raise FailedException('获取失败')
if (not body['data']['info']):
raise FailedException('mv不存在')
singers = []
for s in body['data']['info']['authors']:
singers.append({
'name': s['singername'],
'id': s['singerid'],
'avatar': s['singeravatar'].format(size=1080),
'sizable_avatar': s['singeravatar'],
})
tags = []
for t in body['data']['info']['tags']:
tags.append(t['tag_name'])
return {
'name': body['data']['info']['filename'].replace(body['data']['info']['singername'] + ' - ', ''),
'name_ori': body['data']['info']['videoname'],
'name_extra': body['data']['info']['remark'],
'filename': body['data']['info']['filename'],
'intro': body['data']['info']['description'],
'music_hash': body['data']['info']['audio_info']['hash'],
'music_id': body['data']['info']['audio_info']['audio_id'],
'format_length': utils.timeLengthFormat(body['data']['info']['mv_timelength'] / 1000),
'length': body['data']['info']['mv_timelength'] / 1000,
'hash': body['data']['info']['hash'],
'vid': body['data']['info']['video_id'],
'singer': body['data']['info']['singername'],
'singer_list': singers,
'tags': tags,
'cover': body['data']['info']['imgurl'].format(size=1080),
'sizable_cover': body['data']['info']['imgurl'],
}
async def getMvPlayURL(mvhash):
req = await Httpx.AsyncRequest(f'https://m.kugou.com/app/i/mv.php?cmd=100&hash={mvhash}&ismp3=1&ext=mp4', {
'method': 'GET'
})
body = req.json()
if (body['status'] != 1):
return {}
formatted = {}
if (body['mvdata']['le']):
formatted['270p'] = {
'url': body['mvdata']['le']['downurl'],
'hash': body['mvdata']['le']['hash'],
'bitrate': body['mvdata']['le']['bitrate'],
'format_size': utils.sizeFormat(body['mvdata']['le']['filesize']),
'size': body['mvdata']['le']['filesize'],
}
if (body['mvdata']['sq']):
formatted['720p'] = {
'url': body['mvdata']['sq']['downurl'],
'hash': body['mvdata']['sq']['hash'],
'bitrate': body['mvdata']['sq']['bitrate'],
'format_size': utils.sizeFormat(body['mvdata']['sq']['filesize']),
'size': body['mvdata']['sq']['filesize'],
}
if (body['mvdata']['rq']):
formatted['1080p'] = {
'url': body['mvdata']['rq']['downurl'],
'hash': body['mvdata']['rq']['hash'],
'bitrate': body['mvdata']['rq']['bitrate'],
'format_size': utils.sizeFormat(body['mvdata']['rq']['filesize']),
'size': body['mvdata']['rq']['filesize'],
}
return formatted

View File

@ -1,80 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: player.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import random
from common.exceptions import FailedException
from common import config, utils, variable
from .utils import getKey, signRequest, tools
from .musicInfo import getMusicInfo
import time
async def url(songId, quality):
songId = songId.lower()
body_ = await getMusicInfo(songId)
thash = body_['audio_info'][tools.qualityHashMap[quality]]
albumid = body_['album_info']['album_id'] if (body_.get('album_info') and body_['album_info'].get('album_id')) else None
albumaudioid = body_['album_audio_id'] if (body_.get('album_audio_id')) else None
if (not thash):
raise FailedException('获取歌曲信息失败')
if (not albumid):
albumid = ""
if (not albumaudioid):
albumaudioid = ""
thash = thash.lower()
user_info = config.read_config('module.kg.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.kg'))
params = {
'album_id': albumid,
'userid': user_info['userid'],
'area_code': 1,
'hash': thash,
'module': '',
'mid': user_info['mid'],
'appid': tools.appid,
'ssa_flag': 'is_fromtrack',
'clientver': tools.clientver,
'open_time': time.strftime("%Y%m%d"),
'vipType': 6,
'ptype': 0,
'token': user_info['token'],
'auth': '',
'mtype': 0,
'album_audio_id': albumaudioid,
'behavior': 'play',
'clienttime': int(time.time()),
'pid': tools.pid,
'key': getKey(thash, user_info),
'dfid': '-',
'pidversion': 3001
}
if (tools.version == 'v5'):
params['quality'] = tools.qualityMap[quality]
if (tools.version == "v4"):
params['version'] = tools.clientver
params = utils.mergeDict(tools["extra_params"], params)
headers = {
'User-Agent': 'Android712-AndroidPhone-8983-18-0-NetMusic-wifi',
'KG-THash': '3e5ec6b',
'KG-Rec': '1',
'KG-RC': '1',
}
if (tools['x-router']['enable']):
headers['x-router'] = tools['x-router']['value']
req = await signRequest(tools.url, params, {'headers': headers})
body = req.json()
if body['status'] == 3:
raise FailedException('该歌曲在酷狗没有版权,请换源播放')
elif body['status'] == 2:
raise FailedException('链接获取失败,请检查账号是否有会员或数字专辑是否购买')
elif body['status'] != 1:
raise FailedException('链接获取失败可能是数字专辑或者api失效')
return {
'url': body["url"][0],
'quality': quality
}

View File

@ -1,124 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq - (feat. Huibq)
# - name: refresh_token.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import time
from common import variable
from common import scheduler
from common import config
from common import log
from .utils import signRequest, tools, aes_sign
import ujson as json
logger = log.log('kg_refresh_token')
async def refresh():
if (not config.read_config('module.kg.user.token')):
return
if (not config.read_config('module.kg.user.refresh_token.enable')):
return
if (config.read_config('module.kg.client.appid') == '1005'):
ts = int(time.time() * 1000)
p3 = aes_sign(json.dumps({'clienttime': ts // 1000, 'token': config.read_config('module.kg.user.token')}))
data = {
'p3': p3,
'clienttime_ms': ts,
't1': 0,
't2': 0,
'userid': config.read_config('module.kg.user.userid')
}
params = {
'dfid': '-',
'appid': tools.appid,
'mid': tools.mid,
'clientver': tools.clientver,
'clienttime': ts // 1000
}
headers = {
'User-Agent': 'Android712-AndroidPhone-8983-18-0-NetMusic-wifi',
'KG-THash': '3e5ec6b',
'KG-Rec': '1',
'KG-RC': '1',
}
login_url = config.read_config('module.kg.user.refresh_token.login_url')
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
body = req.json()
if body['error_code'] != 0:
logger.warning('刷新登录失败, code: ' +
str(body['error_code']) + f'\n响应体: {body}')
return
else:
logger.info('刷新登录成功')
config.write_config('module.kg.user.userid',
str(body['data']['userid']))
logger.info(f'已通过相应数据更新userid')
config.write_config('module.kg.user.token',
body['data']['token'])
logger.info('已通过相应数据更新kg_token')
elif (config.read_config('module.kg.client.appid') == '3116'):
ts = int(time.time() * 1000)
p3 = aes_sign(json.dumps({'clienttime': ts // 1000, 'token': config.read_config('module.kg.user.token')}), key=b'c24f74ca2820225badc01946dba4fdf7', iv=b'adc01946dba4fdf7')
data = {
'p3': p3,
'clienttime_ms': ts,
't1': 0,
't2': 0,
'userid': config.read_config('module.kg.user.userid')
}
params = {
'dfid': '-',
'appid': tools.appid,
'mid': tools.mid,
'clientver': tools.clientver,
'clienttime': ts // 1000
}
headers = {
'User-Agent': 'Android712-AndroidPhone-8983-18-0-NetMusic-wifi',
'KG-THash': '3e5ec6b',
'KG-Rec': '1',
'KG-RC': '1',
}
login_url = config.read_config('module.kg.user.refresh_token.login_url')
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
body = req.json()
if body['error_code'] != 0:
logger.warning('刷新登录失败, code: ' +
str(body['error_code']) + f'\n响应体: {body}')
return
else:
logger.info('刷新登录成功')
config.write_config('module.kg.user.userid',
str(body['data']['userid']))
logger.info(f'已通过相应数据更新userid')
config.write_config('module.kg.user.token',
body['data']['token'])
logger.info('已通过相应数据更新kg_token')
if (not variable.use_cookie_pool):
kgconfig = config.read_config('module.kg')
refresh_login_info = kgconfig.get('refresh_token')
if (refresh_login_info):
kgconfig['user']['refresh_token'] = refresh_login_info
kgconfig.pop('refresh_login')
config.write_config('module.kg', kgconfig)
if (config.read_config('module.kg.user.refresh_token.enable') and not variable.use_cookie_pool):
scheduler.append('kg_refresh_token', refresh,
config.read_config('module.kg.user.refresh_token.interval'))
async def refresh_login_for_pool(user_info):
# TODO
pass
def reg_refresh_login_pool_task():
# TODO
pass
if (variable.use_cookie_pool):
# TODO
pass

View File

@ -1,142 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: search.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common import Httpx
from common import utils
from common.exceptions import FailedException
from .utils import buildRequestParams
def formatSubResult(l):
res = []
for songinfo in l:
fileinfo = {}
if (songinfo['FileSize'] != 0):
fileinfo['128k'] = {
'hash': songinfo['FileHash'],
'size': utils.sizeFormat(songinfo['FileSize']),
}
if (songinfo['HQFileSize'] != 0):
fileinfo['320k'] = {
'hash': songinfo['HQFileHash'],
'size': utils.sizeFormat(songinfo['HQFileSize']),
}
if (songinfo['SQFileSize'] != 0):
fileinfo['flac'] = {
'hash': songinfo['SQFileHash'],
'size': utils.sizeFormat(songinfo['SQFileSize']),
}
if (songinfo['ResFileSize'] != 0):
fileinfo['flac24bit'] = {
'hash': songinfo['ResFileHash'],
'size': utils.sizeFormat(songinfo['ResFileSize']),
}
res.append({
'name': songinfo['SongName'],
'name_ori': songinfo['OriSongName'],
'name_extra': songinfo['SongName'].replace(songinfo['OriSongName'], ''),
'singer': songinfo['SingerName'],
'singer_list': [{'name': i['name'], 'id': i['id']} for i in songinfo['Singers']],
'isoriginal': True if (songinfo['IsOriginal'] == 1) else False,
'tag': songinfo.get('TagContent') if songinfo.get('TagContent') else '',
'format_length': utils.timeLengthFormat(songinfo['Duration']),
'length': songinfo['Duration'],
'hash': songinfo['FileHash'],
'file_info': fileinfo,
'songmid': songinfo['Audioid'],
'album_id': songinfo['AlbumID'],
'album': songinfo['AlbumName'],
'language': songinfo['trans_param'].get('language') if songinfo['trans_param'] else '',
'cover': songinfo['Image'].format(size = 1080),
'sizable_cover': songinfo['Image'],
'mvid': songinfo['MvHash'],
})
return res
async def getSongSearchResult(query, page = 1, size = 20):
page = int(page)
size = int(size)
req = await Httpx.AsyncRequest(utils.encodeURI(f'https://songsearch.kugou.com/song_search_v2?' + buildRequestParams({
"keyword": query,
"page": page,
"pagesize": size,
"area_code": 1,
"userid": 0,
"clientver": "",
"platform": "WebFilter",
"filter": 2,
"iscorrection": 1,
"privilege_filter": 0
})), {
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.142.86 Safari/537.36",
"Referer": "https://www.kugou.com",
}
})
body = req.json()
if (body['status'] != 1):
raise FailedException('歌曲搜索失败')
if (body['data']['total'] == 0 or body['data']['lists'] == []):
return {
'total': 0,
'page': page,
'size': size,
'list': [],
}
res = []
for songinfo in body['data']['lists']:
fileinfo = {}
if (songinfo['FileSize'] != 0):
fileinfo['128k'] = {
'hash': songinfo['FileHash'],
'size': utils.sizeFormat(songinfo['FileSize']),
}
if (songinfo['HQFileSize'] != 0):
fileinfo['320k'] = {
'hash': songinfo['HQFileHash'],
'size': utils.sizeFormat(songinfo['HQFileSize']),
}
if (songinfo['SQFileSize'] != 0):
fileinfo['flac'] = {
'hash': songinfo['SQFileHash'],
'size': utils.sizeFormat(songinfo['SQFileSize']),
}
if (songinfo['ResFileSize'] != 0):
fileinfo['flac24bit'] = {
'hash': songinfo['ResFileHash'],
'size': utils.sizeFormat(songinfo['ResFileSize']),
}
res.append({
'name': songinfo['SongName'],
'name_ori': songinfo['OriSongName'],
'name_extra': songinfo['SongName'].replace(songinfo['OriSongName'], ''),
'singer': songinfo['SingerName'],
'singer_list': [{'name': i['name'], 'id': i['id']} for i in songinfo['Singers']],
'isoriginal': True if (songinfo['IsOriginal'] == 1) else False,
'tag': songinfo.get('TagContent') if songinfo.get('TagContent') else '',
'format_length': utils.timeLengthFormat(songinfo['Duration']),
'length': songinfo['Duration'],
'hash': songinfo['FileHash'],
'file_info': fileinfo,
'songmid': songinfo['Audioid'],
'album_id': songinfo['AlbumID'],
'album': songinfo['AlbumName'],
'language': songinfo['trans_param'].get('language') if songinfo['trans_param'] else '',
'cover': songinfo['Image'].format(size = 1080),
'sizable_cover': songinfo['Image'],
'mvid': songinfo['MvHash'],
'subresult': [] if (songinfo['Grp'] == []) else formatSubResult(songinfo['Grp']),
})
return {
'total': body['data']['total'],
'page': page,
'size': size,
'list': res,
}

View File

@ -1,72 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: utils.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from common import utils
from common import config
from common import Httpx
import json
createObject = utils.CreateObject
tools = createObject({
"signkey": config.read_config("module.kg.client.signatureKey"),
"pidversec": config.read_config("module.kg.client.pidversionsecret"),
"clientver": config.read_config("module.kg.client.clientver"),
"x-router": config.read_config("module.kg.tracker.x-router"),
"url": config.read_config("module.kg.tracker.host") + config.read_config("module.kg.tracker.path"),
"version": config.read_config("module.kg.tracker.version"),
"extra_params": config.read_config("module.kg.tracker.extra_params"),
"appid": config.read_config("module.kg.client.appid"),
'mid': config.read_config('module.kg.user.mid'),
"pid": config.read_config("module.kg.client.pid"),
'qualityHashMap': {
'128k': 'hash_128',
'320k': 'hash_320',
'flac': 'hash_flac',
'flac24bit': 'hash_high',
'master': 'hash_128',
},
'qualityMap': {
'128k': '128',
'320k': '320',
'flac': 'flac',
'flac24bit': 'high',
'master': 'viper_atmos',
},
})
def buildSignatureParams(dictionary, body = ""):
joined_str = ''.join([f'{k}={v}' for k, v in dictionary.items()])
return joined_str + body
def buildRequestParams(dictionary: dict):
joined_str = '&'.join([f'{k}={v}' for k, v in dictionary.items()])
return joined_str
def sign(params, body = "", signkey = tools["signkey"]):
if (isinstance(body, dict)):
body = json.dumps(body)
params = utils.sortDict(params)
params = buildSignatureParams(params, body)
return utils.createMD5(signkey + params + signkey)
async def signRequest(url, params, options, signkey = tools["signkey"]):
params['signature'] = sign(params, options.get("body") if options.get("body") else (options.get("data") if options.get("data") else (options.get("json") if options.get("json") else "")), signkey)
url = url + "?" + buildRequestParams(params)
return await Httpx.AsyncRequest(url, options)
def getKey(hash_, user_info):
return utils.createMD5(hash_.lower() + tools.pidversec + tools.appid + user_info['mid'] + user_info['userid'])
def aes_sign(plain_text, key=b'90b8382a1bb4ccdcf063102053fd75b8', iv=b'f063102053fd75b8'):
cipher = AES.new(key, AES.MODE_CBC, iv)
crypto = cipher.encrypt(pad(plain_text.encode(), AES.block_size))
return crypto.hex()

View File

@ -1,117 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: __init__.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import random
from common import Httpx, config, variable
from common.exceptions import FailedException
from common.utils import CreateObject
from .encrypt import base64_encrypt
tools = {
'qualityMap': {
'128k': '128kmp3',
'320k': '320kmp3',
'flac': '2000kflac',
'flac24bit': '4000kflac',
'128kmp3': '128kmp3',
'320kmp3': '320kmp3',
"2000kflac": "2000kflac",
"4000kflac": "4000kflac",
},
'qualityMapReverse': {
128: '128k',
320: '320k',
2000: 'flac',
4000: 'flac24bit',
},
'extMap': {
'128k': 'mp3',
'320k': 'mp3',
'flac': 'flac',
'flac24bit': 'flac',
}
}
async def url(songId, quality):
proto = config.read_config('module.kw.proto')
if (proto == 'bd-api'):
user_info = config.read_config('module.kw.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.kw'))
target_url = f'''https://bd-api.kuwo.cn/api/service/music/downloadInfo/{songId}?isMv=0&format={tools['extMap'][quality]}&br={tools['qualityMap'][quality]}&uid={user_info['uid']}&token={user_info['token']}'''
req = await Httpx.AsyncRequest(target_url, {
'method': 'GET',
'headers': {
'User-Agent': 'Dart/2.14 (dart:io)',
'channel': 'qq',
'plat': 'ar',
'net': 'wifi',
'ver': '3.1.2',
'uid': user_info['uid'],
'devId': user_info['device_id'],
}
})
try:
body = req.json()
data = body['data']
if (body['code'] != 200) or (int(data['audioInfo']['bitrate']) == 1):
raise FailedException('failed')
return {
'url': data['url'].split('?')[0],
'quality': tools['qualityMapReverse'][int(data['audioInfo']['bitrate'])]
}
except:
raise FailedException('failed')
elif (proto == 'kuwodes'):
des_info = config.read_config('module.kw.des')
params = des_info['params'].format(
songId = songId,
map_quality = tools['qualityMap'][quality],
ext = tools['extMap'][quality],
raw_quality = quality,
)
target_url = f'https://{des_info["host"]}/{des_info["path"]}?f={des_info["f"]}&' + ('q=' + base64_encrypt(params) if (des_info["need_encrypt"]) else params)
req = await Httpx.AsyncRequest(target_url, {
'method': 'GET',
'headers': des_info['headers']
})
url = ''
bitrate = 1
if (des_info["response_type"] == 'json'):
url = req.json()
for p in des_info['url_json_path'].split('.'):
url = url.get(p)
if (url == None):
raise FailedException('failed')
bitrate = req.json()
for p in des_info['bitrate_json_path'].split('.'):
bitrate = bitrate.get(p)
if (bitrate == None):
raise FailedException('failed')
elif (des_info['response_type'] == 'text'):
body = req.text
for l in body.split('\n'):
l = l.strip()
if (l.startswith('url=')):
url = l.split('=')[1]
elif (l.startswith('bitrate=')):
bitrate = int(l.split('=')[1])
else:
raise FailedException('配置文件参数response_type填写错误或不支持')
bitrate = int(bitrate)
if (url == '' or bitrate == 1):
raise FailedException('failed')
if (not url.startswith('http')):
raise FailedException('failed')
return {
'url': url.split('?')[0],
'quality': tools['qualityMapReverse'][bitrate]
}
else:
raise FailedException('配置文件参数proto填写错误或不支持')

View File

@ -1,237 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: encrypt.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
# KuwoDES加密实现CV过来的不进行注释
# 下方为原文件版权声明,在此进行保留
# Copyright (C) 2014 LiuLang <gsushzhsosgsu@gmail.com>
# Use of this source code is governed by GPLv3 license that can be found
# in http://www.gnu.org/licenses/gpl-3.0.html
import base64
DES_MODE_DECRYPT = 1
arrayE = [
31, 0, DES_MODE_DECRYPT, 2, 3, 4, -1, -1, 3, 4, 5, 6, 7, 8, -1, -1, 7, 8, 9, 10, 11, 12, -1, -1, 11, 12, 13, 14, 15, 16, -1, -
1, 15, 16, 17, 18, 19, 20, -1, -1, 19, 20, 21, 22, 23, 24, -1, -
1, 23, 24, 25, 26, 27, 28, -1, -1, 27, 28, 29, 30, 31, 30, -1, -1
]
arrayIP = [
57, 49, 41, 33, 25, 17, 9, DES_MODE_DECRYPT, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6
]
arrayIP_1 = [
39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, DES_MODE_DECRYPT, 41, 9, 49, 17, 57, 25, 32, 0, 40, 8, 48, 16, 56, 24
]
arrayLs = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
arrayLsMask = [0, 0x100001, 0x300003]
arrayMask = [2 ** i for i in range(64)]
arrayMask[-1] *= -1
arrayP = [
15, 6, 19, 20, 28, 11, 27, 16,
0, 14, 22, 25, 4, 17, 30, 9,
1, 7, 23, 13, 31, 26, 2, 8,
18, 12, 29, 5, 21, 10, 3, 24,
]
arrayPC_1 = [
56, 48, 40, 32, 24, 16, 8, 0,
57, 49, 41, 33, 25, 17, 9, 1,
58, 50, 42, 34, 26, 18, 10, 2,
59, 51, 43, 35, 62, 54, 46, 38,
30, 22, 14, 6, 61, 53, 45, 37,
29, 21, 13, 5, 60, 52, 44, 36,
28, 20, 12, 4, 27, 19, 11, 3,
]
arrayPC_2 = [
13, 16, 10, 23, 0, 4, -1, -1,
2, 27, 14, 5, 20, 9, -1, -1,
22, 18, 11, 3, 25, 7, -1, -1,
15, 6, 26, 19, 12, 1, -1, -1,
40, 51, 30, 36, 46, 54, -1, -1,
29, 39, 50, 44, 32, 47, -1, -1,
43, 48, 38, 55, 33, 52, -1, -1,
45, 41, 49, 35, 28, 31, -1, -1,
]
matrixNSBox = [[
14, 4, 3, 15, 2, 13, 5, 3,
13, 14, 6, 9, 11, 2, 0, 5,
4, 1, 10, 12, 15, 6, 9, 10,
1, 8, 12, 7, 8, 11, 7, 0,
0, 15, 10, 5, 14, 4, 9, 10,
7, 8, 12, 3, 13, 1, 3, 6,
15, 12, 6, 11, 2, 9, 5, 0,
4, 2, 11, 14, 1, 7, 8, 13, ], [
15, 0, 9, 5, 6, 10, 12, 9,
8, 7, 2, 12, 3, 13, 5, 2,
1, 14, 7, 8, 11, 4, 0, 3,
14, 11, 13, 6, 4, 1, 10, 15,
3, 13, 12, 11, 15, 3, 6, 0,
4, 10, 1, 7, 8, 4, 11, 14,
13, 8, 0, 6, 2, 15, 9, 5,
7, 1, 10, 12, 14, 2, 5, 9, ], [
10, 13, 1, 11, 6, 8, 11, 5,
9, 4, 12, 2, 15, 3, 2, 14,
0, 6, 13, 1, 3, 15, 4, 10,
14, 9, 7, 12, 5, 0, 8, 7,
13, 1, 2, 4, 3, 6, 12, 11,
0, 13, 5, 14, 6, 8, 15, 2,
7, 10, 8, 15, 4, 9, 11, 5,
9, 0, 14, 3, 10, 7, 1, 12, ], [
7, 10, 1, 15, 0, 12, 11, 5,
14, 9, 8, 3, 9, 7, 4, 8,
13, 6, 2, 1, 6, 11, 12, 2,
3, 0, 5, 14, 10, 13, 15, 4,
13, 3, 4, 9, 6, 10, 1, 12,
11, 0, 2, 5, 0, 13, 14, 2,
8, 15, 7, 4, 15, 1, 10, 7,
5, 6, 12, 11, 3, 8, 9, 14, ], [
2, 4, 8, 15, 7, 10, 13, 6,
4, 1, 3, 12, 11, 7, 14, 0,
12, 2, 5, 9, 10, 13, 0, 3,
1, 11, 15, 5, 6, 8, 9, 14,
14, 11, 5, 6, 4, 1, 3, 10,
2, 12, 15, 0, 13, 2, 8, 5,
11, 8, 0, 15, 7, 14, 9, 4,
12, 7, 10, 9, 1, 13, 6, 3, ], [
12, 9, 0, 7, 9, 2, 14, 1,
10, 15, 3, 4, 6, 12, 5, 11,
1, 14, 13, 0, 2, 8, 7, 13,
15, 5, 4, 10, 8, 3, 11, 6,
10, 4, 6, 11, 7, 9, 0, 6,
4, 2, 13, 1, 9, 15, 3, 8,
15, 3, 1, 14, 12, 5, 11, 0,
2, 12, 14, 7, 5, 10, 8, 13, ], [
4, 1, 3, 10, 15, 12, 5, 0,
2, 11, 9, 6, 8, 7, 6, 9,
11, 4, 12, 15, 0, 3, 10, 5,
14, 13, 7, 8, 13, 14, 1, 2,
13, 6, 14, 9, 4, 1, 2, 14,
11, 13, 5, 0, 1, 10, 8, 3,
0, 11, 3, 5, 9, 4, 15, 2,
7, 8, 12, 15, 10, 7, 6, 12, ], [
13, 7, 10, 0, 6, 9, 5, 15,
8, 4, 3, 10, 11, 14, 12, 5,
2, 11, 9, 6, 15, 12, 0, 3,
4, 1, 14, 13, 1, 2, 7, 8,
1, 2, 12, 15, 10, 4, 0, 3,
13, 14, 6, 9, 7, 8, 9, 6,
15, 1, 5, 12, 3, 10, 14, 5,
8, 7, 11, 0, 4, 13, 2, 11, ],
]
SECRET_KEY = b'ylzsxkwm'
def bit_transform(arr_int, n, l):
l2 = 0
for i in range(n):
if arr_int[i] < 0 or (l & arrayMask[arr_int[i]] == 0):
continue
l2 |= arrayMask[i]
return l2
def DES64(longs, l):
out = 0
SOut = 0
pR = [0] * 8
pSource = [0, 0]
sbi = 0
t = 0
L = 0
R = 0
out = bit_transform(arrayIP, 64, l)
pSource[0] = 0xFFFFFFFF & out
pSource[1] = (-4294967296 & out) >> 32
for i in range(16):
R = pSource[1]
R = bit_transform(arrayE, 64, R)
R ^= longs[i]
for j in range(8):
pR[j] = 255 & R >> j * 8
SOut = 0
for sbi in range(7, -1, -1):
SOut <<= 4
SOut |= matrixNSBox[sbi][pR[sbi]]
R = bit_transform(arrayP, 32, SOut)
L = pSource[0]
pSource[0] = pSource[1]
pSource[1] = L ^ R
pSource = pSource[::-1]
out = -4294967296 & pSource[1] << 32 | 0xFFFFFFFF & pSource[0]
out = bit_transform(arrayIP_1, 64, out)
return out
def sub_keys(l, longs, n):
l2 = bit_transform(arrayPC_1, 56, l)
for i in range(16):
l2 = ((l2 & arrayLsMask[arrayLs[i]]) << 28 - arrayLs[i] | (l2 & ~arrayLsMask[arrayLs[i]]) >> arrayLs[i])
longs[i] = bit_transform(arrayPC_2, 64, l2)
j = 0
while n == 1 and j < 8:
l3 = longs[j]
longs[j], longs[15-j] = longs[15-j], longs[j]
j += 1
def encrypt(msg, key=SECRET_KEY):
if isinstance(msg, str):
msg = msg.encode()
if isinstance(key, str):
key = key.encode()
assert (isinstance(msg, bytes))
assert (isinstance(key, bytes))
# 处理密钥块
l = 0
for i in range(8):
l = l | key[i] << i * 8
j = len(msg) // 8
# arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了
arrLong1 = [0] * 16
sub_keys(l, arrLong1, 0)
# arrLong2 存放的是前部分的明文
arrLong2 = [0] * j
for m in range(j):
for n in range(8):
arrLong2[m] |= msg[n + m * 8] << n * 8
# 用于存放密文
arrLong3 = [0] * ((1 + 8 * (j + 1)) // 8)
# 计算前部的数据块(除了最后一部分)
for i1 in range(j):
arrLong3[i1] = DES64(arrLong1, arrLong2[i1])
# 保存多出来的字节
arrByte1 = msg[j*8:]
l2 = 0
for i1 in range(len(msg) % 8):
l2 |= arrByte1[i1] << i1 * 8
# 计算多出的那一位(最后一位)
arrLong3[j] = DES64(arrLong1, l2)
# 将密文转为字节型
arrByte2 = [0] * (8 * len(arrLong3))
i4 = 0
for l3 in arrLong3:
for i6 in range(8):
arrByte2[i4] = (255 & l3 >> i6 * 8)
i4 += 1
return arrByte2
def base64_encrypt(msg):
b1 = encrypt(msg)
b2 = bytearray(b1)
s = base64.encodebytes(b2)
return s.replace(b'\n', b'').decode()

View File

@ -1,63 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: __init__.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import random
from common import Httpx
from common import config
from common import variable
from common.exceptions import FailedException
from . import refresh_login
tools = {
'qualityMap': {
'128k': '1',
'320k': '2',
'flac': '3',
'flac24bit': '4',
"master": "5"
},
'qualityMapReverse': {
'000009': '128k',
'020010': '320k',
'011002': 'flac',
'011005': 'flac24bit',
},
}
async def url(songmid, quality):
info_url = f"http://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceType=2&copyrightId=" + songmid
info_request = await Httpx.AsyncRequest(info_url, {"method": "POST", "cache": 259200})
infobody = info_request.json()
if infobody["code"] != "000000":
raise FailedException("failed to fetch song info")
user_info = config.read_config('module.mg.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.mg'))
req = await Httpx.AsyncRequest(f'https://m.music.migu.cn/migumusic/h5/play/auth/getSongPlayInfo?type={tools["qualityMap"][quality]}&copyrightId={infobody["resource"][0]["copyrightId"]}', {
'method': 'GET',
'headers': {
'User-Agent': user_info['useragent'],
"by": user_info["by"],
"Cookie": "SESSION=" + user_info["session"],
"Referer": "https://m.music.migu.cn/v4/",
"Origin": "https://m.music.migu.cn",
},
})
try:
body = req.json()
if (int(body['code']) != 200 or (not body.get("data")) or (not body["data"]["playUrl"])):
raise FailedException(body.get("msg") if body.get("msg") else "failed")
data = body["data"]
return {
'url': body["data"]["playUrl"].split("?")[0] if body["data"]["playUrl"].split("?")[0].startswith("http") else "http:" + body["data"]["playUrl"].split("?")[0],
'quality': tools['qualityMapReverse'].get(data['formatId']) if (tools['qualityMapReverse'].get(data['formatId'])) else "unknown",
}
except:
raise FailedException('failed')

View File

@ -1,51 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: refresh_login.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common import Httpx
from common import config
from common.exceptions import FailedException
from common import scheduler
from common import variable
from common import log
logger = log.log("migu_refresh_login")
async def do_account_refresh(user_info):
req = await Httpx.AsyncRequest("https://m.music.migu.cn/migumusic/h5/user/auth/userActiveNotice", {
"method": "POST",
"body": "",
"headers": {
"User-Agent": user_info["useragent"],
"by": user_info["by"],
"Cookie": "SESSION=" + user_info["session"],
"Referer": "https://m.music.migu.cn/v4/my",
"Origin": "https://m.music.migu.cn",
},
})
body = req.json()
if (int(body["code"]) != 200):
raise FailedException("咪咕session保活失败: " + str(body["msg"]))
return logger.info("咪咕session保活成功")
if (variable.use_cookie_pool):
users = config.read_config("module.cookiepool.mg")
for u in users:
ref = u.get("refresh_login") if u.get("refresh_login") else {
"enable": False,
"interval": 86400
}
if (ref["enable"]):
scheduler.append("migu_refresh_login_pooled_" + u["by"], do_account_refresh, ref["interval"], {"user_info": u})
else:
u = config.read_config("module.mg.user")
ref = config.read_config("module.mg.user.refresh_login")
if (ref["enable"]):
scheduler.append("migu_refresh_login", do_account_refresh, ref["interval"], {"user_info": u})

View File

@ -1,104 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: QMWSign.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common.utils import createMD5
import re as _re
def v(b):
res = []
p = [21, 4, 9, 26, 16, 20, 27, 30]
for x in p:
res.append(b[x])
return ''.join(res)
def c(b):
res = []
p = [18, 11, 3, 2, 1, 7, 6, 25]
for x in p:
res.append(b[x])
return ''.join(res)
def y(a, b, c):
e = []
r25 = a >> 2
if b is not None and c is not None:
r26 = a & 3
r26_2 = r26 << 4
r26_3 = b >> 4
r26_4 = r26_2 | r26_3
r27 = b & 15
r27_2 = r27 << 2
r27_3 = r27_2 | (c >> 6)
r28 = c & 63
e.append(r25)
e.append(r26_4)
e.append(r27_3)
e.append(r28)
else:
r10 = a >> 2
r11 = a & 3
r11_2 = r11 << 4
e.append(r10)
e.append(r11_2)
return e
def n(ls):
e = []
for i in range(0, len(ls), 3):
if i < len(ls) - 2:
e += y(ls[i], ls[i + 1], ls[i + 2])
else:
e += y(ls[i], None, None)
res = []
b64all = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
for i in e:
res.append(b64all[i])
return ''.join(res)
def t(b):
zd = {
"0": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"A": 10,
"B": 11,
"C": 12,
"D": 13,
"E": 14,
"F": 15
}
ol = [212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6]
res = []
j = 0
for i in range(0, len(b), 2):
one = zd[b[i]]
two = zd[b[i + 1]]
r = one * 16 ^ two
res.append(r ^ ol[j])
j += 1
return res
def sign(params):
md5Str = createMD5(params).upper()
h = v(md5Str)
e = c(md5Str)
ls = t(md5Str)
m = n(ls)
res = 'zzb' + h + m + e
res = res.lower()
r = _re.compile(r'[\\/+]')
res = _re.sub(r, '', res)
return res

View File

@ -1,84 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: __init__.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from .player import url
from .musicInfo import getMusicInfo as _getInfo
from .utils import formatSinger
from .lyric import getLyric as _getLyric
from .mv import getMvPlayURLandInfo as _getMvInfo
from common import utils
from . import refresh_login
async def info(songid):
req = await _getInfo(songid)
singerList = []
for s in req['track_info']['singer']:
s.pop('uin')
s.pop('title')
singerList.append(s)
file_info = {}
if (req['track_info']['file']['size_128mp3'] != 0):
file_info['128k'] = {
'size': utils.sizeFormat(int(req['track_info']['file']['size_128mp3'])),
}
if (req['track_info']['file']['size_320mp3'] != 0):
file_info['320k'] = {
'size': utils.sizeFormat(int(req['track_info']['file']['size_320mp3'])),
}
if (req['track_info']['file']['size_flac'] != 0):
file_info['flac'] = {
'size': utils.sizeFormat(int(req['track_info']['file']['size_flac'])),
}
if (req['track_info']['file']['size_hires'] != 0):
file_info['flac24bit'] = {
'size': utils.sizeFormat(int(req['track_info']['file']['size_hires'])),
}
if (req['track_info']['file']['size_dolby'] != 0):
file_info['dolby'] = {
'size': utils.sizeFormat(int(req['track_info']['file']['size_dolby'])),
}
if (req['track_info']['file']['size_new'][0] != 0):
file_info['master'] = {
'size': utils.sizeFormat(int(req['track_info']['file']['size_new'][0])),
}
genres = []
# fix: KeyError: 'genre'
if (req.get('info') and req['info'].get('genre') and req['info']['genre'].get('content')):
for g in req['info']['genre']['content']:
genres.append(g['value'])
return {
'name': req['track_info']['title'] + ' ' + req['track_info']['subtitle'].strip(),
'name_ori': req['track_info']['title'],
'name_extra': req['track_info']['subtitle'].strip(),
'singer': formatSinger(req['track_info']['singer']),
'singer_list': singerList,
'format_length': utils.timeLengthFormat(int(req['track_info']['interval'])),
'length': int(req['track_info']['interval']),
'media_mid': req['track_info']['file']['media_mid'],
'file_info': file_info,
'songmid': req['track_info']['mid'],
'album_id': req['track_info']['album']['id'],
'album_mid': req['track_info']['album']['mid'],
'album': req['track_info']['album']['title'] + ' ' + req['track_info']['album']['subtitle'].strip(),
'language': req['info']['lan']['content'][0]['value'],
'cover': f'https://y.qq.com/music/photo_new/T002R800x800M000{req["track_info"]["album"]["pmid"]}.jpg',
'sizable_cover': 'https://y.qq.com/music/photo_new/T002R{size}x{size}M000' + f'{req["track_info"]["album"]["pmid"]}.jpg',
'publish_date': req['track_info']['time_public'],
'mvid': req['track_info']['mv']['vid'],
'genre': genres,
'kmid': req['track_info']['ksong']['mid'],
'kid': req['track_info']['ksong']['id'],
'bpm': req['track_info']['bpm'],
}
async def lyric(songId):
return await _getLyric(songId)
async def mv(vid):
return await _getMvInfo(vid)

View File

@ -1,257 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: lyric.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from .utils import signRequest
from .musicInfo import getMusicInfo
from common.exceptions import FailedException
from common.utils import createBase64Decode
from common import variable
from common import qdes
import re
class ParseTools:
def __init__(self):
self.rxps = {
'info': re.compile(r'^{"/'),
'lineTime': re.compile(r'^\[(\d+),\d+\]'),
'lineTime2': re.compile(r'^\[([\d:.]+)\]'),
'wordTime': re.compile(r'\(\d+,\d+\)'),
'wordTimeAll': re.compile(r'(\(\d+,\d+\))'),
'timeLabelFixRxp': re.compile(r'(?:\.0+|0+)$'),
}
def ms_format(self, time_ms):
if not time_ms:
return ''
ms = time_ms % 1000
time_ms /= 1000
m = str(int(time_ms / 60)).zfill(2)
time_ms %= 60
s = str(int(time_ms)).zfill(2)
return f"[{m}:{s}.{str(ms).zfill(3)}]"
def parse_lyric(self, lrc):
lrc = lrc.strip()
lrc = lrc.replace('\r', '')
if not lrc:
return {'lyric': '', 'lxlyric': ''}
lines = lrc.split('\n')
lxlrc_lines = []
lyric_lines = []
for line in lines:
line = line.strip()
result = self.rxps['lineTime'].match(line)
if not result:
if line.startswith('[offset'):
lxlrc_lines.append(line)
lyric_lines.append(line)
if self.rxps['lineTime2'].search(line):
lyric_lines.append(line)
continue
start_ms_time = int(result.group(1))
start_time_str = self.ms_format(start_ms_time)
if not start_time_str:
continue
words = re.sub(self.rxps['lineTime'], '', line)
lyric_lines.append(f"{start_time_str}{re.sub(self.rxps['wordTimeAll'], '', words)}")
times = re.findall(self.rxps['wordTimeAll'], words)
if not times:
continue
times = [
f"<{max(int(match.group(1)) - start_ms_time, 0)},{match.group(2)}>"
for match in re.finditer(r'\((\d+),(\d+)\)', words)
]
word_arr = re.split(self.rxps['wordTime'], words)
new_words = ''.join([f"{time}{word}" for time, word in zip(times, word_arr)])
lxlrc_lines.append(f"{start_time_str}{new_words}")
return {
'lyric': '\n'.join(lyric_lines),
'lxlyric': '\n'.join(lxlrc_lines),
}
def parse_rlyric(self, lrc):
lrc = lrc.strip()
lrc = lrc.replace('\r', '')
if not lrc:
return {'lyric': '', 'lxlyric': ''}
lines = lrc.split('\n')
lyric_lines = []
for line in lines:
line = line.strip()
result = self.rxps['lineTime'].match(line)
if not result:
continue
start_ms_time = int(result.group(1))
start_time_str = self.ms_format(start_ms_time)
if not start_time_str:
continue
words = re.sub(self.rxps['lineTime'], '', line)
lyric_lines.append(f"{start_time_str}{re.sub(self.rxps['wordTimeAll'], '', words)}")
return '\n'.join(lyric_lines)
def remove_tag(self, string):
return re.sub(r'^[\S\s]*?LyricContent="', '', string).replace('"\/>[\S\s]*?$', '')
def get_intv(self, interval):
if '.' not in interval:
interval += '.0'
arr = re.split(':|\.', interval.ljust(8, '0'))[:3]
m, s, ms = map(int, arr)
return m * 3600000 + s * 1000 + ms
def fix_rlrc_time_tag(self, rlrc, lrc):
rlrc_lines = rlrc.split('\n')
lrc_lines = lrc.split('\n')
new_lrc = []
for line in rlrc_lines:
result = self.rxps['lineTime2'].search(line)
if not result:
continue
words = re.sub(self.rxps['lineTime2'], '', line)
if not words.strip():
continue
t1 = self.get_intv(result.group(1))
while lrc_lines:
lrc_line = lrc_lines.pop(0)
lrc_line_result = self.rxps['lineTime2'].search(lrc_line)
if not lrc_line_result:
continue
t2 = self.get_intv(lrc_line_result.group(1))
if abs(t1 - t2) < 100:
new_lrc.append(re.sub(self.rxps['lineTime2'], lrc_line_result.group(0), line))
break
return '\n'.join(new_lrc)
def fix_tlrc_time_tag(self, tlrc, lrc):
tlrc_lines = tlrc.split('\n')
lrc_lines = lrc.split('\n')
new_lrc = []
time_tag_rxp = r'^\[[\d:.]+\]'
for line in tlrc_lines:
result = re.match(time_tag_rxp, line)
if not result:
continue
words = re.sub(time_tag_rxp, '', line)
if not words.strip():
continue
tag = re.sub(r'\[\d+:\d+\.\d+\]', '', result.group(0))
while lrc_lines:
lrc_line = lrc_lines.pop(0)
lrc_line_result = re.match(time_tag_rxp, lrc_line)
if not lrc_line_result:
continue
if tag in lrc_line_result.group(0):
new_lrc.append(re.sub(time_tag_rxp, lrc_line_result.group(0), line))
break
return '\n'.join(new_lrc)
def parse(self, lrc, tlrc, rlrc):
info = {
'lyric': '',
'tlyric': '',
'rlyric': '',
'lxlyric': '',
}
if lrc:
lyric_info = self.parse_lyric(self.remove_tag(lrc))
info['lyric'] = lyric_info['lyric']
info['lxlyric'] = lyric_info['lxlyric']
if rlrc:
info['rlyric'] = self.fix_rlrc_time_tag(self.parse_rlyric(self.remove_tag(rlrc)), info['lyric'])
if tlrc:
info['tlyric'] = self.fix_tlrc_time_tag(tlrc, info['lyric'])
return info
global_parser = ParseTools()
def parseLyric(l, t = '', r = ''):
return global_parser.parse(l, t, r)
async def getLyric(songId):
# mid and Numberid
if (re.match("^[0-9]+$", str(songId))):
songId = int(songId)
else:
try:
getNumberIDRequest = await getMusicInfo(songId)
except:
raise FailedException('歌曲信息获取失败')
songId = getNumberIDRequest['track_info']['id']
req = await signRequest({
"comm": {
"ct": '19',
"cv": '1859',
"uin": '0',
},
"req": {
"method": 'GetPlayLyricInfo',
"module": 'music.musichallSong.PlayLyricInfo',
"param": {
"format": 'json',
"crypt": 1 if variable.qdes_lib_loaded else 0,
"ct": 19,
"cv": 1873,
"interval": 0,
"lrc_t": 0,
"qrc": 1 if variable.qdes_lib_loaded else 0,
"qrc_t": 0,
"roma": 1 if variable.qdes_lib_loaded else 0,
"roma_t": 0,
"songID": songId,
"trans": 1,
"trans_t": 0,
"type": -1,
}
}
})
body = req.json()
if ((body['code'] != 0) or (body['req']['code'] != 0)):
raise FailedException('歌词获取失败')
if (variable.qdes_lib_loaded):
l = body['req']['data']['lyric']
t = body['req']['data']['trans']
r = body['req']['data']['roma']
if (l.startswith('789C') and len(l) < 200): # unsupported format
raise FailedException('纯音乐短歌词不受支持')
dl = qdes.qdes_decrypt(l)
if (t):
dt = qdes.qdes_decrypt(t)
else:
dt = ''
if (r):
dr = qdes.qdes_decrypt(r)
else:
dr = ''
return global_parser.parse(dl, dt, dr)
else: # 不获取QRC时的歌词不被加密解码base64不进行parse不支持逐字和罗马音歌词数据没有毫秒
l = body['req']['data']['lyric']
t = body['req']['data']['trans']
return {
'lyric': createBase64Decode(l).decode('utf-8'),
'tlyric': createBase64Decode(t).decode('utf-8'),
'rlyric': '',
'lxlyric': '',
}

View File

@ -1,34 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: musicInfo.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common.exceptions import FailedException
from .utils import signRequest
async def getMusicInfo(songid):
infoReqBody = {
"comm": {
"ct": '19',
"cv": '1859',
"uin": '0',
},
"req": {
"module": 'music.pf_song_detail_svr',
"method": 'get_song_detail_yqq',
"param": {
"song_type": 0,
"song_mid": songid,
},
},
}
infoRequest = await signRequest(infoReqBody, True)
infoBody = infoRequest.json()
if (infoBody['code'] != 0 or infoBody['req']['code'] != 0):
raise FailedException("获取音乐信息失败")
return infoBody['req']['data']

View File

@ -1,80 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: mv.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import asyncio
import json
from .utils import signRequest
async def getMvPlayURLandInfo(vid):
info = signRequest({
"comm": {"ct": 24, "cv": 4747474},
"mvinfo": {
"module": "video.VideoDataServer",
"method": "get_video_info_batch",
"param": {
"vidlist": [vid],
"required": [
"vid",
"type",
"sid",
"cover_pic",
"duration",
"singers",
"video_switch",
"msg",
"name",
"desc",
"playcnt",
"pubdate",
"isfav",
"gmid",
"songmid"
],
},
},
}, True)
urlreq = signRequest({
"comm": {
"ct": 24,
"cv": 4747474,
"g_tk": 812935580,
"uin": 0,
"format": "json",
"platform": "yqq"
},
"mvUrl": {
"module": "gosrf.Stream.MvUrlProxy",
"method": "GetMvUrls",
"param": {
"vids": [vid],
"request_typet": 10001,
"addrtype": 3,
"format": 264
}
}
})
res = await asyncio.gather(info, urlreq)
i = res[0]
# output i with formatted json
print(json.dumps(i.json(), indent=2, ensure_ascii = False))
url = res[1]
file_info = {}
urlbody = url.json()
if (urlbody['code'] == 0 and urlbody['mvUrl']['code'] == 0):
for u in url['mvUrl']['data'][vid]['mp4']:
if (u['filetype'] == 0):
if (u['fileSize']):
pass
pass
pass
pass
pass

View File

@ -1,58 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: player.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common.exceptions import FailedException
from common import config, utils, variable
from .musicInfo import getMusicInfo
from .utils import tools
from .utils import signRequest
import random
createObject = utils.CreateObject
async def url(songId, quality):
infoBody = await getMusicInfo(songId)
strMediaMid = infoBody['track_info']['file']['media_mid']
user_info = config.read_config('module.tx.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.tx'))
requestBody = {
'req_0': {
'module': 'vkey.GetVkeyServer',
'method': 'CgiGetVkey',
'param': {
'filename': [f"{tools.fileInfo[quality]['h']}{strMediaMid}{tools.fileInfo[quality]['e']}"],
'guid': config.read_config('module.tx.vkeyserver.guid'),
'songmid': [songId],
'songtype': [0],
'uin': str(user_info['uin']),
'loginflag': 1,
'platform': '20',
},
},
'comm': {
"qq": str(user_info['uin']),
"authst": user_info['qqmusic_key'],
"ct": "26",
"cv": "2010101",
"v": "2010101"
},
}
req = await signRequest(requestBody)
body = createObject(req.json())
data = body.req_0.data.midurlinfo[0]
url = data['purl']
if (not url):
raise FailedException('failed')
resultQuality = data['filename'].split('.')[0][:4]
return {
'url': tools.cdnaddr + url,
'quality': tools.qualityMapReverse[resultQuality]
}

View File

@ -1,211 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: refresh_login.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common import Httpx, variable
from common import scheduler
from common import config
from common import log
from .utils import sign
import ujson as json
logger = log.log('qqmusic_refresh_login')
async def refresh():
if (not config.read_config('module.tx.user.qqmusic_key')):
return
if (not config.read_config('module.tx.user.refresh_login.enable')):
return
if (config.read_config('module.tx.user.qqmusic_key').startswith('W_X')):
options = {
'method': 'POST',
'body': json.dumps({
"comm": {
"fPersonality": "0",
"tmeLoginType": "1",
"tmeLoginMethod": "1",
"qq": "",
"authst": "",
"ct": "11",
"cv": "12080008",
"v": "12080008",
"tmeAppID": "qqmusic"
},
"req1": {
"module": "music.login.LoginServer",
"method": "Login",
"param": {
"code": "",
"openid": "",
"refresh_token": "",
"str_musicid": str(config.read_config('module.tx.user.uin')),
"musickey": config.read_config('module.tx.user.qqmusic_key'),
"unionid": "",
"refresh_key": "",
"loginMode": 2
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
logger.warning('刷新登录失败, code: ' +
str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info('刷新登录成功')
config.write_config('module.tx.user.uin',
str(body['req1']['data']['musicid']))
logger.info('已通过相应数据更新uin')
config.write_config('module.tx.user.qqmusic_key',
body['req1']['data']['musickey'])
logger.info('已通过相应数据更新qqmusic_key')
elif (config.read_config('module.tx.user.qqmusic_key').startswith('Q_H_L')):
options = {
'method': 'POST',
'body': json.dumps({
'req1': {
'module': 'QQConnectLogin.LoginServer',
'method': 'QQLogin',
'param': {
'expired_in': 7776000,
'musicid': int(config.read_config('module.tx.user.uin')),
'musickey': config.read_config('module.tx.user.qqmusic_key')
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u6.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
logger.warning('刷新登录失败, code: ' +
str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info('刷新登录成功')
config.write_config('module.tx.user.uin',
str(body['req1']['data']['musicid']))
logger.info('已通过相应数据更新uin')
config.write_config('module.tx.user.qqmusic_key',
body['req1']['data']['musickey'])
logger.info('已通过相应数据更新qqmusic_key')
else:
logger.error('未知的qqmusic_key格式')
if (not variable.use_cookie_pool):
# changed refresh login config path
txconfig = config.read_config('module.tx')
refresh_login_info = txconfig.get('refresh_login')
if (refresh_login_info):
txconfig['user']['refresh_login'] = refresh_login_info
txconfig.pop('refresh_login')
config.write_config('module.tx', txconfig)
if (config.read_config('module.tx.user.refresh_login.enable') and not variable.use_cookie_pool):
scheduler.append('qqmusic_refresh_login', refresh,
config.read_config('module.tx.user.refresh_login.interval'))
async def refresh_login_for_pool(user_info):
if (user_info['qqmusic_key'].startswith('W_X')):
options = {
'method': 'POST',
'body': json.dumps({
"comm": {
"fPersonality": "0",
"tmeLoginType": "1",
"tmeLoginMethod": "1",
"qq": "",
"authst": "",
"ct": "11",
"cv": "12080008",
"v": "12080008",
"tmeAppID": "qqmusic"
},
"req1": {
"module": "music.login.LoginServer",
"method": "Login",
"param": {
"code": "",
"openid": "",
"refresh_token": "",
"str_musicid": str(user_info['uin']),
"musickey": user_info['qqmusic_key'],
"unionid": "",
"refresh_key": "",
"loginMode": 2
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
logger.warning(f'为QQ音乐账号({user_info["uin"]})刷新登录失败, code: ' +
str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info(f'为QQ音乐账号(WeChat_{user_info["uin"]})刷新登录成功')
user_list = config.read_config('module.cookiepool.tx')
user_list[user_list.index(
user_info)]['qqmusic_key'] = body['req1']['data']['musickey']
user_list[user_list.index(
user_info)]['uin'] = str(body['req1']['data']['musicid'])
config.write_config('module.cookiepool.tx', user_list)
logger.info(f'为QQ音乐账号(WeChat_{user_info["uin"]})数据更新完毕')
return
elif (user_info['qqmusic_key'].startswith('Q_H_L')):
options = {
'method': 'POST',
'body': json.dumps({
'req1': {
'module': 'QQConnectLogin.LoginServer',
'method': 'QQLogin',
'param': {
'expired_in': 7776000,
'musicid': int(user_info['uin']),
'musickey': user_info['qqmusic_key']
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u6.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
logger.warning(
f'为QQ音乐账号({user_info["uin"]})刷新登录失败, code: ' + str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info(f'为QQ音乐账号(QQ_{user_info["uin"]})刷新登录成功')
user_list = config.read_config('module.cookiepool.tx')
user_list[user_list.index(
user_info)]['qqmusic_key'] = body['req1']['data']['musickey']
user_list[user_list.index(
user_info)]['uin'] = str(body['req1']['data']['musicid'])
config.write_config('module.cookiepool.tx', user_list)
logger.info(f'为QQ音乐账号(QQ_{user_info["uin"]})数据更新完毕')
return
else:
logger.warning(f'为QQ音乐账号({user_info["uin"]})刷新登录失败: 未知或不支持的key类型')
return
def reg_refresh_login_pool_task():
user_info_pool = config.read_config('module.cookiepool.tx')
for user_info in user_info_pool:
if (user_info['refresh_login'].get('enable')):
scheduler.append(
f'qqmusic_refresh_login_pooled_{user_info["uin"]}', refresh_login_for_pool, user_info['refresh_login']['interval'], args = {'user_info': user_info})
if (variable.use_cookie_pool):
reg_refresh_login_pool_task()

View File

@ -1,71 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: utils.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common import Httpx
from common import utils
from common import config
from .QMWSign import sign
import ujson as json
createObject = utils.CreateObject
tools = createObject({
"fileInfo": {
"128k": {
'e': '.mp3',
'h': 'M500',
},
'320k': {
"e": '.mp3',
'h': 'M800',
},
'flac': {
"e": '.flac',
'h': 'F000',
},
'flac24bit': {
"e": '.flac',
'h': 'RS01',
},
"dolby": {
"e": ".flac",
"h": "Q000",
},
"master": {
"e": ".flac",
"h": "AI00",
}
},
'qualityMapReverse': {
'M500': '128k',
'M800': '320k',
'F000': 'flac',
'RS01': 'flac24bit',
'Q000': 'dolby',
'AI00': 'master'
},
"cdnaddr": config.read_config("module.tx.cdnaddr") if config.read_config("module.tx.cdnaddr") else 'http://ws.stream.qqmusic.qq.com/',
})
async def signRequest(data, cache = False):
data = json.dumps(data)
s = sign(data)
headers = {}
return await Httpx.AsyncRequest('https://u.y.qq.com/cgi-bin/musics.fcg?format=json&sign=' + s, {
'method': 'POST',
'body': data,
'headers': headers,
"cache": (86400 * 30) if cache else "no-cache"
})
def formatSinger(singerList):
n = []
for s in singerList:
n.append(s['name'])
return ''.join(n)

View File

@ -1,75 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: __init__.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
import random
from common import Httpx, variable
from common import config
from common.exceptions import FailedException
from .encrypt import eapiEncrypt
import ujson as json
tools = {
'qualityMap': {
'128k': 'standard',
"192k": "higher",
'320k': 'exhigh',
'flac': 'lossless',
'flac24bit': 'hires',
"dolby": "jyeffect",
"sky": "sky",
"master": "jymaster",
"standard": "standard",
"higher": "higher",
"exhigh": "exhigh",
"lossless": "lossless",
"hires": "hires",
"jyeffect": "jyeffect",
"jymaster": "jymaster",
},
'qualityMapReverse': {
'standard': '128k',
"higher": "192k",
'exhigh': '320k',
'lossless': 'flac',
'hires': 'flac24bit',
"jyeffect": "dolby",
"sky": "sky",
"jymaster": "master",
},
}
async def url(songId, quality):
path = '/api/song/enhance/player/url/v1'
requestUrl = 'https://interface.music.163.com/eapi/song/enhance/player/url/v1'
requestBody = {
"ids": json.dumps([songId]),
"level": tools["qualityMap"][quality],
"encodeType": "flac",
}
if (quality == "sky"):
requestBody["immerseType"] = "c51"
req = await Httpx.AsyncRequest(requestUrl, {
'method': 'POST',
'headers': {
'Cookie': config.read_config('module.wy.user.cookie') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.wy'))['cookie'],
},
'form': eapiEncrypt(path, json.dumps(requestBody))
})
body = req.json()
if (not body.get("data") or (not body.get("data")) or (not body.get("data")[0].get("url"))):
raise FailedException("failed")
data = body["data"][0]
if (data['level'] != tools['qualityMap'][quality]):
raise FailedException("reject unmatched quality")
return {
'url': data["url"].split("?")[0],
'quality': tools['qualityMapReverse'][data['level']]
}

View File

@ -1,76 +0,0 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: eapi.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from ujson import dumps
from os import urandom
from base64 import b64encode
from binascii import hexlify
from Crypto.Cipher import AES
from common import utils
__all__ = ["weEncrypt", "linuxEncrypt", "eEncrypt"]
MODULUS = (
"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7"
"b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280"
"104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932"
"575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b"
"3ece0462db0a22b8e7"
)
PUBKEY = "010001"
NONCE = b"0CoJUm6Qyw8W8jud"
LINUXKEY = b"rFgB&h#%2?^eDg:Q"
EAPIKEY = b'e82ckenh8dichen8'
def weEncrypt(text):
"""
引用自 https://github.com/darknessomi/musicbox/blob/master/NEMbox/encrypt.py#L40
"""
data = dumps(text).encode("utf-8")
secret = create_key(16)
method = {"iv": True, "base64": True}
params = aes(aes(data, NONCE, method), secret, method)
encseckey = rsa(secret, PUBKEY, MODULUS)
return {"params": params, "encSecKey": encseckey}
def linuxEncrypt(text):
"""
参考自 https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js#L28
"""
text = str(text).encode()
data = aes(text, LINUXKEY)
return {"eparams": data.decode()}
def eapiEncrypt(url, text):
text = str(text)
digest = utils.createMD5("nobody{}use{}md5forencrypt".format(url, text))
data = "{}-36cd479b6b5-{}-36cd479b6b5-{}".format(url, text, digest)
return {"params": aes(data.encode(), EAPIKEY).decode("utf-8")}
def aes(text, key, method={}):
pad = 16 - len(text) % 16
text = text + bytearray([pad] * pad)
if "iv" in method:
encryptor = AES.new(key, AES.MODE_CBC, b"0102030405060708")
else:
encryptor = AES.new(key, AES.MODE_ECB)
ciphertext = encryptor.encrypt(text)
if "base64" in method:
return b64encode(ciphertext)
return hexlify(ciphertext).upper()
def rsa(text, pubkey, modulus):
text = text[::-1]
rs = pow(int(hexlify(text), 16),
int(pubkey, 16), int(modulus, 16))
return format(rs, "x").zfill(256)
def create_key(size):
return hexlify(urandom(size))[:16]

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "lx-music-api-server-docs",
"version": "2.0.0",
"description": "lx-music-api-server项目文档.",
"license": "MIT",
"type": "module",
"scripts": {
"docs:build": "vuepress-vite build src",
"docs:clean-dev": "vuepress-vite dev src --clean-cache",
"docs:dev": "vuepress-vite dev src",
"docs:update-package": "pnpm dlx vp-update"
},
"devDependencies": {
"@vuepress/bundler-vite": "2.0.0-rc.9",
"vue": "^3.4.21",
"vuepress": "2.0.0-rc.9",
"vuepress-theme-hope": "2.0.0-rc.34"
}
}

2884
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

859
poetry.lock generated
View File

@ -1,859 +0,0 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "aiohttp"
version = "3.9.3"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"},
{file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"},
{file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"},
{file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"},
{file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"},
{file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"},
{file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"},
{file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"},
{file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"},
{file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"},
{file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"},
{file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"},
{file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"},
{file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"},
{file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"},
{file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"},
{file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"},
{file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"},
{file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"},
{file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"},
{file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"},
{file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"},
{file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"},
{file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"},
{file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"},
{file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"},
{file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"},
{file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"},
{file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"},
{file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"},
{file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"},
{file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"},
{file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"},
{file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"},
{file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"},
{file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"},
{file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"},
{file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"},
{file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"},
{file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"},
{file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"},
{file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"},
{file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"},
{file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"},
{file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"},
{file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"},
{file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"},
{file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"},
{file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"},
{file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"},
{file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"},
{file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"},
{file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"},
{file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"},
{file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"},
{file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"},
{file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"},
{file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"},
{file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"},
{file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"},
{file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"},
{file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"},
{file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"},
{file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"},
{file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"},
{file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"},
{file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"},
{file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"},
{file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"},
{file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"},
{file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"},
{file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"},
{file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"},
{file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"},
{file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"},
{file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"},
]
[package.dependencies]
aiosignal = ">=1.1.2"
async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""}
attrs = ">=17.3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["Brotli", "aiodns", "brotlicffi"]
[[package]]
name = "aiosignal"
version = "1.3.1"
description = "aiosignal: a list of registered asynchronous callbacks"
optional = false
python-versions = ">=3.7"
files = [
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
]
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "async-timeout"
version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
[[package]]
name = "attrs"
version = "23.2.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
files = [
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
]
[package.extras]
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
dev = ["attrs[tests]", "pre-commit"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
[[package]]
name = "certifi"
version = "2024.2.2"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
]
[[package]]
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "colorlog"
version = "6.8.2"
description = "Add colours to the output of Python's logging module."
optional = false
python-versions = ">=3.6"
files = [
{file = "colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33"},
{file = "colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
development = ["black", "flake8", "mypy", "pytest", "types-colorama"]
[[package]]
name = "frozenlist"
version = "1.4.1"
description = "A list-like structure which implements collections.abc.MutableSequence"
optional = false
python-versions = ">=3.8"
files = [
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"},
{file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"},
{file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"},
{file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"},
{file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"},
{file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"},
{file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"},
{file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"},
{file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"},
{file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"},
{file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"},
{file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"},
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
]
[[package]]
name = "idna"
version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
]
[[package]]
name = "multidict"
version = "6.0.5"
description = "multidict implementation"
optional = false
python-versions = ">=3.7"
files = [
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
{file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"},
{file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"},
{file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"},
{file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"},
{file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"},
{file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"},
{file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"},
{file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"},
{file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"},
{file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"},
{file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"},
{file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"},
{file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"},
{file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"},
{file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"},
{file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"},
{file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"},
{file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"},
{file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"},
{file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"},
]
[[package]]
name = "mutagen"
version = "1.47.0"
description = "read and write audio tags for many formats"
optional = false
python-versions = ">=3.7"
files = [
{file = "mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"},
{file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"},
]
[[package]]
name = "pillow"
version = "10.3.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.8"
files = [
{file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
{file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
{file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
{file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
{file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
{file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
{file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
{file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
{file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
{file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
{file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
{file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
{file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
{file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
{file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
{file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
{file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
{file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
{file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
{file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
{file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
{file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
{file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
{file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
{file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
typing = ["typing-extensions"]
xmp = ["defusedxml"]
[[package]]
name = "pycryptodome"
version = "3.20.0"
description = "Cryptographic library for Python"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"},
{file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"},
{file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"},
{file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"},
{file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"},
{file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"},
{file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"},
{file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"},
{file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"},
{file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"},
{file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"},
{file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"},
{file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"},
{file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"},
{file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"},
{file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"},
{file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"},
{file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"},
{file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"},
{file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"},
{file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"},
{file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"},
{file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"},
{file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"},
{file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"},
{file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"},
{file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"},
{file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"},
{file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"},
{file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"},
{file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"},
{file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"},
]
[[package]]
name = "pygments"
version = "2.17.2"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.7"
files = [
{file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
{file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
]
[package.extras]
plugins = ["importlib-metadata"]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.7"
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "ujson"
version = "5.9.0"
description = "Ultra fast JSON encoder and decoder for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "ujson-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab71bf27b002eaf7d047c54a68e60230fbd5cd9da60de7ca0aa87d0bccead8fa"},
{file = "ujson-5.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a365eac66f5aa7a7fdf57e5066ada6226700884fc7dce2ba5483538bc16c8c5"},
{file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e015122b337858dba5a3dc3533af2a8fc0410ee9e2374092f6a5b88b182e9fcc"},
{file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779a2a88c53039bebfbccca934430dabb5c62cc179e09a9c27a322023f363e0d"},
{file = "ujson-5.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10ca3c41e80509fd9805f7c149068fa8dbee18872bbdc03d7cca928926a358d5"},
{file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a566e465cb2fcfdf040c2447b7dd9718799d0d90134b37a20dff1e27c0e9096"},
{file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f833c529e922577226a05bc25b6a8b3eb6c4fb155b72dd88d33de99d53113124"},
{file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b68a0caab33f359b4cbbc10065c88e3758c9f73a11a65a91f024b2e7a1257106"},
{file = "ujson-5.9.0-cp310-cp310-win32.whl", hash = "sha256:7cc7e605d2aa6ae6b7321c3ae250d2e050f06082e71ab1a4200b4ae64d25863c"},
{file = "ujson-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6d3f10eb8ccba4316a6b5465b705ed70a06011c6f82418b59278fbc919bef6f"},
{file = "ujson-5.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b23bbb46334ce51ddb5dded60c662fbf7bb74a37b8f87221c5b0fec1ec6454b"},
{file = "ujson-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6974b3a7c17bbf829e6c3bfdc5823c67922e44ff169851a755eab79a3dd31ec0"},
{file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5964ea916edfe24af1f4cc68488448fbb1ec27a3ddcddc2b236da575c12c8ae"},
{file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ba7cac47dd65ff88571eceeff48bf30ed5eb9c67b34b88cb22869b7aa19600d"},
{file = "ujson-5.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bbd91a151a8f3358c29355a491e915eb203f607267a25e6ab10531b3b157c5e"},
{file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:829a69d451a49c0de14a9fecb2a2d544a9b2c884c2b542adb243b683a6f15908"},
{file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a807ae73c46ad5db161a7e883eec0fbe1bebc6a54890152ccc63072c4884823b"},
{file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8fc2aa18b13d97b3c8ccecdf1a3c405f411a6e96adeee94233058c44ff92617d"},
{file = "ujson-5.9.0-cp311-cp311-win32.whl", hash = "sha256:70e06849dfeb2548be48fdd3ceb53300640bc8100c379d6e19d78045e9c26120"},
{file = "ujson-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7309d063cd392811acc49b5016728a5e1b46ab9907d321ebbe1c2156bc3c0b99"},
{file = "ujson-5.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:20509a8c9f775b3a511e308bbe0b72897ba6b800767a7c90c5cca59d20d7c42c"},
{file = "ujson-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b28407cfe315bd1b34f1ebe65d3bd735d6b36d409b334100be8cdffae2177b2f"},
{file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d302bd17989b6bd90d49bade66943c78f9e3670407dbc53ebcf61271cadc399"},
{file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f21315f51e0db8ee245e33a649dd2d9dce0594522de6f278d62f15f998e050e"},
{file = "ujson-5.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5635b78b636a54a86fdbf6f027e461aa6c6b948363bdf8d4fbb56a42b7388320"},
{file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82b5a56609f1235d72835ee109163c7041b30920d70fe7dac9176c64df87c164"},
{file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5ca35f484622fd208f55041b042d9d94f3b2c9c5add4e9af5ee9946d2d30db01"},
{file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:829b824953ebad76d46e4ae709e940bb229e8999e40881338b3cc94c771b876c"},
{file = "ujson-5.9.0-cp312-cp312-win32.whl", hash = "sha256:25fa46e4ff0a2deecbcf7100af3a5d70090b461906f2299506485ff31d9ec437"},
{file = "ujson-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:60718f1720a61560618eff3b56fd517d107518d3c0160ca7a5a66ac949c6cf1c"},
{file = "ujson-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d581db9db9e41d8ea0b2705c90518ba623cbdc74f8d644d7eb0d107be0d85d9c"},
{file = "ujson-5.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ff741a5b4be2d08fceaab681c9d4bc89abf3c9db600ab435e20b9b6d4dfef12e"},
{file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdcb02cabcb1e44381221840a7af04433c1dc3297af76fde924a50c3054c708c"},
{file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e208d3bf02c6963e6ef7324dadf1d73239fb7008491fdf523208f60be6437402"},
{file = "ujson-5.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4b3917296630a075e04d3d07601ce2a176479c23af838b6cf90a2d6b39b0d95"},
{file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0c4d6adb2c7bb9eb7c71ad6f6f612e13b264942e841f8cc3314a21a289a76c4e"},
{file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0b159efece9ab5c01f70b9d10bbb77241ce111a45bc8d21a44c219a2aec8ddfd"},
{file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0cb4a7814940ddd6619bdce6be637a4b37a8c4760de9373bac54bb7b229698b"},
{file = "ujson-5.9.0-cp38-cp38-win32.whl", hash = "sha256:dc80f0f5abf33bd7099f7ac94ab1206730a3c0a2d17549911ed2cb6b7aa36d2d"},
{file = "ujson-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:506a45e5fcbb2d46f1a51fead991c39529fc3737c0f5d47c9b4a1d762578fc30"},
{file = "ujson-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0fd2eba664a22447102062814bd13e63c6130540222c0aa620701dd01f4be81"},
{file = "ujson-5.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bdf7fc21a03bafe4ba208dafa84ae38e04e5d36c0e1c746726edf5392e9f9f36"},
{file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f909bc08ce01f122fd9c24bc6f9876aa087188dfaf3c4116fe6e4daf7e194f"},
{file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd4ea86c2afd41429751d22a3ccd03311c067bd6aeee2d054f83f97e41e11d8f"},
{file = "ujson-5.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:63fb2e6599d96fdffdb553af0ed3f76b85fda63281063f1cb5b1141a6fcd0617"},
{file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:32bba5870c8fa2a97f4a68f6401038d3f1922e66c34280d710af00b14a3ca562"},
{file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:37ef92e42535a81bf72179d0e252c9af42a4ed966dc6be6967ebfb929a87bc60"},
{file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f69f16b8f1c69da00e38dc5f2d08a86b0e781d0ad3e4cc6a13ea033a439c4844"},
{file = "ujson-5.9.0-cp39-cp39-win32.whl", hash = "sha256:3382a3ce0ccc0558b1c1668950008cece9bf463ebb17463ebf6a8bfc060dae34"},
{file = "ujson-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:6adef377ed583477cf005b58c3025051b5faa6b8cc25876e594afbb772578f21"},
{file = "ujson-5.9.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ffdfebd819f492e48e4f31c97cb593b9c1a8251933d8f8972e81697f00326ff1"},
{file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4eec2ddc046360d087cf35659c7ba0cbd101f32035e19047013162274e71fcf"},
{file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbb90aa5c23cb3d4b803c12aa220d26778c31b6e4b7a13a1f49971f6c7d088e"},
{file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0823cb70866f0d6a4ad48d998dd338dce7314598721bc1b7986d054d782dfd"},
{file = "ujson-5.9.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4e35d7885ed612feb6b3dd1b7de28e89baaba4011ecdf995e88be9ac614765e9"},
{file = "ujson-5.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b048aa93eace8571eedbd67b3766623e7f0acbf08ee291bef7d8106210432427"},
{file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:323279e68c195110ef85cbe5edce885219e3d4a48705448720ad925d88c9f851"},
{file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ac92d86ff34296f881e12aa955f7014d276895e0e4e868ba7fddebbde38e378"},
{file = "ujson-5.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6eecbd09b316cea1fd929b1e25f70382917542ab11b692cb46ec9b0a26c7427f"},
{file = "ujson-5.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:473fb8dff1d58f49912323d7cb0859df5585cfc932e4b9c053bf8cf7f2d7c5c4"},
{file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f91719c6abafe429c1a144cfe27883eace9fb1c09a9c5ef1bcb3ae80a3076a4e"},
{file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b1c0991c4fe256f5fdb19758f7eac7f47caac29a6c57d0de16a19048eb86bad"},
{file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a8ea0f55a1396708e564595aaa6696c0d8af532340f477162ff6927ecc46e21"},
{file = "ujson-5.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:07e0cfdde5fd91f54cd2d7ffb3482c8ff1bf558abf32a8b953a5d169575ae1cd"},
{file = "ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532"},
]
[[package]]
name = "urllib3"
version = "2.2.1"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
{file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "xmltodict"
version = "0.13.0"
description = "Makes working with XML feel like you are working with JSON"
optional = false
python-versions = ">=3.4"
files = [
{file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"},
{file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"},
]
[[package]]
name = "yarl"
version = "1.9.4"
description = "Yet another URL library"
optional = false
python-versions = ">=3.7"
files = [
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
{file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"},
{file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"},
{file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"},
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"},
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"},
{file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"},
{file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"},
{file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"},
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"},
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"},
{file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"},
{file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"},
{file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"},
{file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"},
{file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"},
{file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"},
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"},
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"},
{file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"},
{file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"},
{file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"},
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"},
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"},
{file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"},
{file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"},
{file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"},
{file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"},
{file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"},
]
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "c4e2fb5dbe70bc197d1e1195a93fa6e0a8193af1dae5e0872ff542667ff263c0"

View File

@ -1,24 +0,0 @@
[tool.poetry]
name = "lx-music-api-server-python"
version = "2.0.0.beta-10"
description = "一个适配 LX Music 的 API 后端实现"
authors = ["helloplhm-qwq", "lerdb", "Folltoshe"]
license = "MIT"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.8"
aiohttp = "^3.9.3"
pycryptodome = "^3.20.0"
ujson = "^5.9.0"
requests = "^2.31.0"
colorlog = "^6.8.2"
Pygments = "^2.17.2"
xmltodict = "^0.13.0"
pillow = "^10.3.0"
mutagen = "^1.47.0"
colorama = "^0.4.6"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@ -1,11 +0,0 @@
aiohttp
pycryptodome
ujson
requests
colorlog
pygments
xmltodict
toml
mutagen
pillow
colorama

15
src/.vuepress/config.ts Normal file
View File

@ -0,0 +1,15 @@
import { defineUserConfig } from "vuepress";
import theme from "./theme.js";
export default defineUserConfig({
base: "/",
lang: "zh-CN",
title: "LX Music Api Server",
description: "LX Music Api Server的文档",
theme,
// 和 PWA 一起启用
// shouldPrefetch: false,
});

1
src/.vuepress/navbar.ts Normal file
View File

@ -0,0 +1 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path fill="#FDD7AD" d="M512 0 335.448 88.272l-70.616 35.312-70.624 35.312-176.552 88.28v529.648L512 1024l494.344-247.176V247.176z"/><path fill="#CBB292" d="m759.176 370.76-70.624 35.304-494.344-247.168 70.624-35.312zM512 494.344V1024L17.656 776.824V247.176z"/><path fill="#7F6E5D" d="M1006.344 247.168v529.656L512 1024V494.344l176.552-88.28v70.624l141.24-70.624v-70.616z"/><path fill="#7F5B53" d="M829.792 335.448v70.624L688.56 476.68v-70.624z"/><path fill="#CBB292" d="m829.792 335.448-70.624 35.312-494.344-247.176 70.624-35.312z"/><path fill="#2C3E50" d="m682.52 550.32 157.032-78.512a17.656 17.656 0 0 1 25.552 15.792v9.32a52.96 52.96 0 0 1-29.28 47.376L678.8 622.8a17.656 17.656 0 0 1-25.552-15.792v-9.312a52.96 52.96 0 0 1 29.28-47.376z"/></svg>

After

Width:  |  Height:  |  Size: 854 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024"><defs><linearGradient id="a" x1="522.593" x2="522.593" y1="-70.302" y2="-335.937" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#fe5d5a" stop-opacity=".1"/><stop offset=".908" stop-color="#ef1220" stop-opacity=".5"/></linearGradient><linearGradient id="b" x1="107.12" x2="935.038" y1="-373.67" y2="-373.67" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ff5e59"/><stop offset="1" stop-color="#f01422"/></linearGradient><linearGradient id="c" x1="519.405" x2="519.405" y1="-195.547" y2="-726.816" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ffe2e2"/><stop offset=".888" stop-color="#ff8e8e"/></linearGradient><linearGradient id="d" x1="191.5" x2="483.9" y1="-564.9" y2="-564.9" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#e92700" stop-opacity=".3"/><stop offset=".013" stop-color="#ef1220" stop-opacity=".2"/></linearGradient><linearGradient id="e" x1="403.502" x2="253.121" y1="-847.32" y2="-586.853" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ff5e59"/><stop offset=".201" stop-color="#f01422"/></linearGradient><linearGradient id="f" x1="330.485" x2="330.485" y1="-801.787" y2="-625.789" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ff5e59"/><stop offset=".201" stop-color="#f01422"/></linearGradient><linearGradient id="g" x1="397.351" x2="256.845" y1="-647.231" y2="-890.596" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ffa6a6"/><stop offset=".908" stop-color="#ff6b5d"/></linearGradient></defs><path fill="url(#a)" d="M501.2 662.3 327.6 763.8c-13.9 8.1-14.2 28.1-.5 36.7l179.1 97.7c10.9 5.9 24.1 5.9 34.9-.1l177-97.9c13.6-8.5 13.4-28.3-.3-36.5l-168.4-101c-14.8-9-33.3-9.1-48.2-.4Z"/><path fill="#f63037" d="m110.2 525.7-3.1 77.6 57.5 18.5L184 519.4Z"/><path fill="url(#b)" d="m476.6 363.5-328 154.6c-21 42.7-55.4 65.4-35.5 103.5 4.2 8 9.4 14.4 15.4 18.1l358.2 195.5c21.8 11.9 48.1 11.8 69.8-.2l354-195.8c27.2-16.9 34.8-90.3 7.3-106.8L573 364.1c-29.7-17.8-66.6-18-96.4-.6Z"/><path fill="url(#c)" d="M476.6 298.7 129.4 501.6c-27.8 16.3-28.4 56.3-1 73.3l358.2 195.5c21.8 11.9 48.1 11.8 69.8-.2l354-195.8c27.2-16.9 26.9-56.6-.6-73.1L573 299.3c-29.7-17.8-66.6-18-96.4-.6Z"/><path fill="#ff8989" fill-opacity=".31" d="m481.2 387.8 39.4 123.4c1.1 3.4 4 6 7.6 6.6l173.4 30.4-33-118.3c-.9-3.3-3.6-5.8-7-6.5l-180.4-35.6ZM327 499.2l40.4 101.1L496.7 525c2.5-1.5 3.7-4.5 2.7-7.3l-36-106.8-127.6 65c-8.6 4.3-12.4 14.4-8.8 23.3ZM523.8 540.5l-140.3 77.2L567.2 659c3.2.7 6.6.1 9.3-1.6l134.6-85-174.7-33.8c-4.3-1-8.7-.3-12.6 1.9Z"/><path fill="url(#d)" d="M483.9 406.1c0 35.46-65.46 64.2-146.2 64.2s-146.2-28.74-146.2-64.2c0-35.46 65.46-64.2 146.2-64.2s146.2 28.74 146.2 64.2Z"/><path fill="url(#e)" d="m254.2 188.4-123 83.1c-1.8 1.3-2.6 3.6-1.8 5.7l39.1 110.6c.6 1.7 2 2.9 3.8 3.2l221.8 40.5c1.3.3 2.7-.1 3.7-.8l131.7-93.6c1.9-1.4 2.6-3.9 1.7-6.1l-49.4-107c-.6-1.5-2.1-2.6-3.7-2.8l-220.3-33.5c-1.3-.2-2.6.1-3.6.7Z"/><path fill="url(#f)" d="m528.6 274.5 3 59.1-205 65.6-177.2-72.7-20-49.2 1.9-54.1Z"/><path fill="url(#g)" d="m250.6 138-112.3 76c-6 4.1-8.5 11.7-6.1 18.5l34.2 96.6c1.9 5.4 6.6 9.3 12.1 10.4l211 38.5c4.3.7 8.6-.2 12.1-2.7l120.5-85.5c6.3-4.4 8.4-12.7 5.3-19.7l-43.1-93.5c-2.2-4.9-6.8-8.3-12.1-9.1L262 135.6c-4-.7-8 .2-11.4 2.4Z"/><path fill="#fff" d="m419.8 252.8-79-11-29-57.7c-3.8-7.6-13.2-10.7-20.8-6.9-7.6 3.8-10.7 13.2-6.9 20.8l26.6 52.9-61.8 42.2c-7.1 4.8-8.9 14.5-4.1 21.5 3 4.4 7.9 6.8 12.8 6.8 3 0 6-.9 8.7-2.7l68-46.4 81.1 11.2c.7.1 1.4.1 2.1.1 7.6 0 14.3-5.6 15.3-13.4 1.4-8.4-4.5-16.2-13-17.4Z"/></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1536 1024"><path fill="#1296db" d="M1425.067.256H110.933A110.933 110.933 0 0 0 0 110.848v723.627a110.933 110.933 0 0 0 110.933 110.933h1314.39c61.269 0 110.933-49.75 110.677-110.677V110.848A110.933 110.933 0 0 0 1425.067.256z" class="selected" data-spm-anchor-id="a313x.7781069.0.i4"/><path fill="#FFF" d="M664.747 723.797V435.883L517.12 620.373l-147.456-184.49v288l-148.053-67.158V221.781h147.626l147.627 184.576 147.541-184.576h147.627v565.76z"/><path d="M1024 0h426.667A85.333 85.333 0 0 1 1536 85.333v768a85.333 85.333 0 0 1-85.333 85.334H1024V0z" opacity=".1"/><path fill="#FFF" d="m1256.96 731.307-170.667-216.491h113.75V304.64h113.749v210.176h113.835z" opacity=".5"/></svg>

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" class="icon" viewBox="0 0 3280.944 2800"><path fill="#41b883" d="M1645.332 601.004h375.675L1081.82 2238.478 142.636 601.004h718.477l220.708 379.704 216.013-379.704z"/><path fill="#41b883" d="M142.636 601.004l939.185 1637.474 939.186-1637.474h-375.675l-563.51 982.484-568.208-982.484z"/><path fill="#35495e" d="M513.188 601.004l568.207 987.23 563.511-987.23h-347.498l-216.013 379.704-220.708-379.704zM1607.792 1311.83l594.678 2.293 187.353-316.325-598.662 2.292zM2198.506 1909.57C2867.436 732.7 2939.502 605.426 2937.874 603.78c-.715-.723 45.303-1.314 102.262-1.314s103.562.428 103.562.951c0 .523-208.57 367.978-463.491 816.567L2216.715 2235.6l-102.1.596-102.102.596z"/><path fill="#41b883" d="M1680.563 2233.328c0-1.34 168.208-298.145 440.375-777.048a4135645.775 4135645.775 0 00337.619-594.19l146.13-257.25 170.746-.04 170.747-.04-5.536 9.741c-3.044 5.358-43.727 77.302-90.407 159.875-85.356 150.992-337.562 595.163-656.602 1156.373l-172 302.559-170.536.588c-93.795.322-170.536.069-170.536-.567z"/><path fill="#35495e" d="M1429.783 1625.351l594.679 2.292 187.353-316.324-598.662 2.292z"/><path fill="#41b883" d="M1524.207 1464.903l608.285 6.877 173.746-320.909h-619.072z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

25
src/.vuepress/sidebar.ts Normal file
View File

@ -0,0 +1,25 @@
import { sidebar } from "vuepress-theme-hope";
export default sidebar({
"/": [
"",
{
text: "介绍",
icon: "laptop-code",
prefix: "guide/readme.md",
link: "guide/readme.md",
children: "structure",
},
{
text: "部署",
icon: "book",
prefix: "deploy/",
children: "structure",
},
{
text: "API调用方法",
icon: "person-chalkboard",
link: "guide/api/api.md",
},
],
});

View File

@ -0,0 +1,3 @@
// you can change config here
$colors: #c0392b, #d35400, #f39c12, #27ae60, #16a085, #2980b9, #8e44ad, #2c3e50,
#7f8c8d !default;

View File

@ -0,0 +1 @@
// place your custom styles here

View File

@ -0,0 +1,2 @@
// you can change colors here
$theme-color: #096dd9;

183
src/.vuepress/theme.ts Normal file
View File

@ -0,0 +1,183 @@
import { hopeTheme } from "vuepress-theme-hope";
// import navbar from "./navbar.js";
import sidebar from "./sidebar.js";
export default hopeTheme({
hostname: "https://vuepress-theme-hope-docs-demo.netlify.app",
iconAssets: "fontawesome-with-brands",
logo: "https://img2.imgtp.com/2024/04/08/AaDnm8dY.png",
repo: "vuepress-theme-hope/vuepress-theme-hope",
docsDir: "src",
// 导航栏
// navbar,
// 侧边栏
sidebar,
// 页脚
footer: "Copyright © 2024 All Devloper",
displayFooter: true,
// 加密配置
// encrypt: {
// config: {
// "/demo/encrypt.html": ["1234"],
// },
// },
// 多语言配置
metaLocales: {
editLink: "在 GitHub 上编辑此页",
},
// 如果想要实时查看任何改变,启用它。注: 这对更新性能有很大负面影响
// hotReload: true,
// 在这里配置主题提供的插件
plugins: {
// 注意: 仅用于测试! 你必须自行生成并在生产环境中使用自己的评论服务
comment: {
provider: "Giscus",
repo: "helloplhm-qwq/lx-music-api-server",
repoId: "R_kgDOKr11fg",
category: "Announcements",
categoryId: "DIC_kwDOG_Pt2M4COD69",
},
components: {
components: ["Badge", "VPCard"],
},
// 此处开启了很多功能用于演示,你应仅保留用到的功能。
mdEnhance: {
align: true,
attrs: true,
codetabs: true,
component: true,
demo: true,
figure: true,
imgLazyload: true,
imgSize: true,
include: true,
mark: true,
stylize: [
{
matcher: "Recommended",
replacer: ({ tag }) => {
if (tag === "em")
return {
tag: "Badge",
attrs: { type: "tip" },
content: "Recommended",
};
},
},
],
sub: true,
sup: true,
tabs: true,
tasklist: true,
vPre: true,
// 在启用之前安装 chart.js
// chart: true,
// insert component easily
// 在启用之前安装 echarts
// echarts: true,
// 在启用之前安装 flowchart.ts
// flowchart: true,
// gfm requires mathjax-full to provide tex support
// gfm: true,
// 在启用之前安装 katex
// katex: true,
// 在启用之前安装 mathjax-full
// mathjax: true,
// 在启用之前安装 mermaid
// mermaid: true,
// playground: {
// presets: ["ts", "vue"],
// },
// 在启用之前安装 reveal.js
// revealJs: {
// plugins: ["highlight", "math", "search", "notes", "zoom"],
// },
// 在启用之前安装 @vue/repl
// vuePlayground: true,
// install sandpack-vue3 before enabling it
// sandpack: true,
},
// 如果你需要 PWA。安装 @vuepress/plugin-pwa 并取消下方注释
// pwa: {
// favicon: "/favicon.ico",
// cacheHTML: true,
// cachePic: true,
// appendBase: true,
// apple: {
// icon: "/assets/icon/apple-icon-152.png",
// statusBarColor: "black",
// },
// msTile: {
// image: "/assets/icon/ms-icon-144.png",
// color: "#ffffff",
// },
// manifest: {
// icons: [
// {
// src: "/assets/icon/chrome-mask-512.png",
// sizes: "512x512",
// purpose: "maskable",
// type: "image/png",
// },
// {
// src: "/assets/icon/chrome-mask-192.png",
// sizes: "192x192",
// purpose: "maskable",
// type: "image/png",
// },
// {
// src: "/assets/icon/chrome-512.png",
// sizes: "512x512",
// type: "image/png",
// },
// {
// src: "/assets/icon/chrome-192.png",
// sizes: "192x192",
// type: "image/png",
// },
// ],
// shortcuts: [
// {
// name: "Demo",
// short_name: "Demo",
// url: "/demo/",
// icons: [
// {
// src: "/assets/icon/guide-maskable.png",
// sizes: "192x192",
// purpose: "maskable",
// type: "image/png",
// },
// ],
// },
// ],
// },
// },
},
});

29
src/README.md Normal file
View File

@ -0,0 +1,29 @@
---
title: 首页
home: true
icon: home
heroText: LX Music Api Server
tagline: 适用于 LX Music 的解析接口服务器的 Python 实现
heroImage: https://img2.imgtp.com/2024/04/08/AaDnm8dY.png
actions:
- text: 如何部署?
link: /guide/readme.md
type: primary
icon: arrow-right
- text: API调用
link: /guide/api/api.md
type: secondary
icon: book
features:
- title: 安全性高
details: 拥有一个单独的安全模块,防止被恶意滥用
icon: lock
- title: 部署简单
details: 只需要有一点点基础指的是会看文档即可在10分钟内部署完毕
icon: rocket
- title: 快速响应
details: 采用aiohttp进行异步请求毫秒级响应
icon: magic
footer: 'Copyright © 2024 All Devloper'
footerHtml: true
---

26
src/guide/README.md Normal file
View File

@ -0,0 +1,26 @@
---
head:
- - meta
- name: keywords
content: 即刻开始
title: 介绍
icon: docs
author: ikun0014
date: 2024-04-10
index: true
---
## 部署方法
### Windows
- [点我前往](./deploy-windows.md)
### Linux/Mac OS
- [点我前往](./deploy-linuxordarwin.md)
## 项目官方交流群
- [QQ](https://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=117h8X7TPBWMLwK0Nec_TkdFpqsSs7LJ&group_code=206995059)
- [Telegram](https://t.me/+zBJAaMgr6yZmYWI9)

13
src/guide/api/api.md Normal file
View File

@ -0,0 +1,13 @@
---
head:
- - meta
- name: keywords
content: API调用方法
title: API调用方法
icon: docs
author: ikun0014
date: 2024-04-10
index: false
---
# Test

View File

@ -0,0 +1,13 @@
---
head:
- - meta
- name: keywords
content: Linux or MacOS部署教程
title: Linux or MacOS部署教程
icon: docs
author: ikun0014
date: 2024-04-10
index: false
---
## 111

View File

@ -0,0 +1,16 @@
---
head:
- - meta
- name: keywords
content: Windows部署教程
title: Windows部署教程
icon: docs
author: ikun0014
date: 2024-04-10
index: false
---
## 部署教程
1.下载Python3.10-3.11
前往[Python官网](https://www.python.org/downloads/release/python-3119)下拉即可找到Python3.11.9的下载链接

Some files were not shown because too many files have changed in this diff Show More