mirror of
https://github.com/MeoProject/lx-music-api-server.git
synced 2025-05-23 19:17:41 +08:00
Compare commits
42 Commits
v2.0.0.bet
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
3fb2718cdf | ||
|
0677c0b5ad | ||
|
2400602aae | ||
|
77737d18e6 | ||
|
e13b7e3831 | ||
|
c175312307 | ||
|
b4049ceb69 | ||
|
031834d40f | ||
|
daf34ace39 | ||
|
3b6ee6e9b2 | ||
|
edaec2a3d2 | ||
|
30c0eaa23e | ||
|
412f692ba1 | ||
|
c84c2acbb9 | ||
|
3e5c1cd94b | ||
|
3375f0beba | ||
|
82be199636 | ||
|
f7069fc6f8 | ||
|
ed47e928dd | ||
|
56d19ee671 | ||
|
0433d7a4d1 | ||
|
c63beb983f | ||
|
6d247dbeac | ||
|
d4cb252aa4 | ||
|
1ea53d2ca9 | ||
|
497f21d2d4 | ||
|
8e361ff14b | ||
|
d3ab5f910d | ||
|
6b08683b66 | ||
|
196732ab7d | ||
|
dcfe8a4472 | ||
|
b45c3a7765 | ||
|
5442e3340b | ||
|
92a69f048d | ||
|
0fd9fe1c3e | ||
|
3533a72681 | ||
|
88937e4917 | ||
|
6c6ef01681 | ||
|
f67de8f43b | ||
|
7dbafb958c | ||
|
f7988a48b1 | ||
|
52420696f8 |
13
Dockerfile
13
Dockerfile
@ -1,13 +0,0 @@
|
||||
FROM python:3.10-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./main.py .
|
||||
COPY ./common ./common
|
||||
COPY ./modules ./modules
|
||||
COPY ./requirements.txt .
|
||||
|
||||
# 指定源, 如果后期源挂了, 更换个源就可以.
|
||||
RUN pip install --no-cache -i https://pypi.mirrors.ustc.edu.cn/simple/ -r requirements.txt
|
||||
|
||||
CMD [ "python", "main.py" ]
|
6
build.py
6
build.py
@ -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)
|
||||
|
251
common/Httpx.py
251
common/Httpx.py
@ -1,9 +1,9 @@
|
||||
# ----------------------------------------
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: Httpx.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# - mode: python -
|
||||
# - author: helloplhm-qwq -
|
||||
# - name: Httpx.py -
|
||||
# - project: lx-music-api-server -
|
||||
# - license: MIT -
|
||||
# ----------------------------------------
|
||||
# This file is part of the "lx-music-api-server" project.
|
||||
|
||||
@ -21,15 +21,16 @@ from . import config
|
||||
from . import utils
|
||||
from . import variable
|
||||
|
||||
|
||||
def is_valid_utf8(text) -> bool:
|
||||
try:
|
||||
if isinstance(text, bytes):
|
||||
text = text.decode('utf-8')
|
||||
text = text.decode("utf-8")
|
||||
# 判断是否为有效的utf-8字符串
|
||||
if "\ufffe" in text:
|
||||
return False
|
||||
try:
|
||||
text.encode('utf-8').decode('utf-8')
|
||||
text.encode("utf-8").decode("utf-8")
|
||||
return True
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
@ -37,42 +38,48 @@ def is_valid_utf8(text) -> bool:
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
|
||||
def is_plain_text(text) -> bool:
|
||||
# 判断是否为纯文本
|
||||
pattern = re.compile(r'[^\x00-\x7F]')
|
||||
pattern = re.compile(r"[^\x00-\x7F]")
|
||||
return not bool(pattern.search(text))
|
||||
|
||||
|
||||
def convert_dict_to_form_string(dic: dict) -> str:
|
||||
# 将字典转换为表单字符串
|
||||
return '&'.join([f'{k}={v}' for k, v in dic.items()])
|
||||
return "&".join([f"{k}={v}" for k, v in dic.items()])
|
||||
|
||||
|
||||
def log_plaintext(text: str) -> str:
|
||||
if (text.startswith('{') and text.endswith('}')):
|
||||
if text.startswith("{") and text.endswith("}"):
|
||||
try:
|
||||
text = json.loads(text)
|
||||
except:
|
||||
pass
|
||||
elif (text.startswith('<xml') and text.endswith('>')): # xml data
|
||||
elif text.startswith("<xml") and text.endswith(">"): # xml data
|
||||
try:
|
||||
text = f'xml: {utils.load_xml(text)}'
|
||||
text = f"xml: {utils.load_xml(text)}"
|
||||
except:
|
||||
pass
|
||||
return text
|
||||
|
||||
|
||||
# 内置的UA列表
|
||||
ua_list = [ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0 uacq',
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 uacq',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'
|
||||
]
|
||||
ua_list = [
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0 uacq",
|
||||
"Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 uacq",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
|
||||
]
|
||||
|
||||
# 日志记录器
|
||||
logger = log.log('http_utils')
|
||||
logger = log.log("http_utils")
|
||||
|
||||
def request(url: str, options = {}) -> requests.Response:
|
||||
'''
|
||||
|
||||
def request(url: str, options={}) -> requests.Response:
|
||||
"""
|
||||
Http请求主函数, 用于发送网络请求
|
||||
- url: 需要请求的URL地址(必填)
|
||||
- options: 请求的配置参数(可选, 留空时为GET请求, 总体与nodejs的请求的options填写差不多)
|
||||
@ -84,15 +91,15 @@ def request(url: str, options = {}) -> requests.Response:
|
||||
- no-cache: 不缓存
|
||||
- <int>: 缓存可用秒数
|
||||
- cache-ignore: <list> 缓存忽略关键字
|
||||
|
||||
|
||||
@ return: requests.Response类型的响应数据
|
||||
'''
|
||||
"""
|
||||
# 缓存读取
|
||||
cache_key = f'{url}{options}'
|
||||
if (isinstance(options.get('cache-ignore'), list)):
|
||||
for i in options.get('cache-ignore'):
|
||||
cache_key = cache_key.replace(str(i), '')
|
||||
options.pop('cache-ignore')
|
||||
cache_key = f"{url}{options}"
|
||||
if isinstance(options.get("cache-ignore"), list):
|
||||
for i in options.get("cache-ignore"):
|
||||
cache_key = cache_key.replace(str(i), "")
|
||||
options.pop("cache-ignore")
|
||||
cache_key = utils.createMD5(cache_key)
|
||||
if options.get("cache") and options["cache"] != "no-cache":
|
||||
cache = config.getCache("httpx", cache_key)
|
||||
@ -104,77 +111,84 @@ def request(url: str, options = {}) -> requests.Response:
|
||||
options.pop("cache")
|
||||
else:
|
||||
cache_info = None
|
||||
|
||||
|
||||
# 获取请求方法,没有则默认为GET请求
|
||||
try:
|
||||
method = options['method'].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
|
||||
|
358
common/config.py
358
common/config.py
@ -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()
|
||||
|
@ -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时使用的guid,uin等信息,不需要与cookie中信息一致
|
||||
guid: "114514"
|
||||
uin: "10086"
|
||||
user: # 用户数据,可以通过浏览器获取,需要vip账号来获取会员歌曲,如果没有请留为空值,qqmusic_key可以从Cookie中/客户端的请求体中(comm.authst)获取
|
||||
qqmusic_key: ""
|
||||
uin: "" # key对应的QQ号
|
||||
refresh_key: "" # 刷新失活 qqmusic_key
|
||||
refresh_login: # 刷新登录相关配置,enable是否启动,interval刷新间隔
|
||||
enable: false
|
||||
interval: 86000
|
||||
cdnaddr: http://ws.stream.qqmusic.qq.com/
|
||||
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
|
||||
|
@ -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")
|
||||
|
@ -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
10
main.py
@ -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))
|
||||
|
@ -11,6 +11,7 @@ from common.exceptions import FailedException
|
||||
from common.utils import require
|
||||
from common import log
|
||||
from common import config
|
||||
|
||||
# 从.引入的包并没有在代码中直接使用,但是是用require在请求时进行引入的,不要动
|
||||
from . import kw
|
||||
from . import mg
|
||||
@ -20,194 +21,195 @@ from . import wy
|
||||
import traceback
|
||||
import time
|
||||
|
||||
logger = log.log('api_handler')
|
||||
logger = log.log("api_handler")
|
||||
|
||||
sourceExpirationTime = {
|
||||
'tx': {
|
||||
"tx": {
|
||||
"expire": True,
|
||||
"time": 80400, # 不知道tx为什么要取一个这么不对劲的数字当过期时长
|
||||
},
|
||||
'kg': {
|
||||
"kg": {
|
||||
"expire": True,
|
||||
"time": 24 * 60 * 60, # 24 hours
|
||||
},
|
||||
'kw': {
|
||||
"expire": True,
|
||||
"time": 60 * 60 # 60 minutes
|
||||
},
|
||||
'wy': {
|
||||
"kw": {"expire": True, "time": 60 * 60}, # 60 minutes
|
||||
"wy": {
|
||||
"expire": True,
|
||||
"time": 20 * 60, # 20 minutes
|
||||
},
|
||||
'mg': {
|
||||
"mg": {
|
||||
"expire": False,
|
||||
"time": 0,
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def url(source, songId, quality, query = {}):
|
||||
if (not quality):
|
||||
async def url(source, songId, quality, query={}):
|
||||
if not quality:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': '需要参数"quality"',
|
||||
'data': None,
|
||||
"code": 2,
|
||||
"msg": '需要参数"quality"',
|
||||
"data": None,
|
||||
}
|
||||
|
||||
if (source == "kg"):
|
||||
|
||||
if source == "kg":
|
||||
songId = songId.lower()
|
||||
|
||||
|
||||
try:
|
||||
cache = config.getCache('urls', f'{source}_{songId}_{quality}')
|
||||
cache = config.getCache("urls", f"{source}_{songId}_{quality}")
|
||||
if cache:
|
||||
logger.debug(f'使用缓存的{source}_{songId}_{quality}数据,URL:{cache["url"]}')
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': cache['url'],
|
||||
'extra': {
|
||||
'cache': True,
|
||||
'quality': {
|
||||
'target': quality,
|
||||
'result': quality,
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": cache["url"],
|
||||
"extra": {
|
||||
"cache": True,
|
||||
"quality": {
|
||||
"target": quality,
|
||||
"result": quality,
|
||||
},
|
||||
'expire': {
|
||||
"expire": {
|
||||
# 在更新缓存的时候把有效期的75%作为链接可用时长,现在加回来
|
||||
'time': int(cache['time'] + (sourceExpirationTime[source]['time'] * 0.25)) if cache['expire'] else None,
|
||||
'canExpire': cache['expire'],
|
||||
}
|
||||
"time": (
|
||||
int(cache["time"] + (sourceExpirationTime[source]["time"] * 0.25))
|
||||
if cache["expire"]
|
||||
else None
|
||||
),
|
||||
"canExpire": cache["expire"],
|
||||
},
|
||||
},
|
||||
}
|
||||
except:
|
||||
logger.error(traceback.format_exc())
|
||||
try:
|
||||
func = require('modules.' + source + '.url')
|
||||
func = require("modules." + source + ".url")
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
"code": 1,
|
||||
"msg": "未知的源或不支持的方法",
|
||||
"data": None,
|
||||
}
|
||||
try:
|
||||
result = await func(songId, quality)
|
||||
logger.info(f'获取{source}_{songId}_{quality}成功,URL:{result["url"]}')
|
||||
|
||||
canExpire = sourceExpirationTime[source]['expire']
|
||||
expireTime = sourceExpirationTime[source]['time'] + int(time.time())
|
||||
config.updateCache('urls', f'{source}_{songId}_{quality}', {
|
||||
"expire": canExpire,
|
||||
# 取有效期的75%作为链接可用时长
|
||||
"time": int(expireTime - sourceExpirationTime[source]['time'] * 0.25),
|
||||
"url": result['url'],
|
||||
})
|
||||
canExpire = sourceExpirationTime[source]["expire"]
|
||||
expireTime = int(sourceExpirationTime[source]["time"] * 0.75)
|
||||
expireAt = int(expireTime + time.time())
|
||||
config.updateCache(
|
||||
"urls",
|
||||
f"{source}_{songId}_{quality}",
|
||||
{
|
||||
"expire": canExpire,
|
||||
# 取有效期的75%作为链接可用时长
|
||||
"time": expireAt,
|
||||
"url": result["url"],
|
||||
},
|
||||
expireTime if canExpire else None,
|
||||
)
|
||||
logger.debug(f'缓存已更新:{source}_{songId}_{quality}, URL:{result["url"]}, expire: {expireTime}')
|
||||
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result['url'],
|
||||
'extra': {
|
||||
'cache': False,
|
||||
'quality': {
|
||||
'target': quality,
|
||||
'result': result['quality'],
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": result["url"],
|
||||
"extra": {
|
||||
"cache": False,
|
||||
"quality": {
|
||||
"target": quality,
|
||||
"result": result["quality"],
|
||||
},
|
||||
'expire': {
|
||||
'time': expireTime if canExpire else None,
|
||||
'canExpire': canExpire,
|
||||
"expire": {
|
||||
"time": expireAt if canExpire else None,
|
||||
"canExpire": canExpire,
|
||||
},
|
||||
},
|
||||
}
|
||||
except FailedException as e:
|
||||
logger.info(f'获取{source}_{songId}_{quality}失败,原因:' + e.args[0])
|
||||
logger.info(f"获取{source}_{songId}_{quality}失败,原因:" + e.args[0])
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
"code": 2,
|
||||
"msg": e.args[0],
|
||||
"data": None,
|
||||
}
|
||||
|
||||
|
||||
async def lyric(source, songId, _, query):
|
||||
cache = config.getCache('lyric', f'{source}_{songId}')
|
||||
cache = config.getCache("lyric", f"{source}_{songId}")
|
||||
if cache:
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': cache['data']
|
||||
}
|
||||
return {"code": 0, "msg": "success", "data": cache["data"]}
|
||||
try:
|
||||
func = require('modules.' + source + '.lyric')
|
||||
func = require("modules." + source + ".lyric")
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
"code": 1,
|
||||
"msg": "未知的源或不支持的方法",
|
||||
"data": None,
|
||||
}
|
||||
try:
|
||||
result = await func(songId)
|
||||
config.updateCache('lyric', f'{source}_{songId}', {
|
||||
"data": result,
|
||||
"time": int(time.time() + (86400 * 3)), # 歌词缓存3天
|
||||
"expire": True,
|
||||
})
|
||||
logger.debug(f'缓存已更新:{source}_{songId}, lyric: {result}')
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
expireTime = 86400 * 3
|
||||
expireAt = int(time.time() + expireTime)
|
||||
config.updateCache(
|
||||
"lyric",
|
||||
f"{source}_{songId}",
|
||||
{
|
||||
"data": result,
|
||||
"time": expireAt, # 歌词缓存3天
|
||||
"expire": True,
|
||||
},
|
||||
expireTime,
|
||||
)
|
||||
logger.debug(f"缓存已更新:{source}_{songId}, lyric: {result}")
|
||||
return {"code": 0, "msg": "success", "data": result}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
"code": 2,
|
||||
"msg": e.args[0],
|
||||
"data": None,
|
||||
}
|
||||
|
||||
|
||||
async def search(source, songid, _, query):
|
||||
try:
|
||||
func = require('modules.' + source + '.search')
|
||||
func = require("modules." + source + ".search")
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
"code": 1,
|
||||
"msg": "未知的源或不支持的方法",
|
||||
"data": None,
|
||||
}
|
||||
try:
|
||||
result = await func(songid, query)
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
return {"code": 0, "msg": "success", "data": result}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
"code": 2,
|
||||
"msg": e.args[0],
|
||||
"data": None,
|
||||
}
|
||||
|
||||
|
||||
async def other(method, source, songid, _, query):
|
||||
try:
|
||||
func = require('modules.' + source + '.' + method)
|
||||
func = require("modules." + source + "." + method)
|
||||
except:
|
||||
return {
|
||||
'code': 1,
|
||||
'msg': '未知的源或不支持的方法',
|
||||
'data': None,
|
||||
"code": 1,
|
||||
"msg": "未知的源或不支持的方法",
|
||||
"data": None,
|
||||
}
|
||||
try:
|
||||
result = await func(songid)
|
||||
return {
|
||||
'code': 0,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
}
|
||||
return {"code": 0, "msg": "success", "data": result}
|
||||
except FailedException as e:
|
||||
return {
|
||||
'code': 2,
|
||||
'msg': e.args[0],
|
||||
'data': None,
|
||||
"code": 2,
|
||||
"msg": e.args[0],
|
||||
"data": None,
|
||||
}
|
||||
|
||||
|
||||
async def info_with_query(source, songid, _, query):
|
||||
return await other('info', source, songid, None)
|
||||
return await other("info", source, songid, None)
|
||||
|
@ -22,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 = []
|
||||
|
@ -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"]
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
16
package.json
16
package.json
@ -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
1381
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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
|
@ -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"]
|
||||
|
@ -10,3 +10,5 @@ mutagen
|
||||
pillow
|
||||
colorama
|
||||
ruamel-yaml
|
||||
redis
|
||||
hiredis
|
||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
21
run.py
Normal file
21
run.py
Normal file
@ -0,0 +1,21 @@
|
||||
import os
|
||||
import argparse
|
||||
import subprocess
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Script to set environment variables and run different environments.")
|
||||
parser.add_argument(
|
||||
"environment",
|
||||
choices=["production", "development"],
|
||||
nargs="?",
|
||||
default="production",
|
||||
help="Specify the environment to run.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.environment:
|
||||
os.environ["CURRENT_ENV"] = args.environment
|
||||
subprocess.run(["python", "main.py"])
|
||||
except KeyboardInterrupt:
|
||||
pass
|
16
setup.py
16
setup.py
@ -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",
|
||||
],
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user