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 |
4
.github/workflows/build_beta.yml
vendored
4
.github/workflows/build_beta.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
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 }}
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
@ -63,4 +63,4 @@ jobs:
|
||||
run: |
|
||||
echo ${{ secrets.SCRIPT_BASE64 }} | base64 -d > test.py
|
||||
ESCAPED=`python test.py`
|
||||
curl -s -v "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMediaGroup?chat_id=${{ secrets.TELEGRAM_ID }}&media=%5B%7B%22type%22:%22document%22,%20%22media%22:%22attach://windows%22,%22parse_mode%22:%22MarkdownV2%22,%22caption%22:${ESCAPED}%7D%5D" -F windows=@${{ env.windowsFile }}
|
||||
curl -s -v "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMediaGroup?chat_id=${{ secrets.TELEGRAM_ID }}&media=%5B%7B%22type%22:%22document%22,%20%22media%22:%22attach://windows%22,%22parse_mode%22:%22MarkdownV2%22,%22caption%22:${ESCAPED}%7D%5D" -F windows=@${{ env.windowsFile }}
|
||||
|
16
.github/workflows/build_release.yml
vendored
16
.github/workflows/build_release.yml
vendored
@ -59,6 +59,11 @@ jobs:
|
||||
run: |
|
||||
python -m pip install toml
|
||||
echo PACKAGE_VERSION=`python -c 'import toml; print(toml.load("./pyproject.toml")["tool"]["poetry"]["version"])'` >> $GITHUB_ENV
|
||||
|
||||
- name: Generate Changelog
|
||||
run: |
|
||||
git fetch --prune --unshallow --tags
|
||||
python build.py changelog >> ./changelog.md
|
||||
|
||||
- name: Create git tag
|
||||
uses: pkgdeps/git-tag-action@v2
|
||||
@ -76,14 +81,11 @@ jobs:
|
||||
pattern: dist-*
|
||||
merge-multiple: false
|
||||
|
||||
- name: Rename File
|
||||
- name: Rename Files
|
||||
run: |
|
||||
mv ./dist/dist-windows-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }}.exe ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_windows.exe
|
||||
mv ./dist/dist-macos-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }} ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_macos
|
||||
mv ./dist/dist-ubuntu-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }} ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_linux
|
||||
|
||||
# - name: Generate Changelog
|
||||
# run: python build.py changelog >> ./changelog.md
|
||||
mv ./dist/dist-ubuntu-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }} ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_ubuntu
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
@ -95,6 +97,6 @@ jobs:
|
||||
files: |
|
||||
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_windows.exe
|
||||
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_macos
|
||||
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_linux
|
||||
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_ubuntu
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -22,6 +22,9 @@ test.*
|
||||
*/test.*
|
||||
logs
|
||||
config.json
|
||||
config.yml
|
||||
/config/config.json
|
||||
/config/data.db
|
||||
*.log
|
||||
*.bak
|
||||
*.tmp
|
||||
@ -38,4 +41,7 @@ config.json
|
||||
*.un
|
||||
|
||||
# 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">
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/actions/workflows/build_beta.yml)
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases/latest)
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases)
|
||||
[](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE)
|
||||

