mirror of
https://github.com/MeoProject/lx-music-api-server.git
synced 2025-05-23 19:17:41 +08:00
Compare commits
235 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 | ||
|
4fb209dd94 | ||
|
2d290e05d9 | ||
|
1d0309d5d1 | ||
|
98e6a3280d | ||
|
339e5edf3d | ||
|
a8e4d8ac69 | ||
|
de0b033884 | ||
|
0997464d39 | ||
|
bdd49824aa | ||
|
7a0bf4932d | ||
|
0e3cd085eb | ||
|
567eadcbe1 | ||
|
761f2fc862 | ||
|
068b58a619 | ||
|
ff276cb38a | ||
|
827c5c59b4 | ||
|
47d4bb3d3a | ||
|
af1c69a6e3 | ||
|
61f426e915 | ||
|
0af0515d9c | ||
|
f927f153a8 | ||
|
8457dd4cef | ||
|
31ec95bcc0 | ||
|
7d1047ea34 | ||
|
878a9f8c0d | ||
|
896dd6f698 | ||
|
859beddfaa | ||
|
6ac5d8c388 | ||
|
b202d92b10 | ||
|
50990fed63 | ||
|
56cc849614 | ||
|
5a1a8c4e3a | ||
|
e1d0e16d33 | ||
|
ae729ba66e | ||
|
27a02d0709 | ||
|
67571e37c1 | ||
|
de46ab907f | ||
|
7e35fee479 | ||
|
6d798d807b | ||
|
220bf6831b | ||
|
b615f42a19 | ||
|
5c167b911c | ||
|
8f84af70ac | ||
|
73d95b6032 | ||
|
eca696c79b | ||
|
d1e23d94c6 | ||
|
42fd54389e | ||
|
09f34fd2b9 | ||
|
39caeeb816 | ||
|
54222f89ce | ||
|
610343c8e1 | ||
|
2899cac40c | ||
|
ee0c894d05 | ||
|
08375b9b10 | ||
|
c11cacd8c0 | ||
|
3d2361ea85 | ||
|
1e87fc7194 | ||
|
f3b0a4d5a6 | ||
|
e0f96333d0 | ||
|
5b1f741845 | ||
|
1d246115fc | ||
|
e1d30cfbbf | ||
|
9a9e64ac19 | ||
|
2f869694d5 |
58
.github/workflows/build_beta.yml
vendored
58
.github/workflows/build_beta.yml
vendored
@ -1,16 +1,20 @@
|
||||
# this workflow using github actions to build a binary exe file for windows users
|
||||
|
||||
name: build beta
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.gitignore'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
runs-on: [windows-latest, macos-latest, ubuntu-latest, ubuntu-20.04]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v2
|
||||
@ -26,37 +30,37 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: python -m pip install -r ./requirements.txt
|
||||
|
||||
- name: Build EXE
|
||||
- name: Build
|
||||
run: python build.py build test
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows
|
||||
name: dist-${{ matrix.runs-on }}
|
||||
path: ./dist
|
||||
|
||||
build-linux:
|
||||
|
||||
post:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python 3.11
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Download Build Files
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
path: dist
|
||||
pattern: dist-*
|
||||
merge-multiple: false
|
||||
|
||||
- name: Install pyinstaller
|
||||
run: python -m pip install pyinstaller
|
||||
|
||||
- name: Install dependencies
|
||||
run: python -m pip install -r ./requirements.txt
|
||||
|
||||
- name: Build Executable
|
||||
run: python build.py build test
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: linux
|
||||
path: ./dist
|
||||
- name: Get File Address
|
||||
run: |
|
||||
echo "windowsFile=./dist/dist-windows-latest/lx-music-api-server_$(git rev-parse --short HEAD).exe" >> $GITHUB_ENV
|
||||
echo "macosFile=./dist/dist-macos-latest/lx-music-api-server_$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
echo "linuxFile=./dist/dist-ubuntu-latest/lx-music-api-server_$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Post to Channel
|
||||
run: |
|
||||
echo ${{ secrets.SCRIPT_BASE64 }} | base64 -d > test.py
|
||||
ESCAPED=`python test.py`
|
||||
curl -s -v "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMediaGroup?chat_id=${{ secrets.TELEGRAM_ID }}&media=%5B%7B%22type%22:%22document%22,%20%22media%22:%22attach://windows%22,%22parse_mode%22:%22MarkdownV2%22,%22caption%22:${ESCAPED}%7D%5D" -F windows=@${{ env.windowsFile }}
|
||||
|
94
.github/workflows/build_release.yml
vendored
94
.github/workflows/build_release.yml
vendored
@ -1,12 +1,23 @@
|
||||
|
||||
name: Build Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.gitignore'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
build:
|
||||
if: contains(github.event.head_commit.message, '[release]')
|
||||
strategy:
|
||||
matrix:
|
||||
runs-on: [windows-latest, macos-latest, ubuntu-latest]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v2
|
||||
@ -22,37 +33,70 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: python -m pip install -r ./requirements.txt
|
||||
|
||||
- name: Build EXE
|
||||
- name: Build
|
||||
run: python build.py build release
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows
|
||||
name: dist-${{ matrix.runs-on }}
|
||||
path: ./dist
|
||||
|
||||
build-linux:
|
||||
|
||||
release:
|
||||
if: contains(github.event.head_commit.message, '[release]')
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Python 3.11
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install pyinstaller
|
||||
run: python -m pip install pyinstaller
|
||||
|
||||
- name: Install dependencies
|
||||
run: python -m pip install -r ./requirements.txt
|
||||
|
||||
- name: Build Executable
|
||||
run: python build.py build release
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v2
|
||||
- name: Get version
|
||||
run: |
|
||||
python -m pip install toml
|
||||
echo PACKAGE_VERSION=`python -c 'import toml; print(toml.load("./pyproject.toml")["tool"]["poetry"]["version"])'` >> $GITHUB_ENV
|
||||
|
||||
- name: Generate Changelog
|
||||
run: |
|
||||
git fetch --prune --unshallow --tags
|
||||
python build.py changelog >> ./changelog.md
|
||||
|
||||
- name: Create git tag
|
||||
uses: pkgdeps/git-tag-action@v2
|
||||
with:
|
||||
name: linux
|
||||
path: ./dist
|
||||
github_token: ${{ github.token }}
|
||||
github_repo: ${{ github.repository }}
|
||||
version: ${{ env.PACKAGE_VERSION }}
|
||||
git_commit_sha: ${{ github.sha }}
|
||||
git_tag_prefix: "v"
|
||||
|
||||
- name: Download Build Files
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
pattern: dist-*
|
||||
merge-multiple: false
|
||||
|
||||
- name: Rename Files
|
||||
run: |
|
||||
mv ./dist/dist-windows-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }}.exe ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_windows.exe
|
||||
mv ./dist/dist-macos-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }} ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_macos
|
||||
mv ./dist/dist-ubuntu-latest/lx-music-api-server_${{ env.PACKAGE_VERSION }} ./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_ubuntu
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: ./changelog.md
|
||||
prerelease: false
|
||||
draft: false
|
||||
tag_name: v${{ env.PACKAGE_VERSION }}
|
||||
files: |
|
||||
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_windows.exe
|
||||
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_macos
|
||||
./dist/lx-music-api-server_${{ env.PACKAGE_VERSION }}_ubuntu
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
14
.gitignore
vendored
14
.gitignore
vendored
@ -18,11 +18,16 @@ build
|
||||
# project
|
||||
cache.db
|
||||
data.db
|
||||
test.py
|
||||
*/test.py
|
||||
test.*
|
||||
*/test.*
|
||||
logs
|
||||
config.json
|
||||
config.yml
|
||||
/config/config.json
|
||||
/config/data.db
|
||||
*.log
|
||||
*.bak
|
||||
*.tmp
|
||||
|
||||
# VSCode
|
||||
.history
|
||||
@ -36,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" ]
|
295
README.md
295
README.md
@ -1,90 +1,110 @@
|
||||
简体中文 | [English](README_EN.md)
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||

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

