fix: move wallpaper routes before /{key} catch-all to fix 404
Bing wallpaper endpoint (/bing-wallpapers) was returning 404 from HTTP
because FastAPI matched it against the /{key} catch-all route at position
2 before the specific route at position 13 ever got reached.
- Moved wallpaper routes (bing-wallpapers, bing-wallpaper) and helper code
before the /{key} wildcard route
- Removed dead guard code in get_setting() that was never effective —
FastAPI route matching happens before the handler body executes
- Removed duplicate wallpaper code block at end of file
Root cause: FastAPI matches routes in registration order. /{key} at
position 2 matched 'bing-wallpapers' as the key parameter, returning
'Setting not found' instead of routing to the wallpaper endpoint.
This commit is contained in:
+127
-130
@@ -148,12 +148,136 @@ async def get_ip_allowlist(
|
||||
}
|
||||
|
||||
|
||||
# ── Bing Daily Wallpaper — download 12+ images, rotate hourly ──
|
||||
_WALLPAPER_DIR = Path(__file__).resolve().parent.parent.parent / "web" / "app" / "wallpapers"
|
||||
_MAX_WALLPAPERS = 12 # keep 12 images (12 hours rotation before repeat)
|
||||
_MAX_AGE_DAYS = 7 # auto-clean older than 7 days
|
||||
|
||||
|
||||
def _cleanup_old_wallpapers() -> int:
|
||||
"""Remove wallpapers older than 7 days. Returns count removed."""
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
cutoff = datetime.utcnow() - timedelta(days=_MAX_AGE_DAYS)
|
||||
removed = 0
|
||||
for f in sorted(_WALLPAPER_DIR.glob("*.jpg")):
|
||||
try:
|
||||
mtime = datetime.utcfromtimestamp(f.stat().st_mtime)
|
||||
if mtime < cutoff:
|
||||
f.unlink()
|
||||
removed += 1
|
||||
except Exception:
|
||||
pass
|
||||
return removed
|
||||
|
||||
|
||||
@router.get("/bing-wallpapers", response_model=dict)
|
||||
async def bing_wallpapers():
|
||||
"""Return all cached wallpaper URLs for frontend slideshow rotation.
|
||||
|
||||
Syncs with Bing daily (pulls up to 8 days of images), caches to disk,
|
||||
cleans up old files, returns sorted list of local URLs.
|
||||
"""
|
||||
import httpx
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
_WALLPAPER_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Download today + last 7 days from Bing (n=8 gives ~8 images)
|
||||
downloaded = 0
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=12.0) as client:
|
||||
# Fetch up to 8 recent images from Bing
|
||||
resp = await client.get(
|
||||
"https://cn.bing.com/HPImageArchive.aspx",
|
||||
params={"format": "js", "idx": 0, "n": 8, "mkt": "zh-CN"},
|
||||
)
|
||||
data = resp.json()
|
||||
for img in (data.get("images") or []):
|
||||
img_path = img.get("url")
|
||||
if not img_path:
|
||||
continue
|
||||
# Use date from Bing metadata as filename
|
||||
date_str = img.get("startdate", "") or img.get("fullstartdate", "")[:8] or ""
|
||||
if not date_str:
|
||||
# fallback: extract from URL
|
||||
import re
|
||||
m = re.search(r'OHR\.(\w+)_', img_path)
|
||||
date_str = m.group(1) if m else f"unknown_{downloaded}"
|
||||
|
||||
cached = _WALLPAPER_DIR / f"{date_str}.jpg"
|
||||
if cached.exists() and cached.stat().st_size > 1024:
|
||||
continue # already cached
|
||||
|
||||
try:
|
||||
img_resp = await client.get(f"https://cn.bing.com{img_path}")
|
||||
if img_resp.status_code == 200:
|
||||
cached.write_bytes(img_resp.content)
|
||||
downloaded += 1
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Cleanup old files
|
||||
_cleanup_old_wallpapers()
|
||||
|
||||
# Also keep only newest N files if we somehow have too many
|
||||
all_files = sorted(_WALLPAPER_DIR.glob("*.jpg"), key=lambda f: f.stat().st_mtime, reverse=True)
|
||||
for f in all_files[_MAX_WALLPAPERS:]:
|
||||
try:
|
||||
f.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Return sorted URLs (newest first)
|
||||
urls = [f"/app/wallpapers/{f.name}" for f in sorted(all_files[:_MAX_WALLPAPERS], key=lambda f: f.name, reverse=True)]
|
||||
return {"urls": urls, "count": len(urls)}
|
||||
|
||||
|
||||
# Keep the old single-image endpoint for backward compatibility
|
||||
@router.get("/bing-wallpaper", response_model=dict)
|
||||
async def bing_wallpaper():
|
||||
"""Return today's wallpaper URL (backward compat)."""
|
||||
try:
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
|
||||
_WALLPAPER_DIR.mkdir(parents=True, exist_ok=True)
|
||||
today = datetime.utcnow().strftime("%Y-%m-%d")
|
||||
cached = _WALLPAPER_DIR / f"{today}.jpg"
|
||||
|
||||
if cached.exists() and cached.stat().st_size > 1024:
|
||||
return {"url": f"/app/wallpapers/{today}.jpg"}
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(
|
||||
"https://cn.bing.com/HPImageArchive.aspx",
|
||||
params={"format": "js", "idx": 0, "n": 1, "mkt": "zh-CN"},
|
||||
)
|
||||
data = resp.json()
|
||||
img = (data.get("images") or [{}])[0]
|
||||
img_url = img.get("url")
|
||||
if not img_url:
|
||||
return {"url": None}
|
||||
|
||||
img_resp = await client.get(f"https://cn.bing.com{img_url}")
|
||||
if img_resp.status_code == 200:
|
||||
cached.write_bytes(img_resp.content)
|
||||
|
||||
_cleanup_old_wallpapers()
|
||||
return {"url": f"/app/wallpapers/{today}.jpg"}
|
||||
except Exception:
|
||||
# Try any cached file
|
||||
cached_list = sorted(_WALLPAPER_DIR.glob("*.jpg"))
|
||||
if cached_list:
|
||||
return {"url": f"/app/wallpapers/{cached_list[-1].name}"}
|
||||
return {"url": None}
|
||||
|
||||
|
||||
@router.get("/{key}", response_model=dict)
|
||||
async def get_setting(key: str, admin: Admin = Depends(get_current_admin), db: AsyncSession = Depends(get_db)):
|
||||
"""Get a single setting by key (sensitive values masked)"""
|
||||
# Skip catch-all for known fixed paths
|
||||
if key in ("bing-wallpaper", "bing-wallpapers"):
|
||||
raise HTTPException(status_code=404, detail="use dedicated wallpaper endpoints")
|
||||
repo = SettingRepositoryImpl(db)
|
||||
value = await repo.get(key)
|
||||
if value is None:
|
||||
@@ -1265,130 +1389,3 @@ def _retry_to_dict(job: PushRetryJob) -> dict:
|
||||
"last_error": job.last_error,
|
||||
"created_at": str(job.created_at) if job.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ── Bing Daily Wallpaper — download 12+ images, rotate hourly ──
|
||||
_WALLPAPER_DIR = Path(__file__).resolve().parent.parent.parent / "web" / "app" / "wallpapers"
|
||||
_MAX_WALLPAPERS = 12 # keep 12 images (12 hours rotation before repeat)
|
||||
_MAX_AGE_DAYS = 7 # auto-clean older than 7 days
|
||||
|
||||
|
||||
def _cleanup_old_wallpapers() -> int:
|
||||
"""Remove wallpapers older than 7 days. Returns count removed."""
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
cutoff = datetime.utcnow() - timedelta(days=_MAX_AGE_DAYS)
|
||||
removed = 0
|
||||
for f in sorted(_WALLPAPER_DIR.glob("*.jpg")):
|
||||
try:
|
||||
mtime = datetime.utcfromtimestamp(f.stat().st_mtime)
|
||||
if mtime < cutoff:
|
||||
f.unlink()
|
||||
removed += 1
|
||||
except Exception:
|
||||
pass
|
||||
return removed
|
||||
|
||||
|
||||
@router.get("/bing-wallpapers", response_model=dict)
|
||||
async def bing_wallpapers():
|
||||
"""Return all cached wallpaper URLs for frontend slideshow rotation.
|
||||
|
||||
Syncs with Bing daily (pulls up to 8 days of images), caches to disk,
|
||||
cleans up old files, returns sorted list of local URLs.
|
||||
"""
|
||||
import httpx
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
_WALLPAPER_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Download today + last 7 days from Bing (n=8 gives ~8 images)
|
||||
downloaded = 0
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=12.0) as client:
|
||||
# Fetch up to 8 recent images from Bing
|
||||
resp = await client.get(
|
||||
"https://cn.bing.com/HPImageArchive.aspx",
|
||||
params={"format": "js", "idx": 0, "n": 8, "mkt": "zh-CN"},
|
||||
)
|
||||
data = resp.json()
|
||||
for img in (data.get("images") or []):
|
||||
img_path = img.get("url")
|
||||
if not img_path:
|
||||
continue
|
||||
# Use date from Bing metadata as filename
|
||||
date_str = img.get("startdate", "") or img.get("fullstartdate", "")[:8] or ""
|
||||
if not date_str:
|
||||
# fallback: extract from URL
|
||||
import re
|
||||
m = re.search(r'OHR\.(\w+)_', img_path)
|
||||
date_str = m.group(1) if m else f"unknown_{downloaded}"
|
||||
|
||||
cached = _WALLPAPER_DIR / f"{date_str}.jpg"
|
||||
if cached.exists() and cached.stat().st_size > 1024:
|
||||
continue # already cached
|
||||
|
||||
try:
|
||||
img_resp = await client.get(f"https://cn.bing.com{img_path}")
|
||||
if img_resp.status_code == 200:
|
||||
cached.write_bytes(img_resp.content)
|
||||
downloaded += 1
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Cleanup old files
|
||||
_cleanup_old_wallpapers()
|
||||
|
||||
# Also keep only newest N files if we somehow have too many
|
||||
all_files = sorted(_WALLPAPER_DIR.glob("*.jpg"), key=lambda f: f.stat().st_mtime, reverse=True)
|
||||
for f in all_files[_MAX_WALLPAPERS:]:
|
||||
try:
|
||||
f.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Return sorted URLs (newest first)
|
||||
urls = [f"/app/wallpapers/{f.name}" for f in sorted(all_files[:_MAX_WALLPAPERS], key=lambda f: f.name, reverse=True)]
|
||||
return {"urls": urls, "count": len(urls)}
|
||||
|
||||
|
||||
# Keep the old single-image endpoint for backward compatibility
|
||||
@router.get("/bing-wallpaper", response_model=dict)
|
||||
async def bing_wallpaper():
|
||||
"""Return today's wallpaper URL (backward compat)."""
|
||||
try:
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
|
||||
_WALLPAPER_DIR.mkdir(parents=True, exist_ok=True)
|
||||
today = datetime.utcnow().strftime("%Y-%m-%d")
|
||||
cached = _WALLPAPER_DIR / f"{today}.jpg"
|
||||
|
||||
if cached.exists() and cached.stat().st_size > 1024:
|
||||
return {"url": f"/app/wallpapers/{today}.jpg"}
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(
|
||||
"https://cn.bing.com/HPImageArchive.aspx",
|
||||
params={"format": "js", "idx": 0, "n": 1, "mkt": "zh-CN"},
|
||||
)
|
||||
data = resp.json()
|
||||
img = (data.get("images") or [{}])[0]
|
||||
img_url = img.get("url")
|
||||
if not img_url:
|
||||
return {"url": None}
|
||||
|
||||
img_resp = await client.get(f"https://cn.bing.com{img_url}")
|
||||
if img_resp.status_code == 200:
|
||||
cached.write_bytes(img_resp.content)
|
||||
|
||||
_cleanup_old_wallpapers()
|
||||
return {"url": f"/app/wallpapers/{today}.jpg"}
|
||||
except Exception:
|
||||
# Try any cached file
|
||||
cached_list = sorted(_WALLPAPER_DIR.glob("*.jpg"))
|
||||
if cached_list:
|
||||
return {"url": f"/app/wallpapers/{cached_list[-1].name}"}
|
||||
return {"url": None}
|
||||
|
||||
Reference in New Issue
Block a user