This commit is contained in:
“Folltoshe” 2023-11-10 21:25:59 +08:00
commit 24836934e0
26 changed files with 2165 additions and 0 deletions

31
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

459
common/config.py Normal file
View 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": "客户端versioncodepidversionsecret可能随此值而变化",
"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时使用的guiduin等信息不需要与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
View 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
View 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
View 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
View 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
View 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")

View 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)

View File

@ -0,0 +1,3 @@
CREATE TABLE IF NOT EXISTS data
(key TEXT PRIMARY KEY,
value TEXT)

131
common/utils.py Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
flask[async]
pycryptodome
ujson
requests
colorlog