feat: 更新太大了不想总结自己去看提交记录吧(已知把配置文件换成了yaml

This commit is contained in:
helloplhm-qwq 2024-04-14 19:06:36 +08:00
parent 45e2e7147d
commit 667d420499
No known key found for this signature in database
GPG Key ID: 6BE1B64B905567C7
13 changed files with 432 additions and 412 deletions

4
.gitignore vendored
View File

@ -22,6 +22,7 @@ test.*
*/test.*
logs
config.json
config.yml
/config/config.json
/config/data.db
*.log
@ -41,3 +42,6 @@ config.json
# temp script
lx-music-source-example.js
# dumprecord
dumprecord_*.txt

View File

@ -30,7 +30,7 @@ def get_changelog():
noticeMsg = []
unknownMsg = []
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]
if msg[8:].startswith('notice'):
noticeMsg.append(msg)

View File

@ -14,8 +14,10 @@ import traceback
import sys
import sqlite3
import shutil
import ruamel.yaml as yaml
from . import variable
from .log import log
from . import default_config
import threading
logger = log('config_manager')
@ -45,302 +47,17 @@ class ConfigReadException(Exception):
pass
default = {
"common": {
"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": "客户端versioncodepidversionsecret可能随此值而变化",
"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时使用的guiduin等信息不需要与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",
},
]
},
},
}
default_str = default_config.default
default = yaml.safe_load(default_str)
def handle_default_config():
with open("./config/config.json", "w", encoding="utf-8") as f:
f.write(json.dumps(default, indent=2, ensure_ascii=False,
escape_forward_slashes=False))
f.close()
with open("./config/config.yml", "w", encoding="utf-8") as f:
f.write(default_str)
if (not os.getenv('build')):
logger.info('首次启动或配置文件被删除,已创建默认配置文件')
logger.info(
f'\n建议您到{variable.workdir + os.path.sep}config.json修改配置后重新启动服务器')
f'\n建议您到{variable.workdir + os.path.sep}config.yml修改配置后重新启动服务器')
return default
@ -525,8 +242,8 @@ def push_to_list(key, obj):
def write_config(key, value):
config = None
with open('./config/config.json', 'r', encoding='utf-8') as f:
config = json.load(f)
with open('./config/config.yml', 'r', encoding='utf-8') as f:
config = yaml.YAML().load(f)
keys = key.split('.')
current = config
@ -536,11 +253,15 @@ def write_config(key, value):
current = current[k]
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,
escape_forward_slashes=False)
f.close()
# 设置保留注释和空行的参数
y = yaml.YAML()
y.preserve_quotes = True
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):
@ -639,21 +360,26 @@ def initConfig():
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('配置文件不会自动更新(因为变化太大),请手动修改配置文件重启服务器')
sys.exit(0)
try:
with open("./config/config.json", "r", encoding="utf-8") as f:
with open("./config/config.yml", "r", encoding="utf-8") as f:
try:
variable.config = json.loads(f.read())
variable.config = yaml.safe_load(f.read())
if (not isinstance(variable.config, dict)):
logger.warning('配置文件并不是一个有效的字典,使用默认值')
variable.config = default
with open("./config/config.json", "w", encoding="utf-8") as f:
f.write(json.dumps(variable.config, indent=2,
ensure_ascii=False, escape_forward_slashes=False))
with open("./config/config.yml", "w", encoding="utf-8") as f:
yaml.dump(variable.config, f)
f.close()
except:
if os.path.getsize("./config/config.json") != 0:
logger.error("配置文件加载失败,请检查是否遵循JSON语法规范")
if os.path.getsize("./config/config.yml") != 0:
logger.error("配置文件加载失败,请检查是否遵循YAML语法规范")
sys.exit(1)
else:
variable.config = handle_default_config()