|
||||
[](https://github.com/MeoProject/lx-music-api-server/actions/workflows/build_beta.yml)
|
||||
[](https://github.com/MeoProject/lx-music-api-server/releases/latest)
|
||||
[](https://github.com/MeoProject/lx-music-api-server/releases)
|
||||
[](https://github.com/MeoProject/lx-music-api-server/blob/main/LICENSE)
|
||||
|
||||
</div>
|
||||
|
||||
原仓库:[lx-music-api-server](https://github.com/lxmusics/lx-music-api-server)
|
||||
你可以在原仓库中找到对应的可用源脚本
|
||||
本项目[文档](https://apidoc.zcmonety.xyz)
|
||||
|
||||
由于使用此项目导致的**封号**等情况**与本项目无关**
|
||||
|
||||
本项目不接受私人定制,非**本项目Github发布**所出现问题**与本项目无关**
|
||||
|
||||
## 💡特点
|
||||
|
||||
- [ ] 功能
|
||||
- [ ] 完整性API(歌单,搜索)
|
||||
- [ ] 网易云刷新登录
|
||||
- [x] 网易云刷新登录
|
||||
- [ ] 服务器向webdav
|
||||
- [x] 客户端播放服务器上的本地音乐
|
||||
- [x] Cookie池
|
||||
@ -36,8 +37,8 @@
|
||||
|
||||
### Release 部署(推荐)
|
||||
|
||||
1. 从 [Releases](https://github.com/lxmusics/lx-music-api-server-python/releases)
|
||||
或 [Actions](https://github.com/lxmusics/lx-music-api-server-python/actions)
|
||||
1. 从 [Releases](https://github.com/MeoProject/lx-music-api-server/releases)
|
||||
或 [Actions](https://github.com/MeoProject/lx-music-api-server/actions)
|
||||
下载对应你系统的可执行文件 (从 GitHub Actions 下载需要登录 GitHub 账号)
|
||||
|
||||
2. 运行可执行文件(如果下载的文件是压缩包请先解压)
|
||||
@ -57,8 +58,8 @@
|
||||
2. clone本项目并进入项目目录
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lxmusics/lx-music-api-server-python.git
|
||||
cd lx-music-api-server-python
|
||||
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||
cd lx-music-api-server
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
@ -87,8 +88,8 @@ linux 命令如果为 python3 请自行替换
|
||||
1. clone本项目并进入项目目录
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lxmusics/lx-music-api-server-python.git
|
||||
cd lx-music-api-server-python
|
||||
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||
cd lx-music-api-server
|
||||
```
|
||||
|
||||
2. 安装依赖
|
||||
@ -103,40 +104,6 @@ linux 命令如果为 python3 请自行替换
|
||||
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`字段值中的代码含义
|
||||
@ -241,7 +208,7 @@ linux 命令如果为 python3 请自行替换
|
||||
|
||||
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. 不明所以的变量名
|
||||
|
||||
@ -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 趋势图
|
||||
|
||||
[](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">
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/actions/workflows/build_beta.yml)
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases/latest)
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/releases)
|
||||
[](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE)
|
||||

|
||||
[](https://github.com/MeoProject/lx-music-api-server/actions/workflows/build_beta.yml)
|
||||
[](https://github.com/MeoProject/lx-music-api-server/releases/latest)
|
||||
[](https://github.com/MeoProject/lx-music-api-server/releases)
|
||||
[](https://github.com/MeoProject/lx-music-api-server/blob/main/LICENSE)
|
||||
|
||||
</div>
|
||||
|
||||
Original Repo: [lx-music-api-server](https://github.com/lxmusics/lx-music-api-server)
|
||||
You can find the corresponding available source scripts in the original repository.
|
||||
This project [Document](https://apidoc.zcmonety.xyz)
|
||||
|
||||
**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
|
||||
|
||||
- [ ] Functions
|
||||
- [ ] All APIs (playlist, search)
|
||||
- [ ] Netease Cloud Refresh Login
|
||||
- [x] Netease Cloud Refresh Login
|
||||
- [ ] Client plays music on webdav
|
||||
- [x] Client plays local music on the server
|
||||
- [x] Cookie pool
|
||||
@ -36,7 +36,7 @@ You can find the corresponding available source scripts in the original reposito
|
||||
|
||||
### 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)
|
||||
|
||||
@ -55,8 +55,8 @@ Required environment: Python 3.8+
|
||||
2. Clone this project and enter the project directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lxmusics/lx-music-api-server-python.git
|
||||
cd lx-music-api-server-python
|
||||
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||
cd lx-music-api-server
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lxmusics/lx-music-api-server-python.git
|
||||
cd lx-music-api-server-python
|
||||
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||
cd lx-music-api-server
|
||||
```
|
||||
|
||||
2. Install requirements
|
||||
@ -101,40 +101,6 @@ If you are using linux, you command maybe python3, please replace it yourself.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@ -256,7 +222,7 @@ The code meaning of `statuscode` returned by the interface.
|
||||
|
||||
## 📄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.
|
||||
|
||||
@ -276,8 +242,8 @@ folltoshe+foxmail.com
|
||||
|
||||
## ✨Star trend chart
|
||||
|
||||
[](https://starchart.cc/lxmusics/lx-music-api-server-python)
|
||||
[](https://starchart.cc/MeoProject/lx-music-api-server)
|
||||
|
||||
## ⚙️Contributor
|
||||
|
||||
[](https://github.com/lxmusics/lx-music-api-server-python/graphs/contributors)
|
||||
[](https://github.com/MeoProject/lx-music-api-server/graphs/contributors)
|
||||
|
22
build.py
22
build.py
@ -6,11 +6,11 @@ import os
|
||||
|
||||
|
||||
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]
|
||||
|
||||
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 = []
|
||||
for i in r:
|
||||
if (i):
|
||||
@ -30,17 +30,17 @@ def get_changelog():
|
||||
noticeMsg = []
|
||||
unknownMsg = []
|
||||
for msg in res:
|
||||
if (re.match('[a-f0-9]*.(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|notice)\:', msg[1:-1])):
|
||||
if (re.match('[a-f0-9]*.(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|notice).*?\(?.*?\)?\:', msg[1:-1])):
|
||||
msg = msg[1:-1]
|
||||
if msg[8:].startswith('notice:'):
|
||||
if msg[8:].startswith('notice'):
|
||||
noticeMsg.append(msg)
|
||||
elif msg[8:].startswith('feat:'):
|
||||
elif msg[8:].startswith('feat'):
|
||||
featMsg.append(msg)
|
||||
elif msg[8:].startswith('fix:'):
|
||||
elif msg[8:].startswith('fix'):
|
||||
fixMsg.append(msg)
|
||||
elif msg[8:].startswith('docs:'):
|
||||
elif msg[8:].startswith('docs'):
|
||||
docsMsg.append(msg)
|
||||
elif msg[8:].startswith('build:'):
|
||||
elif msg[8:].startswith('build'):
|
||||
buildMsg.append(msg)
|
||||
else:
|
||||
otherMsg.append(msg)
|
||||
@ -104,6 +104,8 @@ def build_test(fileName):
|
||||
'-m',
|
||||
'PyInstaller',
|
||||
'-F',
|
||||
'-i',
|
||||
'res/icon.ico',
|
||||
'--name',
|
||||
fileName if fileName else f'lx-music-api-server_{sha}',
|
||||
'main.py'])
|
||||
@ -137,6 +139,8 @@ def build_release(fileName = ''):
|
||||
'-m',
|
||||
'PyInstaller',
|
||||
'-F',
|
||||
'-i',
|
||||
'res/icon.ico',
|
||||
'--name',
|
||||
fileName if fileName else f'lx-music-api-server_{vername}',
|
||||
'main.py'])
|
||||
@ -227,4 +231,4 @@ if __name__ == '__main__':
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print('[INFO] Aborting...')
|
||||
sys.exit(0)
|
||||
sys.exit(0)
|
||||
|
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: 更新版本号
|
251
common/Httpx.py
251
common/Httpx.py
@ -1,9 +1,9 @@
|
||||
# ----------------------------------------
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: Httpx.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: Httpx.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# ----------------------------------------
|
||||
# This file is part of the "lx-music-api-server" project.
|
||||
|
||||
@ -21,15 +21,16 @@ from . import config
|
||||
from . import utils
|
||||
from . import variable
|
||||
|
||||
|
||||
def is_valid_utf8(text) -> bool:
|
||||
try:
|
||||
if isinstance(text, bytes):
|
||||
text = text.decode('utf-8')
|
||||
text = text.decode("utf-8")
|
||||
# 判断是否为有效的utf-8字符串
|
||||
if "\ufffe" in text:
|
||||
return False
|
||||
try:
|
||||
text.encode('utf-8').decode('utf-8')
|
||||
text.encode("utf-8").decode("utf-8")
|
||||
return True
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
@ -37,42 +38,48 @@ def is_valid_utf8(text) -> bool:
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
|
||||
def is_plain_text(text) -> bool:
|
||||
# 判断是否为纯文本
|
||||
pattern = re.compile(r'[^\x00-\x7F]')
|
||||
pattern = re.compile(r"[^\x00-\x7F]")
|
||||
return not bool(pattern.search(text))
|
||||
|
||||
|
||||
def convert_dict_to_form_string(dic: dict) -> str:
|
||||
# 将字典转换为表单字符串
|
||||
return '&'.join([f'{k}={v}' for k, v in dic.items()])
|
||||
return "&".join([f"{k}={v}" for k, v in dic.items()])
|
||||
|
||||
|
||||
def log_plaintext(text: str) -> str:
|
||||
if (text.startswith('{') and text.endswith('}')):
|
||||
if text.startswith("{") and text.endswith("}"):
|
||||
try:
|
||||
text = json.loads(text)
|
||||
except:
|
||||
pass
|
||||
elif (text.startswith('<xml') and text.endswith('>')): # xml data
|
||||
elif text.startswith("<xml") and text.endswith(">"): # xml data
|
||||
try:
|
||||
text = f'xml: {utils.load_xml(text)}'
|
||||
text = f"xml: {utils.load_xml(text)}"
|
||||
except:
|
||||
pass
|
||||
return text
|
||||
|
||||
|
||||
# 内置的UA列表
|
||||
ua_list = [ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0 uacq',
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 uacq',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'
|
||||
]
|
||||
ua_list = [
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0 uacq",
|
||||
"Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 uacq",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
|
||||
]
|
||||
|
||||
# 日志记录器
|
||||
logger = log.log('http_utils')
|
||||
logger = log.log("http_utils")
|
||||
|
||||
def request(url: str, options = {}) -> requests.Response:
|
||||
'''
|
||||
|
||||
def request(url: str, options={}) -> requests.Response:
|
||||
"""
|
||||
Http请求主函数, 用于发送网络请求
|
||||
- url: 需要请求的URL地址(必填)
|
||||
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
|
||||
@ -84,15 +91,15 @@ def request(url: str, options = {}) -> requests.Response:
|
||||
- no-cache: 不缓存
|
||||
- <int>: 缓存可用秒数
|
||||
- cache-ignore: <list> 缓存忽略关键字
|
||||
|
||||
|
||||
@ return: requests.Response类型的响应数据
|
||||
'''
|
||||
"""
|
||||
# 缓存读取
|
||||
cache_key = f'{url}{options}'
|
||||
if (isinstance(options.get('cache-ignore'), list)):
|
||||
for i in options.get('cache-ignore'):
|
||||
cache_key = cache_key.replace(str(i), '')
|
||||
options.pop('cache-ignore')
|
||||
cache_key = f"{url}{options}"
|
||||
if isinstance(options.get("cache-ignore"), list):
|
||||
for i in options.get("cache-ignore"):
|
||||
cache_key = cache_key.replace(str(i), "")
|
||||
options.pop("cache-ignore")
|
||||
cache_key = utils.createMD5(cache_key)
|
||||
if options.get("cache") and options["cache"] != "no-cache":
|
||||
cache = config.getCache("httpx", cache_key)
|
||||
@ -104,77 +111,84 @@ def request(url: str, options = {}) -> requests.Response:
|
||||
options.pop("cache")
|
||||
else:
|
||||
cache_info = None
|
||||
|
||||
|
||||
# 获取请求方法,没有则默认为GET请求
|
||||
try:
|
||||
method = options['method']
|
||||
options.pop('method')
|
||||
method = options["method"].upper()
|
||||
options.pop("method")
|
||||
except Exception as e:
|
||||
method = 'GET'
|
||||
method = "GET"
|
||||
# 获取User-Agent,没有则从ua_list中随机选择一个
|
||||
try:
|
||||
d_lower = {k.lower(): v for k, v in options['headers'].items()}
|
||||
useragent = d_lower['user-agent']
|
||||
d_lower = {k.lower(): v for k, v in options["headers"].items()}
|
||||
useragent = d_lower["user-agent"]
|
||||
except:
|
||||
try:
|
||||
options['headers']['User-Agent'] = random.choice(ua_list)
|
||||
options["headers"]["User-Agent"] = random.choice(ua_list)
|
||||
except:
|
||||
options['headers'] = {}
|
||||
options['headers']['User-Agent'] = random.choice(ua_list)
|
||||
options["headers"] = {}
|
||||
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
|
||||
# 获取请求主函数
|
||||
try:
|
||||
reqattr = getattr(requests, method.lower())
|
||||
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头
|
||||
if (method == 'POST') or (method == 'PUT'):
|
||||
if options.get('body'):
|
||||
options['data'] = options['body']
|
||||
options.pop('body')
|
||||
if options.get('form'):
|
||||
options['data'] = convert_dict_to_form_string(options['form'])
|
||||
options.pop('form')
|
||||
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||
if (isinstance(options['data'], dict)):
|
||||
options['data'] = json.dumps(options['data'])
|
||||
if (method == "POST") or (method == "PUT"):
|
||||
if options.get("body"):
|
||||
options["data"] = options["body"]
|
||||
options.pop("body")
|
||||
if options.get("form"):
|
||||
options["data"] = convert_dict_to_form_string(options["form"])
|
||||
options.pop("form")
|
||||
options["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
if isinstance(options["data"], dict):
|
||||
options["data"] = json.dumps(options["data"])
|
||||
# 进行请求
|
||||
try:
|
||||
logger.info("-----start----- " + url)
|
||||
req = reqattr(url, **options)
|
||||
except Exception as e:
|
||||
logger.error(f'HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}')
|
||||
logger.error(f"HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}")
|
||||
raise e
|
||||
# 请求后记录
|
||||
logger.debug(f'Request to {url} succeed with code {req.status_code}')
|
||||
if (req.content.startswith(b'\x78\x9c') or req.content.startswith(b'\x78\x01')): # zlib headers
|
||||
logger.debug(f"Request to {url} succeed with code {req.status_code}")
|
||||
if req.content.startswith(b"\x78\x9c") or req.content.startswith(b"\x78\x01"): # zlib headers
|
||||
try:
|
||||
decompressed = zlib.decompress(req.content)
|
||||
if (is_valid_utf8(decompressed)):
|
||||
if is_valid_utf8(decompressed):
|
||||
logger.debug(log_plaintext(decompressed.decode("utf-8")))
|
||||
else:
|
||||
logger.debug('response is not text binary, ignore logging it')
|
||||
logger.debug("response is not text binary, ignore logging it")
|
||||
except:
|
||||
logger.debug('response is not text binary, ignore logging it')
|
||||
logger.debug("response is not text binary, ignore logging it")
|
||||
else:
|
||||
if (is_valid_utf8(req.content)):
|
||||
if is_valid_utf8(req.content):
|
||||
logger.debug(log_plaintext(req.content.decode("utf-8")))
|
||||
else:
|
||||
logger.debug('response is not text binary, ignore logging it')
|
||||
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)
|
||||
expire_time = (cache_info if isinstance(cache_info, int) else 3600) + int(time.time())
|
||||
config.updateCache("httpx", cache_key, {"expire": True, "time": expire_time, "data": utils.createBase64Encode(cache_data)})
|
||||
expire_time = cache_info if isinstance(cache_info, int) else 3600
|
||||
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)
|
||||
|
||||
def _json():
|
||||
return json.loads(req.content)
|
||||
setattr(req, 'json', _json)
|
||||
|
||||
setattr(req, "json", _json)
|
||||
# 返回请求
|
||||
return req
|
||||
|
||||
@ -184,22 +198,25 @@ def checkcn():
|
||||
req = request("https://mips.kugou.com/check/iscn?&format=json")
|
||||
body = utils.CreateObject(req.json())
|
||||
variable.iscn = bool(body.flag)
|
||||
if (not variable.iscn):
|
||||
variable.fakeip = config.read_config('common.fakeip')
|
||||
if not variable.iscn:
|
||||
variable.fakeip = config.read_config("common.fakeip")
|
||||
logger.info(f"您在非中国大陆服务器({body.country})上启动了项目,已自动开启ip伪装")
|
||||
logger.warning("此方式无法解决咪咕音乐的链接获取问题,您可以配置代理,服务器地址可在下方链接中找到\nhttps://hidemy.io/cn/proxy-list/?country=CN#list")
|
||||
logger.warning(
|
||||
"此方式无法解决咪咕音乐的链接获取问题,您可以配置代理,服务器地址可在下方链接中找到\nhttps://hidemy.io/cn/proxy-list/?country=CN#list"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning('检查服务器位置失败,已忽略')
|
||||
logger.warning("检查服务器位置失败,已忽略")
|
||||
logger.warning(traceback.format_exc())
|
||||
|
||||
|
||||
class ClientResponse:
|
||||
# 这个类为了方便aiohttp响应与requests响应的跨类使用,也为了解决pickle无法缓存的问题
|
||||
def __init__(self, status, content, headers):
|
||||
self.status = status
|
||||
self.content = content
|
||||
self.headers = headers
|
||||
self.text = content.decode("utf-8", errors='ignore')
|
||||
|
||||
self.text = content.decode("utf-8", errors="ignore")
|
||||
|
||||
def json(self):
|
||||
return json.loads(self.content)
|
||||
|
||||
@ -208,11 +225,12 @@ async def convert_to_requests_response(aiohttp_response) -> ClientResponse:
|
||||
content = await aiohttp_response.content.read() # 从aiohttp响应中读取字节数据
|
||||
status_code = aiohttp_response.status # 获取状态码
|
||||
headers = dict(aiohttp_response.headers.items()) # 获取标头信息并转换为字典
|
||||
|
||||
|
||||
return ClientResponse(status_code, content, headers)
|
||||
|
||||
async def AsyncRequest(url, options = {}) -> ClientResponse:
|
||||
'''
|
||||
|
||||
async def AsyncRequest(url, options={}) -> ClientResponse:
|
||||
"""
|
||||
Http异步请求主函数, 用于发送网络请求
|
||||
- url: 需要请求的URL地址(必填)
|
||||
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
|
||||
@ -224,17 +242,17 @@ async def AsyncRequest(url, options = {}) -> ClientResponse:
|
||||
- no-cache: 不缓存
|
||||
- <int>: 缓存可用秒数
|
||||
- cache-ignore: <list> 缓存忽略关键字
|
||||
|
||||
|
||||
@ return: common.Httpx.ClientResponse类型的响应数据
|
||||
'''
|
||||
if (not variable.aioSession):
|
||||
"""
|
||||
if not variable.aioSession:
|
||||
variable.aioSession = aiohttp.ClientSession(trust_env=True)
|
||||
# 缓存读取
|
||||
cache_key = f'{url}{options}'
|
||||
if (isinstance(options.get('cache-ignore'), list)):
|
||||
for i in options.get('cache-ignore'):
|
||||
cache_key = cache_key.replace(str(i), '')
|
||||
options.pop('cache-ignore')
|
||||
cache_key = f"{url}{options}"
|
||||
if isinstance(options.get("cache-ignore"), list):
|
||||
for i in options.get("cache-ignore"):
|
||||
cache_key = cache_key.replace(str(i), "")
|
||||
options.pop("cache-ignore")
|
||||
cache_key = utils.createMD5(cache_key)
|
||||
if options.get("cache") and options["cache"] != "no-cache":
|
||||
cache = config.getCache("httpx_async", cache_key)
|
||||
@ -247,76 +265,81 @@ async def AsyncRequest(url, options = {}) -> ClientResponse:
|
||||
options.pop("cache")
|
||||
else:
|
||||
cache_info = None
|
||||
|
||||
|
||||
# 获取请求方法,没有则默认为GET请求
|
||||
try:
|
||||
method = options['method']
|
||||
options.pop('method')
|
||||
method = options["method"]
|
||||
options.pop("method")
|
||||
except Exception as e:
|
||||
method = 'GET'
|
||||
method = "GET"
|
||||
# 获取User-Agent,没有则从ua_list中随机选择一个
|
||||
try:
|
||||
d_lower = {k.lower(): v for k, v in options['headers'].items()}
|
||||
useragent = d_lower['user-agent']
|
||||
d_lower = {k.lower(): v for k, v in options["headers"].items()}
|
||||
useragent = d_lower["user-agent"]
|
||||
except:
|
||||
try:
|
||||
options['headers']['User-Agent'] = random.choice(ua_list)
|
||||
options["headers"]["User-Agent"] = random.choice(ua_list)
|
||||
except:
|
||||
options['headers'] = {}
|
||||
options['headers']['User-Agent'] = random.choice(ua_list)
|
||||
options["headers"] = {}
|
||||
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
|
||||
# 获取请求主函数
|
||||
try:
|
||||
reqattr = getattr(variable.aioSession, method.lower())
|
||||
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头
|
||||
if (method == 'POST') or (method == 'PUT'):
|
||||
if options.get('body'):
|
||||
options['data'] = options['body']
|
||||
options.pop('body')
|
||||
if options.get('form'):
|
||||
options['data'] = convert_dict_to_form_string(options['form'])
|
||||
options.pop('form')
|
||||
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||
if (isinstance(options['data'], dict)):
|
||||
options['data'] = json.dumps(options['data'])
|
||||
if (method == "POST") or (method == "PUT"):
|
||||
if options.get("body") is not None:
|
||||
options["data"] = options["body"]
|
||||
options.pop("body")
|
||||
if options.get("form") is not None:
|
||||
options["data"] = convert_dict_to_form_string(options["form"])
|
||||
options.pop("form")
|
||||
options["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
if isinstance(options.get("data"), dict):
|
||||
options["data"] = json.dumps(options["data"])
|
||||
# 进行请求
|
||||
try:
|
||||
logger.info("-----start----- " + url)
|
||||
req_ = await reqattr(url, **options)
|
||||
except Exception as e:
|
||||
logger.error(f'HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}')
|
||||
logger.error(f"HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}")
|
||||
raise e
|
||||
# 请求后记录
|
||||
logger.debug(f'Request to {url} succeed with code {req_.status}')
|
||||
logger.debug(f"Request to {url} succeed with code {req_.status}")
|
||||
# 为懒人提供的不用改代码移植的方法
|
||||
# 才不是梓澄呢
|
||||
req = await convert_to_requests_response(req_)
|
||||
if (req.content.startswith(b'\x78\x9c') or req.content.startswith(b'\x78\x01')): # zlib headers
|
||||
if req.content.startswith(b"\x78\x9c") or req.content.startswith(b"\x78\x01"): # zlib headers
|
||||
try:
|
||||
decompressed = zlib.decompress(req.content)
|
||||
if (is_valid_utf8(decompressed)):
|
||||
if is_valid_utf8(decompressed):
|
||||
logger.debug(log_plaintext(decompressed.decode("utf-8")))
|
||||
else:
|
||||
logger.debug('response is not text binary, ignore logging it')
|
||||
logger.debug("response is not text binary, ignore logging it")
|
||||
except:
|
||||
logger.debug('response is not text binary, ignore logging it')
|
||||
logger.debug("response is not text binary, ignore logging it")
|
||||
else:
|
||||
if (is_valid_utf8(req.content)):
|
||||
if is_valid_utf8(req.content):
|
||||
logger.debug(log_plaintext(req.content.decode("utf-8")))
|
||||
else:
|
||||
logger.debug('response is not text binary, ignore logging it')
|
||||
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)
|
||||
expire_time = (cache_info if isinstance(cache_info, int) else 3600) + int(time.time())
|
||||
config.updateCache("httpx_async", cache_key, {"expire": True, "time": expire_time, "data": utils.createBase64Encode(cache_data)})
|
||||
expire_time = cache_info if isinstance(cache_info, int) else 3600
|
||||
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)
|
||||
# 返回请求
|
||||
return req
|
||||
return req
|
||||
|
660
common/config.py
660
common/config.py
@ -13,321 +13,70 @@ import os
|
||||
import traceback
|
||||
import sys
|
||||
import sqlite3
|
||||
import shutil
|
||||
import ruamel.yaml as yaml_
|
||||
from . import variable
|
||||
from .log import log
|
||||
from . import default_config
|
||||
import threading
|
||||
import redis
|
||||
|
||||
logger = log('config_manager')
|
||||
logger = log("config_manager")
|
||||
|
||||
# 创建线程本地存储对象
|
||||
local_data = threading.local()
|
||||
local_cache = threading.local()
|
||||
local_redis = threading.local()
|
||||
|
||||
|
||||
def get_data_connection():
|
||||
# 检查线程本地存储对象是否存在连接对象,如果不存在则创建一个新的连接对象
|
||||
if not hasattr(local_data, 'connection'):
|
||||
local_data.connection = sqlite3.connect('data.db')
|
||||
return local_data.connection
|
||||
|
||||
|
||||
# 创建线程本地存储对象
|
||||
local_cache = threading.local()
|
||||
|
||||
|
||||
def get_cache_connection():
|
||||
# 检查线程本地存储对象是否存在连接对象,如果不存在则创建一个新的连接对象
|
||||
if not hasattr(local_cache, 'connection'):
|
||||
local_cache.connection = sqlite3.connect('cache.db')
|
||||
return local_cache.connection
|
||||
|
||||
|
||||
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):
|
||||
pass
|
||||
|
||||
|
||||
default = {
|
||||
"common": {
|
||||
"host": "0.0.0.0",
|
||||
"_host-desc": "服务器启动时所使用的HOST地址",
|
||||
"ports": [ 9763 ],
|
||||
"_ports-desc": "服务器启动时所使用的端口",
|
||||
"ssl_info": {
|
||||
"desc": "服务器https配置,is_https是这个服务器是否是https服务器,如果你使用了反向代理来转发这个服务器,如果它使用了https,也请将它设置为true",
|
||||
"is_https": False,
|
||||
"enable": False,
|
||||
"ssl_ports": [ 443 ],
|
||||
"path": {
|
||||
"desc": "ssl证书的文件地址",
|
||||
"cert": "/path/to/your/cer",
|
||||
"privkey": "/path/to/your/private/key",
|
||||
},
|
||||
},
|
||||
"reverse_proxy": {
|
||||
"desc": "针对类似于nginx一类的反代的配置",
|
||||
"allow_proxy": True,
|
||||
"_allow_proxy-desc": "是否允许反代",
|
||||
"proxy_whitelist_remote": [
|
||||
"反代时允许的ip来源列表,通常为127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"real_ip_header": 'X-Real-IP',
|
||||
"_real_ip_header-desc": "反代来源ip的来源头,不懂请保持默认",
|
||||
},
|
||||
"debug_mode": False,
|
||||
"_debug_mode-desc": "是否开启调试模式",
|
||||
"log_length_limit": 500,
|
||||
"_log_length_limit-desc": "单条日志长度限制",
|
||||
"fakeip": "1.0.1.114",
|
||||
"_fakeip-desc": "服务器在海外时的IP伪装值",
|
||||
"proxy": {
|
||||
"enable": False,
|
||||
"http_value": "http://127.0.0.1:7890",
|
||||
"https_value": "http://127.0.0.1:7890",
|
||||
},
|
||||
"_proxy-desc": "代理配置,HTTP与HTTPS协议需分开配置",
|
||||
"log_file": True,
|
||||
"_log_file-desc": "是否开启日志文件",
|
||||
'cookiepool': False,
|
||||
'_cookiepool-desc': '是否开启cookie池,这将允许用户配置多个cookie并在请求时随机使用一个,启用后请在module.cookiepool中配置cookie,在user处配置的cookie会被忽略,cookiepool中格式统一为列表嵌套user处的cookie的字典',
|
||||
"allow_download_script": True,
|
||||
'_allow_download_script-desc': '是否允许直接从服务端下载脚本,开启后可以直接访问 /script?key=你的请求key 下载脚本',
|
||||
"download_config": {
|
||||
"desc": "源脚本的相关配置,dev为是否启用开发模式",
|
||||
"name": "修改为你的源脚本名称",
|
||||
"intro": "修改为你的源脚本描述",
|
||||
"author": "修改为你的源脚本作者",
|
||||
"version": "修改为你的源版本",
|
||||
"filename": "lx-music-source.js",
|
||||
"_filename-desc": "客户端保存脚本时的文件名(可能因浏览器不同出现不一样的情况)",
|
||||
"dev": True,
|
||||
"quality": {
|
||||
"kw": ["128k"],
|
||||
"kg": ["128k"],
|
||||
"tx": ["128k"],
|
||||
"wy": ["128k"],
|
||||
"mg": ["128k"],
|
||||
}
|
||||
},
|
||||
"local_music": {
|
||||
"desc": "服务器侧本地音乐相关配置,请确保你的带宽足够",
|
||||
"audio_path": "./audio",
|
||||
"temp_path": "./temp",
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"rate_limit": {
|
||||
"global": 0,
|
||||
"ip": 0,
|
||||
"desc": "请求速率限制,global为全局,ip为单个ip,填入的值为至少间隔多久才能进行一次请求,单位:秒,不限制请填为0"
|
||||
},
|
||||
"key": {
|
||||
"enable": False,
|
||||
"_enable-desc": "是否开启请求key,开启后只有请求头中包含key,且值一样时可以访问API",
|
||||
"ban": True,
|
||||
"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",
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
yaml = yaml_.YAML()
|
||||
default_str = default_config.default
|
||||
default = yaml.load(default_str)
|
||||
|
||||
|
||||
def handle_default_config():
|
||||
with open("./config.json", "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(default, indent=2, ensure_ascii=False,
|
||||
escape_forward_slashes=False))
|
||||
f.close()
|
||||
if (not os.getenv('build')):
|
||||
logger.info('首次启动或配置文件被删除,已创建默认配置文件')
|
||||
with open("./config/config.yml", "w", encoding="utf-8") as f:
|
||||
f.write(default_str)
|
||||
if not os.getenv("build"):
|
||||
logger.info(
|
||||
f'\n建议您到{variable.workdir + os.path.sep}config.json修改配置后重新启动服务器')
|
||||
f"首次启动或配置文件被删除,已创建默认配置文件\n建议您到{variable.workdir + os.path.sep}config.yml修改配置后重新启动服务器"
|
||||
)
|
||||
return default
|
||||
|
||||
|
||||
@ -368,8 +117,7 @@ def save_data(config_data):
|
||||
|
||||
# Insert the new configuration data into the 'data' table
|
||||
for key, value in config_data.items():
|
||||
cursor.execute(
|
||||
"INSERT INTO data (key, value) VALUES (?, ?)", (key, json.dumps(value)))
|
||||
cursor.execute("INSERT INTO data (key, value) VALUES (?, ?)", (key, json.dumps(value)))
|
||||
|
||||
conn.commit()
|
||||
|
||||
@ -378,51 +126,69 @@ def save_data(config_data):
|
||||
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):
|
||||
try:
|
||||
# 连接到数据库(如果数据库不存在,则会自动创建)
|
||||
conn = get_cache_connection()
|
||||
|
||||
# 创建一个游标对象
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT data FROM cache WHERE module=? AND key=?",
|
||||
(module, key))
|
||||
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
cache_data = json.loads(result[0])
|
||||
cache_data["time"] = int(cache_data["time"])
|
||||
if (not cache_data['expire']):
|
||||
return cache_data
|
||||
if (int(time.time()) < int(cache_data['time'])):
|
||||
if read_config("common.cache.adapter") == "redis":
|
||||
redis = get_redis_connection()
|
||||
key = handleBuildRedisKey(module, key)
|
||||
result = redis.get(key)
|
||||
if result:
|
||||
cache_data = json.loads(result)
|
||||
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:
|
||||
pass
|
||||
# traceback.print_exc()
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def updateCache(module, key, data):
|
||||
def updateCache(module, key, data, expire=None):
|
||||
try:
|
||||
# 连接到数据库(如果数据库不存在,则会自动创建)
|
||||
conn = get_cache_connection()
|
||||
|
||||
# 创建一个游标对象
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"SELECT data FROM cache WHERE module=? AND key=?", (module, key))
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
cursor.execute(
|
||||
"UPDATE cache SET data = ? WHERE module = ? AND key = ?", (json.dumps(data), module, key))
|
||||
if read_config("common.cache.adapter") == "redis":
|
||||
redis = get_redis_connection()
|
||||
key = handleBuildRedisKey(module, key)
|
||||
redis.set(key, json.dumps(data), ex=expire if expire and expire > 0 else None)
|
||||
else:
|
||||
cursor.execute(
|
||||
"INSERT INTO cache (module, key, data) VALUES (?, ?, ?)", (module, key, json.dumps(data)))
|
||||
conn.commit()
|
||||
# 连接到数据库(如果数据库不存在,则会自动创建)
|
||||
conn = get_cache_connection()
|
||||
|
||||
# 创建一个游标对象
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT data FROM cache WHERE module=? AND key=?", (module, key))
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
cursor.execute(
|
||||
"UPDATE cache SET data = ? WHERE module = ? AND key = ?", (json.dumps(data), module, key)
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
"INSERT INTO cache (module, key, data) VALUES (?, ?, ?)", (module, key, json.dumps(data))
|
||||
)
|
||||
conn.commit()
|
||||
except:
|
||||
logger.error('缓存写入遇到错误…')
|
||||
logger.error("缓存写入遇到错误…")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
@ -430,13 +196,13 @@ def resetRequestTime(ip):
|
||||
config_data = load_data()
|
||||
try:
|
||||
try:
|
||||
config_data['requestTime'][ip] = 0
|
||||
config_data["requestTime"][ip] = 0
|
||||
except KeyError:
|
||||
config_data['requestTime'] = {}
|
||||
config_data['requestTime'][ip] = 0
|
||||
config_data["requestTime"] = {}
|
||||
config_data["requestTime"][ip] = 0
|
||||
save_data(config_data)
|
||||
except:
|
||||
logger.error('配置写入遇到错误…')
|
||||
logger.error("配置写入遇到错误…")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
@ -444,20 +210,20 @@ def updateRequestTime(ip):
|
||||
try:
|
||||
config_data = load_data()
|
||||
try:
|
||||
config_data['requestTime'][ip] = time.time()
|
||||
config_data["requestTime"][ip] = time.time()
|
||||
except KeyError:
|
||||
config_data['requestTime'] = {}
|
||||
config_data['requestTime'][ip] = time.time()
|
||||
config_data["requestTime"] = {}
|
||||
config_data["requestTime"][ip] = time.time()
|
||||
save_data(config_data)
|
||||
except:
|
||||
logger.error('配置写入遇到错误...')
|
||||
logger.error("配置写入遇到错误...")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
def getRequestTime(ip):
|
||||
config_data = load_data()
|
||||
try:
|
||||
value = config_data['requestTime'][ip]
|
||||
value = config_data["requestTime"][ip]
|
||||
except:
|
||||
value = 0
|
||||
return value
|
||||
@ -465,7 +231,7 @@ def getRequestTime(ip):
|
||||
|
||||
def read_data(key):
|
||||
config = load_data()
|
||||
keys = key.split('.')
|
||||
keys = key.split(".")
|
||||
value = config
|
||||
for k in keys:
|
||||
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):
|
||||
config = load_data()
|
||||
|
||||
keys = key.split('.')
|
||||
keys = key.split(".")
|
||||
current = config
|
||||
for k in keys[:-1]:
|
||||
if k not in current:
|
||||
@ -495,7 +261,7 @@ def write_data(key, value):
|
||||
def push_to_list(key, obj):
|
||||
config = load_data()
|
||||
|
||||
keys = key.split('.')
|
||||
keys = key.split(".")
|
||||
current = config
|
||||
for k in keys[:-1]:
|
||||
if k not in current:
|
||||
@ -512,10 +278,10 @@ def push_to_list(key, obj):
|
||||
|
||||
def write_config(key, value):
|
||||
config = None
|
||||
with open('config.json', 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
with open("./config/config.yml", "r", encoding="utf-8") as f:
|
||||
config = yaml_.YAML().load(f)
|
||||
|
||||
keys = key.split('.')
|
||||
keys = key.split(".")
|
||||
current = config
|
||||
for k in keys[:-1]:
|
||||
if k not in current:
|
||||
@ -523,17 +289,21 @@ def write_config(key, value):
|
||||
current = current[k]
|
||||
|
||||
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,
|
||||
escape_forward_slashes=False)
|
||||
f.close()
|
||||
|
||||
# 设置保留注释和空行的参数
|
||||
y = yaml_.YAML()
|
||||
y.preserve_quotes = True
|
||||
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):
|
||||
try:
|
||||
config = default
|
||||
keys = key.split('.')
|
||||
keys = key.split(".")
|
||||
value = config
|
||||
for k in keys:
|
||||
if isinstance(value, dict):
|
||||
@ -554,7 +324,7 @@ def read_default_config(key):
|
||||
def _read_config(key):
|
||||
try:
|
||||
config = variable.config
|
||||
keys = key.split('.')
|
||||
keys = key.split(".")
|
||||
value = config
|
||||
for k in keys:
|
||||
if isinstance(value, dict):
|
||||
@ -575,7 +345,7 @@ def _read_config(key):
|
||||
def read_config(key):
|
||||
try:
|
||||
config = variable.config
|
||||
keys = key.split('.')
|
||||
keys = key.split(".")
|
||||
value = config
|
||||
for k in keys:
|
||||
if isinstance(value, dict):
|
||||
@ -591,23 +361,23 @@ def read_config(key):
|
||||
return value
|
||||
except:
|
||||
default_value = read_default_config(key)
|
||||
if (isinstance(default_value, type(None))):
|
||||
logger.warning(f'配置文件{key}不存在')
|
||||
if isinstance(default_value, type(None)):
|
||||
logger.warning(f"配置文件{key}不存在")
|
||||
else:
|
||||
for i in range(len(keys)):
|
||||
tk = '.'.join(keys[:(i + 1)])
|
||||
tk = ".".join(keys[: (i + 1)])
|
||||
tkvalue = _read_config(tk)
|
||||
logger.debug(f'configfix: 读取配置文件{tk}的值:{tkvalue}')
|
||||
if ((tkvalue is None) or (tkvalue == {})):
|
||||
logger.debug(f"configfix: 读取配置文件{tk}的值:{tkvalue}")
|
||||
if (tkvalue is None) or (tkvalue == {}):
|
||||
write_config(tk, read_default_config(tk))
|
||||
logger.info(f'配置文件{tk}不存在,已创建')
|
||||
logger.info(f"配置文件{tk}不存在,已创建")
|
||||
return default_value
|
||||
|
||||
|
||||
def write_data(key, value):
|
||||
config = load_data()
|
||||
|
||||
keys = key.split('.')
|
||||
keys = key.split(".")
|
||||
current = config
|
||||
for k in keys[:-1]:
|
||||
if k not in current:
|
||||
@ -619,146 +389,166 @@ def write_data(key, value):
|
||||
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:
|
||||
with open("./config.json", "r", encoding="utf-8") as f:
|
||||
with open("./config/config.yml", "r", encoding="utf-8") as f:
|
||||
try:
|
||||
variable.config = json.loads(f.read())
|
||||
if (not isinstance(variable.config, dict)):
|
||||
logger.warning('配置文件并不是一个有效的字典,使用默认值')
|
||||
variable.config = yaml.load(f.read())
|
||||
if not isinstance(variable.config, dict):
|
||||
logger.warning("配置文件并不是一个有效的字典,使用默认值")
|
||||
variable.config = default
|
||||
with open("./config.json", "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(variable.config, indent=2,
|
||||
ensure_ascii=False, escape_forward_slashes=False))
|
||||
with open("./config/config.yml", "w", encoding="utf-8") as f:
|
||||
yaml.dump(variable.config, f)
|
||||
f.close()
|
||||
except:
|
||||
if os.path.getsize("./config.json") != 0:
|
||||
logger.error("配置文件加载失败,请检查是否遵循JSON语法规范")
|
||||
if os.path.getsize("./config/config.yml") != 0:
|
||||
logger.error("配置文件加载失败,请检查是否遵循YAML语法规范")
|
||||
sys.exit(1)
|
||||
else:
|
||||
variable.config = handle_default_config()
|
||||
except FileNotFoundError:
|
||||
variable.config = handle_default_config()
|
||||
# print(variable.config)
|
||||
variable.log_length_limit = read_config('common.log_length_limit')
|
||||
variable.debug_mode = read_config('common.debug_mode')
|
||||
variable.log_length_limit = read_config("common.log_length_limit")
|
||||
variable.debug_mode = read_config("common.debug_mode")
|
||||
logger.debug("配置文件加载成功")
|
||||
conn = sqlite3.connect('cache.db')
|
||||
|
||||
# 尝试连接数据库
|
||||
handle_connect_db()
|
||||
|
||||
conn = sqlite3.connect("./cache.db")
|
||||
|
||||
# 创建一个游标对象
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建一个表来存储缓存数据
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS cache
|
||||
cursor.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS cache
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
module TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
data TEXT NOT NULL)''')
|
||||
data TEXT NOT NULL)"""
|
||||
)
|
||||
|
||||
conn.close()
|
||||
|
||||
conn2 = sqlite3.connect('data.db')
|
||||
conn2 = sqlite3.connect("./config/data.db")
|
||||
|
||||
# 创建一个游标对象
|
||||
cursor2 = conn2.cursor()
|
||||
|
||||
cursor2.execute('''CREATE TABLE IF NOT EXISTS data
|
||||
cursor2.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS data
|
||||
(key TEXT PRIMARY KEY,
|
||||
value TEXT)''')
|
||||
value TEXT)"""
|
||||
)
|
||||
|
||||
conn2.close()
|
||||
|
||||
logger.debug('数据库初始化成功')
|
||||
logger.debug("数据库初始化成功")
|
||||
|
||||
# handle data
|
||||
all_data_keys = {'banList': [], 'requestTime': {}, 'banListRaw': []}
|
||||
all_data_keys = {"banList": [], "requestTime": {}, "banListRaw": []}
|
||||
data = load_data()
|
||||
if (data == {}):
|
||||
write_data('banList', [])
|
||||
write_data('requestTime', {})
|
||||
logger.info('数据库内容为空,已写入默认值')
|
||||
if data == {}:
|
||||
write_data("banList", [])
|
||||
write_data("requestTime", {})
|
||||
logger.info("数据库内容为空,已写入默认值")
|
||||
for k, v in all_data_keys.items():
|
||||
if (k not in data):
|
||||
if k not in data:
|
||||
write_data(k, v)
|
||||
logger.info(f'数据库中不存在{k},已创建')
|
||||
logger.info(f"数据库中不存在{k},已创建")
|
||||
|
||||
# 处理代理配置
|
||||
if (read_config('common.proxy.enable')):
|
||||
if (read_config('common.proxy.http_value')):
|
||||
os.environ['http_proxy'] = read_config('common.proxy.http_value')
|
||||
logger.info('HTTP协议代理地址: ' +
|
||||
read_config('common.proxy.http_value'))
|
||||
if (read_config('common.proxy.https_value')):
|
||||
os.environ['https_proxy'] = read_config('common.proxy.https_value')
|
||||
logger.info('HTTPS协议代理地址: ' +
|
||||
read_config('common.proxy.https_value'))
|
||||
logger.info('代理功能已开启,请确保代理地址正确,否则无法连接网络')
|
||||
if read_config("common.proxy.enable"):
|
||||
if read_config("common.proxy.http_value"):
|
||||
os.environ["http_proxy"] = read_config("common.proxy.http_value")
|
||||
logger.info("HTTP协议代理地址: " + read_config("common.proxy.http_value"))
|
||||
if read_config("common.proxy.https_value"):
|
||||
os.environ["https_proxy"] = read_config("common.proxy.https_value")
|
||||
logger.info("HTTPS协议代理地址: " + read_config("common.proxy.https_value"))
|
||||
logger.info("代理功能已开启,请确保代理地址正确,否则无法连接网络")
|
||||
|
||||
# cookie池
|
||||
if (read_config('common.cookiepool')):
|
||||
logger.info('已启用cookie池功能,请确定配置的cookie都能正确获取链接')
|
||||
logger.info('传统的源 - 单用户cookie配置将被忽略')
|
||||
logger.info('所以即使某个源你只有一个cookie,也请填写到cookiepool对应的源中,否则将无法使用该cookie')
|
||||
if read_config("common.cookiepool"):
|
||||
logger.info("已启用cookie池功能,请确定配置的cookie都能正确获取链接")
|
||||
logger.info("传统的源 - 单用户cookie配置将被忽略")
|
||||
logger.info("所以即使某个源你只有一个cookie,也请填写到cookiepool对应的源中,否则将无法使用该cookie")
|
||||
variable.use_cookie_pool = True
|
||||
|
||||
# 移除已经过期的封禁数据
|
||||
banlist = read_data('banList')
|
||||
banlistRaw = read_data('banListRaw')
|
||||
banlist = read_data("banList")
|
||||
banlistRaw = read_data("banListRaw")
|
||||
count = 0
|
||||
for b in banlist:
|
||||
if (b['expire'] and (time.time() > b['expire_time'])):
|
||||
if b["expire"] and (time.time() > b["expire_time"]):
|
||||
count += 1
|
||||
banlist.remove(b)
|
||||
if (b['ip'] in banlistRaw):
|
||||
banlistRaw.remove(b['ip'])
|
||||
write_data('banList', banlist)
|
||||
write_data('banListRaw', banlistRaw)
|
||||
if (count != 0):
|
||||
logger.info(f'已移除{count}条过期封禁数据')
|
||||
if b["ip"] in banlistRaw:
|
||||
banlistRaw.remove(b["ip"])
|
||||
write_data("banList", banlist)
|
||||
write_data("banListRaw", banlistRaw)
|
||||
if count != 0:
|
||||
logger.info(f"已移除{count}条过期封禁数据")
|
||||
|
||||
# 处理旧版数据库的banListRaw
|
||||
banlist = read_data('banList')
|
||||
banlistRaw = read_data('banListRaw')
|
||||
if (banlist != [] and banlistRaw == []):
|
||||
banlist = read_data("banList")
|
||||
banlistRaw = read_data("banListRaw")
|
||||
if banlist != [] and banlistRaw == []:
|
||||
for b in banlist:
|
||||
banlistRaw.append(b['ip'])
|
||||
banlistRaw.append(b["ip"])
|
||||
return
|
||||
|
||||
|
||||
def ban_ip(ip_addr, ban_time=-1):
|
||||
if read_config('security.banlist.enable'):
|
||||
banList = read_data('banList')
|
||||
banList.append({
|
||||
'ip': ip_addr,
|
||||
'expire': read_config('security.banlist.expire.enable'),
|
||||
'expire_time': read_config('security.banlist.expire.length') if (ban_time == -1) else ban_time,
|
||||
})
|
||||
write_data('banList', banList)
|
||||
banListRaw = read_data('banListRaw')
|
||||
if (ip_addr not in banListRaw):
|
||||
if read_config("security.banlist.enable"):
|
||||
banList = read_data("banList")
|
||||
banList.append(
|
||||
{
|
||||
"ip": ip_addr,
|
||||
"expire": read_config("security.banlist.expire.enable"),
|
||||
"expire_time": read_config("security.banlist.expire.length") if (ban_time == -1) else ban_time,
|
||||
}
|
||||
)
|
||||
write_data("banList", banList)
|
||||
banListRaw = read_data("banListRaw")
|
||||
if ip_addr not in banListRaw:
|
||||
banListRaw.append(ip_addr)
|
||||
write_data('banListRaw', banListRaw)
|
||||
write_data("banListRaw", banListRaw)
|
||||
else:
|
||||
if (variable.banList_suggest < 10):
|
||||
if variable.banList_suggest < 10:
|
||||
variable.banList_suggest += 1
|
||||
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
|
||||
logger.warning("黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求")
|
||||
|
||||
|
||||
def check_ip_banned(ip_addr):
|
||||
if read_config('security.banlist.enable'):
|
||||
banList = read_data('banList')
|
||||
banlistRaw = read_data('banListRaw')
|
||||
if (ip_addr in banlistRaw):
|
||||
if read_config("security.banlist.enable"):
|
||||
banList = read_data("banList")
|
||||
banlistRaw = read_data("banListRaw")
|
||||
if ip_addr in banlistRaw:
|
||||
for b in banList:
|
||||
if (b['ip'] == ip_addr):
|
||||
if (b['expire']):
|
||||
if (b['expire_time'] > int(time.time())):
|
||||
if b["ip"] == ip_addr:
|
||||
if b["expire"]:
|
||||
if b["expire_time"] > int(time.time()):
|
||||
return True
|
||||
else:
|
||||
banList.remove(b)
|
||||
banlistRaw.remove(b['ip'])
|
||||
write_data('banListRaw', banlistRaw)
|
||||
write_data('banList', banList)
|
||||
banlistRaw.remove(b["ip"])
|
||||
write_data("banListRaw", banlistRaw)
|
||||
write_data("banList", banList)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@ -768,10 +558,10 @@ def check_ip_banned(ip_addr):
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if (variable.banList_suggest <= 10):
|
||||
if variable.banList_suggest <= 10:
|
||||
variable.banList_suggest += 1
|
||||
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
|
||||
logger.warning("黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求")
|
||||
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
|
||||
from PIL import Image
|
||||
import aiohttp
|
||||
from common.utils import createMD5, timeLengthFormat
|
||||
from common.utils import createFileMD5, createMD5, timeLengthFormat
|
||||
from . import log, config
|
||||
from pydub.utils import mediainfo
|
||||
import ujson as json
|
||||
import traceback
|
||||
import mutagen
|
||||
@ -144,16 +143,23 @@ def getAudioMeta(filepath):
|
||||
audio = mutagen.File(filepath)
|
||||
if not audio:
|
||||
return None
|
||||
logger.info(audio.items())
|
||||
logger.debug(audio.items())
|
||||
if (filepath.lower().endswith('.mp3')):
|
||||
cover = audio.get('APIC:')
|
||||
if (cover):
|
||||
cover = convertCover(cover.data)
|
||||
|
||||
lrc_key = None
|
||||
for k in list(audio.keys()):
|
||||
if (k.startswith('USLT')):
|
||||
lrc_key = k
|
||||
break
|
||||
title = audio.get('TIT2')
|
||||
artist = audio.get('TPE1')
|
||||
album = audio.get('TALB')
|
||||
lyric = audio.get('TLRC')
|
||||
if (lrc_key):
|
||||
lyric = audio.get(lrc_key)
|
||||
else:
|
||||
lyric = None
|
||||
if (title):
|
||||
title = title.text
|
||||
if (artist):
|
||||
@ -161,9 +167,21 @@ def getAudioMeta(filepath):
|
||||
if (album):
|
||||
album = album.text
|
||||
if (lyric):
|
||||
lyric = lyric.text
|
||||
else:
|
||||
lyric = [None]
|
||||
lyric = [lyric.text]
|
||||
if (not lyric):
|
||||
if (os.path.isfile(os.path.splitext(filepath)[0] + '.lrc')):
|
||||
with open(os.path.splitext(filepath)[0] + '.lrc', 'r', encoding='utf-8') as f:
|
||||
t = f.read().replace('\ufeff', '')
|
||||
logger.debug(t)
|
||||
lyric = filterLyricLine(t)
|
||||
logger.debug(lyric)
|
||||
if (not checkLyricValid(lyric)):
|
||||
lyric = [None]
|
||||
else:
|
||||
lyric = [lyric]
|
||||
f.close()
|
||||
else:
|
||||
lyric = [None]
|
||||
else:
|
||||
cover = audio.get('cover')
|
||||
if (cover):
|
||||
@ -183,6 +201,8 @@ def getAudioMeta(filepath):
|
||||
lyric = filterLyricLine(f.read())
|
||||
if (not checkLyricValid(lyric)):
|
||||
lyric = [None]
|
||||
else:
|
||||
lyric = [lyric]
|
||||
f.close()
|
||||
else:
|
||||
lyric = [None]
|
||||
@ -198,6 +218,7 @@ def getAudioMeta(filepath):
|
||||
"lyrics": lyric[0],
|
||||
'length': audio.info.length,
|
||||
'format_length': timeLengthFormat(audio.info.length),
|
||||
'md5': createFileMD5(filepath),
|
||||
}
|
||||
except:
|
||||
logger.error(f"get audio meta error: {filepath}")
|
||||
@ -225,7 +246,7 @@ def extractCover(audio_info, temp_path):
|
||||
f.write(audio_info['cover'])
|
||||
return path
|
||||
|
||||
def findAudios():
|
||||
def findAudios(cache):
|
||||
|
||||
available_exts = [
|
||||
'mp3',
|
||||
@ -240,6 +261,9 @@ def findAudios():
|
||||
return []
|
||||
|
||||
audios = []
|
||||
_map = {}
|
||||
for c in cache:
|
||||
_map[c['filepath']] = c
|
||||
for file in files:
|
||||
if (not file.endswith(tuple(available_exts))):
|
||||
continue
|
||||
@ -247,8 +271,11 @@ def findAudios():
|
||||
if (not checkAudioValid(path)):
|
||||
continue
|
||||
logger.info(f"found audio: {path}")
|
||||
meta = getAudioMeta(path)
|
||||
audios = audios + [meta]
|
||||
if (not (_map.get(path) and _map[path]['md5'] == createFileMD5(path))):
|
||||
meta = getAudioMeta(path)
|
||||
audios = audios + [meta]
|
||||
else:
|
||||
audios = audios + [_map[path]]
|
||||
|
||||
return audios
|
||||
|
||||
@ -259,7 +286,14 @@ def getAudioCover(filepath):
|
||||
audio = mutagen.File(filepath)
|
||||
if not audio:
|
||||
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:
|
||||
logger.error(f"get audio cover error: {filepath}")
|
||||
logger.error(traceback.format_exc())
|
||||
@ -308,7 +342,7 @@ def initMain():
|
||||
if (cache['file_list'] == os.listdir(AUDIO_PATH)):
|
||||
audios = cache['audios']
|
||||
else:
|
||||
audios = findAudios()
|
||||
audios = findAudios(cache['audios'])
|
||||
writeLocalCache(audios)
|
||||
for a in audios:
|
||||
map[a['filepath']] = a
|
||||
|
249
common/log.py
249
common/log.py
@ -10,11 +10,20 @@
|
||||
import logging
|
||||
import colorlog
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import io
|
||||
import traceback
|
||||
import time
|
||||
from pygments import highlight
|
||||
from pygments.lexers import PythonLexer
|
||||
from pygments.formatters import TerminalFormatter
|
||||
from .utils import filterFileName, addToGlobalNamespace
|
||||
from .variable import debug_mode, log_length_limit, log_file
|
||||
from .utils import filterFileName, setGlobal, require
|
||||
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):
|
||||
try:
|
||||
@ -22,15 +31,158 @@ if ((not os.path.exists("logs")) and log_file):
|
||||
except:
|
||||
pass
|
||||
|
||||
class Color:
|
||||
"""
|
||||
彩色文字处理器
|
||||
"""
|
||||
|
||||
def highlight_error(error):
|
||||
# 对堆栈跟踪进行语法高亮
|
||||
highlighted_traceback = highlight(
|
||||
error, PythonLexer(), TerminalFormatter())
|
||||
def __getattr__(self, k):
|
||||
return lambda x: f"{getattr(Fore, k.upper())}{x}{Style.RESET_ALL}"
|
||||
|
||||
# 返回语法高亮后的堆栈跟踪字符串
|
||||
return str(highlighted_traceback)
|
||||
color = Color()
|
||||
# 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):
|
||||
# 日志转接器
|
||||
@ -43,10 +195,19 @@ class LogHelper(logging.Handler):
|
||||
log_message = self.format(record)
|
||||
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:
|
||||
# 主类
|
||||
def __init__(self, module_name='Not named logger', output_level='INFO', filename=''):
|
||||
self.name = module_name
|
||||
self._logger = logging.getLogger(module_name)
|
||||
if not output_level.upper() in dir(logging):
|
||||
raise NameError('Unknown loglevel: '+output_level)
|
||||
@ -65,21 +226,12 @@ class log:
|
||||
'CRITICAL': 'red,bg_white',
|
||||
})
|
||||
if log_file:
|
||||
file_formatter = logging.Formatter(
|
||||
'%(asctime)s|[%(name)s/%(levelname)s]|%(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
if filename:
|
||||
filename = filterFileName(filename)
|
||||
else:
|
||||
filename = './logs/' + module_name + '.log'
|
||||
file_handler = logging.FileHandler(filename, encoding="utf-8")
|
||||
file_handler.setFormatter(file_formatter)
|
||||
file_handler_ = logging.FileHandler(
|
||||
"./logs/console_full.log", encoding="utf-8")
|
||||
file_handler_.setFormatter(file_formatter)
|
||||
self._logger.addHandler(file_handler_)
|
||||
self._logger.addHandler(file_handler)
|
||||
self.file = open(filename, 'a+', encoding='utf-8')
|
||||
log_files.append(self.file)
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
self.module_name = module_name
|
||||
@ -88,60 +240,55 @@ class log:
|
||||
debug_handler.setFormatter(formatter)
|
||||
|
||||
def debug(self, message, allow_hidden=True):
|
||||
if self.module_name == "flask" and "\n" in message:
|
||||
if message.startswith("Error"):
|
||||
return self._logger.error(message)
|
||||
for m in message.split("\n"):
|
||||
if "WARNING" in m:
|
||||
self._logger.warning(m)
|
||||
else:
|
||||
self._logger.info(m)
|
||||
return
|
||||
if (log_file):
|
||||
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')
|
||||
|
||||
if len(str(message)) > log_length_limit and allow_hidden:
|
||||
message = str(message)[:log_length_limit] + " ..."
|
||||
self._logger.debug(message)
|
||||
|
||||
def log(self, message, allow_hidden=True):
|
||||
if self.module_name == "flask" and "\n" in message:
|
||||
if message.startswith("Error"):
|
||||
return self._logger.error(message)
|
||||
for m in message.split("\n"):
|
||||
if "WARNING" in m:
|
||||
self._logger.warning(m)
|
||||
else:
|
||||
self._logger.info(m)
|
||||
return
|
||||
if (log_file):
|
||||
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')
|
||||
|
||||
if len(str(message)) > log_length_limit and allow_hidden:
|
||||
message = str(message)[:log_length_limit] + " ..."
|
||||
self._logger.info(message)
|
||||
|
||||
def info(self, message, allow_hidden=True):
|
||||
if self.module_name == "flask" and "\n" in message:
|
||||
if message.startswith("Error"):
|
||||
return self._logger.error(message)
|
||||
for m in message.split("\n"):
|
||||
if "WARNING" in m:
|
||||
self._logger.warning(m)
|
||||
else:
|
||||
self._logger.info(m)
|
||||
return
|
||||
if (log_file):
|
||||
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')
|
||||
|
||||
if len(str(message)) > log_length_limit and allow_hidden:
|
||||
message = str(message)[:log_length_limit] + "..."
|
||||
self._logger.info(message)
|
||||
|
||||
def warning(self, message):
|
||||
if (message.startswith('Traceback')):
|
||||
self._logger.error('\n' + highlight_error(message))
|
||||
self._logger.warning(message)
|
||||
if (log_file):
|
||||
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')
|
||||
|
||||
if (message.strip().startswith('Traceback')):
|
||||
self._logger.warning('\n' + highlight_error(message))
|
||||
else:
|
||||
self._logger.warning(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')):
|
||||
self._logger.error('\n' + highlight_error(message))
|
||||
else:
|
||||
self._logger.error(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):
|
||||
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)
|
||||
|
||||
|
||||
addToGlobalNamespace('print', logprint)
|
||||
setGlobal(logprint, 'print')
|
||||
|
@ -14,6 +14,7 @@ from .log import log
|
||||
from aiohttp.web import Response
|
||||
import ujson as json
|
||||
import re
|
||||
from common.utils import createMD5
|
||||
|
||||
logger = log('lx_script')
|
||||
|
||||
@ -37,8 +38,8 @@ async def get_response(retry = 0):
|
||||
if (retry > 21):
|
||||
logger.warning('请求源脚本内容失败')
|
||||
return
|
||||
baseurl = '/lxmusics/lx-music-api-server/main/lx-music-source-example.js'
|
||||
jsdbaseurl = '/gh/lxmusics/lx-music-api-server@main/lx-music-source-example.js'
|
||||
baseurl = '/MeoProject/lx-music-api-server/main/lx-music-source-example.js'
|
||||
jsdbaseurl = '/gh/MeoProject/lx-music-api-server@main/lx-music-source-example.js'
|
||||
try:
|
||||
i = retry
|
||||
if (i > 10):
|
||||
@ -68,7 +69,7 @@ async def get_script():
|
||||
logger.warning('请求源脚本内容失败')
|
||||
|
||||
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
|
||||
try:
|
||||
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')):
|
||||
newScriptLines.append(f'''const API_URL = "{'https' if config.read_config('common.ssl_info.is_https') else 'http'}://{request.host}"''')
|
||||
elif (line.startswith('const API_KEY')):
|
||||
newScriptLines.append(f'const API_KEY = "{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")):
|
||||
newScriptLines.append(" * @name " + config.read_config("common.download_config.name"))
|
||||
elif (line.startswith("* @description")):
|
||||
newScriptLines.append(" * @description " + config.read_config("common.download_config.intro"))
|
||||
elif (line.startswith("* @author")):
|
||||
newScriptLines.append((" * @author 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")):
|
||||
newScriptLines.append(" * @version " + config.read_config("common.download_config.version"))
|
||||
elif (line.startswith("const DEV_ENABLE ")):
|
||||
newScriptLines.append("const DEV_ENABLE = " + str(config.read_config("common.download_config.dev")).lower())
|
||||
elif (line.startswith("const UPDATE_ENABLE ")):
|
||||
newScriptLines.append("const UPDATE_ENABLE = " + str(config.read_config("common.download_config.update")).lower())
|
||||
else:
|
||||
newScriptLines.append(oline)
|
||||
r = '\n'.join(newScriptLines)
|
||||
|
||||
r = re.sub(r'const MUSIC_QUALITY = {[^}]+}', f'const MUSIC_QUALITY = JSON.parse(\'{json.dumps(config.read_config("common.download_config.quality"))}\')', r)
|
||||
|
||||
|
||||
# 用于检查更新
|
||||
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',
|
||||
headers = {
|
||||
'Content-Disposition': f'''attachment; filename={
|
||||
config.read_config("common.download_config.filename")
|
||||
if config.read_config("common.download_config.filename").endswith(".js")
|
||||
else (config.read_config("common.download_config.filename" + ".js"))}'''
|
||||
else (config.read_config("common.download_config.filename") + ".js")}'''
|
||||
})
|
||||
|
||||
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(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 = {}):
|
||||
global tasks
|
||||
wrapper = taskWrapper(name, task, interval, args)
|
||||
|
@ -7,6 +7,7 @@
|
||||
# ----------------------------------------
|
||||
# This file is part of the "lx-music-api-server" project.
|
||||
|
||||
import hashlib
|
||||
import platform
|
||||
import binascii
|
||||
import builtins
|
||||
@ -15,8 +16,9 @@ import zlib
|
||||
import time
|
||||
import re
|
||||
import xmltodict
|
||||
import ipaddress
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
from hashlib import md5 as handleCreateMD5
|
||||
from typing import Union
|
||||
|
||||
def createBase64Encode(data_bytes):
|
||||
encoded_data = base64.b64encode(data_bytes)
|
||||
@ -51,8 +53,8 @@ def require(module):
|
||||
index += 1
|
||||
return _module
|
||||
|
||||
def addToGlobalNamespace(key, data):
|
||||
setattr(builtins, key, data)
|
||||
def setGlobal(obj, key = ''):
|
||||
setattr(builtins, obj.__name__ if (not key) else key, obj)
|
||||
|
||||
def filterFileName(filename):
|
||||
if platform.system() == 'Windows' or platform.system() == 'Cygwin':
|
||||
@ -64,10 +66,17 @@ def filterFileName(filename):
|
||||
# 将不合法字符替换为下划线
|
||||
return re.sub(illegal_chars, '_', filename)
|
||||
|
||||
def createMD5(s: (str, bytes)):
|
||||
def createMD5(s: Union[str, bytes]):
|
||||
if (isinstance(s, str)):
|
||||
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"):
|
||||
try:
|
||||
@ -191,5 +200,11 @@ def timestamp_format(t):
|
||||
t = int(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)
|
@ -1,47 +1,50 @@
|
||||
# ----------------------------------------
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: variable.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: variable.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# ----------------------------------------
|
||||
# This file is part of the "lx-music-api-server" project.
|
||||
|
||||
import os as _os
|
||||
import ujson as _json
|
||||
import ruamel.yaml as _yaml
|
||||
|
||||
yaml = _yaml.YAML()
|
||||
|
||||
|
||||
def _read_config_file():
|
||||
try:
|
||||
with open("./config.json", "r", encoding = "utf-8") as f:
|
||||
return _json.load(f)
|
||||
with open(f"./config/config.yml", "r", encoding="utf-8") as f:
|
||||
return yaml.load(f.read())
|
||||
except:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
def _read_config(key):
|
||||
try:
|
||||
config = _read_config_file()
|
||||
keys = key.split('.')
|
||||
value = config
|
||||
for k in keys:
|
||||
if isinstance(value, dict):
|
||||
if k not in value and keys.index(k) != len(keys) - 1:
|
||||
value[k] = {}
|
||||
elif k not in value and keys.index(k) == len(keys) - 1:
|
||||
value = None
|
||||
value = value[k]
|
||||
else:
|
||||
config = _read_config_file()
|
||||
keys = key.split('.')
|
||||
value = config
|
||||
for k in keys:
|
||||
if isinstance(value, dict):
|
||||
if k not in value and keys.index(k) != len(keys) - 1:
|
||||
value[k] = []
|
||||
elif k not in value and keys.index(k) == len(keys) - 1:
|
||||
value = None
|
||||
break
|
||||
value = value[k]
|
||||
else:
|
||||
value = None
|
||||
break
|
||||
return value
|
||||
|
||||
|
||||
return value
|
||||
except:
|
||||
return None
|
||||
_dm = _read_config("common.debug_mode")
|
||||
_lm = _read_config("common.log_file")
|
||||
_ll = _read_config("common.log_length_limit")
|
||||
debug_mode = _dm if (_dm) else False
|
||||
debug_mode = True if (_os.getenv('CURRENT_ENV') ==
|
||||
'development') else (_dm if (_dm) else False)
|
||||
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
|
||||
config = {}
|
||||
workdir = _os.getcwd()
|
||||
@ -51,4 +54,8 @@ fake_ip = None
|
||||
aioSession = None
|
||||
qdes_lib_loaded = 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 })
|
182
main.py
182
main.py
@ -1,22 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# ----------------------------------------
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: main.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: main.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# ----------------------------------------
|
||||
# This file is part of the "lx-music-api-server" project.
|
||||
|
||||
import 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 os
|
||||
|
||||
from common.utils import createBase64Decode
|
||||
|
||||
if ((sys.version_info.major == 3 and sys.version_info.minor < 6) or sys.version_info.major == 2):
|
||||
if sys.version_info < (3, 6):
|
||||
print('Python版本过低,请使用Python 3.6+ ')
|
||||
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 lxsecurity
|
||||
from common import log
|
||||
@ -24,52 +35,46 @@ from common import Httpx
|
||||
from common import variable
|
||||
from common import scheduler
|
||||
from common import lx_script
|
||||
from aiohttp.web import Response, FileResponse, StreamResponse
|
||||
import ujson as json
|
||||
import threading
|
||||
import traceback
|
||||
from common import gcsp
|
||||
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)):
|
||||
dic = {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'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")
|
||||
aiologger = log.log('aiohttp_web')
|
||||
|
||||
stopEvent = None
|
||||
if (sys.version_info.minor < 8 and sys.version_info.major == 3):
|
||||
if sys.version_info < (3, 8):
|
||||
logger.warning('您使用的Python版本已经停止更新,不建议继续使用')
|
||||
import concurrent
|
||||
stopEvent = concurrent.futures._base.CancelledError
|
||||
else:
|
||||
stopEvent = asyncio.exceptions.CancelledError
|
||||
|
||||
|
||||
def start_checkcn_thread() -> None:
|
||||
threading.Thread(target=Httpx.checkcn).start()
|
||||
|
||||
# check request info before start
|
||||
|
||||
|
||||
async def handle_before_request(app, handler):
|
||||
async def handle_request(request):
|
||||
try:
|
||||
if (config.read_config('common.reverse_proxy.allow_proxy')):
|
||||
if (request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))):
|
||||
# proxy header
|
||||
if (request.remote in config.read_config('common.reverse_proxy.proxy_whitelist_remote')):
|
||||
request.remote_addr = request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))
|
||||
else:
|
||||
return handleResult({"code": 1, "msg": "反代客户端远程地址不在反代ip白名单中", "data": None}, 403)
|
||||
else:
|
||||
request.remote_addr = request.remote
|
||||
if config.read_config("common.reverse_proxy.allow_proxy") and request.headers.get(
|
||||
config.read_config("common.reverse_proxy.real_ip_header")):
|
||||
if not (config.read_config("common.reverse_proxy.allow_public_ip") or utils.is_local_ip(request.remote)):
|
||||
return handleResult({"code": 1, "msg": "不允许的公网ip转发", "data": None}, 403)
|
||||
# proxy header
|
||||
request.remote_addr = request.headers.get(config.read_config("common.reverse_proxy.real_ip_header"))
|
||||
else:
|
||||
request.remote_addr = request.remote
|
||||
# check ip
|
||||
@ -80,13 +85,13 @@ async def handle_before_request(app, handler):
|
||||
(time.time() - config.getRequestTime('global'))
|
||||
<
|
||||
(config.read_config("security.rate_limit.global"))
|
||||
):
|
||||
):
|
||||
return handleResult({"code": 5, "msg": "全局限速", "data": None}, 429)
|
||||
if (
|
||||
(time.time() - config.getRequestTime(request.remote_addr))
|
||||
<
|
||||
(config.read_config("security.rate_limit.ip"))
|
||||
):
|
||||
):
|
||||
return handleResult({"code": 5, "msg": "IP限速", "data": None}, 429)
|
||||
# update request time
|
||||
config.updateRequestTime('global')
|
||||
@ -95,27 +100,32 @@ async def handle_before_request(app, handler):
|
||||
if (config.read_config("security.allowed_host.enable")):
|
||||
if request.host.split(":")[0] not in config.read_config("security.allowed_host.list"):
|
||||
if config.read_config("security.allowed_host.blacklist.enable"):
|
||||
config.ban_ip(request.remote_addr, int(config.read_config("security.allowed_host.blacklist.length")))
|
||||
config.ban_ip(request.remote_addr, int(
|
||||
config.read_config("security.allowed_host.blacklist.length")))
|
||||
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
||||
|
||||
resp = await handler(request)
|
||||
if (isinstance(resp, (str, list, dict))):
|
||||
resp = handleResult(resp)
|
||||
elif (isinstance(resp, tuple) and len(resp) == 2): # flask like response
|
||||
elif (isinstance(resp, tuple) and len(resp) == 2): # flask like response
|
||||
body, status = resp
|
||||
if (isinstance(body, (str, list, dict))):
|
||||
resp = handleResult(body, status)
|
||||
else:
|
||||
resp = Response(body = str(body), content_type='text/plain', status = status)
|
||||
resp = Response(
|
||||
body=str(body), content_type='text/plain', status=status)
|
||||
elif (not isinstance(resp, (Response, FileResponse, StreamResponse))):
|
||||
resp = Response(body = str(resp), content_type='text/plain', status = 200)
|
||||
aiologger.info(f'{request.remote_addr + ("" if (request.remote == request.remote_addr) else f"|proxy@{request.remote}")} - {request.method} "{request.path}", {resp.status}')
|
||||
resp = Response(
|
||||
body=str(resp), content_type='text/plain', status=200)
|
||||
aiologger.info(
|
||||
f'{request.remote_addr + ("" if (request.remote == request.remote_addr) else f"|proxy@{request.remote}")} - {request.method} "{request.path}", {resp.status}')
|
||||
return resp
|
||||
except:
|
||||
except:
|
||||
logger.error(traceback.format_exc())
|
||||
return {"code": 4, "msg": "内部服务器错误", "data": None}
|
||||
return handle_request
|
||||
|
||||
|
||||
async def main(request):
|
||||
return handleResult({"code": 0, "msg": "success", "data": None})
|
||||
|
||||
@ -126,7 +136,7 @@ async def handle(request):
|
||||
songId = request.match_info.get('songId')
|
||||
quality = request.match_info.get('quality')
|
||||
if (config.read_config("security.key.enable") and request.host.split(':')[0] not in config.read_config('security.whitelist_host')):
|
||||
if (request.headers.get("X-Request-Key")) != 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")):
|
||||
config.ban_ip(request.remote_addr)
|
||||
return handleResult({"code": 1, "msg": "key验证失败", "data": None}, 403)
|
||||
@ -136,10 +146,18 @@ async def handle(request):
|
||||
if (config.read_config('security.lxm_ban.enable')):
|
||||
config.ban_ip(request.remote_addr)
|
||||
return handleResult({"code": 1, "msg": "lxm请求头验证失败", "data": None}, 403)
|
||||
|
||||
|
||||
try:
|
||||
query = dict(request.query)
|
||||
if (method in dir(modules)):
|
||||
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))
|
||||
else:
|
||||
return handleResult(await modules.other(method, source, songId, quality, query))
|
||||
@ -147,18 +165,22 @@ async def handle(request):
|
||||
logger.error(traceback.format_exc())
|
||||
return handleResult({'code': 4, 'msg': '内部服务器错误', 'data': None}, 500)
|
||||
|
||||
|
||||
async def handle_404(request):
|
||||
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
||||
|
||||
|
||||
async def handle_local(request):
|
||||
try:
|
||||
query = dict(request.query)
|
||||
data = query.get('q')
|
||||
data = createBase64Decode(data.replace('-', '+').replace('_', '/'))
|
||||
data = utils.createBase64Decode(
|
||||
data.replace('-', '+').replace('_', '/'))
|
||||
data = json.loads(data)
|
||||
t = request.match_info.get('type')
|
||||
data['t'] = t
|
||||
except:
|
||||
logger.info(traceback.format_exc())
|
||||
return handleResult({'code': 6, 'msg': '请求参数有错', 'data': None}, 404)
|
||||
if (data['t'] == 'u'):
|
||||
if (data['p'] in list(localMusic.map.keys())):
|
||||
@ -192,7 +214,9 @@ async def handle_local(request):
|
||||
'data': localMusic.checkLocalMusic(data['p'])
|
||||
}
|
||||
|
||||
app = aiohttp.web.Application(middlewares=[handle_before_request])
|
||||
app = Application(middlewares=[handle_before_request])
|
||||
utils.setGlobal(app, "app")
|
||||
|
||||
# mainpage
|
||||
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')):
|
||||
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
|
||||
app.router.add_route('*', '/{tail:.*}', handle_404)
|
||||
|
||||
async def run_app():
|
||||
|
||||
async def run_app_host(host):
|
||||
retries = 0
|
||||
while True:
|
||||
if (retries > 4):
|
||||
logger.warning("重试次数已达上限,但仍有部分端口未能完成监听,已自动进行忽略")
|
||||
return
|
||||
break
|
||||
try:
|
||||
host = config.read_config('common.host')
|
||||
ports = [int(port) for port in config.read_config('common.ports')]
|
||||
ssl_ports = [int(port) for port in config.read_config('common.ssl_info.ssl_ports')]
|
||||
|
||||
ports = [int(port)
|
||||
for port in config.read_config('common.ports')]
|
||||
ssl_ports = [int(port) for port in config.read_config(
|
||||
'common.ssl_info.ssl_ports')]
|
||||
final_ssl_ports = []
|
||||
final_ports = []
|
||||
for p in ports:
|
||||
if (p not in ssl_ports and p not in variable.running_ports):
|
||||
if (p not in ssl_ports and f'{host}_{p}' not in variable.running_ports):
|
||||
final_ports.append(p)
|
||||
else:
|
||||
if (p not in variable.running_ports):
|
||||
final_ssl_ports.append(p)
|
||||
# 读取证书和私钥路径
|
||||
cert_path = config.read_config('common.ssl_info.path.cert')
|
||||
privkey_path = config.read_config('common.ssl_info.path.privkey')
|
||||
privkey_path = config.read_config(
|
||||
'common.ssl_info.path.privkey')
|
||||
|
||||
# 创建 HTTP AppRunner
|
||||
http_runner = aiohttp.web.AppRunner(app)
|
||||
@ -237,16 +266,21 @@ async def run_app():
|
||||
# 启动 HTTP 端口监听
|
||||
for port in final_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()
|
||||
variable.running_ports.append(port)
|
||||
logger.info(f"监听 -> http://{host}:{port}")
|
||||
variable.running_ports.append(f'{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 (os.path.exists(cert_path) and os.path.exists(privkey_path)):
|
||||
import ssl
|
||||
# 创建 SSL 上下文,加载配置文件中指定的证书和私钥
|
||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ssl_context = ssl.create_default_context(
|
||||
ssl.Purpose.CLIENT_AUTH)
|
||||
ssl_context.load_cert_chain(cert_path, privkey_path)
|
||||
|
||||
# 创建 HTTPS AppRunner
|
||||
@ -256,21 +290,30 @@ async def run_app():
|
||||
# 启动 HTTPS 端口监听
|
||||
for port in ssl_ports:
|
||||
if (port not in variable.running_ports):
|
||||
https_site = aiohttp.web.TCPSite(https_runner, host, port, ssl_context=ssl_context)
|
||||
https_site = aiohttp.web.TCPSite(
|
||||
https_runner, host, port, ssl_context=ssl_context)
|
||||
await https_site.start()
|
||||
variable.running_ports.append(port)
|
||||
logger.info(f"监听 -> https://{host}:{port}")
|
||||
|
||||
return
|
||||
variable.running_ports.append(f'{host}_{port}')
|
||||
logger.info(f"""监听 -> https://{
|
||||
host if (':' not in host)
|
||||
else '[' + host + ']'
|
||||
}:{port}""")
|
||||
logger.debug(f"HOST({host}) 已完成监听")
|
||||
break
|
||||
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.info('服务器将在10s后再次尝试启动...')
|
||||
await asyncio.sleep(10)
|
||||
logger.info('重新尝试启动...')
|
||||
retries += 1
|
||||
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():
|
||||
@ -293,7 +336,7 @@ async def initMain():
|
||||
logger.info('wating for sessions to complete...')
|
||||
if variable.aioSession:
|
||||
await variable.aioSession.close()
|
||||
|
||||
|
||||
variable.running = False
|
||||
logger.info("Server stopped")
|
||||
|
||||
@ -304,5 +347,20 @@ if __name__ == "__main__":
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except:
|
||||
logger.error('初始化出错,请检查日志')
|
||||
logger.error(traceback.format_exc())
|
||||
logger.critical('初始化出错,请检查日志')
|
||||
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 import log
|
||||
from common import config
|
||||
|
||||
# 从.引入的包并没有在代码中直接使用,但是是用require在请求时进行引入的,不要动
|
||||
from . import kw
|
||||
from . import mg
|
||||
@ -20,194 +21,195 @@ from . import wy
|
||||
import traceback
|
||||
import time
|
||||
|
||||
logger = log.log('api_handler')
|
||||
logger = log.log("api_handler")
|
||||
|
||||
sourceExpirationTime = {
|
||||
'tx': {
|
||||
"tx": {
|
||||
"expire": True,
|
||||
"time": 80400, # 不知道tx为什么要取一个这么不对劲的数字当过期时长
|
||||
},
|
||||
'kg': {
|
||||
"kg": {
|
||||
"expire": True,
|
||||
"time": 24 * 60 * 60, # 24 hours
|
||||
},
|
||||
'kw': {
|
||||
"expire": True,
|
||||
"time": 60 * 60 # 60 minutes
|
||||
},
|
||||
'wy': {
|
||||
"kw": {"expire": True, "time": 60 * 60}, # 60 minutes
|
||||
"wy": {
|
||||
"expire": True,
|
||||
"time": 20 * 60, # 20 minutes
|
||||
},
|
||||
'mg': {
|
||||
"mg": {
|
||||
"expire": False,
|
||||
"time": 0,
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def url(source, songId, quality, query):
|
||||
if (not quality):
|
||||
async def url(source, songId, quality, query={}):
|
||||
if not quality:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': '需要参数"quality"',
|
||||
'data': None,
|
||||
"code": 2,
|
||||
"msg": '需要参数"quality"',
|
||||
"data": None,
|
||||
}
|
||||
|
||||
if (source == "kg"):
|
||||
|
||||
if source == "kg":
|
||||
songId = songId.lower()
|
||||
|
||||
|
||||
try:
|
||||
cache = config.getCache('urls', f'{source}_{songId}_{quality}')
|
||||
cache = config.getCache("urls", f"{source}_{songId}_{quality}")
|
||||
if cache:
|
||||
logger.debug(f'使用缓存的{source}_{songId}_{quality}数据,URL:{cache["url"]}')
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': cache['url'],
|
||||
'extra': {
|
||||
'cache': True,
|
||||
'quality': {
|
||||
'target': quality,
|
||||
'result': quality,
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": cache["url"],
|
||||
"extra": {
|
||||
"cache": True,
|
||||
"quality": {
|
||||
"target": quality,
|
||||
"result": quality,
|
||||
},
|
||||
'expire': {
|
||||
"expire": {
|
||||
# 在更新缓存的时候把有效期的75%作为链接可用时长,现在加回来
|
||||
'time': int(cache['time'] + (sourceExpirationTime[source]['time'] * 0.25)) if cache['expire'] else None,
|
||||
'canExpire': cache['expire'],
|
||||
}
|
||||
"time": (
|
||||
int(cache["time"] + (sourceExpirationTime[source]["time"] * 0.25))
|
||||
if cache["expire"]
|
||||
else None
|
||||
),
|
||||
"canExpire": cache["expire"],
|
||||
},
|
||||
},
|
||||
}
|
||||
except:
|
||||
logger.error(traceback.format_exc())
|
||||
try:
|
||||
func = require('modules.' + source + '.url')
|
||||
func = require("modules." + source + ".url")
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
"code": 1,
|
||||
"msg": "未知的源或不支持的方法",
|
||||
"data": None,
|
||||
}
|
||||
try:
|
||||
result = await func(songId, quality)
|
||||
logger.info(f'获取{source}_{songId}_{quality}成功,URL:{result["url"]}')
|
||||
|
||||
canExpire = sourceExpirationTime[source]['expire']
|
||||
expireTime = sourceExpirationTime[source]['time'] + int(time.time())
|
||||
config.updateCache('urls', f'{source}_{songId}_{quality}', {
|
||||
"expire": canExpire,
|
||||
# 取有效期的75%作为链接可用时长
|
||||
"time": int(expireTime - sourceExpirationTime[source]['time'] * 0.25),
|
||||
"url": result['url'],
|
||||
})
|
||||
canExpire = sourceExpirationTime[source]["expire"]
|
||||
expireTime = int(sourceExpirationTime[source]["time"] * 0.75)
|
||||
expireAt = int(expireTime + time.time())
|
||||
config.updateCache(
|
||||
"urls",
|
||||
f"{source}_{songId}_{quality}",
|
||||
{
|
||||
"expire": canExpire,
|
||||
# 取有效期的75%作为链接可用时长
|
||||
"time": expireAt,
|
||||
"url": result["url"],
|
||||
},
|
||||
expireTime if canExpire else None,
|
||||
)
|
||||
logger.debug(f'缓存已更新:{source}_{songId}_{quality}, URL:{result["url"]}, expire: {expireTime}')
|
||||
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result['url'],
|
||||
'extra': {
|
||||
'cache': False,
|
||||
'quality': {
|
||||
'target': quality,
|
||||
'result': result['quality'],
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": result["url"],
|
||||
"extra": {
|
||||
"cache": False,
|
||||
"quality": {
|
||||
"target": quality,
|
||||
"result": result["quality"],
|
||||
},
|
||||
'expire': {
|
||||
'time': expireTime if canExpire else None,
|
||||
'canExpire': canExpire,
|
||||
"expire": {
|
||||
"time": expireAt if canExpire else None,
|
||||
"canExpire": canExpire,
|
||||
},
|
||||
},
|
||||
}
|
||||
except FailedException as e:
|
||||
logger.info(f'获取{source}_{songId}_{quality}失败,原因:' + e.args[0])
|
||||
logger.info(f"获取{source}_{songId}_{quality}失败,原因:" + e.args[0])
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
"code": 2,
|
||||
"msg": e.args[0],
|
||||
"data": None,
|
||||
}
|
||||
|
||||
|
||||
async def lyric(source, songId, _, query):
|
||||
cache = config.getCache('lyric', f'{source}_{songId}')
|
||||
cache = config.getCache("lyric", f"{source}_{songId}")
|
||||
if cache:
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': cache['data']
|
||||
}
|
||||
return {"code": 0, "msg": "success", "data": cache["data"]}
|
||||
try:
|
||||
func = require('modules.' + source + '.lyric')
|
||||
func = require("modules." + source + ".lyric")
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
"code": 1,
|
||||
"msg": "未知的源或不支持的方法",
|
||||
"data": None,
|
||||
}
|
||||
try:
|
||||
result = await func(songId)
|
||||
config.updateCache('lyric', f'{source}_{songId}', {
|
||||
"data": result,
|
||||
"time": int(time.time() + (86400 * 3)), # 歌词缓存3天
|
||||
"expire": True,
|
||||
})
|
||||
logger.debug(f'缓存已更新:{source}_{songId}, lyric: {result}')
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
expireTime = 86400 * 3
|
||||
expireAt = int(time.time() + expireTime)
|
||||
config.updateCache(
|
||||
"lyric",
|
||||
f"{source}_{songId}",
|
||||
{
|
||||
"data": result,
|
||||
"time": expireAt, # 歌词缓存3天
|
||||
"expire": True,
|
||||
},
|
||||
expireTime,
|
||||
)
|
||||
logger.debug(f"缓存已更新:{source}_{songId}, lyric: {result}")
|
||||
return {"code": 0, "msg": "success", "data": result}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
"code": 2,
|
||||
"msg": e.args[0],
|
||||
"data": None,
|
||||
}
|
||||
|
||||
|
||||
async def search(source, songid, _, query):
|
||||
try:
|
||||
func = require('modules.' + source + '.search')
|
||||
func = require("modules." + source + ".search")
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
"code": 1,
|
||||
"msg": "未知的源或不支持的方法",
|
||||
"data": None,
|
||||
}
|
||||
try:
|
||||
result = await func(songid, query)
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
return {"code": 0, "msg": "success", "data": result}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
"code": 2,
|
||||
"msg": e.args[0],
|
||||
"data": None,
|
||||
}
|
||||
|
||||
|
||||
async def other(method, source, songid, _, query):
|
||||
try:
|
||||
func = require('modules.' + source + '.' + method)
|
||||
func = require("modules." + source + "." + method)
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
"code": 1,
|
||||
"msg": "未知的源或不支持的方法",
|
||||
"data": None,
|
||||
}
|
||||
try:
|
||||
result = await func(songid)
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
return {"code": 0, "msg": "success", "data": result}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
"code": 2,
|
||||
"msg": e.args[0],
|
||||
"data": None,
|
||||
}
|
||||
|
||||
|
||||
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 utils
|
||||
import asyncio
|
||||
from . import refresh_login
|
||||
|
||||
async def info(hash_):
|
||||
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 -
|
||||
# ----------------------------------------
|
||||
# This file is part of the "lx-music-api-server" project.
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
from common import utils
|
||||
from common import config
|
||||
from common import Httpx
|
||||
import json
|
||||
|
||||
createObject = utils.CreateObject
|
||||
|
||||
@ -23,6 +25,7 @@ tools = createObject({
|
||||
"version": config.read_config("module.kg.tracker.version"),
|
||||
"extra_params": config.read_config("module.kg.tracker.extra_params"),
|
||||
"appid": config.read_config("module.kg.client.appid"),
|
||||
'mid': config.read_config('module.kg.user.mid'),
|
||||
"pid": config.read_config("module.kg.client.pid"),
|
||||
'qualityHashMap': {
|
||||
'128k': 'hash_128',
|
||||
@ -49,14 +52,21 @@ def buildRequestParams(dictionary: dict):
|
||||
return joined_str
|
||||
|
||||
def sign(params, body = "", signkey = tools["signkey"]):
|
||||
if (isinstance(body, dict)):
|
||||
body = json.dumps(body)
|
||||
params = utils.sortDict(params)
|
||||
params = buildSignatureParams(params, body)
|
||||
return utils.createMD5(signkey + params + signkey)
|
||||
|
||||
async def signRequest(url, params, options, signkey = tools["signkey"]):
|
||||
params['signature'] = sign(params, options.get("body") if options.get("body") else (options.get("data") if options.get("data") else ""), signkey)
|
||||
params['signature'] = sign(params, options.get("body") if options.get("body") else (options.get("data") if options.get("data") else (options.get("json") if options.get("json") else "")), signkey)
|
||||
url = url + "?" + buildRequestParams(params)
|
||||
return await Httpx.AsyncRequest(url, options)
|
||||
|
||||
def getKey(hash_, user_info):
|
||||
return utils.createMD5(hash_.lower() + tools.pidversec + tools.appid + user_info['mid'] + user_info['userid'])
|
||||
|
||||
def aes_sign(plain_text, key=b'90b8382a1bb4ccdcf063102053fd75b8', iv=b'f063102053fd75b8'):
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
crypto = cipher.encrypt(pad(plain_text.encode(), AES.block_size))
|
||||
return crypto.hex()
|
||||
|
@ -19,6 +19,10 @@ tools = {
|
||||
'320k': '320kmp3',
|
||||
'flac': '2000kflac',
|
||||
'flac24bit': '4000kflac',
|
||||
'128kmp3': '128kmp3',
|
||||
'320kmp3': '320kmp3',
|
||||
"2000kflac": "2000kflac",
|
||||
"4000kflac": "4000kflac",
|
||||
},
|
||||
'qualityMapReverse': {
|
||||
128: '128k',
|
||||
@ -38,7 +42,7 @@ async def url(songId, quality):
|
||||
proto = config.read_config('module.kw.proto')
|
||||
if (proto == 'bd-api'):
|
||||
user_info = config.read_config('module.kw.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.kw'))
|
||||
target_url = f'''https://bd-api.kuwo.cn/api/service/music/downloadInfo/{songId}?isMv=0&format={tools['extMap'][quality]}&br={tools['qualityMap'][quality]}&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, {
|
||||
'method': 'GET',
|
||||
'headers': {
|
||||
@ -110,4 +114,4 @@ async def url(songId, quality):
|
||||
'quality': tools['qualityMapReverse'][bitrate]
|
||||
}
|
||||
else:
|
||||
raise FailedException('配置文件参数proto填写错误或不支持')
|
||||
raise FailedException('配置文件参数proto填写错误或不支持')
|
||||
|
@ -12,48 +12,52 @@ from common import Httpx
|
||||
from common import config
|
||||
from common import variable
|
||||
from common.exceptions import FailedException
|
||||
from . import refresh_login # 删了这个定时任务会寄掉
|
||||
|
||||
tools = {
|
||||
'url': 'https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?toneFlag=__quality__&songId=__songId__&resourceType=2',
|
||||
'qualityMap': {
|
||||
'128k': 'PQ',
|
||||
'320k': 'HQ',
|
||||
'flac': 'SQ',
|
||||
'flac24bit': 'ZQ',
|
||||
'128k': '1',
|
||||
'320k': '2',
|
||||
'flac': '3',
|
||||
'flac24bit': '4',
|
||||
"master": "5"
|
||||
},
|
||||
'qualityMapReverse': {
|
||||
'PQ': '128k',
|
||||
'HQ': '320k',
|
||||
'SQ': 'flac',
|
||||
'ZQ': 'flac24bit',
|
||||
'000009': '128k',
|
||||
'020010': '320k',
|
||||
'011002': 'flac',
|
||||
'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'))
|
||||
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',
|
||||
'headers': {
|
||||
'User-Agent': user_info['useragent'],
|
||||
'aversionid': user_info['aversionid'],
|
||||
'token': user_info['token'],
|
||||
'channel': '0146832',
|
||||
'language': 'Chinese',
|
||||
'ua': 'Android_migu',
|
||||
'mode': 'android',
|
||||
'os': 'Android ' + user_info['osversion'],
|
||||
"by": user_info["by"],
|
||||
"Cookie": "SESSION=" + user_info["session"],
|
||||
"Referer": "https://m.music.migu.cn/v4/",
|
||||
"Origin": "https://m.music.migu.cn",
|
||||
},
|
||||
})
|
||||
try:
|
||||
body = req.json()
|
||||
data = body['data']
|
||||
|
||||
if ((not int(body['code']) == 0) or ( not data['url'])):
|
||||
raise FailedException('failed')
|
||||
if (int(body['code']) != 200 or (not body.get("data")) or (not body["data"]["playUrl"])):
|
||||
raise FailedException(body.get("msg") if body.get("msg") else "failed")
|
||||
|
||||
data = body["data"]
|
||||
|
||||
return {
|
||||
'url': data['url'].split('?')[0],
|
||||
'quality': tools['qualityMapReverse'][data['audioFormatType']],
|
||||
'url': body["data"]["playUrl"].split("?")[0] if body["data"]["playUrl"].split("?")[0].startswith("http") else "http:" + body["data"]["playUrl"].split("?")[0],
|
||||
'quality': tools['qualityMapReverse'].get(data['formatId']) if (tools['qualityMapReverse'].get(data['formatId'])) else "unknown",
|
||||
}
|
||||
except:
|
||||
raise FailedException('failed')
|
||||
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 -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: QMWSign.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# - mode: python -
|
||||
# - author: jixunmoe -
|
||||
# - name: zzc_sign.py -
|
||||
# - project: qmweb-sign -
|
||||
# - 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 re as _re
|
||||
import sys
|
||||
import re
|
||||
|
||||
def v(b):
|
||||
res = []
|
||||
p = [21, 4, 9, 26, 16, 20, 27, 30]
|
||||
for x in p:
|
||||
res.append(b[x])
|
||||
return ''.join(res)
|
||||
from hashlib import sha1
|
||||
from base64 import b64encode
|
||||
|
||||
def c(b):
|
||||
res = []
|
||||
p = [18, 11, 3, 2, 1, 7, 6, 25]
|
||||
for x in p:
|
||||
res.append(b[x])
|
||||
return ''.join(res)
|
||||
PART_1_INDEXES = [23, 14, 6, 36, 16, 40, 7, 19]
|
||||
PART_2_INDEXES = [16, 1, 32, 12, 19, 27, 8, 5]
|
||||
SCRAMBLE_VALUES = [
|
||||
89,
|
||||
39,
|
||||
179,
|
||||
150,
|
||||
218,
|
||||
82,
|
||||
58,
|
||||
252,
|
||||
177,
|
||||
52,
|
||||
186,
|
||||
123,
|
||||
120,
|
||||
64,
|
||||
242,
|
||||
133,
|
||||
143,
|
||||
161,
|
||||
121,
|
||||
179,
|
||||
]
|
||||
|
||||
def y(a, b, c):
|
||||
e = []
|
||||
r25 = a >> 2
|
||||
if b is not None and c is not None:
|
||||
r26 = a & 3
|
||||
r26_2 = r26 << 4
|
||||
r26_3 = b >> 4
|
||||
r26_4 = r26_2 | r26_3
|
||||
r27 = b & 15
|
||||
r27_2 = r27 << 2
|
||||
r27_3 = r27_2 | (c >> 6)
|
||||
r28 = c & 63
|
||||
e.append(r25)
|
||||
e.append(r26_4)
|
||||
e.append(r27_3)
|
||||
e.append(r28)
|
||||
else:
|
||||
r10 = a >> 2
|
||||
r11 = a & 3
|
||||
r11_2 = r11 << 4
|
||||
e.append(r10)
|
||||
e.append(r11_2)
|
||||
return e
|
||||
PART_1_INDEXES = filter(lambda x: x < 40, PART_1_INDEXES)
|
||||
|
||||
def n(ls):
|
||||
e = []
|
||||
for i in range(0, len(ls), 3):
|
||||
if i < len(ls) - 2:
|
||||
e += y(ls[i], ls[i + 1], ls[i + 2])
|
||||
else:
|
||||
e += y(ls[i], None, None)
|
||||
res = []
|
||||
b64all = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
|
||||
for i in e:
|
||||
res.append(b64all[i])
|
||||
return ''.join(res)
|
||||
|
||||
def t(b):
|
||||
zd = {
|
||||
"0": 0,
|
||||
"1": 1,
|
||||
"2": 2,
|
||||
"3": 3,
|
||||
"4": 4,
|
||||
"5": 5,
|
||||
"6": 6,
|
||||
"7": 7,
|
||||
"8": 8,
|
||||
"9": 9,
|
||||
"A": 10,
|
||||
"B": 11,
|
||||
"C": 12,
|
||||
"D": 13,
|
||||
"E": 14,
|
||||
"F": 15
|
||||
}
|
||||
ol = [212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6]
|
||||
res = []
|
||||
j = 0
|
||||
for i in range(0, len(b), 2):
|
||||
one = zd[b[i]]
|
||||
two = zd[b[i + 1]]
|
||||
r = one * 16 ^ two
|
||||
res.append(r ^ ol[j])
|
||||
j += 1
|
||||
return res
|
||||
def sign(payload: str) -> str:
|
||||
hash = sha1(payload.encode("utf-8")).hexdigest().upper()
|
||||
|
||||
def sign(params):
|
||||
md5Str = createMD5(params).upper()
|
||||
h = v(md5Str)
|
||||
e = c(md5Str)
|
||||
ls = t(md5Str)
|
||||
m = n(ls)
|
||||
res = 'zzb' + h + m + e
|
||||
res = res.lower()
|
||||
r = _re.compile(r'[\\/+]')
|
||||
res = _re.sub(r, '', res)
|
||||
return res
|
||||
part1 = "".join(map(lambda i: hash[i], PART_1_INDEXES))
|
||||
part2 = "".join(map(lambda i: hash[i], PART_2_INDEXES))
|
||||
|
||||
part3 = bytearray(20)
|
||||
for i, v in enumerate(SCRAMBLE_VALUES):
|
||||
value = v ^ int(hash[i * 2 : i * 2 + 2], 16)
|
||||
part3[i] = value
|
||||
b64_part = re.sub(rb"[\\/+=]", b"", b64encode(part3)).decode("utf-8")
|
||||
return f"zzc{part1}{b64_part}{part2}".lower()
|
||||
|
@ -26,70 +26,66 @@ class ParseTools:
|
||||
'timeLabelFixRxp': re.compile(r'(?:\.0+|0+)$'),
|
||||
}
|
||||
|
||||
def ms_format(self, time_ms):
|
||||
if not time_ms:
|
||||
def msFormat(self, timeMs):
|
||||
if isinstance(timeMs, float) and timeMs.is_nan():
|
||||
return ''
|
||||
ms = time_ms % 1000
|
||||
time_ms /= 1000
|
||||
m = str(int(time_ms / 60)).zfill(2)
|
||||
time_ms %= 60
|
||||
s = str(int(time_ms)).zfill(2)
|
||||
return f"[{m}:{s}.{str(ms).zfill(3)}]"
|
||||
ms = timeMs % 1000
|
||||
timeMs //= 1000
|
||||
m = str(int(timeMs // 60)).zfill(2)
|
||||
s = str(int(timeMs % 60)).zfill(2)
|
||||
return f'[{m}:{s}.{str(ms).zfill(3)}]'
|
||||
|
||||
def parse_lyric(self, lrc):
|
||||
lrc = lrc.strip()
|
||||
lrc = lrc.replace('\r', '')
|
||||
def parseLyric(self, lrc):
|
||||
lrc = lrc.strip().replace('\r', '')
|
||||
if not lrc:
|
||||
return {'lyric': '', 'lxlyric': ''}
|
||||
# print(lrc)
|
||||
|
||||
lines = lrc.split('\n')
|
||||
|
||||
lxlrc_lines = []
|
||||
lyric_lines = []
|
||||
lxlrcLines = []
|
||||
lrcLines = []
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
result = self.rxps['lineTime'].match(line)
|
||||
if not result:
|
||||
if line.startswith('[offset'):
|
||||
lxlrc_lines.append(line)
|
||||
lyric_lines.append(line)
|
||||
if self.rxps['lineTime2'].search(line):
|
||||
lyric_lines.append(line)
|
||||
lxlrcLines.append(line)
|
||||
lrcLines.append(line)
|
||||
if self.rxps['lineTime2'].match(line):
|
||||
lrcLines.append(line)
|
||||
continue
|
||||
|
||||
start_ms_time = int(result.group(1))
|
||||
start_time_str = self.ms_format(start_ms_time)
|
||||
if not start_time_str:
|
||||
startMsTime = int(result.group(1))
|
||||
startTimeStr = self.msFormat(startMsTime)
|
||||
if not startTimeStr:
|
||||
continue
|
||||
|
||||
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)
|
||||
if not times:
|
||||
continue
|
||||
times = [
|
||||
f"<{max(int(match.group(1)) - start_ms_time, 0)},{match.group(2)}>"
|
||||
for match in re.finditer(r'\((\d+),(\d+)\)', words)
|
||||
]
|
||||
word_arr = re.split(self.rxps['wordTime'], words)
|
||||
new_words = ''.join([f"{time}{word}" for time, word in zip(times, word_arr)])
|
||||
lxlrc_lines.append(f"{start_time_str}{new_words}")
|
||||
_rxp = r"\((\d+),(\d+)\)"
|
||||
times = [f'''<{max(int(re.search(_rxp, time).group(1)) - startMsTime, 0)},{re.search(_rxp, time).group(2)}>''' for time in times]
|
||||
wordArr = re.split(self.rxps['wordTime'], words)
|
||||
newWords = ''.join([f'{time}{wordArr[index]}' for index, time in enumerate(times)])
|
||||
lxlrcLines.append(f'{startTimeStr}{newWords}')
|
||||
|
||||
return {
|
||||
'lyric': '\n'.join(lyric_lines),
|
||||
'lxlyric': '\n'.join(lxlrc_lines),
|
||||
'lyric': '\n'.join(lrcLines),
|
||||
'lxlyric': '\n'.join(lxlrcLines),
|
||||
}
|
||||
|
||||
def parse_rlyric(self, lrc):
|
||||
lrc = lrc.strip()
|
||||
lrc = lrc.replace('\r', '')
|
||||
def parseRlyric(self, lrc):
|
||||
lrc = lrc.strip().replace('\r', '')
|
||||
if not lrc:
|
||||
return {'lyric': '', 'lxlyric': ''}
|
||||
lines = lrc.split('\n')
|
||||
|
||||
lyric_lines = []
|
||||
lines = lrc.split('\n')
|
||||
lrcLines = []
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
@ -97,91 +93,104 @@ class ParseTools:
|
||||
if not result:
|
||||
continue
|
||||
|
||||
start_ms_time = int(result.group(1))
|
||||
start_time_str = self.ms_format(start_ms_time)
|
||||
if not start_time_str:
|
||||
startMsTime = int(result.group(1))
|
||||
startTimeStr = self.msFormat(startMsTime)
|
||||
if not startTimeStr:
|
||||
continue
|
||||
|
||||
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 remove_tag(self, string):
|
||||
def removeTag(self, string):
|
||||
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:
|
||||
interval += '.0'
|
||||
arr = re.split(':|\.', interval.ljust(8, '0'))[:3]
|
||||
m, s, ms = map(int, arr)
|
||||
return m * 3600000 + s * 1000 + ms
|
||||
arr = re.split(r':|\.', interval)
|
||||
while len(arr) < 3:
|
||||
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):
|
||||
rlrc_lines = rlrc.split('\n')
|
||||
lrc_lines = lrc.split('\n')
|
||||
new_lrc = []
|
||||
for line in rlrc_lines:
|
||||
result = self.rxps['lineTime2'].search(line)
|
||||
def fixRlrcTimeTag(self, rlrc, lrc):
|
||||
rlrcLines = rlrc.split('\n')
|
||||
lrcLines = lrc.split('\n')
|
||||
newLrc = []
|
||||
|
||||
for line in rlrcLines:
|
||||
result = self.rxps['lineTime2'].match(line)
|
||||
if not result:
|
||||
continue
|
||||
words = re.sub(self.rxps['lineTime2'], '', line)
|
||||
if not words.strip():
|
||||
continue
|
||||
t1 = self.get_intv(result.group(1))
|
||||
while lrc_lines:
|
||||
lrc_line = lrc_lines.pop(0)
|
||||
lrc_line_result = self.rxps['lineTime2'].search(lrc_line)
|
||||
if not lrc_line_result:
|
||||
continue
|
||||
t2 = self.get_intv(lrc_line_result.group(1))
|
||||
if abs(t1 - t2) < 100:
|
||||
new_lrc.append(re.sub(self.rxps['lineTime2'], lrc_line_result.group(0), line))
|
||||
break
|
||||
return '\n'.join(new_lrc)
|
||||
t1 = self.getIntv(result.group(1))
|
||||
|
||||
def fix_tlrc_time_tag(self, tlrc, lrc):
|
||||
tlrc_lines = tlrc.split('\n')
|
||||
lrc_lines = lrc.split('\n')
|
||||
new_lrc = []
|
||||
time_tag_rxp = r'^\[[\d:.]+\]'
|
||||
|
||||
for line in tlrc_lines:
|
||||
result = re.match(time_tag_rxp, line)
|
||||
while lrcLines:
|
||||
lrcLine = lrcLines.pop(0)
|
||||
lrcLineResult = self.rxps['lineTime2'].match(lrcLine)
|
||||
if not lrcLineResult:
|
||||
continue
|
||||
t2 = self.getIntv(lrcLineResult.group(1))
|
||||
if abs(t1 - t2) < 100:
|
||||
newLrc.append(re.sub(self.rxps['lineTime2'], lrcLineResult.group(0), line))
|
||||
break
|
||||
|
||||
return '\n'.join(newLrc)
|
||||
|
||||
def fixTlrcTimeTag(self, tlrc, lrc):
|
||||
tlrcLines = tlrc.split('\n')
|
||||
lrcLines = lrc.split('\n')
|
||||
newLrc = []
|
||||
|
||||
for line in tlrcLines:
|
||||
result = self.rxps['lineTime2'].match(line)
|
||||
if not result:
|
||||
continue
|
||||
words = re.sub(time_tag_rxp, '', line)
|
||||
words = re.sub(self.rxps['lineTime2'], '', line)
|
||||
if not words.strip():
|
||||
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:
|
||||
lrc_line = lrc_lines.pop(0)
|
||||
lrc_line_result = re.match(time_tag_rxp, lrc_line)
|
||||
if not lrc_line_result:
|
||||
t1 = self.getIntv(time)
|
||||
|
||||
while lrcLines:
|
||||
lrcLine = lrcLines.pop(0)
|
||||
lrcLineResult = self.rxps['lineTime2'].match(lrcLine)
|
||||
if not lrcLineResult:
|
||||
continue
|
||||
if tag in lrc_line_result.group(0):
|
||||
new_lrc.append(re.sub(time_tag_rxp, lrc_line_result.group(0), line))
|
||||
t2 = self.getIntv(lrcLineResult.group(1))
|
||||
if abs(t1 - t2) < 100:
|
||||
newLrc.append(re.sub(self.rxps['lineTime2'], lrcLineResult.group(0), line))
|
||||
break
|
||||
|
||||
return '\n'.join(new_lrc)
|
||||
|
||||
def parse(self, lrc, tlrc, rlrc):
|
||||
return '\n'.join(newLrc)
|
||||
|
||||
def parse(self, lrc, tlrc=None, rlrc=None):
|
||||
info = {
|
||||
'lyric': '',
|
||||
'tlyric': '',
|
||||
'rlyric': '',
|
||||
'lxlyric': '',
|
||||
}
|
||||
|
||||
if lrc:
|
||||
lyric_info = self.parse_lyric(self.remove_tag(lrc))
|
||||
info['lyric'] = lyric_info['lyric']
|
||||
info['lxlyric'] = lyric_info['lxlyric']
|
||||
parsed_lrc = self.parseLyric(self.removeTag(lrc))
|
||||
info['lyric'] = parsed_lrc['lyric']
|
||||
info['lxlyric'] = parsed_lrc['lxlyric']
|
||||
|
||||
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:
|
||||
info['tlyric'] = self.fix_tlrc_time_tag(tlrc, info['lyric'])
|
||||
info['tlyric'] = self.fixTlrcTimeTag(tlrc, info['lyric'])
|
||||
|
||||
return info
|
||||
|
||||
|
@ -21,22 +21,22 @@ async def url(songId, quality):
|
||||
strMediaMid = infoBody['track_info']['file']['media_mid']
|
||||
user_info = config.read_config('module.tx.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.tx'))
|
||||
requestBody = {
|
||||
'req_0': {
|
||||
'module': 'vkey.GetVkeyServer',
|
||||
'method': 'CgiGetVkey',
|
||||
'param': {
|
||||
'filename': [f"{tools.fileInfo[quality]['h']}{strMediaMid}{tools.fileInfo[quality]['e']}"],
|
||||
'guid': config.read_config('module.tx.vkeyserver.guid'),
|
||||
'songmid': [songId],
|
||||
'songtype': [0],
|
||||
'uin': str(user_info['uin']),
|
||||
'loginflag': 1,
|
||||
'platform': '20',
|
||||
"req": {
|
||||
"module": "music.vkey.GetVkey",
|
||||
"method": "UrlGetVkey",
|
||||
"param": {
|
||||
"filename": [f"{tools.fileInfo[quality]['h']}{strMediaMid}{tools.fileInfo[quality]['e']}"],
|
||||
"guid": config.read_config("module.tx.vkeyserver.guid"),
|
||||
"songmid": [songId],
|
||||
"songtype": [0],
|
||||
"uin": str(user_info["uin"]),
|
||||
"loginflag": 1,
|
||||
"platform": "20",
|
||||
},
|
||||
},
|
||||
'comm': {
|
||||
"qq": str(user_info['uin']),
|
||||
"authst": user_info['qqmusic_key'],
|
||||
"comm": {
|
||||
"qq": str(user_info["uin"]),
|
||||
"authst": user_info["qqmusic_key"],
|
||||
"ct": "26",
|
||||
"cv": "2010101",
|
||||
"v": "2010101"
|
||||
@ -44,7 +44,7 @@ async def url(songId, quality):
|
||||
}
|
||||
req = await signRequest(requestBody)
|
||||
body = createObject(req.json())
|
||||
data = body.req_0.data.midurlinfo[0]
|
||||
data = body.req.data.midurlinfo[0]
|
||||
url = data['purl']
|
||||
|
||||
if (not url):
|
||||
|
@ -7,205 +7,151 @@
|
||||
# ----------------------------------------
|
||||
# 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 import Httpx, variable, scheduler, config, log
|
||||
from .utils import sign
|
||||
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():
|
||||
if (not config.read_config('module.tx.user.qqmusic_key')):
|
||||
return
|
||||
if (not config.read_config('module.tx.user.refresh_login.enable')):
|
||||
return
|
||||
if (config.read_config('module.tx.user.qqmusic_key').startswith('W_X')):
|
||||
options = {
|
||||
'method': 'POST',
|
||||
'body': json.dumps({
|
||||
"comm": {
|
||||
"fPersonality": "0",
|
||||
"tmeLoginType": "1",
|
||||
"tmeLoginMethod": "1",
|
||||
"qq": "",
|
||||
"authst": "",
|
||||
"ct": "11",
|
||||
"cv": "12080008",
|
||||
"v": "12080008",
|
||||
"tmeAppID": "qqmusic"
|
||||
},
|
||||
"req1": {
|
||||
"module": "music.login.LoginServer",
|
||||
"method": "Login",
|
||||
"param": {
|
||||
"code": "",
|
||||
"openid": "",
|
||||
"refresh_token": "",
|
||||
"str_musicid": str(config.read_config('module.tx.user.uin')),
|
||||
"musickey": config.read_config('module.tx.user.qqmusic_key'),
|
||||
"unionid": "",
|
||||
"refresh_key": "",
|
||||
"loginMode": 2
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
signature = sign(options['body'])
|
||||
req = await Httpx.AsyncRequest(f'https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
|
||||
body = req.json()
|
||||
if (body['req1']['code'] != 0):
|
||||
logger.warning('刷新登录失败, code: ' +
|
||||
str(body['req1']['code']) + f'\n响应体: {body}')
|
||||
return
|
||||
else:
|
||||
logger.info('刷新登录成功')
|
||||
config.write_config('module.tx.user.uin',
|
||||
str(body['req1']['data']['musicid']))
|
||||
logger.info('已通过相应数据更新uin')
|
||||
config.write_config('module.tx.user.qqmusic_key',
|
||||
body['req1']['data']['musickey'])
|
||||
logger.info('已通过相应数据更新qqmusic_key')
|
||||
elif (config.read_config('module.tx.user.qqmusic_key').startswith('Q_H_L')):
|
||||
options = {
|
||||
'method': 'POST',
|
||||
'body': json.dumps({
|
||||
'req1': {
|
||||
'module': 'QQConnectLogin.LoginServer',
|
||||
'method': 'QQLogin',
|
||||
'param': {
|
||||
'expired_in': 7776000,
|
||||
'musicid': int(config.read_config('module.tx.user.uin')),
|
||||
'musickey': config.read_config('module.tx.user.qqmusic_key')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
signature = sign(options['body'])
|
||||
req = await Httpx.AsyncRequest(f'https://u6.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
|
||||
body = req.json()
|
||||
if (body['req1']['code'] != 0):
|
||||
logger.warning('刷新登录失败, code: ' +
|
||||
str(body['req1']['code']) + f'\n响应体: {body}')
|
||||
return
|
||||
else:
|
||||
logger.info('刷新登录成功')
|
||||
config.write_config('module.tx.user.uin',
|
||||
str(body['req1']['data']['musicid']))
|
||||
logger.info('已通过相应数据更新uin')
|
||||
config.write_config('module.tx.user.qqmusic_key',
|
||||
body['req1']['data']['musickey'])
|
||||
logger.info('已通过相应数据更新qqmusic_key')
|
||||
def _build_request_body(user_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""构建统一请求体结构"""
|
||||
return {
|
||||
"comm": {
|
||||
"fPersonality": "0",
|
||||
"tmeLoginType": "2"
|
||||
if user_info["qqmusic_key"].startswith("Q_H_L")
|
||||
else "1",
|
||||
"qq": str(user_info["uin"]),
|
||||
"authst": user_info["qqmusic_key"],
|
||||
"ct": "11",
|
||||
"cv": "12080008",
|
||||
"v": "12080008",
|
||||
"tmeAppID": "qqmusic",
|
||||
},
|
||||
"req1": {
|
||||
"module": "music.login.LoginServer",
|
||||
"method": "Login",
|
||||
"param": {
|
||||
"str_musicid": str(user_info["uin"]),
|
||||
"musickey": user_info["qqmusic_key"],
|
||||
"refresh_key": user_info.get("refresh_key", ""),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def _update_user_config(
|
||||
user_info: Dict[str, Any], new_data: Dict[str, Any]
|
||||
) -> None:
|
||||
"""统一更新用户配置"""
|
||||
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", "")),
|
||||
}
|
||||
|
||||
if variable.use_cookie_pool:
|
||||
user_list = config.read_config("module.cookiepool.tx")
|
||||
target_user = next((u for u in user_list if u["uin"] == user_info["uin"]), None)
|
||||
if target_user:
|
||||
target_user.update(updates)
|
||||
config.write_config("module.cookiepool.tx", user_list)
|
||||
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):
|
||||
scheduler.append('qqmusic_refresh_login', refresh,
|
||||
config.read_config('module.tx.user.refresh_login.interval'))
|
||||
async def _process_refresh(user_info: Dict[str, Any]) -> Optional[bool]:
|
||||
"""统一处理刷新逻辑"""
|
||||
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')):
|
||||
options = {
|
||||
'method': 'POST',
|
||||
'body': json.dumps({
|
||||
"comm": {
|
||||
"fPersonality": "0",
|
||||
"tmeLoginType": "1",
|
||||
"tmeLoginMethod": "1",
|
||||
"qq": "",
|
||||
"authst": "",
|
||||
"ct": "11",
|
||||
"cv": "12080008",
|
||||
"v": "12080008",
|
||||
"tmeAppID": "qqmusic"
|
||||
# 发送请求
|
||||
response = await Httpx.AsyncRequest(
|
||||
f"https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}",
|
||||
{
|
||||
"method": "POST",
|
||||
"body": json.dumps(request_body),
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
},
|
||||
"req1": {
|
||||
"module": "music.login.LoginServer",
|
||||
"method": "Login",
|
||||
"param": {
|
||||
"code": "",
|
||||
"openid": "",
|
||||
"refresh_token": "",
|
||||
"str_musicid": str(user_info['uin']),
|
||||
"musickey": user_info['qqmusic_key'],
|
||||
"unionid": "",
|
||||
"refresh_key": "",
|
||||
"loginMode": 2
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
signature = sign(options['body'])
|
||||
req = await Httpx.AsyncRequest(f'https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
|
||||
body = req.json()
|
||||
if (body['req1']['code'] != 0):
|
||||
logger.warning(f'为QQ音乐账号({user_info["uin"]})刷新登录失败, code: ' +
|
||||
str(body['req1']['code']) + f'\n响应体: {body}')
|
||||
return
|
||||
else:
|
||||
logger.info(f'为QQ音乐账号(WeChat_{user_info["uin"]})刷新登录成功')
|
||||
user_list = config.read_config('module.cookiepool.tx')
|
||||
user_list[user_list.index(
|
||||
user_info)]['qqmusic_key'] = body['req1']['data']['musickey']
|
||||
user_list[user_list.index(
|
||||
user_info)]['uin'] = str(body['req1']['data']['musicid'])
|
||||
config.write_config('module.cookiepool.tx', user_list)
|
||||
logger.info(f'为QQ音乐账号(WeChat_{user_info["uin"]})数据更新完毕')
|
||||
return
|
||||
elif (user_info['qqmusic_key'].startswith('Q_H_L')):
|
||||
options = {
|
||||
'method': 'POST',
|
||||
'body': json.dumps({
|
||||
'req1': {
|
||||
'module': 'QQConnectLogin.LoginServer',
|
||||
'method': 'QQLogin',
|
||||
'param': {
|
||||
'expired_in': 7776000,
|
||||
'musicid': int(user_info['uin']),
|
||||
'musickey': user_info['qqmusic_key']
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
signature = sign(options['body'])
|
||||
req = await Httpx.AsyncRequest(f'https://u6.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
|
||||
body = req.json()
|
||||
if (body['req1']['code'] != 0):
|
||||
},
|
||||
)
|
||||
|
||||
response_data = response.json()
|
||||
if response_data.get("req1", {}).get("code") != 0:
|
||||
logger.warning(
|
||||
f'为QQ音乐账号({user_info["uin"]})刷新登录失败, code: ' + str(body['req1']['code']) + f'\n响应体: {body}')
|
||||
return
|
||||
else:
|
||||
logger.info(f'为QQ音乐账号(QQ_{user_info["uin"]})刷新登录成功')
|
||||
user_list = config.read_config('module.cookiepool.tx')
|
||||
user_list[user_list.index(
|
||||
user_info)]['qqmusic_key'] = body['req1']['data']['musickey']
|
||||
user_list[user_list.index(
|
||||
user_info)]['uin'] = str(body['req1']['data']['musicid'])
|
||||
config.write_config('module.cookiepool.tx', user_list)
|
||||
logger.info(f'为QQ音乐账号(QQ_{user_info["uin"]})数据更新完毕')
|
||||
return
|
||||
else:
|
||||
logger.warning(f'为QQ音乐账号({user_info["uin"]})刷新登录失败: 未知或不支持的key类型')
|
||||
f"刷新失败 [账号: {user_info['uin']} 代码: {response_data['req1']['code']}]"
|
||||
)
|
||||
return False
|
||||
|
||||
# 更新配置
|
||||
await _update_user_config(user_info, response_data["req1"]["data"])
|
||||
logger.info(f"刷新成功 [账号: {user_info['uin']}]")
|
||||
return True
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error(
|
||||
"响应解析失败 [账号: %s] 原始响应: %s",
|
||||
user_info["uin"],
|
||||
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
|
||||
|
||||
def reg_refresh_login_pool_task():
|
||||
user_info_pool = config.read_config('module.cookiepool.tx')
|
||||
for user_info in user_info_pool:
|
||||
if (user_info['refresh_login'].get('enable')):
|
||||
scheduler.append(
|
||||
f'qqmusic_refresh_login_pooled_{user_info["uin"]}', refresh_login_for_pool, user_info['refresh_login']['interval'], args = {'user_info': user_info})
|
||||
await _process_refresh(
|
||||
{
|
||||
"uin": config.read_config("module.tx.user.uin"),
|
||||
"qqmusic_key": config.read_config("module.tx.user.qqmusic_key"),
|
||||
"refresh_key": config.read_config("module.tx.user.refresh_key"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if (variable.use_cookie_pool):
|
||||
reg_refresh_login_pool_task()
|
||||
async def refresh_login_for_pool(user_info: Dict[str, Any]) -> None:
|
||||
"""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()
|
||||
|
@ -1,9 +1,9 @@
|
||||
# ----------------------------------------
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: __init__.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: __init__.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# ----------------------------------------
|
||||
# This file is part of the "lx-music-api-server" project.
|
||||
|
||||
@ -13,51 +13,107 @@ from common import config
|
||||
from common.exceptions import FailedException
|
||||
from .encrypt import eapiEncrypt
|
||||
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 = {
|
||||
'qualityMap': {
|
||||
'128k': 'standard',
|
||||
"192k": "higher",
|
||||
'320k': 'exhigh',
|
||||
'flac': 'lossless',
|
||||
'flac24bit': 'hires',
|
||||
"dolby": "jyeffect",
|
||||
"sky": "jysky",
|
||||
"sky": "sky",
|
||||
"master": "jymaster",
|
||||
"standard": "standard",
|
||||
"higher": "higher",
|
||||
"exhigh": "exhigh",
|
||||
"lossless": "lossless",
|
||||
"hires": "hires",
|
||||
"jyeffect": "jyeffect",
|
||||
"jymaster": "jymaster",
|
||||
},
|
||||
'qualityMapReverse': {
|
||||
'standard': '128k',
|
||||
"higher": "192k",
|
||||
'exhigh': '320k',
|
||||
'lossless': 'flac',
|
||||
'hires': 'flac24bit',
|
||||
"jyeffect": "dolby",
|
||||
"jysky": "sky",
|
||||
"sky": "sky",
|
||||
"jymaster": "master",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def url(songId, quality):
|
||||
path = '/api/song/enhance/player/url/v1'
|
||||
requestUrl = 'https://interface.music.163.com/eapi/song/enhance/player/url/v1'
|
||||
req = await Httpx.AsyncRequest(requestUrl, {
|
||||
'method': 'POST',
|
||||
'headers': {
|
||||
'Cookie': config.read_config('module.wy.user.cookie') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.wy'))['cookie'],
|
||||
},
|
||||
'form': eapiEncrypt(path, json.dumps({
|
||||
if PROTO == "offcial":
|
||||
path = '/api/song/enhance/player/url/v1'
|
||||
requestUrl = 'https://interface.music.163.com/eapi/song/enhance/player/url/v1'
|
||||
requestBody = {
|
||||
"ids": json.dumps([songId]),
|
||||
"level": tools["qualityMap"][quality],
|
||||
"encodeType": "flac",
|
||||
}))
|
||||
})
|
||||
body = req.json()
|
||||
if (not body.get("data") or (not body.get("data")) or (not body.get("data")[0].get("url"))):
|
||||
raise FailedException("failed")
|
||||
}
|
||||
if (quality == "sky"):
|
||||
requestBody["immerseType"] = "c51"
|
||||
req = await Httpx.AsyncRequest(requestUrl, {
|
||||
'method': 'POST',
|
||||
'headers': {
|
||||
'Cookie': config.read_config('module.wy.user.cookie') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.wy'))['cookie'],
|
||||
},
|
||||
'form': eapiEncrypt(path, json.dumps(requestBody))
|
||||
})
|
||||
body = req.json()
|
||||
if (not body.get("data") or (not body.get("data")) or (not body.get("data")[0].get("url"))):
|
||||
raise FailedException("失败")
|
||||
|
||||
data = body["data"][0]
|
||||
if (data['level'] != tools['qualityMap'][quality]):
|
||||
raise FailedException("reject unmatched quality")
|
||||
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': 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]
|
||||
|
||||
return {
|
||||
'url': data["url"].split("?")[0],
|
||||
'quality': tools['qualityMapReverse'][data['level']]
|
||||
}
|
||||
# 修正:映射服务器返回的 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,23 +1,29 @@
|
||||
[tool.poetry]
|
||||
name = "lx-music-api-server-python"
|
||||
version = "2.0.0.beta-9"
|
||||
name = "lx_music_api_server"
|
||||
version = "2.0.0"
|
||||
description = "一个适配 LX Music 的 API 后端实现"
|
||||
authors = ["helloplhm-qwq", "lerdb", "Folltoshe"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
package-mode = false
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
aiohttp = "^3.9.3"
|
||||
pycryptodome = "^3.20.0"
|
||||
ujson = "^5.9.0"
|
||||
requests = "^2.31.0"
|
||||
colorlog = "^6.8.2"
|
||||
Pygments = "^2.17.2"
|
||||
xmltodict = "^0.13.0"
|
||||
pillow = "^10.2.0"
|
||||
aiohttp = "^3.10.10"
|
||||
pycryptodome = "^3.22.0"
|
||||
ujson = "^5.10.0"
|
||||
requests = "^2.32.3"
|
||||
colorlog = "^6.9.0"
|
||||
Pygments = "^2.19.1"
|
||||
xmltodict = "^0.14.2"
|
||||
pillow = "^10.4.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]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
build-backend = "poetry.core.masonry.api"
|
@ -7,4 +7,8 @@ pygments
|
||||
xmltodict
|
||||
toml
|
||||
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