Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51dc8b55ee | |||
| 609db74a62 | |||
| b169838443 |
@@ -1,6 +1,17 @@
|
|||||||
# API-Python
|
# Python-API
|
||||||
|
|
||||||
> 基于Python的本地Web API项目
|
> 基于Python的本地Web API项目
|
||||||
|
|
||||||
|
#### v260716
|
||||||
|
1. 新增代理接口(包含白名单及密码限制)
|
||||||
|
2. 地址接口新增域名支持
|
||||||
|
3. 取消音乐接口
|
||||||
|
|
||||||
|
#### v260702
|
||||||
|
1. 新增下载接口,同时提供web播放器anian-music.js下载
|
||||||
|
|
||||||
|
#### v260605
|
||||||
|
1. 新增期刊查询接口(数据源easyscholar)
|
||||||
|
|
||||||
#### v260512
|
#### v260512
|
||||||
1. 新增农历、运势、唐诗、地址、时间、谜语等接口及对应数据
|
1. 新增农历、运势、唐诗、地址、时间、谜语等接口及对应数据
|
||||||
|
|||||||
@@ -0,0 +1,460 @@
|
|||||||
|
(() => {
|
||||||
|
let songs, audio, currentIndex = 0;
|
||||||
|
let floatingButton, progressRing, playerPanel, coverImg, popupBgImg, animationFrameId;
|
||||||
|
let lyricLines = [];
|
||||||
|
let isPlayerClosed = false;
|
||||||
|
|
||||||
|
const playlistApiUrl = "https://api.i-meto.com/meting/api?server=netease&type=playlist&id=8034031191";
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// 工具
|
||||||
|
// =========================
|
||||||
|
const getRandomSongIndex = () => {
|
||||||
|
if (!songs || !songs.length) return 0;
|
||||||
|
return Math.floor(Math.random() * songs.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCoverSrc = () => {
|
||||||
|
if (!songs || !songs.length) return "/logo.webp";
|
||||||
|
const pic = songs[currentIndex % songs.length]?.pic;
|
||||||
|
return pic?.trim() ? pic.trim() : "/logo.webp";
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPopupBackground = (src) => {
|
||||||
|
if (!popupBgImg) return;
|
||||||
|
|
||||||
|
const fallback = "/logo.webp";
|
||||||
|
const nextSrc = src?.trim() ? src.trim() : fallback;
|
||||||
|
popupBgImg.src = nextSrc;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// 封面
|
||||||
|
// =========================
|
||||||
|
const setCover = () => {
|
||||||
|
if (!coverImg) return;
|
||||||
|
const cover = getCoverSrc();
|
||||||
|
coverImg.src = cover;
|
||||||
|
setPopupBackground(cover);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// audio
|
||||||
|
// =========================
|
||||||
|
const initAudio = () => {
|
||||||
|
if (!audio) {
|
||||||
|
audio = new Audio();
|
||||||
|
audio.volume = 0.5;
|
||||||
|
|
||||||
|
audio.onplay = () => { syncUI(); setRotate(); };
|
||||||
|
audio.onpause = () => { syncUI(); setRotate(); };
|
||||||
|
audio.onended = () => next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// LRC解析
|
||||||
|
// =========================
|
||||||
|
const parseLrc = (text) => {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const res = [];
|
||||||
|
const reg = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/;
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
const timeMatch = line.match(reg);
|
||||||
|
if (!timeMatch) continue;
|
||||||
|
|
||||||
|
const time =
|
||||||
|
(+timeMatch[1]) * 60 +
|
||||||
|
(+timeMatch[2]) +
|
||||||
|
(+timeMatch[3]) / (timeMatch[3].length === 3 ? 1000 : 100);
|
||||||
|
|
||||||
|
const txt = line.replace(reg, "").trim();
|
||||||
|
if (txt) res.push({ time, text: txt });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.sort((left, right) => left.time - right.time);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// 歌词渲染
|
||||||
|
// =========================
|
||||||
|
const renderLrc = (top, mid, bottom) => {
|
||||||
|
const box = playerPanel?.querySelector(".lrc");
|
||||||
|
if (!box) return;
|
||||||
|
|
||||||
|
box.innerHTML = `
|
||||||
|
<div style="opacity:.4;font-size:12px;line-height:1.2;min-height:16px">${top}</div>
|
||||||
|
<div style="font-size:14px;font-weight:600;margin:2px 0;line-height:1.2;min-height:18px">${mid}</div>
|
||||||
|
<div style="opacity:.4;font-size:12px;line-height:1.2;min-height:16px">${bottom}</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderLoadingLrc = () => {
|
||||||
|
renderLrc("加", "载", "中");
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// loading歌词
|
||||||
|
// =========================
|
||||||
|
const loadLyric = (song) => {
|
||||||
|
lyricLines = [];
|
||||||
|
|
||||||
|
renderLoadingLrc();
|
||||||
|
|
||||||
|
if (!song?.lrc) {
|
||||||
|
renderLrc("-", "暂无歌词", "-");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(song.lrc)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(text => {
|
||||||
|
lyricLines = parseLrc(text);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
lyricLines = [];
|
||||||
|
renderLrc("-", "加载失败", "-");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// load song
|
||||||
|
// =========================
|
||||||
|
const loadSong = () => {
|
||||||
|
if (!songs || !songs.length) return;
|
||||||
|
|
||||||
|
const currentSong = songs[currentIndex % songs.length];
|
||||||
|
initAudio();
|
||||||
|
|
||||||
|
audio.src = currentSong.url;
|
||||||
|
loadLyric(currentSong);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// UI(标题)
|
||||||
|
// =========================
|
||||||
|
const syncUI = () => {
|
||||||
|
if (!playerPanel) return;
|
||||||
|
|
||||||
|
const titleBox = playerPanel.querySelector(".t");
|
||||||
|
if (!titleBox) return;
|
||||||
|
|
||||||
|
const currentSong = songs?.length ? songs[currentIndex % songs.length] : null;
|
||||||
|
|
||||||
|
titleBox.innerHTML = `
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:6px">
|
||||||
|
<div style="flex:1;line-height:1.2">
|
||||||
|
<div style="font-size:13px">${currentSong?.title || "加载中"}</div>
|
||||||
|
<div style="font-size:11px;opacity:.7;margin-top:2px">${currentSong?.author || "请稍候"}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:6px;align-items:center;flex-shrink:0;">
|
||||||
|
<button class="minPlayerBtn" style="
|
||||||
|
width:24px;height:24px;
|
||||||
|
border:none;
|
||||||
|
border-radius:6px;
|
||||||
|
background:rgba(255,255,255,.08);
|
||||||
|
color:#fff;
|
||||||
|
font-size:14px;
|
||||||
|
cursor:pointer;
|
||||||
|
">−</button>
|
||||||
|
|
||||||
|
<button class="closePlayerBtn" style="
|
||||||
|
width:24px;height:24px;
|
||||||
|
border:none;
|
||||||
|
border-radius:6px;
|
||||||
|
background:rgba(255,255,255,.08);
|
||||||
|
color:#fff;
|
||||||
|
font-size:14px;
|
||||||
|
cursor:pointer;
|
||||||
|
">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const isPlaying = audio && !audio.paused;
|
||||||
|
playerPanel.querySelector(".pp").textContent = isPlaying ? "暂停" : "播放";
|
||||||
|
|
||||||
|
// 关闭(彻底关闭)
|
||||||
|
titleBox.querySelector(".closePlayerBtn").onclick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (audio) {
|
||||||
|
audio.pause();
|
||||||
|
audio.src = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
isPlayerClosed = true;
|
||||||
|
|
||||||
|
floatingButton.style.display = "none";
|
||||||
|
playerPanel.style.display = "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 最小化(只收起面板)
|
||||||
|
titleBox.querySelector(".minPlayerBtn").onclick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (playerPanel) playerPanel.style.display = "none";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// rotate
|
||||||
|
// =========================
|
||||||
|
const setRotate = () => {
|
||||||
|
if (!coverImg) return;
|
||||||
|
coverImg.style.animationPlayState = (audio && !audio.paused) ? "running" : "paused";
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// 歌词更新
|
||||||
|
// =========================
|
||||||
|
const updateLrc = () => {
|
||||||
|
if (!lyricLines.length || !audio) return;
|
||||||
|
|
||||||
|
const currentTime = audio.currentTime;
|
||||||
|
|
||||||
|
let lyricIndex = 0;
|
||||||
|
for (let lyricLineIndex = 0; lyricLineIndex < lyricLines.length; lyricLineIndex++) {
|
||||||
|
if (currentTime >= lyricLines[lyricLineIndex].time) lyricIndex = lyricLineIndex;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLrc(
|
||||||
|
lyricLines[lyricIndex - 1]?.text || "-",
|
||||||
|
lyricLines[lyricIndex]?.text || "-",
|
||||||
|
lyricLines[lyricIndex + 1]?.text || "-"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// progress
|
||||||
|
// =========================
|
||||||
|
const progress = () => {
|
||||||
|
if (!audio || !audio.duration) return;
|
||||||
|
|
||||||
|
const progressPercent = audio.currentTime / audio.duration;
|
||||||
|
|
||||||
|
progressRing.style.background =
|
||||||
|
`conic-gradient(#fff ${progressPercent * 360}deg,rgba(255,255,255,.15) 0)`;
|
||||||
|
|
||||||
|
const seek = playerPanel?.querySelector(".seek");
|
||||||
|
if (seek && !seek.matches(":active")) {
|
||||||
|
seek.value = Math.floor(progressPercent * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLrc();
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// play / pause / next
|
||||||
|
// =========================
|
||||||
|
const play = async () => {
|
||||||
|
initAudio();
|
||||||
|
if (!audio.src) loadSong();
|
||||||
|
|
||||||
|
try { await audio.play(); } catch { }
|
||||||
|
|
||||||
|
syncUI();
|
||||||
|
setRotate();
|
||||||
|
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
loop();
|
||||||
|
};
|
||||||
|
|
||||||
|
const pause = () => {
|
||||||
|
if (audio) audio.pause();
|
||||||
|
syncUI();
|
||||||
|
setRotate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
if (!songs || !songs.length) return;
|
||||||
|
|
||||||
|
let newIndex = getRandomSongIndex();
|
||||||
|
if (songs.length > 1) {
|
||||||
|
while (newIndex === currentIndex) newIndex = getRandomSongIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex = newIndex;
|
||||||
|
|
||||||
|
const currentSong = songs[currentIndex % songs.length];
|
||||||
|
|
||||||
|
loadSong();
|
||||||
|
syncUI();
|
||||||
|
setRotate();
|
||||||
|
setCover();
|
||||||
|
|
||||||
|
loadLyric(currentSong);
|
||||||
|
|
||||||
|
audio.play().catch(() => { });
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// volume
|
||||||
|
// =========================
|
||||||
|
const volUp = () => {
|
||||||
|
if (!audio) return;
|
||||||
|
audio.volume = Math.min(1, audio.volume + 0.1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const volDown = () => {
|
||||||
|
if (!audio) return;
|
||||||
|
audio.volume = Math.max(0, audio.volume - 0.1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// loop
|
||||||
|
// =========================
|
||||||
|
const loop = () => {
|
||||||
|
progress();
|
||||||
|
animationFrameId = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// UI init
|
||||||
|
// =========================
|
||||||
|
const initUI = () => {
|
||||||
|
floatingButton = document.createElement("div");
|
||||||
|
floatingButton.style.cssText =
|
||||||
|
"position:fixed;right:18px;bottom:18px;width:58px;height:58px;border-radius:50%;background:#111;z-index:99999;display:flex;align-items:center;justify-content:center;cursor:pointer;overflow:hidden";
|
||||||
|
|
||||||
|
progressRing = document.createElement("div");
|
||||||
|
progressRing.style.cssText =
|
||||||
|
"position:absolute;inset:0;border-radius:50%;background:conic-gradient(rgba(255,255,255,.15) 0deg,transparent 0);z-index:0";
|
||||||
|
|
||||||
|
coverImg = document.createElement("img");
|
||||||
|
coverImg.src = "/logo.webp";
|
||||||
|
coverImg.style.cssText =
|
||||||
|
"width:46px;height:46px;border-radius:50%;object-fit:cover;z-index:2;position:relative;animation:spin 6s linear infinite;animation-play-state:paused";
|
||||||
|
|
||||||
|
floatingButton.appendChild(progressRing);
|
||||||
|
floatingButton.appendChild(coverImg);
|
||||||
|
|
||||||
|
playerPanel = document.createElement("div");
|
||||||
|
playerPanel.style.cssText =
|
||||||
|
"position:fixed;right:18px;bottom:90px;width:240px;background:#000;color:#fff;border-radius:12px;display:none;z-index:99999;overflow:hidden";
|
||||||
|
|
||||||
|
playerPanel.innerHTML = `
|
||||||
|
<div class="t"></div>
|
||||||
|
<div class="lrc" style="margin:10px 0 8px 0;text-align:center"></div>
|
||||||
|
<input class="seek" type="range" min="0" max="1000" value="0"
|
||||||
|
style="width:100%;margin:0 0 8px 0;accent-color:#fff">
|
||||||
|
<div style="display:flex;justify-content:space-evenly;align-items:center">
|
||||||
|
<button class="pp">播放</button>
|
||||||
|
<button class="cc">切歌</button>
|
||||||
|
<button class="vu">音量+</button>
|
||||||
|
<button class="vd">音量-</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const popupBg = document.createElement("img");
|
||||||
|
popupBg.className = "popupBg";
|
||||||
|
popupBg.alt = "";
|
||||||
|
|
||||||
|
const popupShade = document.createElement("div");
|
||||||
|
popupShade.className = "popupShade";
|
||||||
|
|
||||||
|
const popupContent = document.createElement("div");
|
||||||
|
popupContent.className = "popupContent";
|
||||||
|
|
||||||
|
while (playerPanel.firstChild) popupContent.appendChild(playerPanel.firstChild);
|
||||||
|
|
||||||
|
playerPanel.appendChild(popupBg);
|
||||||
|
playerPanel.appendChild(popupShade);
|
||||||
|
playerPanel.appendChild(popupContent);
|
||||||
|
|
||||||
|
popupBgImg = popupBg;
|
||||||
|
setPopupBackground("/logo.webp");
|
||||||
|
|
||||||
|
document.body.appendChild(floatingButton);
|
||||||
|
document.body.appendChild(playerPanel);
|
||||||
|
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
transition: transform .05s ease, background .05s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popupBg {
|
||||||
|
position:absolute;
|
||||||
|
inset:0;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
object-fit:cover;
|
||||||
|
z-index:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popupShade {
|
||||||
|
position:absolute;
|
||||||
|
inset:0;
|
||||||
|
background:rgba(0,0,0,.58);
|
||||||
|
z-index:1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popupContent {
|
||||||
|
position:relative;
|
||||||
|
z-index:2;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
floatingButton.onclick = () => {
|
||||||
|
if (isPlayerClosed) return;
|
||||||
|
playerPanel.style.display = playerPanel.style.display === "none" ? "block" : "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
playerPanel.querySelector(".pp").onclick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!audio || audio.paused) play();
|
||||||
|
else pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
playerPanel.querySelector(".cc").onclick = () => next();
|
||||||
|
playerPanel.querySelector(".vu").onclick = () => volUp();
|
||||||
|
playerPanel.querySelector(".vd").onclick = () => volDown();
|
||||||
|
|
||||||
|
const seek = playerPanel.querySelector(".seek");
|
||||||
|
seek.addEventListener("input", () => {
|
||||||
|
if (!audio || !audio.duration) return;
|
||||||
|
audio.currentTime = (seek.value / 1000) * audio.duration;
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(progress, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// init
|
||||||
|
// =========================
|
||||||
|
initUI();
|
||||||
|
|
||||||
|
renderLoadingLrc();
|
||||||
|
syncUI();
|
||||||
|
|
||||||
|
fetch(playlistApiUrl)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(responseSongs => {
|
||||||
|
songs = responseSongs;
|
||||||
|
|
||||||
|
if (songs && songs.length) {
|
||||||
|
currentIndex = getRandomSongIndex();
|
||||||
|
|
||||||
|
setCover();
|
||||||
|
syncUI();
|
||||||
|
|
||||||
|
loadSong();
|
||||||
|
} else {
|
||||||
|
syncUI();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -1,4 +1,4 @@
|
|||||||
from flask import Flask, jsonify, request, redirect
|
from flask import Flask, jsonify, request, redirect, send_file, Response
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from zoneinfo import ZoneInfo, available_timezones
|
from zoneinfo import ZoneInfo, available_timezones
|
||||||
@@ -9,6 +9,12 @@ import ip2region.util as util
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class LowercasePathMiddleware:
|
class LowercasePathMiddleware:
|
||||||
@@ -29,6 +35,7 @@ CORS(app)
|
|||||||
app.json.ensure_ascii = False
|
app.json.ensure_ascii = False
|
||||||
app.json.sort_keys = False
|
app.json.sort_keys = False
|
||||||
app.wsgi_app = LowercasePathMiddleware(app.wsgi_app)
|
app.wsgi_app = LowercasePathMiddleware(app.wsgi_app)
|
||||||
|
logging.getLogger("werkzeug").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
with (Path(__file__).parent / "data" / "luck-data.json").open("r", encoding="utf-8") as f:
|
with (Path(__file__).parent / "data" / "luck-data.json").open("r", encoding="utf-8") as f:
|
||||||
@@ -92,6 +99,11 @@ for item in RIDDLE_DATA:
|
|||||||
RIDDLE_ANSWER_MAP[answer_lower].append(item)
|
RIDDLE_ANSWER_MAP[answer_lower].append(item)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/favicon.ico")
|
||||||
|
def favicon():
|
||||||
|
logo_file = os.path.join(app.root_path, "data/pan", "zhaoxi-logo.png")
|
||||||
|
return send_file(logo_file, mimetype="image/png")
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return """
|
return """
|
||||||
@@ -102,30 +114,19 @@ th{background-color:#f9fafb;font-weight:600;color:#374151;}
|
|||||||
a{color:#2563eb;text-decoration:none;}
|
a{color:#2563eb;text-decoration:none;}
|
||||||
a:hover{text-decoration:underline;}</style>
|
a:hover{text-decoration:underline;}</style>
|
||||||
<table><thead><tr><th>接口名称</th><th>API 地址</th><th>说明文档</th></tr></thead><tbody>
|
<table><thead><tr><th>接口名称</th><th>API 地址</th><th>说明文档</th></tr></thead><tbody>
|
||||||
<tr><td>音乐接口</td><td><a href="/music/api">/music/api</a></td><td><a href="/music/help">/music/help</a></td></tr>
|
|
||||||
<tr><td>农历接口</td><td><a href="/lunar/api">/lunar/api</a></td><td><a href="/lunar/help">/lunar/help</a></td></tr>
|
<tr><td>农历接口</td><td><a href="/lunar/api">/lunar/api</a></td><td><a href="/lunar/help">/lunar/help</a></td></tr>
|
||||||
<tr><td>运势接口</td><td><a href="/luck/api">/luck/api</a></td><td><a href="/luck/help">/luck/help</a></td></tr>
|
<tr><td>运势接口</td><td><a href="/luck/api">/luck/api</a></td><td><a href="/luck/help">/luck/help</a></td></tr>
|
||||||
<tr><td>唐诗接口</td><td><a href="/poem/api">/poem/api</a></td><td><a href="/poem/help">/poem/help</a></td></tr>
|
<tr><td>唐诗接口</td><td><a href="/poem/api">/poem/api</a></td><td><a href="/poem/help">/poem/help</a></td></tr>
|
||||||
<tr><td>地址接口</td><td><a href="/ip/api">/ip/api</a></td><td><a href="/ip/help">/ip/help</a></td></tr>
|
<tr><td>地址接口</td><td><a href="/ip/api">/ip/api</a></td><td><a href="/ip/help">/ip/help</a></td></tr>
|
||||||
<tr><td>时间接口</td><td><a href="/time/api">/time/api</a></td><td><a href="/time/help">/time/help</a></td></tr>
|
<tr><td>时间接口</td><td><a href="/time/api">/time/api</a></td><td><a href="/time/help">/time/help</a></td></tr>
|
||||||
<tr><td>谜语接口</td><td><a href="/riddle/api">/riddle/api</a></td><td><a href="/riddle/help">/riddle/help</a></td></tr>
|
<tr><td>谜语接口</td><td><a href="/riddle/api">/riddle/api</a></td><td><a href="/riddle/help">/riddle/help</a></td></tr>
|
||||||
|
<tr><td>期刊接口</td><td><a href="/sci/api">/sci/api</a></td><td><a href="/sci/help">/sci/help</a></td></tr>
|
||||||
|
<tr><td>代理接口</td><td><a href="/agent/api">/agent/api</a></td><td><a href="/agent/help">/agent/help</a></td></tr>
|
||||||
|
<tr><td>下载接口</td><td><a href="/pan/api">/pan/api</a></td><td><a href="/pan/help">/pan/help</a></td></tr>
|
||||||
</tbody></table></body></html>
|
</tbody></table></body></html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@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")
|
@app.route("/lunar/help")
|
||||||
def lunar_help():
|
def lunar_help():
|
||||||
@@ -345,32 +346,42 @@ def ip_help():
|
|||||||
"路径": "/ip/api",
|
"路径": "/ip/api",
|
||||||
"功能": "获取IP信息",
|
"功能": "获取IP信息",
|
||||||
"参数": {
|
"参数": {
|
||||||
"ip=xxx": "选填,IPv4地址",
|
"name=xxx": "选填,IPv4地址或域名",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route("/ip/api")
|
@app.route("/ip/api")
|
||||||
def ip():
|
def ip():
|
||||||
args_lower = {k.lower(): v for k, v in request.args.items()}
|
args_lower = {k.lower(): v for k, v in request.args.items()}
|
||||||
if set(args_lower.keys()) - {"ip"}:
|
if set(args_lower.keys()) - {"name"}:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"error": f"{set(args_lower.keys()) - {"ip"}}参数非法",
|
"error": f"{set(args_lower.keys()) - {"name"}}参数非法",
|
||||||
"allowed_params": ["ip=xxx"],
|
"allowed_params": ["name=xxx"],
|
||||||
})
|
})
|
||||||
query_ip = args_lower.get("ip", "").strip()
|
query_name = args_lower.get("name", "").strip()
|
||||||
client_ip = request.headers.get("X-Forwarded-For", request.remote_addr).split(",")[0].strip()
|
client_ip = request.headers.get("X-Forwarded-For", request.remote_addr).split(",")[0].strip()
|
||||||
target_ip = query_ip or client_ip
|
target = query_name or client_ip
|
||||||
params = {"ip": query_ip if query_ip else f"{client_ip}(默认)"}
|
params = {"name": query_name if query_name else f"{client_ip}(默认)"}
|
||||||
try:
|
try:
|
||||||
ip_obj = ipaddress.ip_address(target_ip)
|
ip_obj = ipaddress.ip_address(target)
|
||||||
if ip_obj.version != 4:
|
if ip_obj.version != 4:
|
||||||
resp = {"error": f"ip参数[{target_ip}]不为IPv4地址"}
|
resp = {"error": f"name参数[{target}]不为IPv4地址或可解析为IPv4的域名"}
|
||||||
resp["params"] = params
|
resp["params"] = params
|
||||||
return jsonify(resp)
|
return jsonify(resp)
|
||||||
|
target_ip = target
|
||||||
except ValueError:
|
except ValueError:
|
||||||
resp = {"error": f"ip参数[{target_ip}]不为IPv4地址"}
|
try:
|
||||||
resp["params"] = params
|
addr_infos = socket.getaddrinfo(target, None, socket.AF_INET)
|
||||||
return jsonify(resp)
|
except socket.gaierror:
|
||||||
|
resp = {"error": f"name参数[{target}]不为IPv4地址或可解析为IPv4的域名"}
|
||||||
|
resp["params"] = params
|
||||||
|
return jsonify(resp)
|
||||||
|
resolved_ips = sorted({info[4][0] for info in addr_infos})
|
||||||
|
if not resolved_ips:
|
||||||
|
resp = {"error": f"name参数[{target}]未解析到IPv4地址"}
|
||||||
|
resp["params"] = params
|
||||||
|
return jsonify(resp)
|
||||||
|
target_ip = resolved_ips[0]
|
||||||
try:
|
try:
|
||||||
region = IP2REGION_SEARCHER.search(target_ip)
|
region = IP2REGION_SEARCHER.search(target_ip)
|
||||||
if region:
|
if region:
|
||||||
@@ -514,10 +525,228 @@ def riddle():
|
|||||||
return jsonify({"items": items, "params": params})
|
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.route("/agent/help")
|
||||||
|
def agent_help():
|
||||||
|
return jsonify({
|
||||||
|
"路径": "/agent/api",
|
||||||
|
"功能": "访问目标网址并返回内容",
|
||||||
|
"参数": {
|
||||||
|
"url=xxx": "必填,目标网址(仅支持http/https)",
|
||||||
|
"ref=xxx": "选填,Referer(默认为空)",
|
||||||
|
"pwd=xxx": "选填,访问白名单以外网址时必填",
|
||||||
|
},
|
||||||
|
"白名单": ["anian.net", "*.anian.net"],
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route("/agent/api")
|
||||||
|
def agent():
|
||||||
|
agent_password = "anian-agent-api-pwd"
|
||||||
|
whitelist_domains = {"anian.net"}
|
||||||
|
args_lower = {k.lower(): v for k, v in request.args.items()}
|
||||||
|
allowed_keys = {"url", "ref", "pwd", "password"}
|
||||||
|
extra_params = set(args_lower.keys()) - allowed_keys
|
||||||
|
if extra_params:
|
||||||
|
return jsonify({
|
||||||
|
"error": f"{extra_params}参数非法",
|
||||||
|
"allowed_params": ["url=xxx", "ref=xxx", "pwd=xxx"],
|
||||||
|
})
|
||||||
|
target_url = args_lower.get("url", "").strip()
|
||||||
|
referer = args_lower.get("ref", "").strip()
|
||||||
|
password = args_lower.get("pwd", "").strip() or args_lower.get("password", "").strip()
|
||||||
|
params = {"url": target_url, "ref": referer, "pwd": password}
|
||||||
|
if not target_url:
|
||||||
|
resp = {"error": "url参数不能为空"}
|
||||||
|
resp["params"] = params
|
||||||
|
return jsonify(resp)
|
||||||
|
parsed_url = urllib.parse.urlparse(target_url)
|
||||||
|
if parsed_url.scheme not in {"http", "https"} or not parsed_url.netloc:
|
||||||
|
resp = {"error": f"url参数[{target_url}]不为有效http/https网址"}
|
||||||
|
resp["params"] = params
|
||||||
|
return jsonify(resp)
|
||||||
|
hostname = (parsed_url.hostname or "").rstrip(".").lower()
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(hostname)
|
||||||
|
resp = {"error": "url参数只允许填写域名,不允许IP地址"}
|
||||||
|
resp["params"] = params
|
||||||
|
return jsonify(resp)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
is_whitelisted = any(
|
||||||
|
hostname == domain or hostname.endswith(f".{domain}")
|
||||||
|
for domain in whitelist_domains
|
||||||
|
)
|
||||||
|
if not is_whitelisted and password != agent_password:
|
||||||
|
resp = {"error": "非白名单URL需要正确密码"}
|
||||||
|
resp["params"] = params
|
||||||
|
return jsonify(resp)
|
||||||
|
try:
|
||||||
|
addr_infos = socket.getaddrinfo(hostname, None)
|
||||||
|
except socket.gaierror as error:
|
||||||
|
resp = {"error": f"域名解析失败: {error.strerror or error}"}
|
||||||
|
resp["params"] = params
|
||||||
|
return jsonify(resp)
|
||||||
|
resolved_ips = sorted({info[4][0] for info in addr_infos})
|
||||||
|
for resolved_ip in resolved_ips:
|
||||||
|
ip_obj = ipaddress.ip_address(resolved_ip)
|
||||||
|
if not ip_obj.is_global:
|
||||||
|
resp = {"error": f"域名解析到非公网IP: {resolved_ip}"}
|
||||||
|
resp["params"] = params
|
||||||
|
return jsonify(resp)
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0",
|
||||||
|
"Referer": referer,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
request_obj = urllib.request.Request(target_url, headers=headers)
|
||||||
|
with urllib.request.urlopen(request_obj, timeout=15) as response:
|
||||||
|
body = response.read()
|
||||||
|
content_type = response.headers.get("Content-Type", "application/octet-stream")
|
||||||
|
return Response(body, status=response.getcode(), content_type=content_type)
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
body = error.read()
|
||||||
|
content_type = error.headers.get("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
return Response(body, status=error.code, content_type=content_type)
|
||||||
|
except urllib.error.URLError as error:
|
||||||
|
resp = {"error": f"agent请求失败: {error.reason}"}
|
||||||
|
resp["params"] = params
|
||||||
|
return jsonify(resp)
|
||||||
|
except ValueError as error:
|
||||||
|
resp = {"error": f"agent请求失败: {error}"}
|
||||||
|
resp["params"] = params
|
||||||
|
return jsonify(resp)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/pan/help")
|
||||||
|
def pan_help():
|
||||||
|
return jsonify({
|
||||||
|
"路径": "/pan/api",
|
||||||
|
"功能": "下载文件",
|
||||||
|
"参数": {
|
||||||
|
"name=xxx": "必填,文件名",
|
||||||
|
"download=true/false": "下载/预览",
|
||||||
|
},
|
||||||
|
"列表": ["anian-music.js", "zhaoxi-logo.png"]
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route("/pan/api")
|
||||||
|
def pan_file_download():
|
||||||
|
args_lower = {k.lower(): v for k, v in request.args.items()}
|
||||||
|
allowed_keys = {"name", "download"}
|
||||||
|
extra_params = set(args_lower.keys()) - allowed_keys
|
||||||
|
if extra_params:
|
||||||
|
return jsonify({
|
||||||
|
"error": f"{extra_params}参数非法",
|
||||||
|
"allowed_params": ["name=xxx", "download=true/false"],
|
||||||
|
})
|
||||||
|
file_name = args_lower.get("name", "").strip()
|
||||||
|
download_param = args_lower.get("download", "").strip()
|
||||||
|
if download_param and download_param.lower() not in {"true", "false"}:
|
||||||
|
resp = {
|
||||||
|
"error": f"download参数[{download_param}]不为true/false",
|
||||||
|
"params": {"name": file_name, "download": download_param}
|
||||||
|
}
|
||||||
|
return jsonify(resp)
|
||||||
|
force_download = download_param.lower() == "true"
|
||||||
|
params = {"name": file_name, "download": download_param if download_param else "false(默认)"}
|
||||||
|
if not file_name:
|
||||||
|
resp = {
|
||||||
|
"error": "name参数不能为空",
|
||||||
|
"params": params
|
||||||
|
}
|
||||||
|
return jsonify(resp)
|
||||||
|
pan_dir = Path(__file__).parent / "data" / "pan"
|
||||||
|
target_file = pan_dir / file_name
|
||||||
|
real_pan = pan_dir.resolve()
|
||||||
|
real_target = target_file.resolve()
|
||||||
|
if not str(real_target).startswith(str(real_pan)):
|
||||||
|
resp = {
|
||||||
|
"error": "文件名非法",
|
||||||
|
"params": params
|
||||||
|
}
|
||||||
|
return jsonify(resp)
|
||||||
|
if not target_file.is_file():
|
||||||
|
resp = {
|
||||||
|
"error": f"文件 {file_name} 不存在",
|
||||||
|
"help": "请前往/pan/help查看文件列表",
|
||||||
|
"params": params
|
||||||
|
}
|
||||||
|
return jsonify(resp)
|
||||||
|
return send_file(
|
||||||
|
target_file,
|
||||||
|
as_attachment=force_download,
|
||||||
|
download_name=file_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(_error):
|
def not_found(_error):
|
||||||
return redirect("/"), 302
|
return redirect("/"), 302
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user