195
common/default_config.py Normal file
View 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' # 客户端versioncodepidversionsecret可能随此值而变化
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时使用的guiduin等信息不需要与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'
'''

View File

@ -12,12 +12,14 @@ import colorlog
import os
import sys
import re
import io
import traceback
import time
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from .utils import filterFileName, addToGlobalNamespace
from .variable import debug_mode, log_length_limit, log_file
from .utils import filterFileName, setGlobal
from .variable import debug_mode, log_length_limit, log_file, log_files
from colorama import Fore, Back, Style
from colorama import init as clinit
@ -159,10 +161,19 @@ class LogHelper(logging.Handler):
log_message = self.format(record)
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:
# 主类
def __init__(self, module_name='Not named logger', output_level='INFO', filename=''):
self.name = module_name
self._logger = logging.getLogger(module_name)
if not output_level.upper() in dir(logging):
raise NameError('Unknown loglevel: '+output_level)
@ -181,21 +192,12 @@ class log:
'CRITICAL': 'red,bg_white',
})
if log_file:
file_formatter = logging.Formatter(
'%(asctime)s|[%(name)s/%(levelname)s]|%(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
if filename:
filename = filterFileName(filename)
else:
filename = './logs/' + module_name + '.log'
file_handler = logging.FileHandler(filename, encoding="utf-8")
file_handler.setFormatter(file_formatter)
file_handler_ = logging.FileHandler(
"./logs/console_full.log", encoding="utf-8")
file_handler_.setFormatter(file_formatter)
self._logger.addHandler(file_handler_)
self._logger.addHandler(file_handler)
self.file = open(filename, 'a+', encoding='utf-8')
log_files.append(self.file)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
self.module_name = module_name
@ -204,61 +206,55 @@ class log:
debug_handler.setFormatter(formatter)
def debug(self, message, allow_hidden=True):
if self.module_name == "flask" and "\n" in message:
if message.startswith("Error"):
return self._logger.error(message)
for m in message.split("\n"):
if "WARNING" in m:
self._logger.warning(m)
else:
self._logger.info(m)
return
if (log_file):
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')
if len(str(message)) > log_length_limit and allow_hidden:
message = str(message)[:log_length_limit] + " ..."
self._logger.debug(message)
def log(self, message, allow_hidden=True):
if self.module_name == "flask" and "\n" in message:
if message.startswith("Error"):
return self._logger.error(message)
for m in message.split("\n"):
if "WARNING" in m:
self._logger.warning(m)
else:
self._logger.info(m)
return
if (log_file):
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')
if len(str(message)) > log_length_limit and allow_hidden:
message = str(message)[:log_length_limit] + " ..."
self._logger.info(message)
def info(self, message, allow_hidden=True):
if self.module_name == "flask" and "\n" in message:
if message.startswith("Error"):
return self._logger.error(message)
for m in message.split("\n"):
if "WARNING" in m:
self._logger.warning(m)
else:
self._logger.info(m)
return
if (log_file):
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')
if len(str(message)) > log_length_limit and allow_hidden:
message = str(message)[:log_length_limit] + "..."
self._logger.info(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')):
self._logger.warning('\n' + highlight_error(message))
else:
self._logger.warning(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')):
self._logger.error('\n' + highlight_error(message))
else:
self._logger.error(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):
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)
addToGlobalNamespace('print', logprint)
setGlobal(logprint, 'print')

View File

@ -37,8 +37,8 @@ async def get_response(retry = 0):
if (retry > 21):
logger.warning('请求源脚本内容失败')
return
baseurl = '/lxmusics/lx-music-api-server/main/lx-music-source-example.js'
jsdbaseurl = '/gh/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/MeoProject/lx-music-api-server@main/lx-music-source-example.js'
try:
i = retry
if (i > 10):

View File

@ -40,6 +40,9 @@ class taskWrapper:
logger.error(f"task {self.name} run failed, waiting for next execute...")
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 = {}):
global tasks
wrapper = taskWrapper(name, task, interval, args)

View File

@ -16,6 +16,7 @@ import zlib
import time
import re
import xmltodict
import ipaddress
from urllib.parse import quote, unquote, urlparse
def createBase64Encode(data_bytes):
@ -51,8 +52,8 @@ def require(module):
index += 1
return _module
def addToGlobalNamespace(key, data):
setattr(builtins, key, data)
def setGlobal(obj, key = ''):
setattr(builtins, obj.__name__ if (not key) else key, obj)
def filterFileName(filename):
if platform.system() == 'Windows' or platform.system() == 'Cygwin':
@ -198,5 +199,11 @@ def timestamp_format(t):
t = int(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)

View File

@ -10,13 +10,15 @@
import os as _os
import ujson as _json
def _read_config_file():
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)
except:
return {}
def _read_config(key):
try:
config = _read_config_file()
@ -36,12 +38,15 @@ def _read_config(key):
return value
except:
return None
_dm = _read_config("common.debug_mode")
_lm = _read_config("common.log_file")
_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_file = _lm if (isinstance(_lm , bool)) else True
log_file = _lm if (isinstance(_lm, bool)) else True
running = True
config = {}
workdir = _os.getcwd()
@ -55,3 +60,4 @@ running_ports = []
use_proxy = False
http_proxy = ''
https_proxy = ''
log_files = []

133
main.py
View File

@ -9,14 +9,25 @@
# ----------------------------------------
# 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
from common.utils import createBase64Decode
import os
if ((sys.version_info.major == 3 and sys.version_info.minor < 6) or sys.version_info.major == 2):
print('Python版本过低请使用Python 3.6+ ')
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 lxsecurity
from common import log
@ -24,24 +35,17 @@ from common import Httpx
from common import variable
from common import scheduler
from common import lx_script
from aiohttp.web import Response, FileResponse, StreamResponse
import ujson as json
import threading
import traceback
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)):
dic = {
'code': 0,
'msg': 'success',
'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")
aiologger = log.log('aiohttp_web')
@ -54,20 +58,24 @@ if (sys.version_info.minor < 8 and sys.version_info.major == 3):
else:
stopEvent = asyncio.exceptions.CancelledError
def start_checkcn_thread() -> None:
threading.Thread(target=Httpx.checkcn).start()
# check request info before start
async def handle_before_request(app, handler):
async def handle_request(request):
try:
if (config.read_config('common.reverse_proxy.allow_proxy')):
if (request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))):
# proxy header
if (request.remote in config.read_config('common.reverse_proxy.proxy_whitelist_remote')):
request.remote_addr = request.headers.get(config.read_config('common.reverse_proxy.real_ip_header'))
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'))
else:
return handleResult({"code": 1, "msg": "反代客户端远程地址不在反代ip白名单中", "data": None}, 403)
return handleResult({"code": 1, "msg": "不允许的公网ip转发", "data": None}, 403)
else:
request.remote_addr = request.remote
else:
@ -80,13 +88,13 @@ async def handle_before_request(app, handler):
(time.time() - config.getRequestTime('global'))
<
(config.read_config("security.rate_limit.global"))
):
):
return handleResult({"code": 5, "msg": "全局限速", "data": None}, 429)
if (
(time.time() - config.getRequestTime(request.remote_addr))
<
(config.read_config("security.rate_limit.ip"))
):
):
return handleResult({"code": 5, "msg": "IP限速", "data": None}, 429)
# update request time
config.updateRequestTime('global')
@ -95,27 +103,32 @@ async def handle_before_request(app, handler):
if (config.read_config("security.allowed_host.enable")):
if request.host.split(":")[0] not in config.read_config("security.allowed_host.list"):
if config.read_config("security.allowed_host.blacklist.enable"):
config.ban_ip(request.remote_addr, int(config.read_config("security.allowed_host.blacklist.length")))
config.ban_ip(request.remote_addr, int(
config.read_config("security.allowed_host.blacklist.length")))
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
resp = await handler(request)
if (isinstance(resp, (str, list, dict))):
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
if (isinstance(body, (str, list, dict))):
resp = handleResult(body, status)
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))):
resp = Response(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}')
resp = Response(
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
except:
logger.error(traceback.format_exc())
return {"code": 4, "msg": "内部服务器错误", "data": None}
return handle_request
async def main(request):
return handleResult({"code": 0, "msg": "success", "data": None})
@ -147,14 +160,17 @@ async def handle(request):
logger.error(traceback.format_exc())
return handleResult({'code': 4, 'msg': '内部服务器错误', 'data': None}, 500)
async def handle_404(request):
return handleResult({'code': 6, 'msg': '未找到您所请求的资源', 'data': None}, 404)
async def handle_local(request):
try:
query = dict(request.query)
data = query.get('q')
data = createBase64Decode(data.replace('-', '+').replace('_', '/'))
data = utils.createBase64Decode(
data.replace('-', '+').replace('_', '/'))
data = json.loads(data)
t = request.match_info.get('type')
data['t'] = t
@ -208,28 +224,30 @@ if (config.read_config('common.allow_download_script')):
# 404
app.router.add_route('*', '/{tail:.*}', handle_404)
async def run_app():
async def run_app_host(host):
retries = 0
while True:
if (retries > 4):
logger.warning("重试次数已达上限,但仍有部分端口未能完成监听,已自动进行忽略")
return
break
try:
host = config.read_config('common.host')
ports = [int(port) for port in config.read_config('common.ports')]
ssl_ports = [int(port) for port in config.read_config('common.ssl_info.ssl_ports')]
ports = [int(port)
for port in config.read_config('common.ports')]
ssl_ports = [int(port) for port in config.read_config(
'common.ssl_info.ssl_ports')]
final_ssl_ports = []
final_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)
else:
if (p not in variable.running_ports):
final_ssl_ports.append(p)
# 读取证书和私钥路径
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_runner = aiohttp.web.AppRunner(app)
@ -238,16 +256,21 @@ async def run_app():
# 启动 HTTP 端口监听
for port in final_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()
variable.running_ports.append(port)
logger.info(f"监听 -> http://{host}:{port}")
variable.running_ports.append(f'{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 (os.path.exists(cert_path) and os.path.exists(privkey_path)):
import 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)
# 创建 HTTPS AppRunner
@ -257,12 +280,16 @@ async def run_app():
# 启动 HTTPS 端口监听
for port in ssl_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()
variable.running_ports.append(port)
logger.info(f"监听 -> https://{host}:{port}")
return
variable.running_ports.append(f'{host}_{port}')
logger.info(f"""监听 -> http://{
host if (':' not in host)
else '[' + host + ']'
}:{port}""")
logger.debug(f"HOST({host}) 已完成监听")
break
except OSError as e:
if (str(e).startswith("[Errno 98]") or str(e).startswith('[Errno 10048]')):
logger.error("端口已被占用,请检查\n" + str(e))
@ -271,7 +298,12 @@ async def run_app():
logger.info('重新尝试启动...')
retries += 1
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():
@ -305,5 +337,20 @@ if __name__ == "__main__":
except KeyboardInterrupt:
pass
except:
logger.error('初始化出错,请检查日志')
logger.error(traceback.format_exc())
logger.critical('初始化出错,请检查日志')
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()

View File

@ -22,6 +22,7 @@ async def refresh():
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',

11
script.py Normal file
View 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
View 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',
],
)