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:
+101
-38
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user