feat: .md download bundles thumbnails, auto-cache on channel add
- Bulk 'Download .md' now also caches thumbnails (one action does both); removed the redundant standalone Thumbnails button from the bulk action bar. - Scrape UX optimization: batch job skips videos whose .md already exists (just caches their thumbnail) instead of re-extracting. - Auto-thumbnails: on addChannel, thumbnails cache in the background so a newly-attached channel's catalog looks populated immediately, even before any .md is generated. - Thumbnail fallback: pending videos without a stored thumbnail URL use the canonical https://i.ytimg.com/vi/<id>/hqdefault.jpg, so every video shows a thumbnail (and /api/thumbnails/{id} never 404s — redirects if uncached). - New helpers in pipeline.py: thumbnail_url_for() + cache_thumbnail().
This commit is contained in:
@@ -332,41 +332,29 @@ def build_router(store: Store, cfg: Config, jobs) -> APIRouter:
|
||||
# -------------------------------------------------- thumbnails (local cache)
|
||||
@r.post("/tools/thumbnails")
|
||||
async def download_thumbnails(payload: dict):
|
||||
import requests as _req
|
||||
from ..pipeline import cache_thumbnail
|
||||
video_ids = (payload or {}).get("video_ids") or []
|
||||
channel_id = (payload or {}).get("channel_id")
|
||||
if channel_id and not video_ids:
|
||||
video_ids = [v.video_id for v in store.get_all(channel_id) if v.thumbnail]
|
||||
video_ids = [v.video_id for v in store.get_all(channel_id)]
|
||||
out_dir = Path(cfg.output_dir_resolved).parent / "thumbnails"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
n = 0
|
||||
for vid in video_ids:
|
||||
v = store.get_video(vid)
|
||||
if not v or not v.thumbnail:
|
||||
continue
|
||||
target = out_dir / f"{vid}.jpg"
|
||||
if target.exists():
|
||||
if cache_thumbnail(store, vid, out_dir):
|
||||
n += 1
|
||||
continue
|
||||
try:
|
||||
r2 = _req.get(v.thumbnail, timeout=15, headers={"User-Agent": "Mozilla/5.0"})
|
||||
r2.raise_for_status()
|
||||
target.write_bytes(r2.content)
|
||||
n += 1
|
||||
except Exception:
|
||||
continue
|
||||
return {"downloaded": n, "dir": str(out_dir)}
|
||||
|
||||
@r.get("/thumbnails/{video_id}")
|
||||
def serve_thumbnail(video_id: str):
|
||||
from fastapi.responses import RedirectResponse
|
||||
from ..pipeline import thumbnail_url_for
|
||||
local = Path(cfg.output_dir_resolved).parent / "thumbnails" / f"{video_id}.jpg"
|
||||
if local.exists():
|
||||
return FileResponse(str(local), media_type="image/jpeg")
|
||||
v = store.get_video(video_id)
|
||||
if v and v.thumbnail:
|
||||
return RedirectResponse(url=v.thumbnail, status_code=302)
|
||||
raise HTTPException(404, "no thumbnail")
|
||||
url = thumbnail_url_for(v) if v else f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg"
|
||||
return RedirectResponse(url=url, status_code=302)
|
||||
|
||||
# -------------------------------------------------- clip (transcript segment)
|
||||
@r.get("/clip/{video_id}")
|
||||
|
||||
Reference in New Issue
Block a user