|
||||
[](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池
|
||||
- [x] https监听,多端口监听
|
||||
- [x] 反代兼容性
|
||||
- [x] 获取更高的音质
|
||||
- [x] QRC解密
|
||||
- [ ] 本地化支持(目前仅支持简体中文)
|
||||
- [x] 多端部署(`Windows` `Linux` `MacOS`)
|
||||
|
||||
### poetry部署
|
||||
## 💻部署方法
|
||||
|
||||
### Release 部署(推荐)
|
||||
|
||||
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. 运行可执行文件(如果下载的文件是压缩包请先解压)
|
||||
|
||||
---
|
||||
|
||||
### Poetry 部署
|
||||
|
||||
环境要求: Python 3.8+
|
||||
1. 安装 poetry
|
||||
```bash
|
||||
pip install poetry
|
||||
```
|
||||
|
||||
2. clone本项目
|
||||
```bash
|
||||
git clone https://github.com/lxmusics/lx-music-api-server-python.git
|
||||
cd lx-music-api-server-python
|
||||
```
|
||||
1. 安装 poetry
|
||||
|
||||
```bash
|
||||
pip install poetry
|
||||
```
|
||||
|
||||
2. clone本项目并进入项目目录
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||
cd lx-music-api-server
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
4. 启动
|
||||
```bash
|
||||
poetry shell # 进入poetry环境
|
||||
python main.py # 运行项目
|
||||
```
|
||||
|
||||
```bash
|
||||
poetry shell # 进入poetry环境
|
||||
python main.py # 运行项目
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 直接部署
|
||||
|
||||
环境要求:Python 3.6+, 建议Python 3.8+
|
||||
环境要求:Python 3.6 - 3.11, 建议Python 3.8+
|
||||
|
||||
Python 3.12 及以上会装不上依赖
|
||||
没有其他限制,能用 Python 理论上就能跑起来
|
||||
flask版本即将停止支持,不建议使用
|
||||
测试版本部署,linux 命令如果为 python3 请自行替换:
|
||||
linux 命令如果为 python3 请自行替换
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lxmusics/lx-music-api-server-python.git # clone本项目
|
||||
cd lx-music-api-server-python # 进入项目目录
|
||||
python -m pip install -r ./requirements.txt # 安装依赖
|
||||
python main.py # 启动服务
|
||||
```
|
||||
1. clone本项目并进入项目目录
|
||||
|
||||
对于 release 的部署和上方类似,这里不再赘述
|
||||
```bash
|
||||
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||
cd lx-music-api-server
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
2. 安装依赖
|
||||
|
||||
环境要求:Docker
|
||||
```bash
|
||||
python -m pip install -r ./requirements.txt
|
||||
```
|
||||
|
||||
**该方法未经测试,不知道需要的 Docker 版本,可以自己尝试现有的 Docker 版本是否可以使用**
|
||||
3. 启动
|
||||
|
||||
```bash
|
||||
# 更新软件包
|
||||
sudo apt-get update
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
# 安装Docker,已有跳过
|
||||
sudo apt-get install -y docker.io
|
||||
|
||||
# 创建容器
|
||||
docker run --name lx-music-api-server-python -p 9763:9763 -d ikun0014/lx-music-api-server-python:latest
|
||||
|
||||
# 获取容器目录
|
||||
docker inspect lx-music-api-server-python
|
||||
```
|
||||
|
||||
**配置文件填写需要到容器目录里的 /app 目录填写 config.json**
|
||||
|
||||
## 返回码说明
|
||||
## 📖返回码说明
|
||||
|
||||
接口返回值中`body.code`字段值中的代码含义
|
||||
|
||||
@ -107,100 +127,105 @@ docker inspect lx-music-api-server-python
|
||||
| 429 | 请求过于频繁 |
|
||||
| 500 | 服务器内部错误(对应 body.code 4) |
|
||||
|
||||
## 备注
|
||||
## 🔖备注
|
||||
|
||||
### 本项目中可能会出现以下优秀代码
|
||||
|
||||
1. 三角形具有稳定性
|
||||
```python
|
||||
for a in xxx:
|
||||
if (xxx):
|
||||
if (xxx):
|
||||
|
||||
```python
|
||||
for a in xxx:
|
||||
if (xxx):
|
||||
for b in xxx:
|
||||
if (xxx):
|
||||
if (xxx):
|
||||
while (xxx):
|
||||
for b in xxx:
|
||||
if (xxx):
|
||||
while (xxx):
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
```
|
||||
```
|
||||
|
||||
2. 能一行写完那就坚决不多行
|
||||
```python
|
||||
sys.stdout.write('\r|'+'=' * (int(dd['pares'].index(ds) / total * 50)) + ' ' * (49 - int(dd['pares'].index(ds) / total * 50)) + f'''|{int(dd['pares'].index(ds) / total * 100)}% xx''' + ds['title']+' ' * 20)
|
||||
```
|
||||
|
||||
```python
|
||||
sys.stdout.write('\r|'+'=' * (int(dd['pares'].index(ds) / total * 50)) + ' ' * (49 - int(dd['pares'].index(ds) / total * 50)) + f'''|{int(dd['pares'].index(ds) / total * 100)}% xx''' + ds['title']+' ' * 20)
|
||||
```
|
||||
|
||||
3. 不复用重复部分
|
||||
```python
|
||||
async def other(method, source, songid, _):
|
||||
try:
|
||||
func = require('modules.' + source + '.' + method)
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
}
|
||||
try:
|
||||
result = await func(songid)
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
}
|
||||
|
||||
async def other_with_query(method, source, t, _, query):
|
||||
try:
|
||||
func = require('modules.' + source + '.' + method)
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
}
|
||||
try:
|
||||
result = await func(t, query)
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
}
|
||||
```
|
||||
```python
|
||||
async def other(method, source, songid, _):
|
||||
try:
|
||||
func = require('modules.' + source + '.' + method)
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
}
|
||||
try:
|
||||
result = await func(songid)
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
}
|
||||
|
||||
async def other_with_query(method, source, t, _, query):
|
||||
try:
|
||||
func = require('modules.' + source + '.' + method)
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
}
|
||||
try:
|
||||
result = await func(t, query)
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
}
|
||||
```
|
||||
|
||||
4. 模块不拆分
|
||||
详见[config.py](https://github.com/lxmusics/lx-music-api-server-python/tree/main/common/config.py)
|
||||
|
||||
详见[config.py](https://github.com/MeoProject/lx-music-api-server/tree/main/common/config.py)
|
||||
|
||||
5. 不明所以的变量名
|
||||
```python
|
||||
a = '小明'
|
||||
b = 1
|
||||
c = 2
|
||||
d = b''
|
||||
def e(a, b, c):
|
||||
c = xxx
|
||||
d = xxx
|
||||
f = e(c, b, a)
|
||||
```
|
||||
|
||||
## 项目协议
|
||||
```python
|
||||
a = '小明'
|
||||
b = 1
|
||||
c = 2
|
||||
d = b''
|
||||
def e(a, b, c):
|
||||
c = xxx
|
||||
d = xxx
|
||||
f = e(c, b, a)
|
||||
```
|
||||
|
||||
本项目基于 [MIT](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE) 许可证发行,以下协议是对于 MIT 原协议的补充,如有冲突,以以下协议为准。
|
||||
## 📄项目协议
|
||||
|
||||
本项目基于 [MIT](https://github.com/MeoProject/lx-music-api-server/blob/main/LICENSE) 许可证发行,以下协议是对于 MIT 原协议的补充,如有冲突,以以下协议为准。
|
||||
|
||||
词语约定:本协议中的“本项目”指本音源项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。
|
||||
|
||||
@ -218,12 +243,10 @@ helloplhm-qwq+outlook.com
|
||||
folltoshe+foxmail.com
|
||||
(请将`+`替换成`@`)
|
||||
|
||||
## Star 趋势图
|
||||
## ✨Star 趋势图
|
||||
|
||||
[](https://starchart.cc/lxmusics/lx-music-api-server-python)
|
||||
[](https://starchart.cc/MeoProject/lx-music-api-server)
|
||||
|
||||
## 贡献者
|
||||
## ⚙️贡献者
|
||||
|
||||
<a href="https://github.com/lxmusics/lx-music-api-server-python/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=lxmusics/lx-music-api-server-python" />
|
||||
</a>
|
||||
[](https://github.com/MeoProject/lx-music-api-server/graphs/contributors)
|
||||
|
249
README_EN.md
Normal file
249
README_EN.md
Normal file
@ -0,0 +1,249 @@
|
||||
English | [简体中文](README.md)
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||

|
||||
[](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>
|
||||
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)
|
||||
- [x] Netease Cloud Refresh Login
|
||||
- [ ] Client plays music on webdav
|
||||
- [x] Client plays local music on the server
|
||||
- [x] Cookie pool
|
||||
- [x] HTTPS listening, multi port listening
|
||||
- [x] Reverse proxy compatibility
|
||||
- [x] Get higher quality
|
||||
- [x] QRC decrypt
|
||||
- [ ] Localization (currently only Simplified Chinese is supported)
|
||||
- [x] Platform support: `Windows` `Linux` `MacOS`
|
||||
|
||||
## 💻How to deploy
|
||||
|
||||
### Use Release (recommended)
|
||||
|
||||
1. Download the executable file corresponding to your system from [Releases](https://github.com/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)
|
||||
|
||||
---
|
||||
|
||||
### Use Poetry
|
||||
|
||||
Required environment: Python 3.8+
|
||||
|
||||
1. Install poetry
|
||||
|
||||
```bash
|
||||
pip install poetry
|
||||
```
|
||||
|
||||
2. Clone this project and enter the project directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||
cd lx-music-api-server
|
||||
```
|
||||
|
||||
3. Install requirements
|
||||
|
||||
```bash
|
||||
poetry install --no-root
|
||||
```
|
||||
|
||||
4. Run it
|
||||
|
||||
```bash
|
||||
poetry shell # enter poetry environment
|
||||
python main.py # run project
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Directly deploy
|
||||
|
||||
Required environment: Python 3.6 - 3.11, Python 3.8+ is better.
|
||||
|
||||
Python 3.12 or higher maybe install requirements failed.
|
||||
Without other restrictions, you can run only with Python.
|
||||
If you are using linux, you command maybe python3, please replace it yourself.
|
||||
|
||||
1. Clone this project and enter the project directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MeoProject/lx-music-api-server.git
|
||||
cd lx-music-api-server
|
||||
```
|
||||
|
||||
2. Install requirements
|
||||
|
||||
```bash
|
||||
python -m pip install -r ./requirements.txt
|
||||
```
|
||||
|
||||
3. Run it
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## 📖Return code description
|
||||
|
||||
The code meaning in the `body.code` field value in the interface return value.
|
||||
|
||||
| Value | Meaning |
|
||||
| ---- | --------------------------------------------------- |
|
||||
| 0 | Success |
|
||||
| 1 | IP is banned or does not support anti-generation |
|
||||
| 2 | Fail to obtain. |
|
||||
| 4 | Server internal error (corresponding to statuscode 500) |
|
||||
| 5 | Too frequent requests |
|
||||
| 6 | Parameter error |
|
||||
|
||||
The code meaning of `statuscode` returned by the interface.
|
||||
|
||||
| Value | Meaning |
|
||||
| ---- | -------------------------------------------------- |
|
||||
| 200 | Success |
|
||||
| 403 | IP is banned |
|
||||
| 400 | Parameter error |
|
||||
| 429 | Too frequent requests |
|
||||
| 500 | Server internal error (corresponding to body.code 4) |
|
||||
|
||||
## 🔖Remarks
|
||||
|
||||
### The following excellent codes may appear in this project.
|
||||
|
||||
1. Triangle has stability.
|
||||
|
||||
```python
|
||||
for a in xxx:
|
||||
if (xxx):
|
||||
if (xxx):
|
||||
if (xxx):
|
||||
for b in xxx:
|
||||
if (xxx):
|
||||
while (xxx):
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
```
|
||||
|
||||
2. If you can finish it in one line, then don't write many lines.
|
||||
|
||||
```python
|
||||
sys.stdout.write('\r|'+'=' * (int(dd['pares'].index(ds) / total * 50)) + ' ' * (49 - int(dd['pares'].index(ds) / total * 50)) + f'''|{int(dd['pares'].index(ds) / total * 100)}% xx''' + ds['title']+' ' * 20)
|
||||
```
|
||||
|
||||
3. Do not reuse duplicate parts
|
||||
|
||||
```python
|
||||
async def other(method, source, songid, _):
|
||||
try:
|
||||
func = require('modules.' + source + '.' + method)
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
}
|
||||
try:
|
||||
result = await func(songid)
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
}
|
||||
|
||||
async def other_with_query(method, source, t, _, query):
|
||||
try:
|
||||
func = require('modules.' + source + '.' + method)
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
}
|
||||
try:
|
||||
result = await func(t, query)
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
}
|
||||
```
|
||||
|
||||
4. Module does not split
|
||||
|
||||
Details at [config.py](https://github.com/MeoProject/lx-music-api-server/tree/main/common/config.py)
|
||||
|
||||
5. Unknown variable name
|
||||
|
||||
```python
|
||||
a = '小明'
|
||||
b = 1
|
||||
c = 2
|
||||
d = b''
|
||||
def e(a, b, c):
|
||||
c = xxx
|
||||
d = xxx
|
||||
f = e(c, b, a)
|
||||
```
|
||||
|
||||
## 📄Project agreement
|
||||
|
||||
This project is issued under [MIT](https://github.com/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.
|
||||
|
||||
1. The data source principle of this project is to pull data from the public servers of official music platforms, and display the data after simple screening and merging, so this project is not responsible for the accuracy of the data.
|
||||
2. Copyright data may be generated during the use of this project, and this project does not own the copyright data. In order to avoid infringement, users must clear the copyright data generated during the use of this project within **24 hours**.
|
||||
3. Any direct, indirect, special, accidental or consequential damages of any nature arising from the use of this project (including but not limited to damages caused by loss of goodwill, shutdown, computer failure or malfunction, or any and all other commercial damages or losses) shall be borne by the user.
|
||||
4. This project is completely free of charge, and the open source is published on GitHub for people all over the world to learn and exchange technology. This project does not guarantee that the technology in the project may violate local laws and regulations. **It is forbidden to use this project in violation of local laws and regulations.** The user shall bear any illegal acts caused by the user knowing or not knowing that the local laws and regulations do not allow it, and this project will not bear any direct, indirect, special, accidental or consequential responsibilities.
|
||||
|
||||
If you use this project, you will accept the above agreement on your behalf.
|
||||
|
||||
Music platform is not easy, please respect copyright and support genuine.
|
||||
This project is only used for the exploration and research of technical feasibility, and does not accept any commercial (including but not limited to advertising) cooperation and donation.
|
||||
If you have any questions about this, please mail to:
|
||||
helloplhm-qwq+outlook.com
|
||||
folltoshe+foxmail.com
|
||||
(please replace `+` to `@`)
|
||||
|
||||
## ✨Star trend chart
|
||||
|
||||
[](https://starchart.cc/MeoProject/lx-music-api-server)
|
||||
|
||||
## ⚙️Contributor
|
||||
|
||||
[](https://github.com/MeoProject/lx-music-api-server/graphs/contributors)
|
36
build.py
36
build.py
@ -1,16 +1,26 @@
|
||||
import subprocess
|
||||
import toml
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
|
||||
def get_latest_tag():
|
||||
return subprocess.check_output(['git', 'describe', '--abbrev=0', '--tags']).decode('utf-8').strip()
|
||||
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', '--sort=v:refname']).decode('utf-8').strip().split("\n")
|
||||
n = []
|
||||
for i in r:
|
||||
if (i):
|
||||
n.append(i.strip())
|
||||
return n[index]
|
||||
|
||||
def get_changelog():
|
||||
res = subprocess.check_output(
|
||||
['git', 'log', f'{get_latest_tag()}..HEAD', '--pretty=format:"%h %s"']).decode('utf-8').strip()
|
||||
cmd = ['git', 'log', f'{get_specified_tag(-1)}..HEAD', '--pretty=format:"%h %s"']
|
||||
# print(cmd)
|
||||
res = subprocess.check_output(cmd).decode('utf-8').strip()
|
||||
res = res.split('\n')
|
||||
featMsg = []
|
||||
fixMsg = []
|
||||
@ -20,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)
|
||||
@ -94,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'])
|
||||
@ -121,12 +133,14 @@ def build_release(fileName = ''):
|
||||
'Please check if you forgetting to activate the virtualenv.', sep='\n')
|
||||
sys.exit(1)
|
||||
|
||||
vername = get_latest_tag()
|
||||
vername = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
|
||||
|
||||
popen = subprocess.Popen([get_python_path(),
|
||||
'-m',
|
||||
'PyInstaller',
|
||||
'-F',
|
||||
'-i',
|
||||
'res/icon.ico',
|
||||
'--name',
|
||||
fileName if fileName else f'lx-music-api-server_{vername}',
|
||||
'main.py'])
|
||||
@ -217,4 +231,4 @@ if __name__ == '__main__':
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print('[INFO] Aborting...')
|
||||
sys.exit(0)
|
||||
sys.exit(0)
|
||||
|
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
|
||||
|
636
common/config.py
636
common/config.py
@ -13,297 +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"],
|
||||
}
|
||||
},
|
||||
},
|
||||
"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值计算验证值",
|
||||
},
|
||||
"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",
|
||||
}
|
||||
},
|
||||
"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',
|
||||
},
|
||||
],
|
||||
'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
|
||||
|
||||
|
||||
@ -344,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()
|
||||
|
||||
@ -354,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())
|
||||
|
||||
|
||||
@ -406,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())
|
||||
|
||||
|
||||
@ -420,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
|
||||
@ -441,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:
|
||||
@ -456,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:
|
||||
@ -471,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:
|
||||
@ -488,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:
|
||||
@ -499,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):
|
||||
@ -530,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):
|
||||
@ -551,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):
|
||||
@ -567,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:
|
||||
@ -595,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
|
||||
@ -744,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
|
||||
)
|
396
common/localMusic.py
Normal file
396
common/localMusic.py
Normal file
@ -0,0 +1,396 @@
|
||||
# ----------------------------------------
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: localMusic.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# ----------------------------------------
|
||||
# This file is part of the "lx-music-api-server" project.
|
||||
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
from PIL import Image
|
||||
import aiohttp
|
||||
from common.utils import createFileMD5, createMD5, timeLengthFormat
|
||||
from . import log, config
|
||||
import ujson as json
|
||||
import traceback
|
||||
import mutagen
|
||||
import os
|
||||
|
||||
logger = log.log('local_music_handler')
|
||||
|
||||
audios = []
|
||||
map = {}
|
||||
AUDIO_PATH = config.read_config("common.local_music.audio_path")
|
||||
TEMP_PATH = config.read_config("common.local_music.temp_path")
|
||||
FFMPEG_PATH = None
|
||||
|
||||
def convertCover(input_bytes):
|
||||
if (input_bytes.startswith(b'\xff\xd8\xff\xe0')): # jpg object do not need convert
|
||||
return input_bytes
|
||||
temp = TEMP_PATH + '/' + createMD5(input_bytes) + '.img'
|
||||
with open(temp, 'wb') as f:
|
||||
f.write(input_bytes)
|
||||
f.close()
|
||||
img = Image.open(temp)
|
||||
img = img.convert('RGB')
|
||||
with open(temp + 'crt', 'wb') as f:
|
||||
img.save(f, format='JPEG')
|
||||
f.close()
|
||||
data = None
|
||||
with open(temp + 'crt', 'rb') as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
try:
|
||||
os.remove(temp)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.remove(temp + 'crt')
|
||||
except:
|
||||
pass
|
||||
return data
|
||||
|
||||
def check_ffmpeg():
|
||||
logger.info('正在检查ffmpeg')
|
||||
devnull = open(os.devnull, 'w')
|
||||
linux_bin_path = '/usr/bin/ffmpeg'
|
||||
environ_ffpmeg_path = os.environ.get('FFMPEG_PATH')
|
||||
if (platform.system() == 'Windows' or platform.system() == 'Cygwin'):
|
||||
if (environ_ffpmeg_path and (not environ_ffpmeg_path.endswith('.exe'))):
|
||||
environ_ffpmeg_path += '/ffmpeg.exe'
|
||||
else:
|
||||
if (environ_ffpmeg_path and os.path.isdir(environ_ffpmeg_path)):
|
||||
environ_ffpmeg_path += '/ffmpeg'
|
||||
|
||||
if (environ_ffpmeg_path):
|
||||
try:
|
||||
subprocess.Popen([environ_ffpmeg_path, '-version'], stdout=devnull, stderr=devnull)
|
||||
devnull.close()
|
||||
return environ_ffpmeg_path
|
||||
except:
|
||||
pass
|
||||
|
||||
if (os.path.isfile(linux_bin_path)):
|
||||
try:
|
||||
subprocess.Popen([linux_bin_path, '-version'], stdout=devnull, stderr=devnull)
|
||||
devnull.close()
|
||||
return linux_bin_path
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
subprocess.Popen(['ffmpeg', '-version'], stdout=devnull, stderr=devnull)
|
||||
return 'ffmpeg'
|
||||
except:
|
||||
logger.warning('无法找到ffmpeg,对于本地音乐的一些扩展功能无法使用,如果您不需要,请忽略本条提示')
|
||||
logger.warning('如果您已经安装,请将 FFMPEG_PATH 环境变量设置为您的ffmpeg安装路径或者将其添加到PATH中')
|
||||
return None
|
||||
|
||||
def getAudioCoverFromFFMpeg(path):
|
||||
if (not FFMPEG_PATH):
|
||||
return None
|
||||
cmd = [FFMPEG_PATH, '-i', path, TEMP_PATH + '/_tmp.jpg']
|
||||
popen = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stdout)
|
||||
popen.wait()
|
||||
if (os.path.exists(TEMP_PATH + '/_tmp.jpg')):
|
||||
with open(TEMP_PATH + '/_tmp.jpg', 'rb') as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
try:
|
||||
os.remove(TEMP_PATH + '/_tmp.jpg')
|
||||
except:
|
||||
pass
|
||||
return data
|
||||
|
||||
def readFileCheckCover(path):
|
||||
with open(path, 'rb') as f: # read the first 1MB audio
|
||||
data = f.read(1024 * 1024)
|
||||
return b'image/' in data
|
||||
|
||||
def checkLyricValid(lyric_content):
|
||||
if (lyric_content is None):
|
||||
return False
|
||||
if (lyric_content == ''):
|
||||
return False
|
||||
lines = lyric_content.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if (line == ''):
|
||||
continue
|
||||
if (line.startswith('[')):
|
||||
continue
|
||||
if (not line.startswith('[')):
|
||||
return False
|
||||
return True
|
||||
|
||||
def filterLyricLine(lyric_content: str) -> str:
|
||||
lines = lyric_content.split('\n')
|
||||
completed = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if (line.startswith('[')):
|
||||
completed.append(line)
|
||||
continue
|
||||
return '\n'.join(completed)
|
||||
|
||||
def getAudioMeta(filepath):
|
||||
if not os.path.exists(filepath):
|
||||
return None
|
||||
try:
|
||||
audio = mutagen.File(filepath)
|
||||
if not audio:
|
||||
return None
|
||||
logger.debug(audio.items())
|
||||
if (filepath.lower().endswith('.mp3')):
|
||||
cover = audio.get('APIC:')
|
||||
if (cover):
|
||||
cover = convertCover(cover.data)
|
||||
lrc_key = None
|
||||
for k in list(audio.keys()):
|
||||
if (k.startswith('USLT')):
|
||||
lrc_key = k
|
||||
break
|
||||
title = audio.get('TIT2')
|
||||
artist = audio.get('TPE1')
|
||||
album = audio.get('TALB')
|
||||
if (lrc_key):
|
||||
lyric = audio.get(lrc_key)
|
||||
else:
|
||||
lyric = None
|
||||
if (title):
|
||||
title = title.text
|
||||
if (artist):
|
||||
artist = artist.text
|
||||
if (album):
|
||||
album = album.text
|
||||
if (lyric):
|
||||
lyric = [lyric.text]
|
||||
if (not lyric):
|
||||
if (os.path.isfile(os.path.splitext(filepath)[0] + '.lrc')):
|
||||
with open(os.path.splitext(filepath)[0] + '.lrc', 'r', encoding='utf-8') as f:
|
||||
t = f.read().replace('\ufeff', '')
|
||||
logger.debug(t)
|
||||
lyric = filterLyricLine(t)
|
||||
logger.debug(lyric)
|
||||
if (not checkLyricValid(lyric)):
|
||||
lyric = [None]
|
||||
else:
|
||||
lyric = [lyric]
|
||||
f.close()
|
||||
else:
|
||||
lyric = [None]
|
||||
else:
|
||||
cover = audio.get('cover')
|
||||
if (cover):
|
||||
cover = convertCover(cover[0])
|
||||
else:
|
||||
if (readFileCheckCover(filepath)):
|
||||
cover = getAudioCoverFromFFMpeg(filepath)
|
||||
else:
|
||||
cover = None
|
||||
title = audio.get('title')
|
||||
artist = audio.get('artist')
|
||||
album = audio.get('album')
|
||||
lyric = audio.get('lyrics')
|
||||
if (not lyric):
|
||||
if (os.path.isfile(os.path.splitext(filepath)[0] + '.lrc')):
|
||||
with open(os.path.splitext(filepath)[0] + '.lrc', 'r', encoding='utf-8') as f:
|
||||
lyric = filterLyricLine(f.read())
|
||||
if (not checkLyricValid(lyric)):
|
||||
lyric = [None]
|
||||
else:
|
||||
lyric = [lyric]
|
||||
f.close()
|
||||
else:
|
||||
lyric = [None]
|
||||
return {
|
||||
"filepath": filepath,
|
||||
"title": title[0] if title else '',
|
||||
"artist": '、'.join(artist) if artist else '',
|
||||
"album": album[0] if album else '',
|
||||
"cover_path": extractCover({
|
||||
"filepath": filepath,
|
||||
"cover": cover,
|
||||
}, TEMP_PATH),
|
||||
"lyrics": lyric[0],
|
||||
'length': audio.info.length,
|
||||
'format_length': timeLengthFormat(audio.info.length),
|
||||
'md5': createFileMD5(filepath),
|
||||
}
|
||||
except:
|
||||
logger.error(f"get audio meta error: {filepath}")
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
def checkAudioValid(path):
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
try:
|
||||
audio = mutagen.File(path)
|
||||
if not audio:
|
||||
return False
|
||||
return True
|
||||
except:
|
||||
logger.error(f"check audio valid error: {path}")
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
def extractCover(audio_info, temp_path):
|
||||
if (not audio_info['cover']):
|
||||
return None
|
||||
path = os.path.join(temp_path + '/' + createMD5(audio_info['filepath']) + '_cover.jpg')
|
||||
with open(path, 'wb') as f:
|
||||
f.write(audio_info['cover'])
|
||||
return path
|
||||
|
||||
def findAudios(cache):
|
||||
|
||||
available_exts = [
|
||||
'mp3',
|
||||
'wav',
|
||||
'flac',
|
||||
'ogg',
|
||||
'm4a',
|
||||
]
|
||||
|
||||
files = os.listdir(AUDIO_PATH)
|
||||
if (files == []):
|
||||
return []
|
||||
|
||||
audios = []
|
||||
_map = {}
|
||||
for c in cache:
|
||||
_map[c['filepath']] = c
|
||||
for file in files:
|
||||
if (not file.endswith(tuple(available_exts))):
|
||||
continue
|
||||
path = os.path.join(AUDIO_PATH, file)
|
||||
if (not checkAudioValid(path)):
|
||||
continue
|
||||
logger.info(f"found audio: {path}")
|
||||
if (not (_map.get(path) and _map[path]['md5'] == createFileMD5(path))):
|
||||
meta = getAudioMeta(path)
|
||||
audios = audios + [meta]
|
||||
else:
|
||||
audios = audios + [_map[path]]
|
||||
|
||||
return audios
|
||||
|
||||
def getAudioCover(filepath):
|
||||
if not os.path.exists(filepath):
|
||||
return None
|
||||
try:
|
||||
audio = mutagen.File(filepath)
|
||||
if not audio:
|
||||
return None
|
||||
if (filepath.lower().endswith('mp3')):
|
||||
return audio.get('APIC:').data
|
||||
else:
|
||||
if (readFileCheckCover(filepath)):
|
||||
return getAudioCoverFromFFMpeg(filepath)
|
||||
else:
|
||||
return None
|
||||
|
||||
except:
|
||||
logger.error(f"get audio cover error: {filepath}")
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
def writeAudioCover(filepath):
|
||||
s = getAudioCover(filepath)
|
||||
path = os.path.join(TEMP_PATH + '/' + createMD5(filepath) + '_cover.jpg')
|
||||
with open(path, 'wb') as f:
|
||||
f.write(s)
|
||||
f.close()
|
||||
return path
|
||||
|
||||
def writeLocalCache(audios):
|
||||
with open(TEMP_PATH + '/meta.json', 'w', encoding='utf-8') as f:
|
||||
f.write(json.dumps({
|
||||
"file_list": os.listdir(AUDIO_PATH),
|
||||
"audios": audios
|
||||
}, ensure_ascii = False, indent = 2))
|
||||
f.close()
|
||||
|
||||
def dumpLocalCache():
|
||||
try:
|
||||
TEMP_PATH = config.read_config("common.local_music.temp_path")
|
||||
with open(TEMP_PATH + '/meta.json', 'r', encoding='utf-8') as f:
|
||||
d = json.loads(f.read())
|
||||
return d
|
||||
except:
|
||||
return {
|
||||
"file_list": [],
|
||||
"audios": []
|
||||
}
|
||||
|
||||
def initMain():
|
||||
global FFMPEG_PATH
|
||||
FFMPEG_PATH = check_ffmpeg()
|
||||
logger.debug('找到的ffmpeg命令: ' + str(FFMPEG_PATH))
|
||||
if (not os.path.exists(AUDIO_PATH)):
|
||||
os.mkdir(AUDIO_PATH)
|
||||
logger.info(f"创建本地音乐文件夹 {AUDIO_PATH}")
|
||||
if (not os.path.exists(TEMP_PATH)):
|
||||
os.mkdir(TEMP_PATH)
|
||||
logger.info(f"创建本地音乐临时文件夹 {TEMP_PATH}")
|
||||
global audios
|
||||
cache = dumpLocalCache()
|
||||
if (cache['file_list'] == os.listdir(AUDIO_PATH)):
|
||||
audios = cache['audios']
|
||||
else:
|
||||
audios = findAudios(cache['audios'])
|
||||
writeLocalCache(audios)
|
||||
for a in audios:
|
||||
map[a['filepath']] = a
|
||||
logger.info("初始化本地音乐成功")
|
||||
logger.debug(f'本地音乐列表: {audios}')
|
||||
logger.debug(f'本地音乐map: {map}')
|
||||
|
||||
async def generateAudioFileResonse(path):
|
||||
try:
|
||||
w = map[path]
|
||||
return aiohttp.web.FileResponse(w['filepath'])
|
||||
except:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': '未找到文件',
|
||||
'data': None
|
||||
}, 404
|
||||
|
||||
async def generateAudioCoverResonse(path):
|
||||
try:
|
||||
w = map[path]
|
||||
if (not os.path.exists(w['cover_path'])):
|
||||
p = writeAudioCover(w['filepath'])
|
||||
logger.debug(f"生成音乐封面文件 {w['cover_path']} 成功")
|
||||
return aiohttp.web.FileResponse(p)
|
||||
return aiohttp.web.FileResponse(w['cover_path'])
|
||||
except:
|
||||
logger.debug(traceback.format_exc())
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': '未找到封面',
|
||||
'data': None
|
||||
}, 404
|
||||
|
||||
async def generateAudioLyricResponse(path):
|
||||
try:
|
||||
w = map[path]
|
||||
return w['lyrics']
|
||||
except:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': '未找到歌词',
|
||||
'data': None
|
||||
}, 404
|
||||
|
||||
def checkLocalMusic(path):
|
||||
return {
|
||||
'file': os.path.exists(path),
|
||||
'cover': os.path.exists(map[path]['cover_path']),
|
||||
'lyric': bool(map[path]['lyrics'])
|
||||
}
|
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')
|
||||
|
@ -10,11 +10,11 @@
|
||||
from . import Httpx
|
||||
from . import config
|
||||
from . import scheduler
|
||||
from .variable import iscn
|
||||
from .log import log
|
||||
from aiohttp.web import Response
|
||||
import ujson as json
|
||||
import re
|
||||
from common.utils import createMD5
|
||||
|
||||
logger = log('lx_script')
|
||||
|
||||
@ -38,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):
|
||||
@ -69,13 +69,13 @@ 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')):
|
||||
return 'key验证失败'
|
||||
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') as f:
|
||||
with open('./lx-music-source-example.js', 'r', encoding='utf-8') as f:
|
||||
script = f.read()
|
||||
except:
|
||||
return '本地无源脚本'
|
||||
return {'code': 4, 'msg': '本地无源脚本', 'data': None}, 400
|
||||
scriptLines = script.split('\n')
|
||||
newScriptLines = []
|
||||
for line in scriptLines:
|
||||
@ -84,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,8 +66,17 @@ def filterFileName(filename):
|
||||
# 将不合法字符替换为下划线
|
||||
return re.sub(illegal_chars, '_', filename)
|
||||
|
||||
def createMD5(s: str):
|
||||
return handleCreateMD5(s.encode("utf-8")).hexdigest()
|
||||
def createMD5(s: Union[str, bytes]):
|
||||
if (isinstance(s, str)):
|
||||
s = s.encode("utf-8")
|
||||
return hashlib.md5(s).hexdigest()
|
||||
|
||||
def createFileMD5(path):
|
||||
with open(path, 'rb') as f:
|
||||
md5 = hashlib.md5()
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
md5.update(chunk)
|
||||
return md5.hexdigest()
|
||||
|
||||
def readFile(path, mode = "text"):
|
||||
try:
|
||||
@ -189,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()
|
||||
@ -50,4 +53,9 @@ iscn = True
|
||||
fake_ip = None
|
||||
aioSession = None
|
||||
qdes_lib_loaded = False
|
||||
use_cookie_pool = False
|
||||
use_cookie_pool = False
|
||||
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 })
|
250
main.py
250
main.py
@ -1,67 +1,80 @@
|
||||
#!/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
|
||||
|
||||
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)
|
||||
|
||||
from common import config
|
||||
# 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
|
||||
from common import Httpx
|
||||
from common import variable
|
||||
from common import scheduler
|
||||
from common import lx_script
|
||||
from aiohttp.web import Response
|
||||
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:
|
||||
return Response(body = json.dumps(dic, indent=2, ensure_ascii=False), content_type='application/json', status = status)
|
||||
def handleResult(dic, status=200) -> Response:
|
||||
if (not isinstance(dic, dict)):
|
||||
dic = {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': dic
|
||||
}
|
||||
return Response(body=json.dumps(dic, indent=2, ensure_ascii=False), content_type='application/json', status=status)
|
||||
|
||||
|
||||
logger = log.log("main")
|
||||
aiologger = log.log('aiohttp_web')
|
||||
|
||||
stopEvent = None
|
||||
if (sys.version_info.minor < 8 and sys.version_info.major == 3):
|
||||
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
|
||||
@ -72,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')
|
||||
@ -87,23 +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)):
|
||||
resp = Response(body = resp, content_type='text/plain', status = 200)
|
||||
elif (isinstance(resp, dict)):
|
||||
if (isinstance(resp, (str, list, dict))):
|
||||
resp = handleResult(resp)
|
||||
elif (not isinstance(resp, Response)):
|
||||
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}')
|
||||
elif (isinstance(resp, tuple) and len(resp) == 2): # flask like response
|
||||
body, status = resp
|
||||
if (isinstance(body, (str, list, dict))):
|
||||
resp = handleResult(body, status)
|
||||
else:
|
||||
resp = Response(
|
||||
body=str(body), content_type='text/plain', status=status)
|
||||
elif (not isinstance(resp, (Response, FileResponse, StreamResponse))):
|
||||
resp = Response(
|
||||
body=str(resp), content_type='text/plain', status=200)
|
||||
aiologger.info(
|
||||
f'{request.remote_addr + ("" if (request.remote == request.remote_addr) else f"|proxy@{request.remote}")} - {request.method} "{request.path}", {resp.status}')
|
||||
return resp
|
||||
except:
|
||||
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})
|
||||
|
||||
@ -114,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)
|
||||
@ -124,51 +146,118 @@ 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(source, songId, quality, query))
|
||||
return handleResult(await modules.other(method, source, songId, quality, query))
|
||||
except:
|
||||
logger.error(traceback.format_exc())
|
||||
return handleResult({'code': 4, 'msg': '内部服务器错误', 'data': None}, 500)
|
||||
|
||||
|
||||
async def handle_404(request):
|
||||
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
||||
|
||||
app = aiohttp.web.Application(middlewares=[handle_before_request])
|
||||
|
||||
async def handle_local(request):
|
||||
try:
|
||||
query = dict(request.query)
|
||||
data = query.get('q')
|
||||
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())):
|
||||
return await localMusic.generateAudioFileResonse(data['p'])
|
||||
else:
|
||||
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
||||
if (data['t'] == 'l'):
|
||||
if (data['p'] in list(localMusic.map.keys())):
|
||||
return await localMusic.generateAudioLyricResponse(data['p'])
|
||||
else:
|
||||
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
||||
if (data['t'] == 'p'):
|
||||
if (data['p'] in list(localMusic.map.keys())):
|
||||
return await localMusic.generateAudioCoverResonse(data['p'])
|
||||
else:
|
||||
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
||||
if (data['t'] == 'c'):
|
||||
if (not data['p'] in list(localMusic.map.keys())):
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': {
|
||||
'file': False,
|
||||
'cover': False,
|
||||
'lyric': False
|
||||
}
|
||||
}
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': localMusic.checkLocalMusic(data['p'])
|
||||
}
|
||||
|
||||
app = Application(middlewares=[handle_before_request])
|
||||
utils.setGlobal(app, "app")
|
||||
|
||||
# mainpage
|
||||
app.router.add_get('/', main)
|
||||
|
||||
# api
|
||||
app.router.add_get('/{method}/{source}/{songId}/{quality}', handle)
|
||||
app.router.add_get('/{method}/{source}/{songId}', handle)
|
||||
app.router.add_get('/local/{type}', handle_local)
|
||||
|
||||
if (config.read_config('common.allow_download_script')):
|
||||
app.router.add_get('/script', lx_script.generate_script_response)
|
||||
|
||||
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("重试次数已达上限,但仍有部分端口未能完成监听,已自动进行忽略")
|
||||
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):
|
||||
if (p not in ssl_ports and f'{host}_{p}' not in variable.running_ports):
|
||||
final_ports.append(p)
|
||||
else:
|
||||
final_ssl_ports.append(p)
|
||||
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)
|
||||
@ -176,15 +265,22 @@ async def run_app():
|
||||
|
||||
# 启动 HTTP 端口监听
|
||||
for port in final_ports:
|
||||
http_site = aiohttp.web.TCPSite(http_runner, host, port)
|
||||
await http_site.start()
|
||||
logger.info(f"监听 -> http://{host}:{port}")
|
||||
if (port not in variable.running_ports):
|
||||
http_site = aiohttp.web.TCPSite(
|
||||
http_runner, host, port)
|
||||
await http_site.start()
|
||||
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
|
||||
@ -193,24 +289,37 @@ async def run_app():
|
||||
|
||||
# 启动 HTTPS 端口监听
|
||||
for port in ssl_ports:
|
||||
https_site = aiohttp.web.TCPSite(https_runner, host, port, ssl_context=ssl_context)
|
||||
await https_site.start()
|
||||
logger.info(f"监听 -> https://{host}:{port}")
|
||||
|
||||
return
|
||||
if (port not in variable.running_ports):
|
||||
https_site = aiohttp.web.TCPSite(
|
||||
https_runner, host, port, ssl_context=ssl_context)
|
||||
await https_site.start()
|
||||
variable.running_ports.append(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():
|
||||
await scheduler.run()
|
||||
variable.aioSession = aiohttp.ClientSession(trust_env=True)
|
||||
localMusic.initMain()
|
||||
try:
|
||||
await run_app()
|
||||
logger.info("服务器启动成功,请按下Ctrl + C停止")
|
||||
@ -227,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")
|
||||
|
||||
@ -238,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)
|
||||
|
@ -17,10 +17,12 @@ from .lyric import lyricSearchByHash as _lyricSearch
|
||||
from .mv import getMvInfo as _getMvInfo
|
||||
from .mv import getMvPlayURL as _getMvUrl
|
||||
from .search import getSongSearchResult as _songsearch
|
||||
from . import lite_signin
|
||||
from common.exceptions import FailedException
|
||||
from common import Httpx
|
||||
from common import utils
|
||||
import asyncio
|
||||
from . import refresh_login
|
||||
|
||||
async def info(hash_):
|
||||
tasks = []
|
||||
|
168
modules/kg/lite_signin.py
Normal file
168
modules/kg/lite_signin.py
Normal file
@ -0,0 +1,168 @@
|
||||
# ----------------------------------------
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: lite_signin.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# ----------------------------------------
|
||||
# This file is part of the "lx-music-api-server" project.
|
||||
|
||||
from common.exceptions import FailedException
|
||||
from .utils import buildRequestParams, sign
|
||||
from common import Httpx, config, utils, variable, scheduler, log
|
||||
import random
|
||||
import binascii
|
||||
import time
|
||||
|
||||
logger = log.log('kugou_lite_sign_in')
|
||||
|
||||
|
||||
async def randomMixSongMid():
|
||||
'''
|
||||
通过TOP500榜单获取随机歌曲的mixsongmid
|
||||
'''
|
||||
# 声明榜单url
|
||||
rankUrl = 'http://mobilecdnbj.kugou.com/api/v3/rank/song?version=9108&ranktype=1&plat=0&pagesize=100&area_code=1&page=1&rankid=8888&with_res_tag=0&show_portrait_mv=1'
|
||||
# 请求
|
||||
res = await Httpx.AsyncRequest(rankUrl, {
|
||||
"method": 'GET'
|
||||
})
|
||||
data = res.json()
|
||||
if (data.get('status') != 1):
|
||||
raise FailedException('排行榜获取失败')
|
||||
|
||||
# 随机选择一首歌曲
|
||||
randomSong = random.choice(data['data']['info'])
|
||||
|
||||
# 因为排行榜api不会返回mixsongmid
|
||||
# 所以需要进行一次搜索接口来获取
|
||||
search_req = await Httpx.AsyncRequest(utils.encodeURI(f'https://songsearch.kugou.com/song_search_v2?' + buildRequestParams({
|
||||
"keyword": randomSong['filename'],
|
||||
"area_code": 1,
|
||||
"page": 1,
|
||||
"pagesize": 1,
|
||||
"userid": 0,
|
||||
"clientver": "",
|
||||
"platform": "WebFilter",
|
||||
"filter": 2,
|
||||
"iscorrection": 1,
|
||||
"privilege_filter": 0
|
||||
})), {
|
||||
"headers": {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.142.86 Safari/537.36",
|
||||
"Referer": "https://www.kugou.com",
|
||||
}
|
||||
})
|
||||
|
||||
body = search_req.json()
|
||||
|
||||
if (body.get('status') != 1):
|
||||
raise FailedException('歌曲搜索失败')
|
||||
if (body['data']['total'] == 0 or body['data']['lists'] == []):
|
||||
raise FailedException('歌曲搜索失败')
|
||||
|
||||
return body['data']['lists'][0]['MixSongID']
|
||||
|
||||
|
||||
async def do_account_signin(user_info):
|
||||
'''
|
||||
签到主函数,传入userinfo,响应None就是成功,报错即为不成功
|
||||
'''
|
||||
# 检查用户配置文件,获取mixsongmid
|
||||
mixid = user_info['lite_sign_in']['mixsongmid']['value']
|
||||
if (mixid == 'auto'):
|
||||
mixid = await randomMixSongMid()
|
||||
|
||||
# 声明变量
|
||||
headers = {
|
||||
'User-Agent': f'Android712-AndroidPhone-{config.read_config("module.kg.client.clientver")}-18-0-NetMusic-wifi',
|
||||
'KG-THash': '3e5ec6b',
|
||||
'KG-Rec': '1',
|
||||
'KG-RC': '1',
|
||||
"x-router": "youth.kugou.com"
|
||||
}
|
||||
body = """{"mixsongid":__id__}""".replace("__id__", str(mixid))
|
||||
|
||||
# params = "userid={}&token={}&appid=3116&clientver=10518&clienttime={}&mid={}&uuid={}&dfid=-".format(read_config("common.kg.userid"), read_config("common.kg.token"), int(time.time()), read_config("common.kg.mid"), str(binascii.hexlify(random.randbytes(16)), encoding = "utf-8"))
|
||||
params = {
|
||||
"userid": user_info['userid'],
|
||||
"token": user_info['token'],
|
||||
"appid": 3116,
|
||||
"clientver": config.read_config('module.kg.client.clientver'),
|
||||
"clienttime": int(time.time()),
|
||||
"mid": user_info['mid'],
|
||||
"uuid": str(binascii.hexlify(random.randbytes(16)), encoding="utf-8"),
|
||||
"dfid": "-"
|
||||
}
|
||||
|
||||
params['signature'] = sign(
|
||||
params, body, config.read_config('module.kg.client.signatureKey'))
|
||||
|
||||
# 发送请求
|
||||
req = await Httpx.AsyncRequest(f"https://gateway.kugou.com/v2/report/listen_song?" +
|
||||
buildRequestParams(params), {
|
||||
"method": "POST",
|
||||
"body": body,
|
||||
"headers": headers
|
||||
})
|
||||
req = req.json()
|
||||
|
||||
if req['status'] == 1:
|
||||
return
|
||||
else:
|
||||
raise FailedException(req['error_msg'])
|
||||
|
||||
|
||||
def task_handler():
|
||||
# not lite client configure
|
||||
if (int(config.read_config('module.kg.client.appid')) != 3116):
|
||||
return
|
||||
|
||||
# no user
|
||||
if ((not variable.use_cookie_pool) and (not config.read_config('module.kg.user.token'))):
|
||||
return
|
||||
|
||||
# devide cookiepool
|
||||
if (variable.use_cookie_pool):
|
||||
pool = config.read_config('module.cookiepool.kg')
|
||||
for user in pool:
|
||||
index = pool.index(user)
|
||||
if (user.get('lite_sign_in') is None):
|
||||
user['lite_sign_in'] = {
|
||||
"desc": "是否启用概念版自动签到,仅在appid=3116时运行",
|
||||
"enable": False,
|
||||
"interval": 86400,
|
||||
"mixsongmid": {
|
||||
"desc": "mix_songmid的获取方式, 默认auto, 可以改成一个数字手动",
|
||||
"value": "auto"
|
||||
}
|
||||
}
|
||||
pool[index] = user
|
||||
config.write_config('module.cookiepool.kg', pool)
|
||||
logger.info(f'用户池用户(index = {index})配置缺失lite_sign_in字段,已自动写入')
|
||||
|
||||
# refresh
|
||||
pool = config.read_config('module.cookiepool.kg')
|
||||
# add signin schedule task
|
||||
for user in pool:
|
||||
if (user.get('lite_sign_in').get('enable')):
|
||||
scheduler.append(f'kugou_lite_sign_in_{user["userid"]}', do_account_signin, user['lite_sign_in']['interval'], {'user_info': user})
|
||||
else:
|
||||
user_info = config.read_config('module.kg.user')
|
||||
if (user_info.get('lite_sign_in') is None):
|
||||
user_info['lite_sign_in'] = {
|
||||
"desc": "是否启用概念版自动签到,仅在appid=3116时运行",
|
||||
"enable": False,
|
||||
"interval": 86400,
|
||||
"mixsongmid": {
|
||||
"desc": "mix_songmid的获取方式, 默认auto, 可以改成一个数字手动",
|
||||
"value": "auto"
|
||||
}
|
||||
}
|
||||
config.write_config('module.kg.user', user_info)
|
||||
logger.info('用户配置缺失lite_sign_in字段,已自动写入')
|
||||
|
||||
if (user_info.get('lite_sign_in').get('enable')):
|
||||
scheduler.append(f'kugou_lite_sign_in', do_account_signin, user_info['lite_sign_in']['interval'], {'user_info': user_info})
|
||||
|
||||
task_handler()
|
@ -46,7 +46,7 @@ async def url(songId, quality):
|
||||
'album_audio_id': albumaudioid,
|
||||
'behavior': 'play',
|
||||
'clienttime': int(time.time()),
|
||||
'pid': 2,
|
||||
'pid': tools.pid,
|
||||
'key': getKey(thash, user_info),
|
||||
'dfid': '-',
|
||||
'pidversion': 3001
|
||||
|
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()
|
@ -66,6 +66,7 @@ async def getSongSearchResult(query, page = 1, size = 20):
|
||||
"keyword": query,
|
||||
"page": page,
|
||||
"pagesize": size,
|
||||
"area_code": 1,
|
||||
"userid": 0,
|
||||
"clientver": "",
|
||||
"platform": "WebFilter",
|
||||
|
@ -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,8 @@ 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',
|
||||
'320k': 'hash_320',
|
||||
@ -43,19 +47,26 @@ def buildSignatureParams(dictionary, body = ""):
|
||||
joined_str = ''.join([f'{k}={v}' for k, v in dictionary.items()])
|
||||
return joined_str + body
|
||||
|
||||
def buildRequestParams(dictionary):
|
||||
def buildRequestParams(dictionary: dict):
|
||||
joined_str = '&'.join([f'{k}={v}' for k, v in dictionary.items()])
|
||||
return joined_str
|
||||
|
||||
def sign(params, body = "", signkey = tools["signkey"]):
|
||||
if (isinstance(body, dict)):
|
||||
body = json.dumps(body)
|
||||
params = utils.sortDict(params)
|
||||
params = buildSignatureParams(params, body)
|
||||
return utils.createMD5(signkey + params + signkey)
|
||||
|
||||
async def signRequest(url, params, options, signkey = tools["signkey"]):
|
||||
params['signature'] = sign(params, options.get("body") if options.get("body") else (options.get("data") if options.get("data") else ""), 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': {
|
||||
@ -80,13 +84,14 @@ async def url(songId, quality):
|
||||
url = ''
|
||||
bitrate = 1
|
||||
if (des_info["response_type"] == 'json'):
|
||||
body = req.json()
|
||||
url = req.json()
|
||||
for p in des_info['url_json_path'].split('.'):
|
||||
url = body.get(p)
|
||||
url = url.get(p)
|
||||
if (url == None):
|
||||
raise FailedException('failed')
|
||||
bitrate = req.json()
|
||||
for p in des_info['bitrate_json_path'].split('.'):
|
||||
bitrate = body.get(p)
|
||||
bitrate = bitrate.get(p)
|
||||
if (bitrate == None):
|
||||
raise FailedException('failed')
|
||||
elif (des_info['response_type'] == 'text'):
|
||||
@ -109,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"
|
||||
}
|
||||
}
|
1896
poetry.lock
generated
1896
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,24 +1,29 @@
|
||||
[tool.poetry]
|
||||
name = "lx-music-api-server-python"
|
||||
version = "2.0.0.beta-4"
|
||||
name = "lx_music_api_server"
|
||||
version = "2.0.0"
|
||||
description = "一个适配 LX Music 的 API 后端实现"
|
||||
authors = ["helloplhm-qwq"]
|
||||
authors = ["helloplhm-qwq", "lerdb", "Folltoshe"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
package-mode = false
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
aiohttp = "^3.9.1"
|
||||
pycryptodome = "^3.20.0"
|
||||
ujson = "^5.9.0"
|
||||
requests = "^2.31.0"
|
||||
colorlog = "^6.8.0"
|
||||
Pygments = "^2.17.2"
|
||||
xmltodict = "^0.13.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
Flask = "^3.0.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"
|
@ -5,3 +5,10 @@ requests
|
||||
colorlog
|
||||
pygments
|
||||
xmltodict
|
||||
toml
|
||||
mutagen
|
||||
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