mirror of
https://github.com/MeoProject/lx-music-api-server.git
synced 2025-05-23 19:17:41 +08:00
feat: 更新太大了不想总结自己去看提交记录吧(已知把配置文件换成了yaml
This commit is contained in:
parent
45e2e7147d
commit
667d420499
6
.gitignore
vendored
6
.gitignore
vendored
@ -22,6 +22,7 @@ test.*
|
|||||||
*/test.*
|
*/test.*
|
||||||
logs
|
logs
|
||||||
config.json
|
config.json
|
||||||
|
config.yml
|
||||||
/config/config.json
|
/config/config.json
|
||||||
/config/data.db
|
/config/data.db
|
||||||
*.log
|
*.log
|
||||||
@ -40,4 +41,7 @@ config.json
|
|||||||
*.un
|
*.un
|
||||||
|
|
||||||
# temp script
|
# temp script
|
||||||
lx-music-source-example.js
|
lx-music-source-example.js
|
||||||
|
|
||||||
|
# dumprecord
|
||||||
|
dumprecord_*.txt
|
2
build.py
2
build.py
@ -30,7 +30,7 @@ def get_changelog():
|
|||||||
noticeMsg = []
|
noticeMsg = []
|
||||||
unknownMsg = []
|
unknownMsg = []
|
||||||
for msg in res:
|
for msg in res:
|
||||||
if (re.match('[a-f0-9]*.(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|notice)\(?.*?\)?\:', msg[1:-1])):
|
if (re.match('[a-f0-9]*.(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|notice).*?\(?.*?\)?\:', msg[1:-1])):
|
||||||
msg = msg[1:-1]
|
msg = msg[1:-1]
|
||||||
if msg[8:].startswith('notice'):
|
if msg[8:].startswith('notice'):
|
||||||
noticeMsg.append(msg)
|
noticeMsg.append(msg)
|
||||||
|
334
common/config.py
334
common/config.py
@ -14,8 +14,10 @@ import traceback
|
|||||||
import sys
|
import sys
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import shutil
|
import shutil
|
||||||
|
import ruamel.yaml as yaml
|
||||||
from . import variable
|
from . import variable
|
||||||
from .log import log
|
from .log import log
|
||||||
|
from . import default_config
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
logger = log('config_manager')
|
logger = log('config_manager')
|
||||||
@ -45,302 +47,17 @@ class ConfigReadException(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
default = {
|
default_str = default_config.default
|
||||||
"common": {
|
default = yaml.safe_load(default_str)
|
||||||
"host": "0.0.0.0",
|
|
||||||
"_host-desc": "服务器启动时所使用的HOST地址",
|
|
||||||
"ports": [ 9763 ],
|
|
||||||
"_ports-desc": "服务器启动时所使用的端口",
|
|
||||||
"ssl_info": {
|
|
||||||
"desc": "服务器https配置,is_https是这个服务器是否是https服务器,如果你使用了反向代理来转发这个服务器,如果它使用了https,也请将它设置为true",
|
|
||||||
"is_https": False,
|
|
||||||
"enable": False,
|
|
||||||
"ssl_ports": [ 443 ],
|
|
||||||
"path": {
|
|
||||||
"desc": "ssl证书的文件地址",
|
|
||||||
"cert": "/path/to/your/cer",
|
|
||||||
"privkey": "/path/to/your/private/key",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"reverse_proxy": {
|
|
||||||
"desc": "针对类似于nginx一类的反代的配置",
|
|
||||||
"allow_proxy": True,
|
|
||||||
"_allow_proxy-desc": "是否允许反代",
|
|
||||||
"proxy_whitelist_remote": [
|
|
||||||
"反代时允许的ip来源列表,通常为127.0.0.1",
|
|
||||||
"127.0.0.1"
|
|
||||||
],
|
|
||||||
"real_ip_header": 'X-Real-IP',
|
|
||||||
"_real_ip_header-desc": "反代来源ip的来源头,不懂请保持默认",
|
|
||||||
},
|
|
||||||
"debug_mode": False,
|
|
||||||
"_debug_mode-desc": "是否开启调试模式",
|
|
||||||
"log_length_limit": 500,
|
|
||||||
"_log_length_limit-desc": "单条日志长度限制",
|
|
||||||
"fakeip": "1.0.1.114",
|
|
||||||
"_fakeip-desc": "服务器在海外时的IP伪装值",
|
|
||||||
"proxy": {
|
|
||||||
"enable": False,
|
|
||||||
"http_value": "http://127.0.0.1:7890",
|
|
||||||
"https_value": "http://127.0.0.1:7890",
|
|
||||||
},
|
|
||||||
"_proxy-desc": "代理配置,HTTP与HTTPS协议需分开配置",
|
|
||||||
"log_file": True,
|
|
||||||
"_log_file-desc": "是否开启日志文件",
|
|
||||||
'cookiepool': False,
|
|
||||||
'_cookiepool-desc': '是否开启cookie池,这将允许用户配置多个cookie并在请求时随机使用一个,启用后请在module.cookiepool中配置cookie,在user处配置的cookie会被忽略,cookiepool中格式统一为列表嵌套user处的cookie的字典',
|
|
||||||
"allow_download_script": True,
|
|
||||||
'_allow_download_script-desc': '是否允许直接从服务端下载脚本,开启后可以直接访问 /script?key=你的请求key 下载脚本',
|
|
||||||
"download_config": {
|
|
||||||
"desc": "源脚本的相关配置,dev为是否启用开发模式",
|
|
||||||
"name": "修改为你的源脚本名称",
|
|
||||||
"intro": "修改为你的源脚本描述",
|
|
||||||
"author": "修改为你的源脚本作者",
|
|
||||||
"version": "修改为你的源版本",
|
|
||||||
"filename": "lx-music-source.js",
|
|
||||||
"_filename-desc": "客户端保存脚本时的文件名(可能因浏览器不同出现不一样的情况)",
|
|
||||||
"dev": True,
|
|
||||||
"quality": {
|
|
||||||
"kw": ["128k"],
|
|
||||||
"kg": ["128k"],
|
|
||||||
"tx": ["128k"],
|
|
||||||
"wy": ["128k"],
|
|
||||||
"mg": ["128k"],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"local_music": {
|
|
||||||
"desc": "服务器侧本地音乐相关配置,请确保你的带宽足够",
|
|
||||||
"audio_path": "./audio",
|
|
||||||
"temp_path": "./temp",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"rate_limit": {
|
|
||||||
"global": 0,
|
|
||||||
"ip": 0,
|
|
||||||
"desc": "请求速率限制,global为全局,ip为单个ip,填入的值为至少间隔多久才能进行一次请求,单位:秒,不限制请填为0"
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"enable": False,
|
|
||||||
"_enable-desc": "是否开启请求key,开启后只有请求头中包含key,且值一样时可以访问API",
|
|
||||||
"ban": True,
|
|
||||||
"values": ["114514"],
|
|
||||||
},
|
|
||||||
"whitelist_host": [
|
|
||||||
"localhost",
|
|
||||||
"0.0.0.0",
|
|
||||||
"127.0.0.1",
|
|
||||||
],
|
|
||||||
"_whitelist_host-desc": "强制白名单HOST,不需要加端口号(即不受其他安全设置影响的HOST)",
|
|
||||||
"check_lxm": False,
|
|
||||||
"_check_lxm-desc": "是否检查lxm请求头(正常的LX Music请求时都会携带这个请求头)",
|
|
||||||
"lxm_ban": True,
|
|
||||||
"_lxm_ban-desc": "lxm请求头不存在或不匹配时是否将用户IP加入黑名单",
|
|
||||||
"allowed_host": {
|
|
||||||
"desc": "HOST允许列表,启用后只允许列表内的HOST访问服务器,不需要加端口号",
|
|
||||||
"enable": False,
|
|
||||||
"blacklist": {
|
|
||||||
"desc": "当用户访问的HOST并不在允许列表中时是否将请求IP加入黑名单,长度单位:秒",
|
|
||||||
"enable": False,
|
|
||||||
"length": 0,
|
|
||||||
},
|
|
||||||
"list": [
|
|
||||||
"localhost",
|
|
||||||
"0.0.0.0",
|
|
||||||
"127.0.0.1",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"banlist": {
|
|
||||||
"desc": "是否启用黑名单(全局设置,关闭后已存储的值并不受影响,但不会再检查)",
|
|
||||||
"enable": True,
|
|
||||||
"expire": {
|
|
||||||
"desc": "是否启用黑名单IP过期(关闭后其他地方的配置会失效)",
|
|
||||||
"enable": True,
|
|
||||||
"length": 86400 * 7, # 七天
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"module": {
|
|
||||||
"kg": {
|
|
||||||
"desc": "酷狗音乐相关配置",
|
|
||||||
"client": {
|
|
||||||
"desc": "客户端请求配置,不懂请保持默认,修改请统一为字符串格式",
|
|
||||||
"appid": "1005",
|
|
||||||
"_appid-desc": "酷狗音乐的appid,官方安卓为1005,官方PC为1001",
|
|
||||||
"signatureKey": "OIlwieks28dk2k092lksi2UIkp",
|
|
||||||
"_signatureKey-desc": "客户端signature采用的key值,需要与appid对应",
|
|
||||||
"clientver": "12029",
|
|
||||||
"_clientver-desc": "客户端versioncode,pidversionsecret可能随此值而变化",
|
|
||||||
"pidversionsecret": "57ae12eb6890223e355ccfcb74edf70d",
|
|
||||||
"_pidversionsecret-desc": "获取URL时所用的key值计算验证值",
|
|
||||||
"pid": "2",
|
|
||||||
},
|
|
||||||
"tracker": {
|
|
||||||
"desc": "trackerapi请求配置,不懂请保持默认,修改请统一为字符串格式",
|
|
||||||
"host": "https://gateway.kugou.com",
|
|
||||||
"path": "/v5/url",
|
|
||||||
"version": "v5",
|
|
||||||
"x-router": {
|
|
||||||
"desc": "当host为gateway.kugou.com时需要追加此头,为tracker类地址时则不需要",
|
|
||||||
"enable": True,
|
|
||||||
"value": "tracker.kugou.com",
|
|
||||||
},
|
|
||||||
"extra_params": {},
|
|
||||||
"_extra_params-desc": "自定义添加的param,优先级大于默认,填写类型为普通的JSON数据,会自动转换为请求param",
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"desc": "此处内容请统一抓包获取,需要vip账号来获取会员歌曲,如果没有请留为空值,mid必填,可以瞎填一段数字",
|
|
||||||
"token": "",
|
|
||||||
"userid": "0",
|
|
||||||
"mid": "114514",
|
|
||||||
"lite_sign_in": {
|
|
||||||
"desc": "是否启用概念版自动签到,仅在appid=3116时运行",
|
|
||||||
"enable": False,
|
|
||||||
"interval": 86400,
|
|
||||||
"mixsongmid": {
|
|
||||||
"desc": "mix_songmid的获取方式, 默认auto, 可以改成一个数字手动",
|
|
||||||
"value": "auto"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"refresh_token": {
|
|
||||||
"desc": "酷狗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": {
|
|
||||||
"desc": "QQ音乐相关配置",
|
|
||||||
"vkeyserver": {
|
|
||||||
"desc": "请求官方api时使用的guid,uin等信息,不需要与cookie中信息一致",
|
|
||||||
"guid": "114514",
|
|
||||||
"uin": "10086",
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"desc": "用户数据,可以通过浏览器获取,需要vip账号来获取会员歌曲,如果没有请留为空值,qqmusic_key可以从Cookie中/客户端的请求体中(comm.authst)获取",
|
|
||||||
"qqmusic_key": "",
|
|
||||||
"uin": "",
|
|
||||||
"_uin-desc": "key对应的QQ号",
|
|
||||||
'refresh_login': {
|
|
||||||
'desc': '刷新登录相关配置,enable是否启动,interval刷新间隔',
|
|
||||||
'enable': False,
|
|
||||||
'interval': 86000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cdnaddr": "http://ws.stream.qqmusic.qq.com/",
|
|
||||||
},
|
|
||||||
"wy": {
|
|
||||||
"desc": "网易云音乐相关配置",
|
|
||||||
"user": {
|
|
||||||
"desc": "账号cookie数据,可以通过浏览器获取,需要vip账号来获取会员歌曲,如果没有请留为空值",
|
|
||||||
"cookie": ""
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"mg": {
|
|
||||||
"desc": "咪咕音乐相关配置",
|
|
||||||
"user": {
|
|
||||||
"desc": "研究不深,后两项自行抓包获取,网页端cookie",
|
|
||||||
"by": "",
|
|
||||||
"session": "",
|
|
||||||
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36",
|
|
||||||
"refresh_login": {
|
|
||||||
"enable": False,
|
|
||||||
"interval": 86400,
|
|
||||||
"desc": "进行cookie保活"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"kw": {
|
|
||||||
"desc": "酷我音乐相关配置,proto支持值:['bd-api', 'kuwodes']",
|
|
||||||
"proto": "bd-api",
|
|
||||||
"user": {
|
|
||||||
"uid": "0",
|
|
||||||
"token": "",
|
|
||||||
"device_id": "0",
|
|
||||||
},
|
|
||||||
"des": {
|
|
||||||
"desc": "kuwodes接口(mobi, nmobi)一类的加密相关配置",
|
|
||||||
"f": "kuwo",
|
|
||||||
"need_encrypt": True,
|
|
||||||
"param填写注释": "{songId}为歌曲id, {map_quality}为map后的歌曲音质(酷我规范), {raw_quality}为请求时的歌曲音质(LX规范), {ext}为歌曲文件扩展名",
|
|
||||||
"params": "type=convert_url_with_sign&rid={songId}&quality={map_quality}&ext={ext}",
|
|
||||||
"host": "nmobi.kuwo.cn",
|
|
||||||
"path": "mobi.s",
|
|
||||||
"response_types": ['这里是reponse_type的所有支持值,当设置为json时会使用到下面的两个值来获取url/bitrate,如果为text,则为传统的逐行解析方式', 'json', 'text'],
|
|
||||||
"response_type": "json",
|
|
||||||
"url_json_path": "data.url",
|
|
||||||
"bitrate_json_path": "data.bitrate",
|
|
||||||
"headers": {
|
|
||||||
"User-Agent": 'okhttp/3.10.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'cookiepool': {
|
|
||||||
'kg': [
|
|
||||||
{
|
|
||||||
'userid': '0',
|
|
||||||
'token': '',
|
|
||||||
'mid': '114514',
|
|
||||||
"lite_sign_in": {
|
|
||||||
"desc": "是否启用概念版自动签到,仅在appid=3116时运行",
|
|
||||||
"enable": False,
|
|
||||||
"interval": 86400,
|
|
||||||
"mixsongmid": {
|
|
||||||
"desc": "mix_songmid的获取方式, 默认auto, 可以改成一个数字手动",
|
|
||||||
"value": "auto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'tx': [
|
|
||||||
{
|
|
||||||
'qqmusic_key': '',
|
|
||||||
'uin': '',
|
|
||||||
'refresh_login': {
|
|
||||||
'desc': 'cookie池中对于此账号刷新登录的配置,账号间互不干扰',
|
|
||||||
'enable': False,
|
|
||||||
'interval': 86000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'wy': [
|
|
||||||
{
|
|
||||||
'cookie': '',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'mg': [
|
|
||||||
{
|
|
||||||
'by': '',
|
|
||||||
'session': '',
|
|
||||||
'useragent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36',
|
|
||||||
"refresh_login": {
|
|
||||||
"enable": False,
|
|
||||||
"interval": 86400
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'kw': [
|
|
||||||
{
|
|
||||||
"uid": "0",
|
|
||||||
"token": "",
|
|
||||||
"device_id": "0",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def handle_default_config():
|
def handle_default_config():
|
||||||
with open("./config/config.json", "w", encoding="utf-8") as f:
|
with open("./config/config.yml", "w", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(default, indent=2, ensure_ascii=False,
|
f.write(default_str)
|
||||||
escape_forward_slashes=False))
|
|
||||||
f.close()
|
|
||||||
if (not os.getenv('build')):
|
if (not os.getenv('build')):
|
||||||
logger.info('首次启动或配置文件被删除,已创建默认配置文件')
|
logger.info('首次启动或配置文件被删除,已创建默认配置文件')
|
||||||
logger.info(
|
logger.info(
|
||||||
f'\n建议您到{variable.workdir + os.path.sep}config.json修改配置后重新启动服务器')
|
f'\n建议您到{variable.workdir + os.path.sep}config.yml修改配置后重新启动服务器')
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
@ -525,8 +242,8 @@ def push_to_list(key, obj):
|
|||||||
|
|
||||||
def write_config(key, value):
|
def write_config(key, value):
|
||||||
config = None
|
config = None
|
||||||
with open('./config/config.json', 'r', encoding='utf-8') as f:
|
with open('./config/config.yml', 'r', encoding='utf-8') as f:
|
||||||
config = json.load(f)
|
config = yaml.YAML().load(f)
|
||||||
|
|
||||||
keys = key.split('.')
|
keys = key.split('.')
|
||||||
current = config
|
current = config
|
||||||
@ -536,11 +253,15 @@ def write_config(key, value):
|
|||||||
current = current[k]
|
current = current[k]
|
||||||
|
|
||||||
current[keys[-1]] = value
|
current[keys[-1]] = value
|
||||||
variable.config = config
|
|
||||||
with open('./config/config.json', 'w', encoding='utf-8') as f:
|
# 设置保留注释和空行的参数
|
||||||
json.dump(config, f, indent=2, ensure_ascii=False,
|
y = yaml.YAML()
|
||||||
escape_forward_slashes=False)
|
y.preserve_quotes = True
|
||||||
f.close()
|
y.preserve_blank_lines = True
|
||||||
|
|
||||||
|
# 写入配置并保留注释和空行
|
||||||
|
with open('./config/config.yml', 'w', encoding='utf-8') as f:
|
||||||
|
y.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
def read_default_config(key):
|
def read_default_config(key):
|
||||||
@ -639,21 +360,26 @@ def initConfig():
|
|||||||
shutil.move('config.json','./config')
|
shutil.move('config.json','./config')
|
||||||
if (os.path.exists('./data.db')):
|
if (os.path.exists('./data.db')):
|
||||||
shutil.move('./data.db','./config')
|
shutil.move('./data.db','./config')
|
||||||
|
if (os.path.exists('./config/config.json')):
|
||||||
|
os.rename('./config/config.json', './config/config.json.bak')
|
||||||
|
handle_default_config()
|
||||||
|
logger.warning('json配置文件已不再使用,已将其重命名为config.json.bak')
|
||||||
|
logger.warning('配置文件不会自动更新(因为变化太大),请手动修改配置文件重启服务器')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open("./config/config.json", "r", encoding="utf-8") as f:
|
with open("./config/config.yml", "r", encoding="utf-8") as f:
|
||||||
try:
|
try:
|
||||||
variable.config = json.loads(f.read())
|
variable.config = yaml.safe_load(f.read())
|
||||||
if (not isinstance(variable.config, dict)):
|
if (not isinstance(variable.config, dict)):
|
||||||
logger.warning('配置文件并不是一个有效的字典,使用默认值')
|
logger.warning('配置文件并不是一个有效的字典,使用默认值')
|
||||||
variable.config = default
|
variable.config = default
|
||||||
with open("./config/config.json", "w", encoding="utf-8") as f:
|
with open("./config/config.yml", "w", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(variable.config, indent=2,
|
yaml.dump(variable.config, f)
|
||||||
ensure_ascii=False, escape_forward_slashes=False))
|
|
||||||
f.close()
|
f.close()
|
||||||
except:
|
except:
|
||||||
if os.path.getsize("./config/config.json") != 0:
|
if os.path.getsize("./config/config.yml") != 0:
|
||||||
logger.error("配置文件加载失败,请检查是否遵循JSON语法规范")
|
logger.error("配置文件加载失败,请检查是否遵循YAML语法规范")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
variable.config = handle_default_config()
|
variable.config = handle_default_config()
|
||||||
|
195
common/default_config.py
Normal file
195
common/default_config.py
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
default = '''
|
||||||
|
common:
|
||||||
|
hosts: # 服务器监听地址
|
||||||
|
- 0.0.0.0
|
||||||
|
# - '::' # 取消这一行的注释,启用 ipv6 监听
|
||||||
|
ports: # 服务器启动时所使用的端口
|
||||||
|
- 9763
|
||||||
|
ssl_info: # 服务器https配置
|
||||||
|
# 这个服务器是否是https服务器,如果你使用了反向代理来转发这个服务器,如果它使用了https,也请将它设置为true
|
||||||
|
is_https: false
|
||||||
|
# python原生https监听
|
||||||
|
enable: false
|
||||||
|
ssl_ports:
|
||||||
|
- 443
|
||||||
|
path: # ssl证书的文件地址
|
||||||
|
cert: /path/to/your/cer
|
||||||
|
privkey: /path/to/your/private/key
|
||||||
|
reverse_proxy: # 针对类似于nginx一类的反代的配置
|
||||||
|
allow_public_ip: false # 允许来自公网的转发
|
||||||
|
allow_proxy: true # 是否允许反代
|
||||||
|
real_ip_header: X-Real-IP # 反代来源ip的来源头,不懂请保持默认
|
||||||
|
debug_mode: false # 是否开启调试模式
|
||||||
|
log_length_limit: 500 # 单条日志长度限制
|
||||||
|
fakeip: 1.0.1.114 # 服务器在海外时的IP伪装值
|
||||||
|
proxy: # 代理配置,HTTP与HTTPS协议需分开配置
|
||||||
|
enable: false
|
||||||
|
http_value: http://127.0.0.1:7890
|
||||||
|
https_value: http://127.0.0.1:7890
|
||||||
|
log_file: true # 是否存储日志文件
|
||||||
|
cookiepool: false # 是否开启cookie池,这将允许用户配置多个cookie并在请求时随机使用一个,启用后请在module.cookiepool中配置cookie,在user处配置的cookie会被忽略,cookiepool中格式统一为列表嵌套user处的cookie的字典
|
||||||
|
allow_download_script: true # 是否允许直接从服务端下载脚本,开启后可以直接访问 /script?key=你的请求key 下载脚本
|
||||||
|
download_config:
|
||||||
|
desc: 源脚本的相关配置,dev为是否启用开发模式
|
||||||
|
name: 修改为你的源脚本名称
|
||||||
|
intro: 修改为你的源脚本描述
|
||||||
|
author: 修改为你的源脚本作者
|
||||||
|
version: 修改为你的源版本
|
||||||
|
filename: lx-music-source.js # 客户端保存脚本时的文件名(可能因浏览器不同出现不一样的情况)
|
||||||
|
dev: true
|
||||||
|
quality:
|
||||||
|
kw: [128k]
|
||||||
|
kg: [128k]
|
||||||
|
tx: [128k]
|
||||||
|
wy: [128k]
|
||||||
|
mg: [128k]
|
||||||
|
local_music: # 服务器侧本地音乐相关配置,如果需要使用此功能请确保你的带宽足够
|
||||||
|
audio_path: ./audio
|
||||||
|
temp_path: ./temp
|
||||||
|
|
||||||
|
security:
|
||||||
|
rate_limit: # 请求速率限制 填入的值为至少间隔多久才能进行一次请求,单位:秒,不限制请填为0
|
||||||
|
global: 0 # 全局
|
||||||
|
ip: 0 # 单个IP
|
||||||
|
key:
|
||||||
|
enable: false # 是否开启请求key,开启后只有请求头中包含key,且值一样时可以访问API
|
||||||
|
ban: true
|
||||||
|
values: # 填自己所有的请求key
|
||||||
|
- '114514'
|
||||||
|
whitelist_host: # 强制白名单HOST,不需要加端口号(即不受其他安全设置影响的HOST)
|
||||||
|
- localhost
|
||||||
|
- 0.0.0.0
|
||||||
|
- 127.0.0.1
|
||||||
|
check_lxm: false # 是否检查lxm请求头(正常的LX Music在内置源请求时都会携带这个请求头)
|
||||||
|
lxm_ban: true # lxm请求头不存在或不匹配时是否将用户IP加入黑名单
|
||||||
|
allowed_host: # HOST允许列表,启用后只允许列表内的HOST访问服务器,不需要加端口号
|
||||||
|
enable: false
|
||||||
|
blacklist: # 当用户访问的HOST并不在允许列表中时是否将请求IP加入黑名单,长度单位:秒
|
||||||
|
enable: false
|
||||||
|
length: 0
|
||||||
|
list:
|
||||||
|
- localhost
|
||||||
|
- 0.0.0.0
|
||||||
|
- 127.0.0.1
|
||||||
|
banlist: # 是否启用黑名单(全局设置,关闭后已存储的值并不受影响,但不会再检查)
|
||||||
|
enable: true
|
||||||
|
expire: # 是否启用黑名单IP过期(关闭后其他地方的配置会失效)
|
||||||
|
enable: true
|
||||||
|
length: 604800
|
||||||
|
|
||||||
|
module:
|
||||||
|
kg: # 酷狗音乐相关配置
|
||||||
|
client: # 客户端请求配置,不懂请保持默认,修改请统一为字符串格式
|
||||||
|
appid: '1005' # 酷狗音乐的appid,官方安卓为1005,官方PC为1001
|
||||||
|
signatureKey: OIlwieks28dk2k092lksi2UIkp # 客户端signature采用的key值,需要与appid对应
|
||||||
|
clientver: '12029' # 客户端versioncode,pidversionsecret可能随此值而变化
|
||||||
|
pidversionsecret: 57ae12eb6890223e355ccfcb74edf70d # 获取URL时所用的key值计算验证值
|
||||||
|
pid: '2' # url接口的pid
|
||||||
|
tracker: # trackerapi请求配置,不懂请保持默认,修改请统一为字符串格式
|
||||||
|
host: https://gateway.kugou.com
|
||||||
|
path: /v5/url
|
||||||
|
version: v5
|
||||||
|
x-router: # 当host为gateway.kugou.com时需要追加此头,为tracker类地址时则不需要
|
||||||
|
enable: true
|
||||||
|
value: tracker.kugou.com
|
||||||
|
extra_params: {} # 自定义添加的param,优先级大于默认,填写类型为普通的JSON数据,会自动转换为请求param
|
||||||
|
user: # 此处内容请统一抓包获取(/v5/url),需要vip账号来获取会员歌曲,如果没有请留为空值,mid必填,可以瞎填一段数字
|
||||||
|
token: ''
|
||||||
|
userid: '0'
|
||||||
|
mid: '114514'
|
||||||
|
lite_sign_in: # 是否启用概念版自动签到,仅在appid=3116时运行
|
||||||
|
enable: false
|
||||||
|
interval: 86400
|
||||||
|
mixsongmid: # mix_songmid的获取方式, 默认auto, 可以改成一个数字手动
|
||||||
|
value: auto
|
||||||
|
refresh_token: # 酷狗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音乐相关配置
|
||||||
|
vkeyserver: # 请求官方api时使用的guid,uin等信息,不需要与cookie中信息一致
|
||||||
|
guid: '114514'
|
||||||
|
uin: '10086'
|
||||||
|
user: # 用户数据,可以通过浏览器获取,需要vip账号来获取会员歌曲,如果没有请留为空值,qqmusic_key可以从Cookie中/客户端的请求体中(comm.authst)获取
|
||||||
|
qqmusic_key: ''
|
||||||
|
uin: '' # key对应的QQ号
|
||||||
|
refresh_login: # 刷新登录相关配置,enable是否启动,interval刷新间隔
|
||||||
|
enable: false
|
||||||
|
interval: 86000
|
||||||
|
cdnaddr: http://ws.stream.qqmusic.qq.com/
|
||||||
|
|
||||||
|
wy: # 网易云音乐相关配置
|
||||||
|
user: # 账号cookie数据,可以通过浏览器获取,需要vip账号来获取会员歌曲,如果没有请留为空值
|
||||||
|
cookie: ''
|
||||||
|
|
||||||
|
mg: # 咪咕音乐相关配置
|
||||||
|
user: # 研究不深,后两项自行抓包获取,网页端cookie
|
||||||
|
by: ''
|
||||||
|
session: ''
|
||||||
|
useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36
|
||||||
|
refresh_login: # cookie保活配置
|
||||||
|
enable: false
|
||||||
|
interval: 86400
|
||||||
|
|
||||||
|
kw: # 酷我音乐相关配置,proto支持值:['bd-api', 'kuwodes']
|
||||||
|
proto: bd-api
|
||||||
|
user:
|
||||||
|
uid: '0'
|
||||||
|
token: ''
|
||||||
|
device_id: '0'
|
||||||
|
des: # kuwodes接口(mobi, nmobi)一类的加密相关配置
|
||||||
|
f: kuwo
|
||||||
|
need_encrypt: true # 是否开启kuwodes
|
||||||
|
# {songId}为歌曲id
|
||||||
|
# {map_quality}为map后的歌曲音质(酷我规范)
|
||||||
|
# {raw_quality}为请求时的歌曲音质(LX规范)
|
||||||
|
# {ext}为歌曲文件扩展名
|
||||||
|
params: type=convert_url_with_sign&rid={songId}&quality={map_quality}&ext={ext}
|
||||||
|
host: nmobi.kuwo.cn
|
||||||
|
path: mobi.s
|
||||||
|
# 这里是reponse_type的所有支持值,当设置为json时会使用到下面的两个值来获取url/bitrate,如果为text,则为传统的逐行解析方式
|
||||||
|
response_type: json
|
||||||
|
url_json_path: data.url
|
||||||
|
bitrate_json_path: data.bitrate
|
||||||
|
headers:
|
||||||
|
User-Agent: okhttp/3.10.0
|
||||||
|
|
||||||
|
cookiepool:
|
||||||
|
kg:
|
||||||
|
- userid: '0'
|
||||||
|
token: ''
|
||||||
|
mid: '114514'
|
||||||
|
lite_sign_in: # 是否启用概念版自动签到,仅在appid=3116时运行
|
||||||
|
enable: false
|
||||||
|
interval: 86400
|
||||||
|
mixsongmid: # mix_songmid的获取方式, 默认auto, 可以改成一个数字手动
|
||||||
|
value: auto
|
||||||
|
refresh_login: # cookie池中对于此账号刷新登录的配置,账号间互不干扰
|
||||||
|
enable: false
|
||||||
|
interval: 604800
|
||||||
|
login_url: http://login.user.kugou.com/v4/login_by_token
|
||||||
|
|
||||||
|
tx:
|
||||||
|
- qqmusic_key: ''
|
||||||
|
uin: ''
|
||||||
|
refresh_login: # cookie池中对于此账号刷新登录的配置,账号间互不干扰
|
||||||
|
enable: false
|
||||||
|
interval: 86000
|
||||||
|
|
||||||
|
wy:
|
||||||
|
- cookie: ''
|
||||||
|
|
||||||
|
mg:
|
||||||
|
- by: ''
|
||||||
|
session: ''
|
||||||
|
useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36
|
||||||
|
refresh_login:
|
||||||
|
enable: false
|
||||||
|
interval: 86400
|
||||||
|
|
||||||
|
kw:
|
||||||
|
- uid: '0'
|
||||||
|
token: ''
|
||||||
|
device_id: '0'
|
||||||
|
'''
|
@ -12,12 +12,14 @@ import colorlog
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
|
import io
|
||||||
import traceback
|
import traceback
|
||||||
|
import time
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
from pygments.lexers import PythonLexer
|
from pygments.lexers import PythonLexer
|
||||||
from pygments.formatters import TerminalFormatter
|
from pygments.formatters import TerminalFormatter
|
||||||
from .utils import filterFileName, addToGlobalNamespace
|
from .utils import filterFileName, setGlobal
|
||||||
from .variable import debug_mode, log_length_limit, log_file
|
from .variable import debug_mode, log_length_limit, log_file, log_files
|
||||||
from colorama import Fore, Back, Style
|
from colorama import Fore, Back, Style
|
||||||
from colorama import init as clinit
|
from colorama import init as clinit
|
||||||
|
|
||||||
@ -159,10 +161,19 @@ class LogHelper(logging.Handler):
|
|||||||
log_message = self.format(record)
|
log_message = self.format(record)
|
||||||
self.custom_logger.info(log_message)
|
self.custom_logger.info(log_message)
|
||||||
|
|
||||||
|
class fileWriter(logging.Handler):
|
||||||
|
def __init__(self, f: io.TextIOWrapper, f2: logging.Formatter):
|
||||||
|
self.file = f
|
||||||
|
self.formatter = f2
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord):
|
||||||
|
self.file.write(self.format(record) + '\n')
|
||||||
|
self.file.flush()
|
||||||
|
|
||||||
class log:
|
class log:
|
||||||
# 主类
|
# 主类
|
||||||
def __init__(self, module_name='Not named logger', output_level='INFO', filename=''):
|
def __init__(self, module_name='Not named logger', output_level='INFO', filename=''):
|
||||||
|
self.name = module_name
|
||||||
self._logger = logging.getLogger(module_name)
|
self._logger = logging.getLogger(module_name)
|
||||||
if not output_level.upper() in dir(logging):
|
if not output_level.upper() in dir(logging):
|
||||||
raise NameError('Unknown loglevel: '+output_level)
|
raise NameError('Unknown loglevel: '+output_level)
|
||||||
@ -181,21 +192,12 @@ class log:
|
|||||||
'CRITICAL': 'red,bg_white',
|
'CRITICAL': 'red,bg_white',
|
||||||
})
|
})
|
||||||
if log_file:
|
if log_file:
|
||||||
file_formatter = logging.Formatter(
|
|
||||||
'%(asctime)s|[%(name)s/%(levelname)s]|%(message)s',
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
|
||||||
)
|
|
||||||
if filename:
|
if filename:
|
||||||
filename = filterFileName(filename)
|
filename = filterFileName(filename)
|
||||||
else:
|
else:
|
||||||
filename = './logs/' + module_name + '.log'
|
filename = './logs/' + module_name + '.log'
|
||||||
file_handler = logging.FileHandler(filename, encoding="utf-8")
|
self.file = open(filename, 'a+', encoding='utf-8')
|
||||||
file_handler.setFormatter(file_formatter)
|
log_files.append(self.file)
|
||||||
file_handler_ = logging.FileHandler(
|
|
||||||
"./logs/console_full.log", encoding="utf-8")
|
|
||||||
file_handler_.setFormatter(file_formatter)
|
|
||||||
self._logger.addHandler(file_handler_)
|
|
||||||
self._logger.addHandler(file_handler)
|
|
||||||
console_handler = logging.StreamHandler()
|
console_handler = logging.StreamHandler()
|
||||||
console_handler.setFormatter(formatter)
|
console_handler.setFormatter(formatter)
|
||||||
self.module_name = module_name
|
self.module_name = module_name
|
||||||
@ -204,61 +206,55 @@ class log:
|
|||||||
debug_handler.setFormatter(formatter)
|
debug_handler.setFormatter(formatter)
|
||||||
|
|
||||||
def debug(self, message, allow_hidden=True):
|
def debug(self, message, allow_hidden=True):
|
||||||
if self.module_name == "flask" and "\n" in message:
|
if (log_file):
|
||||||
if message.startswith("Error"):
|
self.file.write('{time}|[{name}/DEBUG]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
return self._logger.error(message)
|
|
||||||
for m in message.split("\n"):
|
|
||||||
if "WARNING" in m:
|
|
||||||
self._logger.warning(m)
|
|
||||||
else:
|
|
||||||
self._logger.info(m)
|
|
||||||
return
|
|
||||||
if len(str(message)) > log_length_limit and allow_hidden:
|
if len(str(message)) > log_length_limit and allow_hidden:
|
||||||
message = str(message)[:log_length_limit] + " ..."
|
message = str(message)[:log_length_limit] + " ..."
|
||||||
self._logger.debug(message)
|
self._logger.debug(message)
|
||||||
|
|
||||||
def log(self, message, allow_hidden=True):
|
def log(self, message, allow_hidden=True):
|
||||||
if self.module_name == "flask" and "\n" in message:
|
if (log_file):
|
||||||
if message.startswith("Error"):
|
self.file.write('{time}|[{name}/INFO]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
return self._logger.error(message)
|
|
||||||
for m in message.split("\n"):
|
|
||||||
if "WARNING" in m:
|
|
||||||
self._logger.warning(m)
|
|
||||||
else:
|
|
||||||
self._logger.info(m)
|
|
||||||
return
|
|
||||||
if len(str(message)) > log_length_limit and allow_hidden:
|
if len(str(message)) > log_length_limit and allow_hidden:
|
||||||
message = str(message)[:log_length_limit] + " ..."
|
message = str(message)[:log_length_limit] + " ..."
|
||||||
self._logger.info(message)
|
self._logger.info(message)
|
||||||
|
|
||||||
def info(self, message, allow_hidden=True):
|
def info(self, message, allow_hidden=True):
|
||||||
if self.module_name == "flask" and "\n" in message:
|
if (log_file):
|
||||||
if message.startswith("Error"):
|
self.file.write('{time}|[{name}/INFO]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
return self._logger.error(message)
|
|
||||||
for m in message.split("\n"):
|
|
||||||
if "WARNING" in m:
|
|
||||||
self._logger.warning(m)
|
|
||||||
else:
|
|
||||||
self._logger.info(m)
|
|
||||||
return
|
|
||||||
if len(str(message)) > log_length_limit and allow_hidden:
|
if len(str(message)) > log_length_limit and allow_hidden:
|
||||||
message = str(message)[:log_length_limit] + "..."
|
message = str(message)[:log_length_limit] + "..."
|
||||||
self._logger.info(message)
|
self._logger.info(message)
|
||||||
|
|
||||||
def warning(self, message):
|
def warning(self, message):
|
||||||
|
if (log_file):
|
||||||
|
self.file.write('{time}|[{name}/WARNING]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
|
|
||||||
if (message.strip().startswith('Traceback')):
|
if (message.strip().startswith('Traceback')):
|
||||||
self._logger.warning('\n' + highlight_error(message))
|
self._logger.warning('\n' + highlight_error(message))
|
||||||
else:
|
else:
|
||||||
self._logger.warning(message)
|
self._logger.warning(message)
|
||||||
|
|
||||||
def error(self, message):
|
def error(self, message):
|
||||||
|
if (log_file):
|
||||||
|
self.file.write('{time}|[{name}/ERROR]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
|
|
||||||
if (message.startswith('Traceback')):
|
if (message.startswith('Traceback')):
|
||||||
self._logger.error('\n' + highlight_error(message))
|
self._logger.error('\n' + highlight_error(message))
|
||||||
else:
|
else:
|
||||||
self._logger.error(message)
|
self._logger.error(message)
|
||||||
|
|
||||||
def critical(self, message):
|
def critical(self, message):
|
||||||
self._logger.critical(message)
|
if (log_file):
|
||||||
|
self.file.write('{time}|[{name}/CRITICAL]{msg}'.format(time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), name = self.module_name, msg = message) + '\n')
|
||||||
|
|
||||||
|
if (message.startswith('Traceback')):
|
||||||
|
self._logger.critical('\n' + highlight_error(message))
|
||||||
|
else:
|
||||||
|
self._logger.critical(message)
|
||||||
|
|
||||||
def set_level(self, loglevel):
|
def set_level(self, loglevel):
|
||||||
loglevel_upper = loglevel.upper()
|
loglevel_upper = loglevel.upper()
|
||||||
@ -280,4 +276,4 @@ def logprint(*args, sep=' ', end='', file=None, flush=None):
|
|||||||
printlogger.info(sep.join(str(arg) for arg in args), allow_hidden=False)
|
printlogger.info(sep.join(str(arg) for arg in args), allow_hidden=False)
|
||||||
|
|
||||||
|
|
||||||
addToGlobalNamespace('print', logprint)
|
setGlobal(logprint, 'print')
|
||||||
|
@ -37,8 +37,8 @@ async def get_response(retry = 0):
|
|||||||
if (retry > 21):
|
if (retry > 21):
|
||||||
logger.warning('请求源脚本内容失败')
|
logger.warning('请求源脚本内容失败')
|
||||||
return
|
return
|
||||||
baseurl = '/lxmusics/lx-music-api-server/main/lx-music-source-example.js'
|
baseurl = '/MeoProject/lx-music-api-server/main/lx-music-source-example.js'
|
||||||
jsdbaseurl = '/gh/lxmusics/lx-music-api-server@main/lx-music-source-example.js'
|
jsdbaseurl = '/gh/MeoProject/lx-music-api-server@main/lx-music-source-example.js'
|
||||||
try:
|
try:
|
||||||
i = retry
|
i = retry
|
||||||
if (i > 10):
|
if (i > 10):
|
||||||
|
@ -40,6 +40,9 @@ class taskWrapper:
|
|||||||
logger.error(f"task {self.name} run failed, waiting for next execute...")
|
logger.error(f"task {self.name} run failed, waiting for next execute...")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'SchedulerTaskWrapper(name="{self.name}", interval={self.interval}, function={self.function}, args={self.args}, latest_execute={self.latest_execute})'
|
||||||
|
|
||||||
def append(name, task, interval = 86400, args = {}):
|
def append(name, task, interval = 86400, args = {}):
|
||||||
global tasks
|
global tasks
|
||||||
wrapper = taskWrapper(name, task, interval, args)
|
wrapper = taskWrapper(name, task, interval, args)
|
||||||
|
@ -16,6 +16,7 @@ import zlib
|
|||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
import ipaddress
|
||||||
from urllib.parse import quote, unquote, urlparse
|
from urllib.parse import quote, unquote, urlparse
|
||||||
|
|
||||||
def createBase64Encode(data_bytes):
|
def createBase64Encode(data_bytes):
|
||||||
@ -51,8 +52,8 @@ def require(module):
|
|||||||
index += 1
|
index += 1
|
||||||
return _module
|
return _module
|
||||||
|
|
||||||
def addToGlobalNamespace(key, data):
|
def setGlobal(obj, key = ''):
|
||||||
setattr(builtins, key, data)
|
setattr(builtins, obj.__name__ if (not key) else key, obj)
|
||||||
|
|
||||||
def filterFileName(filename):
|
def filterFileName(filename):
|
||||||
if platform.system() == 'Windows' or platform.system() == 'Cygwin':
|
if platform.system() == 'Windows' or platform.system() == 'Cygwin':
|
||||||
@ -198,5 +199,11 @@ def timestamp_format(t):
|
|||||||
t = int(t)
|
t = int(t)
|
||||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t))
|
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t))
|
||||||
|
|
||||||
addToGlobalNamespace('require', require)
|
def is_local_ip(ip):
|
||||||
|
try:
|
||||||
|
i = ipaddress.ip_address(ip)
|
||||||
|
return i.is_private
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
setGlobal(require)
|
@ -1,22 +1,24 @@
|
|||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# - mode: python -
|
# - mode: python -
|
||||||
# - author: helloplhm-qwq -
|
# - author: helloplhm-qwq -
|
||||||
# - name: variable.py -
|
# - name: variable.py -
|
||||||
# - project: lx-music-api-server -
|
# - project: lx-music-api-server -
|
||||||
# - license: MIT -
|
# - license: MIT -
|
||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# This file is part of the "lx-music-api-server" project.
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
import os as _os
|
import os as _os
|
||||||
import ujson as _json
|
import ujson as _json
|
||||||
|
|
||||||
|
|
||||||
def _read_config_file():
|
def _read_config_file():
|
||||||
try:
|
try:
|
||||||
with open("./config/config.json", "r", encoding = "utf-8") as f:
|
with open("./config/config.json", "r", encoding="utf-8") as f:
|
||||||
return _json.load(f)
|
return _json.load(f)
|
||||||
except:
|
except:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _read_config(key):
|
def _read_config(key):
|
||||||
try:
|
try:
|
||||||
config = _read_config_file()
|
config = _read_config_file()
|
||||||
@ -36,12 +38,15 @@ def _read_config(key):
|
|||||||
return value
|
return value
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
_dm = _read_config("common.debug_mode")
|
_dm = _read_config("common.debug_mode")
|
||||||
_lm = _read_config("common.log_file")
|
_lm = _read_config("common.log_file")
|
||||||
_ll = _read_config("common.log_length_limit")
|
_ll = _read_config("common.log_length_limit")
|
||||||
debug_mode = _dm if (_dm) else False
|
debug_mode = True if (_os.getenv('CURRENT_ENV') ==
|
||||||
|
'development') else (_dm if (_dm) else False)
|
||||||
log_length_limit = _ll if (_ll) else 500
|
log_length_limit = _ll if (_ll) else 500
|
||||||
log_file = _lm if (isinstance(_lm , bool)) else True
|
log_file = _lm if (isinstance(_lm, bool)) else True
|
||||||
running = True
|
running = True
|
||||||
config = {}
|
config = {}
|
||||||
workdir = _os.getcwd()
|
workdir = _os.getcwd()
|
||||||
@ -55,3 +60,4 @@ running_ports = []
|
|||||||
use_proxy = False
|
use_proxy = False
|
||||||
http_proxy = ''
|
http_proxy = ''
|
||||||
https_proxy = ''
|
https_proxy = ''
|
||||||
|
log_files = []
|
||||||
|
149
main.py
149
main.py
@ -1,22 +1,33 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# - mode: python -
|
# - mode: python -
|
||||||
# - author: helloplhm-qwq -
|
# - author: helloplhm-qwq -
|
||||||
# - name: main.py -
|
# - name: main.py -
|
||||||
# - project: lx-music-api-server -
|
# - project: lx-music-api-server -
|
||||||
# - license: MIT -
|
# - license: MIT -
|
||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# This file is part of the "lx-music-api-server" project.
|
# This file is part of the "lx-music-api-server" project.
|
||||||
|
|
||||||
|
import time
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
import threading
|
||||||
|
import ujson as json
|
||||||
|
from aiohttp.web import Response, FileResponse, StreamResponse
|
||||||
|
from io import TextIOWrapper
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
from common.utils import createBase64Decode
|
|
||||||
|
|
||||||
if ((sys.version_info.major == 3 and sys.version_info.minor < 6) or sys.version_info.major == 2):
|
if ((sys.version_info.major == 3 and sys.version_info.minor < 6) or sys.version_info.major == 2):
|
||||||
print('Python版本过低,请使用Python 3.6+ ')
|
print('Python版本过低,请使用Python 3.6+ ')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# fix: module not found: common/modules
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from common import utils
|
||||||
from common import config, localMusic
|
from common import config, localMusic
|
||||||
from common import lxsecurity
|
from common import lxsecurity
|
||||||
from common import log
|
from common import log
|
||||||
@ -24,24 +35,17 @@ from common import Httpx
|
|||||||
from common import variable
|
from common import variable
|
||||||
from common import scheduler
|
from common import scheduler
|
||||||
from common import lx_script
|
from common import lx_script
|
||||||
from aiohttp.web import Response, FileResponse, StreamResponse
|
|
||||||
import ujson as json
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
import modules
|
import modules
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
|
|
||||||
def handleResult(dic, status = 200) -> Response:
|
def handleResult(dic, status=200) -> Response:
|
||||||
if (not isinstance(dic, dict)):
|
if (not isinstance(dic, dict)):
|
||||||
dic = {
|
dic = {
|
||||||
'code': 0,
|
'code': 0,
|
||||||
'msg': 'success',
|
'msg': 'success',
|
||||||
'data': dic
|
'data': dic
|
||||||
}
|
}
|
||||||
return Response(body = json.dumps(dic, indent=2, ensure_ascii=False), content_type='application/json', status = status)
|
return Response(body=json.dumps(dic, indent=2, ensure_ascii=False), content_type='application/json', status=status)
|
||||||
|
|
||||||
|
|
||||||
logger = log.log("main")
|
logger = log.log("main")
|
||||||
aiologger = log.log('aiohttp_web')
|
aiologger = log.log('aiohttp_web')
|
||||||
@ -54,20 +58,24 @@ if (sys.version_info.minor < 8 and sys.version_info.major == 3):
|
|||||||
else:
|
else:
|
||||||
stopEvent = asyncio.exceptions.CancelledError
|
stopEvent = asyncio.exceptions.CancelledError
|
||||||
|
|
||||||
|
|
||||||
def start_checkcn_thread() -> None:
|
def start_checkcn_thread() -> None:
|
||||||
threading.Thread(target=Httpx.checkcn).start()
|
threading.Thread(target=Httpx.checkcn).start()
|
||||||
|
|
||||||
# check request info before start
|
# check request info before start
|
||||||
|
|
||||||
|
|
||||||
async def handle_before_request(app, handler):
|
async def handle_before_request(app, handler):
|
||||||
async def handle_request(request):
|
async def handle_request(request):
|
||||||
try:
|
try:
|
||||||
if (config.read_config('common.reverse_proxy.allow_proxy')):
|
if (config.read_config('common.reverse_proxy.allow_proxy')):
|
||||||
if (request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))):
|
if (request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))):
|
||||||
# proxy header
|
# proxy header
|
||||||
if (request.remote in config.read_config('common.reverse_proxy.proxy_whitelist_remote')):
|
if (config.read_config('common.reverse_proxy.allow_public_ip') and (not utils.is_private_ip(request.remote))):
|
||||||
request.remote_addr = request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))
|
request.remote_addr = request.headers.get(
|
||||||
|
config.read_config('common.reverse_proxy.real_ip_header'))
|
||||||
else:
|
else:
|
||||||
return handleResult({"code": 1, "msg": "反代客户端远程地址不在反代ip白名单中", "data": None}, 403)
|
return handleResult({"code": 1, "msg": "不允许的公网ip转发", "data": None}, 403)
|
||||||
else:
|
else:
|
||||||
request.remote_addr = request.remote
|
request.remote_addr = request.remote
|
||||||
else:
|
else:
|
||||||
@ -80,13 +88,13 @@ async def handle_before_request(app, handler):
|
|||||||
(time.time() - config.getRequestTime('global'))
|
(time.time() - config.getRequestTime('global'))
|
||||||
<
|
<
|
||||||
(config.read_config("security.rate_limit.global"))
|
(config.read_config("security.rate_limit.global"))
|
||||||
):
|
):
|
||||||
return handleResult({"code": 5, "msg": "全局限速", "data": None}, 429)
|
return handleResult({"code": 5, "msg": "全局限速", "data": None}, 429)
|
||||||
if (
|
if (
|
||||||
(time.time() - config.getRequestTime(request.remote_addr))
|
(time.time() - config.getRequestTime(request.remote_addr))
|
||||||
<
|
<
|
||||||
(config.read_config("security.rate_limit.ip"))
|
(config.read_config("security.rate_limit.ip"))
|
||||||
):
|
):
|
||||||
return handleResult({"code": 5, "msg": "IP限速", "data": None}, 429)
|
return handleResult({"code": 5, "msg": "IP限速", "data": None}, 429)
|
||||||
# update request time
|
# update request time
|
||||||
config.updateRequestTime('global')
|
config.updateRequestTime('global')
|
||||||
@ -95,27 +103,32 @@ async def handle_before_request(app, handler):
|
|||||||
if (config.read_config("security.allowed_host.enable")):
|
if (config.read_config("security.allowed_host.enable")):
|
||||||
if request.host.split(":")[0] not in config.read_config("security.allowed_host.list"):
|
if request.host.split(":")[0] not in config.read_config("security.allowed_host.list"):
|
||||||
if config.read_config("security.allowed_host.blacklist.enable"):
|
if config.read_config("security.allowed_host.blacklist.enable"):
|
||||||
config.ban_ip(request.remote_addr, int(config.read_config("security.allowed_host.blacklist.length")))
|
config.ban_ip(request.remote_addr, int(
|
||||||
|
config.read_config("security.allowed_host.blacklist.length")))
|
||||||
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
||||||
|
|
||||||
resp = await handler(request)
|
resp = await handler(request)
|
||||||
if (isinstance(resp, (str, list, dict))):
|
if (isinstance(resp, (str, list, dict))):
|
||||||
resp = handleResult(resp)
|
resp = handleResult(resp)
|
||||||
elif (isinstance(resp, tuple) and len(resp) == 2): # flask like response
|
elif (isinstance(resp, tuple) and len(resp) == 2): # flask like response
|
||||||
body, status = resp
|
body, status = resp
|
||||||
if (isinstance(body, (str, list, dict))):
|
if (isinstance(body, (str, list, dict))):
|
||||||
resp = handleResult(body, status)
|
resp = handleResult(body, status)
|
||||||
else:
|
else:
|
||||||
resp = Response(body = str(body), content_type='text/plain', status = status)
|
resp = Response(
|
||||||
|
body=str(body), content_type='text/plain', status=status)
|
||||||
elif (not isinstance(resp, (Response, FileResponse, StreamResponse))):
|
elif (not isinstance(resp, (Response, FileResponse, StreamResponse))):
|
||||||
resp = Response(body = str(resp), content_type='text/plain', status = 200)
|
resp = Response(
|
||||||
aiologger.info(f'{request.remote_addr + ("" if (request.remote == request.remote_addr) else f"|proxy@{request.remote}")} - {request.method} "{request.path}", {resp.status}')
|
body=str(resp), content_type='text/plain', status=200)
|
||||||
|
aiologger.info(
|
||||||
|
f'{request.remote_addr + ("" if (request.remote == request.remote_addr) else f"|proxy@{request.remote}")} - {request.method} "{request.path}", {resp.status}')
|
||||||
return resp
|
return resp
|
||||||
except:
|
except:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"code": 4, "msg": "内部服务器错误", "data": None}
|
return {"code": 4, "msg": "内部服务器错误", "data": None}
|
||||||
return handle_request
|
return handle_request
|
||||||
|
|
||||||
|
|
||||||
async def main(request):
|
async def main(request):
|
||||||
return handleResult({"code": 0, "msg": "success", "data": None})
|
return handleResult({"code": 0, "msg": "success", "data": None})
|
||||||
|
|
||||||
@ -136,7 +149,7 @@ async def handle(request):
|
|||||||
if (config.read_config('security.lxm_ban.enable')):
|
if (config.read_config('security.lxm_ban.enable')):
|
||||||
config.ban_ip(request.remote_addr)
|
config.ban_ip(request.remote_addr)
|
||||||
return handleResult({"code": 1, "msg": "lxm请求头验证失败", "data": None}, 403)
|
return handleResult({"code": 1, "msg": "lxm请求头验证失败", "data": None}, 403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query = dict(request.query)
|
query = dict(request.query)
|
||||||
if (method in dir(modules)):
|
if (method in dir(modules)):
|
||||||
@ -147,14 +160,17 @@ async def handle(request):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return handleResult({'code': 4, 'msg': '内部服务器错误', 'data': None}, 500)
|
return handleResult({'code': 4, 'msg': '内部服务器错误', 'data': None}, 500)
|
||||||
|
|
||||||
|
|
||||||
async def handle_404(request):
|
async def handle_404(request):
|
||||||
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
|
||||||
|
|
||||||
|
|
||||||
async def handle_local(request):
|
async def handle_local(request):
|
||||||
try:
|
try:
|
||||||
query = dict(request.query)
|
query = dict(request.query)
|
||||||
data = query.get('q')
|
data = query.get('q')
|
||||||
data = createBase64Decode(data.replace('-', '+').replace('_', '/'))
|
data = utils.createBase64Decode(
|
||||||
|
data.replace('-', '+').replace('_', '/'))
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
t = request.match_info.get('type')
|
t = request.match_info.get('type')
|
||||||
data['t'] = t
|
data['t'] = t
|
||||||
@ -208,28 +224,30 @@ if (config.read_config('common.allow_download_script')):
|
|||||||
# 404
|
# 404
|
||||||
app.router.add_route('*', '/{tail:.*}', handle_404)
|
app.router.add_route('*', '/{tail:.*}', handle_404)
|
||||||
|
|
||||||
async def run_app():
|
|
||||||
|
async def run_app_host(host):
|
||||||
retries = 0
|
retries = 0
|
||||||
while True:
|
while True:
|
||||||
if (retries > 4):
|
if (retries > 4):
|
||||||
logger.warning("重试次数已达上限,但仍有部分端口未能完成监听,已自动进行忽略")
|
logger.warning("重试次数已达上限,但仍有部分端口未能完成监听,已自动进行忽略")
|
||||||
return
|
break
|
||||||
try:
|
try:
|
||||||
host = config.read_config('common.host')
|
ports = [int(port)
|
||||||
ports = [int(port) for port in config.read_config('common.ports')]
|
for port in config.read_config('common.ports')]
|
||||||
ssl_ports = [int(port) for port in config.read_config('common.ssl_info.ssl_ports')]
|
ssl_ports = [int(port) for port in config.read_config(
|
||||||
|
'common.ssl_info.ssl_ports')]
|
||||||
final_ssl_ports = []
|
final_ssl_ports = []
|
||||||
final_ports = []
|
final_ports = []
|
||||||
for p in ports:
|
for p in ports:
|
||||||
if (p not in ssl_ports and p not in variable.running_ports):
|
if (p not in ssl_ports and f'{host}_{p}' not in variable.running_ports):
|
||||||
final_ports.append(p)
|
final_ports.append(p)
|
||||||
else:
|
else:
|
||||||
if (p not in variable.running_ports):
|
if (p not in variable.running_ports):
|
||||||
final_ssl_ports.append(p)
|
final_ssl_ports.append(p)
|
||||||
# 读取证书和私钥路径
|
# 读取证书和私钥路径
|
||||||
cert_path = config.read_config('common.ssl_info.path.cert')
|
cert_path = config.read_config('common.ssl_info.path.cert')
|
||||||
privkey_path = config.read_config('common.ssl_info.path.privkey')
|
privkey_path = config.read_config(
|
||||||
|
'common.ssl_info.path.privkey')
|
||||||
|
|
||||||
# 创建 HTTP AppRunner
|
# 创建 HTTP AppRunner
|
||||||
http_runner = aiohttp.web.AppRunner(app)
|
http_runner = aiohttp.web.AppRunner(app)
|
||||||
@ -238,16 +256,21 @@ async def run_app():
|
|||||||
# 启动 HTTP 端口监听
|
# 启动 HTTP 端口监听
|
||||||
for port in final_ports:
|
for port in final_ports:
|
||||||
if (port not in variable.running_ports):
|
if (port not in variable.running_ports):
|
||||||
http_site = aiohttp.web.TCPSite(http_runner, host, port)
|
http_site = aiohttp.web.TCPSite(
|
||||||
|
http_runner, host, port)
|
||||||
await http_site.start()
|
await http_site.start()
|
||||||
variable.running_ports.append(port)
|
variable.running_ports.append(f'{host}_{port}')
|
||||||
logger.info(f"监听 -> http://{host}:{port}")
|
logger.info(f"""监听 -> http://{
|
||||||
|
host if (':' not in host)
|
||||||
|
else '[' + host + ']'
|
||||||
|
}:{port}""")
|
||||||
|
|
||||||
if (config.read_config("common.ssl_info.enable") and final_ssl_ports != []):
|
if (config.read_config("common.ssl_info.enable") and final_ssl_ports != []):
|
||||||
if (os.path.exists(cert_path) and os.path.exists(privkey_path)):
|
if (os.path.exists(cert_path) and os.path.exists(privkey_path)):
|
||||||
import ssl
|
import ssl
|
||||||
# 创建 SSL 上下文,加载配置文件中指定的证书和私钥
|
# 创建 SSL 上下文,加载配置文件中指定的证书和私钥
|
||||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
ssl_context = ssl.create_default_context(
|
||||||
|
ssl.Purpose.CLIENT_AUTH)
|
||||||
ssl_context.load_cert_chain(cert_path, privkey_path)
|
ssl_context.load_cert_chain(cert_path, privkey_path)
|
||||||
|
|
||||||
# 创建 HTTPS AppRunner
|
# 创建 HTTPS AppRunner
|
||||||
@ -257,12 +280,16 @@ async def run_app():
|
|||||||
# 启动 HTTPS 端口监听
|
# 启动 HTTPS 端口监听
|
||||||
for port in ssl_ports:
|
for port in ssl_ports:
|
||||||
if (port not in variable.running_ports):
|
if (port not in variable.running_ports):
|
||||||
https_site = aiohttp.web.TCPSite(https_runner, host, port, ssl_context=ssl_context)
|
https_site = aiohttp.web.TCPSite(
|
||||||
|
https_runner, host, port, ssl_context=ssl_context)
|
||||||
await https_site.start()
|
await https_site.start()
|
||||||
variable.running_ports.append(port)
|
variable.running_ports.append(f'{host}_{port}')
|
||||||
logger.info(f"监听 -> https://{host}:{port}")
|
logger.info(f"""监听 -> http://{
|
||||||
|
host if (':' not in host)
|
||||||
return
|
else '[' + host + ']'
|
||||||
|
}:{port}""")
|
||||||
|
logger.debug(f"HOST({host}) 已完成监听")
|
||||||
|
break
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if (str(e).startswith("[Errno 98]") or str(e).startswith('[Errno 10048]')):
|
if (str(e).startswith("[Errno 98]") or str(e).startswith('[Errno 10048]')):
|
||||||
logger.error("端口已被占用,请检查\n" + str(e))
|
logger.error("端口已被占用,请检查\n" + str(e))
|
||||||
@ -271,7 +298,12 @@ async def run_app():
|
|||||||
logger.info('重新尝试启动...')
|
logger.info('重新尝试启动...')
|
||||||
retries += 1
|
retries += 1
|
||||||
else:
|
else:
|
||||||
raise
|
logger.error("未知错误,请检查\n" + traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
async def run_app():
|
||||||
|
for host in config.read_config('common.hosts'):
|
||||||
|
await run_app_host(host)
|
||||||
|
|
||||||
|
|
||||||
async def initMain():
|
async def initMain():
|
||||||
@ -294,7 +326,7 @@ async def initMain():
|
|||||||
logger.info('wating for sessions to complete...')
|
logger.info('wating for sessions to complete...')
|
||||||
if variable.aioSession:
|
if variable.aioSession:
|
||||||
await variable.aioSession.close()
|
await variable.aioSession.close()
|
||||||
|
|
||||||
variable.running = False
|
variable.running = False
|
||||||
logger.info("Server stopped")
|
logger.info("Server stopped")
|
||||||
|
|
||||||
@ -305,5 +337,20 @@ if __name__ == "__main__":
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
except:
|
except:
|
||||||
logger.error('初始化出错,请检查日志')
|
logger.critical('初始化出错,请检查日志')
|
||||||
logger.error(traceback.format_exc())
|
logger.critical(traceback.format_exc())
|
||||||
|
with open('dumprecord_{}.txt'.format(int(time.time())), 'w', encoding='utf-8') as f:
|
||||||
|
f.write(traceback.format_exc())
|
||||||
|
e = '\n\nGlobal variable object:\n\n'
|
||||||
|
for k in dir(variable):
|
||||||
|
e += (k + ' = ' + str(getattr(variable, k)) + '\n') if (not k.startswith('_')) else ''
|
||||||
|
f.write(e)
|
||||||
|
e = '\n\nsys.modules:\n\n'
|
||||||
|
for k in sys.modules:
|
||||||
|
e += (k + ' = ' + str(sys.modules[k]) + '\n') if (not k.startswith('_')) else ''
|
||||||
|
f.write(e)
|
||||||
|
logger.critical('dumprecord_{}.txt 已保存至当前目录'.format(int(time.time())))
|
||||||
|
finally:
|
||||||
|
for f in variable.log_files:
|
||||||
|
if (f and isinstance(f, TextIOWrapper)):
|
||||||
|
f.close()
|
||||||
|
@ -22,6 +22,7 @@ async def refresh():
|
|||||||
return
|
return
|
||||||
if (not config.read_config('module.tx.user.refresh_login.enable')):
|
if (not config.read_config('module.tx.user.refresh_login.enable')):
|
||||||
return
|
return
|
||||||
|
print(config.read_config('module.tx.user.qqmusic_key'))
|
||||||
if (config.read_config('module.tx.user.qqmusic_key').startswith('W_X')):
|
if (config.read_config('module.tx.user.qqmusic_key').startswith('W_X')):
|
||||||
options = {
|
options = {
|
||||||
'method': 'POST',
|
'method': 'POST',
|
||||||
|
11
script.py
Normal file
11
script.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# https://github.com/python-poetry/poetry/issues/241#issuecomment-445434646
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name): # python 3.7+, otherwise define each script manually
|
||||||
|
name = name.replace('_', '-')
|
||||||
|
subprocess.run(
|
||||||
|
['python', '-u', '-m', name] + sys.argv[1:]
|
||||||
|
) # run whatever you like based on 'name'
|
24
setup.py
Normal file
24
setup.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
import toml
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
|
||||||
|
description = toml.load("./pyproject.toml")["tool"]["poetry"]["description"]
|
||||||
|
except Exception:
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Description not available"
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='lx_music_api_server_setup',
|
||||||
|
version=version,
|
||||||
|
scripts=['poetry_run.py'],
|
||||||
|
author='helloplhm-qwq',
|
||||||
|
author_email='helloplhm-qwq@outlook.com',
|
||||||
|
description=description,
|
||||||
|
url='https://github.com/helloplhm-qwq/lx-music-api-server',
|
||||||
|
classifiers=[
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
],
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user