from flask import Flask, jsonify, request, redirect
from datetime import datetime, date
import ipaddress
from zoneinfo import ZoneInfo, available_timezones
from flask_cors import CORS
from lunar_python import Solar
import ip2region.searcher as xdb
import ip2region.util as util
from pathlib import Path
import json
import random
import urllib.error
import urllib.parse
import urllib.request
class LowercasePathMiddleware:
def __init__(self, app, exclude_extensions=None):
self.app = app
self.exclude_extensions = exclude_extensions or set()
def __call__(self, environ, start_response):
path = environ.get("PATH_INFO", "")
if path:
last_segment = path.rsplit('/', 1)[-1]
if '.' not in last_segment:
environ['PATH_INFO'] = path.lower()
return self.app(environ, start_response)
app = Flask(__name__)
CORS(app)
app.json.ensure_ascii = False
app.json.sort_keys = False
app.wsgi_app = LowercasePathMiddleware(app.wsgi_app)
with (Path(__file__).parent / "data" / "luck-data.json").open("r", encoding="utf-8") as f:
LUCK_DATA = json.load(f)
with (Path(__file__).parent / "data" / "poem-300.json").open("r", encoding="utf-8") as f:
POEM_DATA = json.load(f)
with (Path(__file__).parent / "data" / "riddle.json").open("r", encoding="utf-8") as f:
RIDDLE_DATA = json.load(f)
IP2REGION_DB_PATH = str(Path(__file__).parent / "data" / "ip2region_v4.xdb")
IP2REGION_VERSION = util.IPv4
IP2REGION_CONTENT = util.load_content_from_file(IP2REGION_DB_PATH)
IP2REGION_SEARCHER = xdb.new_with_buffer(IP2REGION_VERSION, IP2REGION_CONTENT)
ALL_TIMEZONES = sorted(available_timezones())
LOWER_TIMEZONE_MAP = {tz.lower(): tz for tz in ALL_TIMEZONES}
LUCK_ID_MAP = {item.get("id"): item for item in LUCK_DATA}
LUCK_TITLE_MAP = {}
LUCK_TYPE_MAP = {}
for item in LUCK_DATA:
title_lower = item.get("title", "").lower()
type_lower = item.get("type", "").lower()
if title_lower not in LUCK_TITLE_MAP:
LUCK_TITLE_MAP[title_lower] = []
if type_lower not in LUCK_TYPE_MAP:
LUCK_TYPE_MAP[type_lower] = []
LUCK_TITLE_MAP[title_lower].append(item)
LUCK_TYPE_MAP[type_lower].append(item)
POEM_ID_MAP = {item.get("id"): item for item in POEM_DATA}
POEM_TITLE_MAP = {}
POEM_AUTHOR_MAP = {}
POEM_TYPE_MAP = {}
for item in POEM_DATA:
title_lower = item.get("title", "").lower()
author_lower = item.get("author", "").lower()
type_lower = item.get("type", "").lower()
if title_lower not in POEM_TITLE_MAP:
POEM_TITLE_MAP[title_lower] = []
if author_lower not in POEM_AUTHOR_MAP:
POEM_AUTHOR_MAP[author_lower] = []
if type_lower not in POEM_TYPE_MAP:
POEM_TYPE_MAP[type_lower] = []
POEM_TITLE_MAP[title_lower].append(item)
POEM_AUTHOR_MAP[author_lower].append(item)
POEM_TYPE_MAP[type_lower].append(item)
RIDDLE_RIDDLE_MAP = {}
RIDDLE_ANSWER_MAP = {}
for item in RIDDLE_DATA:
riddle_lower = item.get("riddle", "").lower()
answer_lower = item.get("answer", "").lower()
if riddle_lower not in RIDDLE_RIDDLE_MAP:
RIDDLE_RIDDLE_MAP[riddle_lower] = []
if answer_lower not in RIDDLE_ANSWER_MAP:
RIDDLE_ANSWER_MAP[answer_lower] = []
RIDDLE_RIDDLE_MAP[riddle_lower].append(item)
RIDDLE_ANSWER_MAP[answer_lower].append(item)
@app.route("/")
def index():
return """
"""
@app.route("/music/help")
def music_help():
return jsonify({
"路径": "/music/api",
"功能": "获取音乐信息",
"参数": {
"server=netease/tencent/kugou/baidu/kuwo": "必填,音乐平台",
"type=search/song/album/artist/playlist/lrc/url/pic": "必填,操作类型",
"id=xxx": "必填,资源ID",
"author=xxx": "选填,仅lrc/url/pic需要",
},
})
@app.route("/lunar/help")
def lunar_help():
return jsonify({
"路径": "/lunar/api",
"功能": "获取农历信息",
"参数": {
"date=YYYY-MM-DD": "选填,指定日期",
},
})
@app.route("/lunar/api")
def lunar():
args_lower = {k.lower(): v for k, v in request.args.items()}
if set(args_lower.keys()) - {"date"}:
return jsonify({
"error": f"{set(args_lower.keys()) - {"date"}}参数非法",
"allowed_params": ["date=YYYY-MM-DD"],
})
ds = args_lower.get("date", "").strip()
d = datetime.now(ZoneInfo("Asia/Shanghai")).date()
params = {"date": ds if ds else f"{d}(默认,Asia/Shanghai)"}
if ds:
try:
d = datetime.strptime(ds, "%Y-%m-%d").date()
except ValueError:
resp = {"error": f"date参数[{ds}]不为YYYY-MM-DD格式"}
if params:
resp["params"] = params
return jsonify(resp)
solar = Solar.fromYmd(d.year, d.month, d.day)
lunar = solar.getLunar()
jieqi = lunar.getJieQi() or ""
festivals = lunar.getFestivals()
traditional_festival = festivals[0] if festivals else ""
lunar_month = lunar.getMonthInChinese()
season_map = {"立春": "春", "立夏": "夏", "立秋": "秋", "立冬": "冬"}
latest = None
for j in season_map.keys():
solar_jieqi = Solar.fromYmd(d.year, 1, 1).getLunar().getJieQiTable()[j]
jd = date(solar_jieqi.getYear(), solar_jieqi.getMonth(), solar_jieqi.getDay())
if jd <= d:
if latest is None or jd > latest[0]:
latest = (jd, season_map[j])
resp = {
"nian": lunar.getYearInGanZhi(),
"yue": lunar_month,
"ri": lunar.getDayInChinese(),
"shengxiao": lunar.getYearShengXiao(),
"jijie": latest[1] if latest else "冬",
"jieri": traditional_festival,
"jieqi": jieqi,
}
resp["params"] = params
return jsonify(resp)
@app.route("/luck/help")
def luck_help():
return jsonify({
"路径": "/luck/api",
"功能": "获取运势信息",
"参数": {
"id=xxx": "选填,基于排序查询",
"title=xxx": "选填,基于签名查询",
"type=xxx": "选填,基于类型查询",
},
})
@app.route("/luck/api")
def luck():
args_lower = {k.lower(): v for k, v in request.args.items()}
if set(args_lower.keys()) - {"id", "title", "type"}:
return jsonify({
"error": f"{set(args_lower.keys()) - {"id", "title", "type"}}参数非法",
"allowed_params": ["id=xxx", "title=xxx", "type=xxx"],
})
luck_id = args_lower.get("id", "").strip()
luck_title = args_lower.get("title", "").strip()
luck_type = args_lower.get("type", "").strip()
params = {"id": luck_id, "title": luck_title, "type": luck_type}
if not luck_id and not luck_title and not luck_type:
resp = random.choice(LUCK_DATA)
return jsonify(resp)
results = LUCK_DATA
if luck_id:
try:
search_id = int(luck_id)
item = LUCK_ID_MAP.get(search_id)
results = [item] if item else []
except ValueError:
resp = {"error": f"id参数[{luck_id}]不为整数"}
resp["params"] = params
return jsonify(resp)
if luck_title:
title_lower = luck_title.lower()
if luck_id:
results = [item for item in results if title_lower in item.get("title", "").lower()]
else:
results = LUCK_TITLE_MAP.get(title_lower, [])
if luck_type:
type_lower = luck_type.lower()
if luck_id or luck_title:
results = [item for item in results if type_lower in item.get("type", "").lower()]
else:
results = LUCK_TYPE_MAP.get(type_lower, [])
if not results:
resp = {"error": "未找到符合条件的数据"}
resp["params"] = params
return jsonify(resp)
return jsonify({"items": results, "params": params})
@app.route("/poem/help")
def poem_help():
return jsonify({
"路径": "/poem/api",
"功能": "获取唐诗信息",
"参数": {
"id=xxx": "选填,基于排序查询",
"title=xxx": "选填,基于标题查询",
"author=xxx": "选填,基于作者查询",
"type=xxx": "选填,基于类型查询",
"full=true/false": "选填,是否返回整首诗(默认为false)",
},
})
@app.route("/poem/api")
def poem():
args_lower = {k.lower(): v for k, v in request.args.items()}
if set(args_lower.keys()) - {"id", "title", "author", "type", "full"}:
return jsonify({
"error": f"{set(args_lower.keys()) - {"id", "title", "author", "type", "full"}}参数非法",
"allowed_params": ["id=xxx", "title=xxx", "author=xxx", "type=xxx", "full=true/false"],
})
poem_id = args_lower.get("id", "").strip()
poem_title = args_lower.get("title", "").strip()
poem_author = args_lower.get("author", "").strip()
poem_type = args_lower.get("type", "").strip()
full_param = args_lower.get("full", "").strip()
params = {"id": poem_id, "title": poem_title, "author": poem_author, "type": poem_type, "full": full_param if full_param else "false(默认)"}
if full_param.lower() not in {"true", "false", ""}:
resp = {"error": f"full参数[{full_param}]不为true/false"}
resp["params"] = params
return jsonify(resp)
full = full_param.lower() == "true"
if not poem_id and not poem_title and not poem_author and not poem_type:
item = random.choice(POEM_DATA)
contents = item.get("contents", "")
if full:
content = "".join(contents.splitlines())
else:
lines = [line for line in contents.split("\n") if line.strip()]
content = random.choice(lines) if lines else ""
resp = {
"id": item.get("id", ""),
"content": content,
"title": item.get("title", ""),
"author": item.get("author", ""),
"type": item.get("type", ""),
}
resp["params"] = params
return jsonify(resp)
results = POEM_DATA
if poem_id:
try:
search_id = int(poem_id)
item = POEM_ID_MAP.get(search_id)
results = [item] if item else []
except ValueError:
resp = {"error": f"id参数[{poem_id}]不为整数"}
resp["params"] = params
return jsonify(resp)
if poem_title:
title_lower = poem_title.lower()
if poem_id:
results = [item for item in results if title_lower in item.get("title", "").lower()]
else:
results = POEM_TITLE_MAP.get(title_lower, [])
if poem_author:
author_lower = poem_author.lower()
if poem_id or poem_title:
results = [item for item in results if author_lower in item.get("author", "").lower()]
else:
results = POEM_AUTHOR_MAP.get(author_lower, [])
if poem_type:
type_lower = poem_type.lower()
if poem_id or poem_title or poem_author:
results = [item for item in results if type_lower in item.get("type", "").lower()]
else:
results = POEM_TYPE_MAP.get(type_lower, [])
if not results:
resp = {"error": "未找到符合条件的数据"}
resp["params"] = params
return jsonify(resp)
formatted_results = []
for item in results:
contents = item.get("contents", "")
if full:
content = "".join(contents.splitlines())
else:
lines = [line for line in contents.split("\n") if line.strip()]
content = random.choice(lines) if lines else ""
formatted_results.append({
"id": item.get("id", ""),
"content": content,
"title": item.get("title", ""),
"author": item.get("author", ""),
"type": item.get("type", ""),
})
return jsonify({"items": formatted_results, "params": params})
@app.route("/ip/help")
def ip_help():
return jsonify({
"路径": "/ip/api",
"功能": "获取IP信息",
"参数": {
"ip=xxx": "选填,IPv4地址",
},
})
@app.route("/ip/api")
def ip():
args_lower = {k.lower(): v for k, v in request.args.items()}
if set(args_lower.keys()) - {"ip"}:
return jsonify({
"error": f"{set(args_lower.keys()) - {"ip"}}参数非法",
"allowed_params": ["ip=xxx"],
})
query_ip = args_lower.get("ip", "").strip()
client_ip = request.headers.get("X-Forwarded-For", request.remote_addr).split(",")[0].strip()
target_ip = query_ip or client_ip
params = {"ip": query_ip if query_ip else f"{client_ip}(默认)"}
try:
ip_obj = ipaddress.ip_address(target_ip)
if ip_obj.version != 4:
resp = {"error": f"ip参数[{target_ip}]不为IPv4地址"}
resp["params"] = params
return jsonify(resp)
except ValueError:
resp = {"error": f"ip参数[{target_ip}]不为IPv4地址"}
resp["params"] = params
return jsonify(resp)
try:
region = IP2REGION_SEARCHER.search(target_ip)
if region:
region_parts = region.split("|")
resp = {
"ip": target_ip,
"country": region_parts[0] if len(region_parts) > 0 else "",
"province": region_parts[1] if len(region_parts) > 1 else "",
"city": region_parts[2] if len(region_parts) > 2 else "",
"isp": region_parts[3] if len(region_parts) > 3 else "",
"region": region,
}
resp["params"] = params
return jsonify(resp)
except Exception:
pass
resp = {
"ip": target_ip,
"country": "",
"province": "",
"city": "",
"isp": "",
"region": "",
}
resp["params"] = params
return jsonify(resp)
@app.route("/time/help")
def time_help():
return jsonify({
"路径": "/time/api",
"功能": "获取时间信息",
"参数": {
"tz=xxx": "选填,时区(默认为Shanghai)",
},
})
@app.route("/time/api")
def time_api():
args_lower = {k.lower(): v for k, v in request.args.items()}
if set(args_lower.keys()) - {"tz"}:
return jsonify({
"error": f"{set(args_lower.keys()) - {"tz"}}参数非法",
"allowed_params": ["tz=xxx"],
})
tz = args_lower.get("tz", "").strip()
tz_key = None
params = {"tz": tz if tz else "Shanghai(默认)"}
if tz:
low = tz.lower()
exact = LOWER_TIMEZONE_MAP.get(low)
if exact:
tz_key = exact
elif "/" not in tz:
suffix_matches = [name for name in ALL_TIMEZONES if name.lower().endswith(f"/{low}")]
if len(suffix_matches) == 1:
tz_key = suffix_matches[0]
else:
fuzzy_matches = [name for name in ALL_TIMEZONES if low in name.lower()]
if len(fuzzy_matches) == 1:
tz_key = fuzzy_matches[0]
else:
fuzzy_matches = [name for name in ALL_TIMEZONES if low in name.lower()]
if len(fuzzy_matches) == 1:
tz_key = fuzzy_matches[0]
if tz_key is None:
resp = {"error": f"tz参数[{tz}]不为有效时区名称"}
resp["params"] = params
return jsonify(resp)
else:
tz_key = "Asia/Shanghai"
try:
tzinfo = ZoneInfo(tz_key)
except Exception:
tzinfo = ZoneInfo("Asia/Shanghai")
now = datetime.now(tzinfo)
weekday_iso = now.isoweekday()
week_number = now.isocalendar()[1]
resp = {
"date": now.strftime("%Y-%m-%d"),
"time": now.strftime("%H:%M:%S"),
"timestamp": now.timestamp(),
"tz": tzinfo.key,
"weekday": weekday_iso,
"week": week_number,
}
resp["params"] = params
return jsonify(resp)
@app.route("/riddle/help")
def riddle_help():
return jsonify({
"路径": "/riddle/api",
"功能": "获取谜语信息",
"参数": {
"riddle=xxx": "选填,基于谜面查询",
"answer=xxx": "选填,基于谜底查询",
},
})
@app.route("/riddle/api")
def riddle():
args_lower = {k.lower(): v for k, v in request.args.items()}
if set(args_lower.keys()) - {"riddle", "answer"}:
return jsonify({
"error": f"{set(args_lower.keys()) - {"riddle", "answer"}}参数非法",
"allowed_params": ["riddle=xxx", "answer=xxx"],
})
query_riddle = args_lower.get("riddle", "").strip()
query_answer = args_lower.get("answer", "").strip()
params = {"riddle": query_riddle, "answer": query_answer}
if not query_riddle and not query_answer:
item = random.choice(RIDDLE_DATA)
resp = {
"riddle": item.get("riddle", ""),
"answer": item.get("answer", ""),
}
resp["params"] = params
return jsonify(resp)
results = RIDDLE_DATA
if query_riddle:
riddle_lower = query_riddle.lower()
results = RIDDLE_RIDDLE_MAP.get(riddle_lower, [])
if query_answer:
answer_lower = query_answer.lower()
if query_riddle:
results = [item for item in results if answer_lower in item.get("answer", "").lower()]
else:
results = RIDDLE_ANSWER_MAP.get(answer_lower, [])
if not results:
resp = {"error": "未找到符合条件的数据"}
resp["params"] = params
return jsonify(resp)
items = [{
"riddle": item.get("riddle", ""),
"answer": item.get("answer", ""),
} for item in results]
return jsonify({"items": items, "params": params})
@app.route("/sci/help")
def sci_help():
return jsonify({
"路径": "/sci/api",
"功能": "获取期刊信息",
"参数": {
"nane=xxx": "必填,期刊名称(可缩写)",
},
})
@app.route("/sci/api")
def sci():
args_lower = {k.lower(): v for k, v in request.args.items()}
if set(args_lower.keys()) - {"nane", "name"}:
return jsonify({
"error": f"{set(args_lower.keys()) - {"nane", "name"}}参数非法",
"allowed_params": ["nane=xxx"],
})
publication_name = args_lower.get("nane", "").strip() or args_lower.get("name", "").strip()
params = {"nane": publication_name}
if not publication_name:
resp = {"error": "nane参数不能为空"}
resp["params"] = params
return jsonify(resp)
query = urllib.parse.urlencode({
"secretKey": "0361aa1c9a8b4a30889ab6b02eae902c",
"publicationName": publication_name,
})
url = f"https://www.easyscholar.cc/open/getPublicationRank?{query}"
try:
request_obj = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(request_obj, timeout=10) as response:
raw_data = response.read().decode("utf-8")
except urllib.error.HTTPError as error:
resp = {"error": f"期刊接口请求失败: HTTP {error.code}"}
resp["params"] = params
return jsonify(resp)
except urllib.error.URLError as error:
resp = {"error": f"期刊接口请求失败: {error.reason}"}
resp["params"] = params
return jsonify(resp)
try:
result = json.loads(raw_data)
except json.JSONDecodeError:
resp = {"error": "期刊接口返回内容不是有效JSON"}
resp["params"] = params
return jsonify(resp)
if result.get("code") != 200:
resp = {"error": result.get("msg", "期刊接口返回异常")}
resp["params"] = params
return jsonify(resp)
data = result.get("data") or {}
official_rank = data.get("officialRank") or {}
all_rank = official_rank.get("all") or {}
resp = {
"CCF等级": all_rank.get("ccf", ""),
"影响因子": all_rank.get("sciif", ""),
"JCR分区": all_rank.get("sci", ""),
"中科院分区": all_rank.get("sciUp", ""),
"TOP期刊": all_rank.get("sciUpTop", ""),
}
resp["params"] = params
return jsonify(resp)
@app.errorhandler(404)
def not_found(_error):
return redirect("/"), 302
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)