docs: 新增文档分支
11
.github/dependabot.yml
vendored
@ -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"
|
66
.github/workflows/build_beta.yml
vendored
@ -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 }}
|
102
.github/workflows/build_release.yml
vendored
@ -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
@ -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
@ -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/
|
||||
|
13
Dockerfile
@ -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
@ -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
@ -1,282 +0,0 @@
|
||||
简体中文 | [English](README_EN.md)
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||

|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/actions/workflows/build_beta.yml)
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases/latest)
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases)
|
||||
[](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 趋势图
|
||||
|
||||
[](https://starchart.cc/lxmusics/lx-music-api-server-python)
|
||||
|
||||
## ⚙️贡献者
|
||||
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/graphs/contributors)
|
276
README_EN.md
@ -1,276 +0,0 @@
|
||||
English | [简体中文](README.md)
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||

|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/actions/workflows/build_beta.yml)
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases/latest)
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases)
|
||||
[](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
|
||||
|
||||
[](https://starchart.cc/lxmusics/lx-music-api-server-python)
|
||||
|
||||
## ⚙️Contributor
|
||||
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/graphs/contributors)
|
234
build.py
@ -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)
|
@ -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:])]
|
322
common/Httpx.py
@ -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
|
797
common/config.py
@ -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": "客户端versioncode,pidversionsecret可能随此值而变化",
|
||||
"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时使用的guid,uin等信息,不需要与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()
|
@ -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
|
@ -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'])
|
||||
}
|
283
common/log.py
@ -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)
|
@ -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)
|
@ -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
|
@ -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
|
@ -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解密库未被加载')
|
@ -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")
|
202
common/utils.py
@ -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)
|
||||
|
@ -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 = ''
|
@ -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:
|
||||
|
@ -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
@ -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())
|
@ -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)
|
@ -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('暂不支持该类型搜索')
|
@ -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()
|
@ -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)
|
@ -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 ''
|
@ -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
|
@ -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
|
||||
}
|
@ -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
|
@ -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,
|
||||
}
|
@ -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()
|
@ -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填写错误或不支持')
|
@ -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()
|
@ -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©rightId=" + 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]}©rightId={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')
|
@ -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})
|
@ -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
|
@ -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)
|
@ -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': '',
|
||||
}
|
@ -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']
|
@ -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
|
||||
|
||||
|
@ -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]
|
||||
}
|
@ -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()
|
@ -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)
|
@ -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']]
|
||||
}
|
@ -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
@ -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
859
poetry.lock
generated
@ -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"
|
@ -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"
|
@ -1,11 +0,0 @@
|
||||
aiohttp
|
||||
pycryptodome
|
||||
ujson
|
||||
requests
|
||||
colorlog
|
||||
pygments
|
||||
xmltodict
|
||||
toml
|
||||
mutagen
|
||||
pillow
|
||||
colorama
|
15
src/.vuepress/config.ts
Normal 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
@ -0,0 +1 @@
|
||||
|
BIN
src/.vuepress/public/assets/icon/apple-icon-152.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/.vuepress/public/assets/icon/chrome-192.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/.vuepress/public/assets/icon/chrome-512.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/.vuepress/public/assets/icon/chrome-mask-192.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/.vuepress/public/assets/icon/chrome-mask-512.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/.vuepress/public/assets/icon/guide-maskable.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/.vuepress/public/assets/icon/ms-icon-144.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
1
src/.vuepress/public/assets/image/advanced.svg
Normal file
After Width: | Height: | Size: 26 KiB |
1
src/.vuepress/public/assets/image/blog.svg
Normal file
After Width: | Height: | Size: 9.8 KiB |
1
src/.vuepress/public/assets/image/box.svg
Normal 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 |
1
src/.vuepress/public/assets/image/features.svg
Normal 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 |
1
src/.vuepress/public/assets/image/github-dark.svg
Normal 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 |
1
src/.vuepress/public/assets/image/github-light.svg
Normal 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 |
1
src/.vuepress/public/assets/image/layout.svg
Normal file
After Width: | Height: | Size: 9.0 KiB |
1
src/.vuepress/public/assets/image/markdown.svg
Normal 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 |
BIN
src/.vuepress/public/favicon.ico
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
src/.vuepress/public/logo.png
Normal file
After Width: | Height: | Size: 92 KiB |
1
src/.vuepress/public/logo.svg
Normal 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
@ -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",
|
||||
},
|
||||
],
|
||||
});
|
3
src/.vuepress/styles/config.scss
Normal file
@ -0,0 +1,3 @@
|
||||
// you can change config here
|
||||
$colors: #c0392b, #d35400, #f39c12, #27ae60, #16a085, #2980b9, #8e44ad, #2c3e50,
|
||||
#7f8c8d !default;
|
1
src/.vuepress/styles/index.scss
Normal file
@ -0,0 +1 @@
|
||||
// place your custom styles here
|
2
src/.vuepress/styles/palette.scss
Normal file
@ -0,0 +1,2 @@
|
||||
// you can change colors here
|
||||
$theme-color: #096dd9;
|
183
src/.vuepress/theme.ts
Normal 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
@ -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
@ -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
@ -0,0 +1,13 @@
|
||||
---
|
||||
head:
|
||||
- - meta
|
||||
- name: keywords
|
||||
content: API调用方法
|
||||
title: API调用方法
|
||||
icon: docs
|
||||
author: ikun0014
|
||||
date: 2024-04-10
|
||||
index: false
|
||||
---
|
||||
|
||||
# Test
|
13
src/guide/deploy-linuxordarwin.md
Normal 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
|
16
src/guide/deploy-windows.md
Normal 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的下载链接
|