增加wtf顶部筛选布局|实现标签排除筛选|支持proxy参数自定义反代|增加简单的概要标签|增加瀑布流选中反馈与单击放大
This commit is contained in:
1 parent
ef66583b9c
commit
ec9d373ab8
10 files changed
+1130
-54
No files matched your search
@@ -12,7 +12,11 @@ from app.core.runtime_settings import (
|
||||
runtime_config_from_values,
|
||||
set_runtime_setting,
|
||||
)
|
||||
from app.core.pximg_reverse_proxy import DEFAULT_PXIMG_MIRROR_HOST, normalize_pximg_mirror_host
|
||||
from app.core.pximg_reverse_proxy import (
|
||||
DEFAULT_PXIMG_MIRROR_HOST,
|
||||
normalize_pximg_custom_mirror_host,
|
||||
normalize_pximg_mirror_host,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -255,6 +259,7 @@ async def get_settings(
|
||||
"image_proxy": {
|
||||
"use_pixiv_cat": bool(runtime.image_proxy_use_pixiv_cat),
|
||||
"pximg_mirror_host": str(runtime.image_proxy_pximg_mirror_host or DEFAULT_PXIMG_MIRROR_HOST),
|
||||
"extra_pximg_mirror_hosts": list(getattr(runtime, "image_proxy_extra_pximg_mirror_hosts", []) or []),
|
||||
},
|
||||
"random": random_defaults,
|
||||
"security": {"hide_origin_url_in_public_json": bool(runtime.hide_origin_url_in_public_json)},
|
||||
@@ -371,6 +376,28 @@ async def update_settings(
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Invalid image_proxy.pximg_mirror_host", status_code=400)
|
||||
updates.append(("image_proxy.pximg_mirror_host", str(host)))
|
||||
|
||||
if "extra_pximg_mirror_hosts" in image_proxy:
|
||||
raw = image_proxy.get("extra_pximg_mirror_hosts")
|
||||
if raw is None:
|
||||
updates.append(("image_proxy.extra_pximg_mirror_hosts", []))
|
||||
else:
|
||||
if not isinstance(raw, list):
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Invalid image_proxy.extra_pximg_mirror_hosts", status_code=400)
|
||||
candidates = _as_str_list(raw)
|
||||
if len(candidates) > 200:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Invalid image_proxy.extra_pximg_mirror_hosts", status_code=400)
|
||||
normalized: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for item in candidates:
|
||||
host = normalize_pximg_custom_mirror_host(item)
|
||||
if host is None:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Invalid image_proxy.extra_pximg_mirror_hosts", status_code=400)
|
||||
if host in seen:
|
||||
continue
|
||||
seen.add(host)
|
||||
normalized.append(host)
|
||||
updates.append(("image_proxy.extra_pximg_mirror_hosts", normalized))
|
||||
|
||||
random = body.get("random")
|
||||
if random is not None:
|
||||
if not isinstance(random, dict):
|
||||
|
||||
@@ -19,6 +19,7 @@ def _build_docs_html(*, base_url: str) -> str:
|
||||
|
||||
examples = {
|
||||
"img_default": u("/random"),
|
||||
"img_proxy_cat": u("/random?proxy=i-pixiv-cat"),
|
||||
"img_pixiv_cat": u("/random?pixiv_cat=1"),
|
||||
"img_pixiv_re": u("/random?pixiv_cat=1&pximg_mirror_host=re"),
|
||||
"img_redirect": u("/random?redirect=1"),
|
||||
@@ -303,7 +304,8 @@ def _build_docs_html(*, base_url: str) -> str:
|
||||
<p><span class="kbd">/status</span> 为公开仪表盘:展示 API 状态、图库概览、/random 请求统计;<span class="kbd">/status.json</span> 为机器可读 JSON。</p>
|
||||
<pre><code>{examples["status"]}
|
||||
{examples["status_json"]}</code></pre>
|
||||
<p><span class="kbd">/wtf</span> 为瀑布流:参数与 <span class="kbd">/random</span> 完全一致(例如 r18/标签/分辨率/热度等),页面内部会自动加上 <span class="kbd">adaptive=1</span> 与 <span class="kbd">redirect=1</span> 来做屏幕适配与稳定链接。</p>
|
||||
<p><span class="kbd">/wtf</span> 为瀑布流:支持 <span class="kbd">/random</span> 的全部过滤参数(例如 r18/标签/分辨率/热度/排除标签等),默认会补上 <span class="kbd">adaptive=1</span> 以更适合不同屏幕。</p>
|
||||
<p class="muted">/wtf 额外支持布局参数:<code>view=single|masonry|tiles</code>,以及 <code>wtf_mcols</code>/<code>wtf_mgap</code>/<code>wtf_tcols</code>/<code>wtf_tgap</code>/<code>wtf_tratio</code>(仅影响页面布局)。另有 <code>wtf_gender=girls|boys</code> 与概要标签 <code>@male</code>/<code>@female</code>(仅 /wtf 端展开,用于快速“只看女生/只看男生”)。</p>
|
||||
<pre><code>{examples["wtf"]}
|
||||
{examples["wtf_r18"]}</code></pre>
|
||||
</section>
|
||||
@@ -332,6 +334,15 @@ def _build_docs_html(*, base_url: str) -> str:
|
||||
<span class="muted">quality 会先抽样 N 张候选再按热度/分辨率打分选图。</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>proxy</code></td>
|
||||
<td>
|
||||
指定本次请求的图片上游镜像(优先级最高)。会隐式开启第三方镜像(等价于 <code>pixiv_cat=1</code>),并覆盖地区自动选择。<br/>
|
||||
内置值:<code>cat</code>/<code>re</code>/<code>nl</code>,也支持 <code>pixiv-cat</code>/<code>i-pixiv-cat</code> 等写法。<br/>
|
||||
自定义镜像:可填写你自建镜像域名(需在管理端“自定义镜像白名单”允许)。<br/>
|
||||
示例:<a href="{examples["img_proxy_cat"]}">{examples["img_proxy_cat"]}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pixiv_cat</code></td>
|
||||
<td>
|
||||
@@ -417,6 +428,15 @@ def _build_docs_html(*, base_url: str) -> str:
|
||||
<p>示例:R18 + 插画 + 竖图 + 250万像素以上 + 收藏≥2 + 评论≥5 + 标签包含 loli(直接出图)。</p>
|
||||
<pre><code>{examples["complex"]}</code></pre>
|
||||
</section>
|
||||
|
||||
<section class="card" style="margin-top: 14px;">
|
||||
<h2>6) 管理端导入(可选)</h2>
|
||||
<p class="muted">管理端支持从文件批量导入图片链接:.txt(每行一个 URL)或 PixivBatchDownloader 导出的 .json。</p>
|
||||
<ul>
|
||||
<li><strong>.json</strong> 导入会自动提取图片链接,并尽可能填充已有元数据/标签;不依赖 refresh token 也能使用(无需额外触发补全)。</li>
|
||||
<li><strong>.txt</strong> 导入仅包含 URL,推荐在有 refresh token 时启用导入后补全,以获得更完整的标签/尺寸/R18/AI 等信息。</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -14,6 +14,7 @@ import sqlalchemy as sa
|
||||
from app.core.pixiv_urls import ALLOWED_IMAGE_EXTS
|
||||
from app.core.pximg_reverse_proxy import (
|
||||
normalize_pximg_mirror_host,
|
||||
normalize_pximg_proxy,
|
||||
pick_pximg_mirror_host_for_request,
|
||||
rewrite_pximg_to_mirror,
|
||||
)
|
||||
@@ -277,6 +278,7 @@ async def proxy_image(
|
||||
background_tasks: BackgroundTasks,
|
||||
pixiv_cat: int = 0,
|
||||
pximg_mirror_host: str | None = None,
|
||||
proxy: str | None = None,
|
||||
):
|
||||
ext = (ext or "").lower()
|
||||
if ext not in ALLOWED_IMAGE_EXTS:
|
||||
@@ -284,12 +286,12 @@ async def proxy_image(
|
||||
if pixiv_cat not in {0, 1}:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pixiv_cat", status_code=400)
|
||||
|
||||
mirror_host_override: str | None = None
|
||||
pximg_mirror_host_override: str | None = None
|
||||
if pximg_mirror_host is not None:
|
||||
raw = str(pximg_mirror_host or "").strip()
|
||||
if raw:
|
||||
mirror_host_override = normalize_pximg_mirror_host(raw)
|
||||
if mirror_host_override is None:
|
||||
pximg_mirror_host_override = normalize_pximg_mirror_host(raw)
|
||||
if pximg_mirror_host_override is None:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pximg_mirror_host", status_code=400)
|
||||
|
||||
engine = request.app.state.engine
|
||||
@@ -327,7 +329,16 @@ async def proxy_image(
|
||||
needs_hydrate = tag_row is None
|
||||
|
||||
runtime = await load_runtime_config(engine)
|
||||
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1
|
||||
proxy_override: str | None = None
|
||||
if proxy is not None:
|
||||
raw = str(proxy or "").strip()
|
||||
if raw:
|
||||
proxy_override = normalize_pximg_proxy(raw, extra_hosts=runtime.image_proxy_extra_pximg_mirror_hosts)
|
||||
if proxy_override is None:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported proxy", status_code=400)
|
||||
|
||||
mirror_host_override = proxy_override or pximg_mirror_host_override
|
||||
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 or proxy_override is not None
|
||||
runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
|
||||
mirror_host = mirror_host_override or (
|
||||
pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host)
|
||||
|
||||
@@ -7,6 +7,7 @@ from app.core.http_stream import stream_url
|
||||
from app.core.pixiv_urls import ALLOWED_IMAGE_EXTS
|
||||
from app.core.pximg_reverse_proxy import (
|
||||
normalize_pximg_mirror_host,
|
||||
normalize_pximg_proxy,
|
||||
pick_pximg_mirror_host_for_request,
|
||||
rewrite_pximg_to_mirror,
|
||||
)
|
||||
@@ -26,6 +27,7 @@ async def legacy_multi(
|
||||
ext: str,
|
||||
pixiv_cat: int = 0,
|
||||
pximg_mirror_host: str | None = None,
|
||||
proxy: str | None = None,
|
||||
):
|
||||
if int(illust_id) <= 0:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported illust_id", status_code=400)
|
||||
@@ -38,12 +40,12 @@ async def legacy_multi(
|
||||
if pixiv_cat not in {0, 1}:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pixiv_cat", status_code=400)
|
||||
|
||||
mirror_host_override: str | None = None
|
||||
pximg_mirror_host_override: str | None = None
|
||||
if pximg_mirror_host is not None:
|
||||
raw = str(pximg_mirror_host or "").strip()
|
||||
if raw:
|
||||
mirror_host_override = normalize_pximg_mirror_host(raw)
|
||||
if mirror_host_override is None:
|
||||
pximg_mirror_host_override = normalize_pximg_mirror_host(raw)
|
||||
if pximg_mirror_host_override is None:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pximg_mirror_host", status_code=400)
|
||||
|
||||
engine = request.app.state.engine
|
||||
@@ -55,7 +57,17 @@ async def legacy_multi(
|
||||
raise ApiError(code=ErrorCode.NOT_FOUND, message="Image not found", status_code=404)
|
||||
|
||||
runtime = await load_runtime_config(engine)
|
||||
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1
|
||||
|
||||
proxy_override: str | None = None
|
||||
if proxy is not None:
|
||||
raw = str(proxy or "").strip()
|
||||
if raw:
|
||||
proxy_override = normalize_pximg_proxy(raw, extra_hosts=runtime.image_proxy_extra_pximg_mirror_hosts)
|
||||
if proxy_override is None:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported proxy", status_code=400)
|
||||
|
||||
mirror_host_override = proxy_override or pximg_mirror_host_override
|
||||
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 or proxy_override is not None
|
||||
runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
|
||||
mirror_host = mirror_host_override or (
|
||||
pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host)
|
||||
@@ -91,6 +103,7 @@ async def legacy_single(
|
||||
ext: str,
|
||||
pixiv_cat: int = 0,
|
||||
pximg_mirror_host: str | None = None,
|
||||
proxy: str | None = None,
|
||||
):
|
||||
if int(illust_id) <= 0:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported illust_id", status_code=400)
|
||||
@@ -101,12 +114,12 @@ async def legacy_single(
|
||||
if pixiv_cat not in {0, 1}:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pixiv_cat", status_code=400)
|
||||
|
||||
mirror_host_override: str | None = None
|
||||
pximg_mirror_host_override: str | None = None
|
||||
if pximg_mirror_host is not None:
|
||||
raw = str(pximg_mirror_host or "").strip()
|
||||
if raw:
|
||||
mirror_host_override = normalize_pximg_mirror_host(raw)
|
||||
if mirror_host_override is None:
|
||||
pximg_mirror_host_override = normalize_pximg_mirror_host(raw)
|
||||
if pximg_mirror_host_override is None:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pximg_mirror_host", status_code=400)
|
||||
|
||||
engine = request.app.state.engine
|
||||
@@ -118,7 +131,17 @@ async def legacy_single(
|
||||
raise ApiError(code=ErrorCode.NOT_FOUND, message="Image not found", status_code=404)
|
||||
|
||||
runtime = await load_runtime_config(engine)
|
||||
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1
|
||||
|
||||
proxy_override: str | None = None
|
||||
if proxy is not None:
|
||||
raw = str(proxy or "").strip()
|
||||
if raw:
|
||||
proxy_override = normalize_pximg_proxy(raw, extra_hosts=runtime.image_proxy_extra_pximg_mirror_hosts)
|
||||
if proxy_override is None:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported proxy", status_code=400)
|
||||
|
||||
mirror_host_override = proxy_override or pximg_mirror_host_override
|
||||
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 or proxy_override is not None
|
||||
runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
|
||||
mirror_host = mirror_host_override or (
|
||||
pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host)
|
||||
|
||||
@@ -15,6 +15,7 @@ from app.core.http_stream import stream_url
|
||||
from app.core.imgproxy import build_signed_processing_url, load_imgproxy_config_from_settings
|
||||
from app.core.pximg_reverse_proxy import (
|
||||
normalize_pximg_mirror_host,
|
||||
normalize_pximg_proxy,
|
||||
pick_pximg_mirror_host_for_request,
|
||||
rewrite_pximg_to_mirror,
|
||||
)
|
||||
@@ -131,6 +132,7 @@ async def random_image(
|
||||
adaptive: int = 0,
|
||||
pixiv_cat: int = 0,
|
||||
pximg_mirror_host: str | None = None,
|
||||
proxy: str | None = None,
|
||||
min_width: int = 0,
|
||||
min_height: int = 0,
|
||||
min_pixels: int = 0,
|
||||
@@ -185,12 +187,12 @@ async def random_image(
|
||||
if pixiv_cat not in {0, 1}:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pixiv_cat", status_code=400)
|
||||
|
||||
mirror_host_override: str | None = None
|
||||
pximg_mirror_host_override: str | None = None
|
||||
if pximg_mirror_host is not None:
|
||||
raw = str(pximg_mirror_host or "").strip()
|
||||
if raw:
|
||||
mirror_host_override = normalize_pximg_mirror_host(raw)
|
||||
if mirror_host_override is None:
|
||||
pximg_mirror_host_override = normalize_pximg_mirror_host(raw)
|
||||
if pximg_mirror_host_override is None:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported pximg_mirror_host", status_code=400)
|
||||
|
||||
layout_source = "orientation"
|
||||
@@ -381,7 +383,17 @@ async def random_image(
|
||||
engine = request.app.state.engine
|
||||
Session = create_sessionmaker(engine)
|
||||
runtime = await load_runtime_config(engine)
|
||||
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1
|
||||
|
||||
proxy_override: str | None = None
|
||||
if proxy is not None:
|
||||
raw = str(proxy or "").strip()
|
||||
if raw:
|
||||
proxy_override = normalize_pximg_proxy(raw, extra_hosts=runtime.image_proxy_extra_pximg_mirror_hosts)
|
||||
if proxy_override is None:
|
||||
raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported proxy", status_code=400)
|
||||
|
||||
mirror_host_override = proxy_override or pximg_mirror_host_override
|
||||
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1 or proxy_override is not None
|
||||
runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
|
||||
mirror_host = mirror_host_override or (
|
||||
pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host)
|
||||
@@ -757,10 +769,13 @@ async def random_image(
|
||||
|
||||
if format == "image" and redirect == 1:
|
||||
qp: list[tuple[str, str]] = []
|
||||
if int(pixiv_cat) == 1:
|
||||
qp.append(("pixiv_cat", "1"))
|
||||
if mirror_host_override is not None:
|
||||
qp.append(("pximg_mirror_host", str(mirror_host_override)))
|
||||
if proxy_override is not None:
|
||||
qp.append(("proxy", str(proxy_override)))
|
||||
else:
|
||||
if int(pixiv_cat) == 1:
|
||||
qp.append(("pixiv_cat", "1"))
|
||||
if pximg_mirror_host_override is not None:
|
||||
qp.append(("pximg_mirror_host", str(pximg_mirror_host_override)))
|
||||
qs = ("?" + "&".join([f"{k}={v}" for k, v in qp])) if qp else ""
|
||||
resp = RedirectResponse(
|
||||
url=f"/i/{image.id}.{image.ext}{qs}",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import ipaddress
|
||||
import re
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
|
||||
@@ -15,18 +17,30 @@ ALLOWED_PXIMG_MIRROR_HOSTS: tuple[str, ...] = (
|
||||
_PXIMG_MIRROR_ALIASES: dict[str, str] = {
|
||||
# pixiv.cat
|
||||
"cat": "i.pixiv.cat",
|
||||
"pixiv-cat": "i.pixiv.cat",
|
||||
"pixiv.cat": "i.pixiv.cat",
|
||||
"i-pixiv-cat": "i.pixiv.cat",
|
||||
"i.pixiv.cat": "i.pixiv.cat",
|
||||
# pixiv.re (often recommended for CN mainland)
|
||||
"re": "i.pixiv.re",
|
||||
"pixiv-re": "i.pixiv.re",
|
||||
"pixiv.re": "i.pixiv.re",
|
||||
"i-pixiv-re": "i.pixiv.re",
|
||||
"i.pixiv.re": "i.pixiv.re",
|
||||
# pixiv.nl (often recommended for CN mainland)
|
||||
"nl": "i.pixiv.nl",
|
||||
"pixiv-nl": "i.pixiv.nl",
|
||||
"pixiv.nl": "i.pixiv.nl",
|
||||
"i-pixiv-nl": "i.pixiv.nl",
|
||||
"i.pixiv.nl": "i.pixiv.nl",
|
||||
}
|
||||
|
||||
_CUSTOM_HOST_RE = re.compile(
|
||||
r"^(?=.{1,253}$)"
|
||||
r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)"
|
||||
r"(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))+$"
|
||||
)
|
||||
|
||||
|
||||
_REQUEST_COUNTRY_HEADERS: tuple[str, ...] = (
|
||||
# Cloudflare
|
||||
@@ -133,6 +147,80 @@ def normalize_pximg_mirror_host(value: object) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def normalize_pximg_custom_mirror_host(value: object) -> str | None:
|
||||
"""
|
||||
Normalize a *custom* pximg mirror host. This does NOT enforce an allowlist.
|
||||
|
||||
Intended to be used for admin-configured allowlists (trusted) or after an allowlist check
|
||||
is performed at the request boundary.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
|
||||
raw = str(value or "").strip().lower()
|
||||
if not raw:
|
||||
return None
|
||||
|
||||
if raw.startswith("http://") or raw.startswith("https://"):
|
||||
try:
|
||||
parsed = urlparse(raw)
|
||||
except Exception:
|
||||
return None
|
||||
raw = (parsed.hostname or "").strip().lower()
|
||||
if not raw:
|
||||
return None
|
||||
|
||||
raw = raw.strip().strip(".")
|
||||
if not raw:
|
||||
return None
|
||||
|
||||
if raw in {"localhost"}:
|
||||
return None
|
||||
|
||||
try:
|
||||
ipaddress.ip_address(raw)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
return None
|
||||
|
||||
if not _CUSTOM_HOST_RE.match(raw):
|
||||
return None
|
||||
|
||||
return raw
|
||||
|
||||
|
||||
def normalize_pximg_proxy(value: object, *, extra_hosts: list[str] | tuple[str, ...] | None) -> str | None:
|
||||
"""
|
||||
Normalize pximg mirror selection from a public `proxy=` query param.
|
||||
|
||||
- Built-in hosts/aliases are always allowed.
|
||||
- Custom hosts are only allowed if present in `extra_hosts` allowlist.
|
||||
"""
|
||||
built_in = normalize_pximg_mirror_host(value)
|
||||
if built_in is not None:
|
||||
return built_in
|
||||
|
||||
candidate = normalize_pximg_custom_mirror_host(value)
|
||||
if candidate is None:
|
||||
return None
|
||||
|
||||
if not extra_hosts:
|
||||
return None
|
||||
|
||||
allow: set[str] = set()
|
||||
for item in extra_hosts:
|
||||
norm = normalize_pximg_custom_mirror_host(item)
|
||||
if norm:
|
||||
allow.add(norm)
|
||||
|
||||
if candidate in allow:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def rewrite_pximg_to_mirror(url: str, *, mirror_host: str) -> str:
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
@@ -142,7 +230,7 @@ def rewrite_pximg_to_mirror(url: str, *, mirror_host: str) -> str:
|
||||
if not is_pximg_image_url(url):
|
||||
return url
|
||||
|
||||
mirror = normalize_pximg_mirror_host(mirror_host) or ""
|
||||
mirror = normalize_pximg_mirror_host(mirror_host) or normalize_pximg_custom_mirror_host(mirror_host) or ""
|
||||
if not mirror:
|
||||
return url
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
|
||||
from app.core.pximg_reverse_proxy import DEFAULT_PXIMG_MIRROR_HOST, normalize_pximg_mirror_host
|
||||
from app.core.pximg_reverse_proxy import (
|
||||
DEFAULT_PXIMG_MIRROR_HOST,
|
||||
normalize_pximg_custom_mirror_host,
|
||||
normalize_pximg_mirror_host,
|
||||
)
|
||||
from app.core.logging import get_logger
|
||||
from app.core.time import iso_utc_ms
|
||||
from app.db.models.runtime_settings import RuntimeSetting
|
||||
@@ -57,6 +61,7 @@ class RuntimeConfig:
|
||||
proxy_default_pool_id: int | None
|
||||
image_proxy_use_pixiv_cat: bool
|
||||
image_proxy_pximg_mirror_host: str
|
||||
image_proxy_extra_pximg_mirror_hosts: list[str]
|
||||
random_defaults: dict[str, Any]
|
||||
hide_origin_url_in_public_json: bool
|
||||
rate_limit: dict[str, Any]
|
||||
@@ -72,6 +77,7 @@ class RuntimeConfig:
|
||||
proxy_default_pool_id=None,
|
||||
image_proxy_use_pixiv_cat=False,
|
||||
image_proxy_pximg_mirror_host=str(DEFAULT_PXIMG_MIRROR_HOST),
|
||||
image_proxy_extra_pximg_mirror_hosts=[],
|
||||
random_defaults={},
|
||||
hide_origin_url_in_public_json=True,
|
||||
rate_limit={},
|
||||
@@ -140,6 +146,16 @@ def runtime_config_from_values(values: dict[str, Any]) -> RuntimeConfig:
|
||||
mirror_host_raw = values.get("image_proxy.pximg_mirror_host")
|
||||
mirror_host = normalize_pximg_mirror_host(mirror_host_raw)
|
||||
|
||||
extra_mirrors_raw = _as_str_list(values.get("image_proxy.extra_pximg_mirror_hosts"))
|
||||
extra_mirrors: list[str] = []
|
||||
seen_extra: set[str] = set()
|
||||
for item in extra_mirrors_raw:
|
||||
host = normalize_pximg_custom_mirror_host(item)
|
||||
if not host or host in seen_extra:
|
||||
continue
|
||||
seen_extra.add(host)
|
||||
extra_mirrors.append(host)
|
||||
|
||||
random_defaults_raw = values.get("random.defaults")
|
||||
random_defaults = defaults.random_defaults
|
||||
if isinstance(random_defaults_raw, dict):
|
||||
@@ -164,6 +180,7 @@ def runtime_config_from_values(values: dict[str, Any]) -> RuntimeConfig:
|
||||
if image_proxy_use_pixiv_cat is not None
|
||||
else defaults.image_proxy_use_pixiv_cat,
|
||||
image_proxy_pximg_mirror_host=str(mirror_host or defaults.image_proxy_pximg_mirror_host),
|
||||
image_proxy_extra_pximg_mirror_hosts=extra_mirrors,
|
||||
random_defaults=random_defaults,
|
||||
hide_origin_url_in_public_json=hide_origin_url
|
||||
if hide_origin_url is not None
|
||||
|
||||
@@ -29,6 +29,7 @@ describe("SettingsPage", () => {
|
||||
expect(body.settings.proxy.default_pool_id).toBe("");
|
||||
expect(body.settings.image_proxy.use_pixiv_cat).toBe(false);
|
||||
expect(body.settings.image_proxy.pximg_mirror_host).toBe("i.pixiv.cat");
|
||||
expect(body.settings.image_proxy.extra_pximg_mirror_hosts).toEqual([]);
|
||||
expect(body.settings.random.default_attempts).toBe(3);
|
||||
expect(body.settings.random.default_r18_strict).toBe(true);
|
||||
expect(body.settings.random.fail_cooldown_ms).toBe(600000);
|
||||
@@ -37,7 +38,7 @@ describe("SettingsPage", () => {
|
||||
expect(body.settings.security.hide_origin_url_in_public_json).toBe(true);
|
||||
expect(body.settings.rate_limit.pixiv_hydrate_min_interval_ms).toBe(800);
|
||||
expect(body.settings.rate_limit.pixiv_hydrate_jitter_ms).toBe(200);
|
||||
return new Response(JSON.stringify({ ok: true, updated: 9, request_id: "req_save" }), {
|
||||
return new Response(JSON.stringify({ ok: true, updated: 10, request_id: "req_save" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
@@ -54,7 +55,7 @@ describe("SettingsPage", () => {
|
||||
allowlist_domains: ["pixiv.net"],
|
||||
default_pool_id: "",
|
||||
},
|
||||
image_proxy: { use_pixiv_cat: false, pximg_mirror_host: "i.pixiv.cat" },
|
||||
image_proxy: { use_pixiv_cat: false, pximg_mirror_host: "i.pixiv.cat", extra_pximg_mirror_hosts: [] },
|
||||
random: { default_attempts: 3, default_r18_strict: true, fail_cooldown_ms: 600000, strategy: "quality", quality_samples: 5 },
|
||||
security: { hide_origin_url_in_public_json: true },
|
||||
rate_limit: {},
|
||||
@@ -98,7 +99,7 @@ describe("SettingsPage", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: /保存设置/ }));
|
||||
|
||||
expect(await screen.findByText("保存成功")).toBeInTheDocument();
|
||||
expect(await screen.findByText(/更新条目数:\s*9/)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/更新条目数:\s*10/)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/请求ID:\s*req_save/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -37,6 +37,7 @@ type SettingsFormValues = {
|
||||
proxy_default_pool_id: number;
|
||||
image_proxy_use_pixiv_cat: boolean;
|
||||
image_proxy_pximg_mirror_host: "i.pixiv.cat" | "i.pixiv.re" | "i.pixiv.nl";
|
||||
image_proxy_extra_pximg_mirror_hosts: string[];
|
||||
random_default_attempts: number;
|
||||
random_default_r18_strict: boolean;
|
||||
random_fail_cooldown_ms: number;
|
||||
@@ -147,6 +148,7 @@ export function SettingsPage() {
|
||||
proxy_default_pool_id: asInt(proxy.default_pool_id, 0),
|
||||
image_proxy_use_pixiv_cat: asBool(imageProxy.use_pixiv_cat, false),
|
||||
image_proxy_pximg_mirror_host: asPximgMirrorHost(imageProxy.pximg_mirror_host, "i.pixiv.cat"),
|
||||
image_proxy_extra_pximg_mirror_hosts: asStrList(imageProxy.extra_pximg_mirror_hosts),
|
||||
random_default_attempts: asInt(random.default_attempts, 3),
|
||||
random_default_r18_strict: asBool(random.default_r18_strict, true),
|
||||
random_fail_cooldown_ms: asInt(random.fail_cooldown_ms, 600_000),
|
||||
@@ -174,6 +176,7 @@ export function SettingsPage() {
|
||||
image_proxy: {
|
||||
use_pixiv_cat: values.image_proxy_use_pixiv_cat,
|
||||
pximg_mirror_host: values.image_proxy_pximg_mirror_host,
|
||||
extra_pximg_mirror_hosts: values.image_proxy_extra_pximg_mirror_hosts,
|
||||
},
|
||||
random: {
|
||||
default_attempts: values.random_default_attempts,
|
||||
@@ -312,6 +315,13 @@ export function SettingsPage() {
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="自定义镜像白名单"
|
||||
name="image_proxy_extra_pximg_mirror_hosts"
|
||||
extra="用于公开接口的 proxy= 参数:仅允许这里配置的自定义域名被用作图片上游镜像(防止 SSRF)。示例:i.mirror.example.com"
|
||||
>
|
||||
<Select mode="tags" style={{ maxWidth: 520 }} tokenSeparators={[",", "\n", " "]} placeholder="例如:i.mirror.example.com" />
|
||||
</Form.Item>
|
||||
|
||||
<Typography.Title level={5} style={{ marginTop: 12 }}>
|
||||
随机接口设置
|
||||
|
||||
Reference in new issue
Block a user