mirror of
https://github.com/MeoProject/lx-music-api-server.git
synced 2025-05-23 19:17:41 +08:00
init
This commit is contained in:
commit
24836934e0
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Git Ignore for lx-music-api-server
|
||||||
|
|
||||||
|
# python
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.pyz
|
||||||
|
*/__pycache__/*
|
||||||
|
__pycache__/
|
||||||
|
*.egg-info
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# nodejs
|
||||||
|
*.node
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# npm
|
||||||
|
*.tgz
|
||||||
|
*.tar.gz
|
||||||
|
*.zip
|
||||||
|
*.tar
|
||||||
|
|
||||||
|
# project
|
||||||
|
cache.db
|
||||||
|
data.db
|
||||||
|
logs
|
||||||
|
config.json
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.history
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 LX Music
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
67
README.md
Normal file
67
README.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# lx-music-api-server-python
|
||||||
|
|
||||||
|
LX Music非官方测试接口服务器的Python实现(需自行写源)
|
||||||
|
|
||||||
|
## 还在开发中
|
||||||
|
**主开发是高一住校学生,只有周末有时间回复,也欢迎所有人来贡献代码,我们在这里万分感谢**
|
||||||
|
**由于项目不够完善,更新后可能会导致config模块报错,之后有时间再修,目前的解决方法是删掉config.json然后让项目重新创建,账号配置自己备份吧**
|
||||||
|
|
||||||
|
## 使用此项目导致的封号等情况与开发者无关
|
||||||
|
|
||||||
|
## 部署方法
|
||||||
|
环境要求:Python 3.8+
|
||||||
|
没有其他限制,能用Python理论上就能跑起来
|
||||||
|
|
||||||
|
测试版本部署,linux命令如果为python3请自行替换:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/lxmusics/lx-music-api-server-python.git # clone本项目
|
||||||
|
cd python # 后期可能会有nodejs版
|
||||||
|
python -m pip install -r ./requirements.txt # 安装依赖
|
||||||
|
python main.py # 启动服务
|
||||||
|
```
|
||||||
|
|
||||||
|
对于release的部署和上方类似,这里不再赘述
|
||||||
|
|
||||||
|
### 返回码说明
|
||||||
|
|
||||||
|
接口返回值中`body.code`字段值中的代码含义
|
||||||
|
|
||||||
|
| 内容 | 含义 |
|
||||||
|
|------|--------------------------------------|
|
||||||
|
| 0 |成功 |
|
||||||
|
| 1 |IP被封禁 |
|
||||||
|
| 2 |获取失败 |
|
||||||
|
| 4 |服务器内部错误(对应statuscode 500) |
|
||||||
|
| 5 |请求过于频繁 |
|
||||||
|
| 6 |参数错误 |
|
||||||
|
|
||||||
|
接口返回的`statuscode`对应的代码含义
|
||||||
|
|
||||||
|
| 内容 | 含义 |
|
||||||
|
|------|-----------------------------------|
|
||||||
|
| 200 |成功 |
|
||||||
|
| 403 |IP被封禁 |
|
||||||
|
| 400 |参数错误 |
|
||||||
|
| 429 |请求过于频繁 |
|
||||||
|
| 500 |服务器内部错误(对应body.code 4) |
|
||||||
|
|
||||||
|
### 项目协议
|
||||||
|
|
||||||
|
本项目基于 [MIT](https://github.com/lxmusics/lx-music-api-server/blob/main/LICENSE) 许可证发行,以下协议是对于MIT原协议的补充,如有冲突,以以下协议为准。
|
||||||
|
|
||||||
|
词语约定:本协议中的“本项目”指本音源项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。
|
||||||
|
|
||||||
|
1. 本项目的数据来源原理是从各官方音乐平台的公开服务器中拉取数据,经过对数据简单地筛选与合并后进行展示,因此本项目不对数据的准确性负责。
|
||||||
|
2. 使用本项目的过程中可能会产生版权数据,对于这些版权数据,本项目不拥有它们的所有权,为了避免造成侵权,使用者务必在**24小时**内清除使用本项目的过程中所产生的版权数据。
|
||||||
|
3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。
|
||||||
|
4. 本项目完全免费,且开源发布于 GitHub 面向全世界人用作对技术的学习交流,本项目不对项目内的技术可能存在违反当地法律法规的行为作保证,**禁止在违反当地法律法规的情况下使用本项目**,对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。
|
||||||
|
|
||||||
|
若你使用了本项目,将代表你接受以上协议。
|
||||||
|
|
||||||
|
音乐平台不易,请尊重版权,支持正版。
|
||||||
|
本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。
|
||||||
|
若对此有疑问请 mail to:
|
||||||
|
helloplhm-qwq+outlook.com
|
||||||
|
folltoshe+foxmail.com
|
||||||
|
(请将`+`替换成`@`)
|
83
apis/__init__.py
Normal file
83
apis/__init__.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - 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.
|
||||||
|
# Do not edit except you konw what you are doing.
|
||||||
|
|
||||||
|
from common.utils import require
|
||||||
|
from common.exceptions import FailedException
|
||||||
|
from common import log
|
||||||
|
from common import config
|
||||||
|
from . import kw
|
||||||
|
from . import mg
|
||||||
|
from . import kg
|
||||||
|
from . import tx
|
||||||
|
from . import wy
|
||||||
|
import traceback
|
||||||
|
import time
|
||||||
|
|
||||||
|
logger = log.log('api_handler')
|
||||||
|
|
||||||
|
sourceExpirationTime = {
|
||||||
|
'tx': {
|
||||||
|
"expire": True,
|
||||||
|
"time": 15 * 60 * 60, # 15 hours
|
||||||
|
},
|
||||||
|
'kg': {
|
||||||
|
"expire": True,
|
||||||
|
"time": 15 * 60 * 60, # 15 hours
|
||||||
|
},
|
||||||
|
'kw': {
|
||||||
|
"expire": True,
|
||||||
|
"time": 30 * 60 # 30 minutes
|
||||||
|
},
|
||||||
|
'wy': {
|
||||||
|
"expire": True,
|
||||||
|
"time": 10 * 60, # 10 minutes
|
||||||
|
},
|
||||||
|
'mg': {
|
||||||
|
# no expiration
|
||||||
|
"expire": False,
|
||||||
|
"time": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def SongURL(source, songId, quality):
|
||||||
|
try:
|
||||||
|
c = config.getCache('urls', f'{source}_{songId}_{quality}')
|
||||||
|
if c:
|
||||||
|
logger.debug(f'使用缓存的{source}_{songId}_{quality}数据,URL:{c["url"]}')
|
||||||
|
return {
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'success',
|
||||||
|
'data': c['url'],
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
func = require('apis.' + source).url
|
||||||
|
try:
|
||||||
|
url = await func(songId, quality)
|
||||||
|
logger.debug(f'获取{source}_{songId}_{quality}成功,URL:{url}')
|
||||||
|
config.updateCache('urls', f'{source}_{songId}_{quality}', {
|
||||||
|
"expire": sourceExpirationTime[source]['expire'],
|
||||||
|
"time": sourceExpirationTime[source]['time'] + int(time.time()),
|
||||||
|
"url": url,
|
||||||
|
})
|
||||||
|
logger.debug(f'缓存已更新:{source}_{songId}_{quality}, URL:{url}, expire: {sourceExpirationTime[source]["time"] + int(time.time())}')
|
||||||
|
return {
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'success',
|
||||||
|
'data': url,
|
||||||
|
}
|
||||||
|
except FailedException as e:
|
||||||
|
return {
|
||||||
|
'code': 2,
|
||||||
|
'msg': e.args[0],
|
||||||
|
'data': None,
|
||||||
|
}
|
122
apis/kg/__init__.py
Normal file
122
apis/kg/__init__.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: __init__.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
from common.exceptions import FailedException
|
||||||
|
from common import utils
|
||||||
|
from common import config
|
||||||
|
from common import Httpx
|
||||||
|
import time
|
||||||
|
|
||||||
|
jsobject = utils.jsobject
|
||||||
|
|
||||||
|
def buildsignparams(dictionary, body = ""):
|
||||||
|
joined_str = ''.join([f'{k}={v}' for k, v in dictionary.items()])
|
||||||
|
return joined_str + body
|
||||||
|
|
||||||
|
def buildrequestparams(dictionary):
|
||||||
|
joined_str = '&'.join([f'{k}={v}' for k, v in dictionary.items()])
|
||||||
|
return joined_str
|
||||||
|
|
||||||
|
tools = jsobject({
|
||||||
|
"signkey": config.read_config("module.kg.client.signatureKey"),
|
||||||
|
"pidversec": config.read_config("module.kg.client.pidversionsecret"),
|
||||||
|
"clientver": config.read_config("module.kg.client.clientver"),
|
||||||
|
"x-router": config.read_config("module.kg.tracker.x-router"),
|
||||||
|
"url": config.read_config("module.kg.tracker.host") + config.read_config("module.kg.tracker.path"),
|
||||||
|
"version": config.read_config("module.kg.tracker.version"),
|
||||||
|
"userid": config.read_config("module.kg.user.userid"),
|
||||||
|
"token": config.read_config("module.kg.user.token"),
|
||||||
|
"mid": config.read_config("module.kg.user.mid"),
|
||||||
|
"extra_params": config.read_config("module.kg.tracker.extra_params"),
|
||||||
|
"appid": config.read_config("module.kg.client.appid"),
|
||||||
|
'qualityHashMap': {
|
||||||
|
'128k': '128hash',
|
||||||
|
'320k': '320hash',
|
||||||
|
'flac': 'sqhash',
|
||||||
|
'flac24bit': 'highhash',
|
||||||
|
},
|
||||||
|
'qualityMap': {
|
||||||
|
'128k': '128',
|
||||||
|
'320k': '320',
|
||||||
|
'flac': 'flac',
|
||||||
|
'flac24bit': 'high',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def sign(params, body = ""):
|
||||||
|
params = utils.sort_dict(params)
|
||||||
|
params = buildsignparams(params, body)
|
||||||
|
return utils.md5(tools["signkey"] + params + tools["signkey"])
|
||||||
|
|
||||||
|
def signRequest(url, params, options):
|
||||||
|
params = utils.merge_dict(tools["extra_params"], params)
|
||||||
|
url = url + "?" + buildrequestparams(params) + "&signature=" + sign(params, options.get("body") if options.get("body") else (options.get("data") if options.get("data") else ""))
|
||||||
|
return Httpx.request(url, options)
|
||||||
|
|
||||||
|
def getKey(hash_):
|
||||||
|
# print(hash_ + tools.pidversec + tools.appid + tools.mid + tools.userid)
|
||||||
|
return utils.md5(hash_.lower() + tools.pidversec + tools.appid + tools.mid + tools.userid)
|
||||||
|
|
||||||
|
async def url(songId, quality):
|
||||||
|
inforeq = Httpx.request("https://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash=" + songId)
|
||||||
|
body_ = jsobject(inforeq.json())
|
||||||
|
thash = body_.extra[tools.qualityHashMap[quality]]
|
||||||
|
albumid = body_.albumid
|
||||||
|
albumaudioid = body_.album_audio_id
|
||||||
|
if (not thash):
|
||||||
|
raise FailedException('获取歌曲信息失败')
|
||||||
|
if (not albumid):
|
||||||
|
albumid = 0
|
||||||
|
if (not albumaudioid):
|
||||||
|
albumaudioid = 0
|
||||||
|
params = {
|
||||||
|
'album_id': albumid,
|
||||||
|
'userid': tools.userid,
|
||||||
|
'area_code': 1,
|
||||||
|
'hash': thash.lower(),
|
||||||
|
'module': '',
|
||||||
|
'mid': tools.mid,
|
||||||
|
'appid': tools.appid,
|
||||||
|
'ssa_flag': 'is_fromtrack',
|
||||||
|
'clientver': tools.clientver,
|
||||||
|
'open_time': time.strftime("%Y%m%d"),
|
||||||
|
'vipType': 6,
|
||||||
|
'ptype': 0,
|
||||||
|
'token': tools.token,
|
||||||
|
'auth': '',
|
||||||
|
'mtype': 0,
|
||||||
|
'album_audio_id': albumaudioid,
|
||||||
|
'behavior': 'play',
|
||||||
|
'clienttime': int(time.time()),
|
||||||
|
'pid': 2,
|
||||||
|
'key': getKey(thash),
|
||||||
|
'dfid': '-',
|
||||||
|
'pidversion': 3001
|
||||||
|
}
|
||||||
|
if (tools.version == 'v5'):
|
||||||
|
params['quality'] = tools.qualityMap[quality]
|
||||||
|
# print(params.quality)
|
||||||
|
headers = jsobject({
|
||||||
|
'User-Agent': 'Android712-AndroidPhone-8983-18-0-NetMusic-wifi',
|
||||||
|
'KG-THash': '3e5ec6b',
|
||||||
|
'KG-Rec': '1',
|
||||||
|
'KG-RC': '1',
|
||||||
|
})
|
||||||
|
if (tools['x-router']['enable']):
|
||||||
|
headers['x-router'] = tools['x-router']['value']
|
||||||
|
req = signRequest(tools.url, params, {'headers': headers})
|
||||||
|
body = jsobject(req.json())
|
||||||
|
|
||||||
|
if body.status == 3:
|
||||||
|
raise FailedException('该歌曲在酷狗没有版权,请换源播放')
|
||||||
|
elif body.status == 2:
|
||||||
|
raise FailedException('链接获取失败,请检查账号信息是否过期或本歌曲为数字专辑')
|
||||||
|
elif body.status != 1:
|
||||||
|
raise FailedException('链接获取失败,可能是数字专辑或者api失效')
|
||||||
|
return body.url[0]
|
46
apis/kw/__init__.py
Normal file
46
apis/kw/__init__.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: __init__.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
from common import Httpx
|
||||||
|
from common.exceptions import FailedException
|
||||||
|
|
||||||
|
tools = {
|
||||||
|
'qualityMap': {
|
||||||
|
'128k': '128kmp3',
|
||||||
|
'320k': '320kmp3',
|
||||||
|
'flac': '2000kflac',
|
||||||
|
},
|
||||||
|
'extMap': {
|
||||||
|
'128k': 'mp3',
|
||||||
|
'320k': 'mp3',
|
||||||
|
'flac': 'flac',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async def url(songId, quality):
|
||||||
|
target_url = f'''https://bd-api.kuwo.cn/api/service/music/downloadInfo/{songId}?isMv=0&format={tools['extMap'][quality]}&br={tools['qualityMap'][quality]}&level='''
|
||||||
|
req = Httpx.request(target_url, {
|
||||||
|
'method': 'GET',
|
||||||
|
'headers': {
|
||||||
|
'User-Agent': 'okhttp/3.10.0',
|
||||||
|
'channel': 'qq',
|
||||||
|
'plat': 'ar',
|
||||||
|
'net': 'wifi',
|
||||||
|
'ver': '3.1.2',
|
||||||
|
'uid': '',
|
||||||
|
'devId': '0',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
body = req.json()
|
||||||
|
if (body['code'] != 200) or (body['data']['audioInfo']['bitrate'] == 1):
|
||||||
|
raise FailedException('failed')
|
||||||
|
return body['data']['url'].split('?')[0]
|
||||||
|
except:
|
||||||
|
raise FailedException('failed')
|
50
apis/mg/__init__.py
Normal file
50
apis/mg/__init__.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: __init__.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
from common import Httpx
|
||||||
|
from common import config
|
||||||
|
from common.exceptions import FailedException
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
'token': config.read_config('module.mg.user.token'),
|
||||||
|
'aversionid': config.read_config('module.mg.user.aversionid'),
|
||||||
|
'useragent': config.read_config('module.mg.user.useragent'),
|
||||||
|
'osversion': config.read_config('module.mg.user.osversion'),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def url(songId, quality):
|
||||||
|
req = Httpx.request(tools['url'].replace('__quality__', tools['qualityMap'][quality]).replace('__songId__', songId), {
|
||||||
|
'method': 'GET',
|
||||||
|
'headers': {
|
||||||
|
'User-Agent': tools['useragent'],
|
||||||
|
'aversionid': tools['aversionid'],
|
||||||
|
'token': tools['token'],
|
||||||
|
'channel': '0146832',
|
||||||
|
'language': 'Chinese',
|
||||||
|
'ua': 'Android_migu',
|
||||||
|
'mode': 'android',
|
||||||
|
'os': 'Android ' + tools['osversion'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
body = req.json()
|
||||||
|
# print((body['data']['url']))
|
||||||
|
if ((not int(body['code']) == 0) or ( not body['data']['url'])):
|
||||||
|
raise FailedException('failed')
|
||||||
|
return body['data']['url'].split('?')[0]
|
||||||
|
except:
|
||||||
|
raise FailedException('failed')
|
104
apis/tx/QMWSign.py
Normal file
104
apis/tx/QMWSign.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: QMWSign.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
from common.utils import md5 as _md5
|
||||||
|
import re as _re
|
||||||
|
|
||||||
|
def v(b):
|
||||||
|
res = []
|
||||||
|
p = [21, 4, 9, 26, 16, 20, 27, 30]
|
||||||
|
for x in p:
|
||||||
|
res.append(b[x])
|
||||||
|
return ''.join(res)
|
||||||
|
|
||||||
|
def c(b):
|
||||||
|
res = []
|
||||||
|
p = [18, 11, 3, 2, 1, 7, 6, 25]
|
||||||
|
for x in p:
|
||||||
|
res.append(b[x])
|
||||||
|
return ''.join(res)
|
||||||
|
|
||||||
|
def y(a, b, c):
|
||||||
|
e = []
|
||||||
|
r25 = a >> 2
|
||||||
|
if b is not None and c is not None:
|
||||||
|
r26 = a & 3
|
||||||
|
r26_2 = r26 << 4
|
||||||
|
r26_3 = b >> 4
|
||||||
|
r26_4 = r26_2 | r26_3
|
||||||
|
r27 = b & 15
|
||||||
|
r27_2 = r27 << 2
|
||||||
|
r27_3 = r27_2 | (c >> 6)
|
||||||
|
r28 = c & 63
|
||||||
|
e.append(r25)
|
||||||
|
e.append(r26_4)
|
||||||
|
e.append(r27_3)
|
||||||
|
e.append(r28)
|
||||||
|
else:
|
||||||
|
r10 = a >> 2
|
||||||
|
r11 = a & 3
|
||||||
|
r11_2 = r11 << 4
|
||||||
|
e.append(r10)
|
||||||
|
e.append(r11_2)
|
||||||
|
return e
|
||||||
|
|
||||||
|
def n(ls):
|
||||||
|
e = []
|
||||||
|
for i in range(0, len(ls), 3):
|
||||||
|
if i < len(ls) - 2:
|
||||||
|
e += y(ls[i], ls[i + 1], ls[i + 2])
|
||||||
|
else:
|
||||||
|
e += y(ls[i], None, None)
|
||||||
|
res = []
|
||||||
|
b64all = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
|
||||||
|
for i in e:
|
||||||
|
res.append(b64all[i])
|
||||||
|
return ''.join(res)
|
||||||
|
|
||||||
|
def t(b):
|
||||||
|
zd = {
|
||||||
|
"0": 0,
|
||||||
|
"1": 1,
|
||||||
|
"2": 2,
|
||||||
|
"3": 3,
|
||||||
|
"4": 4,
|
||||||
|
"5": 5,
|
||||||
|
"6": 6,
|
||||||
|
"7": 7,
|
||||||
|
"8": 8,
|
||||||
|
"9": 9,
|
||||||
|
"A": 10,
|
||||||
|
"B": 11,
|
||||||
|
"C": 12,
|
||||||
|
"D": 13,
|
||||||
|
"E": 14,
|
||||||
|
"F": 15
|
||||||
|
}
|
||||||
|
ol = [212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6]
|
||||||
|
res = []
|
||||||
|
j = 0
|
||||||
|
for i in range(0, len(b), 2):
|
||||||
|
one = zd[b[i]]
|
||||||
|
two = zd[b[i + 1]]
|
||||||
|
r = one * 16 ^ two
|
||||||
|
res.append(r ^ ol[j])
|
||||||
|
j += 1
|
||||||
|
return res
|
||||||
|
|
||||||
|
def sign(params):
|
||||||
|
md5Str = _md5(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
|
84
apis/tx/__init__.py
Normal file
84
apis/tx/__init__.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: __init__.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
from common.exceptions import FailedException
|
||||||
|
from common import Httpx
|
||||||
|
from common import utils
|
||||||
|
from common import config
|
||||||
|
from .QMWSign import sign
|
||||||
|
import ujson as json
|
||||||
|
|
||||||
|
jsobject = utils.jsobject
|
||||||
|
|
||||||
|
tools = jsobject({
|
||||||
|
"fileInfo": {
|
||||||
|
"128k": {
|
||||||
|
'e': '.mp3',
|
||||||
|
'h': 'M500',
|
||||||
|
},
|
||||||
|
'320k': {
|
||||||
|
"e": '.mp3',
|
||||||
|
'h': 'M800',
|
||||||
|
},
|
||||||
|
'flac': {
|
||||||
|
"e": '.flac',
|
||||||
|
'h': 'F000',
|
||||||
|
},
|
||||||
|
'flac24bit': {
|
||||||
|
"e": '.flac',
|
||||||
|
'h': 'RS01',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"key": config.read_config("module.tx.user.qqmusic_key"),
|
||||||
|
"loginuin": config.read_config("module.tx.user.uin"),
|
||||||
|
"guid": config.read_config("module.tx.vkeyserver.guid"),
|
||||||
|
"uin": config.read_config("module.tx.vkeyserver.uin"),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def signRequest(data):
|
||||||
|
data = json.dumps(data)
|
||||||
|
s = sign(data)
|
||||||
|
headers = {}
|
||||||
|
return Httpx.request('https://u.y.qq.com/cgi-bin/musics.fcg?format=json&sign=' + s, {
|
||||||
|
'method': 'POST',
|
||||||
|
'body': data,
|
||||||
|
'headers': headers
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def url(songId, quality):
|
||||||
|
requestBody = {
|
||||||
|
'req_0': {
|
||||||
|
'module': 'vkey.GetVkeyServer',
|
||||||
|
'method': 'CgiGetVkey',
|
||||||
|
'param': {
|
||||||
|
'filename': [f"{tools.fileInfo[quality]['h']}{songId}{tools.fileInfo[quality]['e']}"],
|
||||||
|
'guid': tools.guid,
|
||||||
|
'songmid': [songId],
|
||||||
|
'songtype': [0],
|
||||||
|
'uin': tools.uin,
|
||||||
|
'loginflag': 1,
|
||||||
|
'platform': '20',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'comm': {
|
||||||
|
"qq": tools.loginuin,
|
||||||
|
"authst": tools.key,
|
||||||
|
"ct": "26",
|
||||||
|
"cv": "2010101",
|
||||||
|
"v": "2010101"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req = signRequest(requestBody)
|
||||||
|
body = jsobject(req.json())
|
||||||
|
# js const { purl } = data.req_0.data.midurlinfo[0]
|
||||||
|
if (not body.req_0.data.midurlinfo[0]['purl']):
|
||||||
|
raise FailedException('failed')
|
||||||
|
return 'http://ws.stream.qqmusic.qq.com/' + body.req_0.data.midurlinfo[0]['purl']
|
46
apis/wy/__init__.py
Normal file
46
apis/wy/__init__.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: __init__.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
from common import Httpx
|
||||||
|
from common import config
|
||||||
|
from common.exceptions import FailedException
|
||||||
|
from .encrypt import eapiEncrypt
|
||||||
|
import ujson as json
|
||||||
|
|
||||||
|
tools = {
|
||||||
|
'qualityMap': {
|
||||||
|
'128k': 'standard',
|
||||||
|
'320k': 'exhigh',
|
||||||
|
'flac': 'lossless',
|
||||||
|
'flac24bit': 'hires',
|
||||||
|
"jyeffect": "jyeffect",
|
||||||
|
"jysky": "jysky",
|
||||||
|
"jymaster": "jymaster",
|
||||||
|
},
|
||||||
|
'cookie': config.read_config('module.wy.user.cookie'),
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = Httpx.request(requestUrl, {
|
||||||
|
'method': 'POST',
|
||||||
|
'headers': {
|
||||||
|
'Cookie': tools['cookie'],
|
||||||
|
},
|
||||||
|
'form': eapiEncrypt(path, json.dumps({
|
||||||
|
"ids": json.dumps([songId]),
|
||||||
|
"level": tools["qualityMap"][quality],
|
||||||
|
"encodeType": "flac",
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
body = json.loads(req.text)
|
||||||
|
if (not body.get("data") or (not body.get("data")) or (not body.get("data")[0].get("url"))):
|
||||||
|
raise FailedException("failed")
|
||||||
|
return body["data"][0]["url"].split("?")[0]
|
87
apis/wy/encrypt.py
Normal file
87
apis/wy/encrypt.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: eapi.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
from json import dumps
|
||||||
|
from os import urandom
|
||||||
|
from base64 import b64encode
|
||||||
|
from binascii import hexlify
|
||||||
|
from hashlib import md5
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
__all__ = ["weEncrypt", "linuxEncrypt", "eEncrypt", "MD5"]
|
||||||
|
|
||||||
|
MODULUS = (
|
||||||
|
"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7"
|
||||||
|
"b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280"
|
||||||
|
"104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932"
|
||||||
|
"575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b"
|
||||||
|
"3ece0462db0a22b8e7"
|
||||||
|
)
|
||||||
|
PUBKEY = "010001"
|
||||||
|
NONCE = b"0CoJUm6Qyw8W8jud"
|
||||||
|
LINUXKEY = b"rFgB&h#%2?^eDg:Q"
|
||||||
|
EAPIKEY = b'e82ckenh8dichen8'
|
||||||
|
|
||||||
|
|
||||||
|
def MD5(value):
|
||||||
|
m = md5()
|
||||||
|
m.update(value.encode())
|
||||||
|
return m.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def weEncrypt(text):
|
||||||
|
"""
|
||||||
|
引用自 https://github.com/darknessomi/musicbox/blob/master/NEMbox/encrypt.py#L40
|
||||||
|
"""
|
||||||
|
data = dumps(text).encode("utf-8")
|
||||||
|
secret = create_key(16)
|
||||||
|
method = {"iv": True, "base64": True}
|
||||||
|
params = aes(aes(data, NONCE, method), secret, method)
|
||||||
|
encseckey = rsa(secret, PUBKEY, MODULUS)
|
||||||
|
return {"params": params, "encSecKey": encseckey}
|
||||||
|
|
||||||
|
|
||||||
|
def linuxEncrypt(text):
|
||||||
|
"""
|
||||||
|
参考自 https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js#L28
|
||||||
|
"""
|
||||||
|
text = str(text).encode()
|
||||||
|
data = aes(text, LINUXKEY)
|
||||||
|
return {"eparams": data.decode()}
|
||||||
|
|
||||||
|
|
||||||
|
def eapiEncrypt(url, text):
|
||||||
|
text = str(text)
|
||||||
|
digest = MD5("nobody{}use{}md5forencrypt".format(url, text))
|
||||||
|
data = "{}-36cd479b6b5-{}-36cd479b6b5-{}".format(url, text, digest)
|
||||||
|
return {"params": aes(data.encode(), EAPIKEY).decode("utf-8")}
|
||||||
|
|
||||||
|
|
||||||
|
def aes(text, key, method={}):
|
||||||
|
pad = 16 - len(text) % 16
|
||||||
|
text = text + bytearray([pad] * pad)
|
||||||
|
if "iv" in method:
|
||||||
|
encryptor = AES.new(key, AES.MODE_CBC, b"0102030405060708")
|
||||||
|
else:
|
||||||
|
encryptor = AES.new(key, AES.MODE_ECB)
|
||||||
|
ciphertext = encryptor.encrypt(text)
|
||||||
|
if "base64" in method:
|
||||||
|
return b64encode(ciphertext)
|
||||||
|
return hexlify(ciphertext).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def rsa(text, pubkey, modulus):
|
||||||
|
text = text[::-1]
|
||||||
|
rs = pow(int(hexlify(text), 16),
|
||||||
|
int(pubkey, 16), int(modulus, 16))
|
||||||
|
return format(rs, "x").zfill(256)
|
||||||
|
|
||||||
|
|
||||||
|
def create_key(size):
|
||||||
|
return hexlify(urandom(size))[:16]
|
41
common/EncryptUtils.py
Normal file
41
common/EncryptUtils.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: EncryptUtils.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES, DES
|
||||||
|
import binascii
|
||||||
|
import base64
|
||||||
|
|
||||||
|
def AESEncrypt(plainText, key, iv):
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||||
|
if isinstance(plainText, str):
|
||||||
|
plainText = plainText.encode('utf-8')
|
||||||
|
return cipher.encrypt(pad(plainText))
|
||||||
|
|
||||||
|
def AESDecrypt(cipherText, key, iv):
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||||
|
return unpad(cipher.decrypt(cipherText))
|
||||||
|
|
||||||
|
def hexAESDecrypt(cipherText, key, iv):
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||||
|
if isinstance(cipherText, str):
|
||||||
|
cipherText = cipherText.encode('utf-8')
|
||||||
|
return unpad(cipher.decrypt(binascii.unhexlify(cipherText)))
|
||||||
|
|
||||||
|
def base64AESDecrypt(cipherText, key, iv):
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||||
|
if isinstance(cipherText, str):
|
||||||
|
cipherText = cipherText.encode('utf-8')
|
||||||
|
return unpad(cipher.decrypt(base64.b64decode(cipherText)))
|
||||||
|
|
||||||
|
def pad(s):
|
||||||
|
return s + (16 - len(s) % 16) * chr(16 - len(s) % 16)
|
||||||
|
|
||||||
|
def unpad(s):
|
||||||
|
return s[:-ord(s[len(s) - 1:])]
|
187
common/Httpx.py
Normal file
187
common/Httpx.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - 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.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import requests
|
||||||
|
import random
|
||||||
|
import traceback
|
||||||
|
import zlib
|
||||||
|
import ujson as json
|
||||||
|
from .log import log
|
||||||
|
import re
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
def is_valid_utf8(text):
|
||||||
|
# 判断是否为有效的utf-8字符串
|
||||||
|
if "\ufffe" in text:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
text.encode('utf-8').decode('utf-8')
|
||||||
|
return True
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_plain_text(text):
|
||||||
|
# 判断是否为纯文本
|
||||||
|
pattern = re.compile(r'[^\x00-\x7F]')
|
||||||
|
return not bool(pattern.search(text))
|
||||||
|
|
||||||
|
def convert_dict_to_form_string(dic):
|
||||||
|
# 将字典转换为表单字符串
|
||||||
|
return '&'.join([f'{k}={v}' for k, v in dic.items()])
|
||||||
|
|
||||||
|
# 内置的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'.split('||')
|
||||||
|
|
||||||
|
# 日志记录器
|
||||||
|
logger = log('http_utils')
|
||||||
|
|
||||||
|
def request(url, options = {}):
|
||||||
|
'''
|
||||||
|
- Http请求主函数, 用于发送网络请求
|
||||||
|
- url: 需要请求的URL地址(必填)
|
||||||
|
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
|
||||||
|
- method: 请求方法
|
||||||
|
- headers: 请求头
|
||||||
|
- body: 请求体(也可使用python原生requests库的data参数)
|
||||||
|
- form: 提交的表单数据
|
||||||
|
|
||||||
|
@ return: requests.Response类型的响应数据
|
||||||
|
'''
|
||||||
|
# 获取请求方法,没有则默认为GET请求
|
||||||
|
try:
|
||||||
|
method = options['method']
|
||||||
|
options.pop('method')
|
||||||
|
except Exception as e:
|
||||||
|
method = 'GET'
|
||||||
|
# 获取User-Agent,没有则从ua_list中随机选择一个
|
||||||
|
try:
|
||||||
|
d_lower = {k.lower(): v for k, v in options['headers'].items()}
|
||||||
|
useragent = d_lower['user-agent']
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
options['headers']['User-Agent'] = random.choice(ua_list)
|
||||||
|
except:
|
||||||
|
options['headers'] = {}
|
||||||
|
options['headers']['User-Agent'] = random.choice(ua_list)
|
||||||
|
# 获取请求主函数
|
||||||
|
try:
|
||||||
|
reqattr = getattr(requests, method.lower())
|
||||||
|
except AttributeError:
|
||||||
|
raise AttributeError('Unsupported method: '+method)
|
||||||
|
# 请求前记录
|
||||||
|
logger.debug(f'HTTP Request: {url}\noptions: {options}')
|
||||||
|
# 转换body/form参数为原生的data参数,并为form请求追加Content-Type头
|
||||||
|
if (method == 'POST') or (method == 'PUT'):
|
||||||
|
if options.get('body'):
|
||||||
|
options['data'] = options['body']
|
||||||
|
options.pop('body')
|
||||||
|
if options.get('form'):
|
||||||
|
options['data'] = convert_dict_to_form_string(options['form'])
|
||||||
|
options.pop('form')
|
||||||
|
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||||
|
# 进行请求
|
||||||
|
try:
|
||||||
|
req = reqattr(url, **options)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'HTTP Request runs into an Error: {traceback.format_exc()}')
|
||||||
|
raise e
|
||||||
|
# 请求后记录
|
||||||
|
logger.debug(f'Request to {url} succeed with code {req.status_code}')
|
||||||
|
# 记录响应数据
|
||||||
|
try:
|
||||||
|
logger.debug(json.loads(req.content.decode("utf-8")))
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
logger.debug(json.loads(zlib.decompress(req.content).decode("utf-8")))
|
||||||
|
except zlib.error:
|
||||||
|
if is_valid_utf8(req.text) and is_plain_text(req.text):
|
||||||
|
logger.debug(req.text)
|
||||||
|
else:
|
||||||
|
logger.debug(binascii.hexlify(req.content))
|
||||||
|
except:
|
||||||
|
logger.debug(zlib.decompress(req.content).decode("utf-8") if is_valid_utf8(zlib.decompress(req.content).decode("utf-8")) and is_plain_text(zlib.decompress(req.content).decode("utf-8")) else binascii.hexlify(zlib.decompress(req.content)))
|
||||||
|
# 返回请求
|
||||||
|
return req
|
||||||
|
|
||||||
|
"""
|
||||||
|
async def asyncrequest(url, options={}):
|
||||||
|
'''
|
||||||
|
- Asynchronous HTTP request function used for sending network requests
|
||||||
|
- url: URL address to be requested (required)
|
||||||
|
- options: Configuration parameters for the request (optional, defaults to GET request)
|
||||||
|
- method: Request method
|
||||||
|
- headers: Request headers
|
||||||
|
- body: Request body (can also use the native 'data' parameter of the 'requests' library)
|
||||||
|
- form: Submitted form data
|
||||||
|
|
||||||
|
@ return: aiohttp.ClientResponse type response data
|
||||||
|
'''
|
||||||
|
# Get the request method, defaulting to GET if not provided
|
||||||
|
try:
|
||||||
|
method = options['method']
|
||||||
|
options.pop('method')
|
||||||
|
except KeyError:
|
||||||
|
method = 'GET'
|
||||||
|
# Get the User-Agent, choose randomly from ua_list if not present
|
||||||
|
try:
|
||||||
|
d_lower = {k.lower(): v for k, v in options['headers'].items()}
|
||||||
|
useragent = d_lower['user-agent']
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
options['headers']['User-Agent'] = random.choice(ua_list)
|
||||||
|
except:
|
||||||
|
options['headers'] = {}
|
||||||
|
options['headers']['User-Agent'] = random.choice(ua_list)
|
||||||
|
# Get the request function
|
||||||
|
try:
|
||||||
|
reqattr = getattr(aiohttp.ClientSession(), method.lower())
|
||||||
|
except AttributeError:
|
||||||
|
raise AttributeError('Unsupported method: ' + method)
|
||||||
|
# Log before the request
|
||||||
|
logger.debug(f'HTTP Request: {url}\noptions: {options}')
|
||||||
|
# Convert body/form parameter to native 'data' parameter and add 'Content-Type' header for form requests
|
||||||
|
if method in ['POST', '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'
|
||||||
|
# Send the request
|
||||||
|
try:
|
||||||
|
async with reqattr(url, **options) as req:
|
||||||
|
res = await req.read()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'HTTP Request runs into an Error: {traceback.format_exc()}')
|
||||||
|
raise e
|
||||||
|
# Log after the request
|
||||||
|
logger.debug(f'Request to {url} succeed with code {req.status}')
|
||||||
|
# Log the response data
|
||||||
|
try:
|
||||||
|
logger.debug(json.loads(res.decode("utf-8")))
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
logger.debug(json.loads(zlib.decompress(res).decode("utf-8")))
|
||||||
|
except zlib.error:
|
||||||
|
if is_valid_utf8(req.text) and is_plain_text(req.text):
|
||||||
|
logger.debug(req.text)
|
||||||
|
else:
|
||||||
|
logger.debug(binascii.hexlify(res))
|
||||||
|
except:
|
||||||
|
logger.debug(
|
||||||
|
zlib.decompress(res).decode("utf-8") if is_valid_utf8(zlib.decompress(res).decode("utf-8")) and is_plain_text(
|
||||||
|
zlib.decompress(res).decode("utf-8")) else binascii.hexlify(zlib.decompress(res)))
|
||||||
|
# Return the response
|
||||||
|
return req
|
||||||
|
|
||||||
|
"""
|
0
common/__init__.py
Normal file
0
common/__init__.py
Normal file
459
common/config.py
Normal file
459
common/config.py
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: config.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
import ujson as json
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
from .utils import unique_list, readfile
|
||||||
|
from . import variable
|
||||||
|
from .log import log
|
||||||
|
# from dbutils.pooled_db import PooledDB
|
||||||
|
import threading
|
||||||
|
|
||||||
|
# 创建线程本地存储对象
|
||||||
|
local_data = 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
|
||||||
|
|
||||||
|
logger = log('config_manager')
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigReadException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
default = {
|
||||||
|
"common": {
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"_host-desc": "服务器启动时所使用的HOST地址",
|
||||||
|
"port": "9763",
|
||||||
|
"_port_desc": "服务器启动时所使用的端口",
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"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": "客户端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号"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
logger.info('首次启动或配置文件被删除,已创建默认配置文件')
|
||||||
|
logger.info(
|
||||||
|
f'\n请到{variable.workdir + os.path.sep}config.json修改配置后重新启动服务器')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigReadException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_data():
|
||||||
|
config_data = {}
|
||||||
|
try:
|
||||||
|
# Connect to the database
|
||||||
|
conn = get_data_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Retrieve all configuration data from the 'config' table
|
||||||
|
cursor.execute("SELECT key, value FROM data")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
key, value = row
|
||||||
|
config_data[key] = json.loads(value)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading config: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
return config_data
|
||||||
|
|
||||||
|
|
||||||
|
def save_data(config_data):
|
||||||
|
try:
|
||||||
|
# Connect to the database
|
||||||
|
conn = get_data_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Clear existing data in the 'data' table
|
||||||
|
cursor.execute("DELETE FROM data")
|
||||||
|
|
||||||
|
# Insert the new configuration data into the 'data' table
|
||||||
|
for key, value in config_data.items():
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO data (key, value) VALUES (?, ?)", (key, json.dumps(value)))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error saving config: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def getCache(module, key):
|
||||||
|
try:
|
||||||
|
# 连接到数据库(如果数据库不存在,则会自动创建)
|
||||||
|
conn = get_cache_connection()
|
||||||
|
|
||||||
|
# 创建一个游标对象
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT data FROM cache WHERE module=? AND key=?",
|
||||||
|
(module, key))
|
||||||
|
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if result:
|
||||||
|
cache_data = json.loads(result[0])
|
||||||
|
if (not cache_data['expire']):
|
||||||
|
return cache_data
|
||||||
|
if (int(time.time()) < cache_data['time']):
|
||||||
|
return cache_data
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
# traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def updateCache(module, key, data):
|
||||||
|
try:
|
||||||
|
# 连接到数据库(如果数据库不存在,则会自动创建)
|
||||||
|
conn = get_cache_connection()
|
||||||
|
|
||||||
|
# 创建一个游标对象
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT data FROM cache WHERE module=? AND key=?", (module, key))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if result:
|
||||||
|
cache_data = json.loads(result[0])
|
||||||
|
if isinstance(cache_data, dict):
|
||||||
|
cache_data.update(data)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Cache data for module '{module}' and key '{key}' is not a dictionary.")
|
||||||
|
else:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO cache (module, key, data) VALUES (?, ?, ?)", (module, key, json.dumps(data)))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
except:
|
||||||
|
logger.error('缓存写入遇到错误…')
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def resetRequestTime(ip):
|
||||||
|
config_data = load_data()
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
config_data['requestTime'][ip] = 0
|
||||||
|
except KeyError:
|
||||||
|
config_data['requestTime'] = {}
|
||||||
|
config_data['requestTime'][ip] = 0
|
||||||
|
save_data(config_data)
|
||||||
|
except:
|
||||||
|
logger.error('配置写入遇到错误…')
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def updateRequestTime(ip):
|
||||||
|
try:
|
||||||
|
config_data = load_data()
|
||||||
|
try:
|
||||||
|
config_data['requestTime'][ip] = time.time()
|
||||||
|
except KeyError:
|
||||||
|
config_data['requestTime'] = {}
|
||||||
|
config_data['requestTime'][ip] = time.time()
|
||||||
|
save_data(config_data)
|
||||||
|
except:
|
||||||
|
logger.error('配置写入遇到错误...')
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def getRequestTime(ip):
|
||||||
|
config_data = load_data()
|
||||||
|
try:
|
||||||
|
value = config_data['requestTime'][ip]
|
||||||
|
except:
|
||||||
|
value = 0
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def read_data(key):
|
||||||
|
config = load_data()
|
||||||
|
keys = key.split('.')
|
||||||
|
value = config
|
||||||
|
for k in keys:
|
||||||
|
if k not in value and keys.index(k) != len(keys) - 1:
|
||||||
|
value[k] = {}
|
||||||
|
elif k not in value and keys.index(k) == len(keys) - 1:
|
||||||
|
value = None
|
||||||
|
value = value[k]
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def write_data(key, value):
|
||||||
|
config = load_data()
|
||||||
|
|
||||||
|
keys = key.split('.')
|
||||||
|
current = config
|
||||||
|
for k in keys[:-1]:
|
||||||
|
if k not in current:
|
||||||
|
current[k] = {}
|
||||||
|
current = current[k]
|
||||||
|
|
||||||
|
current[keys[-1]] = value
|
||||||
|
|
||||||
|
save_data(config)
|
||||||
|
|
||||||
|
|
||||||
|
def push_to_list(key, obj):
|
||||||
|
config = load_data()
|
||||||
|
|
||||||
|
keys = key.split('.')
|
||||||
|
current = config
|
||||||
|
for k in keys[:-1]:
|
||||||
|
if k not in current:
|
||||||
|
current[k] = {}
|
||||||
|
current = current[k]
|
||||||
|
|
||||||
|
if keys[-1] not in current:
|
||||||
|
current[keys[-1]] = []
|
||||||
|
|
||||||
|
current[keys[-1]].append(obj)
|
||||||
|
|
||||||
|
save_data(config)
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(key):
|
||||||
|
config = variable.config
|
||||||
|
keys = key.split('.')
|
||||||
|
value = config
|
||||||
|
for k in keys:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
if k not in value and keys.index(k) != len(keys) - 1:
|
||||||
|
value[k] = {}
|
||||||
|
elif k not in value and keys.index(k) == len(keys) - 1:
|
||||||
|
value = None
|
||||||
|
value = value[k]
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
break
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def initConfig():
|
||||||
|
try:
|
||||||
|
with open("./config.json", "r", encoding="utf-8") as f:
|
||||||
|
try:
|
||||||
|
variable.config = json.loads(f.read())
|
||||||
|
except:
|
||||||
|
if os.path.getsize("./config.json") != 0:
|
||||||
|
logger.error("配置文件加载失败,请检查是否遵循JSON语法规范")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
variable.config = handle_default_config()
|
||||||
|
except FileNotFoundError:
|
||||||
|
variable.config = handle_default_config()
|
||||||
|
# print(variable.config)
|
||||||
|
logger.debug("配置文件加载成功")
|
||||||
|
conn = sqlite3.connect('cache.db')
|
||||||
|
|
||||||
|
# 创建一个游标对象
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 创建一个表来存储缓存数据
|
||||||
|
cursor.execute(readfile(variable.workdir +
|
||||||
|
'/common/sql/create_cache_db.sql'))
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
conn2 = sqlite3.connect('data.db')
|
||||||
|
|
||||||
|
# 创建一个游标对象
|
||||||
|
cursor2 = conn2.cursor()
|
||||||
|
|
||||||
|
cursor2.execute(readfile(variable.workdir +
|
||||||
|
'/common/sql/create_data_db.sql'))
|
||||||
|
|
||||||
|
conn2.close()
|
||||||
|
|
||||||
|
logger.debug('数据库初始化成功')
|
||||||
|
|
||||||
|
# print
|
||||||
|
if (load_data() == {}):
|
||||||
|
write_data('banList', [])
|
||||||
|
write_data('requestTime', {})
|
||||||
|
logger.debug('数据库内容为空,已写入默认值')
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
if (variable.banList_suggest < 10):
|
||||||
|
variable.banList_suggest += 1
|
||||||
|
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
|
||||||
|
|
||||||
|
|
||||||
|
def check_ip_banned(ip_addr):
|
||||||
|
if read_config('security.banlist.enable'):
|
||||||
|
banList = read_data('banList')
|
||||||
|
for ban in banList:
|
||||||
|
if (ban['ip'] == ip_addr):
|
||||||
|
if (ban['expire']):
|
||||||
|
if (ban['expire_time'] > int(time.time())):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
banList.remove(ban)
|
||||||
|
write_data('banList', banList)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
variable.banList_suggest += 1
|
||||||
|
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
|
||||||
|
|
||||||
|
|
||||||
|
initConfig()
|
13
common/exceptions.py
Normal file
13
common/exceptions.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: exceptions.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
class FailedException(Exception):
|
||||||
|
# 此错误用于处理代理API请求失败的情况
|
||||||
|
pass
|
238
common/kwdes.py
Normal file
238
common/kwdes.py
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: kwdes.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
# KuwoDES加密实现,CV过来的,不进行注释
|
||||||
|
# 下方为原文件版权声明,在此进行保留
|
||||||
|
# Copyright (C) 2014 LiuLang <gsushzhsosgsu@gmail.com>
|
||||||
|
# Use of this source code is governed by GPLv3 license that can be found
|
||||||
|
# in http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
DES_MODE_DECRYPT = 1
|
||||||
|
|
||||||
|
arrayE = [
|
||||||
|
31, 0, DES_MODE_DECRYPT, 2, 3, 4, -1, -1, 3, 4, 5, 6, 7, 8, -1, -1, 7, 8, 9, 10, 11, 12, -1, -1, 11, 12, 13, 14, 15, 16, -1, -
|
||||||
|
1, 15, 16, 17, 18, 19, 20, -1, -1, 19, 20, 21, 22, 23, 24, -1, -
|
||||||
|
1, 23, 24, 25, 26, 27, 28, -1, -1, 27, 28, 29, 30, 31, 30, -1, -1
|
||||||
|
]
|
||||||
|
|
||||||
|
arrayIP = [
|
||||||
|
57, 49, 41, 33, 25, 17, 9, DES_MODE_DECRYPT, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6
|
||||||
|
]
|
||||||
|
|
||||||
|
arrayIP_1 = [
|
||||||
|
39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, DES_MODE_DECRYPT, 41, 9, 49, 17, 57, 25, 32, 0, 40, 8, 48, 16, 56, 24
|
||||||
|
]
|
||||||
|
arrayLs = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
|
||||||
|
arrayLsMask = [0, 0x100001, 0x300003]
|
||||||
|
arrayMask = [2 ** i for i in range(64)]
|
||||||
|
arrayMask[-1] *= -1
|
||||||
|
arrayP = [
|
||||||
|
15, 6, 19, 20, 28, 11, 27, 16,
|
||||||
|
0, 14, 22, 25, 4, 17, 30, 9,
|
||||||
|
1, 7, 23, 13, 31, 26, 2, 8,
|
||||||
|
18, 12, 29, 5, 21, 10, 3, 24,
|
||||||
|
]
|
||||||
|
arrayPC_1 = [
|
||||||
|
56, 48, 40, 32, 24, 16, 8, 0,
|
||||||
|
57, 49, 41, 33, 25, 17, 9, 1,
|
||||||
|
58, 50, 42, 34, 26, 18, 10, 2,
|
||||||
|
59, 51, 43, 35, 62, 54, 46, 38,
|
||||||
|
30, 22, 14, 6, 61, 53, 45, 37,
|
||||||
|
29, 21, 13, 5, 60, 52, 44, 36,
|
||||||
|
28, 20, 12, 4, 27, 19, 11, 3,
|
||||||
|
]
|
||||||
|
arrayPC_2 = [
|
||||||
|
13, 16, 10, 23, 0, 4, -1, -1,
|
||||||
|
2, 27, 14, 5, 20, 9, -1, -1,
|
||||||
|
22, 18, 11, 3, 25, 7, -1, -1,
|
||||||
|
15, 6, 26, 19, 12, 1, -1, -1,
|
||||||
|
40, 51, 30, 36, 46, 54, -1, -1,
|
||||||
|
29, 39, 50, 44, 32, 47, -1, -1,
|
||||||
|
43, 48, 38, 55, 33, 52, -1, -1,
|
||||||
|
45, 41, 49, 35, 28, 31, -1, -1,
|
||||||
|
]
|
||||||
|
matrixNSBox = [[
|
||||||
|
14, 4, 3, 15, 2, 13, 5, 3,
|
||||||
|
13, 14, 6, 9, 11, 2, 0, 5,
|
||||||
|
4, 1, 10, 12, 15, 6, 9, 10,
|
||||||
|
1, 8, 12, 7, 8, 11, 7, 0,
|
||||||
|
0, 15, 10, 5, 14, 4, 9, 10,
|
||||||
|
7, 8, 12, 3, 13, 1, 3, 6,
|
||||||
|
15, 12, 6, 11, 2, 9, 5, 0,
|
||||||
|
4, 2, 11, 14, 1, 7, 8, 13, ], [
|
||||||
|
15, 0, 9, 5, 6, 10, 12, 9,
|
||||||
|
8, 7, 2, 12, 3, 13, 5, 2,
|
||||||
|
1, 14, 7, 8, 11, 4, 0, 3,
|
||||||
|
14, 11, 13, 6, 4, 1, 10, 15,
|
||||||
|
3, 13, 12, 11, 15, 3, 6, 0,
|
||||||
|
4, 10, 1, 7, 8, 4, 11, 14,
|
||||||
|
13, 8, 0, 6, 2, 15, 9, 5,
|
||||||
|
7, 1, 10, 12, 14, 2, 5, 9, ], [
|
||||||
|
10, 13, 1, 11, 6, 8, 11, 5,
|
||||||
|
9, 4, 12, 2, 15, 3, 2, 14,
|
||||||
|
0, 6, 13, 1, 3, 15, 4, 10,
|
||||||
|
14, 9, 7, 12, 5, 0, 8, 7,
|
||||||
|
13, 1, 2, 4, 3, 6, 12, 11,
|
||||||
|
0, 13, 5, 14, 6, 8, 15, 2,
|
||||||
|
7, 10, 8, 15, 4, 9, 11, 5,
|
||||||
|
9, 0, 14, 3, 10, 7, 1, 12, ], [
|
||||||
|
7, 10, 1, 15, 0, 12, 11, 5,
|
||||||
|
14, 9, 8, 3, 9, 7, 4, 8,
|
||||||
|
13, 6, 2, 1, 6, 11, 12, 2,
|
||||||
|
3, 0, 5, 14, 10, 13, 15, 4,
|
||||||
|
13, 3, 4, 9, 6, 10, 1, 12,
|
||||||
|
11, 0, 2, 5, 0, 13, 14, 2,
|
||||||
|
8, 15, 7, 4, 15, 1, 10, 7,
|
||||||
|
5, 6, 12, 11, 3, 8, 9, 14, ], [
|
||||||
|
2, 4, 8, 15, 7, 10, 13, 6,
|
||||||
|
4, 1, 3, 12, 11, 7, 14, 0,
|
||||||
|
12, 2, 5, 9, 10, 13, 0, 3,
|
||||||
|
1, 11, 15, 5, 6, 8, 9, 14,
|
||||||
|
14, 11, 5, 6, 4, 1, 3, 10,
|
||||||
|
2, 12, 15, 0, 13, 2, 8, 5,
|
||||||
|
11, 8, 0, 15, 7, 14, 9, 4,
|
||||||
|
12, 7, 10, 9, 1, 13, 6, 3, ], [
|
||||||
|
12, 9, 0, 7, 9, 2, 14, 1,
|
||||||
|
10, 15, 3, 4, 6, 12, 5, 11,
|
||||||
|
1, 14, 13, 0, 2, 8, 7, 13,
|
||||||
|
15, 5, 4, 10, 8, 3, 11, 6,
|
||||||
|
10, 4, 6, 11, 7, 9, 0, 6,
|
||||||
|
4, 2, 13, 1, 9, 15, 3, 8,
|
||||||
|
15, 3, 1, 14, 12, 5, 11, 0,
|
||||||
|
2, 12, 14, 7, 5, 10, 8, 13, ], [
|
||||||
|
4, 1, 3, 10, 15, 12, 5, 0,
|
||||||
|
2, 11, 9, 6, 8, 7, 6, 9,
|
||||||
|
11, 4, 12, 15, 0, 3, 10, 5,
|
||||||
|
14, 13, 7, 8, 13, 14, 1, 2,
|
||||||
|
13, 6, 14, 9, 4, 1, 2, 14,
|
||||||
|
11, 13, 5, 0, 1, 10, 8, 3,
|
||||||
|
0, 11, 3, 5, 9, 4, 15, 2,
|
||||||
|
7, 8, 12, 15, 10, 7, 6, 12, ], [
|
||||||
|
13, 7, 10, 0, 6, 9, 5, 15,
|
||||||
|
8, 4, 3, 10, 11, 14, 12, 5,
|
||||||
|
2, 11, 9, 6, 15, 12, 0, 3,
|
||||||
|
4, 1, 14, 13, 1, 2, 7, 8,
|
||||||
|
1, 2, 12, 15, 10, 4, 0, 3,
|
||||||
|
13, 14, 6, 9, 7, 8, 9, 6,
|
||||||
|
15, 1, 5, 12, 3, 10, 14, 5,
|
||||||
|
8, 7, 11, 0, 4, 13, 2, 11, ],
|
||||||
|
]
|
||||||
|
|
||||||
|
SECRET_KEY = b'ylzsxkwm'
|
||||||
|
|
||||||
|
|
||||||
|
def bit_transform(arr_int, n, l):
|
||||||
|
l2 = 0
|
||||||
|
for i in range(n):
|
||||||
|
if arr_int[i] < 0 or (l & arrayMask[arr_int[i]] == 0):
|
||||||
|
continue
|
||||||
|
l2 |= arrayMask[i]
|
||||||
|
return l2
|
||||||
|
|
||||||
|
|
||||||
|
def DES64(longs, l):
|
||||||
|
out = 0
|
||||||
|
SOut = 0
|
||||||
|
pR = [0] * 8
|
||||||
|
pSource = [0, 0]
|
||||||
|
sbi = 0
|
||||||
|
t = 0
|
||||||
|
L = 0
|
||||||
|
R = 0
|
||||||
|
out = bit_transform(arrayIP, 64, l)
|
||||||
|
pSource[0] = 0xFFFFFFFF & out
|
||||||
|
pSource[1] = (-4294967296 & out) >> 32
|
||||||
|
for i in range(16):
|
||||||
|
R = pSource[1]
|
||||||
|
R = bit_transform(arrayE, 64, R)
|
||||||
|
R ^= longs[i]
|
||||||
|
for j in range(8):
|
||||||
|
pR[j] = 255 & R >> j * 8
|
||||||
|
SOut = 0
|
||||||
|
for sbi in range(7, -1, -1):
|
||||||
|
SOut <<= 4
|
||||||
|
SOut |= matrixNSBox[sbi][pR[sbi]]
|
||||||
|
|
||||||
|
R = bit_transform(arrayP, 32, SOut)
|
||||||
|
L = pSource[0]
|
||||||
|
pSource[0] = pSource[1]
|
||||||
|
pSource[1] = L ^ R
|
||||||
|
pSource = pSource[::-1]
|
||||||
|
out = -4294967296 & pSource[1] << 32 | 0xFFFFFFFF & pSource[0]
|
||||||
|
out = bit_transform(arrayIP_1, 64, out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def sub_keys(l, longs, n):
|
||||||
|
l2 = bit_transform(arrayPC_1, 56, l)
|
||||||
|
for i in range(16):
|
||||||
|
l2 = ((l2 & arrayLsMask[arrayLs[i]]) << 28 - arrayLs[i] | (l2 & ~arrayLsMask[arrayLs[i]]) >> arrayLs[i])
|
||||||
|
longs[i] = bit_transform(arrayPC_2, 64, l2)
|
||||||
|
j = 0
|
||||||
|
while n == 1 and j < 8:
|
||||||
|
l3 = longs[j]
|
||||||
|
longs[j], longs[15-j] = longs[15-j], longs[j]
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(msg, key=SECRET_KEY):
|
||||||
|
if isinstance(msg, str):
|
||||||
|
msg = msg.encode()
|
||||||
|
if isinstance(key, str):
|
||||||
|
key = key.encode()
|
||||||
|
assert (isinstance(msg, bytes))
|
||||||
|
assert (isinstance(key, bytes))
|
||||||
|
|
||||||
|
# 处理密钥块
|
||||||
|
l = 0
|
||||||
|
for i in range(8):
|
||||||
|
l = l | key[i] << i * 8
|
||||||
|
|
||||||
|
j = len(msg) // 8
|
||||||
|
# arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了
|
||||||
|
arrLong1 = [0] * 16
|
||||||
|
sub_keys(l, arrLong1, 0)
|
||||||
|
# arrLong2 存放的是前部分的明文
|
||||||
|
arrLong2 = [0] * j
|
||||||
|
for m in range(j):
|
||||||
|
for n in range(8):
|
||||||
|
arrLong2[m] |= msg[n + m * 8] << n * 8
|
||||||
|
|
||||||
|
# 用于存放密文
|
||||||
|
arrLong3 = [0] * ((1 + 8 * (j + 1)) // 8)
|
||||||
|
# 计算前部的数据块(除了最后一部分)
|
||||||
|
for i1 in range(j):
|
||||||
|
arrLong3[i1] = DES64(arrLong1, arrLong2[i1])
|
||||||
|
|
||||||
|
# 保存多出来的字节
|
||||||
|
arrByte1 = msg[j*8:]
|
||||||
|
l2 = 0
|
||||||
|
for i1 in range(len(msg) % 8):
|
||||||
|
l2 |= arrByte1[i1] << i1 * 8
|
||||||
|
# 计算多出的那一位(最后一位)
|
||||||
|
arrLong3[j] = DES64(arrLong1, l2)
|
||||||
|
|
||||||
|
# 将密文转为字节型
|
||||||
|
arrByte2 = [0] * (8 * len(arrLong3))
|
||||||
|
i4 = 0
|
||||||
|
for l3 in arrLong3:
|
||||||
|
for i6 in range(8):
|
||||||
|
arrByte2[i4] = (255 & l3 >> i6 * 8)
|
||||||
|
i4 += 1
|
||||||
|
return arrByte2
|
||||||
|
|
||||||
|
|
||||||
|
def base64_encrypt(msg):
|
||||||
|
b1 = encrypt(msg)
|
||||||
|
b2 = bytearray(b1)
|
||||||
|
s = base64.encodebytes(b2)
|
||||||
|
return s.replace(b'\n', b'').decode()
|
139
common/log.py
Normal file
139
common/log.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: log.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import colorlog
|
||||||
|
import os
|
||||||
|
from .utils import sanitize_filename
|
||||||
|
from .variable import debug_mode, log_length_limit
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.mkdir("logs")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class flaskLogHelper(logging.Handler):
|
||||||
|
# werkzeug日志转接器
|
||||||
|
def __init__(self, custom_logger):
|
||||||
|
super().__init__()
|
||||||
|
self.custom_logger = custom_logger
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
#print(record)
|
||||||
|
log_message = self.format(record)
|
||||||
|
self.custom_logger.info(log_message)
|
||||||
|
|
||||||
|
class log:
|
||||||
|
# 主类
|
||||||
|
def __init__(self, module_name = 'Not named logger', output_level = 'INFO', filename = ''):
|
||||||
|
self._logger = logging.getLogger(module_name)
|
||||||
|
if not output_level.upper() in dir(logging):
|
||||||
|
raise NameError('Unknown loglevel: '+output_level)
|
||||||
|
if not debug_mode:
|
||||||
|
self._logger.setLevel(getattr(logging, output_level.upper()))
|
||||||
|
else:
|
||||||
|
self._logger.setLevel(logging.DEBUG)
|
||||||
|
formatter = colorlog.ColoredFormatter(
|
||||||
|
'%(log_color)s%(asctime)s|[%(name)s/%(levelname)s]|%(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S',
|
||||||
|
log_colors={
|
||||||
|
'DEBUG': 'cyan',
|
||||||
|
'INFO': 'green',
|
||||||
|
'WARNING': 'yellow',
|
||||||
|
'ERROR': 'red',
|
||||||
|
'CRITICAL': 'red,bg_white',
|
||||||
|
})
|
||||||
|
file_formatter = logging.Formatter(
|
||||||
|
'%(asctime)s|[%(name)s/%(levelname)s]|%(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
if filename:
|
||||||
|
filename = sanitize_filename(filename)
|
||||||
|
else:
|
||||||
|
filename = './logs/' + module_name + '.log'
|
||||||
|
file_handler = logging.FileHandler(filename, encoding = "utf-8")
|
||||||
|
file_handler.setFormatter(file_formatter)
|
||||||
|
file_handler_ = logging.FileHandler("./logs/console_full.log", encoding = "utf-8")
|
||||||
|
file_handler_.setFormatter(file_formatter)
|
||||||
|
self._logger.addHandler(file_handler_)
|
||||||
|
self._logger.addHandler(file_handler)
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
self.module_name = module_name
|
||||||
|
self._logger.addHandler(console_handler)
|
||||||
|
self.debug_ = logging.getLogger(module_name + '_levelChangedMessage')
|
||||||
|
debug_handler = logging.StreamHandler()
|
||||||
|
debug_handler.setFormatter(formatter)
|
||||||
|
self.debug_.addHandler(debug_handler)
|
||||||
|
self.debug_.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
def debug(self, message, allow_hidden = True):
|
||||||
|
if self.module_name == "flask" and "\n" in message:
|
||||||
|
if message.startswith("Error"):
|
||||||
|
return self._logger.error(message)
|
||||||
|
for m in message.split("\n"):
|
||||||
|
if "WARNING" in m:
|
||||||
|
self._logger.warning(m)
|
||||||
|
else:
|
||||||
|
self._logger.info(m)
|
||||||
|
return
|
||||||
|
if len(str(message)) > log_length_limit and allow_hidden:
|
||||||
|
message = str(message)[:log_length_limit] + "..."
|
||||||
|
self._logger.debug(message)
|
||||||
|
|
||||||
|
def log(self, message, allow_hidden = True):
|
||||||
|
if self.module_name == "flask" and "\n" in message:
|
||||||
|
if message.startswith("Error"):
|
||||||
|
return self._logger.error(message)
|
||||||
|
for m in message.split("\n"):
|
||||||
|
if "WARNING" in m:
|
||||||
|
self._logger.warning(m)
|
||||||
|
else:
|
||||||
|
self._logger.info(m)
|
||||||
|
return
|
||||||
|
if len(str(message)) > log_length_limit and allow_hidden:
|
||||||
|
message = str(message)[:log_length_limit] + "..."
|
||||||
|
self._logger.info(message)
|
||||||
|
|
||||||
|
def info(self, message, allow_hidden = True):
|
||||||
|
if self.module_name == "flask" and "\n" in message:
|
||||||
|
if message.startswith("Error"):
|
||||||
|
return self._logger.error(message)
|
||||||
|
for m in message.split("\n"):
|
||||||
|
if "WARNING" in m:
|
||||||
|
self._logger.warning(m)
|
||||||
|
else:
|
||||||
|
self._logger.info(m)
|
||||||
|
return
|
||||||
|
if len(str(message)) > log_length_limit and allow_hidden:
|
||||||
|
message = str(message)[:log_length_limit] + "..."
|
||||||
|
self._logger.info(message)
|
||||||
|
|
||||||
|
def warning(self, message):
|
||||||
|
self._logger.warning(message)
|
||||||
|
|
||||||
|
def error(self, message):
|
||||||
|
self._logger.error(message)
|
||||||
|
|
||||||
|
def critical(self, message):
|
||||||
|
self._logger.critical(message)
|
||||||
|
|
||||||
|
def set_level(self, loglevel):
|
||||||
|
loglevel_upper = loglevel.upper()
|
||||||
|
if not loglevel_upper in dir(logging):
|
||||||
|
raise NameError('Unknown loglevel: '+loglevel)
|
||||||
|
self.debug_.debug('loglevel changed to: '+ loglevel_upper)
|
||||||
|
self._logger.setLevel(getattr(logging, loglevel_upper))
|
||||||
|
|
||||||
|
def getLogger(self):
|
||||||
|
return self._logger
|
||||||
|
|
||||||
|
def addHandler(self, handler):
|
||||||
|
self._logger.addHandler(handler)
|
43
common/lxsecurity.py
Normal file
43
common/lxsecurity.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: lxsecurity.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
import ujson as json
|
||||||
|
import binascii
|
||||||
|
import re
|
||||||
|
|
||||||
|
# js: path = url.replace(/^https?:\/\/[\w.:]+\//, '/')
|
||||||
|
def checklxmheader(lxm, url):
|
||||||
|
try:
|
||||||
|
path = url.replace(re.findall(r'(?:https?:\/\/[\w.:]+\/)', url)[0], '/').replace('//', '/')
|
||||||
|
retvalue = re.findall(r'(?:\d\w)+', path)[0]
|
||||||
|
|
||||||
|
cop, version = tuple(lxm.split('&'))
|
||||||
|
version = (3 - len(version) % 3) * '0' + version
|
||||||
|
cop = utils.inflate_raw_sync(binascii.unhexlify(cop.encode('utf-8'))).decode('utf-8')
|
||||||
|
cop = utils.from_base64(cop).decode('utf-8')
|
||||||
|
# print(retvalue + version)
|
||||||
|
arr, outsideversion = tuple([cop.split(']')[0] + ']', cop.split(']')[1]])
|
||||||
|
arr = json.loads(arr)
|
||||||
|
version = re.findall("\\d+", version)[0]
|
||||||
|
if (not outsideversion.startswith(version)):
|
||||||
|
return False
|
||||||
|
if (
|
||||||
|
(not (version) in ("".join(arr))) and
|
||||||
|
(not (retvalue) in "".join(arr))
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
exports = {
|
||||||
|
|
||||||
|
}
|
60
common/scheduler.py
Normal file
60
common/scheduler.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: scheduler.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
# 一个简单的循环任务调度器
|
||||||
|
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from .variable import running
|
||||||
|
from . import log
|
||||||
|
|
||||||
|
logger = log.log("scheduler")
|
||||||
|
|
||||||
|
global tasks
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
class taskWrapper:
|
||||||
|
def __init__(self, name, function, interval = 86400, latest_execute = 0):
|
||||||
|
self.function = function
|
||||||
|
self.interval = interval
|
||||||
|
self.name = name
|
||||||
|
self.latest_execute = latest_execute
|
||||||
|
|
||||||
|
def check_available(self):
|
||||||
|
return (time.time() - self.latest_execute) >= self.interval
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
logger.info(f"task {self.name} run start")
|
||||||
|
self.function()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"task {self.name} run failed, waiting for next execute...")
|
||||||
|
|
||||||
|
def append(name, task, interval = 86400):
|
||||||
|
global tasks
|
||||||
|
logger.debug(f"new task ({name}) registered")
|
||||||
|
wrapper = taskWrapper(name, task, interval)
|
||||||
|
return tasks.append(wrapper)
|
||||||
|
|
||||||
|
def thread_runner():
|
||||||
|
global tasks
|
||||||
|
while True:
|
||||||
|
if not running:
|
||||||
|
return
|
||||||
|
for t in tasks:
|
||||||
|
if t.check_available():
|
||||||
|
t.latest_execute = int(time.time())
|
||||||
|
threading.Thread(target = t.run).start()
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
def run():
|
||||||
|
logger.debug("scheduler thread starting...")
|
||||||
|
threading.Thread(target = thread_runner).start()
|
||||||
|
logger.debug("schedluer thread load success")
|
5
common/sql/create_cache_db.sql
Normal file
5
common/sql/create_cache_db.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS cache
|
||||||
|
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
module TEXT NOT NULL,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL)
|
3
common/sql/create_data_db.sql
Normal file
3
common/sql/create_data_db.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS data
|
||||||
|
(key TEXT PRIMARY KEY,
|
||||||
|
value TEXT)
|
131
common/utils.py
Normal file
131
common/utils.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: utils.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
import platform
|
||||||
|
import binascii
|
||||||
|
import base64
|
||||||
|
import zlib
|
||||||
|
import re
|
||||||
|
import ujson as json
|
||||||
|
from urllib.parse import quote
|
||||||
|
from hashlib import md5 as _md5
|
||||||
|
from flask import Response
|
||||||
|
|
||||||
|
def to_base64(data_bytes):
|
||||||
|
encoded_data = base64.b64encode(data_bytes)
|
||||||
|
return encoded_data.decode('utf-8')
|
||||||
|
|
||||||
|
def to_hex(data_bytes):
|
||||||
|
hex_encoded = binascii.hexlify(data_bytes)
|
||||||
|
return hex_encoded.decode('utf-8')
|
||||||
|
|
||||||
|
def from_base64(data):
|
||||||
|
decoded_data = base64.b64decode(data)
|
||||||
|
return decoded_data
|
||||||
|
|
||||||
|
def from_hex(data):
|
||||||
|
decoded_data = binascii.unhexlify(data.decode('utf-8'))
|
||||||
|
return decoded_data
|
||||||
|
|
||||||
|
def inflate_raw_sync(data):
|
||||||
|
decompress_obj = zlib.decompressobj(-zlib.MAX_WBITS)
|
||||||
|
decompressed_data = decompress_obj.decompress(data) + decompress_obj.flush()
|
||||||
|
return decompressed_data
|
||||||
|
|
||||||
|
def require(module):
|
||||||
|
index = 0
|
||||||
|
module_array = module.split('.')
|
||||||
|
for m in module_array:
|
||||||
|
if index == 0:
|
||||||
|
_module = __import__(m)
|
||||||
|
index += 1
|
||||||
|
else:
|
||||||
|
_module = getattr(_module, m)
|
||||||
|
index += 1
|
||||||
|
return _module
|
||||||
|
|
||||||
|
def sanitize_filename(filename):
|
||||||
|
if platform.system() == 'Windows' or platform.system() == 'Cygwin':
|
||||||
|
# Windows不合法文件名字符
|
||||||
|
illegal_chars = r'[<>:"/\\|?*\x00-\x1f]'
|
||||||
|
else:
|
||||||
|
# 不合法文件名字符
|
||||||
|
illegal_chars = r'[/\x00-\x1f]'
|
||||||
|
# 将不合法字符替换为下划线
|
||||||
|
return re.sub(illegal_chars, '_', filename)
|
||||||
|
|
||||||
|
def md5(s: str):
|
||||||
|
# 计算md5
|
||||||
|
# print(s)
|
||||||
|
return _md5(s.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
def readfile(path, mode = "text"):
|
||||||
|
try:
|
||||||
|
fileObj = open(path, "rb")
|
||||||
|
except FileNotFoundError:
|
||||||
|
return "file not found"
|
||||||
|
content = fileObj.read()
|
||||||
|
if mode == "base64":
|
||||||
|
return to_base64(content)
|
||||||
|
elif mode == "hex":
|
||||||
|
return to_hex(content)
|
||||||
|
elif mode == "text":
|
||||||
|
return content.decode("utf-8")
|
||||||
|
else:
|
||||||
|
return "unsupported mode"
|
||||||
|
|
||||||
|
def unique_list(list_in):
|
||||||
|
unique_list = []
|
||||||
|
[unique_list.append(x) for x in list_in if x not in unique_list]
|
||||||
|
return unique_list
|
||||||
|
|
||||||
|
def format_dict_json(dic):
|
||||||
|
return Response(json.dumps(dic, indent=2, ensure_ascii=False), mimetype = "application/json")
|
||||||
|
|
||||||
|
def encodeURIComponent(component):
|
||||||
|
return quote(component)
|
||||||
|
|
||||||
|
def sort_dict(dictionary):
|
||||||
|
sorted_items = sorted(dictionary.items())
|
||||||
|
sorted_dict = {k: v for k, v in sorted_items}
|
||||||
|
return sorted_dict
|
||||||
|
|
||||||
|
def merge_dict(dict1, dict2):
|
||||||
|
merged_dict = dict2.copy()
|
||||||
|
merged_dict.update(dict1)
|
||||||
|
return merged_dict
|
||||||
|
|
||||||
|
class jsobject(dict):
|
||||||
|
def __init__(self, d):
|
||||||
|
super().__init__(d)
|
||||||
|
self._raw = d
|
||||||
|
for key, value in d.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
setattr(self, key, jsobject(value))
|
||||||
|
else:
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
super().__setattr__(key, value)
|
||||||
|
if key != "_raw":
|
||||||
|
self._raw[key] = value
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
result = {}
|
||||||
|
for key, value in self.items():
|
||||||
|
if isinstance(value, jsobject):
|
||||||
|
result[key] = value.to_dict()
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __getattr__(self, UNUSED):
|
||||||
|
return None
|
||||||
|
|
21
common/variable.py
Normal file
21
common/variable.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# ----------------------------------------
|
||||||
|
# - 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.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
debug_mode = True
|
||||||
|
log_length_limit = 50000000
|
||||||
|
running = True
|
||||||
|
config = {}
|
||||||
|
pool_data = None
|
||||||
|
pool_cache = None
|
||||||
|
workdir = os.getcwd()
|
||||||
|
banList_suggest = 0
|
79
main.py
Normal file
79
main.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# - mode: python -
|
||||||
|
# - author: helloplhm-qwq -
|
||||||
|
# - name: main.py -
|
||||||
|
# - project: lx-music-api-server -
|
||||||
|
# - license: MIT -
|
||||||
|
# ----------------------------------------
|
||||||
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
# Do not edit except you know what you are doing.
|
||||||
|
|
||||||
|
# flask
|
||||||
|
from flask import Flask, request
|
||||||
|
|
||||||
|
# create flask app
|
||||||
|
app = Flask("LXMusicTestAPI")
|
||||||
|
|
||||||
|
# redirect the default flask logging to custom
|
||||||
|
import logging
|
||||||
|
from common import config
|
||||||
|
from common import log
|
||||||
|
|
||||||
|
flask_logger = log.log('flask')
|
||||||
|
logging.getLogger('werkzeug').addHandler(log.flaskLogHelper(flask_logger))
|
||||||
|
logger = log.log("main")
|
||||||
|
|
||||||
|
from common import utils
|
||||||
|
from common import lxsecurity
|
||||||
|
from apis import SongURL
|
||||||
|
|
||||||
|
require = utils.require
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return utils.format_dict_json({"code": 0, "msg": "success", "data": None}), 200
|
||||||
|
|
||||||
|
@app.route('/<method>/<source>/<songId>/<quality>')
|
||||||
|
async def handle(method, source, songId, 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 (config.read_config("security.key.ban")):
|
||||||
|
config.ban_ip(request.remote_addr)
|
||||||
|
return utils.format_dict_json({"code": 1, "msg": "key验证失败", "data": None}), 403
|
||||||
|
if (config.read_config('security.check_lxm.enable') and request.host.split(':')[0] not in config.read_config('security.whitelist_host')):
|
||||||
|
lxm = request.headers.get('lxm')
|
||||||
|
if (not lxsecurity.checklxmheader(lxm, request.url)):
|
||||||
|
if (config.read_config('security.lxm_ban.enable')):
|
||||||
|
config.ban_ip(request.remote_addr)
|
||||||
|
return utils.format_dict_json({"code": 1, "msg": "lxm请求头验证失败", "data": None}), 403
|
||||||
|
|
||||||
|
if method == 'url':
|
||||||
|
return utils.format_dict_json(await SongURL(source, songId, quality))
|
||||||
|
else:
|
||||||
|
return utils.format_dict_json({'code': 6, 'msg': '未知的请求类型: ' + method, 'data': None}), 400
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def _500(_):
|
||||||
|
return utils.format_dict_json({'code': 4, 'msg': '内部服务器错误', 'data': None}), 500
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def _404(_):
|
||||||
|
return utils.format_dict_json({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}), 404
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def check():
|
||||||
|
if (request.headers.get("X-Real-IP")):
|
||||||
|
request.remote_addr = request.headers.get("X-Real-IP")
|
||||||
|
if (config.check_ip_banned(request.remote_addr)):
|
||||||
|
return utils.format_dict_json({"code": 1, "msg": "您的IP已被封禁", "data": None}), 403
|
||||||
|
config.updateRequestTime(request.remote_addr)
|
||||||
|
if (config.read_config("security.allowed_host.enable")):
|
||||||
|
if request.remote_host.split(":")[0] not in config.read_config("security.allowed_host.list"):
|
||||||
|
if config.read_config("security.allowed_host.blacklist.enable"):
|
||||||
|
config.ban_ip(request.remote_addr, int(config.read_config("security.allowed_host.blacklist.length")))
|
||||||
|
return utils.format_dict_json({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}), 404
|
||||||
|
|
||||||
|
|
||||||
|
app.run(host=config.read_config('common.host'), port=config.read_config('common.port'))
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
flask[async]
|
||||||
|
pycryptodome
|
||||||
|
ujson
|
||||||
|
requests
|
||||||
|
colorlog
|
Loading…
x
Reference in New Issue
Block a user