mirror of
https://github.com/MeoProject/lx-music-api-server.git
synced 2025-05-23 19:17:41 +08:00
Compare commits
171 Commits
v2.0.0.bet
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
3fb2718cdf | ||
|
0677c0b5ad | ||
|
2400602aae | ||
|
77737d18e6 | ||
|
e13b7e3831 | ||
|
c175312307 | ||
|
b4049ceb69 | ||
|
031834d40f | ||
|
daf34ace39 | ||
|
3b6ee6e9b2 | ||
|
edaec2a3d2 | ||
|
30c0eaa23e | ||
|
412f692ba1 | ||
|
c84c2acbb9 | ||
|
3e5c1cd94b | ||
|
3375f0beba | ||
|
82be199636 | ||
|
f7069fc6f8 | ||
|
ed47e928dd | ||
|
56d19ee671 | ||
|
0433d7a4d1 | ||
|
c63beb983f | ||
|
6d247dbeac | ||
|
d4cb252aa4 | ||
|
1ea53d2ca9 | ||
|
497f21d2d4 | ||
|
8e361ff14b | ||
|
d3ab5f910d | ||
|
6b08683b66 | ||
|
196732ab7d | ||
|
dcfe8a4472 | ||
|
b45c3a7765 | ||
|
5442e3340b | ||
|
92a69f048d | ||
|
0fd9fe1c3e | ||
|
3533a72681 | ||
|
88937e4917 | ||
|
6c6ef01681 | ||
|
f67de8f43b | ||
|
7dbafb958c | ||
|
f7988a48b1 | ||
|
52420696f8 | ||
|
aa1894b66f | ||
|
a177ebb3e6 | ||
|
94778abece | ||
|
4709eea052 | ||
|
2370f73fa8 | ||
|
f58d1b2069 | ||
|
b13c7d77ab | ||
|
f54b3d38dd | ||
|
edfc6396d8 | ||
|
6dee331484 | ||
|
bb1df10f1c | ||
|
39fa65c2b6 | ||
|
9adc778c05 | ||
|
3a2da26245 | ||
|
193618b34e | ||
|
e62ef1ce82 | ||
|
fe942f4ed9 | ||
|
c9d5df7fe8 | ||
|
15bfed2625 | ||
|
2e6c49e2fe | ||
|
9bcfd581d9 | ||
|
4bf675ee3b | ||
|
089d1c2cde | ||
|
8919767eb5 | ||
|
c254555ee7 | ||
|
b82795eb13 | ||
|
f0ecd9f269 | ||
|
d1e68719ba | ||
|
67c11df74c | ||
|
c4b38a47bb | ||
|
cca000e318 | ||
|
097b82c0c2 | ||
|
2e67b1967a | ||
|
bdf03dbfdd | ||
|
10dcfdb8aa | ||
|
3ea3c75859 | ||
|
faeea6c42a | ||
|
1f36aecff4 | ||
|
a70a646a06 | ||
|
e78599de70 | ||
|
33bfdf1928 | ||
|
2141b7d665 | ||
|
c014d8a58a | ||
|
c22aa35d30 | ||
|
f8b0126066 | ||
|
4ac0633d8e | ||
|
622344516d | ||
|
30dcf0a5be | ||
|
2dbb84ff28 | ||
|
f2e3f8335b | ||
|
f93a888259 | ||
|
c11718e2a8 | ||
|
1da6ddaab5 | ||
|
5d687e6573 | ||
|
f1640760af | ||
|
9066a251a1 | ||
|
0502286ac8 | ||
|
bf16930c70 | ||
|
56a8c2d18c | ||
|
5076086b0e | ||
|
d22b434b4a | ||
|
9d985d58cb | ||
|
2463660e90 | ||
|
acb84a5cb9 | ||
|
1c939b5f23 | ||
|
17332fa751 | ||
|
d6c113c293 | ||
|
afd39e1c46 | ||
|
667d420499 | ||
|
45e2e7147d | ||
|
3f4e921e2a | ||
|
facb6d81e6 | ||
|
a769b23cdf | ||
|
6412003d89 | ||
|
c42d30f737 | ||
|
4d6ff2626b | ||
|
7e10d98c47 | ||
|
6062e23e52 | ||
|
2436852848 | ||
|
576ee65820 | ||
|
d19c343615 | ||
|
27cf9d51a9 | ||
|
1a2f7e2e55 | ||
|
e6115b555b | ||
|
64a1b2ffac | ||
|
5e70560df9 | ||
|
bf2cd1dd36 | ||
|
0c5e405c31 | ||
|
f06b6e8710 | ||
|
cd34deca63 | ||
|
1b13a8e845 | ||
|
f806ffc32e | ||
|
fa53fdb4e1 | ||
|
b4ba6f5d86 | ||
|
de5043607d | ||
|
7ea698e147 | ||
|
267cf81425 | ||
|
546d94ab9a | ||
|
5645bf8c72 | ||
|
e9e6f327e6 | ||
|
f24762ecb7 | ||
|
3f346f79b6 | ||
|
72b7840819 | ||
|
1d640f8dd4 | ||
|
27e2f6677d | ||
|
715206ea6a | ||
|
f88496023f | ||
|
de3e8da790 | ||
|
7c5b8f2be0 | ||
|
ef77cdf7db | ||
|
48d8434e95 | ||
|
24b7082d0e | ||
|
bbdb3eea7a | ||
|
6c33f5971a | ||
|
da9be3da49 | ||
|
3e55a86669 | ||
|
d7dcc4401e | ||
|
d4b5c06592 | ||
|
f5f35bd8e0 | ||
|
41443f6325 | ||
|
bd7abc9a12 | ||
|
00de8745b5 | ||
|
6ef26f864c | ||
|
46a270f4f4 | ||
|
d2bedcfd73 | ||
|
b9a1e530dd | ||
|
35f1fe513a | ||
|
a55da25de5 | ||
|
b965fbfb37 |
2
.github/workflows/build_beta.yml
vendored
2
.github/workflows/build_beta.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
runs-on: [windows-latest, macos-latest, ubuntu-latest]
|
runs-on: [windows-latest, macos-latest, ubuntu-latest, ubuntu-20.04]
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
|
14
.github/workflows/build_release.yml
vendored
14
.github/workflows/build_release.yml
vendored
@ -60,6 +60,11 @@ jobs:
|
|||||||
python -m pip install toml
|
python -m pip install toml
|
||||||
echo PACKAGE_VERSION=`python -c 'import toml; print(toml.load("./pyproject.toml")["tool"]["poetry"]["version"])'` >> $GITHUB_ENV
|
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
|
- name: Create git tag
|
||||||
uses: pkgdeps/git-tag-action@v2
|
uses: pkgdeps/git-tag-action@v2
|
||||||
with:
|
with:
|
||||||
@ -76,14 +81,11 @@ jobs:
|
|||||||
pattern: dist-*
|
pattern: dist-*
|
||||||
merge-multiple: false
|
merge-multiple: false
|
||||||
|
|
||||||
- name: Rename File
|
- name: Rename Files
|
||||||
run: |
|
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-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-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 }}_linux
|
mv ./dist/dist-ubuntu-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }} ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_ubuntu
|
||||||
|
|
||||||
# - name: Generate Changelog
|
|
||||||
# run: python build.py changelog >> ./changelog.md
|
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
@ -95,6 +97,6 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_windows.exe
|
./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 }}_macos
|
||||||
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_linux
|
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_ubuntu
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -22,6 +22,9 @@ test.*
|
|||||||
*/test.*
|
*/test.*
|
||||||
logs
|
logs
|
||||||
config.json
|
config.json
|
||||||
|
config.yml
|
||||||
|
/config/config.json
|
||||||
|
/config/data.db
|
||||||
*.log
|
*.log
|
||||||
*.bak
|
*.bak
|
||||||
*.tmp
|
*.tmp
|
||||||
@ -39,3 +42,6 @@ config.json
|
|||||||
|
|
||||||
# temp script
|
# temp script
|
||||||
lx-music-source-example.js
|
lx-music-source-example.js
|
||||||
|
|
||||||
|
# dumprecord
|
||||||
|
dumprecord_*.txt
|
13
Dockerfile
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" ]
|
|
73
README.md
73
README.md
@ -2,26 +2,27 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
[](https://github.com/lxmusics/lx-music-api-server-python/actions/workflows/build_beta.yml)
|
[](https://github.com/MeoProject/lx-music-api-server/actions/workflows/build_beta.yml)
|
||||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases/latest)
|
[](https://github.com/MeoProject/lx-music-api-server/releases/latest)
|
||||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases)
|
[](https://github.com/MeoProject/lx-music-api-server/releases)
|
||||||
[](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE)
|
[](https://github.com/MeoProject/lx-music-api-server/blob/main/LICENSE)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
原仓库:[lx-music-api-server](https://github.com/lxmusics/lx-music-api-server)
|
本项目[文档](https://apidoc.zcmonety.xyz)
|
||||||
你可以在原仓库中找到对应的可用源脚本
|
|
||||||
|
|
||||||
由于使用此项目导致的**封号**等情况**与本项目无关**
|
由于使用此项目导致的**封号**等情况**与本项目无关**
|
||||||
|
|
||||||
|
本项目不接受私人定制,非**本项目Github发布**所出现问题**与本项目无关**
|
||||||
|
|
||||||
## 💡特点
|
## 💡特点
|
||||||
|
|
||||||
- [ ] 功能
|
- [ ] 功能
|
||||||
- [ ] 完整性API(歌单,搜索)
|
- [ ] 完整性API(歌单,搜索)
|
||||||
- [ ] 网易云刷新登录
|
- [x] 网易云刷新登录
|
||||||
- [ ] 服务器向webdav
|
- [ ] 服务器向webdav
|
||||||
- [x] 客户端播放服务器上的本地音乐
|
- [x] 客户端播放服务器上的本地音乐
|
||||||
- [x] Cookie池
|
- [x] Cookie池
|
||||||
@ -36,8 +37,8 @@
|
|||||||
|
|
||||||
### Release 部署(推荐)
|
### Release 部署(推荐)
|
||||||
|
|
||||||
1. 从 [Releases](https://github.com/lxmusics/lx-music-api-server-python/releases)
|
1. 从 [Releases](https://github.com/MeoProject/lx-music-api-server/releases)
|
||||||
或 [Actions](https://github.com/lxmusics/lx-music-api-server-python/actions)
|
或 [Actions](https://github.com/MeoProject/lx-music-api-server/actions)
|
||||||
下载对应你系统的可执行文件 (从 GitHub Actions 下载需要登录 GitHub 账号)
|
下载对应你系统的可执行文件 (从 GitHub Actions 下载需要登录 GitHub 账号)
|
||||||
|
|
||||||
2. 运行可执行文件(如果下载的文件是压缩包请先解压)
|
2. 运行可执行文件(如果下载的文件是压缩包请先解压)
|
||||||
@ -57,8 +58,8 @@
|
|||||||
2. clone本项目并进入项目目录
|
2. clone本项目并进入项目目录
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/lxmusics/lx-music-api-server-python.git
|
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||||
cd lx-music-api-server-python
|
cd lx-music-api-server
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 安装依赖
|
3. 安装依赖
|
||||||
@ -87,8 +88,8 @@ linux 命令如果为 python3 请自行替换
|
|||||||
1. clone本项目并进入项目目录
|
1. clone本项目并进入项目目录
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/lxmusics/lx-music-api-server-python.git
|
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||||
cd lx-music-api-server-python
|
cd lx-music-api-server
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 安装依赖
|
2. 安装依赖
|
||||||
@ -103,40 +104,6 @@ linux 命令如果为 python3 请自行替换
|
|||||||
python main.py
|
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 -d ikun0014/lx-music-api-server-python:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 获取容器目录
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker inspect lx-music-api-server-python
|
|
||||||
```
|
|
||||||
|
|
||||||
5. 到容器目录里的 `/app` 目录填写 `config.json`
|
|
||||||
|
|
||||||
## 📖返回码说明
|
## 📖返回码说明
|
||||||
|
|
||||||
接口返回值中`body.code`字段值中的代码含义
|
接口返回值中`body.code`字段值中的代码含义
|
||||||
@ -241,7 +208,7 @@ linux 命令如果为 python3 请自行替换
|
|||||||
|
|
||||||
4. 模块不拆分
|
4. 模块不拆分
|
||||||
|
|
||||||
详见[config.py](https://github.com/lxmusics/lx-music-api-server-python/tree/main/common/config.py)
|
详见[config.py](https://github.com/MeoProject/lx-music-api-server/tree/main/common/config.py)
|
||||||
|
|
||||||
5. 不明所以的变量名
|
5. 不明所以的变量名
|
||||||
|
|
||||||
@ -258,7 +225,7 @@ linux 命令如果为 python3 请自行替换
|
|||||||
|
|
||||||
## 📄项目协议
|
## 📄项目协议
|
||||||
|
|
||||||
本项目基于 [MIT](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE) 许可证发行,以下协议是对于 MIT 原协议的补充,如有冲突,以以下协议为准。
|
本项目基于 [MIT](https://github.com/MeoProject/lx-music-api-server/blob/main/LICENSE) 许可证发行,以下协议是对于 MIT 原协议的补充,如有冲突,以以下协议为准。
|
||||||
|
|
||||||
词语约定:本协议中的“本项目”指本音源项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。
|
词语约定:本协议中的“本项目”指本音源项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。
|
||||||
|
|
||||||
@ -278,8 +245,8 @@ folltoshe+foxmail.com
|
|||||||
|
|
||||||
## ✨Star 趋势图
|
## ✨Star 趋势图
|
||||||
|
|
||||||
[](https://starchart.cc/lxmusics/lx-music-api-server-python)
|
[](https://starchart.cc/MeoProject/lx-music-api-server)
|
||||||
|
|
||||||
## ⚙️贡献者
|
## ⚙️贡献者
|
||||||
|
|
||||||
[](https://github.com/lxmusics/lx-music-api-server-python/graphs/contributors)
|
[](https://github.com/MeoProject/lx-music-api-server/graphs/contributors)
|
||||||
|
72
README_EN.md
72
README_EN.md
@ -2,26 +2,26 @@ English | [简体中文](README.md)
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
[](https://github.com/lxmusics/lx-music-api-server-python/actions/workflows/build_beta.yml)
|
[](https://github.com/MeoProject/lx-music-api-server/actions/workflows/build_beta.yml)
|
||||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases/latest)
|
[](https://github.com/MeoProject/lx-music-api-server/releases/latest)
|
||||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases)
|
[](https://github.com/MeoProject/lx-music-api-server/releases)
|
||||||
[](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE)
|
[](https://github.com/MeoProject/lx-music-api-server/blob/main/LICENSE)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
This project [Document](https://apidoc.zcmonety.xyz)
|
||||||
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.
|
**Ban** and other situations caused by the use of this project have **nothing** to do with this project.
|
||||||
|
|
||||||
|
This project does not accept private customization, and the problems that occur in the non-**Github release of this project** have nothing to do with this project**
|
||||||
|
|
||||||
## 💡Feature
|
## 💡Feature
|
||||||
|
|
||||||
- [ ] Functions
|
- [ ] Functions
|
||||||
- [ ] All APIs (playlist, search)
|
- [ ] All APIs (playlist, search)
|
||||||
- [ ] Netease Cloud Refresh Login
|
- [x] Netease Cloud Refresh Login
|
||||||
- [ ] Client plays music on webdav
|
- [ ] Client plays music on webdav
|
||||||
- [x] Client plays local music on the server
|
- [x] Client plays local music on the server
|
||||||
- [x] Cookie pool
|
- [x] Cookie pool
|
||||||
@ -36,7 +36,7 @@ You can find the corresponding available source scripts in the original reposito
|
|||||||
|
|
||||||
### Use Release (recommended)
|
### 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)
|
1. Download the executable file corresponding to your system from [Releases](https://github.com/MeoProject/lx-music-api-server/releases) or [Actions](https://github.com/MeoProject/lx-music-api-server/actions)
|
||||||
|
|
||||||
2. Run the downloaded executable file (maybe you need to unzip the downloaded file if it is a compressed files)
|
2. Run the downloaded executable file (maybe you need to unzip the downloaded file if it is a compressed files)
|
||||||
|
|
||||||
@ -55,8 +55,8 @@ Required environment: Python 3.8+
|
|||||||
2. Clone this project and enter the project directory
|
2. Clone this project and enter the project directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/lxmusics/lx-music-api-server-python.git
|
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||||
cd lx-music-api-server-python
|
cd lx-music-api-server
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Install requirements
|
3. Install requirements
|
||||||
@ -85,8 +85,8 @@ If you are using linux, you command maybe python3, please replace it yourself.
|
|||||||
1. Clone this project and enter the project directory
|
1. Clone this project and enter the project directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/lxmusics/lx-music-api-server-python.git
|
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||||
cd lx-music-api-server-python
|
cd lx-music-api-server
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install requirements
|
2. Install requirements
|
||||||
@ -101,40 +101,6 @@ If you are using linux, you command maybe python3, please replace it yourself.
|
|||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Use Docker
|
|
||||||
|
|
||||||
Required environment: Docker
|
|
||||||
|
|
||||||
This method **has not been tested**, and we don't know the required Docker version, so you can try whether the existing Docker version can be run 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 -d ikun0014/lx-music-api-server-python:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Get container directory
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker inspect lx-music-api-server-python
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Go to the `/app` directory in the container directory and modify `config.json`.
|
|
||||||
|
|
||||||
## 📖Return code description
|
## 📖Return code description
|
||||||
|
|
||||||
The code meaning in the `body.code` field value in the interface return value.
|
The code meaning in the `body.code` field value in the interface return value.
|
||||||
@ -239,7 +205,7 @@ The code meaning of `statuscode` returned by the interface.
|
|||||||
|
|
||||||
4. Module does not split
|
4. Module does not split
|
||||||
|
|
||||||
Details at [config.py](https://github.com/lxmusics/lx-music-api-server-python/tree/main/common/config.py)
|
Details at [config.py](https://github.com/MeoProject/lx-music-api-server/tree/main/common/config.py)
|
||||||
|
|
||||||
5. Unknown variable name
|
5. Unknown variable name
|
||||||
|
|
||||||
@ -256,7 +222,7 @@ The code meaning of `statuscode` returned by the interface.
|
|||||||
|
|
||||||
## 📄Project agreement
|
## 📄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.
|
This project is issued under [MIT](https://github.com/MeoProject/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.
|
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.
|
||||||
|
|
||||||
@ -276,8 +242,8 @@ folltoshe+foxmail.com
|
|||||||
|
|
||||||
## ✨Star trend chart
|
## ✨Star trend chart
|
||||||
|
|
||||||
[](https://starchart.cc/lxmusics/lx-music-api-server-python)
|
[](https://starchart.cc/MeoProject/lx-music-api-server)
|
||||||
|
|
||||||
## ⚙️Contributor
|
## ⚙️Contributor
|
||||||
|
|
||||||
[](https://github.com/lxmusics/lx-music-api-server-python/graphs/contributors)
|
[](https://github.com/MeoProject/lx-music-api-server/graphs/contributors)
|
||||||
|
20
build.py
20
build.py
@ -6,11 +6,11 @@ import os
|
|||||||
|
|
||||||
|
|
||||||
def get_latest_tag():
|
def get_latest_tag():
|
||||||
t = subprocess.check_output(['git', 'tag']).decode('utf-8').strip().split("\n")
|
t = subprocess.check_output(['git', 'tag', '--sort=v:refname']).decode('utf-8').strip().split("\n")
|
||||||
return t[-1] if (t[-1] != toml.load("./pyproject.toml")["tool"]["poetry"]["version"]) else t[-2]
|
return t[-1] if (t[-1] != toml.load("./pyproject.toml")["tool"]["poetry"]["version"]) else t[-2]
|
||||||
|
|
||||||
def get_specified_tag(index):
|
def get_specified_tag(index):
|
||||||
r = subprocess.check_output(['git', 'tag']).decode('utf-8').strip().split("\n")
|
r = subprocess.check_output(['git', 'tag', '--sort=v:refname']).decode('utf-8').strip().split("\n")
|
||||||
n = []
|
n = []
|
||||||
for i in r:
|
for i in r:
|
||||||
if (i):
|
if (i):
|
||||||
@ -30,17 +30,17 @@ def get_changelog():
|
|||||||
noticeMsg = []
|
noticeMsg = []
|
||||||
unknownMsg = []
|
unknownMsg = []
|
||||||
for msg in res:
|
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])):
|
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]
|
msg = msg[1:-1]
|
||||||
if msg[8:].startswith('notice:'):
|
if msg[8:].startswith('notice'):
|
||||||
noticeMsg.append(msg)
|
noticeMsg.append(msg)
|
||||||
elif msg[8:].startswith('feat:'):
|
elif msg[8:].startswith('feat'):
|
||||||
featMsg.append(msg)
|
featMsg.append(msg)
|
||||||
elif msg[8:].startswith('fix:'):
|
elif msg[8:].startswith('fix'):
|
||||||
fixMsg.append(msg)
|
fixMsg.append(msg)
|
||||||
elif msg[8:].startswith('docs:'):
|
elif msg[8:].startswith('docs'):
|
||||||
docsMsg.append(msg)
|
docsMsg.append(msg)
|
||||||
elif msg[8:].startswith('build:'):
|
elif msg[8:].startswith('build'):
|
||||||
buildMsg.append(msg)
|
buildMsg.append(msg)
|
||||||
else:
|
else:
|
||||||
otherMsg.append(msg)
|
otherMsg.append(msg)
|
||||||
@ -104,6 +104,8 @@ def build_test(fileName):
|
|||||||
'-m',
|
'-m',
|
||||||
'PyInstaller',
|
'PyInstaller',
|
||||||
'-F',
|
'-F',
|
||||||
|
'-i',
|
||||||
|
'res/icon.ico',
|
||||||
'--name',
|
'--name',
|
||||||
fileName if fileName else f'lx-music-api-server_{sha}',
|
fileName if fileName else f'lx-music-api-server_{sha}',
|
||||||
'main.py'])
|
'main.py'])
|
||||||
@ -137,6 +139,8 @@ def build_release(fileName = ''):
|
|||||||
'-m',
|
'-m',
|
||||||
'PyInstaller',
|
'PyInstaller',
|
||||||
'-F',
|
'-F',
|
||||||
|
'-i',
|
||||||
|
'res/icon.ico',
|
||||||
'--name',
|
'--name',
|
||||||
fileName if fileName else f'lx-music-api-server_{vername}',
|
fileName if fileName else f'lx-music-api-server_{vername}',
|
||||||
'main.py'])
|
'main.py'])
|
||||||
|
15
changelog.md
15
changelog.md
@ -1,15 +0,0 @@
|
|||||||
## bug修复
|
|
||||||
- 068b58a fix: [release] 修复release乱码问题
|
|
||||||
- 61f426e fix: 修复非大陆服务器酷狗歌曲搜索
|
|
||||||
- 0af0515 fix: 支持海外????
|
|
||||||
## 构建配置
|
|
||||||
- af1c69a build: 修复构建
|
|
||||||
- f927f15 build: [release] 我来刷版本号辣
|
|
||||||
- 8457dd4 build: [release] 我来刷版本号辣
|
|
||||||
- 31ec95b build: [release] 我来刷版本号辣
|
|
||||||
- 7d1047e build: [release] 我来刷版本号辣
|
|
||||||
- 878a9f8 build: [release] 我来刷版本号辣
|
|
||||||
## 其他
|
|
||||||
- ff276cb chore: [release] 更新beta-8
|
|
||||||
- 827c5c5 chore: 提交更新日志
|
|
||||||
- 47d4bb3 chore: 更新版本号
|
|
231
common/Httpx.py
231
common/Httpx.py
@ -21,15 +21,16 @@ from . import config
|
|||||||
from . import utils
|
from . import utils
|
||||||
from . import variable
|
from . import variable
|
||||||
|
|
||||||
|
|
||||||
def is_valid_utf8(text) -> bool:
|
def is_valid_utf8(text) -> bool:
|
||||||
try:
|
try:
|
||||||
if isinstance(text, bytes):
|
if isinstance(text, bytes):
|
||||||
text = text.decode('utf-8')
|
text = text.decode("utf-8")
|
||||||
# 判断是否为有效的utf-8字符串
|
# 判断是否为有效的utf-8字符串
|
||||||
if "\ufffe" in text:
|
if "\ufffe" in text:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
text.encode('utf-8').decode('utf-8')
|
text.encode("utf-8").decode("utf-8")
|
||||||
return True
|
return True
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
return False
|
return False
|
||||||
@ -37,42 +38,48 @@ def is_valid_utf8(text) -> bool:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_plain_text(text) -> bool:
|
def is_plain_text(text) -> bool:
|
||||||
# 判断是否为纯文本
|
# 判断是否为纯文本
|
||||||
pattern = re.compile(r'[^\x00-\x7F]')
|
pattern = re.compile(r"[^\x00-\x7F]")
|
||||||
return not bool(pattern.search(text))
|
return not bool(pattern.search(text))
|
||||||
|
|
||||||
|
|
||||||
def convert_dict_to_form_string(dic: dict) -> str:
|
def convert_dict_to_form_string(dic: dict) -> str:
|
||||||
# 将字典转换为表单字符串
|
# 将字典转换为表单字符串
|
||||||
return '&'.join([f'{k}={v}' for k, v in dic.items()])
|
return "&".join([f"{k}={v}" for k, v in dic.items()])
|
||||||
|
|
||||||
|
|
||||||
def log_plaintext(text: str) -> str:
|
def log_plaintext(text: str) -> str:
|
||||||
if (text.startswith('{') and text.endswith('}')):
|
if text.startswith("{") and text.endswith("}"):
|
||||||
try:
|
try:
|
||||||
text = json.loads(text)
|
text = json.loads(text)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
elif (text.startswith('<xml') and text.endswith('>')): # xml data
|
elif text.startswith("<xml") and text.endswith(">"): # xml data
|
||||||
try:
|
try:
|
||||||
text = f'xml: {utils.load_xml(text)}'
|
text = f"xml: {utils.load_xml(text)}"
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
# 内置的UA列表
|
# 内置的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',
|
ua_list = [
|
||||||
'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 (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 (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; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0",
|
||||||
'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 (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; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 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'
|
"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')
|
logger = log.log("http_utils")
|
||||||
|
|
||||||
def request(url: str, options = {}) -> requests.Response:
|
|
||||||
'''
|
def request(url: str, options={}) -> requests.Response:
|
||||||
|
"""
|
||||||
Http请求主函数, 用于发送网络请求
|
Http请求主函数, 用于发送网络请求
|
||||||
- url: 需要请求的URL地址(必填)
|
- url: 需要请求的URL地址(必填)
|
||||||
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
|
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
|
||||||
@ -86,13 +93,13 @@ def request(url: str, options = {}) -> requests.Response:
|
|||||||
- cache-ignore: <list> 缓存忽略关键字
|
- cache-ignore: <list> 缓存忽略关键字
|
||||||
|
|
||||||
@ return: requests.Response类型的响应数据
|
@ return: requests.Response类型的响应数据
|
||||||
'''
|
"""
|
||||||
# 缓存读取
|
# 缓存读取
|
||||||
cache_key = f'{url}{options}'
|
cache_key = f"{url}{options}"
|
||||||
if (isinstance(options.get('cache-ignore'), list)):
|
if isinstance(options.get("cache-ignore"), list):
|
||||||
for i in options.get('cache-ignore'):
|
for i in options.get("cache-ignore"):
|
||||||
cache_key = cache_key.replace(str(i), '')
|
cache_key = cache_key.replace(str(i), "")
|
||||||
options.pop('cache-ignore')
|
options.pop("cache-ignore")
|
||||||
cache_key = utils.createMD5(cache_key)
|
cache_key = utils.createMD5(cache_key)
|
||||||
if options.get("cache") and options["cache"] != "no-cache":
|
if options.get("cache") and options["cache"] != "no-cache":
|
||||||
cache = config.getCache("httpx", cache_key)
|
cache = config.getCache("httpx", cache_key)
|
||||||
@ -105,76 +112,83 @@ def request(url: str, options = {}) -> requests.Response:
|
|||||||
else:
|
else:
|
||||||
cache_info = None
|
cache_info = None
|
||||||
|
|
||||||
|
|
||||||
# 获取请求方法,没有则默认为GET请求
|
# 获取请求方法,没有则默认为GET请求
|
||||||
try:
|
try:
|
||||||
method = options['method']
|
method = options["method"].upper()
|
||||||
options.pop('method')
|
options.pop("method")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
method = 'GET'
|
method = "GET"
|
||||||
# 获取User-Agent,没有则从ua_list中随机选择一个
|
# 获取User-Agent,没有则从ua_list中随机选择一个
|
||||||
try:
|
try:
|
||||||
d_lower = {k.lower(): v for k, v in options['headers'].items()}
|
d_lower = {k.lower(): v for k, v in options["headers"].items()}
|
||||||
useragent = d_lower['user-agent']
|
useragent = d_lower["user-agent"]
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
options['headers']['User-Agent'] = random.choice(ua_list)
|
options["headers"]["User-Agent"] = random.choice(ua_list)
|
||||||
except:
|
except:
|
||||||
options['headers'] = {}
|
options["headers"] = {}
|
||||||
options['headers']['User-Agent'] = random.choice(ua_list)
|
options["headers"]["User-Agent"] = random.choice(ua_list)
|
||||||
# 检查是否在国内
|
# 检查是否在国内
|
||||||
if ((not variable.iscn) and (not options["headers"].get("X-Forwarded-For"))):
|
if (not variable.iscn) and (not options["headers"].get("X-Forwarded-For")):
|
||||||
options["headers"]["X-Forwarded-For"] = variable.fakeip
|
options["headers"]["X-Forwarded-For"] = variable.fakeip
|
||||||
# 获取请求主函数
|
# 获取请求主函数
|
||||||
try:
|
try:
|
||||||
reqattr = getattr(requests, method.lower())
|
reqattr = getattr(requests, method.lower())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise AttributeError('Unsupported method: '+method)
|
raise AttributeError("Unsupported method: " + method)
|
||||||
# 请求前记录
|
# 请求前记录
|
||||||
logger.debug(f'HTTP Request: {url}\noptions: {options}')
|
logger.debug(f"HTTP Request: {url}\noptions: {options}")
|
||||||
# 转换body/form参数为原生的data参数,并为form请求追加Content-Type头
|
# 转换body/form参数为原生的data参数,并为form请求追加Content-Type头
|
||||||
if (method == 'POST') or (method == 'PUT'):
|
if (method == "POST") or (method == "PUT"):
|
||||||
if options.get('body'):
|
if options.get("body"):
|
||||||
options['data'] = options['body']
|
options["data"] = options["body"]
|
||||||
options.pop('body')
|
options.pop("body")
|
||||||
if options.get('form'):
|
if options.get("form"):
|
||||||
options['data'] = convert_dict_to_form_string(options['form'])
|
options["data"] = convert_dict_to_form_string(options["form"])
|
||||||
options.pop('form')
|
options.pop("form")
|
||||||
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
|
options["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
if (isinstance(options['data'], dict)):
|
if isinstance(options["data"], dict):
|
||||||
options['data'] = json.dumps(options['data'])
|
options["data"] = json.dumps(options["data"])
|
||||||
# 进行请求
|
# 进行请求
|
||||||
try:
|
try:
|
||||||
logger.info("-----start----- " + url)
|
logger.info("-----start----- " + url)
|
||||||
req = reqattr(url, **options)
|
req = reqattr(url, **options)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}')
|
logger.error(f"HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}")
|
||||||
raise e
|
raise e
|
||||||
# 请求后记录
|
# 请求后记录
|
||||||
logger.debug(f'Request to {url} succeed with code {req.status_code}')
|
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
|
if req.content.startswith(b"\x78\x9c") or req.content.startswith(b"\x78\x01"): # zlib headers
|
||||||
try:
|
try:
|
||||||
decompressed = zlib.decompress(req.content)
|
decompressed = zlib.decompress(req.content)
|
||||||
if (is_valid_utf8(decompressed)):
|
if is_valid_utf8(decompressed):
|
||||||
logger.debug(log_plaintext(decompressed.decode("utf-8")))
|
logger.debug(log_plaintext(decompressed.decode("utf-8")))
|
||||||
else:
|
else:
|
||||||
logger.debug('response is not text binary, ignore logging it')
|
logger.debug("response is not text binary, ignore logging it")
|
||||||
except:
|
except:
|
||||||
logger.debug('response is not text binary, ignore logging it')
|
logger.debug("response is not text binary, ignore logging it")
|
||||||
else:
|
else:
|
||||||
if (is_valid_utf8(req.content)):
|
if is_valid_utf8(req.content):
|
||||||
logger.debug(log_plaintext(req.content.decode("utf-8")))
|
logger.debug(log_plaintext(req.content.decode("utf-8")))
|
||||||
else:
|
else:
|
||||||
logger.debug('response is not text binary, ignore logging it')
|
logger.debug("response is not text binary, ignore logging it")
|
||||||
# 缓存写入
|
# 缓存写入
|
||||||
if (cache_info and cache_info != "no-cache"):
|
if cache_info and cache_info != "no-cache":
|
||||||
cache_data = pickle.dumps(req)
|
cache_data = pickle.dumps(req)
|
||||||
expire_time = (cache_info if isinstance(cache_info, int) else 3600) + int(time.time())
|
expire_time = cache_info if isinstance(cache_info, int) else 3600
|
||||||
config.updateCache("httpx", cache_key, {"expire": True, "time": expire_time, "data": utils.createBase64Encode(cache_data)})
|
expire_at = int((time.time()) + expire_time)
|
||||||
|
config.updateCache(
|
||||||
|
"httpx",
|
||||||
|
cache_key,
|
||||||
|
{"expire": True, "time": expire_at, "data": utils.createBase64Encode(cache_data)},
|
||||||
|
expire_time,
|
||||||
|
)
|
||||||
logger.debug("缓存已更新: " + url)
|
logger.debug("缓存已更新: " + url)
|
||||||
|
|
||||||
def _json():
|
def _json():
|
||||||
return json.loads(req.content)
|
return json.loads(req.content)
|
||||||
setattr(req, 'json', _json)
|
|
||||||
|
setattr(req, "json", _json)
|
||||||
# 返回请求
|
# 返回请求
|
||||||
return req
|
return req
|
||||||
|
|
||||||
@ -184,21 +198,24 @@ def checkcn():
|
|||||||
req = request("https://mips.kugou.com/check/iscn?&format=json")
|
req = request("https://mips.kugou.com/check/iscn?&format=json")
|
||||||
body = utils.CreateObject(req.json())
|
body = utils.CreateObject(req.json())
|
||||||
variable.iscn = bool(body.flag)
|
variable.iscn = bool(body.flag)
|
||||||
if (not variable.iscn):
|
if not variable.iscn:
|
||||||
variable.fakeip = config.read_config('common.fakeip')
|
variable.fakeip = config.read_config("common.fakeip")
|
||||||
logger.info(f"您在非中国大陆服务器({body.country})上启动了项目,已自动开启ip伪装")
|
logger.info(f"您在非中国大陆服务器({body.country})上启动了项目,已自动开启ip伪装")
|
||||||
logger.warning("此方式无法解决咪咕音乐的链接获取问题,您可以配置代理,服务器地址可在下方链接中找到\nhttps://hidemy.io/cn/proxy-list/?country=CN#list")
|
logger.warning(
|
||||||
|
"此方式无法解决咪咕音乐的链接获取问题,您可以配置代理,服务器地址可在下方链接中找到\nhttps://hidemy.io/cn/proxy-list/?country=CN#list"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('检查服务器位置失败,已忽略')
|
logger.warning("检查服务器位置失败,已忽略")
|
||||||
logger.warning(traceback.format_exc())
|
logger.warning(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
class ClientResponse:
|
class ClientResponse:
|
||||||
# 这个类为了方便aiohttp响应与requests响应的跨类使用,也为了解决pickle无法缓存的问题
|
# 这个类为了方便aiohttp响应与requests响应的跨类使用,也为了解决pickle无法缓存的问题
|
||||||
def __init__(self, status, content, headers):
|
def __init__(self, status, content, headers):
|
||||||
self.status = status
|
self.status = status
|
||||||
self.content = content
|
self.content = content
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
self.text = content.decode("utf-8", errors='ignore')
|
self.text = content.decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
return json.loads(self.content)
|
return json.loads(self.content)
|
||||||
@ -211,8 +228,9 @@ async def convert_to_requests_response(aiohttp_response) -> ClientResponse:
|
|||||||
|
|
||||||
return ClientResponse(status_code, content, headers)
|
return ClientResponse(status_code, content, headers)
|
||||||
|
|
||||||
async def AsyncRequest(url, options = {}) -> ClientResponse:
|
|
||||||
'''
|
async def AsyncRequest(url, options={}) -> ClientResponse:
|
||||||
|
"""
|
||||||
Http异步请求主函数, 用于发送网络请求
|
Http异步请求主函数, 用于发送网络请求
|
||||||
- url: 需要请求的URL地址(必填)
|
- url: 需要请求的URL地址(必填)
|
||||||
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
|
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
|
||||||
@ -226,15 +244,15 @@ async def AsyncRequest(url, options = {}) -> ClientResponse:
|
|||||||
- cache-ignore: <list> 缓存忽略关键字
|
- cache-ignore: <list> 缓存忽略关键字
|
||||||
|
|
||||||
@ return: common.Httpx.ClientResponse类型的响应数据
|
@ return: common.Httpx.ClientResponse类型的响应数据
|
||||||
'''
|
"""
|
||||||
if (not variable.aioSession):
|
if not variable.aioSession:
|
||||||
variable.aioSession = aiohttp.ClientSession(trust_env=True)
|
variable.aioSession = aiohttp.ClientSession(trust_env=True)
|
||||||
# 缓存读取
|
# 缓存读取
|
||||||
cache_key = f'{url}{options}'
|
cache_key = f"{url}{options}"
|
||||||
if (isinstance(options.get('cache-ignore'), list)):
|
if isinstance(options.get("cache-ignore"), list):
|
||||||
for i in options.get('cache-ignore'):
|
for i in options.get("cache-ignore"):
|
||||||
cache_key = cache_key.replace(str(i), '')
|
cache_key = cache_key.replace(str(i), "")
|
||||||
options.pop('cache-ignore')
|
options.pop("cache-ignore")
|
||||||
cache_key = utils.createMD5(cache_key)
|
cache_key = utils.createMD5(cache_key)
|
||||||
if options.get("cache") and options["cache"] != "no-cache":
|
if options.get("cache") and options["cache"] != "no-cache":
|
||||||
cache = config.getCache("httpx_async", cache_key)
|
cache = config.getCache("httpx_async", cache_key)
|
||||||
@ -248,75 +266,80 @@ async def AsyncRequest(url, options = {}) -> ClientResponse:
|
|||||||
else:
|
else:
|
||||||
cache_info = None
|
cache_info = None
|
||||||
|
|
||||||
|
|
||||||
# 获取请求方法,没有则默认为GET请求
|
# 获取请求方法,没有则默认为GET请求
|
||||||
try:
|
try:
|
||||||
method = options['method']
|
method = options["method"]
|
||||||
options.pop('method')
|
options.pop("method")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
method = 'GET'
|
method = "GET"
|
||||||
# 获取User-Agent,没有则从ua_list中随机选择一个
|
# 获取User-Agent,没有则从ua_list中随机选择一个
|
||||||
try:
|
try:
|
||||||
d_lower = {k.lower(): v for k, v in options['headers'].items()}
|
d_lower = {k.lower(): v for k, v in options["headers"].items()}
|
||||||
useragent = d_lower['user-agent']
|
useragent = d_lower["user-agent"]
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
options['headers']['User-Agent'] = random.choice(ua_list)
|
options["headers"]["User-Agent"] = random.choice(ua_list)
|
||||||
except:
|
except:
|
||||||
options['headers'] = {}
|
options["headers"] = {}
|
||||||
options['headers']['User-Agent'] = random.choice(ua_list)
|
options["headers"]["User-Agent"] = random.choice(ua_list)
|
||||||
# 检查是否在国内
|
# 检查是否在国内
|
||||||
if ((not variable.iscn) and (not options["headers"].get("X-Forwarded-For"))):
|
if (not variable.iscn) and (not options["headers"].get("X-Forwarded-For")):
|
||||||
options["headers"]["X-Forwarded-For"] = variable.fakeip
|
options["headers"]["X-Forwarded-For"] = variable.fakeip
|
||||||
# 获取请求主函数
|
# 获取请求主函数
|
||||||
try:
|
try:
|
||||||
reqattr = getattr(variable.aioSession, method.lower())
|
reqattr = getattr(variable.aioSession, method.lower())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise AttributeError('Unsupported method: '+method)
|
raise AttributeError("Unsupported method: " + method)
|
||||||
# 请求前记录
|
# 请求前记录
|
||||||
logger.debug(f'HTTP Request: {url}\noptions: {options}')
|
logger.debug(f"HTTP Request: {url}\noptions: {options}")
|
||||||
# 转换body/form参数为原生的data参数,并为form请求追加Content-Type头
|
# 转换body/form参数为原生的data参数,并为form请求追加Content-Type头
|
||||||
if (method == 'POST') or (method == 'PUT'):
|
if (method == "POST") or (method == "PUT"):
|
||||||
if options.get('body'):
|
if options.get("body") is not None:
|
||||||
options['data'] = options['body']
|
options["data"] = options["body"]
|
||||||
options.pop('body')
|
options.pop("body")
|
||||||
if options.get('form'):
|
if options.get("form") is not None:
|
||||||
options['data'] = convert_dict_to_form_string(options['form'])
|
options["data"] = convert_dict_to_form_string(options["form"])
|
||||||
options.pop('form')
|
options.pop("form")
|
||||||
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
|
options["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
if (isinstance(options['data'], dict)):
|
if isinstance(options.get("data"), dict):
|
||||||
options['data'] = json.dumps(options['data'])
|
options["data"] = json.dumps(options["data"])
|
||||||
# 进行请求
|
# 进行请求
|
||||||
try:
|
try:
|
||||||
logger.info("-----start----- " + url)
|
logger.info("-----start----- " + url)
|
||||||
req_ = await reqattr(url, **options)
|
req_ = await reqattr(url, **options)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}')
|
logger.error(f"HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}")
|
||||||
raise e
|
raise e
|
||||||
# 请求后记录
|
# 请求后记录
|
||||||
logger.debug(f'Request to {url} succeed with code {req_.status}')
|
logger.debug(f"Request to {url} succeed with code {req_.status}")
|
||||||
# 为懒人提供的不用改代码移植的方法
|
# 为懒人提供的不用改代码移植的方法
|
||||||
# 才不是梓澄呢
|
# 才不是梓澄呢
|
||||||
req = await convert_to_requests_response(req_)
|
req = await convert_to_requests_response(req_)
|
||||||
if (req.content.startswith(b'\x78\x9c') or req.content.startswith(b'\x78\x01')): # zlib headers
|
if req.content.startswith(b"\x78\x9c") or req.content.startswith(b"\x78\x01"): # zlib headers
|
||||||
try:
|
try:
|
||||||
decompressed = zlib.decompress(req.content)
|
decompressed = zlib.decompress(req.content)
|
||||||
if (is_valid_utf8(decompressed)):
|
if is_valid_utf8(decompressed):
|
||||||
logger.debug(log_plaintext(decompressed.decode("utf-8")))
|
logger.debug(log_plaintext(decompressed.decode("utf-8")))
|
||||||
else:
|
else:
|
||||||
logger.debug('response is not text binary, ignore logging it')
|
logger.debug("response is not text binary, ignore logging it")
|
||||||
except:
|
except:
|
||||||
logger.debug('response is not text binary, ignore logging it')
|
logger.debug("response is not text binary, ignore logging it")
|
||||||
else:
|
else:
|
||||||
if (is_valid_utf8(req.content)):
|
if is_valid_utf8(req.content):
|
||||||
logger.debug(log_plaintext(req.content.decode("utf-8")))
|
logger.debug(log_plaintext(req.content.decode("utf-8")))
|
||||||
else:
|
else:
|
||||||
logger.debug('response is not text binary, ignore logging it')
|
logger.debug("response is not text binary, ignore logging it")
|
||||||
# 缓存写入
|
# 缓存写入
|
||||||
if (cache_info and cache_info != "no-cache"):
|
if cache_info and cache_info != "no-cache":
|
||||||
cache_data = pickle.dumps(req)
|
cache_data = pickle.dumps(req)
|
||||||
expire_time = (cache_info if isinstance(cache_info, int) else 3600) + int(time.time())
|
expire_time = cache_info if isinstance(cache_info, int) else 3600
|
||||||
config.updateCache("httpx_async", cache_key, {"expire": True, "time": expire_time, "data": utils.createBase64Encode(cache_data)})
|
expire_at = int((time.time()) + expire_time)
|
||||||
|
config.updateCache(
|
||||||
|
"httpx_async",
|
||||||
|
cache_key,
|
||||||
|
{"expire": True, "time": expire_at, "data": utils.createBase64Encode(cache_data)},
|
||||||
|
expire_time,
|
||||||
|
)
|
||||||
logger.debug("缓存已更新: " + url)
|
logger.debug("缓存已更新: " + url)
|
||||||
# 返回请求
|
# 返回请求
|
||||||
return req
|
return req
|
660
common/config.py
660
common/config.py
@ -13,321 +13,70 @@ import os
|
|||||||
import traceback
|
import traceback
|
||||||
import sys
|
import sys
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import shutil
|
||||||
|
import ruamel.yaml as yaml_
|
||||||
from . import variable
|
from . import variable
|
||||||
from .log import log
|
from .log import log
|
||||||
|
from . import default_config
|
||||||
import threading
|
import threading
|
||||||
|
import redis
|
||||||
|
|
||||||
logger = log('config_manager')
|
logger = log("config_manager")
|
||||||
|
|
||||||
# 创建线程本地存储对象
|
# 创建线程本地存储对象
|
||||||
local_data = threading.local()
|
local_data = threading.local()
|
||||||
|
local_cache = threading.local()
|
||||||
|
local_redis = threading.local()
|
||||||
|
|
||||||
|
|
||||||
def get_data_connection():
|
def get_data_connection():
|
||||||
# 检查线程本地存储对象是否存在连接对象,如果不存在则创建一个新的连接对象
|
|
||||||
if not hasattr(local_data, 'connection'):
|
|
||||||
local_data.connection = sqlite3.connect('data.db')
|
|
||||||
return local_data.connection
|
return local_data.connection
|
||||||
|
|
||||||
|
|
||||||
# 创建线程本地存储对象
|
|
||||||
local_cache = threading.local()
|
|
||||||
|
|
||||||
|
|
||||||
def get_cache_connection():
|
def get_cache_connection():
|
||||||
# 检查线程本地存储对象是否存在连接对象,如果不存在则创建一个新的连接对象
|
|
||||||
if not hasattr(local_cache, 'connection'):
|
|
||||||
local_cache.connection = sqlite3.connect('cache.db')
|
|
||||||
return local_cache.connection
|
return local_cache.connection
|
||||||
|
|
||||||
|
|
||||||
|
def get_redis_connection():
|
||||||
|
return local_redis.connection
|
||||||
|
|
||||||
|
|
||||||
|
def handle_connect_db():
|
||||||
|
try:
|
||||||
|
local_data.connection = sqlite3.connect("./config/data.db")
|
||||||
|
if read_config("common.cache.adapter") == "redis":
|
||||||
|
host = read_config("common.cache.redis.host")
|
||||||
|
port = read_config("common.cache.redis.port")
|
||||||
|
user = read_config("common.cache.redis.user")
|
||||||
|
password = read_config("common.cache.redis.password")
|
||||||
|
db = read_config("common.cache.redis.db")
|
||||||
|
client = redis.Redis(host=host, port=port, username=user, password=password, db=db)
|
||||||
|
if not client.ping():
|
||||||
|
raise
|
||||||
|
local_redis.connection = client
|
||||||
|
else:
|
||||||
|
local_cache.connection = sqlite3.connect("./cache.db")
|
||||||
|
except:
|
||||||
|
logger.error("连接数据库失败")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
class ConfigReadException(Exception):
|
class ConfigReadException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
default = {
|
yaml = yaml_.YAML()
|
||||||
"common": {
|
default_str = default_config.default
|
||||||
"host": "0.0.0.0",
|
default = yaml.load(default_str)
|
||||||
"_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,
|
|
||||||
"value": "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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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": "研究不深,后两项自行抓包获取,在header里",
|
|
||||||
"aversionid": "",
|
|
||||||
"token": "",
|
|
||||||
"osversion": "10",
|
|
||||||
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"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': [
|
|
||||||
{
|
|
||||||
'aversionid': '',
|
|
||||||
'token': '',
|
|
||||||
'osversion': '10',
|
|
||||||
'useragent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'kw': [
|
|
||||||
{
|
|
||||||
"uid": "0",
|
|
||||||
"token": "",
|
|
||||||
"device_id": "0",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def handle_default_config():
|
def handle_default_config():
|
||||||
with open("./config.json", "w", encoding="utf-8") as f:
|
with open("./config/config.yml", "w", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(default, indent=2, ensure_ascii=False,
|
f.write(default_str)
|
||||||
escape_forward_slashes=False))
|
if not os.getenv("build"):
|
||||||
f.close()
|
|
||||||
if (not os.getenv('build')):
|
|
||||||
logger.info('首次启动或配置文件被删除,已创建默认配置文件')
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f'\n建议您到{variable.workdir + os.path.sep}config.json修改配置后重新启动服务器')
|
f"首次启动或配置文件被删除,已创建默认配置文件\n建议您到{variable.workdir + os.path.sep}config.yml修改配置后重新启动服务器"
|
||||||
|
)
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
@ -368,8 +117,7 @@ def save_data(config_data):
|
|||||||
|
|
||||||
# Insert the new configuration data into the 'data' table
|
# Insert the new configuration data into the 'data' table
|
||||||
for key, value in config_data.items():
|
for key, value in config_data.items():
|
||||||
cursor.execute(
|
cursor.execute("INSERT INTO data (key, value) VALUES (?, ?)", (key, json.dumps(value)))
|
||||||
"INSERT INTO data (key, value) VALUES (?, ?)", (key, json.dumps(value)))
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@ -378,51 +126,69 @@ def save_data(config_data):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def handleBuildRedisKey(module, key):
|
||||||
|
prefix = read_config("common.cache.redis.key_prefix")
|
||||||
|
return f"{prefix}:{module}:{key}"
|
||||||
|
|
||||||
|
|
||||||
def getCache(module, key):
|
def getCache(module, key):
|
||||||
try:
|
try:
|
||||||
# 连接到数据库(如果数据库不存在,则会自动创建)
|
if read_config("common.cache.adapter") == "redis":
|
||||||
conn = get_cache_connection()
|
redis = get_redis_connection()
|
||||||
|
key = handleBuildRedisKey(module, key)
|
||||||
# 创建一个游标对象
|
result = redis.get(key)
|
||||||
cursor = conn.cursor()
|
if result:
|
||||||
|
cache_data = json.loads(result)
|
||||||
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
|
return cache_data
|
||||||
|
else:
|
||||||
|
# 连接到数据库(如果数据库不存在,则会自动创建)
|
||||||
|
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:
|
except:
|
||||||
pass
|
pass
|
||||||
# traceback.print_exc()
|
# traceback.print_exc()
|
||||||
return False
|
return None
|
||||||
|
|
||||||
|
|
||||||
def updateCache(module, key, data):
|
def updateCache(module, key, data, expire=None):
|
||||||
try:
|
try:
|
||||||
# 连接到数据库(如果数据库不存在,则会自动创建)
|
if read_config("common.cache.adapter") == "redis":
|
||||||
conn = get_cache_connection()
|
redis = get_redis_connection()
|
||||||
|
key = handleBuildRedisKey(module, key)
|
||||||
# 创建一个游标对象
|
redis.set(key, json.dumps(data), ex=expire if expire and expire > 0 else None)
|
||||||
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:
|
else:
|
||||||
cursor.execute(
|
# 连接到数据库(如果数据库不存在,则会自动创建)
|
||||||
"INSERT INTO cache (module, key, data) VALUES (?, ?, ?)", (module, key, json.dumps(data)))
|
conn = get_cache_connection()
|
||||||
conn.commit()
|
|
||||||
|
# 创建一个游标对象
|
||||||
|
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:
|
except:
|
||||||
logger.error('缓存写入遇到错误…')
|
logger.error("缓存写入遇到错误…")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
@ -430,13 +196,13 @@ def resetRequestTime(ip):
|
|||||||
config_data = load_data()
|
config_data = load_data()
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
config_data['requestTime'][ip] = 0
|
config_data["requestTime"][ip] = 0
|
||||||
except KeyError:
|
except KeyError:
|
||||||
config_data['requestTime'] = {}
|
config_data["requestTime"] = {}
|
||||||
config_data['requestTime'][ip] = 0
|
config_data["requestTime"][ip] = 0
|
||||||
save_data(config_data)
|
save_data(config_data)
|
||||||
except:
|
except:
|
||||||
logger.error('配置写入遇到错误…')
|
logger.error("配置写入遇到错误…")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
@ -444,20 +210,20 @@ def updateRequestTime(ip):
|
|||||||
try:
|
try:
|
||||||
config_data = load_data()
|
config_data = load_data()
|
||||||
try:
|
try:
|
||||||
config_data['requestTime'][ip] = time.time()
|
config_data["requestTime"][ip] = time.time()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
config_data['requestTime'] = {}
|
config_data["requestTime"] = {}
|
||||||
config_data['requestTime'][ip] = time.time()
|
config_data["requestTime"][ip] = time.time()
|
||||||
save_data(config_data)
|
save_data(config_data)
|
||||||
except:
|
except:
|
||||||
logger.error('配置写入遇到错误...')
|
logger.error("配置写入遇到错误...")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
def getRequestTime(ip):
|
def getRequestTime(ip):
|
||||||
config_data = load_data()
|
config_data = load_data()
|
||||||
try:
|
try:
|
||||||
value = config_data['requestTime'][ip]
|
value = config_data["requestTime"][ip]
|
||||||
except:
|
except:
|
||||||
value = 0
|
value = 0
|
||||||
return value
|
return value
|
||||||
@ -465,7 +231,7 @@ def getRequestTime(ip):
|
|||||||
|
|
||||||
def read_data(key):
|
def read_data(key):
|
||||||
config = load_data()
|
config = load_data()
|
||||||
keys = key.split('.')
|
keys = key.split(".")
|
||||||
value = config
|
value = config
|
||||||
for k in keys:
|
for k in keys:
|
||||||
if k not in value and keys.index(k) != len(keys) - 1:
|
if k not in value and keys.index(k) != len(keys) - 1:
|
||||||
@ -480,7 +246,7 @@ def read_data(key):
|
|||||||
def write_data(key, value):
|
def write_data(key, value):
|
||||||
config = load_data()
|
config = load_data()
|
||||||
|
|
||||||
keys = key.split('.')
|
keys = key.split(".")
|
||||||
current = config
|
current = config
|
||||||
for k in keys[:-1]:
|
for k in keys[:-1]:
|
||||||
if k not in current:
|
if k not in current:
|
||||||
@ -495,7 +261,7 @@ def write_data(key, value):
|
|||||||
def push_to_list(key, obj):
|
def push_to_list(key, obj):
|
||||||
config = load_data()
|
config = load_data()
|
||||||
|
|
||||||
keys = key.split('.')
|
keys = key.split(".")
|
||||||
current = config
|
current = config
|
||||||
for k in keys[:-1]:
|
for k in keys[:-1]:
|
||||||
if k not in current:
|
if k not in current:
|
||||||
@ -512,10 +278,10 @@ def push_to_list(key, obj):
|
|||||||
|
|
||||||
def write_config(key, value):
|
def write_config(key, value):
|
||||||
config = None
|
config = None
|
||||||
with open('config.json', 'r', encoding='utf-8') as f:
|
with open("./config/config.yml", "r", encoding="utf-8") as f:
|
||||||
config = json.load(f)
|
config = yaml_.YAML().load(f)
|
||||||
|
|
||||||
keys = key.split('.')
|
keys = key.split(".")
|
||||||
current = config
|
current = config
|
||||||
for k in keys[:-1]:
|
for k in keys[:-1]:
|
||||||
if k not in current:
|
if k not in current:
|
||||||
@ -523,17 +289,21 @@ def write_config(key, value):
|
|||||||
current = current[k]
|
current = current[k]
|
||||||
|
|
||||||
current[keys[-1]] = value
|
current[keys[-1]] = value
|
||||||
variable.config = config
|
|
||||||
with open('config.json', 'w', encoding='utf-8') as f:
|
# 设置保留注释和空行的参数
|
||||||
json.dump(config, f, indent=2, ensure_ascii=False,
|
y = yaml_.YAML()
|
||||||
escape_forward_slashes=False)
|
y.preserve_quotes = True
|
||||||
f.close()
|
y.preserve_blank_lines = True
|
||||||
|
|
||||||
|
# 写入配置并保留注释和空行
|
||||||
|
with open("./config/config.yml", "w", encoding="utf-8") as f:
|
||||||
|
y.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
def read_default_config(key):
|
def read_default_config(key):
|
||||||
try:
|
try:
|
||||||
config = default
|
config = default
|
||||||
keys = key.split('.')
|
keys = key.split(".")
|
||||||
value = config
|
value = config
|
||||||
for k in keys:
|
for k in keys:
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
@ -554,7 +324,7 @@ def read_default_config(key):
|
|||||||
def _read_config(key):
|
def _read_config(key):
|
||||||
try:
|
try:
|
||||||
config = variable.config
|
config = variable.config
|
||||||
keys = key.split('.')
|
keys = key.split(".")
|
||||||
value = config
|
value = config
|
||||||
for k in keys:
|
for k in keys:
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
@ -575,7 +345,7 @@ def _read_config(key):
|
|||||||
def read_config(key):
|
def read_config(key):
|
||||||
try:
|
try:
|
||||||
config = variable.config
|
config = variable.config
|
||||||
keys = key.split('.')
|
keys = key.split(".")
|
||||||
value = config
|
value = config
|
||||||
for k in keys:
|
for k in keys:
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
@ -591,23 +361,23 @@ def read_config(key):
|
|||||||
return value
|
return value
|
||||||
except:
|
except:
|
||||||
default_value = read_default_config(key)
|
default_value = read_default_config(key)
|
||||||
if (isinstance(default_value, type(None))):
|
if isinstance(default_value, type(None)):
|
||||||
logger.warning(f'配置文件{key}不存在')
|
logger.warning(f"配置文件{key}不存在")
|
||||||
else:
|
else:
|
||||||
for i in range(len(keys)):
|
for i in range(len(keys)):
|
||||||
tk = '.'.join(keys[:(i + 1)])
|
tk = ".".join(keys[: (i + 1)])
|
||||||
tkvalue = _read_config(tk)
|
tkvalue = _read_config(tk)
|
||||||
logger.debug(f'configfix: 读取配置文件{tk}的值:{tkvalue}')
|
logger.debug(f"configfix: 读取配置文件{tk}的值:{tkvalue}")
|
||||||
if ((tkvalue is None) or (tkvalue == {})):
|
if (tkvalue is None) or (tkvalue == {}):
|
||||||
write_config(tk, read_default_config(tk))
|
write_config(tk, read_default_config(tk))
|
||||||
logger.info(f'配置文件{tk}不存在,已创建')
|
logger.info(f"配置文件{tk}不存在,已创建")
|
||||||
return default_value
|
return default_value
|
||||||
|
|
||||||
|
|
||||||
def write_data(key, value):
|
def write_data(key, value):
|
||||||
config = load_data()
|
config = load_data()
|
||||||
|
|
||||||
keys = key.split('.')
|
keys = key.split(".")
|
||||||
current = config
|
current = config
|
||||||
for k in keys[:-1]:
|
for k in keys[:-1]:
|
||||||
if k not in current:
|
if k not in current:
|
||||||
@ -619,146 +389,166 @@ def write_data(key, value):
|
|||||||
save_data(config)
|
save_data(config)
|
||||||
|
|
||||||
|
|
||||||
def initConfig():
|
def init_config():
|
||||||
|
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")
|
||||||
|
if os.path.exists("./config/config.json"):
|
||||||
|
os.rename("./config/config.json", "./config/config.json.bak")
|
||||||
|
handle_default_config()
|
||||||
|
logger.warning("json配置文件已不再使用,已将其重命名为config.json.bak")
|
||||||
|
logger.warning("配置文件不会自动更新(因为变化太大),请手动修改配置文件重启服务器")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open("./config.json", "r", encoding="utf-8") as f:
|
with open("./config/config.yml", "r", encoding="utf-8") as f:
|
||||||
try:
|
try:
|
||||||
variable.config = json.loads(f.read())
|
variable.config = yaml.load(f.read())
|
||||||
if (not isinstance(variable.config, dict)):
|
if not isinstance(variable.config, dict):
|
||||||
logger.warning('配置文件并不是一个有效的字典,使用默认值')
|
logger.warning("配置文件并不是一个有效的字典,使用默认值")
|
||||||
variable.config = default
|
variable.config = default
|
||||||
with open("./config.json", "w", encoding="utf-8") as f:
|
with open("./config/config.yml", "w", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(variable.config, indent=2,
|
yaml.dump(variable.config, f)
|
||||||
ensure_ascii=False, escape_forward_slashes=False))
|
|
||||||
f.close()
|
f.close()
|
||||||
except:
|
except:
|
||||||
if os.path.getsize("./config.json") != 0:
|
if os.path.getsize("./config/config.yml") != 0:
|
||||||
logger.error("配置文件加载失败,请检查是否遵循JSON语法规范")
|
logger.error("配置文件加载失败,请检查是否遵循YAML语法规范")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
variable.config = handle_default_config()
|
variable.config = handle_default_config()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
variable.config = handle_default_config()
|
variable.config = handle_default_config()
|
||||||
# print(variable.config)
|
# print(variable.config)
|
||||||
variable.log_length_limit = read_config('common.log_length_limit')
|
variable.log_length_limit = read_config("common.log_length_limit")
|
||||||
variable.debug_mode = read_config('common.debug_mode')
|
variable.debug_mode = read_config("common.debug_mode")
|
||||||
logger.debug("配置文件加载成功")
|
logger.debug("配置文件加载成功")
|
||||||
conn = sqlite3.connect('cache.db')
|
|
||||||
|
# 尝试连接数据库
|
||||||
|
handle_connect_db()
|
||||||
|
|
||||||
|
conn = sqlite3.connect("./cache.db")
|
||||||
|
|
||||||
# 创建一个游标对象
|
# 创建一个游标对象
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 创建一个表来存储缓存数据
|
# 创建一个表来存储缓存数据
|
||||||
cursor.execute('''CREATE TABLE IF NOT EXISTS cache
|
cursor.execute(
|
||||||
|
"""CREATE TABLE IF NOT EXISTS cache
|
||||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
module TEXT NOT NULL,
|
module TEXT NOT NULL,
|
||||||
key TEXT NOT NULL,
|
key TEXT NOT NULL,
|
||||||
data TEXT NOT NULL)''')
|
data TEXT NOT NULL)"""
|
||||||
|
)
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
conn2 = sqlite3.connect('data.db')
|
conn2 = sqlite3.connect("./config/data.db")
|
||||||
|
|
||||||
# 创建一个游标对象
|
# 创建一个游标对象
|
||||||
cursor2 = conn2.cursor()
|
cursor2 = conn2.cursor()
|
||||||
|
|
||||||
cursor2.execute('''CREATE TABLE IF NOT EXISTS data
|
cursor2.execute(
|
||||||
|
"""CREATE TABLE IF NOT EXISTS data
|
||||||
(key TEXT PRIMARY KEY,
|
(key TEXT PRIMARY KEY,
|
||||||
value TEXT)''')
|
value TEXT)"""
|
||||||
|
)
|
||||||
|
|
||||||
conn2.close()
|
conn2.close()
|
||||||
|
|
||||||
logger.debug('数据库初始化成功')
|
logger.debug("数据库初始化成功")
|
||||||
|
|
||||||
# handle data
|
# handle data
|
||||||
all_data_keys = {'banList': [], 'requestTime': {}, 'banListRaw': []}
|
all_data_keys = {"banList": [], "requestTime": {}, "banListRaw": []}
|
||||||
data = load_data()
|
data = load_data()
|
||||||
if (data == {}):
|
if data == {}:
|
||||||
write_data('banList', [])
|
write_data("banList", [])
|
||||||
write_data('requestTime', {})
|
write_data("requestTime", {})
|
||||||
logger.info('数据库内容为空,已写入默认值')
|
logger.info("数据库内容为空,已写入默认值")
|
||||||
for k, v in all_data_keys.items():
|
for k, v in all_data_keys.items():
|
||||||
if (k not in data):
|
if k not in data:
|
||||||
write_data(k, v)
|
write_data(k, v)
|
||||||
logger.info(f'数据库中不存在{k},已创建')
|
logger.info(f"数据库中不存在{k},已创建")
|
||||||
|
|
||||||
# 处理代理配置
|
# 处理代理配置
|
||||||
if (read_config('common.proxy.enable')):
|
if read_config("common.proxy.enable"):
|
||||||
if (read_config('common.proxy.http_value')):
|
if read_config("common.proxy.http_value"):
|
||||||
os.environ['http_proxy'] = read_config('common.proxy.http_value')
|
os.environ["http_proxy"] = read_config("common.proxy.http_value")
|
||||||
logger.info('HTTP协议代理地址: ' +
|
logger.info("HTTP协议代理地址: " + read_config("common.proxy.http_value"))
|
||||||
read_config('common.proxy.http_value'))
|
if read_config("common.proxy.https_value"):
|
||||||
if (read_config('common.proxy.https_value')):
|
os.environ["https_proxy"] = 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('HTTPS协议代理地址: ' +
|
logger.info("代理功能已开启,请确保代理地址正确,否则无法连接网络")
|
||||||
read_config('common.proxy.https_value'))
|
|
||||||
logger.info('代理功能已开启,请确保代理地址正确,否则无法连接网络')
|
|
||||||
|
|
||||||
# cookie池
|
# cookie池
|
||||||
if (read_config('common.cookiepool')):
|
if read_config("common.cookiepool"):
|
||||||
logger.info('已启用cookie池功能,请确定配置的cookie都能正确获取链接')
|
logger.info("已启用cookie池功能,请确定配置的cookie都能正确获取链接")
|
||||||
logger.info('传统的源 - 单用户cookie配置将被忽略')
|
logger.info("传统的源 - 单用户cookie配置将被忽略")
|
||||||
logger.info('所以即使某个源你只有一个cookie,也请填写到cookiepool对应的源中,否则将无法使用该cookie')
|
logger.info("所以即使某个源你只有一个cookie,也请填写到cookiepool对应的源中,否则将无法使用该cookie")
|
||||||
variable.use_cookie_pool = True
|
variable.use_cookie_pool = True
|
||||||
|
|
||||||
# 移除已经过期的封禁数据
|
# 移除已经过期的封禁数据
|
||||||
banlist = read_data('banList')
|
banlist = read_data("banList")
|
||||||
banlistRaw = read_data('banListRaw')
|
banlistRaw = read_data("banListRaw")
|
||||||
count = 0
|
count = 0
|
||||||
for b in banlist:
|
for b in banlist:
|
||||||
if (b['expire'] and (time.time() > b['expire_time'])):
|
if b["expire"] and (time.time() > b["expire_time"]):
|
||||||
count += 1
|
count += 1
|
||||||
banlist.remove(b)
|
banlist.remove(b)
|
||||||
if (b['ip'] in banlistRaw):
|
if b["ip"] in banlistRaw:
|
||||||
banlistRaw.remove(b['ip'])
|
banlistRaw.remove(b["ip"])
|
||||||
write_data('banList', banlist)
|
write_data("banList", banlist)
|
||||||
write_data('banListRaw', banlistRaw)
|
write_data("banListRaw", banlistRaw)
|
||||||
if (count != 0):
|
if count != 0:
|
||||||
logger.info(f'已移除{count}条过期封禁数据')
|
logger.info(f"已移除{count}条过期封禁数据")
|
||||||
|
|
||||||
# 处理旧版数据库的banListRaw
|
# 处理旧版数据库的banListRaw
|
||||||
banlist = read_data('banList')
|
banlist = read_data("banList")
|
||||||
banlistRaw = read_data('banListRaw')
|
banlistRaw = read_data("banListRaw")
|
||||||
if (banlist != [] and banlistRaw == []):
|
if banlist != [] and banlistRaw == []:
|
||||||
for b in banlist:
|
for b in banlist:
|
||||||
banlistRaw.append(b['ip'])
|
banlistRaw.append(b["ip"])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def ban_ip(ip_addr, ban_time=-1):
|
def ban_ip(ip_addr, ban_time=-1):
|
||||||
if read_config('security.banlist.enable'):
|
if read_config("security.banlist.enable"):
|
||||||
banList = read_data('banList')
|
banList = read_data("banList")
|
||||||
banList.append({
|
banList.append(
|
||||||
'ip': ip_addr,
|
{
|
||||||
'expire': read_config('security.banlist.expire.enable'),
|
"ip": ip_addr,
|
||||||
'expire_time': read_config('security.banlist.expire.length') if (ban_time == -1) else ban_time,
|
"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):
|
write_data("banList", banList)
|
||||||
|
banListRaw = read_data("banListRaw")
|
||||||
|
if ip_addr not in banListRaw:
|
||||||
banListRaw.append(ip_addr)
|
banListRaw.append(ip_addr)
|
||||||
write_data('banListRaw', banListRaw)
|
write_data("banListRaw", banListRaw)
|
||||||
else:
|
else:
|
||||||
if (variable.banList_suggest < 10):
|
if variable.banList_suggest < 10:
|
||||||
variable.banList_suggest += 1
|
variable.banList_suggest += 1
|
||||||
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
|
logger.warning("黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求")
|
||||||
|
|
||||||
|
|
||||||
def check_ip_banned(ip_addr):
|
def check_ip_banned(ip_addr):
|
||||||
if read_config('security.banlist.enable'):
|
if read_config("security.banlist.enable"):
|
||||||
banList = read_data('banList')
|
banList = read_data("banList")
|
||||||
banlistRaw = read_data('banListRaw')
|
banlistRaw = read_data("banListRaw")
|
||||||
if (ip_addr in banlistRaw):
|
if ip_addr in banlistRaw:
|
||||||
for b in banList:
|
for b in banList:
|
||||||
if (b['ip'] == ip_addr):
|
if b["ip"] == ip_addr:
|
||||||
if (b['expire']):
|
if b["expire"]:
|
||||||
if (b['expire_time'] > int(time.time())):
|
if b["expire_time"] > int(time.time()):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
banList.remove(b)
|
banList.remove(b)
|
||||||
banlistRaw.remove(b['ip'])
|
banlistRaw.remove(b["ip"])
|
||||||
write_data('banListRaw', banlistRaw)
|
write_data("banListRaw", banlistRaw)
|
||||||
write_data('banList', banList)
|
write_data("banList", banList)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
@ -768,10 +558,10 @@ def check_ip_banned(ip_addr):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if (variable.banList_suggest <= 10):
|
if variable.banList_suggest <= 10:
|
||||||
variable.banList_suggest += 1
|
variable.banList_suggest += 1
|
||||||
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
|
logger.warning("黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
initConfig()
|
init_config()
|
||||||
|
236
common/default_config.py
Normal file
236
common/default_config.py
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
default = """\
|
||||||
|
common:
|
||||||
|
hosts: # 服务器监听地址
|
||||||
|
- 0.0.0.0
|
||||||
|
# - '::' # 取消这一行的注释,启用 ipv6 监听
|
||||||
|
ports: # 服务器启动时所使用的端口
|
||||||
|
- 9763
|
||||||
|
ssl_info: # 服务器https配置
|
||||||
|
# 这个服务器是否是https服务器,如果你使用了反向代理来转发这个服务器,如果它使用了https,也请将它设置为true
|
||||||
|
is_https: false
|
||||||
|
# python原生https监听
|
||||||
|
enable: false
|
||||||
|
ssl_ports:
|
||||||
|
- 443
|
||||||
|
path: # ssl证书的文件地址
|
||||||
|
cert: /path/to/your/cer
|
||||||
|
privkey: /path/to/your/private/key
|
||||||
|
reverse_proxy: # 针对类似于nginx一类的反代的配置
|
||||||
|
allow_public_ip: false # 允许来自公网的转发
|
||||||
|
allow_proxy: true # 是否允许反代
|
||||||
|
real_ip_header: X-Real-IP # 反代来源ip的来源头,不懂请保持默认
|
||||||
|
debug_mode: false # 是否开启调试模式
|
||||||
|
log_length_limit: 500 # 单条日志长度限制
|
||||||
|
fakeip: 1.0.1.114 # 服务器在海外时的IP伪装值
|
||||||
|
proxy: # 代理配置,HTTP与HTTPS协议需分开配置
|
||||||
|
enable: false
|
||||||
|
http_value: http://127.0.0.1:7890
|
||||||
|
https_value: http://127.0.0.1:7890
|
||||||
|
log_file: true # 是否存储日志文件
|
||||||
|
cookiepool: false # 是否开启cookie池,这将允许用户配置多个cookie并在请求时随机使用一个,启用后请在module.cookiepool中配置cookie,在user处配置的cookie会被忽略,cookiepool中格式统一为列表嵌套user处的cookie的字典
|
||||||
|
allow_download_script: true # 是否允许直接从服务端下载脚本,开启后可以直接访问 /script?key=你的请求key 下载脚本
|
||||||
|
download_config: # 源脚本的相关配置
|
||||||
|
name: 修改为你的源脚本名称
|
||||||
|
intro: 修改为你的源脚本描述
|
||||||
|
author: 修改为你的源脚本作者
|
||||||
|
version: 修改为你的源版本
|
||||||
|
filename: lx-music-source.js # 客户端保存脚本时的文件名(可能因浏览器不同出现不一样的情况)
|
||||||
|
dev: true # 是否启用开发模式
|
||||||
|
update: true # 是否开启脚本更新提醒
|
||||||
|
# 可用参数
|
||||||
|
# {updateUrl}为更新地址(带请求key)
|
||||||
|
# {url}为请求时的url(不带请求的param)
|
||||||
|
# {key}为请求时携带的key
|
||||||
|
updateMsg: "源脚本有更新啦,更新地址:\\n{updateUrl}"
|
||||||
|
quality:
|
||||||
|
kw: [128k]
|
||||||
|
kg: [128k]
|
||||||
|
tx: [128k]
|
||||||
|
wy: [128k]
|
||||||
|
mg: [128k]
|
||||||
|
local_music: # 服务器侧本地音乐相关配置,如果需要使用此功能请确保你的带宽足够
|
||||||
|
audio_path: ./audio
|
||||||
|
temp_path: ./temp
|
||||||
|
# 缓存配置
|
||||||
|
cache:
|
||||||
|
# 适配器 [redis,sql]
|
||||||
|
adapter: sql
|
||||||
|
# redis 配置
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
db: 0
|
||||||
|
user: ""
|
||||||
|
password: ""
|
||||||
|
key_prefix: "LXAPISERVER"
|
||||||
|
|
||||||
|
security:
|
||||||
|
rate_limit: # 请求速率限制 填入的值为至少间隔多久才能进行一次请求,单位:秒,不限制请填为0
|
||||||
|
global: 0 # 全局
|
||||||
|
ip: 0 # 单个IP
|
||||||
|
key:
|
||||||
|
enable: false # 是否开启请求key,开启后只有请求头中包含key,且值一样时可以访问API
|
||||||
|
ban: true
|
||||||
|
values: # 填自己所有的请求key
|
||||||
|
- "114514"
|
||||||
|
whitelist_host: # 强制白名单HOST,不需要加端口号(即不受其他安全设置影响的HOST)
|
||||||
|
- localhost
|
||||||
|
- 0.0.0.0
|
||||||
|
- 127.0.0.1
|
||||||
|
check_lxm: false # 是否检查lxm请求头(正常的LX Music在内置源请求时都会携带这个请求头)
|
||||||
|
lxm_ban: true # lxm请求头不存在或不匹配时是否将用户IP加入黑名单
|
||||||
|
allowed_host: # HOST允许列表,启用后只允许列表内的HOST访问服务器,不需要加端口号
|
||||||
|
enable: false
|
||||||
|
blacklist: # 当用户访问的HOST并不在允许列表中时是否将请求IP加入黑名单,长度单位:秒
|
||||||
|
enable: false
|
||||||
|
length: 0
|
||||||
|
list:
|
||||||
|
- localhost
|
||||||
|
- 0.0.0.0
|
||||||
|
- 127.0.0.1
|
||||||
|
banlist: # 是否启用黑名单(全局设置,关闭后已存储的值并不受影响,但不会再检查)
|
||||||
|
enable: true
|
||||||
|
expire: # 是否启用黑名单IP过期(关闭后其他地方的配置会失效)
|
||||||
|
enable: true
|
||||||
|
length: 604800
|
||||||
|
|
||||||
|
module:
|
||||||
|
kg: # 酷狗音乐相关配置
|
||||||
|
enable: true # 是否开启本平台服务
|
||||||
|
client: # 客户端请求配置,不懂请保持默认,修改请统一为字符串格式
|
||||||
|
appid: "1005" # 酷狗音乐的appid,官方安卓为1005,官方PC为1001
|
||||||
|
signatureKey: OIlwieks28dk2k092lksi2UIkp # 客户端signature采用的key值,需要与appid对应
|
||||||
|
clientver: "12029" # 客户端versioncode,pidversionsecret可能随此值而变化
|
||||||
|
pidversionsecret: 57ae12eb6890223e355ccfcb74edf70d # 获取URL时所用的key值计算验证值
|
||||||
|
pid: "2" # url接口的pid
|
||||||
|
tracker: # trackerapi请求配置,不懂请保持默认,修改请统一为字符串格式
|
||||||
|
host: https://gateway.kugou.com
|
||||||
|
path: /v5/url
|
||||||
|
version: v5
|
||||||
|
x-router: # 当host为gateway.kugou.com时需要追加此头,为tracker类地址时则不需要
|
||||||
|
enable: true
|
||||||
|
value: tracker.kugou.com
|
||||||
|
extra_params: {} # 自定义添加的param,优先级大于默认,填写类型为普通的JSON数据,会自动转换为请求param
|
||||||
|
user: # 此处内容请统一抓包获取(/v5/url),需要vip账号来获取会员歌曲,如果没有请留为空值,mid必填,可以瞎填一段数字
|
||||||
|
token: ""
|
||||||
|
userid: "0"
|
||||||
|
mid: "114514"
|
||||||
|
lite_sign_in: # 是否启用概念版自动签到,仅在appid=3116时运行
|
||||||
|
enable: false
|
||||||
|
interval: 86400
|
||||||
|
mixsongmid: # mix_songmid的获取方式, 默认auto, 可以改成一个数字手动
|
||||||
|
value: auto
|
||||||
|
refresh_login: # 酷狗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: # QQ音乐相关配置
|
||||||
|
enable: true # 是否开启本平台服务
|
||||||
|
vkeyserver: # 请求官方api时使用的guid,uin等信息,不需要与cookie中信息一致
|
||||||
|
guid: "114514"
|
||||||
|
uin: "10086"
|
||||||
|
user: # 用户数据,可以通过浏览器获取,需要vip账号来获取会员歌曲,如果没有请留为空值,qqmusic_key可以从Cookie中/客户端的请求体中(comm.authst)获取
|
||||||
|
qqmusic_key: ""
|
||||||
|
uin: "" # key对应的QQ号
|
||||||
|
refresh_key: "" # 刷新失活 qqmusic_key
|
||||||
|
refresh_login: # 刷新登录相关配置,enable是否启动,interval刷新间隔
|
||||||
|
enable: false
|
||||||
|
interval: 86000
|
||||||
|
cdnaddr: http://ws.stream.qqmusic.qq.com/
|
||||||
|
|
||||||
|
wy: # 网易云音乐相关配置, proto支持值: ['offcial', 'ncmapi']
|
||||||
|
enable: true # 是否开启本平台服务
|
||||||
|
proto: offcial
|
||||||
|
user:
|
||||||
|
cookie: "" # 账号cookie数据,可以通过浏览器获取,需要vip账号来获取会员歌曲,如果没有请留为空值
|
||||||
|
refresh_login:
|
||||||
|
enable: false
|
||||||
|
interval: 86400
|
||||||
|
ncmapi:
|
||||||
|
api_url: "" # NeteaseCloudMusicApi的URL, 自行参考https://gitlab.com/Binaryify/neteasecloudmusicapi搭建
|
||||||
|
|
||||||
|
mg: # 咪咕音乐相关配置
|
||||||
|
enable: true # 是否开启本平台服务
|
||||||
|
user: # 研究不深,后两项自行抓包获取,网页端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: # cookie保活配置
|
||||||
|
enable: false
|
||||||
|
interval: 86400
|
||||||
|
|
||||||
|
kw: # 酷我音乐相关配置,proto支持值:['bd-api', 'kuwodes']
|
||||||
|
enable: true # 是否开启本平台服务
|
||||||
|
proto: bd-api
|
||||||
|
user:
|
||||||
|
uid: "0"
|
||||||
|
token: ""
|
||||||
|
device_id: "0"
|
||||||
|
des: # kuwodes接口(mobi, nmobi)一类的加密相关配置
|
||||||
|
f: kuwo
|
||||||
|
need_encrypt: true # 是否开启kuwodes
|
||||||
|
# {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
|
||||||
|
# 这里是reponse_type的所有支持值,当设置为json时会使用到下面的两个值来获取url/bitrate,如果为text,则为传统的逐行解析方式
|
||||||
|
response_type: json
|
||||||
|
url_json_path: data.url
|
||||||
|
bitrate_json_path: data.bitrate
|
||||||
|
headers:
|
||||||
|
User-Agent: okhttp/3.10.0
|
||||||
|
|
||||||
|
gcsp: # 歌词适配后端配置
|
||||||
|
# 请注意只允许私用,不要给原作者带来麻烦,谢谢
|
||||||
|
enable: false # 是否启用歌词适配后端
|
||||||
|
path: /client/cgi-bin/api.fcg # 后端接口地址
|
||||||
|
enable_verify: false # 是否启用后端验证
|
||||||
|
package_md5: "" # apk包的md5值,用于验证
|
||||||
|
salt_1: "NDRjZGIzNzliNzEe" # 后端验证参数1
|
||||||
|
salt_2: "6562653262383463363633646364306534333668" # 后端验证参数2
|
||||||
|
|
||||||
|
cookiepool:
|
||||||
|
kg:
|
||||||
|
- userid: "0"
|
||||||
|
token: ""
|
||||||
|
mid: "114514"
|
||||||
|
lite_sign_in: # 是否启用概念版自动签到,仅在appid=3116时运行
|
||||||
|
enable: false
|
||||||
|
interval: 86400
|
||||||
|
mixsongmid: # mix_songmid的获取方式, 默认auto, 可以改成一个数字手动
|
||||||
|
value: auto
|
||||||
|
refresh_login: # cookie池中对于此账号刷新登录的配置,账号间互不干扰
|
||||||
|
enable: false
|
||||||
|
login_url: http://login.user.kugou.com/v4/login_by_token
|
||||||
|
|
||||||
|
tx:
|
||||||
|
- qqmusic_key: ""
|
||||||
|
uin: ""
|
||||||
|
refresh_key: ""
|
||||||
|
refresh_login: # cookie池中对于此账号刷新登录的配置,账号间互不干扰
|
||||||
|
enable: false
|
||||||
|
interval: 86000
|
||||||
|
|
||||||
|
wy:
|
||||||
|
- cookie: ""
|
||||||
|
refresh_login: # cookie池中对于此账号刷新登录的配置,账号间互不干扰
|
||||||
|
enable: false
|
||||||
|
interval: 86400
|
||||||
|
|
||||||
|
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"
|
||||||
|
"""
|
93
common/gcsp.py
Normal file
93
common/gcsp.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: gcsp.py -
|
||||||
|
# - project: moonsea_api(private) -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "moonsea_api" project and featured for the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
import zlib
|
||||||
|
import binascii
|
||||||
|
import time
|
||||||
|
import ujson as json
|
||||||
|
import modules
|
||||||
|
from .utils import createMD5 as hashMd5
|
||||||
|
from . import config
|
||||||
|
from aiohttp.web import Response, Request
|
||||||
|
|
||||||
|
PACKAGE = config.read_config("module.gcsp.package_md5") # pkg md5
|
||||||
|
SALT_1 = config.read_config("module.gcsp.salt_1") # salt 1
|
||||||
|
SALT_2 = config.read_config("module.gcsp.salt_2") # salt 2
|
||||||
|
NEED_VERIFY = config.read_config("module.gcsp.enable_verify") # need verify
|
||||||
|
|
||||||
|
qm = {
|
||||||
|
'mp3': '128k',
|
||||||
|
'hq': '320k',
|
||||||
|
'sq': 'flac',
|
||||||
|
"hr": "flac24bit",
|
||||||
|
'hires': 'flac24bit'
|
||||||
|
}
|
||||||
|
|
||||||
|
pm = {
|
||||||
|
'qq': 'tx',
|
||||||
|
'wyy': 'wy',
|
||||||
|
'kugou': 'kg',
|
||||||
|
"kuwo": "kw",
|
||||||
|
"mgu": "mg"
|
||||||
|
}
|
||||||
|
|
||||||
|
internal_trans = {
|
||||||
|
"time": "请求检验失败,请检查系统时间是否为标准时间",
|
||||||
|
"sign": "请求检验失败,请检查应用是否被修改或更新到最新版本",
|
||||||
|
}
|
||||||
|
|
||||||
|
def decode(indata):
|
||||||
|
return json.loads(binascii.unhexlify(zlib.decompress(indata)))
|
||||||
|
|
||||||
|
def verify(data):
|
||||||
|
if (not NEED_VERIFY):
|
||||||
|
return "success"
|
||||||
|
sign_1 = hashMd5(PACKAGE + data["time"] + SALT_2)
|
||||||
|
sign_2 = hashMd5((json.dumps(data["text_1"]) + json.dumps(data["text_2"]) + sign_1 + data["time"] + SALT_1).replace("\\", "").replace("}\"", "}").replace("\"{", "{"))
|
||||||
|
|
||||||
|
if (data["sign_1"] != sign_1 or data["sign_2"] != sign_2):
|
||||||
|
return "sign"
|
||||||
|
if int(time.time()) - int(data["time"]) > 10:
|
||||||
|
return "time"
|
||||||
|
return "success"
|
||||||
|
|
||||||
|
async def handleGcspBody(body):
|
||||||
|
data = decode(body)
|
||||||
|
result = verify(data)
|
||||||
|
if (result != "success"):
|
||||||
|
return zlib.compress(json.dumps({"code": "403", "error_msg": internal_trans[result], "data": None}, ensure_ascii = False).encode("utf-8"))
|
||||||
|
|
||||||
|
data["te"] = json.loads(data["text_1"])
|
||||||
|
|
||||||
|
body = await modules.url(pm[data["te"]["platform"]], data["te"]["t1"], qm[data["te"]["t2"]])
|
||||||
|
|
||||||
|
if (body["code"] == 0):
|
||||||
|
return zlib.compress(json.dumps({"code": "200", "error_msg": "success", "data": body["data"] if (pm[data["te"]["platform"]] != "kw") else {"bitrate": "123", "url": body["data"]}}, ensure_ascii = False).encode("utf-8"))
|
||||||
|
else:
|
||||||
|
return zlib.compress(json.dumps({"code": "403", "error_msg": "内部系统错误,请稍后再试", "data": None}, ensure_ascii = False).encode("utf-8"))
|
||||||
|
|
||||||
|
async def handle_request(request: Request):
|
||||||
|
if (request.method == "POST"):
|
||||||
|
content_size = request.content_length
|
||||||
|
if (content_size > 5 * 1024): # 5kb
|
||||||
|
return Response(
|
||||||
|
body = "Request Entity Too Large",
|
||||||
|
status = 413
|
||||||
|
)
|
||||||
|
body = await request.read()
|
||||||
|
return Response(
|
||||||
|
body = await handleGcspBody(body),
|
||||||
|
content_type = "application/octet-stream",
|
||||||
|
status = 200
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
body = "Method Not Allowed",
|
||||||
|
status = 405
|
||||||
|
)
|
@ -12,9 +12,8 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from common.utils import createMD5, timeLengthFormat
|
from common.utils import createFileMD5, createMD5, timeLengthFormat
|
||||||
from . import log, config
|
from . import log, config
|
||||||
from pydub.utils import mediainfo
|
|
||||||
import ujson as json
|
import ujson as json
|
||||||
import traceback
|
import traceback
|
||||||
import mutagen
|
import mutagen
|
||||||
@ -144,16 +143,23 @@ def getAudioMeta(filepath):
|
|||||||
audio = mutagen.File(filepath)
|
audio = mutagen.File(filepath)
|
||||||
if not audio:
|
if not audio:
|
||||||
return None
|
return None
|
||||||
logger.info(audio.items())
|
logger.debug(audio.items())
|
||||||
if (filepath.lower().endswith('.mp3')):
|
if (filepath.lower().endswith('.mp3')):
|
||||||
cover = audio.get('APIC:')
|
cover = audio.get('APIC:')
|
||||||
if (cover):
|
if (cover):
|
||||||
cover = convertCover(cover.data)
|
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')
|
title = audio.get('TIT2')
|
||||||
artist = audio.get('TPE1')
|
artist = audio.get('TPE1')
|
||||||
album = audio.get('TALB')
|
album = audio.get('TALB')
|
||||||
lyric = audio.get('TLRC')
|
if (lrc_key):
|
||||||
|
lyric = audio.get(lrc_key)
|
||||||
|
else:
|
||||||
|
lyric = None
|
||||||
if (title):
|
if (title):
|
||||||
title = title.text
|
title = title.text
|
||||||
if (artist):
|
if (artist):
|
||||||
@ -161,9 +167,21 @@ def getAudioMeta(filepath):
|
|||||||
if (album):
|
if (album):
|
||||||
album = album.text
|
album = album.text
|
||||||
if (lyric):
|
if (lyric):
|
||||||
lyric = lyric.text
|
lyric = [lyric.text]
|
||||||
else:
|
if (not lyric):
|
||||||
lyric = [None]
|
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:
|
else:
|
||||||
cover = audio.get('cover')
|
cover = audio.get('cover')
|
||||||
if (cover):
|
if (cover):
|
||||||
@ -183,6 +201,8 @@ def getAudioMeta(filepath):
|
|||||||
lyric = filterLyricLine(f.read())
|
lyric = filterLyricLine(f.read())
|
||||||
if (not checkLyricValid(lyric)):
|
if (not checkLyricValid(lyric)):
|
||||||
lyric = [None]
|
lyric = [None]
|
||||||
|
else:
|
||||||
|
lyric = [lyric]
|
||||||
f.close()
|
f.close()
|
||||||
else:
|
else:
|
||||||
lyric = [None]
|
lyric = [None]
|
||||||
@ -198,6 +218,7 @@ def getAudioMeta(filepath):
|
|||||||
"lyrics": lyric[0],
|
"lyrics": lyric[0],
|
||||||
'length': audio.info.length,
|
'length': audio.info.length,
|
||||||
'format_length': timeLengthFormat(audio.info.length),
|
'format_length': timeLengthFormat(audio.info.length),
|
||||||
|
'md5': createFileMD5(filepath),
|
||||||
}
|
}
|
||||||
except:
|
except:
|
||||||
logger.error(f"get audio meta error: {filepath}")
|
logger.error(f"get audio meta error: {filepath}")
|
||||||
@ -225,7 +246,7 @@ def extractCover(audio_info, temp_path):
|
|||||||
f.write(audio_info['cover'])
|
f.write(audio_info['cover'])
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def findAudios():
|
def findAudios(cache):
|
||||||
|
|
||||||
available_exts = [
|
available_exts = [
|
||||||
'mp3',
|
'mp3',
|
||||||
@ -240,6 +261,9 @@ def findAudios():
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
audios = []
|
audios = []
|
||||||
|
_map = {}
|
||||||
|
for c in cache:
|
||||||
|
_map[c['filepath']] = c
|
||||||
for file in files:
|
for file in files:
|
||||||
if (not file.endswith(tuple(available_exts))):
|
if (not file.endswith(tuple(available_exts))):
|
||||||
continue
|
continue
|
||||||
@ -247,8 +271,11 @@ def findAudios():
|
|||||||
if (not checkAudioValid(path)):
|
if (not checkAudioValid(path)):
|
||||||
continue
|
continue
|
||||||
logger.info(f"found audio: {path}")
|
logger.info(f"found audio: {path}")
|
||||||
meta = getAudioMeta(path)
|
if (not (_map.get(path) and _map[path]['md5'] == createFileMD5(path))):
|
||||||
audios = audios + [meta]
|
meta = getAudioMeta(path)
|
||||||
|
audios = audios + [meta]
|
||||||
|
else:
|
||||||
|
audios = audios + [_map[path]]
|
||||||
|
|
||||||
return audios
|
return audios
|
||||||
|
|
||||||
@ -259,7 +286,14 @@ def getAudioCover(filepath):
|
|||||||
audio = mutagen.File(filepath)
|
audio = mutagen.File(filepath)
|
||||||
if not audio:
|
if not audio:
|
||||||
return None
|
return None
|
||||||
return convertCover(audio.get('APIC:').data)
|
if (filepath.lower().endswith('mp3')):
|
||||||
|
return audio.get('APIC:').data
|
||||||
|
else:
|
||||||
|
if (readFileCheckCover(filepath)):
|
||||||
|
return getAudioCoverFromFFMpeg(filepath)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
except:
|
except:
|
||||||
logger.error(f"get audio cover error: {filepath}")
|
logger.error(f"get audio cover error: {filepath}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@ -308,7 +342,7 @@ def initMain():
|
|||||||
if (cache['file_list'] == os.listdir(AUDIO_PATH)):
|
if (cache['file_list'] == os.listdir(AUDIO_PATH)):
|
||||||
audios = cache['audios']
|
audios = cache['audios']
|
||||||
else:
|
else:
|
||||||
audios = findAudios()
|
audios = findAudios(cache['audios'])
|
||||||
writeLocalCache(audios)
|
writeLocalCache(audios)
|
||||||
for a in audios:
|
for a in audios:
|
||||||
map[a['filepath']] = a
|
map[a['filepath']] = a
|
||||||
|
249
common/log.py
249
common/log.py
@ -10,11 +10,20 @@
|
|||||||
import logging
|
import logging
|
||||||
import colorlog
|
import colorlog
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import io
|
||||||
|
import traceback
|
||||||
|
import time
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
from pygments.lexers import PythonLexer
|
from pygments.lexers import PythonLexer
|
||||||
from pygments.formatters import TerminalFormatter
|
from pygments.formatters import TerminalFormatter
|
||||||
from .utils import filterFileName, addToGlobalNamespace
|
from .utils import filterFileName, setGlobal, require
|
||||||
from .variable import debug_mode, log_length_limit, log_file
|
from .variable import debug_mode, log_length_limit, log_file, log_files
|
||||||
|
from colorama import Fore, Style
|
||||||
|
from colorama import init as clinit
|
||||||
|
|
||||||
|
clinit() # 初始化 colorama
|
||||||
|
|
||||||
if ((not os.path.exists("logs")) and log_file):
|
if ((not os.path.exists("logs")) and log_file):
|
||||||
try:
|
try:
|
||||||
@ -22,15 +31,158 @@ if ((not os.path.exists("logs")) and log_file):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class Color:
|
||||||
|
"""
|
||||||
|
彩色文字处理器
|
||||||
|
"""
|
||||||
|
|
||||||
def highlight_error(error):
|
def __getattr__(self, k):
|
||||||
# 对堆栈跟踪进行语法高亮
|
return lambda x: f"{getattr(Fore, k.upper())}{x}{Style.RESET_ALL}"
|
||||||
highlighted_traceback = highlight(
|
|
||||||
error, PythonLexer(), TerminalFormatter())
|
|
||||||
|
|
||||||
# 返回语法高亮后的堆栈跟踪字符串
|
color = Color()
|
||||||
return str(highlighted_traceback)
|
# purple_parentheses = color.purple('(') + '{}' + color.purple(')')
|
||||||
|
|
||||||
|
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 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]
|
||||||
|
}
|
||||||
|
# print(lineMap)
|
||||||
|
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
|
||||||
|
firstLineNumber = max(l - 3, 1)
|
||||||
|
line_number = firstLineNumber
|
||||||
|
for i in range(len(code['result'])):
|
||||||
|
c = code["result"][i]
|
||||||
|
if (c.startswith(code['current']) and (not viewed) and (i <= 3)):
|
||||||
|
viewed = True
|
||||||
|
line_number = line_number + 1
|
||||||
|
# print(line_number)
|
||||||
|
cc.append((' ' * (10 - len(str(l))) + f'{color.red(str(l))} >|' + python_highlight(c)))
|
||||||
|
else:
|
||||||
|
cc.append((' ' * (10 - len(str(line_number))) + f'{color.blue(str(line_number))} |' + python_highlight(c)))
|
||||||
|
line_number = line_number + 1
|
||||||
|
# print(line_number)
|
||||||
|
code = "\n".join(cc)
|
||||||
|
p = '"' + p + '"'
|
||||||
|
final.append(f" File {color.yellow(f'{p}')} in {color.cyan(f) + '()' if (not f.startswith('<')) else ''}\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')
|
||||||
|
'''
|
||||||
|
lines = [i for i in error.split("\n") if (i.strip() and not is_rubbish(i))]
|
||||||
|
|
||||||
|
final = []
|
||||||
|
ispass = False # pass handle next line
|
||||||
|
for index in range(len(lines)):
|
||||||
|
i = lines[index]
|
||||||
|
i2 = i.strip()
|
||||||
|
if (ispass):
|
||||||
|
ispass = False
|
||||||
|
continue
|
||||||
|
if (i2.startswith("Traceback (most recent call last):")):
|
||||||
|
final.append(color.cyan(i))
|
||||||
|
elif (i2.startswith("During handling of the above exception, another exception occurred:")):
|
||||||
|
final.append(color.cyan(i))
|
||||||
|
elif (i2.startswith("The above exception was the direct cause of the following exception:")):
|
||||||
|
final.append(color.cyan(i))
|
||||||
|
elif (i2.startswith("File")):
|
||||||
|
p, l, f = stack_info(i2)
|
||||||
|
p = p[1:-1]
|
||||||
|
if (p.startswith('<') or not os.path.isfile(p)):
|
||||||
|
final.append(f' File {color.yellow("\"" + p + "\"")} in {color.cyan(f)}() [line {color.blue(l)}]\n')
|
||||||
|
_probably_err_name = lines[index + 1].split(':')[0]
|
||||||
|
if not (lines.get(index + 1, '').strip().startswith("File") or require(
|
||||||
|
('builtins.' + _probably_err_name) if ("." not in _probably_err_name) else _probably_err_name
|
||||||
|
) is Exception):
|
||||||
|
final.append(f" > {color.red(l)} |" + python_highlight(lines[index + 1]))
|
||||||
|
ispass = True
|
||||||
|
continue
|
||||||
|
codeMap = read_code(p, l)
|
||||||
|
line_length = len(codeMap['result'])
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
except:
|
||||||
|
sys.stderr.write('格式化错误失败,使用默认格式\n' + traceback.format_exc())
|
||||||
|
if (isinstance(e, Exception)):
|
||||||
|
return stack_error(e)
|
||||||
|
else:
|
||||||
|
return e
|
||||||
|
|
||||||
class LogHelper(logging.Handler):
|
class LogHelper(logging.Handler):
|
||||||
# 日志转接器
|
# 日志转接器
|
||||||
@ -43,10 +195,19 @@ class LogHelper(logging.Handler):
|
|||||||
log_message = self.format(record)
|
log_message = self.format(record)
|
||||||
self.custom_logger.info(log_message)
|
self.custom_logger.info(log_message)
|
||||||
|
|
||||||
|
class fileWriter(logging.Handler):
|
||||||
|
def __init__(self, f: io.TextIOWrapper, f2: logging.Formatter):
|
||||||
|
self.file = f
|
||||||
|
self.formatter = f2
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord):
|
||||||
|
self.file.write(self.format(record) + '\n')
|
||||||
|
self.file.flush()
|
||||||
|
|
||||||
class log:
|
class log:
|
||||||
# 主类
|
# 主类
|
||||||
def __init__(self, module_name='Not named logger', output_level='INFO', filename=''):
|
def __init__(self, module_name='Not named logger', output_level='INFO', filename=''):
|
||||||
|
self.name = module_name
|
||||||
self._logger = logging.getLogger(module_name)
|
self._logger = logging.getLogger(module_name)
|
||||||
if not output_level.upper() in dir(logging):
|
if not output_level.upper() in dir(logging):
|
||||||
raise NameError('Unknown loglevel: '+output_level)
|
raise NameError('Unknown loglevel: '+output_level)
|
||||||
@ -65,21 +226,12 @@ class log:
|
|||||||
'CRITICAL': 'red,bg_white',
|
'CRITICAL': 'red,bg_white',
|
||||||
})
|
})
|
||||||
if log_file:
|
if log_file:
|
||||||
file_formatter = logging.Formatter(
|
|
||||||
'%(asctime)s|[%(name)s/%(levelname)s]|%(message)s',
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
|
||||||
)
|
|
||||||
if filename:
|
if filename:
|
||||||
filename = filterFileName(filename)
|
filename = filterFileName(filename)
|
||||||
else:
|
else:
|
||||||
filename = './logs/' + module_name + '.log'
|
filename = './logs/' + module_name + '.log'
|
||||||
file_handler = logging.FileHandler(filename, encoding="utf-8")
|
self.file = open(filename, 'a+', encoding='utf-8')
|
||||||
file_handler.setFormatter(file_formatter)
|
log_files.append(self.file)
|
||||||
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 = logging.StreamHandler()
|
||||||
console_handler.setFormatter(formatter)
|
console_handler.setFormatter(formatter)
|
||||||
self.module_name = module_name
|
self.module_name = module_name
|
||||||
@ -88,60 +240,55 @@ class log:
|
|||||||
debug_handler.setFormatter(formatter)
|
debug_handler.setFormatter(formatter)
|
||||||
|
|
||||||
def debug(self, message, allow_hidden=True):
|
def debug(self, message, allow_hidden=True):
|
||||||
if self.module_name == "flask" and "\n" in message:
|
if (log_file):
|
||||||
if message.startswith("Error"):
|
self.file.write('{time}|[{name}/DEBUG]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
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:
|
if len(str(message)) > log_length_limit and allow_hidden:
|
||||||
message = str(message)[:log_length_limit] + " ..."
|
message = str(message)[:log_length_limit] + " ..."
|
||||||
self._logger.debug(message)
|
self._logger.debug(message)
|
||||||
|
|
||||||
def log(self, message, allow_hidden=True):
|
def log(self, message, allow_hidden=True):
|
||||||
if self.module_name == "flask" and "\n" in message:
|
if (log_file):
|
||||||
if message.startswith("Error"):
|
self.file.write('{time}|[{name}/INFO]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
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:
|
if len(str(message)) > log_length_limit and allow_hidden:
|
||||||
message = str(message)[:log_length_limit] + " ..."
|
message = str(message)[:log_length_limit] + " ..."
|
||||||
self._logger.info(message)
|
self._logger.info(message)
|
||||||
|
|
||||||
def info(self, message, allow_hidden=True):
|
def info(self, message, allow_hidden=True):
|
||||||
if self.module_name == "flask" and "\n" in message:
|
if (log_file):
|
||||||
if message.startswith("Error"):
|
self.file.write('{time}|[{name}/INFO]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
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:
|
if len(str(message)) > log_length_limit and allow_hidden:
|
||||||
message = str(message)[:log_length_limit] + "..."
|
message = str(message)[:log_length_limit] + "..."
|
||||||
self._logger.info(message)
|
self._logger.info(message)
|
||||||
|
|
||||||
def warning(self, message):
|
def warning(self, message):
|
||||||
if (message.startswith('Traceback')):
|
if (log_file):
|
||||||
self._logger.error('\n' + highlight_error(message))
|
self.file.write('{time}|[{name}/WARNING]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
self._logger.warning(message)
|
|
||||||
|
if (message.strip().startswith('Traceback')):
|
||||||
|
self._logger.warning('\n' + highlight_error(message))
|
||||||
|
else:
|
||||||
|
self._logger.warning(message)
|
||||||
|
|
||||||
def error(self, message):
|
def error(self, message):
|
||||||
|
if (log_file):
|
||||||
|
self.file.write('{time}|[{name}/ERROR]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
|
|
||||||
if (message.startswith('Traceback')):
|
if (message.startswith('Traceback')):
|
||||||
self._logger.error('\n' + highlight_error(message))
|
self._logger.error('\n' + highlight_error(message))
|
||||||
else:
|
else:
|
||||||
self._logger.error(message)
|
self._logger.error(message)
|
||||||
|
|
||||||
def critical(self, message):
|
def critical(self, message):
|
||||||
self._logger.critical(message)
|
if (log_file):
|
||||||
|
self.file.write('{time}|[{name}/CRITICAL]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
|
|
||||||
|
if (message.startswith('Traceback')):
|
||||||
|
self._logger.critical('\n' + highlight_error(message))
|
||||||
|
else:
|
||||||
|
self._logger.critical(message)
|
||||||
|
|
||||||
def set_level(self, loglevel):
|
def set_level(self, loglevel):
|
||||||
loglevel_upper = loglevel.upper()
|
loglevel_upper = loglevel.upper()
|
||||||
@ -163,4 +310,4 @@ def logprint(*args, sep=' ', end='', file=None, flush=None):
|
|||||||
printlogger.info(sep.join(str(arg) for arg in args), allow_hidden=False)
|
printlogger.info(sep.join(str(arg) for arg in args), allow_hidden=False)
|
||||||
|
|
||||||
|
|
||||||
addToGlobalNamespace('print', logprint)
|
setGlobal(logprint, 'print')
|
||||||
|
@ -14,6 +14,7 @@ from .log import log
|
|||||||
from aiohttp.web import Response
|
from aiohttp.web import Response
|
||||||
import ujson as json
|
import ujson as json
|
||||||
import re
|
import re
|
||||||
|
from common.utils import createMD5
|
||||||
|
|
||||||
logger = log('lx_script')
|
logger = log('lx_script')
|
||||||
|
|
||||||
@ -37,8 +38,8 @@ async def get_response(retry = 0):
|
|||||||
if (retry > 21):
|
if (retry > 21):
|
||||||
logger.warning('请求源脚本内容失败')
|
logger.warning('请求源脚本内容失败')
|
||||||
return
|
return
|
||||||
baseurl = '/lxmusics/lx-music-api-server/main/lx-music-source-example.js'
|
baseurl = '/MeoProject/lx-music-api-server/main/lx-music-source-example.js'
|
||||||
jsdbaseurl = '/gh/lxmusics/lx-music-api-server@main/lx-music-source-example.js'
|
jsdbaseurl = '/gh/MeoProject/lx-music-api-server@main/lx-music-source-example.js'
|
||||||
try:
|
try:
|
||||||
i = retry
|
i = retry
|
||||||
if (i > 10):
|
if (i > 10):
|
||||||
@ -68,7 +69,7 @@ async def get_script():
|
|||||||
logger.warning('请求源脚本内容失败')
|
logger.warning('请求源脚本内容失败')
|
||||||
|
|
||||||
async def generate_script_response(request):
|
async def generate_script_response(request):
|
||||||
if (request.query.get('key') != config.read_config('security.key.value') and config.read_config('security.key.enable')):
|
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
|
return {'code': 6, 'msg': 'key验证失败', 'data': None}, 403
|
||||||
try:
|
try:
|
||||||
with open('./lx-music-source-example.js', 'r', encoding='utf-8') as f:
|
with open('./lx-music-source-example.js', 'r', encoding='utf-8') as f:
|
||||||
@ -83,29 +84,43 @@ async def generate_script_response(request):
|
|||||||
if (line.startswith('const API_URL')):
|
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}"''')
|
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')):
|
elif (line.startswith('const API_KEY')):
|
||||||
newScriptLines.append(f'const API_KEY = "{config.read_config("security.key.value")}"')
|
newScriptLines.append(f"""const API_KEY = `{request.query.get("key") if request.query.get("key") else ''''''}`""")
|
||||||
elif (line.startswith("* @name")):
|
elif (line.startswith("* @name")):
|
||||||
newScriptLines.append(" * @name " + config.read_config("common.download_config.name"))
|
newScriptLines.append(" * @name " + config.read_config("common.download_config.name"))
|
||||||
elif (line.startswith("* @description")):
|
elif (line.startswith("* @description")):
|
||||||
newScriptLines.append(" * @description " + config.read_config("common.download_config.intro"))
|
newScriptLines.append(" * @description " + config.read_config("common.download_config.intro"))
|
||||||
elif (line.startswith("* @author")):
|
elif (line.startswith("* @author")):
|
||||||
newScriptLines.append((" * @author helloplhm-qwq & Folltoshe & " + config.read_config("common.download_config.author")) if config.read_config("common.download_config.author") else " * @author helloplhm-qwq & Folltoshe")
|
newScriptLines.append(" * @author " + config.read_config("common.download_config.author"))
|
||||||
elif (line.startswith("* @version")):
|
elif (line.startswith("* @version")):
|
||||||
newScriptLines.append(" * @version " + config.read_config("common.download_config.version"))
|
newScriptLines.append(" * @version " + config.read_config("common.download_config.version"))
|
||||||
elif (line.startswith("const DEV_ENABLE ")):
|
elif (line.startswith("const DEV_ENABLE ")):
|
||||||
newScriptLines.append("const DEV_ENABLE = " + str(config.read_config("common.download_config.dev")).lower())
|
newScriptLines.append("const DEV_ENABLE = " + str(config.read_config("common.download_config.dev")).lower())
|
||||||
|
elif (line.startswith("const UPDATE_ENABLE ")):
|
||||||
|
newScriptLines.append("const UPDATE_ENABLE = " + str(config.read_config("common.download_config.update")).lower())
|
||||||
else:
|
else:
|
||||||
newScriptLines.append(oline)
|
newScriptLines.append(oline)
|
||||||
r = '\n'.join(newScriptLines)
|
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)
|
r = re.sub(r'const MUSIC_QUALITY = {[^}]+}', f'const MUSIC_QUALITY = JSON.parse(\'{json.dumps(config.read_config("common.download_config.quality"))}\')', r)
|
||||||
|
|
||||||
|
# 用于检查更新
|
||||||
|
if (config.read_config("common.download_config.update")):
|
||||||
|
md5 = createMD5(r)
|
||||||
|
r = r.replace(r"const SCRIPT_MD5 = ''", f"const SCRIPT_MD5 = '{md5}'")
|
||||||
|
if (request.query.get('checkUpdate')):
|
||||||
|
if (request.query.get('checkUpdate') == md5):
|
||||||
|
return {'code': 0, 'msg': 'success', 'data': None}, 200
|
||||||
|
url = f"{'https' if config.read_config('common.ssl_info.is_https') else 'http'}://{request.host}/script"
|
||||||
|
updateUrl = f"{url}{('?key=' + request.query.get('key')) if request.query.get('key') else ''}"
|
||||||
|
updateMsg = config.read_config('common.download_config.updateMsg').format(updateUrl = updateUrl, url = url, key = request.query.get('key')).replace('\\n', '\n')
|
||||||
|
return {'code': 0, 'msg': 'success', 'data': {'updateMsg': updateMsg, 'updateUrl': updateUrl}}, 200
|
||||||
|
|
||||||
return Response(text = r, content_type = 'text/javascript',
|
return Response(text = r, content_type = 'text/javascript',
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Disposition': f'''attachment; filename={
|
'Content-Disposition': f'''attachment; filename={
|
||||||
config.read_config("common.download_config.filename")
|
config.read_config("common.download_config.filename")
|
||||||
if config.read_config("common.download_config.filename").endswith(".js")
|
if config.read_config("common.download_config.filename").endswith(".js")
|
||||||
else (config.read_config("common.download_config.filename" + ".js"))}'''
|
else (config.read_config("common.download_config.filename") + ".js")}'''
|
||||||
})
|
})
|
||||||
|
|
||||||
if (config.read_config('common.allow_download_script')):
|
if (config.read_config('common.allow_download_script')):
|
||||||
|
@ -40,6 +40,9 @@ class taskWrapper:
|
|||||||
logger.error(f"task {self.name} run failed, waiting for next execute...")
|
logger.error(f"task {self.name} run failed, waiting for next execute...")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'SchedulerTaskWrapper(name="{self.name}", interval={self.interval}, function={self.function}, args={self.args}, latest_execute={self.latest_execute})'
|
||||||
|
|
||||||
def append(name, task, interval = 86400, args = {}):
|
def append(name, task, interval = 86400, args = {}):
|
||||||
global tasks
|
global tasks
|
||||||
wrapper = taskWrapper(name, task, interval, args)
|
wrapper = taskWrapper(name, task, interval, args)
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# This file is part of the "lx-music-api-server" project.
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import platform
|
import platform
|
||||||
import binascii
|
import binascii
|
||||||
import builtins
|
import builtins
|
||||||
@ -15,8 +16,9 @@ import zlib
|
|||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
import ipaddress
|
||||||
from urllib.parse import quote, unquote, urlparse
|
from urllib.parse import quote, unquote, urlparse
|
||||||
from hashlib import md5 as handleCreateMD5
|
from typing import Union
|
||||||
|
|
||||||
def createBase64Encode(data_bytes):
|
def createBase64Encode(data_bytes):
|
||||||
encoded_data = base64.b64encode(data_bytes)
|
encoded_data = base64.b64encode(data_bytes)
|
||||||
@ -51,8 +53,8 @@ def require(module):
|
|||||||
index += 1
|
index += 1
|
||||||
return _module
|
return _module
|
||||||
|
|
||||||
def addToGlobalNamespace(key, data):
|
def setGlobal(obj, key = ''):
|
||||||
setattr(builtins, key, data)
|
setattr(builtins, obj.__name__ if (not key) else key, obj)
|
||||||
|
|
||||||
def filterFileName(filename):
|
def filterFileName(filename):
|
||||||
if platform.system() == 'Windows' or platform.system() == 'Cygwin':
|
if platform.system() == 'Windows' or platform.system() == 'Cygwin':
|
||||||
@ -64,10 +66,17 @@ def filterFileName(filename):
|
|||||||
# 将不合法字符替换为下划线
|
# 将不合法字符替换为下划线
|
||||||
return re.sub(illegal_chars, '_', filename)
|
return re.sub(illegal_chars, '_', filename)
|
||||||
|
|
||||||
def createMD5(s: (str, bytes)):
|
def createMD5(s: Union[str, bytes]):
|
||||||
if (isinstance(s, str)):
|
if (isinstance(s, str)):
|
||||||
s = s.encode("utf-8")
|
s = s.encode("utf-8")
|
||||||
return handleCreateMD5(s).hexdigest()
|
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"):
|
def readFile(path, mode = "text"):
|
||||||
try:
|
try:
|
||||||
@ -191,5 +200,11 @@ def timestamp_format(t):
|
|||||||
t = int(t)
|
t = int(t)
|
||||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t))
|
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t))
|
||||||
|
|
||||||
addToGlobalNamespace('require', require)
|
def is_local_ip(ip):
|
||||||
|
try:
|
||||||
|
i = ipaddress.ip_address(ip)
|
||||||
|
return i.is_private
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
setGlobal(require)
|
@ -8,40 +8,43 @@
|
|||||||
# This file is part of the "lx-music-api-server" project.
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
import os as _os
|
import os as _os
|
||||||
import ujson as _json
|
import ruamel.yaml as _yaml
|
||||||
|
|
||||||
|
yaml = _yaml.YAML()
|
||||||
|
|
||||||
|
|
||||||
def _read_config_file():
|
def _read_config_file():
|
||||||
try:
|
try:
|
||||||
with open("./config.json", "r", encoding = "utf-8") as f:
|
with open(f"./config/config.yml", "r", encoding="utf-8") as f:
|
||||||
return _json.load(f)
|
return yaml.load(f.read())
|
||||||
except:
|
except:
|
||||||
pass
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _read_config(key):
|
def _read_config(key):
|
||||||
try:
|
config = _read_config_file()
|
||||||
config = _read_config_file()
|
keys = key.split('.')
|
||||||
keys = key.split('.')
|
value = config
|
||||||
value = config
|
for k in keys:
|
||||||
for k in keys:
|
if isinstance(value, dict):
|
||||||
if isinstance(value, dict):
|
if k not in value and keys.index(k) != len(keys) - 1:
|
||||||
if k not in value and keys.index(k) != len(keys) - 1:
|
value[k] = []
|
||||||
value[k] = {}
|
elif k not in value and keys.index(k) == len(keys) - 1:
|
||||||
elif k not in value and keys.index(k) == len(keys) - 1:
|
|
||||||
value = None
|
|
||||||
value = value[k]
|
|
||||||
else:
|
|
||||||
value = None
|
value = None
|
||||||
break
|
value = value[k]
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
break
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
return value
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
_dm = _read_config("common.debug_mode")
|
_dm = _read_config("common.debug_mode")
|
||||||
_lm = _read_config("common.log_file")
|
_lm = _read_config("common.log_file")
|
||||||
_ll = _read_config("common.log_length_limit")
|
_ll = _read_config("common.log_length_limit")
|
||||||
debug_mode = _dm if (_dm) else False
|
debug_mode = True if (_os.getenv('CURRENT_ENV') ==
|
||||||
|
'development') else (_dm if (_dm) else False)
|
||||||
log_length_limit = _ll if (_ll) else 500
|
log_length_limit = _ll if (_ll) else 500
|
||||||
log_file = _lm if (not isinstance(_lm , type(None))) else True
|
log_file = _lm if (isinstance(_lm, bool)) else True
|
||||||
running = True
|
running = True
|
||||||
config = {}
|
config = {}
|
||||||
workdir = _os.getcwd()
|
workdir = _os.getcwd()
|
||||||
@ -52,3 +55,7 @@ aioSession = None
|
|||||||
qdes_lib_loaded = False
|
qdes_lib_loaded = False
|
||||||
use_cookie_pool = False
|
use_cookie_pool = False
|
||||||
running_ports = []
|
running_ports = []
|
||||||
|
use_proxy = False
|
||||||
|
http_proxy = ''
|
||||||
|
https_proxy = ''
|
||||||
|
log_files = []
|
||||||
|
13
deps/pyqdes/README.md
vendored
Normal file
13
deps/pyqdes/README.md
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
## pyqdes QRC解密库
|
||||||
|
|
||||||
|
构建:
|
||||||
|
```bash
|
||||||
|
python setup.py bdist_wheel
|
||||||
|
```
|
||||||
|
|
||||||
|
到dist目录pip安装那个whl文件
|
||||||
|
```bash
|
||||||
|
pip install xxx.whl
|
||||||
|
```
|
||||||
|
|
||||||
|
api服务器会自行识别
|
266
deps/pyqdes/des.cpp
vendored
Normal file
266
deps/pyqdes/des.cpp
vendored
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
/*********************************************************************
|
||||||
|
* Filename: des.cpp
|
||||||
|
* Author: Brad Conte (brad AT radconte.com)
|
||||||
|
* Copyright:
|
||||||
|
* Disclaimer: This code is presented "as is" without any guarantees.
|
||||||
|
* Details: Implementation of the DES encryption algorithm.
|
||||||
|
Modes of operation (such as CBC) are not included.
|
||||||
|
The formal NIST algorithm specification can be found here:
|
||||||
|
* http://csrc.nist.gov/publications/fips/fips46-3/fips46-3.pdf
|
||||||
|
*********************************************************************/
|
||||||
|
|
||||||
|
/*************************** HEADER FILES ***************************/
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <memory.h>
|
||||||
|
#include "des.h"
|
||||||
|
|
||||||
|
/****************************** MACROS ******************************/
|
||||||
|
// Obtain bit "b" from the left and shift it "c" places from the right
|
||||||
|
#define BITNUM(a, b, c) (((a[(b) / 32 * 4 + 3 - (b) % 32 / 8] >> (7 - (b % 8))) & 0x01) << (c))
|
||||||
|
#define BITNUMINTR(a, b, c) ((((a) >> (31 - (b))) & 0x00000001) << (c))
|
||||||
|
#define BITNUMINTL(a, b, c) ((((a) << (b)) & 0x80000000) >> (c))
|
||||||
|
|
||||||
|
// This macro converts a 6 bit block with the S-Box row defined as the first and last
|
||||||
|
// bits to a 6 bit block with the row defined by the first two bits.
|
||||||
|
#define SBOXBIT(a) (((a)&0x20) | (((a)&0x1f) >> 1) | (((a)&0x01) << 4))
|
||||||
|
|
||||||
|
/**************************** VARIABLES *****************************/
|
||||||
|
static const BYTE sbox1[64] = {
|
||||||
|
14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
|
||||||
|
0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
|
||||||
|
4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
|
||||||
|
15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13};
|
||||||
|
static const BYTE sbox2[64] = {
|
||||||
|
15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
|
||||||
|
3, 13, 4, 7, 15, 2, 8, 15, 12, 0, 1, 10, 6, 9, 11, 5,
|
||||||
|
0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
|
||||||
|
13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9};
|
||||||
|
static const BYTE sbox3[64] = {
|
||||||
|
10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
|
||||||
|
13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
|
||||||
|
13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
|
||||||
|
1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12};
|
||||||
|
static const BYTE sbox4[64] = {
|
||||||
|
7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
|
||||||
|
13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
|
||||||
|
10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
|
||||||
|
3, 15, 0, 6, 10, 10, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14};
|
||||||
|
static const BYTE sbox5[64] = {
|
||||||
|
2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
|
||||||
|
14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
|
||||||
|
4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
|
||||||
|
11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3};
|
||||||
|
static const BYTE sbox6[64] = {
|
||||||
|
12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
|
||||||
|
10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
|
||||||
|
9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
|
||||||
|
4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13};
|
||||||
|
static const BYTE sbox7[64] = {
|
||||||
|
4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
|
||||||
|
13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
|
||||||
|
1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
|
||||||
|
6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12};
|
||||||
|
static const BYTE sbox8[64] = {
|
||||||
|
13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
|
||||||
|
1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
|
||||||
|
7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
|
||||||
|
2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11};
|
||||||
|
|
||||||
|
/*********************** FUNCTION DEFINITIONS ***********************/
|
||||||
|
// Initial (Inv)Permutation step
|
||||||
|
void IP(WORD state[], const BYTE in[])
|
||||||
|
{
|
||||||
|
state[0] = BITNUM(in, 57, 31) | BITNUM(in, 49, 30) | BITNUM(in, 41, 29) | BITNUM(in, 33, 28) |
|
||||||
|
BITNUM(in, 25, 27) | BITNUM(in, 17, 26) | BITNUM(in, 9, 25) | BITNUM(in, 1, 24) |
|
||||||
|
BITNUM(in, 59, 23) | BITNUM(in, 51, 22) | BITNUM(in, 43, 21) | BITNUM(in, 35, 20) |
|
||||||
|
BITNUM(in, 27, 19) | BITNUM(in, 19, 18) | BITNUM(in, 11, 17) | BITNUM(in, 3, 16) |
|
||||||
|
BITNUM(in, 61, 15) | BITNUM(in, 53, 14) | BITNUM(in, 45, 13) | BITNUM(in, 37, 12) |
|
||||||
|
BITNUM(in, 29, 11) | BITNUM(in, 21, 10) | BITNUM(in, 13, 9) | BITNUM(in, 5, 8) |
|
||||||
|
BITNUM(in, 63, 7) | BITNUM(in, 55, 6) | BITNUM(in, 47, 5) | BITNUM(in, 39, 4) |
|
||||||
|
BITNUM(in, 31, 3) | BITNUM(in, 23, 2) | BITNUM(in, 15, 1) | BITNUM(in, 7, 0);
|
||||||
|
|
||||||
|
state[1] = BITNUM(in, 56, 31) | BITNUM(in, 48, 30) | BITNUM(in, 40, 29) | BITNUM(in, 32, 28) |
|
||||||
|
BITNUM(in, 24, 27) | BITNUM(in, 16, 26) | BITNUM(in, 8, 25) | BITNUM(in, 0, 24) |
|
||||||
|
BITNUM(in, 58, 23) | BITNUM(in, 50, 22) | BITNUM(in, 42, 21) | BITNUM(in, 34, 20) |
|
||||||
|
BITNUM(in, 26, 19) | BITNUM(in, 18, 18) | BITNUM(in, 10, 17) | BITNUM(in, 2, 16) |
|
||||||
|
BITNUM(in, 60, 15) | BITNUM(in, 52, 14) | BITNUM(in, 44, 13) | BITNUM(in, 36, 12) |
|
||||||
|
BITNUM(in, 28, 11) | BITNUM(in, 20, 10) | BITNUM(in, 12, 9) | BITNUM(in, 4, 8) |
|
||||||
|
BITNUM(in, 62, 7) | BITNUM(in, 54, 6) | BITNUM(in, 46, 5) | BITNUM(in, 38, 4) |
|
||||||
|
BITNUM(in, 30, 3) | BITNUM(in, 22, 2) | BITNUM(in, 14, 1) | BITNUM(in, 6, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InvIP(WORD state[], BYTE in[])
|
||||||
|
{
|
||||||
|
in[3] = BITNUMINTR(state[1], 7, 7) | BITNUMINTR(state[0], 7, 6) | BITNUMINTR(state[1], 15, 5) |
|
||||||
|
BITNUMINTR(state[0], 15, 4) | BITNUMINTR(state[1], 23, 3) | BITNUMINTR(state[0], 23, 2) |
|
||||||
|
BITNUMINTR(state[1], 31, 1) | BITNUMINTR(state[0], 31, 0);
|
||||||
|
|
||||||
|
in[2] = BITNUMINTR(state[1], 6, 7) | BITNUMINTR(state[0], 6, 6) | BITNUMINTR(state[1], 14, 5) |
|
||||||
|
BITNUMINTR(state[0], 14, 4) | BITNUMINTR(state[1], 22, 3) | BITNUMINTR(state[0], 22, 2) |
|
||||||
|
BITNUMINTR(state[1], 30, 1) | BITNUMINTR(state[0], 30, 0);
|
||||||
|
|
||||||
|
in[1] = BITNUMINTR(state[1], 5, 7) | BITNUMINTR(state[0], 5, 6) | BITNUMINTR(state[1], 13, 5) |
|
||||||
|
BITNUMINTR(state[0], 13, 4) | BITNUMINTR(state[1], 21, 3) | BITNUMINTR(state[0], 21, 2) |
|
||||||
|
BITNUMINTR(state[1], 29, 1) | BITNUMINTR(state[0], 29, 0);
|
||||||
|
|
||||||
|
in[0] = BITNUMINTR(state[1], 4, 7) | BITNUMINTR(state[0], 4, 6) | BITNUMINTR(state[1], 12, 5) |
|
||||||
|
BITNUMINTR(state[0], 12, 4) | BITNUMINTR(state[1], 20, 3) | BITNUMINTR(state[0], 20, 2) |
|
||||||
|
BITNUMINTR(state[1], 28, 1) | BITNUMINTR(state[0], 28, 0);
|
||||||
|
|
||||||
|
in[7] = BITNUMINTR(state[1], 3, 7) | BITNUMINTR(state[0], 3, 6) | BITNUMINTR(state[1], 11, 5) |
|
||||||
|
BITNUMINTR(state[0], 11, 4) | BITNUMINTR(state[1], 19, 3) | BITNUMINTR(state[0], 19, 2) |
|
||||||
|
BITNUMINTR(state[1], 27, 1) | BITNUMINTR(state[0], 27, 0);
|
||||||
|
|
||||||
|
in[6] = BITNUMINTR(state[1], 2, 7) | BITNUMINTR(state[0], 2, 6) | BITNUMINTR(state[1], 10, 5) |
|
||||||
|
BITNUMINTR(state[0], 10, 4) | BITNUMINTR(state[1], 18, 3) | BITNUMINTR(state[0], 18, 2) |
|
||||||
|
BITNUMINTR(state[1], 26, 1) | BITNUMINTR(state[0], 26, 0);
|
||||||
|
|
||||||
|
in[5] = BITNUMINTR(state[1], 1, 7) | BITNUMINTR(state[0], 1, 6) | BITNUMINTR(state[1], 9, 5) |
|
||||||
|
BITNUMINTR(state[0], 9, 4) | BITNUMINTR(state[1], 17, 3) | BITNUMINTR(state[0], 17, 2) |
|
||||||
|
BITNUMINTR(state[1], 25, 1) | BITNUMINTR(state[0], 25, 0);
|
||||||
|
|
||||||
|
in[4] = BITNUMINTR(state[1], 0, 7) | BITNUMINTR(state[0], 0, 6) | BITNUMINTR(state[1], 8, 5) |
|
||||||
|
BITNUMINTR(state[0], 8, 4) | BITNUMINTR(state[1], 16, 3) | BITNUMINTR(state[0], 16, 2) |
|
||||||
|
BITNUMINTR(state[1], 24, 1) | BITNUMINTR(state[0], 24, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
WORD f(WORD state, const BYTE key[])
|
||||||
|
{
|
||||||
|
BYTE lrgstate[6]; //,i;
|
||||||
|
WORD t1, t2;
|
||||||
|
|
||||||
|
// Expantion Permutation
|
||||||
|
t1 = BITNUMINTL(state, 31, 0) | ((state & 0xf0000000) >> 1) | BITNUMINTL(state, 4, 5) |
|
||||||
|
BITNUMINTL(state, 3, 6) | ((state & 0x0f000000) >> 3) | BITNUMINTL(state, 8, 11) |
|
||||||
|
BITNUMINTL(state, 7, 12) | ((state & 0x00f00000) >> 5) | BITNUMINTL(state, 12, 17) |
|
||||||
|
BITNUMINTL(state, 11, 18) | ((state & 0x000f0000) >> 7) | BITNUMINTL(state, 16, 23);
|
||||||
|
|
||||||
|
t2 = BITNUMINTL(state, 15, 0) | ((state & 0x0000f000) << 15) | BITNUMINTL(state, 20, 5) |
|
||||||
|
BITNUMINTL(state, 19, 6) | ((state & 0x00000f00) << 13) | BITNUMINTL(state, 24, 11) |
|
||||||
|
BITNUMINTL(state, 23, 12) | ((state & 0x000000f0) << 11) | BITNUMINTL(state, 28, 17) |
|
||||||
|
BITNUMINTL(state, 27, 18) | ((state & 0x0000000f) << 9) | BITNUMINTL(state, 0, 23);
|
||||||
|
|
||||||
|
lrgstate[0] = (t1 >> 24) & 0x000000ff;
|
||||||
|
lrgstate[1] = (t1 >> 16) & 0x000000ff;
|
||||||
|
lrgstate[2] = (t1 >> 8) & 0x000000ff;
|
||||||
|
lrgstate[3] = (t2 >> 24) & 0x000000ff;
|
||||||
|
lrgstate[4] = (t2 >> 16) & 0x000000ff;
|
||||||
|
lrgstate[5] = (t2 >> 8) & 0x000000ff;
|
||||||
|
|
||||||
|
// Key XOR
|
||||||
|
lrgstate[0] ^= key[0];
|
||||||
|
lrgstate[1] ^= key[1];
|
||||||
|
lrgstate[2] ^= key[2];
|
||||||
|
lrgstate[3] ^= key[3];
|
||||||
|
lrgstate[4] ^= key[4];
|
||||||
|
lrgstate[5] ^= key[5];
|
||||||
|
|
||||||
|
// S-Box Permutation
|
||||||
|
state = (sbox1[SBOXBIT(lrgstate[0] >> 2)] << 28) |
|
||||||
|
(sbox2[SBOXBIT(((lrgstate[0] & 0x03) << 4) | (lrgstate[1] >> 4))] << 24) |
|
||||||
|
(sbox3[SBOXBIT(((lrgstate[1] & 0x0f) << 2) | (lrgstate[2] >> 6))] << 20) |
|
||||||
|
(sbox4[SBOXBIT(lrgstate[2] & 0x3f)] << 16) |
|
||||||
|
(sbox5[SBOXBIT(lrgstate[3] >> 2)] << 12) |
|
||||||
|
(sbox6[SBOXBIT(((lrgstate[3] & 0x03) << 4) | (lrgstate[4] >> 4))] << 8) |
|
||||||
|
(sbox7[SBOXBIT(((lrgstate[4] & 0x0f) << 2) | (lrgstate[5] >> 6))] << 4) |
|
||||||
|
sbox8[SBOXBIT(lrgstate[5] & 0x3f)];
|
||||||
|
|
||||||
|
// P-Box Permutation
|
||||||
|
state = BITNUMINTL(state, 15, 0) | BITNUMINTL(state, 6, 1) | BITNUMINTL(state, 19, 2) |
|
||||||
|
BITNUMINTL(state, 20, 3) | BITNUMINTL(state, 28, 4) | BITNUMINTL(state, 11, 5) |
|
||||||
|
BITNUMINTL(state, 27, 6) | BITNUMINTL(state, 16, 7) | BITNUMINTL(state, 0, 8) |
|
||||||
|
BITNUMINTL(state, 14, 9) | BITNUMINTL(state, 22, 10) | BITNUMINTL(state, 25, 11) |
|
||||||
|
BITNUMINTL(state, 4, 12) | BITNUMINTL(state, 17, 13) | BITNUMINTL(state, 30, 14) |
|
||||||
|
BITNUMINTL(state, 9, 15) | BITNUMINTL(state, 1, 16) | BITNUMINTL(state, 7, 17) |
|
||||||
|
BITNUMINTL(state, 23, 18) | BITNUMINTL(state, 13, 19) | BITNUMINTL(state, 31, 20) |
|
||||||
|
BITNUMINTL(state, 26, 21) | BITNUMINTL(state, 2, 22) | BITNUMINTL(state, 8, 23) |
|
||||||
|
BITNUMINTL(state, 18, 24) | BITNUMINTL(state, 12, 25) | BITNUMINTL(state, 29, 26) |
|
||||||
|
BITNUMINTL(state, 5, 27) | BITNUMINTL(state, 21, 28) | BITNUMINTL(state, 10, 29) |
|
||||||
|
BITNUMINTL(state, 3, 30) | BITNUMINTL(state, 24, 31);
|
||||||
|
|
||||||
|
// Return the final state value
|
||||||
|
return (state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void des_key_setup(const BYTE key[], BYTE schedule[][6], DES_MODE mode)
|
||||||
|
{
|
||||||
|
WORD i, j, to_gen, C, D;
|
||||||
|
const WORD key_rnd_shift[16] = {1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1};
|
||||||
|
const WORD key_perm_c[28] = {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};
|
||||||
|
const WORD key_perm_d[28] = {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};
|
||||||
|
const WORD key_compression[48] = {13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9,
|
||||||
|
22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1,
|
||||||
|
40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47,
|
||||||
|
43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31};
|
||||||
|
|
||||||
|
// Permutated Choice #1 (copy the key in, ignoring parity bits).
|
||||||
|
for (i = 0, j = 31, C = 0; i < 28; ++i, --j)
|
||||||
|
C |= BITNUM(key, key_perm_c[i], j);
|
||||||
|
for (i = 0, j = 31, D = 0; i < 28; ++i, --j)
|
||||||
|
D |= BITNUM(key, key_perm_d[i], j);
|
||||||
|
|
||||||
|
// Generate the 16 subkeys.
|
||||||
|
for (i = 0; i < 16; ++i)
|
||||||
|
{
|
||||||
|
C = ((C << key_rnd_shift[i]) | (C >> (28 - key_rnd_shift[i]))) & 0xfffffff0;
|
||||||
|
D = ((D << key_rnd_shift[i]) | (D >> (28 - key_rnd_shift[i]))) & 0xfffffff0;
|
||||||
|
|
||||||
|
// Decryption subkeys are reverse order of encryption subkeys so
|
||||||
|
// generate them in reverse if the key schedule is for decryption useage.
|
||||||
|
if (mode == DES_DECRYPT)
|
||||||
|
to_gen = 15 - i;
|
||||||
|
else /*(if mode == DES_ENCRYPT)*/
|
||||||
|
to_gen = i;
|
||||||
|
// Initialize the array
|
||||||
|
for (j = 0; j < 6; ++j)
|
||||||
|
schedule[to_gen][j] = 0;
|
||||||
|
for (j = 0; j < 24; ++j)
|
||||||
|
schedule[to_gen][j / 8] |= BITNUMINTR(C, key_compression[j], 7 - (j % 8));
|
||||||
|
for (; j < 48; ++j)
|
||||||
|
schedule[to_gen][j / 8] |= BITNUMINTR(D, key_compression[j] - 27, 7 - (j % 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void des_crypt(const BYTE in[], BYTE out[], const BYTE key[][6])
|
||||||
|
{
|
||||||
|
WORD state[2], idx, t;
|
||||||
|
|
||||||
|
IP(state, in);
|
||||||
|
|
||||||
|
for (idx = 0; idx < 15; ++idx)
|
||||||
|
{
|
||||||
|
t = state[1];
|
||||||
|
state[1] = f(state[1], key[idx]) ^ state[0];
|
||||||
|
state[0] = t;
|
||||||
|
}
|
||||||
|
// Perform the final loop manually as it doesn't switch sides
|
||||||
|
state[0] = f(state[1], key[15]) ^ state[0];
|
||||||
|
|
||||||
|
InvIP(state, out);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
void three_des_key_setup(const BYTE key[], BYTE schedule[][16][6], DES_MODE mode)
|
||||||
|
{
|
||||||
|
if (mode == DES_ENCRYPT)
|
||||||
|
{
|
||||||
|
des_key_setup(&key[0], schedule[0], mode);
|
||||||
|
des_key_setup(&key[8], schedule[1], DES_DECRYPT);
|
||||||
|
des_key_setup(&key[16], schedule[2], mode);
|
||||||
|
}
|
||||||
|
else // if (mode == DES_DECRYPT
|
||||||
|
{
|
||||||
|
des_key_setup(&key[16], schedule[0], mode);
|
||||||
|
des_key_setup(&key[8], schedule[1], DES_ENCRYPT);
|
||||||
|
des_key_setup(&key[0], schedule[2], mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void three_des_crypt(const BYTE in[], BYTE out[], const BYTE key[][16][6])
|
||||||
|
{
|
||||||
|
des_crypt(in, out, key[0]);
|
||||||
|
des_crypt(out, out, key[1]);
|
||||||
|
des_crypt(out, out, key[2]);
|
||||||
|
}
|
||||||
|
*/
|
26
deps/pyqdes/des.h
vendored
Normal file
26
deps/pyqdes/des.h
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#ifndef DES_H
|
||||||
|
#define DES_H
|
||||||
|
|
||||||
|
/*************************** HEADER FILES ***************************/
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/****************************** MACROS ******************************/
|
||||||
|
#define DES_BLOCK_SIZE 8 // DES operates on 8 bytes at a time
|
||||||
|
|
||||||
|
/**************************** DATA TYPES ****************************/
|
||||||
|
typedef unsigned char BYTE; // 8-bit byte
|
||||||
|
typedef unsigned int WORD; // 32-bit word, change to "long" for 16-bit machines
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
DES_ENCRYPT,
|
||||||
|
DES_DECRYPT
|
||||||
|
} DES_MODE;
|
||||||
|
|
||||||
|
/*********************** FUNCTION DECLARATIONS **********************/
|
||||||
|
void des_key_setup(const BYTE key[], BYTE schedule[][6], DES_MODE mode);
|
||||||
|
void des_crypt(const BYTE in[], BYTE out[], const BYTE key[][6]);
|
||||||
|
|
||||||
|
// void three_des_key_setup(const BYTE key[], BYTE schedule[][16][6], DES_MODE mode);
|
||||||
|
// void three_des_crypt(const BYTE in[], BYTE out[], const BYTE key[][16][6]);
|
||||||
|
|
||||||
|
#endif // DES_H
|
56
deps/pyqdes/main.cpp
vendored
Normal file
56
deps/pyqdes/main.cpp
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#include "des.cpp"
|
||||||
|
#include <pybind11/pybind11.h>
|
||||||
|
|
||||||
|
unsigned char KEY1[] = "!@#)(NHLiuy*$%^&";
|
||||||
|
unsigned char KEY2[] = "123ZXC!@#)(*$%^&";
|
||||||
|
unsigned char KEY3[] = "!@#)(*$%^&abcDEF";
|
||||||
|
|
||||||
|
int func_des(unsigned char *buff, unsigned char *key, int len)
|
||||||
|
{
|
||||||
|
BYTE schedule[16][6];
|
||||||
|
des_key_setup(key, schedule, DES_ENCRYPT);
|
||||||
|
for (int i = 0; i < len; i += 8)
|
||||||
|
des_crypt(buff + i, buff + i, schedule);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int func_ddes(unsigned char *buff, unsigned char *key, int len)
|
||||||
|
{
|
||||||
|
BYTE schedule[16][6];
|
||||||
|
des_key_setup(key, schedule, DES_DECRYPT);
|
||||||
|
for (int i = 0; i < len; i += 8)
|
||||||
|
des_crypt(buff + i, buff + i, schedule);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyricDecode_(unsigned char *content, int len)
|
||||||
|
{
|
||||||
|
func_ddes(content, KEY1, len);
|
||||||
|
func_des(content, KEY2, len);
|
||||||
|
func_ddes(content, KEY3, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace py = pybind11;
|
||||||
|
|
||||||
|
// 修改 LyricDecode 函数,接受和返回字节数组
|
||||||
|
py::bytes LyricDecode(py::bytes input)
|
||||||
|
{
|
||||||
|
// 获取输入字节数组的指针和长度
|
||||||
|
const char *input_ptr = PyBytes_AsString(input.ptr());
|
||||||
|
Py_ssize_t input_len = PyBytes_Size(input.ptr());
|
||||||
|
|
||||||
|
// 复制输入数据以便修改
|
||||||
|
std::vector<unsigned char> data(input_ptr, input_ptr + input_len);
|
||||||
|
|
||||||
|
// 调用 LyricDecode 函数进行解密
|
||||||
|
LyricDecode_(data.data(), data.size());
|
||||||
|
|
||||||
|
// 创建输出字节数组
|
||||||
|
py::bytes output(reinterpret_cast<const char *>(data.data()), data.size());
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
PYBIND11_MODULE(qdes, m) {
|
||||||
|
m.def("LyricDecode", &LyricDecode, "Decrypt a string");
|
||||||
|
}
|
16
deps/pyqdes/setup.py
vendored
Normal file
16
deps/pyqdes/setup.py
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from setuptools import setup, Extension
|
||||||
|
import pybind11
|
||||||
|
|
||||||
|
module = Extension(
|
||||||
|
'qdes',
|
||||||
|
sources=['main.cpp'],
|
||||||
|
include_dirs=[pybind11.get_include(), pybind11.get_include(user=True)],
|
||||||
|
extra_compile_args=["-std=c++11"], # 添加编译器选项
|
||||||
|
)
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='qdes',
|
||||||
|
version='1.0',
|
||||||
|
description='QDES encryption/decryption module',
|
||||||
|
ext_modules=[module],
|
||||||
|
)
|
@ -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:
|
|
||||||
|
|
288
lx-music-source-example.js
Normal file
288
lx-music-source-example.js
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
/*!
|
||||||
|
* @name 替换为你的音乐源名称
|
||||||
|
* @description 替换为你的音乐源介绍
|
||||||
|
* @version v2.0.1
|
||||||
|
* @author Folltoshe & helloplhm-qwq & lerdb
|
||||||
|
* @repository https://github.com/lxmusics/lx-music-api-server
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 是否开启开发模式
|
||||||
|
const DEV_ENABLE = true
|
||||||
|
// 是否开启更新提醒
|
||||||
|
const UPDATE_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
|
||||||
|
|
||||||
|
// MD5值,用来检查更新
|
||||||
|
const SCRIPT_MD5 = ''
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 checkUpdate = async () => {
|
||||||
|
const request = await httpFetch(`${API_URL}/script?key=${API_KEY}&checkUpdate=${SCRIPT_MD5}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': `${env ? `lx-music-${env}/${version}` : `lx-music-request/${version}`}`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { body } = request
|
||||||
|
|
||||||
|
if (!body || body.code !== 0) console.log('checkUpdate failed')
|
||||||
|
else {
|
||||||
|
console.log('checkUpdate success')
|
||||||
|
if (body.data != null) {
|
||||||
|
globalThis.lx.send(lx.EVENT_NAMES.updateAlert, { log: body.data.updateMsg, updateUrl: body.data.updateUrl })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成歌曲信息
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查更新
|
||||||
|
if (UPDATE_ENABLE) checkUpdate()
|
||||||
|
// 向 LX Music 发送初始化成功事件
|
||||||
|
send(EVENT_NAMES.inited, { status: true, openDevTools: DEV_ENABLE, sources: musicSources })
|
166
main.py
166
main.py
@ -9,14 +9,25 @@
|
|||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# This file is part of the "lx-music-api-server" project.
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
import time
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
import threading
|
||||||
|
import ujson as json
|
||||||
|
from aiohttp.web import Response, FileResponse, StreamResponse, Application
|
||||||
|
from io import TextIOWrapper
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
from common.utils import createBase64Decode
|
if sys.version_info < (3, 6):
|
||||||
|
|
||||||
if ((sys.version_info.major == 3 and sys.version_info.minor < 6) or sys.version_info.major == 2):
|
|
||||||
print('Python版本过低,请使用Python 3.6+ ')
|
print('Python版本过低,请使用Python 3.6+ ')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# fix: module not found: common/modules
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from common import utils
|
||||||
from common import config, localMusic
|
from common import config, localMusic
|
||||||
from common import lxsecurity
|
from common import lxsecurity
|
||||||
from common import log
|
from common import log
|
||||||
@ -24,52 +35,46 @@ from common import Httpx
|
|||||||
from common import variable
|
from common import variable
|
||||||
from common import scheduler
|
from common import scheduler
|
||||||
from common import lx_script
|
from common import lx_script
|
||||||
from aiohttp.web import Response, FileResponse, StreamResponse
|
from common import gcsp
|
||||||
import ujson as json
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
import modules
|
import modules
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
|
|
||||||
def handleResult(dic, status = 200) -> Response:
|
def handleResult(dic, status=200) -> Response:
|
||||||
if (not isinstance(dic, dict)):
|
if (not isinstance(dic, dict)):
|
||||||
dic = {
|
dic = {
|
||||||
'code': 0,
|
'code': 0,
|
||||||
'msg': 'success',
|
'msg': 'success',
|
||||||
'data': dic
|
'data': dic
|
||||||
}
|
}
|
||||||
return Response(body = json.dumps(dic, indent=2, ensure_ascii=False), content_type='application/json', status = status)
|
return Response(body=json.dumps(dic, indent=2, ensure_ascii=False), content_type='application/json', status=status)
|
||||||
|
|
||||||
|
|
||||||
logger = log.log("main")
|
logger = log.log("main")
|
||||||
aiologger = log.log('aiohttp_web')
|
aiologger = log.log('aiohttp_web')
|
||||||
|
|
||||||
stopEvent = None
|
stopEvent = None
|
||||||
if (sys.version_info.minor < 8 and sys.version_info.major == 3):
|
if sys.version_info < (3, 8):
|
||||||
logger.warning('您使用的Python版本已经停止更新,不建议继续使用')
|
logger.warning('您使用的Python版本已经停止更新,不建议继续使用')
|
||||||
import concurrent
|
import concurrent
|
||||||
stopEvent = concurrent.futures._base.CancelledError
|
stopEvent = concurrent.futures._base.CancelledError
|
||||||
else:
|
else:
|
||||||
stopEvent = asyncio.exceptions.CancelledError
|
stopEvent = asyncio.exceptions.CancelledError
|
||||||
|
|
||||||
|
|
||||||
def start_checkcn_thread() -> None:
|
def start_checkcn_thread() -> None:
|
||||||
threading.Thread(target=Httpx.checkcn).start()
|
threading.Thread(target=Httpx.checkcn).start()
|
||||||
|
|
||||||
# check request info before start
|
# check request info before start
|
||||||
|
|
||||||
|
|
||||||
async def handle_before_request(app, handler):
|
async def handle_before_request(app, handler):
|
||||||
async def handle_request(request):
|
async def handle_request(request):
|
||||||
try:
|
try:
|
||||||
if (config.read_config('common.reverse_proxy.allow_proxy')):
|
if config.read_config("common.reverse_proxy.allow_proxy") and request.headers.get(
|
||||||
if (request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))):
|
config.read_config("common.reverse_proxy.real_ip_header")):
|
||||||
# proxy header
|
if not (config.read_config("common.reverse_proxy.allow_public_ip") or utils.is_local_ip(request.remote)):
|
||||||
if (request.remote in config.read_config('common.reverse_proxy.proxy_whitelist_remote')):
|
return handleResult({"code": 1, "msg": "不允许的公网ip转发", "data": None}, 403)
|
||||||
request.remote_addr = request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))
|
# proxy header
|
||||||
else:
|
request.remote_addr = request.headers.get(config.read_config("common.reverse_proxy.real_ip_header"))
|
||||||
return handleResult({"code": 1, "msg": "反代客户端远程地址不在反代ip白名单中", "data": None}, 403)
|
|
||||||
else:
|
|
||||||
request.remote_addr = request.remote
|
|
||||||
else:
|
else:
|
||||||
request.remote_addr = request.remote
|
request.remote_addr = request.remote
|
||||||
# check ip
|
# check ip
|
||||||
@ -80,13 +85,13 @@ async def handle_before_request(app, handler):
|
|||||||
(time.time() - config.getRequestTime('global'))
|
(time.time() - config.getRequestTime('global'))
|
||||||
<
|
<
|
||||||
(config.read_config("security.rate_limit.global"))
|
(config.read_config("security.rate_limit.global"))
|
||||||
):
|
):
|
||||||
return handleResult({"code": 5, "msg": "全局限速", "data": None}, 429)
|
return handleResult({"code": 5, "msg": "全局限速", "data": None}, 429)
|
||||||
if (
|
if (
|
||||||
(time.time() - config.getRequestTime(request.remote_addr))
|
(time.time() - config.getRequestTime(request.remote_addr))
|
||||||
<
|
<
|
||||||
(config.read_config("security.rate_limit.ip"))
|
(config.read_config("security.rate_limit.ip"))
|
||||||
):
|
):
|
||||||
return handleResult({"code": 5, "msg": "IP限速", "data": None}, 429)
|
return handleResult({"code": 5, "msg": "IP限速", "data": None}, 429)
|
||||||
# update request time
|
# update request time
|
||||||
config.updateRequestTime('global')
|
config.updateRequestTime('global')
|
||||||
@ -95,27 +100,32 @@ async def handle_before_request(app, handler):
|
|||||||
if (config.read_config("security.allowed_host.enable")):
|
if (config.read_config("security.allowed_host.enable")):
|
||||||
if request.host.split(":")[0] not in config.read_config("security.allowed_host.list"):
|
if request.host.split(":")[0] not in config.read_config("security.allowed_host.list"):
|
||||||
if config.read_config("security.allowed_host.blacklist.enable"):
|
if config.read_config("security.allowed_host.blacklist.enable"):
|
||||||
config.ban_ip(request.remote_addr, int(config.read_config("security.allowed_host.blacklist.length")))
|
config.ban_ip(request.remote_addr, int(
|
||||||
|
config.read_config("security.allowed_host.blacklist.length")))
|
||||||
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
||||||
|
|
||||||
resp = await handler(request)
|
resp = await handler(request)
|
||||||
if (isinstance(resp, (str, list, dict))):
|
if (isinstance(resp, (str, list, dict))):
|
||||||
resp = handleResult(resp)
|
resp = handleResult(resp)
|
||||||
elif (isinstance(resp, tuple) and len(resp) == 2): # flask like response
|
elif (isinstance(resp, tuple) and len(resp) == 2): # flask like response
|
||||||
body, status = resp
|
body, status = resp
|
||||||
if (isinstance(body, (str, list, dict))):
|
if (isinstance(body, (str, list, dict))):
|
||||||
resp = handleResult(body, status)
|
resp = handleResult(body, status)
|
||||||
else:
|
else:
|
||||||
resp = Response(body = str(body), content_type='text/plain', status = status)
|
resp = Response(
|
||||||
|
body=str(body), content_type='text/plain', status=status)
|
||||||
elif (not isinstance(resp, (Response, FileResponse, StreamResponse))):
|
elif (not isinstance(resp, (Response, FileResponse, StreamResponse))):
|
||||||
resp = Response(body = str(resp), content_type='text/plain', status = 200)
|
resp = Response(
|
||||||
aiologger.info(f'{request.remote_addr + ("" if (request.remote == request.remote_addr) else f"|proxy@{request.remote}")} - {request.method} "{request.path}", {resp.status}')
|
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
|
return resp
|
||||||
except:
|
except:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"code": 4, "msg": "内部服务器错误", "data": None}
|
return {"code": 4, "msg": "内部服务器错误", "data": None}
|
||||||
return handle_request
|
return handle_request
|
||||||
|
|
||||||
|
|
||||||
async def main(request):
|
async def main(request):
|
||||||
return handleResult({"code": 0, "msg": "success", "data": None})
|
return handleResult({"code": 0, "msg": "success", "data": None})
|
||||||
|
|
||||||
@ -126,7 +136,7 @@ async def handle(request):
|
|||||||
songId = request.match_info.get('songId')
|
songId = request.match_info.get('songId')
|
||||||
quality = request.match_info.get('quality')
|
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 (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")) != config.read_config("security.key.value"):
|
if (request.headers.get("X-Request-Key")) not in config.read_config("security.key.values"):
|
||||||
if (config.read_config("security.key.ban")):
|
if (config.read_config("security.key.ban")):
|
||||||
config.ban_ip(request.remote_addr)
|
config.ban_ip(request.remote_addr)
|
||||||
return handleResult({"code": 1, "msg": "key验证失败", "data": None}, 403)
|
return handleResult({"code": 1, "msg": "key验证失败", "data": None}, 403)
|
||||||
@ -140,6 +150,14 @@ async def handle(request):
|
|||||||
try:
|
try:
|
||||||
query = dict(request.query)
|
query = dict(request.query)
|
||||||
if (method in dir(modules)):
|
if (method in dir(modules)):
|
||||||
|
source_enable = config.read_config(f'module.{source}.enable')
|
||||||
|
if not source_enable:
|
||||||
|
return handleResult({
|
||||||
|
'code': 4,
|
||||||
|
'msg': '此平台已停止服务',
|
||||||
|
'data': None,
|
||||||
|
"Your IP": request.remote_addr
|
||||||
|
}, 404)
|
||||||
return handleResult(await getattr(modules, method)(source, songId, quality, query))
|
return handleResult(await getattr(modules, method)(source, songId, quality, query))
|
||||||
else:
|
else:
|
||||||
return handleResult(await modules.other(method, source, songId, quality, query))
|
return handleResult(await modules.other(method, source, songId, quality, query))
|
||||||
@ -147,18 +165,22 @@ async def handle(request):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return handleResult({'code': 4, 'msg': '内部服务器错误', 'data': None}, 500)
|
return handleResult({'code': 4, 'msg': '内部服务器错误', 'data': None}, 500)
|
||||||
|
|
||||||
|
|
||||||
async def handle_404(request):
|
async def handle_404(request):
|
||||||
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
||||||
|
|
||||||
|
|
||||||
async def handle_local(request):
|
async def handle_local(request):
|
||||||
try:
|
try:
|
||||||
query = dict(request.query)
|
query = dict(request.query)
|
||||||
data = query.get('q')
|
data = query.get('q')
|
||||||
data = createBase64Decode(data.replace('-', '+').replace('_', '/'))
|
data = utils.createBase64Decode(
|
||||||
|
data.replace('-', '+').replace('_', '/'))
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
t = request.match_info.get('type')
|
t = request.match_info.get('type')
|
||||||
data['t'] = t
|
data['t'] = t
|
||||||
except:
|
except:
|
||||||
|
logger.info(traceback.format_exc())
|
||||||
return handleResult({'code': 6, 'msg': '请求参数有错', 'data': None}, 404)
|
return handleResult({'code': 6, 'msg': '请求参数有错', 'data': None}, 404)
|
||||||
if (data['t'] == 'u'):
|
if (data['t'] == 'u'):
|
||||||
if (data['p'] in list(localMusic.map.keys())):
|
if (data['p'] in list(localMusic.map.keys())):
|
||||||
@ -192,7 +214,9 @@ async def handle_local(request):
|
|||||||
'data': localMusic.checkLocalMusic(data['p'])
|
'data': localMusic.checkLocalMusic(data['p'])
|
||||||
}
|
}
|
||||||
|
|
||||||
app = aiohttp.web.Application(middlewares=[handle_before_request])
|
app = Application(middlewares=[handle_before_request])
|
||||||
|
utils.setGlobal(app, "app")
|
||||||
|
|
||||||
# mainpage
|
# mainpage
|
||||||
app.router.add_get('/', main)
|
app.router.add_get('/', main)
|
||||||
|
|
||||||
@ -204,31 +228,36 @@ app.router.add_get('/local/{type}', handle_local)
|
|||||||
if (config.read_config('common.allow_download_script')):
|
if (config.read_config('common.allow_download_script')):
|
||||||
app.router.add_get('/script', lx_script.generate_script_response)
|
app.router.add_get('/script', lx_script.generate_script_response)
|
||||||
|
|
||||||
|
if (config.read_config('module.gcsp.enable')):
|
||||||
|
app.router.add_route('*', config.read_config('module.gcsp.path'), gcsp.handle_request)
|
||||||
|
|
||||||
# 404
|
# 404
|
||||||
app.router.add_route('*', '/{tail:.*}', handle_404)
|
app.router.add_route('*', '/{tail:.*}', handle_404)
|
||||||
|
|
||||||
async def run_app():
|
|
||||||
|
async def run_app_host(host):
|
||||||
retries = 0
|
retries = 0
|
||||||
while True:
|
while True:
|
||||||
if (retries > 4):
|
if (retries > 4):
|
||||||
logger.warning("重试次数已达上限,但仍有部分端口未能完成监听,已自动进行忽略")
|
logger.warning("重试次数已达上限,但仍有部分端口未能完成监听,已自动进行忽略")
|
||||||
return
|
break
|
||||||
try:
|
try:
|
||||||
host = config.read_config('common.host')
|
ports = [int(port)
|
||||||
ports = [int(port) for port in config.read_config('common.ports')]
|
for port in config.read_config('common.ports')]
|
||||||
ssl_ports = [int(port) for port in config.read_config('common.ssl_info.ssl_ports')]
|
ssl_ports = [int(port) for port in config.read_config(
|
||||||
|
'common.ssl_info.ssl_ports')]
|
||||||
final_ssl_ports = []
|
final_ssl_ports = []
|
||||||
final_ports = []
|
final_ports = []
|
||||||
for p in ports:
|
for p in ports:
|
||||||
if (p not in ssl_ports and p not in variable.running_ports):
|
if (p not in ssl_ports and f'{host}_{p}' not in variable.running_ports):
|
||||||
final_ports.append(p)
|
final_ports.append(p)
|
||||||
else:
|
else:
|
||||||
if (p not in variable.running_ports):
|
if (p not in variable.running_ports):
|
||||||
final_ssl_ports.append(p)
|
final_ssl_ports.append(p)
|
||||||
# 读取证书和私钥路径
|
# 读取证书和私钥路径
|
||||||
cert_path = config.read_config('common.ssl_info.path.cert')
|
cert_path = config.read_config('common.ssl_info.path.cert')
|
||||||
privkey_path = config.read_config('common.ssl_info.path.privkey')
|
privkey_path = config.read_config(
|
||||||
|
'common.ssl_info.path.privkey')
|
||||||
|
|
||||||
# 创建 HTTP AppRunner
|
# 创建 HTTP AppRunner
|
||||||
http_runner = aiohttp.web.AppRunner(app)
|
http_runner = aiohttp.web.AppRunner(app)
|
||||||
@ -237,16 +266,21 @@ async def run_app():
|
|||||||
# 启动 HTTP 端口监听
|
# 启动 HTTP 端口监听
|
||||||
for port in final_ports:
|
for port in final_ports:
|
||||||
if (port not in variable.running_ports):
|
if (port not in variable.running_ports):
|
||||||
http_site = aiohttp.web.TCPSite(http_runner, host, port)
|
http_site = aiohttp.web.TCPSite(
|
||||||
|
http_runner, host, port)
|
||||||
await http_site.start()
|
await http_site.start()
|
||||||
variable.running_ports.append(port)
|
variable.running_ports.append(f'{host}_{port}')
|
||||||
logger.info(f"监听 -> http://{host}:{port}")
|
logger.info(f"""监听 -> http://{
|
||||||
|
host if (':' not in host)
|
||||||
|
else '[' + host + ']'
|
||||||
|
}:{port}""")
|
||||||
|
|
||||||
if (config.read_config("common.ssl_info.enable") and final_ssl_ports != []):
|
if (config.read_config("common.ssl_info.enable") and final_ssl_ports != []):
|
||||||
if (os.path.exists(cert_path) and os.path.exists(privkey_path)):
|
if (os.path.exists(cert_path) and os.path.exists(privkey_path)):
|
||||||
import ssl
|
import ssl
|
||||||
# 创建 SSL 上下文,加载配置文件中指定的证书和私钥
|
# 创建 SSL 上下文,加载配置文件中指定的证书和私钥
|
||||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
ssl_context = ssl.create_default_context(
|
||||||
|
ssl.Purpose.CLIENT_AUTH)
|
||||||
ssl_context.load_cert_chain(cert_path, privkey_path)
|
ssl_context.load_cert_chain(cert_path, privkey_path)
|
||||||
|
|
||||||
# 创建 HTTPS AppRunner
|
# 创建 HTTPS AppRunner
|
||||||
@ -256,21 +290,30 @@ async def run_app():
|
|||||||
# 启动 HTTPS 端口监听
|
# 启动 HTTPS 端口监听
|
||||||
for port in ssl_ports:
|
for port in ssl_ports:
|
||||||
if (port not in variable.running_ports):
|
if (port not in variable.running_ports):
|
||||||
https_site = aiohttp.web.TCPSite(https_runner, host, port, ssl_context=ssl_context)
|
https_site = aiohttp.web.TCPSite(
|
||||||
|
https_runner, host, port, ssl_context=ssl_context)
|
||||||
await https_site.start()
|
await https_site.start()
|
||||||
variable.running_ports.append(port)
|
variable.running_ports.append(f'{host}_{port}')
|
||||||
logger.info(f"监听 -> https://{host}:{port}")
|
logger.info(f"""监听 -> https://{
|
||||||
|
host if (':' not in host)
|
||||||
return
|
else '[' + host + ']'
|
||||||
|
}:{port}""")
|
||||||
|
logger.debug(f"HOST({host}) 已完成监听")
|
||||||
|
break
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if str(e).startswith("[Errno 98]"):
|
if (str(e).startswith("[Errno 98]") or str(e).startswith('[Errno 10048]')):
|
||||||
logger.error("端口已被占用,请检查\n" + str(e))
|
logger.error("端口已被占用,请检查\n" + str(e))
|
||||||
logger.info('服务器将在10s后再次尝试启动...')
|
logger.info('服务器将在10s后再次尝试启动...')
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
logger.info('重新尝试启动...')
|
logger.info('重新尝试启动...')
|
||||||
retries += 1
|
retries += 1
|
||||||
else:
|
else:
|
||||||
raise
|
logger.error("未知错误,请检查\n" + traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
async def run_app():
|
||||||
|
for host in config.read_config('common.hosts'):
|
||||||
|
await run_app_host(host)
|
||||||
|
|
||||||
|
|
||||||
async def initMain():
|
async def initMain():
|
||||||
@ -304,5 +347,20 @@ if __name__ == "__main__":
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
except:
|
except:
|
||||||
logger.error('初始化出错,请检查日志')
|
logger.critical('初始化出错,请检查日志')
|
||||||
logger.error(traceback.format_exc())
|
logger.critical(traceback.format_exc())
|
||||||
|
with open('dumprecord_{}.txt'.format(int(time.time())), 'w', encoding='utf-8') as f:
|
||||||
|
f.write(traceback.format_exc())
|
||||||
|
e = '\n\nGlobal variable object:\n\n'
|
||||||
|
for k in dir(variable):
|
||||||
|
e += (k + ' = ' + str(getattr(variable, k)) + '\n') if (not k.startswith('_')) else ''
|
||||||
|
f.write(e)
|
||||||
|
e = '\n\nsys.modules:\n\n'
|
||||||
|
for k in sys.modules:
|
||||||
|
e += (k + ' = ' + str(sys.modules[k]) + '\n') if (not k.startswith('_')) else ''
|
||||||
|
f.write(e)
|
||||||
|
logger.critical('dumprecord_{}.txt 已保存至当前目录'.format(int(time.time())))
|
||||||
|
finally:
|
||||||
|
for f in variable.log_files:
|
||||||
|
if (f and isinstance(f, TextIOWrapper)):
|
||||||
|
f.close()
|
||||||
|
@ -11,6 +11,7 @@ from common.exceptions import FailedException
|
|||||||
from common.utils import require
|
from common.utils import require
|
||||||
from common import log
|
from common import log
|
||||||
from common import config
|
from common import config
|
||||||
|
|
||||||
# 从.引入的包并没有在代码中直接使用,但是是用require在请求时进行引入的,不要动
|
# 从.引入的包并没有在代码中直接使用,但是是用require在请求时进行引入的,不要动
|
||||||
from . import kw
|
from . import kw
|
||||||
from . import mg
|
from . import mg
|
||||||
@ -20,194 +21,195 @@ from . import wy
|
|||||||
import traceback
|
import traceback
|
||||||
import time
|
import time
|
||||||
|
|
||||||
logger = log.log('api_handler')
|
logger = log.log("api_handler")
|
||||||
|
|
||||||
sourceExpirationTime = {
|
sourceExpirationTime = {
|
||||||
'tx': {
|
"tx": {
|
||||||
"expire": True,
|
"expire": True,
|
||||||
"time": 80400, # 不知道tx为什么要取一个这么不对劲的数字当过期时长
|
"time": 80400, # 不知道tx为什么要取一个这么不对劲的数字当过期时长
|
||||||
},
|
},
|
||||||
'kg': {
|
"kg": {
|
||||||
"expire": True,
|
"expire": True,
|
||||||
"time": 24 * 60 * 60, # 24 hours
|
"time": 24 * 60 * 60, # 24 hours
|
||||||
},
|
},
|
||||||
'kw': {
|
"kw": {"expire": True, "time": 60 * 60}, # 60 minutes
|
||||||
"expire": True,
|
"wy": {
|
||||||
"time": 60 * 60 # 60 minutes
|
|
||||||
},
|
|
||||||
'wy': {
|
|
||||||
"expire": True,
|
"expire": True,
|
||||||
"time": 20 * 60, # 20 minutes
|
"time": 20 * 60, # 20 minutes
|
||||||
},
|
},
|
||||||
'mg': {
|
"mg": {
|
||||||
"expire": False,
|
"expire": False,
|
||||||
"time": 0,
|
"time": 0,
|
||||||
}
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def url(source, songId, quality, query):
|
async def url(source, songId, quality, query={}):
|
||||||
if (not quality):
|
if not quality:
|
||||||
return {
|
return {
|
||||||
'code': 2,
|
"code": 2,
|
||||||
'msg': '需要参数"quality"',
|
"msg": '需要参数"quality"',
|
||||||
'data': None,
|
"data": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source == "kg"):
|
if source == "kg":
|
||||||
songId = songId.lower()
|
songId = songId.lower()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cache = config.getCache('urls', f'{source}_{songId}_{quality}')
|
cache = config.getCache("urls", f"{source}_{songId}_{quality}")
|
||||||
if cache:
|
if cache:
|
||||||
logger.debug(f'使用缓存的{source}_{songId}_{quality}数据,URL:{cache["url"]}')
|
logger.debug(f'使用缓存的{source}_{songId}_{quality}数据,URL:{cache["url"]}')
|
||||||
return {
|
return {
|
||||||
'code': 0,
|
"code": 0,
|
||||||
'msg': 'success',
|
"msg": "success",
|
||||||
'data': cache['url'],
|
"data": cache["url"],
|
||||||
'extra': {
|
"extra": {
|
||||||
'cache': True,
|
"cache": True,
|
||||||
'quality': {
|
"quality": {
|
||||||
'target': quality,
|
"target": quality,
|
||||||
'result': quality,
|
"result": quality,
|
||||||
},
|
},
|
||||||
'expire': {
|
"expire": {
|
||||||
# 在更新缓存的时候把有效期的75%作为链接可用时长,现在加回来
|
# 在更新缓存的时候把有效期的75%作为链接可用时长,现在加回来
|
||||||
'time': int(cache['time'] + (sourceExpirationTime[source]['time'] * 0.25)) if cache['expire'] else None,
|
"time": (
|
||||||
'canExpire': cache['expire'],
|
int(cache["time"] + (sourceExpirationTime[source]["time"] * 0.25))
|
||||||
}
|
if cache["expire"]
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"canExpire": cache["expire"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
except:
|
except:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
try:
|
try:
|
||||||
func = require('modules.' + source + '.url')
|
func = require("modules." + source + ".url")
|
||||||
except:
|
except:
|
||||||
return {
|
return {
|
||||||
'code': 1,
|
"code": 1,
|
||||||
'msg': '未知的源或不支持的方法',
|
"msg": "未知的源或不支持的方法",
|
||||||
'data': None,
|
"data": None,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = await func(songId, quality)
|
result = await func(songId, quality)
|
||||||
logger.info(f'获取{source}_{songId}_{quality}成功,URL:{result["url"]}')
|
logger.info(f'获取{source}_{songId}_{quality}成功,URL:{result["url"]}')
|
||||||
|
|
||||||
canExpire = sourceExpirationTime[source]['expire']
|
canExpire = sourceExpirationTime[source]["expire"]
|
||||||
expireTime = sourceExpirationTime[source]['time'] + int(time.time())
|
expireTime = int(sourceExpirationTime[source]["time"] * 0.75)
|
||||||
config.updateCache('urls', f'{source}_{songId}_{quality}', {
|
expireAt = int(expireTime + time.time())
|
||||||
"expire": canExpire,
|
config.updateCache(
|
||||||
# 取有效期的75%作为链接可用时长
|
"urls",
|
||||||
"time": int(expireTime - sourceExpirationTime[source]['time'] * 0.25),
|
f"{source}_{songId}_{quality}",
|
||||||
"url": result['url'],
|
{
|
||||||
})
|
"expire": canExpire,
|
||||||
|
# 取有效期的75%作为链接可用时长
|
||||||
|
"time": expireAt,
|
||||||
|
"url": result["url"],
|
||||||
|
},
|
||||||
|
expireTime if canExpire else None,
|
||||||
|
)
|
||||||
logger.debug(f'缓存已更新:{source}_{songId}_{quality}, URL:{result["url"]}, expire: {expireTime}')
|
logger.debug(f'缓存已更新:{source}_{songId}_{quality}, URL:{result["url"]}, expire: {expireTime}')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'code': 0,
|
"code": 0,
|
||||||
'msg': 'success',
|
"msg": "success",
|
||||||
'data': result['url'],
|
"data": result["url"],
|
||||||
'extra': {
|
"extra": {
|
||||||
'cache': False,
|
"cache": False,
|
||||||
'quality': {
|
"quality": {
|
||||||
'target': quality,
|
"target": quality,
|
||||||
'result': result['quality'],
|
"result": result["quality"],
|
||||||
},
|
},
|
||||||
'expire': {
|
"expire": {
|
||||||
'time': expireTime if canExpire else None,
|
"time": expireAt if canExpire else None,
|
||||||
'canExpire': canExpire,
|
"canExpire": canExpire,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
except FailedException as e:
|
except FailedException as e:
|
||||||
logger.info(f'获取{source}_{songId}_{quality}失败,原因:' + e.args[0])
|
logger.info(f"获取{source}_{songId}_{quality}失败,原因:" + e.args[0])
|
||||||
return {
|
return {
|
||||||
'code': 2,
|
"code": 2,
|
||||||
'msg': e.args[0],
|
"msg": e.args[0],
|
||||||
'data': None,
|
"data": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def lyric(source, songId, _, query):
|
async def lyric(source, songId, _, query):
|
||||||
cache = config.getCache('lyric', f'{source}_{songId}')
|
cache = config.getCache("lyric", f"{source}_{songId}")
|
||||||
if cache:
|
if cache:
|
||||||
return {
|
return {"code": 0, "msg": "success", "data": cache["data"]}
|
||||||
'code': 0,
|
|
||||||
'msg': 'success',
|
|
||||||
'data': cache['data']
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
func = require('modules.' + source + '.lyric')
|
func = require("modules." + source + ".lyric")
|
||||||
except:
|
except:
|
||||||
return {
|
return {
|
||||||
'code': 1,
|
"code": 1,
|
||||||
'msg': '未知的源或不支持的方法',
|
"msg": "未知的源或不支持的方法",
|
||||||
'data': None,
|
"data": None,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = await func(songId)
|
result = await func(songId)
|
||||||
config.updateCache('lyric', f'{source}_{songId}', {
|
expireTime = 86400 * 3
|
||||||
"data": result,
|
expireAt = int(time.time() + expireTime)
|
||||||
"time": int(time.time() + (86400 * 3)), # 歌词缓存3天
|
config.updateCache(
|
||||||
"expire": True,
|
"lyric",
|
||||||
})
|
f"{source}_{songId}",
|
||||||
logger.debug(f'缓存已更新:{source}_{songId}, lyric: {result}')
|
{
|
||||||
return {
|
"data": result,
|
||||||
'code': 0,
|
"time": expireAt, # 歌词缓存3天
|
||||||
'msg': 'success',
|
"expire": True,
|
||||||
'data': result
|
},
|
||||||
}
|
expireTime,
|
||||||
|
)
|
||||||
|
logger.debug(f"缓存已更新:{source}_{songId}, lyric: {result}")
|
||||||
|
return {"code": 0, "msg": "success", "data": result}
|
||||||
except FailedException as e:
|
except FailedException as e:
|
||||||
return {
|
return {
|
||||||
'code': 2,
|
"code": 2,
|
||||||
'msg': e.args[0],
|
"msg": e.args[0],
|
||||||
'data': None,
|
"data": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def search(source, songid, _, query):
|
async def search(source, songid, _, query):
|
||||||
try:
|
try:
|
||||||
func = require('modules.' + source + '.search')
|
func = require("modules." + source + ".search")
|
||||||
except:
|
except:
|
||||||
return {
|
return {
|
||||||
'code': 1,
|
"code": 1,
|
||||||
'msg': '未知的源或不支持的方法',
|
"msg": "未知的源或不支持的方法",
|
||||||
'data': None,
|
"data": None,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = await func(songid, query)
|
result = await func(songid, query)
|
||||||
return {
|
return {"code": 0, "msg": "success", "data": result}
|
||||||
'code': 0,
|
|
||||||
'msg': 'success',
|
|
||||||
'data': result
|
|
||||||
}
|
|
||||||
except FailedException as e:
|
except FailedException as e:
|
||||||
return {
|
return {
|
||||||
'code': 2,
|
"code": 2,
|
||||||
'msg': e.args[0],
|
"msg": e.args[0],
|
||||||
'data': None,
|
"data": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def other(method, source, songid, _, query):
|
async def other(method, source, songid, _, query):
|
||||||
try:
|
try:
|
||||||
func = require('modules.' + source + '.' + method)
|
func = require("modules." + source + "." + method)
|
||||||
except:
|
except:
|
||||||
return {
|
return {
|
||||||
'code': 1,
|
"code": 1,
|
||||||
'msg': '未知的源或不支持的方法',
|
"msg": "未知的源或不支持的方法",
|
||||||
'data': None,
|
"data": None,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = await func(songid)
|
result = await func(songid)
|
||||||
return {
|
return {"code": 0, "msg": "success", "data": result}
|
||||||
'code': 0,
|
|
||||||
'msg': 'success',
|
|
||||||
'data': result
|
|
||||||
}
|
|
||||||
except FailedException as e:
|
except FailedException as e:
|
||||||
return {
|
return {
|
||||||
'code': 2,
|
"code": 2,
|
||||||
'msg': e.args[0],
|
"msg": e.args[0],
|
||||||
'data': None,
|
"data": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def info_with_query(source, songid, _, query):
|
async def info_with_query(source, songid, _, query):
|
||||||
return await other('info', source, songid, None)
|
return await other("info", source, songid, None)
|
||||||
|
@ -22,6 +22,7 @@ from common.exceptions import FailedException
|
|||||||
from common import Httpx
|
from common import Httpx
|
||||||
from common import utils
|
from common import utils
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from . import refresh_login
|
||||||
|
|
||||||
async def info(hash_):
|
async def info(hash_):
|
||||||
tasks = []
|
tasks = []
|
||||||
|
209
modules/kg/refresh_login.py
Normal file
209
modules/kg/refresh_login.py
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq - (feat. Huibq and ikun0014)
|
||||||
|
# - name: refresh_login.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_login')
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh():
|
||||||
|
if (not config.read_config('module.kg.user.token')):
|
||||||
|
return
|
||||||
|
if (not config.read_config('module.kg.user.refresh_login.enable')):
|
||||||
|
return
|
||||||
|
|
||||||
|
user_id = config.read_config('module.kg.user.userid')
|
||||||
|
token = config.read_config('module.kg.user.token')
|
||||||
|
|
||||||
|
if (config.read_config('module.kg.client.appid') == '1005'):
|
||||||
|
ts = int(time.time() * 1000)
|
||||||
|
p3 = aes_sign(json.dumps({'clienttime': ts // 1000, 'token': token}))
|
||||||
|
data = {
|
||||||
|
'p3': p3,
|
||||||
|
'clienttime_ms': ts,
|
||||||
|
't1': 0,
|
||||||
|
't2': 0,
|
||||||
|
'userid': user_id
|
||||||
|
}
|
||||||
|
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_login.login_url')
|
||||||
|
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
|
||||||
|
body = req.json()
|
||||||
|
if body['error_code'] != 0:
|
||||||
|
logger.warning(f'酷狗音乐账号(UID_{user_id})刷新登录失败, code: ' +
|
||||||
|
str(body['error_code']) + f'\n响应体: {body}')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.info(f'酷狗音乐账号(UID_{user_id})刷新登录成功')
|
||||||
|
config.write_config('module.kg.user.userid',
|
||||||
|
str(body['data']['userid']))
|
||||||
|
config.write_config('module.kg.user.token',
|
||||||
|
body['data']['token'])
|
||||||
|
logger.info(f'为酷狗音乐账号(UID_{user_id})数据更新完毕')
|
||||||
|
return
|
||||||
|
elif (config.read_config('module.kg.client.appid') == '3116'):
|
||||||
|
ts = int(time.time() * 1000)
|
||||||
|
p3 = aes_sign(json.dumps({'clienttime': ts // 1000, 'token': token}), key=b'c24f74ca2820225badc01946dba4fdf7', iv=b'adc01946dba4fdf7')
|
||||||
|
data = {
|
||||||
|
'p3': p3,
|
||||||
|
'clienttime_ms': ts,
|
||||||
|
't1': 0,
|
||||||
|
't2': 0,
|
||||||
|
'userid': user_id
|
||||||
|
}
|
||||||
|
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_login.login_url')
|
||||||
|
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
|
||||||
|
body = req.json()
|
||||||
|
if body['error_code'] != 0:
|
||||||
|
logger.warning(f'酷狗音乐账号(UID_{user_id})刷新登录失败, code: ' +
|
||||||
|
str(body['error_code']) + f'\n响应体: {body}')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.info(f'酷狗音乐账号(UID_{user_id})刷新登录成功')
|
||||||
|
config.write_config('module.kg.user.userid',
|
||||||
|
str(body['data']['userid']))
|
||||||
|
config.write_config('module.kg.user.token',
|
||||||
|
body['data']['token'])
|
||||||
|
logger.info(f'为酷狗音乐账号(UID_{user_id})数据更新完毕')
|
||||||
|
return
|
||||||
|
|
||||||
|
if (not variable.use_cookie_pool):
|
||||||
|
kgconfig = config.read_config('module.kg')
|
||||||
|
refresh_login_info = kgconfig.get('refresh_login')
|
||||||
|
if (refresh_login_info):
|
||||||
|
kgconfig['user']['refresh_login'] = refresh_login_info
|
||||||
|
kgconfig.pop('refresh_login')
|
||||||
|
config.write_config('module.kg', kgconfig)
|
||||||
|
|
||||||
|
if (config.read_config('module.kg.user.refresh_login.enable') and not variable.use_cookie_pool):
|
||||||
|
scheduler.append('kg_refresh_login', refresh,
|
||||||
|
config.read_config('module.kg.user.refresh_login.interval'))
|
||||||
|
|
||||||
|
async def refresh_login_for_pool(user_info):
|
||||||
|
user_id = user_info["userid"]
|
||||||
|
token = user_info["token"]
|
||||||
|
if (config.read_config('module.kg.client.appid') == '1005'):
|
||||||
|
ts = int(time.time() * 1000)
|
||||||
|
p3 = aes_sign(json.dumps({'clienttime': ts // 1000, 'token': token}))
|
||||||
|
data = {
|
||||||
|
'p3': p3,
|
||||||
|
'clienttime_ms': ts,
|
||||||
|
't1': 0,
|
||||||
|
't2': 0,
|
||||||
|
'userid': user_id
|
||||||
|
}
|
||||||
|
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 = user_info["refresh_login"]["login_url"]
|
||||||
|
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
|
||||||
|
body = req.json()
|
||||||
|
if body['error_code'] != 0:
|
||||||
|
logger.warning(f'酷狗音乐账号(UID_{user_id})刷新登录失败, code: ' +
|
||||||
|
str(body['error_code']) + f'\n响应体: {body}')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.info(f'为酷狗音乐账号(UID_{user_id})刷新登录成功')
|
||||||
|
user_list = config.read_config('module.cookiepool.kg')
|
||||||
|
user_list[user_list.index(
|
||||||
|
user_info)]['token'] = body['data']['token']
|
||||||
|
user_list[user_list.index(
|
||||||
|
user_info)]['userid'] = str(body['data']['userid'])
|
||||||
|
config.write_config('module.cookiepool.kg', user_list)
|
||||||
|
logger.info(f'为酷狗音乐账号(UID_{user_id})数据更新完毕')
|
||||||
|
elif (config.read_config('module.kg.client.appid') == '3116'):
|
||||||
|
ts = int(time.time() * 1000)
|
||||||
|
p3 = aes_sign(json.dumps({'clienttime': ts // 1000, 'token': token}), key=b'c24f74ca2820225badc01946dba4fdf7', iv=b'adc01946dba4fdf7')
|
||||||
|
data = {
|
||||||
|
'p3': p3,
|
||||||
|
'clienttime_ms': ts,
|
||||||
|
't1': 0,
|
||||||
|
't2': 0,
|
||||||
|
'userid': user_id
|
||||||
|
}
|
||||||
|
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 = user_info["refresh_login"]["login_url"]
|
||||||
|
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
|
||||||
|
body = req.json()
|
||||||
|
if body['error_code'] != 0:
|
||||||
|
logger.warning(f'酷狗音乐账号(UID_{user_id})刷新登录失败, code: ' +
|
||||||
|
str(body['error_code']) + f'\n响应体: {body}')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.info(f'为酷狗音乐账号(UID_{user_id})刷新登录成功')
|
||||||
|
user_list = config.read_config('module.cookiepool.kg')
|
||||||
|
user_list[user_list.index(
|
||||||
|
user_info)]['token'] = body['data']['token']
|
||||||
|
user_list[user_list.index(
|
||||||
|
user_info)]['userid'] = str(body['data']['userid'])
|
||||||
|
config.write_config('module.cookiepool.kg', user_list)
|
||||||
|
logger.info(f'为酷狗音乐账号(UID_{user_id})数据更新完毕')
|
||||||
|
return
|
||||||
|
|
||||||
|
def reg_refresh_login_pool_task():
|
||||||
|
user_info_pool = config.read_config('module.cookiepool.kg')
|
||||||
|
for user_info in user_info_pool:
|
||||||
|
if (user_info['refresh_login'].get('enable')):
|
||||||
|
scheduler.append(
|
||||||
|
f'kgmusic_refresh_login_pooled_{user_info["userid"]}', refresh_login_for_pool, int(604800), args = {'user_info': user_info})
|
||||||
|
|
||||||
|
if (variable.use_cookie_pool):
|
||||||
|
reg_refresh_login_pool_task()
|
@ -6,10 +6,12 @@
|
|||||||
# - license: MIT -
|
# - license: MIT -
|
||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# This file is part of the "lx-music-api-server" project.
|
# 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 utils
|
||||||
from common import config
|
from common import config
|
||||||
from common import Httpx
|
from common import Httpx
|
||||||
|
import json
|
||||||
|
|
||||||
createObject = utils.CreateObject
|
createObject = utils.CreateObject
|
||||||
|
|
||||||
@ -23,6 +25,7 @@ tools = createObject({
|
|||||||
"version": config.read_config("module.kg.tracker.version"),
|
"version": config.read_config("module.kg.tracker.version"),
|
||||||
"extra_params": config.read_config("module.kg.tracker.extra_params"),
|
"extra_params": config.read_config("module.kg.tracker.extra_params"),
|
||||||
"appid": config.read_config("module.kg.client.appid"),
|
"appid": config.read_config("module.kg.client.appid"),
|
||||||
|
'mid': config.read_config('module.kg.user.mid'),
|
||||||
"pid": config.read_config("module.kg.client.pid"),
|
"pid": config.read_config("module.kg.client.pid"),
|
||||||
'qualityHashMap': {
|
'qualityHashMap': {
|
||||||
'128k': 'hash_128',
|
'128k': 'hash_128',
|
||||||
@ -49,14 +52,21 @@ def buildRequestParams(dictionary: dict):
|
|||||||
return joined_str
|
return joined_str
|
||||||
|
|
||||||
def sign(params, body = "", signkey = tools["signkey"]):
|
def sign(params, body = "", signkey = tools["signkey"]):
|
||||||
|
if (isinstance(body, dict)):
|
||||||
|
body = json.dumps(body)
|
||||||
params = utils.sortDict(params)
|
params = utils.sortDict(params)
|
||||||
params = buildSignatureParams(params, body)
|
params = buildSignatureParams(params, body)
|
||||||
return utils.createMD5(signkey + params + signkey)
|
return utils.createMD5(signkey + params + signkey)
|
||||||
|
|
||||||
async def signRequest(url, params, options, signkey = tools["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 ""), 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)
|
url = url + "?" + buildRequestParams(params)
|
||||||
return await Httpx.AsyncRequest(url, options)
|
return await Httpx.AsyncRequest(url, options)
|
||||||
|
|
||||||
def getKey(hash_, user_info):
|
def getKey(hash_, user_info):
|
||||||
return utils.createMD5(hash_.lower() + tools.pidversec + tools.appid + user_info['mid'] + user_info['userid'])
|
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()
|
||||||
|
@ -19,6 +19,10 @@ tools = {
|
|||||||
'320k': '320kmp3',
|
'320k': '320kmp3',
|
||||||
'flac': '2000kflac',
|
'flac': '2000kflac',
|
||||||
'flac24bit': '4000kflac',
|
'flac24bit': '4000kflac',
|
||||||
|
'128kmp3': '128kmp3',
|
||||||
|
'320kmp3': '320kmp3',
|
||||||
|
"2000kflac": "2000kflac",
|
||||||
|
"4000kflac": "4000kflac",
|
||||||
},
|
},
|
||||||
'qualityMapReverse': {
|
'qualityMapReverse': {
|
||||||
128: '128k',
|
128: '128k',
|
||||||
@ -38,7 +42,7 @@ async def url(songId, quality):
|
|||||||
proto = config.read_config('module.kw.proto')
|
proto = config.read_config('module.kw.proto')
|
||||||
if (proto == 'bd-api'):
|
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'))
|
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]}&uin={user_info['uid']}&token={user_info['token']}'''
|
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, {
|
req = await Httpx.AsyncRequest(target_url, {
|
||||||
'method': 'GET',
|
'method': 'GET',
|
||||||
'headers': {
|
'headers': {
|
||||||
|
@ -12,48 +12,52 @@ from common import Httpx
|
|||||||
from common import config
|
from common import config
|
||||||
from common import variable
|
from common import variable
|
||||||
from common.exceptions import FailedException
|
from common.exceptions import FailedException
|
||||||
|
from . import refresh_login # 删了这个定时任务会寄掉
|
||||||
|
|
||||||
tools = {
|
tools = {
|
||||||
'url': 'https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?toneFlag=__quality__&songId=__songId__&resourceType=2',
|
|
||||||
'qualityMap': {
|
'qualityMap': {
|
||||||
'128k': 'PQ',
|
'128k': '1',
|
||||||
'320k': 'HQ',
|
'320k': '2',
|
||||||
'flac': 'SQ',
|
'flac': '3',
|
||||||
'flac24bit': 'ZQ',
|
'flac24bit': '4',
|
||||||
|
"master": "5"
|
||||||
},
|
},
|
||||||
'qualityMapReverse': {
|
'qualityMapReverse': {
|
||||||
'PQ': '128k',
|
'000009': '128k',
|
||||||
'HQ': '320k',
|
'020010': '320k',
|
||||||
'SQ': 'flac',
|
'011002': 'flac',
|
||||||
'ZQ': 'flac24bit',
|
'011005': 'flac24bit',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def url(songId, quality):
|
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'))
|
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(tools['url'].replace('__quality__', tools['qualityMap'][quality]).replace('__songId__', songId), {
|
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',
|
'method': 'GET',
|
||||||
'headers': {
|
'headers': {
|
||||||
'User-Agent': user_info['useragent'],
|
'User-Agent': user_info['useragent'],
|
||||||
'aversionid': user_info['aversionid'],
|
"by": user_info["by"],
|
||||||
'token': user_info['token'],
|
"Cookie": "SESSION=" + user_info["session"],
|
||||||
'channel': '0146832',
|
"Referer": "https://m.music.migu.cn/v4/",
|
||||||
'language': 'Chinese',
|
"Origin": "https://m.music.migu.cn",
|
||||||
'ua': 'Android_migu',
|
|
||||||
'mode': 'android',
|
|
||||||
'os': 'Android ' + user_info['osversion'],
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
try:
|
try:
|
||||||
body = req.json()
|
body = req.json()
|
||||||
data = body['data']
|
|
||||||
|
|
||||||
if ((not int(body['code']) == 0) or ( not data['url'])):
|
if (int(body['code']) != 200 or (not body.get("data")) or (not body["data"]["playUrl"])):
|
||||||
raise FailedException('failed')
|
raise FailedException(body.get("msg") if body.get("msg") else "failed")
|
||||||
|
|
||||||
|
data = body["data"]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'url': data['url'].split('?')[0],
|
'url': body["data"]["playUrl"].split("?")[0] if body["data"]["playUrl"].split("?")[0].startswith("http") else "http:" + body["data"]["playUrl"].split("?")[0],
|
||||||
'quality': tools['qualityMapReverse'][data['audioFormatType']],
|
'quality': tools['qualityMapReverse'].get(data['formatId']) if (tools['qualityMapReverse'].get(data['formatId'])) else "unknown",
|
||||||
}
|
}
|
||||||
except:
|
except:
|
||||||
raise FailedException('failed')
|
raise FailedException('failed')
|
51
modules/mg/refresh_login.py
Normal file
51
modules/mg/refresh_login.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - 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 +1,55 @@
|
|||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# - mode: python -
|
# - mode: python -
|
||||||
# - author: helloplhm-qwq -
|
# - author: jixunmoe -
|
||||||
# - name: QMWSign.py -
|
# - name: zzc_sign.py -
|
||||||
# - project: lx-music-api-server -
|
# - project: qmweb-sign -
|
||||||
# - license: MIT -
|
# - license: MIT -
|
||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# This file is part of the "lx-music-api-server" project.
|
# This file is part of the "qmweb-sign" project.
|
||||||
|
|
||||||
from common.utils import createMD5
|
import sys
|
||||||
import re as _re
|
import re
|
||||||
|
|
||||||
def v(b):
|
from hashlib import sha1
|
||||||
res = []
|
from base64 import b64encode
|
||||||
p = [21, 4, 9, 26, 16, 20, 27, 30]
|
|
||||||
for x in p:
|
|
||||||
res.append(b[x])
|
|
||||||
return ''.join(res)
|
|
||||||
|
|
||||||
def c(b):
|
PART_1_INDEXES = [23, 14, 6, 36, 16, 40, 7, 19]
|
||||||
res = []
|
PART_2_INDEXES = [16, 1, 32, 12, 19, 27, 8, 5]
|
||||||
p = [18, 11, 3, 2, 1, 7, 6, 25]
|
SCRAMBLE_VALUES = [
|
||||||
for x in p:
|
89,
|
||||||
res.append(b[x])
|
39,
|
||||||
return ''.join(res)
|
179,
|
||||||
|
150,
|
||||||
|
218,
|
||||||
|
82,
|
||||||
|
58,
|
||||||
|
252,
|
||||||
|
177,
|
||||||
|
52,
|
||||||
|
186,
|
||||||
|
123,
|
||||||
|
120,
|
||||||
|
64,
|
||||||
|
242,
|
||||||
|
133,
|
||||||
|
143,
|
||||||
|
161,
|
||||||
|
121,
|
||||||
|
179,
|
||||||
|
]
|
||||||
|
|
||||||
def y(a, b, c):
|
PART_1_INDEXES = filter(lambda x: x < 40, PART_1_INDEXES)
|
||||||
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):
|
def sign(payload: str) -> str:
|
||||||
zd = {
|
hash = sha1(payload.encode("utf-8")).hexdigest().upper()
|
||||||
"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):
|
part1 = "".join(map(lambda i: hash[i], PART_1_INDEXES))
|
||||||
md5Str = createMD5(params).upper()
|
part2 = "".join(map(lambda i: hash[i], PART_2_INDEXES))
|
||||||
h = v(md5Str)
|
|
||||||
e = c(md5Str)
|
part3 = bytearray(20)
|
||||||
ls = t(md5Str)
|
for i, v in enumerate(SCRAMBLE_VALUES):
|
||||||
m = n(ls)
|
value = v ^ int(hash[i * 2 : i * 2 + 2], 16)
|
||||||
res = 'zzb' + h + m + e
|
part3[i] = value
|
||||||
res = res.lower()
|
b64_part = re.sub(rb"[\\/+=]", b"", b64encode(part3)).decode("utf-8")
|
||||||
r = _re.compile(r'[\\/+]')
|
return f"zzc{part1}{b64_part}{part2}".lower()
|
||||||
res = _re.sub(r, '', res)
|
|
||||||
return res
|
|
||||||
|
@ -26,70 +26,66 @@ class ParseTools:
|
|||||||
'timeLabelFixRxp': re.compile(r'(?:\.0+|0+)$'),
|
'timeLabelFixRxp': re.compile(r'(?:\.0+|0+)$'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def ms_format(self, time_ms):
|
def msFormat(self, timeMs):
|
||||||
if not time_ms:
|
if isinstance(timeMs, float) and timeMs.is_nan():
|
||||||
return ''
|
return ''
|
||||||
ms = time_ms % 1000
|
ms = timeMs % 1000
|
||||||
time_ms /= 1000
|
timeMs //= 1000
|
||||||
m = str(int(time_ms / 60)).zfill(2)
|
m = str(int(timeMs // 60)).zfill(2)
|
||||||
time_ms %= 60
|
s = str(int(timeMs % 60)).zfill(2)
|
||||||
s = str(int(time_ms)).zfill(2)
|
return f'[{m}:{s}.{str(ms).zfill(3)}]'
|
||||||
return f"[{m}:{s}.{str(ms).zfill(3)}]"
|
|
||||||
|
|
||||||
def parse_lyric(self, lrc):
|
def parseLyric(self, lrc):
|
||||||
lrc = lrc.strip()
|
lrc = lrc.strip().replace('\r', '')
|
||||||
lrc = lrc.replace('\r', '')
|
|
||||||
if not lrc:
|
if not lrc:
|
||||||
return {'lyric': '', 'lxlyric': ''}
|
return {'lyric': '', 'lxlyric': ''}
|
||||||
lines = lrc.split('\n')
|
# print(lrc)
|
||||||
|
|
||||||
lxlrc_lines = []
|
lines = lrc.split('\n')
|
||||||
lyric_lines = []
|
lxlrcLines = []
|
||||||
|
lrcLines = []
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
result = self.rxps['lineTime'].match(line)
|
result = self.rxps['lineTime'].match(line)
|
||||||
if not result:
|
if not result:
|
||||||
if line.startswith('[offset'):
|
if line.startswith('[offset'):
|
||||||
lxlrc_lines.append(line)
|
lxlrcLines.append(line)
|
||||||
lyric_lines.append(line)
|
lrcLines.append(line)
|
||||||
if self.rxps['lineTime2'].search(line):
|
if self.rxps['lineTime2'].match(line):
|
||||||
lyric_lines.append(line)
|
lrcLines.append(line)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
start_ms_time = int(result.group(1))
|
startMsTime = int(result.group(1))
|
||||||
start_time_str = self.ms_format(start_ms_time)
|
startTimeStr = self.msFormat(startMsTime)
|
||||||
if not start_time_str:
|
if not startTimeStr:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
words = re.sub(self.rxps['lineTime'], '', line)
|
words = re.sub(self.rxps['lineTime'], '', line)
|
||||||
|
|
||||||
lyric_lines.append(f"{start_time_str}{re.sub(self.rxps['wordTimeAll'], '', words)}")
|
lrcLines.append(f'{startTimeStr}{re.sub(self.rxps["wordTimeAll"], "", words)}')
|
||||||
|
|
||||||
times = re.findall(self.rxps['wordTimeAll'], words)
|
times = re.findall(self.rxps['wordTimeAll'], words)
|
||||||
if not times:
|
if not times:
|
||||||
continue
|
continue
|
||||||
times = [
|
_rxp = r"\((\d+),(\d+)\)"
|
||||||
f"<{max(int(match.group(1)) - start_ms_time, 0)},{match.group(2)}>"
|
times = [f'''<{max(int(re.search(_rxp, time).group(1)) - startMsTime, 0)},{re.search(_rxp, time).group(2)}>''' for time in times]
|
||||||
for match in re.finditer(r'\((\d+),(\d+)\)', words)
|
wordArr = re.split(self.rxps['wordTime'], words)
|
||||||
]
|
newWords = ''.join([f'{time}{wordArr[index]}' for index, time in enumerate(times)])
|
||||||
word_arr = re.split(self.rxps['wordTime'], words)
|
lxlrcLines.append(f'{startTimeStr}{newWords}')
|
||||||
new_words = ''.join([f"{time}{word}" for time, word in zip(times, word_arr)])
|
|
||||||
lxlrc_lines.append(f"{start_time_str}{new_words}")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'lyric': '\n'.join(lyric_lines),
|
'lyric': '\n'.join(lrcLines),
|
||||||
'lxlyric': '\n'.join(lxlrc_lines),
|
'lxlyric': '\n'.join(lxlrcLines),
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_rlyric(self, lrc):
|
def parseRlyric(self, lrc):
|
||||||
lrc = lrc.strip()
|
lrc = lrc.strip().replace('\r', '')
|
||||||
lrc = lrc.replace('\r', '')
|
|
||||||
if not lrc:
|
if not lrc:
|
||||||
return {'lyric': '', 'lxlyric': ''}
|
return {'lyric': '', 'lxlyric': ''}
|
||||||
lines = lrc.split('\n')
|
|
||||||
|
|
||||||
lyric_lines = []
|
lines = lrc.split('\n')
|
||||||
|
lrcLines = []
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
@ -97,91 +93,104 @@ class ParseTools:
|
|||||||
if not result:
|
if not result:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
start_ms_time = int(result.group(1))
|
startMsTime = int(result.group(1))
|
||||||
start_time_str = self.ms_format(start_ms_time)
|
startTimeStr = self.msFormat(startMsTime)
|
||||||
if not start_time_str:
|
if not startTimeStr:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
words = re.sub(self.rxps['lineTime'], '', line)
|
words = re.sub(self.rxps['lineTime'], '', line)
|
||||||
|
lrcLines.append(f'{startTimeStr}{re.sub(self.rxps["wordTimeAll"], "", words)}')
|
||||||
|
|
||||||
lyric_lines.append(f"{start_time_str}{re.sub(self.rxps['wordTimeAll'], '', words)}")
|
return '\n'.join(lrcLines)
|
||||||
|
|
||||||
return '\n'.join(lyric_lines)
|
def removeTag(self, string):
|
||||||
|
|
||||||
def remove_tag(self, string):
|
|
||||||
return re.sub(r'^[\S\s]*?LyricContent="', '', string).replace('"\/>[\S\s]*?$', '')
|
return re.sub(r'^[\S\s]*?LyricContent="', '', string).replace('"\/>[\S\s]*?$', '')
|
||||||
|
|
||||||
def get_intv(self, interval):
|
def getIntv(self, interval):
|
||||||
|
if not interval:
|
||||||
|
return 0
|
||||||
if '.' not in interval:
|
if '.' not in interval:
|
||||||
interval += '.0'
|
interval += '.0'
|
||||||
arr = re.split(':|\.', interval.ljust(8, '0'))[:3]
|
arr = re.split(r':|\.', interval)
|
||||||
m, s, ms = map(int, arr)
|
while len(arr) < 3:
|
||||||
return m * 3600000 + s * 1000 + ms
|
arr.insert(0, '0')
|
||||||
|
m, s, ms = arr
|
||||||
|
return int(m) * 3600000 + int(s) * 1000 + int(ms)
|
||||||
|
|
||||||
def fix_rlrc_time_tag(self, rlrc, lrc):
|
def fixRlrcTimeTag(self, rlrc, lrc):
|
||||||
rlrc_lines = rlrc.split('\n')
|
rlrcLines = rlrc.split('\n')
|
||||||
lrc_lines = lrc.split('\n')
|
lrcLines = lrc.split('\n')
|
||||||
new_lrc = []
|
newLrc = []
|
||||||
for line in rlrc_lines:
|
|
||||||
result = self.rxps['lineTime2'].search(line)
|
for line in rlrcLines:
|
||||||
|
result = self.rxps['lineTime2'].match(line)
|
||||||
if not result:
|
if not result:
|
||||||
continue
|
continue
|
||||||
words = re.sub(self.rxps['lineTime2'], '', line)
|
words = re.sub(self.rxps['lineTime2'], '', line)
|
||||||
if not words.strip():
|
if not words.strip():
|
||||||
continue
|
continue
|
||||||
t1 = self.get_intv(result.group(1))
|
t1 = self.getIntv(result.group(1))
|
||||||
while lrc_lines:
|
|
||||||
lrc_line = lrc_lines.pop(0)
|
while lrcLines:
|
||||||
lrc_line_result = self.rxps['lineTime2'].search(lrc_line)
|
lrcLine = lrcLines.pop(0)
|
||||||
if not lrc_line_result:
|
lrcLineResult = self.rxps['lineTime2'].match(lrcLine)
|
||||||
|
if not lrcLineResult:
|
||||||
continue
|
continue
|
||||||
t2 = self.get_intv(lrc_line_result.group(1))
|
t2 = self.getIntv(lrcLineResult.group(1))
|
||||||
if abs(t1 - t2) < 100:
|
if abs(t1 - t2) < 100:
|
||||||
new_lrc.append(re.sub(self.rxps['lineTime2'], lrc_line_result.group(0), line))
|
newLrc.append(re.sub(self.rxps['lineTime2'], lrcLineResult.group(0), line))
|
||||||
break
|
break
|
||||||
return '\n'.join(new_lrc)
|
|
||||||
|
|
||||||
def fix_tlrc_time_tag(self, tlrc, lrc):
|
return '\n'.join(newLrc)
|
||||||
tlrc_lines = tlrc.split('\n')
|
|
||||||
lrc_lines = lrc.split('\n')
|
|
||||||
new_lrc = []
|
|
||||||
time_tag_rxp = r'^\[[\d:.]+\]'
|
|
||||||
|
|
||||||
for line in tlrc_lines:
|
def fixTlrcTimeTag(self, tlrc, lrc):
|
||||||
result = re.match(time_tag_rxp, line)
|
tlrcLines = tlrc.split('\n')
|
||||||
|
lrcLines = lrc.split('\n')
|
||||||
|
newLrc = []
|
||||||
|
|
||||||
|
for line in tlrcLines:
|
||||||
|
result = self.rxps['lineTime2'].match(line)
|
||||||
if not result:
|
if not result:
|
||||||
continue
|
continue
|
||||||
words = re.sub(time_tag_rxp, '', line)
|
words = re.sub(self.rxps['lineTime2'], '', line)
|
||||||
if not words.strip():
|
if not words.strip():
|
||||||
continue
|
continue
|
||||||
tag = re.sub(r'\[\d+:\d+\.\d+\]', '', result.group(0))
|
time = result.group(1)
|
||||||
|
if '.' in time:
|
||||||
|
time += '0' * (3 - len(time.split('.')[1]))
|
||||||
|
|
||||||
while lrc_lines:
|
t1 = self.getIntv(time)
|
||||||
lrc_line = lrc_lines.pop(0)
|
|
||||||
lrc_line_result = re.match(time_tag_rxp, lrc_line)
|
while lrcLines:
|
||||||
if not lrc_line_result:
|
lrcLine = lrcLines.pop(0)
|
||||||
|
lrcLineResult = self.rxps['lineTime2'].match(lrcLine)
|
||||||
|
if not lrcLineResult:
|
||||||
continue
|
continue
|
||||||
if tag in lrc_line_result.group(0):
|
t2 = self.getIntv(lrcLineResult.group(1))
|
||||||
new_lrc.append(re.sub(time_tag_rxp, lrc_line_result.group(0), line))
|
if abs(t1 - t2) < 100:
|
||||||
|
newLrc.append(re.sub(self.rxps['lineTime2'], lrcLineResult.group(0), line))
|
||||||
break
|
break
|
||||||
|
|
||||||
return '\n'.join(new_lrc)
|
return '\n'.join(newLrc)
|
||||||
|
|
||||||
def parse(self, lrc, tlrc, rlrc):
|
def parse(self, lrc, tlrc=None, rlrc=None):
|
||||||
info = {
|
info = {
|
||||||
'lyric': '',
|
'lyric': '',
|
||||||
'tlyric': '',
|
'tlyric': '',
|
||||||
'rlyric': '',
|
'rlyric': '',
|
||||||
'lxlyric': '',
|
'lxlyric': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
if lrc:
|
if lrc:
|
||||||
lyric_info = self.parse_lyric(self.remove_tag(lrc))
|
parsed_lrc = self.parseLyric(self.removeTag(lrc))
|
||||||
info['lyric'] = lyric_info['lyric']
|
info['lyric'] = parsed_lrc['lyric']
|
||||||
info['lxlyric'] = lyric_info['lxlyric']
|
info['lxlyric'] = parsed_lrc['lxlyric']
|
||||||
|
|
||||||
if rlrc:
|
if rlrc:
|
||||||
info['rlyric'] = self.fix_rlrc_time_tag(self.parse_rlyric(self.remove_tag(rlrc)), info['lyric'])
|
info['rlyric'] = self.fixRlrcTimeTag(self.parseRlyric(self.removeTag(rlrc)), info['lyric'])
|
||||||
|
|
||||||
if tlrc:
|
if tlrc:
|
||||||
info['tlyric'] = self.fix_tlrc_time_tag(tlrc, info['lyric'])
|
info['tlyric'] = self.fixTlrcTimeTag(tlrc, info['lyric'])
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
@ -21,22 +21,22 @@ async def url(songId, quality):
|
|||||||
strMediaMid = infoBody['track_info']['file']['media_mid']
|
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'))
|
user_info = config.read_config('module.tx.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.tx'))
|
||||||
requestBody = {
|
requestBody = {
|
||||||
'req_0': {
|
"req": {
|
||||||
'module': 'vkey.GetVkeyServer',
|
"module": "music.vkey.GetVkey",
|
||||||
'method': 'CgiGetVkey',
|
"method": "UrlGetVkey",
|
||||||
'param': {
|
"param": {
|
||||||
'filename': [f"{tools.fileInfo[quality]['h']}{strMediaMid}{tools.fileInfo[quality]['e']}"],
|
"filename": [f"{tools.fileInfo[quality]['h']}{strMediaMid}{tools.fileInfo[quality]['e']}"],
|
||||||
'guid': config.read_config('module.tx.vkeyserver.guid'),
|
"guid": config.read_config("module.tx.vkeyserver.guid"),
|
||||||
'songmid': [songId],
|
"songmid": [songId],
|
||||||
'songtype': [0],
|
"songtype": [0],
|
||||||
'uin': str(user_info['uin']),
|
"uin": str(user_info["uin"]),
|
||||||
'loginflag': 1,
|
"loginflag": 1,
|
||||||
'platform': '20',
|
"platform": "20",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'comm': {
|
"comm": {
|
||||||
"qq": str(user_info['uin']),
|
"qq": str(user_info["uin"]),
|
||||||
"authst": user_info['qqmusic_key'],
|
"authst": user_info["qqmusic_key"],
|
||||||
"ct": "26",
|
"ct": "26",
|
||||||
"cv": "2010101",
|
"cv": "2010101",
|
||||||
"v": "2010101"
|
"v": "2010101"
|
||||||
@ -44,7 +44,7 @@ async def url(songId, quality):
|
|||||||
}
|
}
|
||||||
req = await signRequest(requestBody)
|
req = await signRequest(requestBody)
|
||||||
body = createObject(req.json())
|
body = createObject(req.json())
|
||||||
data = body.req_0.data.midurlinfo[0]
|
data = body.req.data.midurlinfo[0]
|
||||||
url = data['purl']
|
url = data['purl']
|
||||||
|
|
||||||
if (not url):
|
if (not url):
|
||||||
|
@ -7,205 +7,151 @@
|
|||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# This file is part of the "lx-music-api-server" project.
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
from common import Httpx, variable
|
from common import Httpx, variable, scheduler, config, log
|
||||||
from common import scheduler
|
|
||||||
from common import config
|
|
||||||
from common import log
|
|
||||||
from .utils import sign
|
from .utils import sign
|
||||||
import ujson as json
|
import ujson as json
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
logger = log.log('qqmusic_refresh_login')
|
logger = log.log("qqmusic_refresh_login")
|
||||||
|
|
||||||
|
|
||||||
async def refresh():
|
def _build_request_body(user_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
if (not config.read_config('module.tx.user.qqmusic_key')):
|
"""构建统一请求体结构"""
|
||||||
return
|
return {
|
||||||
if (not config.read_config('module.tx.user.refresh_login.enable')):
|
"comm": {
|
||||||
return
|
"fPersonality": "0",
|
||||||
if (config.read_config('module.tx.user.qqmusic_key').startswith('W_X')):
|
"tmeLoginType": "2"
|
||||||
options = {
|
if user_info["qqmusic_key"].startswith("Q_H_L")
|
||||||
'method': 'POST',
|
else "1",
|
||||||
'body': json.dumps({
|
"qq": str(user_info["uin"]),
|
||||||
"comm": {
|
"authst": user_info["qqmusic_key"],
|
||||||
"fPersonality": "0",
|
"ct": "11",
|
||||||
"tmeLoginType": "1",
|
"cv": "12080008",
|
||||||
"tmeLoginMethod": "1",
|
"v": "12080008",
|
||||||
"qq": "",
|
"tmeAppID": "qqmusic",
|
||||||
"authst": "",
|
},
|
||||||
"ct": "11",
|
"req1": {
|
||||||
"cv": "12080008",
|
"module": "music.login.LoginServer",
|
||||||
"v": "12080008",
|
"method": "Login",
|
||||||
"tmeAppID": "qqmusic"
|
"param": {
|
||||||
},
|
"str_musicid": str(user_info["uin"]),
|
||||||
"req1": {
|
"musickey": user_info["qqmusic_key"],
|
||||||
"module": "music.login.LoginServer",
|
"refresh_key": user_info.get("refresh_key", ""),
|
||||||
"method": "Login",
|
},
|
||||||
"param": {
|
},
|
||||||
"code": "",
|
}
|
||||||
"openid": "",
|
|
||||||
"refresh_token": "",
|
|
||||||
"str_musicid": str(config.read_config('module.tx.user.uin')),
|
async def _update_user_config(
|
||||||
"musickey": config.read_config('module.tx.user.qqmusic_key'),
|
user_info: Dict[str, Any], new_data: Dict[str, Any]
|
||||||
"unionid": "",
|
) -> None:
|
||||||
"refresh_key": "",
|
"""统一更新用户配置"""
|
||||||
"loginMode": 2
|
updates = {
|
||||||
}
|
"uin": str(new_data.get("musicid", user_info["uin"])),
|
||||||
}
|
"qqmusic_key": new_data.get("musickey", user_info["qqmusic_key"]),
|
||||||
})
|
"refresh_key": new_data.get("refresh_key", user_info.get("refresh_key", "")),
|
||||||
}
|
}
|
||||||
signature = sign(options['body'])
|
|
||||||
req = await Httpx.AsyncRequest(f'https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
|
if variable.use_cookie_pool:
|
||||||
body = req.json()
|
user_list = config.read_config("module.cookiepool.tx")
|
||||||
if (body['req1']['code'] != 0):
|
target_user = next((u for u in user_list if u["uin"] == user_info["uin"]), None)
|
||||||
logger.warning('刷新登录失败, code: ' +
|
if target_user:
|
||||||
str(body['req1']['code']) + f'\n响应体: {body}')
|
target_user.update(updates)
|
||||||
return
|
config.write_config("module.cookiepool.tx", user_list)
|
||||||
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:
|
else:
|
||||||
logger.error('未知的qqmusic_key格式')
|
for key, value in updates.items():
|
||||||
|
config.write_config(f"module.tx.user.{key}", value)
|
||||||
|
|
||||||
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):
|
async def _process_refresh(user_info: Dict[str, Any]) -> Optional[bool]:
|
||||||
scheduler.append('qqmusic_refresh_login', refresh,
|
"""统一处理刷新逻辑"""
|
||||||
config.read_config('module.tx.user.refresh_login.interval'))
|
try:
|
||||||
|
# 构建请求参数
|
||||||
|
request_body = _build_request_body(user_info)
|
||||||
|
signature = sign(json.dumps(request_body))
|
||||||
|
|
||||||
async def refresh_login_for_pool(user_info):
|
# 发送请求
|
||||||
if (user_info['qqmusic_key'].startswith('W_X')):
|
response = await Httpx.AsyncRequest(
|
||||||
options = {
|
f"https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}",
|
||||||
'method': 'POST',
|
{
|
||||||
'body': json.dumps({
|
"method": "POST",
|
||||||
"comm": {
|
"body": json.dumps(request_body),
|
||||||
"fPersonality": "0",
|
"headers": {
|
||||||
"tmeLoginType": "1",
|
"Content-Type": "application/json",
|
||||||
"tmeLoginMethod": "1",
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
"qq": "",
|
|
||||||
"authst": "",
|
|
||||||
"ct": "11",
|
|
||||||
"cv": "12080008",
|
|
||||||
"v": "12080008",
|
|
||||||
"tmeAppID": "qqmusic"
|
|
||||||
},
|
},
|
||||||
"req1": {
|
},
|
||||||
"module": "music.login.LoginServer",
|
)
|
||||||
"method": "Login",
|
|
||||||
"param": {
|
response_data = response.json()
|
||||||
"code": "",
|
if response_data.get("req1", {}).get("code") != 0:
|
||||||
"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(
|
logger.warning(
|
||||||
f'为QQ音乐账号({user_info["uin"]})刷新登录失败, code: ' + str(body['req1']['code']) + f'\n响应体: {body}')
|
f"刷新失败 [账号: {user_info['uin']} 代码: {response_data['req1']['code']}]"
|
||||||
return
|
)
|
||||||
else:
|
return False
|
||||||
logger.info(f'为QQ音乐账号(QQ_{user_info["uin"]})刷新登录成功')
|
|
||||||
user_list = config.read_config('module.cookiepool.tx')
|
# 更新配置
|
||||||
user_list[user_list.index(
|
await _update_user_config(user_info, response_data["req1"]["data"])
|
||||||
user_info)]['qqmusic_key'] = body['req1']['data']['musickey']
|
logger.info(f"刷新成功 [账号: {user_info['uin']}]")
|
||||||
user_list[user_list.index(
|
return True
|
||||||
user_info)]['uin'] = str(body['req1']['data']['musicid'])
|
|
||||||
config.write_config('module.cookiepool.tx', user_list)
|
except json.JSONDecodeError:
|
||||||
logger.info(f'为QQ音乐账号(QQ_{user_info["uin"]})数据更新完毕')
|
logger.error(
|
||||||
return
|
"响应解析失败 [账号: %s] 原始响应: %s",
|
||||||
else:
|
user_info["uin"],
|
||||||
logger.warning(f'为QQ音乐账号({user_info["uin"]})刷新登录失败: 未知或不支持的key类型')
|
response.text[:100],
|
||||||
|
)
|
||||||
|
except KeyError as e:
|
||||||
|
logger.error(
|
||||||
|
"响应数据格式异常 [账号: %s] 缺失字段: %s", user_info["uin"], str(e)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"刷新过程异常 [账号: %s] 错误信息: %s",
|
||||||
|
user_info["uin"],
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh() -> None:
|
||||||
|
"""主刷新入口(非Cookie池模式)"""
|
||||||
|
if not config.read_config("module.tx.user.refresh_login.enable"):
|
||||||
return
|
return
|
||||||
|
await _process_refresh(
|
||||||
def reg_refresh_login_pool_task():
|
{
|
||||||
user_info_pool = config.read_config('module.cookiepool.tx')
|
"uin": config.read_config("module.tx.user.uin"),
|
||||||
for user_info in user_info_pool:
|
"qqmusic_key": config.read_config("module.tx.user.qqmusic_key"),
|
||||||
if (user_info['refresh_login'].get('enable')):
|
"refresh_key": config.read_config("module.tx.user.refresh_key"),
|
||||||
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):
|
async def refresh_login_for_pool(user_info: Dict[str, Any]) -> None:
|
||||||
reg_refresh_login_pool_task()
|
"""Cookie池刷新入口"""
|
||||||
|
if user_info.get("refresh_login", {}).get("enable", False):
|
||||||
|
await _process_refresh(user_info)
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_scheduler() -> None:
|
||||||
|
"""初始化定时任务"""
|
||||||
|
if variable.use_cookie_pool:
|
||||||
|
user_list = config.read_config("module.cookiepool.tx")
|
||||||
|
for user in user_list:
|
||||||
|
if user.get("refresh_login", {}).get("enable", False):
|
||||||
|
scheduler.append(
|
||||||
|
f"qq_refresh_{user['uin']}",
|
||||||
|
refresh_login_for_pool,
|
||||||
|
user["refresh_login"].get("interval", 3600),
|
||||||
|
args={"user_info": user},
|
||||||
|
)
|
||||||
|
elif config.read_config("module.tx.user.refresh_login.enable"):
|
||||||
|
scheduler.append(
|
||||||
|
"qqmusic_main_refresh",
|
||||||
|
refresh,
|
||||||
|
config.read_config("module.tx.user.refresh_login.interval"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 初始化定时任务
|
||||||
|
_setup_scheduler()
|
||||||
|
@ -13,51 +13,107 @@ from common import config
|
|||||||
from common.exceptions import FailedException
|
from common.exceptions import FailedException
|
||||||
from .encrypt import eapiEncrypt
|
from .encrypt import eapiEncrypt
|
||||||
import ujson as json
|
import ujson as json
|
||||||
|
from . import refresh_login
|
||||||
|
|
||||||
|
PROTO = config.read_config("module.wy.proto")
|
||||||
|
API_URL = config.read_config("module.wy.ncmapi.api_url")
|
||||||
|
|
||||||
tools = {
|
tools = {
|
||||||
'qualityMap': {
|
'qualityMap': {
|
||||||
'128k': 'standard',
|
'128k': 'standard',
|
||||||
|
"192k": "higher",
|
||||||
'320k': 'exhigh',
|
'320k': 'exhigh',
|
||||||
'flac': 'lossless',
|
'flac': 'lossless',
|
||||||
'flac24bit': 'hires',
|
'flac24bit': 'hires',
|
||||||
"dolby": "jyeffect",
|
"dolby": "jyeffect",
|
||||||
"sky": "jysky",
|
"sky": "sky",
|
||||||
"master": "jymaster",
|
"master": "jymaster",
|
||||||
|
"standard": "standard",
|
||||||
|
"higher": "higher",
|
||||||
|
"exhigh": "exhigh",
|
||||||
|
"lossless": "lossless",
|
||||||
|
"hires": "hires",
|
||||||
|
"jyeffect": "jyeffect",
|
||||||
|
"jymaster": "jymaster",
|
||||||
},
|
},
|
||||||
'qualityMapReverse': {
|
'qualityMapReverse': {
|
||||||
'standard': '128k',
|
'standard': '128k',
|
||||||
|
"higher": "192k",
|
||||||
'exhigh': '320k',
|
'exhigh': '320k',
|
||||||
'lossless': 'flac',
|
'lossless': 'flac',
|
||||||
'hires': 'flac24bit',
|
'hires': 'flac24bit',
|
||||||
"jyeffect": "dolby",
|
"jyeffect": "dolby",
|
||||||
"jysky": "sky",
|
"sky": "sky",
|
||||||
"jymaster": "master",
|
"jymaster": "master",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def url(songId, quality):
|
async def url(songId, quality):
|
||||||
path = '/api/song/enhance/player/url/v1'
|
if PROTO == "offcial":
|
||||||
requestUrl = 'https://interface.music.163.com/eapi/song/enhance/player/url/v1'
|
path = '/api/song/enhance/player/url/v1'
|
||||||
req = await Httpx.AsyncRequest(requestUrl, {
|
requestUrl = 'https://interface.music.163.com/eapi/song/enhance/player/url/v1'
|
||||||
'method': 'POST',
|
requestBody = {
|
||||||
'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({
|
|
||||||
"ids": json.dumps([songId]),
|
"ids": json.dumps([songId]),
|
||||||
"level": tools["qualityMap"][quality],
|
"level": tools["qualityMap"][quality],
|
||||||
"encodeType": "flac",
|
"encodeType": "flac",
|
||||||
}))
|
}
|
||||||
})
|
if (quality == "sky"):
|
||||||
body = req.json()
|
requestBody["immerseType"] = "c51"
|
||||||
if (not body.get("data") or (not body.get("data")) or (not body.get("data")[0].get("url"))):
|
req = await Httpx.AsyncRequest(requestUrl, {
|
||||||
raise FailedException("failed")
|
'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("失败")
|
||||||
|
|
||||||
data = body["data"][0]
|
data = body["data"][0]
|
||||||
if (data['level'] != tools['qualityMap'][quality]):
|
|
||||||
raise FailedException("reject unmatched quality")
|
|
||||||
|
|
||||||
return {
|
# 修正:映射服务器返回的 level 为标准化值
|
||||||
'url': data["url"].split("?")[0],
|
data_level = data['level']
|
||||||
'quality': tools['qualityMapReverse'][data['level']]
|
expected_level = tools["qualityMap"][quality]
|
||||||
}
|
|
||||||
|
# 检查客户端请求的 quality 与服务器返回的 level 是否匹配
|
||||||
|
if data_level != expected_level:
|
||||||
|
raise FailedException(
|
||||||
|
f"reject unmatched quality: expected={expected_level}, got={data_level}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'url': data["url"].split("?")[0],
|
||||||
|
'quality': tools['qualityMapReverse'][data['level']]
|
||||||
|
}
|
||||||
|
elif (PROTO == "ncmapi") and (API_URL):
|
||||||
|
requestUrl = f"{API_URL}/song/url/v1"
|
||||||
|
requestBody = {
|
||||||
|
"ids": songId,
|
||||||
|
"level": tools["qualityMap"][quality],
|
||||||
|
"cookie": config.read_config('module.wy.user.cookie') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.wy'))['cookie']
|
||||||
|
}
|
||||||
|
req = await Httpx.AsyncRequest(requestUrl, {
|
||||||
|
"method": "GET",
|
||||||
|
"params": requestBody
|
||||||
|
})
|
||||||
|
body = req.json()
|
||||||
|
if (body["code"] != 200) or (not body.get("data")):
|
||||||
|
raise FailedException("失败")
|
||||||
|
data = body["data"][0]
|
||||||
|
|
||||||
|
# 修正:映射服务器返回的 level 为标准化值
|
||||||
|
data_level = data['level']
|
||||||
|
expected_level = tools["qualityMap"][quality]
|
||||||
|
|
||||||
|
# 检查客户端请求的 quality 与服务器返回的 level 是否匹配
|
||||||
|
if data_level != expected_level:
|
||||||
|
raise FailedException(
|
||||||
|
f"reject unmatched quality: expected={expected_level}, got={data_level}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'url': data["url"].split("?")[0],
|
||||||
|
'quality': quality
|
||||||
|
}
|
||||||
|
100
modules/wy/refresh_login.py
Normal file
100
modules/wy/refresh_login.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: lerdb -
|
||||||
|
# - 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 common.exceptions import FailedException
|
||||||
|
from time import time
|
||||||
|
from random import randint
|
||||||
|
from .encrypt import eapiEncrypt
|
||||||
|
import ujson as json
|
||||||
|
|
||||||
|
logger = log.log("wy_refresh_login")
|
||||||
|
|
||||||
|
|
||||||
|
def cookieStr2Dict(cookieStr):
|
||||||
|
cookieDict = {}
|
||||||
|
for line in cookieStr.split(";"):
|
||||||
|
if line.strip() == "":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
name, value = line.strip().split("=", 1)
|
||||||
|
cookieDict[name] = value
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
return cookieDict
|
||||||
|
|
||||||
|
|
||||||
|
def cookieDict2Str(cookieDict):
|
||||||
|
cookieStr = ""
|
||||||
|
for name, value in cookieDict.items():
|
||||||
|
cookieStr += f"{name}={value}; "
|
||||||
|
return cookieStr
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh(cookie:str):
|
||||||
|
"""
|
||||||
|
网易云刷新登录
|
||||||
|
|
||||||
|
@param cookie: 网易云音乐cookie
|
||||||
|
"""
|
||||||
|
cookie = cookieStr2Dict(cookie)
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"
|
||||||
|
}
|
||||||
|
baseUrl = "http://interface.music.163.com/eapi/"
|
||||||
|
path = "/api/login/token/refresh"
|
||||||
|
header = {
|
||||||
|
"osver": cookie.get("osver", "17.4.1"),
|
||||||
|
"deviceId": cookie.get("deviceId",""),
|
||||||
|
"os": cookie.get("os","ios"),
|
||||||
|
"appver": cookie.get("appver", ("9.0.65" if cookie.get("os") != "pc" else "")),
|
||||||
|
"versioncode": cookie.get("versioncode", "140"),
|
||||||
|
"mobilename": cookie.get("mobilename", ""),
|
||||||
|
"buildver": cookie.get("buildver", str(time())[:10]),
|
||||||
|
"resolution": cookie.get("resolution", "1920x1080"),
|
||||||
|
"__csrf": cookie.get("__csrf", ""),
|
||||||
|
"channel": cookie.get("channel", ""),
|
||||||
|
"requestId": str(time() * 1000)[:13] + "_" + f"{randint(0, 9999):0>4}",
|
||||||
|
}
|
||||||
|
if cookie.get("MUSIC_U"):
|
||||||
|
header["MUSIC_U"] = cookie.get("MUSIC_U")
|
||||||
|
if cookie.get("MUSIC_A"):
|
||||||
|
header["MUSIC_A"] = cookie.get("MUSIC_A")
|
||||||
|
headers["Cookie"] = cookieDict2Str(header)
|
||||||
|
|
||||||
|
req = await Httpx.AsyncRequest(
|
||||||
|
baseUrl + path[5:],
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"headers": headers,
|
||||||
|
"form": eapiEncrypt(path, json.dumps({"header": header, "e_r": False})),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
body = req.json()
|
||||||
|
if int(body["code"]) != 200:
|
||||||
|
raise FailedException("网易云刷新登录失败(code: " + body["code"] + ")")
|
||||||
|
return logger.info("网易云刷新登录成功")
|
||||||
|
|
||||||
|
if (variable.use_cookie_pool):
|
||||||
|
cookies = config.read_config("module.cookiepool.wy")
|
||||||
|
for c in cookies:
|
||||||
|
ref = c.get("refresh_login") if c.get("refresh_login") else {
|
||||||
|
"enable": False,
|
||||||
|
"interval": 86400
|
||||||
|
}
|
||||||
|
if (ref["enable"]):
|
||||||
|
scheduler.append("wy_refresh_login_pooled_" + c["cookie"][:32], refresh, ref["interval"], {"cookie": c["cookie"]})
|
||||||
|
else:
|
||||||
|
c = config.read_config("module.wy.user.cookie")
|
||||||
|
ref = config.read_config("module.wy.user.refresh_login")
|
||||||
|
if (ref["enable"]):
|
||||||
|
scheduler.append("wy_refresh_login", refresh, ref["interval"], {"cookie": c})
|
13
package.json
Normal file
13
package.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "lx-music-api-server",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "一个适配 LX Music 的 API 后端实现",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "python run.py development",
|
||||||
|
"prod": "python run.py production",
|
||||||
|
"install": "pip install -r requirements.txt",
|
||||||
|
"poetry:install": "poetry install",
|
||||||
|
"poetry:development": "poetry run python run.py development",
|
||||||
|
"poetry:production": "poetry run python run.py production"
|
||||||
|
}
|
||||||
|
}
|
1765
poetry.lock
generated
1765
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,22 +1,28 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "lx-music-api-server-python"
|
name = "lx_music_api_server"
|
||||||
version = "2.0.0.beta-9"
|
version = "2.0.0"
|
||||||
description = "一个适配 LX Music 的 API 后端实现"
|
description = "一个适配 LX Music 的 API 后端实现"
|
||||||
authors = ["helloplhm-qwq", "lerdb", "Folltoshe"]
|
authors = ["helloplhm-qwq", "lerdb", "Folltoshe"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.8"
|
||||||
aiohttp = "^3.9.3"
|
aiohttp = "^3.10.10"
|
||||||
pycryptodome = "^3.20.0"
|
pycryptodome = "^3.22.0"
|
||||||
ujson = "^5.9.0"
|
ujson = "^5.10.0"
|
||||||
requests = "^2.31.0"
|
requests = "^2.32.3"
|
||||||
colorlog = "^6.8.2"
|
colorlog = "^6.9.0"
|
||||||
Pygments = "^2.17.2"
|
Pygments = "^2.19.1"
|
||||||
xmltodict = "^0.13.0"
|
xmltodict = "^0.14.2"
|
||||||
pillow = "^10.2.0"
|
pillow = "^10.4.0"
|
||||||
mutagen = "^1.47.0"
|
mutagen = "^1.47.0"
|
||||||
|
colorama = "^0.4.6"
|
||||||
|
ruamel-yaml = "^0.18.10"
|
||||||
|
pybind11 = "^2.13.6"
|
||||||
|
redis = "^5.2.1"
|
||||||
|
hiredis = "^3.1.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
@ -8,3 +8,7 @@ xmltodict
|
|||||||
toml
|
toml
|
||||||
mutagen
|
mutagen
|
||||||
pillow
|
pillow
|
||||||
|
colorama
|
||||||
|
ruamel-yaml
|
||||||
|
redis
|
||||||
|
hiredis
|
||||||
|
BIN
res/icon.ico
Normal file
BIN
res/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
BIN
res/icon.png
Normal file
BIN
res/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
21
run.py
Normal file
21
run.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Script to set environment variables and run different environments.")
|
||||||
|
parser.add_argument(
|
||||||
|
"environment",
|
||||||
|
choices=["production", "development"],
|
||||||
|
nargs="?",
|
||||||
|
default="production",
|
||||||
|
help="Specify the environment to run.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
if args.environment:
|
||||||
|
os.environ["CURRENT_ENV"] = args.environment
|
||||||
|
subprocess.run(["python", "main.py"])
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
11
script.py
Normal file
11
script.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# https://github.com/python-poetry/poetry/issues/241#issuecomment-445434646
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name): # python 3.7+, otherwise define each script manually
|
||||||
|
name = name.replace('_', '-')
|
||||||
|
subprocess.run(
|
||||||
|
['python', '-u', '-m', name] + sys.argv[1:]
|
||||||
|
) # run whatever you like based on 'name'
|
24
setup.py
Normal file
24
setup.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
import toml
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
|
||||||
|
description = toml.load("./pyproject.toml")["tool"]["poetry"]["description"]
|
||||||
|
except Exception:
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Description not available"
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="lx_music_api_server_setup",
|
||||||
|
version=version,
|
||||||
|
scripts=["run.py"],
|
||||||
|
author="helloplhm-qwq",
|
||||||
|
author_email="helloplhm-qwq@outlook.com",
|
||||||
|
description=description,
|
||||||
|
url="https://github.com/helloplhm-qwq/lx-music-api-server",
|
||||||
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
],
|
||||||
|
)
|
24
termux.py
Normal file
24
termux.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# for installing the dependencies on Termux.
|
||||||
|
# ruamel-yaml-clib need to build from source, but it will throw an error during
|
||||||
|
# the build process in default configuration. To fix this, we need to set
|
||||||
|
# CFLAGS environment variable to "-Wno-incompatible-function-pointer-types".
|
||||||
|
# Resolution from: https://github.com/termux/termux-packages/issues/16746
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def is_termux():
|
||||||
|
return "termux" in os.environ.get("HOME", "")
|
||||||
|
|
||||||
|
def install_ruamel_clib():
|
||||||
|
if (is_termux()):
|
||||||
|
# https://github.com/termux/termux-packages/issues/16746
|
||||||
|
os.environ["CFLAGS"] = "-Wno-incompatible-function-pointer-types"
|
||||||
|
subprocess.run(["poetry", "run", "pip", "install", "ruamel-yaml-clib"])
|
||||||
|
|
||||||
|
def install():
|
||||||
|
install_ruamel_clib()
|
||||||
|
subprocess.run(["poetry", "install"])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
install()
|
Loading…
x
Reference in New Issue
Block a user