Compare commits

...

42 Commits

Author SHA1 Message Date
ikun0014
3fb2718cdf feat: zzc sign 2025-03-28 16:51:41 +08:00
ikun0014
0677c0b5ad
Merge pull request #99 from luren-dc/main
feat: 刷新失活 qqmusic_key
2025-03-27 16:55:56 +08:00
Luren
2400602aae fix: 修复报错 2025-03-27 04:44:24 +00:00
Luren
77737d18e6 feat: 刷新失活 qqmusic_key 2025-03-27 04:38:10 +00:00
006lp
e13b7e3831
fix: attempt to fix quality level comparison logic in wy module (#98)
Main improvements:
1. Added explicit server response level comparison logic
2. Fixed data field reference error in NCMAPI path
3. Unified quality validation process between official and NCMAPI interfaces
2025-03-22 01:27:21 +08:00
lerdb
c175312307
chore: [release] release v2.0.0
此项目可能长期处于停更状态,但是如果你有好的想法也欢迎提交pr,我会在review后合并
2025-03-18 19:35:00 +08:00
dependabot[bot]
b4049ceb69
build(deps): bump pycryptodome from 3.21.0 to 3.22.0 (#97)
Bumps [pycryptodome](https://github.com/Legrandin/pycryptodome) from 3.21.0 to 3.22.0.
- [Release notes](https://github.com/Legrandin/pycryptodome/releases)
- [Changelog](https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst)
- [Commits](https://github.com/Legrandin/pycryptodome/compare/v3.21.0...v3.22.0)

---
updated-dependencies:
- dependency-name: pycryptodome
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-18 19:28:00 +08:00
dependabot[bot]
031834d40f
build(deps): bump pygments from 2.18.0 to 2.19.1 (#93)
Bumps [pygments](https://github.com/pygments/pygments) from 2.18.0 to 2.19.1.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.18.0...2.19.1)

---
updated-dependencies:
- dependency-name: pygments
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-02 20:11:09 +08:00
dependabot[bot]
daf34ace39
Merge pull request #92 from MeoProject/dependabot/pip/ruamel-yaml-0.18.10 2025-01-07 04:31:28 +00:00
dependabot[bot]
3b6ee6e9b2
build(deps): bump ruamel-yaml from 0.18.7 to 0.18.10
Bumps ruamel-yaml from 0.18.7 to 0.18.10.

---
updated-dependencies:
- dependency-name: ruamel-yaml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-06 23:33:14 +00:00
dependabot[bot]
edaec2a3d2
Merge pull request #91 from MeoProject/dependabot/pip/ruamel-yaml-0.18.7 2024-12-31 02:43:36 +00:00
dependabot[bot]
30c0eaa23e
build(deps): bump ruamel-yaml from 0.18.6 to 0.18.7
Bumps ruamel-yaml from 0.18.6 to 0.18.7.

---
updated-dependencies:
- dependency-name: ruamel-yaml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 23:49:10 +00:00
ikun0014
412f692ba1
Update default_config.py 2024-12-18 19:55:43 +08:00
ikun0014
c84c2acbb9
Update player.py 2024-12-18 19:55:26 +08:00
ikun0014
3e5c1cd94b
你好今年是哪年 2024-12-18 19:55:02 +08:00
ikun0014
3375f0beba
Merge pull request #88 from 006lp/main
Add: More detailed log output
2024-12-17 23:45:43 +08:00
ikun0014
82be199636
Update build.py 2024-12-17 23:09:20 +08:00
006lp
f7069fc6f8
Fix: Map the server-returned level to a standardized value 2024-12-17 20:44:07 +08:00
ikun0014
ed47e928dd
feat: 支持NeteaseCloudMusicApi 2024-12-17 20:37:34 +08:00
006lp
56d19ee671
Add: Use third-party Netease Cloud Music API server to send GET request for the link (temporary workaround) 2024-12-17 19:23:56 +08:00
dependabot[bot]
0433d7a4d1
Merge pull request #87 from MeoProject/dependabot/pip/hiredis-3.1.0 2024-12-10 08:38:46 +00:00
dependabot[bot]
c63beb983f
build(deps): bump hiredis from 3.0.0 to 3.1.0
Bumps [hiredis](https://github.com/redis/hiredis-py) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/redis/hiredis-py/releases)
- [Changelog](https://github.com/redis/hiredis-py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/hiredis-py/compare/v3.0.0...v3.1.0)

---
updated-dependencies:
- dependency-name: hiredis
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 08:36:22 +00:00
dependabot[bot]
6d247dbeac
Merge pull request #86 from MeoProject/dependabot/pip/redis-5.2.1 2024-12-10 08:34:53 +00:00
dependabot[bot]
d4cb252aa4
build(deps): bump redis from 5.2.0 to 5.2.1
Bumps [redis](https://github.com/redis/redis-py) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v5.2.0...v5.2.1)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 23:25:52 +00:00
lerdb
1ea53d2ca9
chore: 补两个括号 2024-11-19 23:05:26 +08:00
ikun0014
497f21d2d4
Merge pull request #85 from Folltoshe/main
fix: 修复使用redis缓存的时候过期时间错误
2024-11-17 22:44:52 +08:00
Folltoshe
8e361ff14b deps: 更新依赖 2024-11-17 22:42:12 +08:00
Folltoshe
d3ab5f910d fix: 修复使用redis缓存的时候过期时间错误 2024-11-17 22:34:14 +08:00
ikun0014
6b08683b66
Merge pull request #84 from Folltoshe/main
feat: 支持使用redis缓存数据
2024-11-17 22:03:47 +08:00
Folltoshe
196732ab7d feat: 支持使用redis缓存数据 2024-11-17 21:58:42 +08:00
ikun0014
dcfe8a4472
Merge pull request #83 from Folltoshe/main
chore: 优化启动脚本
2024-11-17 21:29:10 +08:00
Folltoshe
b45c3a7765 fix: 修复报错 2024-11-17 20:58:12 +08:00
Folltoshe
5442e3340b chore: 优化启动脚本 2024-11-17 20:57:16 +08:00
dependabot[bot]
92a69f048d
Merge pull request #82 from MeoProject/dependabot/pip/colorlog-6.9.0 2024-11-05 10:31:32 +00:00
dependabot[bot]
0fd9fe1c3e
build(deps): bump colorlog from 6.8.2 to 6.9.0
Bumps [colorlog](https://github.com/borntyping/python-colorlog) from 6.8.2 to 6.9.0.
- [Release notes](https://github.com/borntyping/python-colorlog/releases)
- [Commits](https://github.com/borntyping/python-colorlog/compare/v6.8.2...v6.9.0)

---
updated-dependencies:
- dependency-name: colorlog
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 23:14:51 +00:00
ikun0014
3533a72681
feat: 支持关闭某个平台的服务 2024-10-28 18:07:25 +08:00
dependabot[bot]
88937e4917
build(deps): bump xmltodict from 0.14.1 to 0.14.2 (#80)
Bumps [xmltodict](https://github.com/martinblech/xmltodict) from 0.14.1 to 0.14.2.
- [Changelog](https://github.com/martinblech/xmltodict/blob/master/CHANGELOG.md)
- [Commits](https://github.com/martinblech/xmltodict/compare/v0.14.1...v0.14.2)

---
updated-dependencies:
- dependency-name: xmltodict
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-27 19:33:57 +08:00
ikun0014
6c6ef01681
chore: 统一为refresh_login 2024-10-16 20:49:49 +08:00
dependabot[bot]
f67de8f43b
build(deps): bump aiohttp from 3.10.9 to 3.10.10 (#79)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.9 to 3.10.10.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.9...v3.10.10)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 14:50:01 +08:00
dependabot[bot]
7dbafb958c
build(deps): bump xmltodict from 0.13.0 to 0.14.1 (#78)
Bumps [xmltodict](https://github.com/martinblech/xmltodict) from 0.13.0 to 0.14.1.
- [Changelog](https://github.com/martinblech/xmltodict/blob/master/CHANGELOG.md)
- [Commits](https://github.com/martinblech/xmltodict/compare/v0.13.0...v0.14.1)

---
updated-dependencies:
- dependency-name: xmltodict
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 09:43:40 +08:00
lerdb
f7988a48b1
chore: 合并log输出 2024-10-10 17:09:10 +08:00
lerdb
52420696f8
fix: 修复一个逻辑漏洞 2024-10-10 17:08:23 +08:00
24 changed files with 1681 additions and 1372 deletions

View File

@ -1,13 +0,0 @@
FROM python:3.10-alpine
WORKDIR /app
COPY ./main.py .
COPY ./common ./common
COPY ./modules ./modules
COPY ./requirements.txt .
# 指定源, 如果后期源挂了, 更换个源就可以.
RUN pip install --no-cache -i https://pypi.mirrors.ustc.edu.cn/simple/ -r requirements.txt
CMD [ "python", "main.py" ]

View File

@ -105,7 +105,7 @@ def build_test(fileName):
'PyInstaller',
'-F',
'-i',
'icon.ico',
'res/icon.ico',
'--name',
fileName if fileName else f'lx-music-api-server_{sha}',
'main.py'])
@ -140,7 +140,7 @@ def build_release(fileName = ''):
'PyInstaller',
'-F',
'-i',
'icon.ico',
'res/icon.ico',
'--name',
fileName if fileName else f'lx-music-api-server_{vername}',
'main.py'])
@ -231,4 +231,4 @@ if __name__ == '__main__':
main()
except KeyboardInterrupt:
print('[INFO] Aborting...')
sys.exit(0)
sys.exit(0)

View File

@ -1,9 +1,9 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: Httpx.py -
# - project: lx-music-api-server -
# - license: MIT -
# - mode: python -
# - author: helloplhm-qwq -
# - name: Httpx.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
@ -21,15 +21,16 @@ from . import config
from . import utils
from . import variable
def is_valid_utf8(text) -> bool:
try:
if isinstance(text, bytes):
text = text.decode('utf-8')
text = text.decode("utf-8")
# 判断是否为有效的utf-8字符串
if "\ufffe" in text:
return False
try:
text.encode('utf-8').decode('utf-8')
text.encode("utf-8").decode("utf-8")
return True
except UnicodeDecodeError:
return False
@ -37,42 +38,48 @@ def is_valid_utf8(text) -> bool:
logger.error(traceback.format_exc())
return False
def is_plain_text(text) -> bool:
# 判断是否为纯文本
pattern = re.compile(r'[^\x00-\x7F]')
pattern = re.compile(r"[^\x00-\x7F]")
return not bool(pattern.search(text))
def convert_dict_to_form_string(dic: dict) -> str:
# 将字典转换为表单字符串
return '&'.join([f'{k}={v}' for k, v in dic.items()])
return "&".join([f"{k}={v}" for k, v in dic.items()])
def log_plaintext(text: str) -> str:
if (text.startswith('{') and text.endswith('}')):
if text.startswith("{") and text.endswith("}"):
try:
text = json.loads(text)
except:
pass
elif (text.startswith('<xml') and text.endswith('>')): # xml data
elif text.startswith("<xml") and text.endswith(">"): # xml data
try:
text = f'xml: {utils.load_xml(text)}'
text = f"xml: {utils.load_xml(text)}"
except:
pass
return text
# 内置的UA列表
ua_list = [ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0 uacq',
'Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 uacq',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'
]
ua_list = [
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0 uacq",
"Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 uacq",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
]
# 日志记录器
logger = log.log('http_utils')
logger = log.log("http_utils")
def request(url: str, options = {}) -> requests.Response:
'''
def request(url: str, options={}) -> requests.Response:
"""
Http请求主函数, 用于发送网络请求
- url: 需要请求的URL地址(必填)
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
@ -84,15 +91,15 @@ def request(url: str, options = {}) -> requests.Response:
- no-cache: 不缓存
- <int>: 缓存可用秒数
- cache-ignore: <list> 缓存忽略关键字
@ return: requests.Response类型的响应数据
'''
"""
# 缓存读取
cache_key = f'{url}{options}'
if (isinstance(options.get('cache-ignore'), list)):
for i in options.get('cache-ignore'):
cache_key = cache_key.replace(str(i), '')
options.pop('cache-ignore')
cache_key = f"{url}{options}"
if isinstance(options.get("cache-ignore"), list):
for i in options.get("cache-ignore"):
cache_key = cache_key.replace(str(i), "")
options.pop("cache-ignore")
cache_key = utils.createMD5(cache_key)
if options.get("cache") and options["cache"] != "no-cache":
cache = config.getCache("httpx", cache_key)
@ -104,77 +111,84 @@ def request(url: str, options = {}) -> requests.Response:
options.pop("cache")
else:
cache_info = None
# 获取请求方法没有则默认为GET请求
try:
method = options['method'].upper()
options.pop('method')
method = options["method"].upper()
options.pop("method")
except Exception as e:
method = 'GET'
method = "GET"
# 获取User-Agent没有则从ua_list中随机选择一个
try:
d_lower = {k.lower(): v for k, v in options['headers'].items()}
useragent = d_lower['user-agent']
d_lower = {k.lower(): v for k, v in options["headers"].items()}
useragent = d_lower["user-agent"]
except:
try:
options['headers']['User-Agent'] = random.choice(ua_list)
options["headers"]["User-Agent"] = random.choice(ua_list)
except:
options['headers'] = {}
options['headers']['User-Agent'] = random.choice(ua_list)
options["headers"] = {}
options["headers"]["User-Agent"] = random.choice(ua_list)
# 检查是否在国内
if ((not variable.iscn) and (not options["headers"].get("X-Forwarded-For"))):
if (not variable.iscn) and (not options["headers"].get("X-Forwarded-For")):
options["headers"]["X-Forwarded-For"] = variable.fakeip
# 获取请求主函数
try:
reqattr = getattr(requests, method.lower())
except AttributeError:
raise AttributeError('Unsupported method: '+method)
raise AttributeError("Unsupported method: " + method)
# 请求前记录
logger.debug(f'HTTP Request: {url}\noptions: {options}')
logger.debug(f"HTTP Request: {url}\noptions: {options}")
# 转换body/form参数为原生的data参数并为form请求追加Content-Type头
if (method == 'POST') or (method == 'PUT'):
if options.get('body'):
options['data'] = options['body']
options.pop('body')
if options.get('form'):
options['data'] = convert_dict_to_form_string(options['form'])
options.pop('form')
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
if (isinstance(options['data'], dict)):
options['data'] = json.dumps(options['data'])
if (method == "POST") or (method == "PUT"):
if options.get("body"):
options["data"] = options["body"]
options.pop("body")
if options.get("form"):
options["data"] = convert_dict_to_form_string(options["form"])
options.pop("form")
options["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
if isinstance(options["data"], dict):
options["data"] = json.dumps(options["data"])
# 进行请求
try:
logger.info("-----start----- " + url)
req = reqattr(url, **options)
except Exception as e:
logger.error(f'HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}')
logger.error(f"HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}")
raise e
# 请求后记录
logger.debug(f'Request to {url} succeed with code {req.status_code}')
if (req.content.startswith(b'\x78\x9c') or req.content.startswith(b'\x78\x01')): # zlib headers
logger.debug(f"Request to {url} succeed with code {req.status_code}")
if req.content.startswith(b"\x78\x9c") or req.content.startswith(b"\x78\x01"): # zlib headers
try:
decompressed = zlib.decompress(req.content)
if (is_valid_utf8(decompressed)):
if is_valid_utf8(decompressed):
logger.debug(log_plaintext(decompressed.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
except:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
else:
if (is_valid_utf8(req.content)):
if is_valid_utf8(req.content):
logger.debug(log_plaintext(req.content.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
# 缓存写入
if (cache_info and cache_info != "no-cache"):
if cache_info and cache_info != "no-cache":
cache_data = pickle.dumps(req)
expire_time = (cache_info if isinstance(cache_info, int) else 3600) + int(time.time())
config.updateCache("httpx", cache_key, {"expire": True, "time": expire_time, "data": utils.createBase64Encode(cache_data)})
expire_time = cache_info if isinstance(cache_info, int) else 3600
expire_at = int((time.time()) + expire_time)
config.updateCache(
"httpx",
cache_key,
{"expire": True, "time": expire_at, "data": utils.createBase64Encode(cache_data)},
expire_time,
)
logger.debug("缓存已更新: " + url)
def _json():
return json.loads(req.content)
setattr(req, 'json', _json)
setattr(req, "json", _json)
# 返回请求
return req
@ -184,22 +198,25 @@ def checkcn():
req = request("https://mips.kugou.com/check/iscn?&format=json")
body = utils.CreateObject(req.json())
variable.iscn = bool(body.flag)
if (not variable.iscn):
variable.fakeip = config.read_config('common.fakeip')
if not variable.iscn:
variable.fakeip = config.read_config("common.fakeip")
logger.info(f"您在非中国大陆服务器({body.country})上启动了项目已自动开启ip伪装")
logger.warning("此方式无法解决咪咕音乐的链接获取问题,您可以配置代理,服务器地址可在下方链接中找到\nhttps://hidemy.io/cn/proxy-list/?country=CN#list")
logger.warning(
"此方式无法解决咪咕音乐的链接获取问题,您可以配置代理,服务器地址可在下方链接中找到\nhttps://hidemy.io/cn/proxy-list/?country=CN#list"
)
except Exception as e:
logger.warning('检查服务器位置失败,已忽略')
logger.warning("检查服务器位置失败,已忽略")
logger.warning(traceback.format_exc())
class ClientResponse:
# 这个类为了方便aiohttp响应与requests响应的跨类使用也为了解决pickle无法缓存的问题
def __init__(self, status, content, headers):
self.status = status
self.content = content
self.headers = headers
self.text = content.decode("utf-8", errors='ignore')
self.text = content.decode("utf-8", errors="ignore")
def json(self):
return json.loads(self.content)
@ -208,11 +225,12 @@ async def convert_to_requests_response(aiohttp_response) -> ClientResponse:
content = await aiohttp_response.content.read() # 从aiohttp响应中读取字节数据
status_code = aiohttp_response.status # 获取状态码
headers = dict(aiohttp_response.headers.items()) # 获取标头信息并转换为字典
return ClientResponse(status_code, content, headers)
async def AsyncRequest(url, options = {}) -> ClientResponse:
'''
async def AsyncRequest(url, options={}) -> ClientResponse:
"""
Http异步请求主函数, 用于发送网络请求
- url: 需要请求的URL地址(必填)
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
@ -224,17 +242,17 @@ async def AsyncRequest(url, options = {}) -> ClientResponse:
- no-cache: 不缓存
- <int>: 缓存可用秒数
- cache-ignore: <list> 缓存忽略关键字
@ return: common.Httpx.ClientResponse类型的响应数据
'''
if (not variable.aioSession):
"""
if not variable.aioSession:
variable.aioSession = aiohttp.ClientSession(trust_env=True)
# 缓存读取
cache_key = f'{url}{options}'
if (isinstance(options.get('cache-ignore'), list)):
for i in options.get('cache-ignore'):
cache_key = cache_key.replace(str(i), '')
options.pop('cache-ignore')
cache_key = f"{url}{options}"
if isinstance(options.get("cache-ignore"), list):
for i in options.get("cache-ignore"):
cache_key = cache_key.replace(str(i), "")
options.pop("cache-ignore")
cache_key = utils.createMD5(cache_key)
if options.get("cache") and options["cache"] != "no-cache":
cache = config.getCache("httpx_async", cache_key)
@ -247,76 +265,81 @@ async def AsyncRequest(url, options = {}) -> ClientResponse:
options.pop("cache")
else:
cache_info = None
# 获取请求方法没有则默认为GET请求
try:
method = options['method']
options.pop('method')
method = options["method"]
options.pop("method")
except Exception as e:
method = 'GET'
method = "GET"
# 获取User-Agent没有则从ua_list中随机选择一个
try:
d_lower = {k.lower(): v for k, v in options['headers'].items()}
useragent = d_lower['user-agent']
d_lower = {k.lower(): v for k, v in options["headers"].items()}
useragent = d_lower["user-agent"]
except:
try:
options['headers']['User-Agent'] = random.choice(ua_list)
options["headers"]["User-Agent"] = random.choice(ua_list)
except:
options['headers'] = {}
options['headers']['User-Agent'] = random.choice(ua_list)
options["headers"] = {}
options["headers"]["User-Agent"] = random.choice(ua_list)
# 检查是否在国内
if ((not variable.iscn) and (not options["headers"].get("X-Forwarded-For"))):
if (not variable.iscn) and (not options["headers"].get("X-Forwarded-For")):
options["headers"]["X-Forwarded-For"] = variable.fakeip
# 获取请求主函数
try:
reqattr = getattr(variable.aioSession, method.lower())
except AttributeError:
raise AttributeError('Unsupported method: '+method)
raise AttributeError("Unsupported method: " + method)
# 请求前记录
logger.debug(f'HTTP Request: {url}\noptions: {options}')
logger.debug(f"HTTP Request: {url}\noptions: {options}")
# 转换body/form参数为原生的data参数并为form请求追加Content-Type头
if (method == 'POST') or (method == 'PUT'):
if (options.get('body') is not None):
options['data'] = options['body']
options.pop('body')
if (options.get('form') is not None):
options['data'] = convert_dict_to_form_string(options['form'])
options.pop('form')
options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
if (isinstance(options.get('data'), dict)):
options['data'] = json.dumps(options['data'])
if (method == "POST") or (method == "PUT"):
if options.get("body") is not None:
options["data"] = options["body"]
options.pop("body")
if options.get("form") is not None:
options["data"] = convert_dict_to_form_string(options["form"])
options.pop("form")
options["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
if isinstance(options.get("data"), dict):
options["data"] = json.dumps(options["data"])
# 进行请求
try:
logger.info("-----start----- " + url)
req_ = await reqattr(url, **options)
except Exception as e:
logger.error(f'HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}')
logger.error(f"HTTP Request runs into an Error: {log.highlight_error(traceback.format_exc())}")
raise e
# 请求后记录
logger.debug(f'Request to {url} succeed with code {req_.status}')
logger.debug(f"Request to {url} succeed with code {req_.status}")
# 为懒人提供的不用改代码移植的方法
# 才不是梓澄呢
req = await convert_to_requests_response(req_)
if (req.content.startswith(b'\x78\x9c') or req.content.startswith(b'\x78\x01')): # zlib headers
if req.content.startswith(b"\x78\x9c") or req.content.startswith(b"\x78\x01"): # zlib headers
try:
decompressed = zlib.decompress(req.content)
if (is_valid_utf8(decompressed)):
if is_valid_utf8(decompressed):
logger.debug(log_plaintext(decompressed.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
except:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
else:
if (is_valid_utf8(req.content)):
if is_valid_utf8(req.content):
logger.debug(log_plaintext(req.content.decode("utf-8")))
else:
logger.debug('response is not text binary, ignore logging it')
logger.debug("response is not text binary, ignore logging it")
# 缓存写入
if (cache_info and cache_info != "no-cache"):
if cache_info and cache_info != "no-cache":
cache_data = pickle.dumps(req)
expire_time = (cache_info if isinstance(cache_info, int) else 3600) + int(time.time())
config.updateCache("httpx_async", cache_key, {"expire": True, "time": expire_time, "data": utils.createBase64Encode(cache_data)})
expire_time = cache_info if isinstance(cache_info, int) else 3600
expire_at = int((time.time()) + expire_time)
config.updateCache(
"httpx_async",
cache_key,
{"expire": True, "time": expire_at, "data": utils.createBase64Encode(cache_data)},
expire_time,
)
logger.debug("缓存已更新: " + url)
# 返回请求
return req
return req

View File

@ -19,33 +19,52 @@ from . import variable
from .log import log
from . import default_config
import threading
import redis
logger = log('config_manager')
logger = log("config_manager")
# 创建线程本地存储对象
local_data = threading.local()
local_cache = threading.local()
local_redis = threading.local()
def get_data_connection():
# 检查线程本地存储对象是否存在连接对象,如果不存在则创建一个新的连接对象
if (not hasattr(local_data, 'connection')):
local_data.connection = sqlite3.connect('./config/data.db')
return local_data.connection
# 创建线程本地存储对象
local_cache = threading.local()
def get_cache_connection():
# 检查线程本地存储对象是否存在连接对象,如果不存在则创建一个新的连接对象
if not hasattr(local_cache, 'connection'):
local_cache.connection = sqlite3.connect('./cache.db')
return local_cache.connection
def get_redis_connection():
return local_redis.connection
def handle_connect_db():
try:
local_data.connection = sqlite3.connect("./config/data.db")
if read_config("common.cache.adapter") == "redis":
host = read_config("common.cache.redis.host")
port = read_config("common.cache.redis.port")
user = read_config("common.cache.redis.user")
password = read_config("common.cache.redis.password")
db = read_config("common.cache.redis.db")
client = redis.Redis(host=host, port=port, username=user, password=password, db=db)
if not client.ping():
raise
local_redis.connection = client
else:
local_cache.connection = sqlite3.connect("./cache.db")
except:
logger.error("连接数据库失败")
sys.exit(1)
class ConfigReadException(Exception):
pass
yaml = yaml_.YAML()
default_str = default_config.default
default = yaml.load(default_str)
@ -54,10 +73,10 @@ default = yaml.load(default_str)
def handle_default_config():
with open("./config/config.yml", "w", encoding="utf-8") as f:
f.write(default_str)
if (not os.getenv('build')):
logger.info('首次启动或配置文件被删除,已创建默认配置文件')
if not os.getenv("build"):
logger.info(
f'\n建议您到{variable.workdir + os.path.sep}config.yml修改配置后重新启动服务器')
f"首次启动或配置文件被删除,已创建默认配置文件\n建议您到{variable.workdir + os.path.sep}config.yml修改配置后重新启动服务器"
)
return default
@ -98,8 +117,7 @@ def save_data(config_data):
# Insert the new configuration data into the 'data' table
for key, value in config_data.items():
cursor.execute(
"INSERT INTO data (key, value) VALUES (?, ?)", (key, json.dumps(value)))
cursor.execute("INSERT INTO data (key, value) VALUES (?, ?)", (key, json.dumps(value)))
conn.commit()
@ -108,51 +126,69 @@ def save_data(config_data):
logger.error(traceback.format_exc())
def handleBuildRedisKey(module, key):
prefix = read_config("common.cache.redis.key_prefix")
return f"{prefix}:{module}:{key}"
def getCache(module, key):
try:
# 连接到数据库(如果数据库不存在,则会自动创建)
conn = get_cache_connection()
# 创建一个游标对象
cursor = conn.cursor()
cursor.execute("SELECT data FROM cache WHERE module=? AND key=?",
(module, key))
result = cursor.fetchone()
if result:
cache_data = json.loads(result[0])
cache_data["time"] = int(cache_data["time"])
if (not cache_data['expire']):
return cache_data
if (int(time.time()) < int(cache_data['time'])):
if read_config("common.cache.adapter") == "redis":
redis = get_redis_connection()
key = handleBuildRedisKey(module, key)
result = redis.get(key)
if result:
cache_data = json.loads(result)
return cache_data
else:
# 连接到数据库(如果数据库不存在,则会自动创建)
conn = get_cache_connection()
# 创建一个游标对象
cursor = conn.cursor()
cursor.execute("SELECT data FROM cache WHERE module=? AND key=?", (module, key))
result = cursor.fetchone()
if result:
cache_data = json.loads(result[0])
cache_data["time"] = int(cache_data["time"])
if not cache_data["expire"]:
return cache_data
if int(time.time()) < int(cache_data["time"]):
return cache_data
except:
pass
# traceback.print_exc()
return False
return None
def updateCache(module, key, data):
def updateCache(module, key, data, expire=None):
try:
# 连接到数据库(如果数据库不存在,则会自动创建)
conn = get_cache_connection()
# 创建一个游标对象
cursor = conn.cursor()
cursor.execute(
"SELECT data FROM cache WHERE module=? AND key=?", (module, key))
result = cursor.fetchone()
if result:
cursor.execute(
"UPDATE cache SET data = ? WHERE module = ? AND key = ?", (json.dumps(data), module, key))
if read_config("common.cache.adapter") == "redis":
redis = get_redis_connection()
key = handleBuildRedisKey(module, key)
redis.set(key, json.dumps(data), ex=expire if expire and expire > 0 else None)
else:
cursor.execute(
"INSERT INTO cache (module, key, data) VALUES (?, ?, ?)", (module, key, json.dumps(data)))
conn.commit()
# 连接到数据库(如果数据库不存在,则会自动创建)
conn = get_cache_connection()
# 创建一个游标对象
cursor = conn.cursor()
cursor.execute("SELECT data FROM cache WHERE module=? AND key=?", (module, key))
result = cursor.fetchone()
if result:
cursor.execute(
"UPDATE cache SET data = ? WHERE module = ? AND key = ?", (json.dumps(data), module, key)
)
else:
cursor.execute(
"INSERT INTO cache (module, key, data) VALUES (?, ?, ?)", (module, key, json.dumps(data))
)
conn.commit()
except:
logger.error('缓存写入遇到错误…')
logger.error("缓存写入遇到错误…")
logger.error(traceback.format_exc())
@ -160,13 +196,13 @@ def resetRequestTime(ip):
config_data = load_data()
try:
try:
config_data['requestTime'][ip] = 0
config_data["requestTime"][ip] = 0
except KeyError:
config_data['requestTime'] = {}
config_data['requestTime'][ip] = 0
config_data["requestTime"] = {}
config_data["requestTime"][ip] = 0
save_data(config_data)
except:
logger.error('配置写入遇到错误…')
logger.error("配置写入遇到错误…")
logger.error(traceback.format_exc())
@ -174,20 +210,20 @@ def updateRequestTime(ip):
try:
config_data = load_data()
try:
config_data['requestTime'][ip] = time.time()
config_data["requestTime"][ip] = time.time()
except KeyError:
config_data['requestTime'] = {}
config_data['requestTime'][ip] = time.time()
config_data["requestTime"] = {}
config_data["requestTime"][ip] = time.time()
save_data(config_data)
except:
logger.error('配置写入遇到错误...')
logger.error("配置写入遇到错误...")
logger.error(traceback.format_exc())
def getRequestTime(ip):
config_data = load_data()
try:
value = config_data['requestTime'][ip]
value = config_data["requestTime"][ip]
except:
value = 0
return value
@ -195,7 +231,7 @@ def getRequestTime(ip):
def read_data(key):
config = load_data()
keys = key.split('.')
keys = key.split(".")
value = config
for k in keys:
if k not in value and keys.index(k) != len(keys) - 1:
@ -210,7 +246,7 @@ def read_data(key):
def write_data(key, value):
config = load_data()
keys = key.split('.')
keys = key.split(".")
current = config
for k in keys[:-1]:
if k not in current:
@ -225,7 +261,7 @@ def write_data(key, value):
def push_to_list(key, obj):
config = load_data()
keys = key.split('.')
keys = key.split(".")
current = config
for k in keys[:-1]:
if k not in current:
@ -242,10 +278,10 @@ def push_to_list(key, obj):
def write_config(key, value):
config = None
with open('./config/config.yml', 'r', encoding='utf-8') as f:
with open("./config/config.yml", "r", encoding="utf-8") as f:
config = yaml_.YAML().load(f)
keys = key.split('.')
keys = key.split(".")
current = config
for k in keys[:-1]:
if k not in current:
@ -260,14 +296,14 @@ def write_config(key, value):
y.preserve_blank_lines = True
# 写入配置并保留注释和空行
with open('./config/config.yml', 'w', encoding='utf-8') as f:
with open("./config/config.yml", "w", encoding="utf-8") as f:
y.dump(config, f)
def read_default_config(key):
try:
config = default
keys = key.split('.')
keys = key.split(".")
value = config
for k in keys:
if isinstance(value, dict):
@ -288,7 +324,7 @@ def read_default_config(key):
def _read_config(key):
try:
config = variable.config
keys = key.split('.')
keys = key.split(".")
value = config
for k in keys:
if isinstance(value, dict):
@ -309,7 +345,7 @@ def _read_config(key):
def read_config(key):
try:
config = variable.config
keys = key.split('.')
keys = key.split(".")
value = config
for k in keys:
if isinstance(value, dict):
@ -325,23 +361,23 @@ def read_config(key):
return value
except:
default_value = read_default_config(key)
if (isinstance(default_value, type(None))):
logger.warning(f'配置文件{key}不存在')
if isinstance(default_value, type(None)):
logger.warning(f"配置文件{key}不存在")
else:
for i in range(len(keys)):
tk = '.'.join(keys[:(i + 1)])
tk = ".".join(keys[: (i + 1)])
tkvalue = _read_config(tk)
logger.debug(f'configfix: 读取配置文件{tk}的值:{tkvalue}')
if ((tkvalue is None) or (tkvalue == {})):
logger.debug(f"configfix: 读取配置文件{tk}的值:{tkvalue}")
if (tkvalue is None) or (tkvalue == {}):
write_config(tk, read_default_config(tk))
logger.info(f'配置文件{tk}不存在,已创建')
logger.info(f"配置文件{tk}不存在,已创建")
return default_value
def write_data(key, value):
config = load_data()
keys = key.split('.')
keys = key.split(".")
current = config
for k in keys[:-1]:
if k not in current:
@ -353,26 +389,26 @@ def write_data(key, value):
save_data(config)
def initConfig():
if (not os.path.exists('./config')):
os.mkdir('config')
if (os.path.exists('./config.json')):
shutil.move('config.json','./config')
if (os.path.exists('./data.db')):
shutil.move('./data.db','./config')
if (os.path.exists('./config/config.json')):
os.rename('./config/config.json', './config/config.json.bak')
def init_config():
if not os.path.exists("./config"):
os.mkdir("config")
if os.path.exists("./config.json"):
shutil.move("config.json", "./config")
if os.path.exists("./data.db"):
shutil.move("./data.db", "./config")
if os.path.exists("./config/config.json"):
os.rename("./config/config.json", "./config/config.json.bak")
handle_default_config()
logger.warning('json配置文件已不再使用已将其重命名为config.json.bak')
logger.warning('配置文件不会自动更新(因为变化太大),请手动修改配置文件重启服务器')
logger.warning("json配置文件已不再使用已将其重命名为config.json.bak")
logger.warning("配置文件不会自动更新(因为变化太大),请手动修改配置文件重启服务器")
sys.exit(0)
try:
with open("./config/config.yml", "r", encoding="utf-8") as f:
try:
variable.config = yaml.load(f.read())
if (not isinstance(variable.config, dict)):
logger.warning('配置文件并不是一个有效的字典,使用默认值')
if not isinstance(variable.config, dict):
logger.warning("配置文件并不是一个有效的字典,使用默认值")
variable.config = default
with open("./config/config.yml", "w", encoding="utf-8") as f:
yaml.dump(variable.config, f)
@ -386,125 +422,133 @@ def initConfig():
except FileNotFoundError:
variable.config = handle_default_config()
# print(variable.config)
variable.log_length_limit = read_config('common.log_length_limit')
variable.debug_mode = read_config('common.debug_mode')
variable.log_length_limit = read_config("common.log_length_limit")
variable.debug_mode = read_config("common.debug_mode")
logger.debug("配置文件加载成功")
conn = sqlite3.connect('./cache.db')
# 尝试连接数据库
handle_connect_db()
conn = sqlite3.connect("./cache.db")
# 创建一个游标对象
cursor = conn.cursor()
# 创建一个表来存储缓存数据
cursor.execute('''CREATE TABLE IF NOT EXISTS cache
cursor.execute(
"""CREATE TABLE IF NOT EXISTS cache
(id INTEGER PRIMARY KEY AUTOINCREMENT,
module TEXT NOT NULL,
key TEXT NOT NULL,
data TEXT NOT NULL)''')
data TEXT NOT NULL)"""
)
conn.close()
conn2 = sqlite3.connect('./config/data.db')
conn2 = sqlite3.connect("./config/data.db")
# 创建一个游标对象
cursor2 = conn2.cursor()
cursor2.execute('''CREATE TABLE IF NOT EXISTS data
cursor2.execute(
"""CREATE TABLE IF NOT EXISTS data
(key TEXT PRIMARY KEY,
value TEXT)''')
value TEXT)"""
)
conn2.close()
logger.debug('数据库初始化成功')
logger.debug("数据库初始化成功")
# handle data
all_data_keys = {'banList': [], 'requestTime': {}, 'banListRaw': []}
all_data_keys = {"banList": [], "requestTime": {}, "banListRaw": []}
data = load_data()
if (data == {}):
write_data('banList', [])
write_data('requestTime', {})
logger.info('数据库内容为空,已写入默认值')
if data == {}:
write_data("banList", [])
write_data("requestTime", {})
logger.info("数据库内容为空,已写入默认值")
for k, v in all_data_keys.items():
if (k not in data):
if k not in data:
write_data(k, v)
logger.info(f'数据库中不存在{k},已创建')
logger.info(f"数据库中不存在{k},已创建")
# 处理代理配置
if (read_config('common.proxy.enable')):
if (read_config('common.proxy.http_value')):
os.environ['http_proxy'] = read_config('common.proxy.http_value')
logger.info('HTTP协议代理地址: ' +
read_config('common.proxy.http_value'))
if (read_config('common.proxy.https_value')):
os.environ['https_proxy'] = read_config('common.proxy.https_value')
logger.info('HTTPS协议代理地址: ' +
read_config('common.proxy.https_value'))
logger.info('代理功能已开启,请确保代理地址正确,否则无法连接网络')
if read_config("common.proxy.enable"):
if read_config("common.proxy.http_value"):
os.environ["http_proxy"] = read_config("common.proxy.http_value")
logger.info("HTTP协议代理地址: " + read_config("common.proxy.http_value"))
if read_config("common.proxy.https_value"):
os.environ["https_proxy"] = read_config("common.proxy.https_value")
logger.info("HTTPS协议代理地址: " + read_config("common.proxy.https_value"))
logger.info("代理功能已开启,请确保代理地址正确,否则无法连接网络")
# cookie池
if (read_config('common.cookiepool')):
logger.info('已启用cookie池功能请确定配置的cookie都能正确获取链接')
logger.info('传统的源 - 单用户cookie配置将被忽略')
logger.info('所以即使某个源你只有一个cookie也请填写到cookiepool对应的源中否则将无法使用该cookie')
if read_config("common.cookiepool"):
logger.info("已启用cookie池功能请确定配置的cookie都能正确获取链接")
logger.info("传统的源 - 单用户cookie配置将被忽略")
logger.info("所以即使某个源你只有一个cookie也请填写到cookiepool对应的源中否则将无法使用该cookie")
variable.use_cookie_pool = True
# 移除已经过期的封禁数据
banlist = read_data('banList')
banlistRaw = read_data('banListRaw')
banlist = read_data("banList")
banlistRaw = read_data("banListRaw")
count = 0
for b in banlist:
if (b['expire'] and (time.time() > b['expire_time'])):
if b["expire"] and (time.time() > b["expire_time"]):
count += 1
banlist.remove(b)
if (b['ip'] in banlistRaw):
banlistRaw.remove(b['ip'])
write_data('banList', banlist)
write_data('banListRaw', banlistRaw)
if (count != 0):
logger.info(f'已移除{count}条过期封禁数据')
if b["ip"] in banlistRaw:
banlistRaw.remove(b["ip"])
write_data("banList", banlist)
write_data("banListRaw", banlistRaw)
if count != 0:
logger.info(f"已移除{count}条过期封禁数据")
# 处理旧版数据库的banListRaw
banlist = read_data('banList')
banlistRaw = read_data('banListRaw')
if (banlist != [] and banlistRaw == []):
banlist = read_data("banList")
banlistRaw = read_data("banListRaw")
if banlist != [] and banlistRaw == []:
for b in banlist:
banlistRaw.append(b['ip'])
banlistRaw.append(b["ip"])
return
def ban_ip(ip_addr, ban_time=-1):
if read_config('security.banlist.enable'):
banList = read_data('banList')
banList.append({
'ip': ip_addr,
'expire': read_config('security.banlist.expire.enable'),
'expire_time': read_config('security.banlist.expire.length') if (ban_time == -1) else ban_time,
})
write_data('banList', banList)
banListRaw = read_data('banListRaw')
if (ip_addr not in banListRaw):
if read_config("security.banlist.enable"):
banList = read_data("banList")
banList.append(
{
"ip": ip_addr,
"expire": read_config("security.banlist.expire.enable"),
"expire_time": read_config("security.banlist.expire.length") if (ban_time == -1) else ban_time,
}
)
write_data("banList", banList)
banListRaw = read_data("banListRaw")
if ip_addr not in banListRaw:
banListRaw.append(ip_addr)
write_data('banListRaw', banListRaw)
write_data("banListRaw", banListRaw)
else:
if (variable.banList_suggest < 10):
if variable.banList_suggest < 10:
variable.banList_suggest += 1
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
logger.warning("黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求")
def check_ip_banned(ip_addr):
if read_config('security.banlist.enable'):
banList = read_data('banList')
banlistRaw = read_data('banListRaw')
if (ip_addr in banlistRaw):
if read_config("security.banlist.enable"):
banList = read_data("banList")
banlistRaw = read_data("banListRaw")
if ip_addr in banlistRaw:
for b in banList:
if (b['ip'] == ip_addr):
if (b['expire']):
if (b['expire_time'] > int(time.time())):
if b["ip"] == ip_addr:
if b["expire"]:
if b["expire_time"] > int(time.time()):
return True
else:
banList.remove(b)
banlistRaw.remove(b['ip'])
write_data('banListRaw', banlistRaw)
write_data('banList', banList)
banlistRaw.remove(b["ip"])
write_data("banListRaw", banlistRaw)
write_data("banList", banList)
return False
else:
return True
@ -514,10 +558,10 @@ def check_ip_banned(ip_addr):
else:
return False
else:
if (variable.banList_suggest <= 10):
if variable.banList_suggest <= 10:
variable.banList_suggest += 1
logger.warning('黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求')
logger.warning("黑名单功能已被关闭,我们墙裂建议你开启这个功能以防止恶意请求")
return False
initConfig()
init_config()

View File

@ -51,6 +51,18 @@ common:
local_music: # 服务器侧本地音乐相关配置,如果需要使用此功能请确保你的带宽足够
audio_path: ./audio
temp_path: ./temp
# 缓存配置
cache:
# 适配器 [redis,sql]
adapter: sql
# redis 配置
redis:
host: 127.0.0.1
port: 6379
db: 0
user: ""
password: ""
key_prefix: "LXAPISERVER"
security:
rate_limit: # 请求速率限制 填入的值为至少间隔多久才能进行一次请求单位不限制请填为0
@ -84,6 +96,7 @@ security:
module:
kg: # 酷狗音乐相关配置
enable: true # 是否开启本平台服务
client: # 客户端请求配置,不懂请保持默认,修改请统一为字符串格式
appid: "1005" # 酷狗音乐的appid官方安卓为1005官方PC为1001
signatureKey: OIlwieks28dk2k092lksi2UIkp # 客户端signature采用的key值需要与appid对应
@ -107,34 +120,38 @@ module:
interval: 86400
mixsongmid: # mix_songmid的获取方式, 默认auto, 可以改成一个数字手动
value: auto
refresh_token: # 酷狗token保活相关配置30天不刷新token会失效enable是否启动interval刷新间隔。默认appid=1005时有效3116需要更换signatureKey
refresh_login: # 酷狗token保活相关配置30天不刷新token会失效enable是否启动interval刷新间隔。默认appid=1005时有效3116需要更换signatureKey
enable: false
interval: 86000
login_url: http://login.user.kugou.com/v4/login_by_token
tx: # QQ音乐相关配置
enable: true # 是否开启本平台服务
vkeyserver: # 请求官方api时使用的guiduin等信息不需要与cookie中信息一致
guid: "114514"
uin: "10086"
user: # 用户数据可以通过浏览器获取需要vip账号来获取会员歌曲如果没有请留为空值qqmusic_key可以从Cookie中/客户端的请求体中comm.authst获取
qqmusic_key: ""
uin: "" # key对应的QQ号
refresh_key: "" # 刷新失活 qqmusic_key
refresh_login: # 刷新登录相关配置enable是否启动interval刷新间隔
enable: false
interval: 86000
cdnaddr: http://ws.stream.qqmusic.qq.com/
vkey_api: # 第三方Vkey获取API
use_vkey_api: false
vkey_api_url: "xxx"
wy: # 网易云音乐相关配置
wy: # 网易云音乐相关配置, proto支持值: ['offcial', 'ncmapi']
enable: true # 是否开启本平台服务
proto: offcial
user:
cookie: "" # 账号cookie数据可以通过浏览器获取需要vip账号来获取会员歌曲如果没有请留为空值
refresh_login:
enable: false
interval: 86400
ncmapi:
api_url: "" # NeteaseCloudMusicApi的URL, 自行参考https://gitlab.com/Binaryify/neteasecloudmusicapi搭建
mg: # 咪咕音乐相关配置
enable: true # 是否开启本平台服务
user: # 研究不深后两项自行抓包获取网页端cookie
by: ""
session: ""
@ -144,6 +161,7 @@ module:
interval: 86400
kw: # 酷我音乐相关配置proto支持值['bd-api', 'kuwodes']
enable: true # 是否开启本平台服务
proto: bd-api
user:
uid: "0"
@ -192,6 +210,7 @@ module:
tx:
- qqmusic_key: ""
uin: ""
refresh_key: ""
refresh_login: # cookie池中对于此账号刷新登录的配置账号间互不干扰
enable: false
interval: 86000

View File

@ -8,36 +8,34 @@
# This file is part of the "lx-music-api-server" project.
import os as _os
import ujson as _json
import ruamel.yaml as _yaml
yaml = _yaml.YAML()
def _read_config_file():
try:
with open("./config/config.json", "r", encoding="utf-8") as f:
return _json.load(f)
with open(f"./config/config.yml", "r", encoding="utf-8") as f:
return yaml.load(f.read())
except:
return {}
return []
def _read_config(key):
try:
config = _read_config_file()
keys = key.split('.')
value = config
for k in keys:
if isinstance(value, dict):
if k not in value and keys.index(k) != len(keys) - 1:
value[k] = {}
elif k not in value and keys.index(k) == len(keys) - 1:
value = None
value = value[k]
else:
config = _read_config_file()
keys = key.split('.')
value = config
for k in keys:
if isinstance(value, dict):
if k not in value and keys.index(k) != len(keys) - 1:
value[k] = []
elif k not in value and keys.index(k) == len(keys) - 1:
value = None
break
return value
except:
return None
value = value[k]
else:
value = None
break
return value
_dm = _read_config("common.debug_mode")

View File

@ -1,15 +0,0 @@
version: "3.8"
services:
lx:
container_name: lx-server
build: .
ports:
- "9763:9763"
volumes:
- .:/app
environment:
TZ: 'Asia/Shanghai'
restart: always
networks:
default:

10
main.py
View File

@ -71,7 +71,7 @@ async def handle_before_request(app, handler):
try:
if config.read_config("common.reverse_proxy.allow_proxy") and request.headers.get(
config.read_config("common.reverse_proxy.real_ip_header")):
if not config.read_config("common.reverse_proxy.allow_public_ip") or utils.is_local_ip(request.remote):
if not (config.read_config("common.reverse_proxy.allow_public_ip") or utils.is_local_ip(request.remote)):
return handleResult({"code": 1, "msg": "不允许的公网ip转发", "data": None}, 403)
# proxy header
request.remote_addr = request.headers.get(config.read_config("common.reverse_proxy.real_ip_header"))
@ -150,6 +150,14 @@ async def handle(request):
try:
query = dict(request.query)
if (method in dir(modules)):
source_enable = config.read_config(f'module.{source}.enable')
if not source_enable:
return handleResult({
'code': 4,
'msg': '此平台已停止服务',
'data': None,
"Your IP": request.remote_addr
}, 404)
return handleResult(await getattr(modules, method)(source, songId, quality, query))
else:
return handleResult(await modules.other(method, source, songId, quality, query))

View File

@ -11,6 +11,7 @@ from common.exceptions import FailedException
from common.utils import require
from common import log
from common import config
# 从.引入的包并没有在代码中直接使用但是是用require在请求时进行引入的不要动
from . import kw
from . import mg
@ -20,194 +21,195 @@ from . import wy
import traceback
import time
logger = log.log('api_handler')
logger = log.log("api_handler")
sourceExpirationTime = {
'tx': {
"tx": {
"expire": True,
"time": 80400, # 不知道tx为什么要取一个这么不对劲的数字当过期时长
},
'kg': {
"kg": {
"expire": True,
"time": 24 * 60 * 60, # 24 hours
},
'kw': {
"expire": True,
"time": 60 * 60 # 60 minutes
},
'wy': {
"kw": {"expire": True, "time": 60 * 60}, # 60 minutes
"wy": {
"expire": True,
"time": 20 * 60, # 20 minutes
},
'mg': {
"mg": {
"expire": False,
"time": 0,
}
},
}
async def url(source, songId, quality, query = {}):
if (not quality):
async def url(source, songId, quality, query={}):
if not quality:
return {
'code': 2,
'msg': '需要参数"quality"',
'data': None,
"code": 2,
"msg": '需要参数"quality"',
"data": None,
}
if (source == "kg"):
if source == "kg":
songId = songId.lower()
try:
cache = config.getCache('urls', f'{source}_{songId}_{quality}')
cache = config.getCache("urls", f"{source}_{songId}_{quality}")
if cache:
logger.debug(f'使用缓存的{source}_{songId}_{quality}数据URL{cache["url"]}')
return {
'code': 0,
'msg': 'success',
'data': cache['url'],
'extra': {
'cache': True,
'quality': {
'target': quality,
'result': quality,
"code": 0,
"msg": "success",
"data": cache["url"],
"extra": {
"cache": True,
"quality": {
"target": quality,
"result": quality,
},
'expire': {
"expire": {
# 在更新缓存的时候把有效期的75%作为链接可用时长,现在加回来
'time': int(cache['time'] + (sourceExpirationTime[source]['time'] * 0.25)) if cache['expire'] else None,
'canExpire': cache['expire'],
}
"time": (
int(cache["time"] + (sourceExpirationTime[source]["time"] * 0.25))
if cache["expire"]
else None
),
"canExpire": cache["expire"],
},
},
}
except:
logger.error(traceback.format_exc())
try:
func = require('modules.' + source + '.url')
func = require("modules." + source + ".url")
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
"code": 1,
"msg": "未知的源或不支持的方法",
"data": None,
}
try:
result = await func(songId, quality)
logger.info(f'获取{source}_{songId}_{quality}成功URL{result["url"]}')
canExpire = sourceExpirationTime[source]['expire']
expireTime = sourceExpirationTime[source]['time'] + int(time.time())
config.updateCache('urls', f'{source}_{songId}_{quality}', {
"expire": canExpire,
# 取有效期的75%作为链接可用时长
"time": int(expireTime - sourceExpirationTime[source]['time'] * 0.25),
"url": result['url'],
})
canExpire = sourceExpirationTime[source]["expire"]
expireTime = int(sourceExpirationTime[source]["time"] * 0.75)
expireAt = int(expireTime + time.time())
config.updateCache(
"urls",
f"{source}_{songId}_{quality}",
{
"expire": canExpire,
# 取有效期的75%作为链接可用时长
"time": expireAt,
"url": result["url"],
},
expireTime if canExpire else None,
)
logger.debug(f'缓存已更新:{source}_{songId}_{quality}, URL{result["url"]}, expire: {expireTime}')
return {
'code': 0,
'msg': 'success',
'data': result['url'],
'extra': {
'cache': False,
'quality': {
'target': quality,
'result': result['quality'],
"code": 0,
"msg": "success",
"data": result["url"],
"extra": {
"cache": False,
"quality": {
"target": quality,
"result": result["quality"],
},
'expire': {
'time': expireTime if canExpire else None,
'canExpire': canExpire,
"expire": {
"time": expireAt if canExpire else None,
"canExpire": canExpire,
},
},
}
except FailedException as e:
logger.info(f'获取{source}_{songId}_{quality}失败,原因:' + e.args[0])
logger.info(f"获取{source}_{songId}_{quality}失败,原因:" + e.args[0])
return {
'code': 2,
'msg': e.args[0],
'data': None,
"code": 2,
"msg": e.args[0],
"data": None,
}
async def lyric(source, songId, _, query):
cache = config.getCache('lyric', f'{source}_{songId}')
cache = config.getCache("lyric", f"{source}_{songId}")
if cache:
return {
'code': 0,
'msg': 'success',
'data': cache['data']
}
return {"code": 0, "msg": "success", "data": cache["data"]}
try:
func = require('modules.' + source + '.lyric')
func = require("modules." + source + ".lyric")
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
"code": 1,
"msg": "未知的源或不支持的方法",
"data": None,
}
try:
result = await func(songId)
config.updateCache('lyric', f'{source}_{songId}', {
"data": result,
"time": int(time.time() + (86400 * 3)), # 歌词缓存3天
"expire": True,
})
logger.debug(f'缓存已更新:{source}_{songId}, lyric: {result}')
return {
'code': 0,
'msg': 'success',
'data': result
}
expireTime = 86400 * 3
expireAt = int(time.time() + expireTime)
config.updateCache(
"lyric",
f"{source}_{songId}",
{
"data": result,
"time": expireAt, # 歌词缓存3天
"expire": True,
},
expireTime,
)
logger.debug(f"缓存已更新:{source}_{songId}, lyric: {result}")
return {"code": 0, "msg": "success", "data": result}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
"code": 2,
"msg": e.args[0],
"data": None,
}
async def search(source, songid, _, query):
try:
func = require('modules.' + source + '.search')
func = require("modules." + source + ".search")
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
"code": 1,
"msg": "未知的源或不支持的方法",
"data": None,
}
try:
result = await func(songid, query)
return {
'code': 0,
'msg': 'success',
'data': result
}
return {"code": 0, "msg": "success", "data": result}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
"code": 2,
"msg": e.args[0],
"data": None,
}
async def other(method, source, songid, _, query):
try:
func = require('modules.' + source + '.' + method)
func = require("modules." + source + "." + method)
except:
return {
'code': 1,
'msg': '未知的源或不支持的方法',
'data': None,
"code": 1,
"msg": "未知的源或不支持的方法",
"data": None,
}
try:
result = await func(songid)
return {
'code': 0,
'msg': 'success',
'data': result
}
return {"code": 0, "msg": "success", "data": result}
except FailedException as e:
return {
'code': 2,
'msg': e.args[0],
'data': None,
"code": 2,
"msg": e.args[0],
"data": None,
}
async def info_with_query(source, songid, _, query):
return await other('info', source, songid, None)
return await other("info", source, songid, None)

View File

@ -22,7 +22,7 @@ from common.exceptions import FailedException
from common import Httpx
from common import utils
import asyncio
from . import refresh_token
from . import refresh_login
async def info(hash_):
tasks = []

View File

@ -1,7 +1,7 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq - (feat. Huibq and ikun0014)
# - name: refresh_token.py -
# - name: refresh_login.py -
# - project: lx-music-api-server -
# - license: MIT -
# ----------------------------------------
@ -14,13 +14,13 @@ from common import log
from .utils import signRequest, tools, aes_sign
import ujson as json
logger = log.log('kg_refresh_token')
logger = log.log('kg_refresh_login')
async def refresh():
if (not config.read_config('module.kg.user.token')):
return
if (not config.read_config('module.kg.user.refresh_token.enable')):
if (not config.read_config('module.kg.user.refresh_login.enable')):
return
user_id = config.read_config('module.kg.user.userid')
@ -49,7 +49,7 @@ async def refresh():
'KG-Rec': '1',
'KG-RC': '1',
}
login_url = config.read_config('module.kg.user.refresh_token.login_url')
login_url = config.read_config('module.kg.user.refresh_login.login_url')
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
body = req.json()
if body['error_code'] != 0:
@ -87,7 +87,7 @@ async def refresh():
'KG-Rec': '1',
'KG-RC': '1',
}
login_url = config.read_config('module.kg.user.refresh_token.login_url')
login_url = config.read_config('module.kg.user.refresh_login.login_url')
req = await signRequest(login_url, params, {'method': 'POST', 'json': data, 'headers': headers})
body = req.json()
if body['error_code'] != 0:
@ -105,15 +105,15 @@ async def refresh():
if (not variable.use_cookie_pool):
kgconfig = config.read_config('module.kg')
refresh_login_info = kgconfig.get('refresh_token')
refresh_login_info = kgconfig.get('refresh_login')
if (refresh_login_info):
kgconfig['user']['refresh_token'] = refresh_login_info
kgconfig['user']['refresh_login'] = refresh_login_info
kgconfig.pop('refresh_login')
config.write_config('module.kg', kgconfig)
if (config.read_config('module.kg.user.refresh_token.enable') and not variable.use_cookie_pool):
scheduler.append('kg_refresh_token', refresh,
config.read_config('module.kg.user.refresh_token.interval'))
if (config.read_config('module.kg.user.refresh_login.enable') and not variable.use_cookie_pool):
scheduler.append('kg_refresh_login', refresh,
config.read_config('module.kg.user.refresh_login.interval'))
async def refresh_login_for_pool(user_info):
user_id = user_info["userid"]

View File

@ -1,104 +1,55 @@
# ----------------------------------------
# - mode: python -
# - author: helloplhm-qwq -
# - name: QMWSign.py -
# - project: lx-music-api-server -
# - license: MIT -
# - mode: python -
# - author: jixunmoe -
# - name: zzc_sign.py -
# - project: qmweb-sign -
# - license: MIT -
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
# This file is part of the "qmweb-sign" project.
from common.utils import createMD5
import re as _re
import sys
import re
def v(b):
res = []
p = [21, 4, 9, 26, 16, 20, 27, 30]
for x in p:
res.append(b[x])
return ''.join(res)
from hashlib import sha1
from base64 import b64encode
def c(b):
res = []
p = [18, 11, 3, 2, 1, 7, 6, 25]
for x in p:
res.append(b[x])
return ''.join(res)
PART_1_INDEXES = [23, 14, 6, 36, 16, 40, 7, 19]
PART_2_INDEXES = [16, 1, 32, 12, 19, 27, 8, 5]
SCRAMBLE_VALUES = [
89,
39,
179,
150,
218,
82,
58,
252,
177,
52,
186,
123,
120,
64,
242,
133,
143,
161,
121,
179,
]
def y(a, b, c):
e = []
r25 = a >> 2
if b is not None and c is not None:
r26 = a & 3
r26_2 = r26 << 4
r26_3 = b >> 4
r26_4 = r26_2 | r26_3
r27 = b & 15
r27_2 = r27 << 2
r27_3 = r27_2 | (c >> 6)
r28 = c & 63
e.append(r25)
e.append(r26_4)
e.append(r27_3)
e.append(r28)
else:
r10 = a >> 2
r11 = a & 3
r11_2 = r11 << 4
e.append(r10)
e.append(r11_2)
return e
PART_1_INDEXES = filter(lambda x: x < 40, PART_1_INDEXES)
def n(ls):
e = []
for i in range(0, len(ls), 3):
if i < len(ls) - 2:
e += y(ls[i], ls[i + 1], ls[i + 2])
else:
e += y(ls[i], None, None)
res = []
b64all = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
for i in e:
res.append(b64all[i])
return ''.join(res)
def t(b):
zd = {
"0": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"A": 10,
"B": 11,
"C": 12,
"D": 13,
"E": 14,
"F": 15
}
ol = [212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6]
res = []
j = 0
for i in range(0, len(b), 2):
one = zd[b[i]]
two = zd[b[i + 1]]
r = one * 16 ^ two
res.append(r ^ ol[j])
j += 1
return res
def sign(payload: str) -> str:
hash = sha1(payload.encode("utf-8")).hexdigest().upper()
def sign(params):
md5Str = createMD5(params).upper()
h = v(md5Str)
e = c(md5Str)
ls = t(md5Str)
m = n(ls)
res = 'zzb' + h + m + e
res = res.lower()
r = _re.compile(r'[\\/+]')
res = _re.sub(r, '', res)
return res
part1 = "".join(map(lambda i: hash[i], PART_1_INDEXES))
part2 = "".join(map(lambda i: hash[i], PART_2_INDEXES))
part3 = bytearray(20)
for i, v in enumerate(SCRAMBLE_VALUES):
value = v ^ int(hash[i * 2 : i * 2 + 2], 16)
part3[i] = value
b64_part = re.sub(rb"[\\/+=]", b"", b64encode(part3)).decode("utf-8")
return f"zzc{part1}{b64_part}{part2}".lower()

View File

@ -8,7 +8,7 @@
# This file is part of the "lx-music-api-server" project.
from common.exceptions import FailedException
from common import config, utils, variable, Httpx
from common import config, utils, variable
from .musicInfo import getMusicInfo
from .utils import tools
from .utils import signRequest
@ -16,52 +16,27 @@ import random
createObject = utils.CreateObject
index_map = {
'dolby': 4,
'master': 3
}
async def vkeyUrl(i, q, b):
apiNode = config.read_config("module.tx.vkey_api.vkey_api_url")
filename = b['track_info']['file']['media_mid']
if (q in index_map.keys()):
filename = b['track_info']['vs'][index_map[q]]
if (not filename): raise FailedException('未找到该音质')
filename = f"{tools.fileInfo[q]['h']}{filename}{tools.fileInfo[q]['e']}"
src = f"{tools.fileInfo[q]['h']}{i}{tools.fileInfo[q]['e']}"
url = apiNode + f'?filename={filename}&guid={config.read_config("module.tx.vkeyserver.guid")}&uin={config.read_config("module.tx.vkeyserver.uin")}&src={src}'
req = await Httpx.AsyncRequest(url)
body = req.json()
purl = body['data'][0]['purl']
return {
'url': tools.cdnaddr + purl,
'quality': q
}
async def url(songId, quality):
infoBody = await getMusicInfo(songId)
strMediaMid = infoBody['track_info']['file']['media_mid']
if (config.read_config("module.tx.vkey_api.use_vkey_api")):
return await vkeyUrl(songId, quality, infoBody)
user_info = config.read_config('module.tx.user') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.tx'))
requestBody = {
'req_0': {
'module': 'vkey.GetVkeyServer',
'method': 'CgiGetVkey',
'param': {
'filename': [f"{tools.fileInfo[quality]['h']}{strMediaMid}{tools.fileInfo[quality]['e']}"],
'guid': config.read_config('module.tx.vkeyserver.guid'),
'songmid': [songId],
'songtype': [0],
'uin': str(user_info['uin']),
'loginflag': 1,
'platform': '20',
"req": {
"module": "music.vkey.GetVkey",
"method": "UrlGetVkey",
"param": {
"filename": [f"{tools.fileInfo[quality]['h']}{strMediaMid}{tools.fileInfo[quality]['e']}"],
"guid": config.read_config("module.tx.vkeyserver.guid"),
"songmid": [songId],
"songtype": [0],
"uin": str(user_info["uin"]),
"loginflag": 1,
"platform": "20",
},
},
'comm': {
"qq": str(user_info['uin']),
"authst": user_info['qqmusic_key'],
"comm": {
"qq": str(user_info["uin"]),
"authst": user_info["qqmusic_key"],
"ct": "26",
"cv": "2010101",
"v": "2010101"
@ -69,7 +44,7 @@ async def url(songId, quality):
}
req = await signRequest(requestBody)
body = createObject(req.json())
data = body.req_0.data.midurlinfo[0]
data = body.req.data.midurlinfo[0]
url = data['purl']
if (not url):

View File

@ -7,206 +7,151 @@
# ----------------------------------------
# This file is part of the "lx-music-api-server" project.
from common import Httpx, variable
from common import scheduler
from common import config
from common import log
from common import Httpx, variable, scheduler, config, log
from .utils import sign
import ujson as json
from typing import Dict, Any, Optional
logger = log.log('qqmusic_refresh_login')
logger = log.log("qqmusic_refresh_login")
async def refresh():
if (not config.read_config('module.tx.user.qqmusic_key')):
return
if (not config.read_config('module.tx.user.refresh_login.enable')):
return
print(config.read_config('module.tx.user.qqmusic_key'))
if (config.read_config('module.tx.user.qqmusic_key').startswith('W_X')):
options = {
'method': 'POST',
'body': json.dumps({
"comm": {
"fPersonality": "0",
"tmeLoginType": "1",
"tmeLoginMethod": "1",
"qq": "",
"authst": "",
"ct": "11",
"cv": "12080008",
"v": "12080008",
"tmeAppID": "qqmusic"
},
"req1": {
"module": "music.login.LoginServer",
"method": "Login",
"param": {
"code": "",
"openid": "",
"refresh_token": "",
"str_musicid": str(config.read_config('module.tx.user.uin')),
"musickey": config.read_config('module.tx.user.qqmusic_key'),
"unionid": "",
"refresh_key": "",
"loginMode": 2
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
logger.warning('刷新登录失败, code: ' +
str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info('刷新登录成功')
config.write_config('module.tx.user.uin',
str(body['req1']['data']['musicid']))
logger.info('已通过相应数据更新uin')
config.write_config('module.tx.user.qqmusic_key',
body['req1']['data']['musickey'])
logger.info('已通过相应数据更新qqmusic_key')
elif (config.read_config('module.tx.user.qqmusic_key').startswith('Q_H_L')):
options = {
'method': 'POST',
'body': json.dumps({
'req1': {
'module': 'QQConnectLogin.LoginServer',
'method': 'QQLogin',
'param': {
'expired_in': 7776000,
'musicid': int(config.read_config('module.tx.user.uin')),
'musickey': config.read_config('module.tx.user.qqmusic_key')
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u6.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
logger.warning('刷新登录失败, code: ' +
str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info('刷新登录成功')
config.write_config('module.tx.user.uin',
str(body['req1']['data']['musicid']))
logger.info('已通过相应数据更新uin')
config.write_config('module.tx.user.qqmusic_key',
body['req1']['data']['musickey'])
logger.info('已通过相应数据更新qqmusic_key')
def _build_request_body(user_info: Dict[str, Any]) -> Dict[str, Any]:
"""构建统一请求体结构"""
return {
"comm": {
"fPersonality": "0",
"tmeLoginType": "2"
if user_info["qqmusic_key"].startswith("Q_H_L")
else "1",
"qq": str(user_info["uin"]),
"authst": user_info["qqmusic_key"],
"ct": "11",
"cv": "12080008",
"v": "12080008",
"tmeAppID": "qqmusic",
},
"req1": {
"module": "music.login.LoginServer",
"method": "Login",
"param": {
"str_musicid": str(user_info["uin"]),
"musickey": user_info["qqmusic_key"],
"refresh_key": user_info.get("refresh_key", ""),
},
},
}
async def _update_user_config(
user_info: Dict[str, Any], new_data: Dict[str, Any]
) -> None:
"""统一更新用户配置"""
updates = {
"uin": str(new_data.get("musicid", user_info["uin"])),
"qqmusic_key": new_data.get("musickey", user_info["qqmusic_key"]),
"refresh_key": new_data.get("refresh_key", user_info.get("refresh_key", "")),
}
if variable.use_cookie_pool:
user_list = config.read_config("module.cookiepool.tx")
target_user = next((u for u in user_list if u["uin"] == user_info["uin"]), None)
if target_user:
target_user.update(updates)
config.write_config("module.cookiepool.tx", user_list)
else:
logger.error('未知的qqmusic_key格式')
for key, value in updates.items():
config.write_config(f"module.tx.user.{key}", value)
if (not variable.use_cookie_pool):
# changed refresh login config path
txconfig = config.read_config('module.tx')
refresh_login_info = txconfig.get('refresh_login')
if (refresh_login_info):
txconfig['user']['refresh_login'] = refresh_login_info
txconfig.pop('refresh_login')
config.write_config('module.tx', txconfig)
if (config.read_config('module.tx.user.refresh_login.enable') and not variable.use_cookie_pool):
scheduler.append('qqmusic_refresh_login', refresh,
config.read_config('module.tx.user.refresh_login.interval'))
async def _process_refresh(user_info: Dict[str, Any]) -> Optional[bool]:
"""统一处理刷新逻辑"""
try:
# 构建请求参数
request_body = _build_request_body(user_info)
signature = sign(json.dumps(request_body))
async def refresh_login_for_pool(user_info):
if (user_info['qqmusic_key'].startswith('W_X')):
options = {
'method': 'POST',
'body': json.dumps({
"comm": {
"fPersonality": "0",
"tmeLoginType": "1",
"tmeLoginMethod": "1",
"qq": "",
"authst": "",
"ct": "11",
"cv": "12080008",
"v": "12080008",
"tmeAppID": "qqmusic"
# 发送请求
response = await Httpx.AsyncRequest(
f"https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}",
{
"method": "POST",
"body": json.dumps(request_body),
"headers": {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
"req1": {
"module": "music.login.LoginServer",
"method": "Login",
"param": {
"code": "",
"openid": "",
"refresh_token": "",
"str_musicid": str(user_info['uin']),
"musickey": user_info['qqmusic_key'],
"unionid": "",
"refresh_key": "",
"loginMode": 2
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
logger.warning(f'为QQ音乐账号({user_info["uin"]})刷新登录失败, code: ' +
str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info(f'为QQ音乐账号(WeChat_{user_info["uin"]})刷新登录成功')
user_list = config.read_config('module.cookiepool.tx')
user_list[user_list.index(
user_info)]['qqmusic_key'] = body['req1']['data']['musickey']
user_list[user_list.index(
user_info)]['uin'] = str(body['req1']['data']['musicid'])
config.write_config('module.cookiepool.tx', user_list)
logger.info(f'为QQ音乐账号(WeChat_{user_info["uin"]})数据更新完毕')
return
elif (user_info['qqmusic_key'].startswith('Q_H_L')):
options = {
'method': 'POST',
'body': json.dumps({
'req1': {
'module': 'QQConnectLogin.LoginServer',
'method': 'QQLogin',
'param': {
'expired_in': 7776000,
'musicid': int(user_info['uin']),
'musickey': user_info['qqmusic_key']
}
}
})
}
signature = sign(options['body'])
req = await Httpx.AsyncRequest(f'https://u6.y.qq.com/cgi-bin/musics.fcg?sign={signature}', options)
body = req.json()
if (body['req1']['code'] != 0):
},
)
response_data = response.json()
if response_data.get("req1", {}).get("code") != 0:
logger.warning(
f'为QQ音乐账号({user_info["uin"]})刷新登录失败, code: ' + str(body['req1']['code']) + f'\n响应体: {body}')
return
else:
logger.info(f'为QQ音乐账号(QQ_{user_info["uin"]})刷新登录成功')
user_list = config.read_config('module.cookiepool.tx')
user_list[user_list.index(
user_info)]['qqmusic_key'] = body['req1']['data']['musickey']
user_list[user_list.index(
user_info)]['uin'] = str(body['req1']['data']['musicid'])
config.write_config('module.cookiepool.tx', user_list)
logger.info(f'为QQ音乐账号(QQ_{user_info["uin"]})数据更新完毕')
return
else:
logger.warning(f'为QQ音乐账号({user_info["uin"]})刷新登录失败: 未知或不支持的key类型')
f"刷新失败 [账号: {user_info['uin']} 代码: {response_data['req1']['code']}]"
)
return False
# 更新配置
await _update_user_config(user_info, response_data["req1"]["data"])
logger.info(f"刷新成功 [账号: {user_info['uin']}]")
return True
except json.JSONDecodeError:
logger.error(
"响应解析失败 [账号: %s] 原始响应: %s",
user_info["uin"],
response.text[:100],
)
except KeyError as e:
logger.error(
"响应数据格式异常 [账号: %s] 缺失字段: %s", user_info["uin"], str(e)
)
except Exception as e:
logger.error(
"刷新过程异常 [账号: %s] 错误信息: %s",
user_info["uin"],
str(e),
)
return False
async def refresh() -> None:
"""主刷新入口非Cookie池模式"""
if not config.read_config("module.tx.user.refresh_login.enable"):
return
def reg_refresh_login_pool_task():
user_info_pool = config.read_config('module.cookiepool.tx')
for user_info in user_info_pool:
if (user_info['refresh_login'].get('enable')):
scheduler.append(
f'qqmusic_refresh_login_pooled_{user_info["uin"]}', refresh_login_for_pool, user_info['refresh_login']['interval'], args = {'user_info': user_info})
await _process_refresh(
{
"uin": config.read_config("module.tx.user.uin"),
"qqmusic_key": config.read_config("module.tx.user.qqmusic_key"),
"refresh_key": config.read_config("module.tx.user.refresh_key"),
}
)
if (variable.use_cookie_pool):
reg_refresh_login_pool_task()
async def refresh_login_for_pool(user_info: Dict[str, Any]) -> None:
"""Cookie池刷新入口"""
if user_info.get("refresh_login", {}).get("enable", False):
await _process_refresh(user_info)
def _setup_scheduler() -> None:
"""初始化定时任务"""
if variable.use_cookie_pool:
user_list = config.read_config("module.cookiepool.tx")
for user in user_list:
if user.get("refresh_login", {}).get("enable", False):
scheduler.append(
f"qq_refresh_{user['uin']}",
refresh_login_for_pool,
user["refresh_login"].get("interval", 3600),
args={"user_info": user},
)
elif config.read_config("module.tx.user.refresh_login.enable"):
scheduler.append(
"qqmusic_main_refresh",
refresh,
config.read_config("module.tx.user.refresh_login.interval"),
)
# 初始化定时任务
_setup_scheduler()

View File

@ -15,6 +15,9 @@ from .encrypt import eapiEncrypt
import ujson as json
from . import refresh_login
PROTO = config.read_config("module.wy.proto")
API_URL = config.read_config("module.wy.ncmapi.api_url")
tools = {
'qualityMap': {
'128k': 'standard',
@ -45,32 +48,72 @@ tools = {
},
}
async def url(songId, quality):
path = '/api/song/enhance/player/url/v1'
requestUrl = 'https://interface.music.163.com/eapi/song/enhance/player/url/v1'
requestBody = {
"ids": json.dumps([songId]),
"level": tools["qualityMap"][quality],
"encodeType": "flac",
}
if (quality == "sky"):
requestBody["immerseType"] = "c51"
req = await Httpx.AsyncRequest(requestUrl, {
'method': 'POST',
'headers': {
'Cookie': config.read_config('module.wy.user.cookie') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.wy'))['cookie'],
},
'form': eapiEncrypt(path, json.dumps(requestBody))
})
body = req.json()
if (not body.get("data") or (not body.get("data")) or (not body.get("data")[0].get("url"))):
raise FailedException("failed")
if PROTO == "offcial":
path = '/api/song/enhance/player/url/v1'
requestUrl = 'https://interface.music.163.com/eapi/song/enhance/player/url/v1'
requestBody = {
"ids": json.dumps([songId]),
"level": tools["qualityMap"][quality],
"encodeType": "flac",
}
if (quality == "sky"):
requestBody["immerseType"] = "c51"
req = await Httpx.AsyncRequest(requestUrl, {
'method': 'POST',
'headers': {
'Cookie': config.read_config('module.wy.user.cookie') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.wy'))['cookie'],
},
'form': eapiEncrypt(path, json.dumps(requestBody))
})
body = req.json()
if (not body.get("data") or (not body.get("data")) or (not body.get("data")[0].get("url"))):
raise FailedException("失败")
data = body["data"][0]
if (data['level'] != tools['qualityMap'][quality]):
raise FailedException("reject unmatched quality")
data = body["data"][0]
# 修正:映射服务器返回的 level 为标准化值
data_level = data['level']
expected_level = tools["qualityMap"][quality]
# 检查客户端请求的 quality 与服务器返回的 level 是否匹配
if data_level != expected_level:
raise FailedException(
f"reject unmatched quality: expected={expected_level}, got={data_level}"
)
return {
'url': data["url"].split("?")[0],
'quality': tools['qualityMapReverse'][data['level']]
}
elif (PROTO == "ncmapi") and (API_URL):
requestUrl = f"{API_URL}/song/url/v1"
requestBody = {
"ids": songId,
"level": tools["qualityMap"][quality],
"cookie": config.read_config('module.wy.user.cookie') if (not variable.use_cookie_pool) else random.choice(config.read_config('module.cookiepool.wy'))['cookie']
}
req = await Httpx.AsyncRequest(requestUrl, {
"method": "GET",
"params": requestBody
})
body = req.json()
if (body["code"] != 200) or (not body.get("data")):
raise FailedException("失败")
data = body["data"][0]
return {
'url': data["url"].split("?")[0],
'quality': tools['qualityMapReverse'][data['level']]
}
# 修正:映射服务器返回的 level 为标准化值
data_level = data['level']
expected_level = tools["qualityMap"][quality]
# 检查客户端请求的 quality 与服务器返回的 level 是否匹配
if data_level != expected_level:
raise FailedException(
f"reject unmatched quality: expected={expected_level}, got={data_level}"
)
return {
'url': data["url"].split("?")[0],
'quality': quality
}

View File

@ -1,13 +1,13 @@
{
"name": "lx-music-api-server",
"version": "2.0.0.beta-12",
"version": "2.0.0",
"description": "一个适配 LX Music 的 API 后端实现",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "poetry run python poetry_run.py development",
"prod": "poetry run python poetry_run.py production",
"development": "poetry run python poetry_run.py development",
"production": "poetry run python poetry_run.py production",
"install": "poetry install"
"dev": "python run.py development",
"prod": "python run.py production",
"install": "pip install -r requirements.txt",
"poetry:install": "poetry install",
"poetry:development": "poetry run python run.py development",
"poetry:production": "poetry run python run.py production"
}
}
}

1381
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
import os
import argparse
import subprocess
def main_production():
os.environ['CURRENT_ENV'] = 'production'
subprocess.run(["python", "main.py"])
def main_development():
os.environ['CURRENT_ENV'] = 'development'
subprocess.run(["python", "main.py"])
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Script to set environment variables and run different environments.")
parser.add_argument("environment", choices=["production", "development"], help="Specify the environment to run.")
args = parser.parse_args()
try:
if args.environment == "production":
main_production()
elif args.environment == "development":
main_development()
except KeyboardInterrupt:
pass

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "lx_music_api_server"
version = "2.0.0.beta-12"
version = "2.0.0"
description = "一个适配 LX Music 的 API 后端实现"
authors = ["helloplhm-qwq", "lerdb", "Folltoshe"]
license = "MIT"
@ -9,18 +9,20 @@ package-mode = false
[tool.poetry.dependencies]
python = "^3.8"
aiohttp = "^3.10.9"
pycryptodome = "^3.21.0"
aiohttp = "^3.10.10"
pycryptodome = "^3.22.0"
ujson = "^5.10.0"
requests = "^2.32.3"
colorlog = "^6.8.2"
Pygments = "^2.18.0"
xmltodict = "^0.13.0"
colorlog = "^6.9.0"
Pygments = "^2.19.1"
xmltodict = "^0.14.2"
pillow = "^10.4.0"
mutagen = "^1.47.0"
colorama = "^0.4.6"
ruamel-yaml = "^0.18.6"
ruamel-yaml = "^0.18.10"
pybind11 = "^2.13.6"
redis = "^5.2.1"
hiredis = "^3.1.0"
[build-system]
requires = ["poetry-core"]

View File

@ -10,3 +10,5 @@ mutagen
pillow
colorama
ruamel-yaml
redis
hiredis

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

21
run.py Normal file
View File

@ -0,0 +1,21 @@
import os
import argparse
import subprocess
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Script to set environment variables and run different environments.")
parser.add_argument(
"environment",
choices=["production", "development"],
nargs="?",
default="production",
help="Specify the environment to run.",
)
args = parser.parse_args()
try:
if args.environment:
os.environ["CURRENT_ENV"] = args.environment
subprocess.run(["python", "main.py"])
except KeyboardInterrupt:
pass

View File

@ -9,16 +9,16 @@ except Exception:
description = "Description not available"
setup(
name='lx_music_api_server_setup',
name="lx_music_api_server_setup",
version=version,
scripts=['poetry_run.py'],
author='helloplhm-qwq',
author_email='helloplhm-qwq@outlook.com',
scripts=["run.py"],
author="helloplhm-qwq",
author_email="helloplhm-qwq@outlook.com",
description=description,
url='https://github.com/helloplhm-qwq/lx-music-api-server',
url="https://github.com/helloplhm-qwq/lx-music-api-server",
classifiers=[
'Programming Language :: Python :: 3',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
)