feat: Bing wallpaper slideshow — 12 images, hourly rotation, weekly cleanup

- Frontend: fullscreen wallpaper background with 1.5s crossfade transition,
  login card centered with glass effect, images rotate every hour
- Backend: new /api/settings/bing-wallpapers endpoint fetches 8 recent images
  from Bing HPImageArchive, caches to web/app/wallpapers/, returns all URLs
- Auto-cleanup: removes files older than 7 days, keeps max 12 newest
- Old /bing-wallpaper endpoint kept for backward compatibility

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-05-31 20:51:50 +08:00
parent 3d5870b404
commit c9baecfdfa
2 changed files with 160 additions and 90 deletions
+101 -38
View File
@@ -1264,31 +1264,109 @@ def _retry_to_dict(job: PushRetryJob) -> dict:
}
# ── Bing Daily Wallpaper — download + cache to disk ──
# ── 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
@router.get("/bing-wallpaper", response_model=dict)
async def bing_wallpaper():
"""Download the Bing daily wallpaper, cache to disk, return the local URL.
No auth required — used by the login page.
A cron job or this endpoint itself cleans up wallpapers 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)
today = datetime.utcnow().strftime("%Y-%m-%d")
cached = _WALLPAPER_DIR / f"{today}.jpg"
# Return cached version if already downloaded today
if cached.exists() and cached.stat().st_size > 1024:
return {"url": f"/app/wallpapers/{today}.jpg"}
# 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:
# 1. Get today's image metadata
resp = await client.get(
"https://cn.bing.com/HPImageArchive.aspx",
params={"format": "js", "idx": 0, "n": 1, "mkt": "zh-CN"},
@@ -1297,32 +1375,17 @@ async def bing_wallpaper():
img = (data.get("images") or [{}])[0]
img_url = img.get("url")
if not img_url:
raise ValueError("no image url from Bing")
return {"url": None}
# 2. Download the actual image
img_resp = await client.get(f"https://cn.bing.com{img_url}")
if img_resp.status_code != 200:
raise ValueError(f"Bing returned {img_resp.status_code}")
# 3. Save to disk
cached.write_bytes(img_resp.content)
# 4. Remove wallpapers older than 7 days
cutoff = datetime.utcnow() - timedelta(days=7)
for f in _WALLPAPER_DIR.glob("*.jpg"):
try:
mtime = datetime.utcfromtimestamp(f.stat().st_mtime)
if mtime < cutoff:
f.unlink()
except Exception:
pass
if img_resp.status_code == 200:
cached.write_bytes(img_resp.content)
_cleanup_old_wallpapers()
return {"url": f"/app/wallpapers/{today}.jpg"}
except Exception:
# If download fails, try yesterday's cached file
yesterday = (datetime.utcnow() - timedelta(days=1)).strftime("%Y-%m-%d")
yesterday_cached = _WALLPAPER_DIR / f"{yesterday}.jpg"
if yesterday_cached.exists():
return {"url": f"/app/wallpapers/{yesterday}.jpg"}
# 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}
return {"url": f"/app/wallpapers/{today}.jpg"}