feat(downloads): editable details with clickable channel and extra links
The edit (pencil) action now edits a download's full display metadata — title, channel name, channel link and any number of extra reference URLs — instead of just the name. The channel and links render as clickable links on the library card, and the channel link is auto-filled from the source (yt-dlp channel_url) when available. Shared watch pages resolve the same per-download overrides, so a rename/channel/link edit is reflected on the public /watch page too, with every link clickable. Adds migration 0049 (media_assets.uploader_url; download_jobs.display_uploader, display_uploader_url, extra_links) and generalizes the rename endpoint into a metadata update with URL validation. EN/HU/DE strings included.
This commit is contained in:
parent
04d35375ed
commit
8c86c6b4a8
12 changed files with 327 additions and 41 deletions
|
|
@ -35,10 +35,10 @@ def _link_or_404(db: Session, token: str) -> DownloadLink:
|
|||
return link
|
||||
|
||||
|
||||
def _ready_asset(db: Session, link: DownloadLink) -> MediaAsset:
|
||||
def _ready_target(db: Session, link: DownloadLink) -> tuple[DownloadJob, MediaAsset]:
|
||||
job = db.get(DownloadJob, link.job_id)
|
||||
asset = db.get(MediaAsset, job.asset_id) if job and job.asset_id else None
|
||||
if asset is None or asset.status != "ready" or not asset.rel_path:
|
||||
if job is None or asset is None or asset.status != "ready" or not asset.rel_path:
|
||||
raise HTTPException(status_code=404, detail="This video is no longer available.")
|
||||
# Verify the file is actually on disk here (not just flagged ready), so the watch page never
|
||||
# loads a player for a missing file — meta and file agree.
|
||||
|
|
@ -46,7 +46,11 @@ def _ready_asset(db: Session, link: DownloadLink) -> MediaAsset:
|
|||
root = Path(settings.download_root).resolve()
|
||||
if root not in path.parents or not path.exists():
|
||||
raise HTTPException(status_code=404, detail="This video is no longer available.")
|
||||
return asset
|
||||
return job, asset
|
||||
|
||||
|
||||
def _ready_asset(db: Session, link: DownloadLink) -> MediaAsset:
|
||||
return _ready_target(db, link)[1]
|
||||
|
||||
|
||||
def _file_url(token: str, grant: str | None = None) -> str:
|
||||
|
|
@ -61,10 +65,10 @@ def watch_meta(token: str, db: Session = Depends(get_db)) -> dict:
|
|||
link = _link_or_404(db, token)
|
||||
if link.password_hash:
|
||||
return {"needs_password": True}
|
||||
asset = _ready_asset(db, link)
|
||||
job, asset = _ready_target(db, link)
|
||||
link.view_count += 1
|
||||
db.commit()
|
||||
return {"needs_password": False, **linksmod.public_meta(link, asset, _file_url(token))}
|
||||
return {"needs_password": False, **linksmod.public_meta(link, job, asset, _file_url(token))}
|
||||
|
||||
|
||||
@router.post("/watch/{token}/unlock")
|
||||
|
|
@ -75,15 +79,15 @@ def watch_unlock(token: str, payload: dict, db: Session = Depends(get_db)) -> di
|
|||
link = _link_or_404(db, token)
|
||||
if not link.password_hash:
|
||||
# Not a password link — nothing to unlock; just hand back the plain meta.
|
||||
asset = _ready_asset(db, link)
|
||||
return {"needs_password": False, **linksmod.public_meta(link, asset, _file_url(token))}
|
||||
job, asset = _ready_target(db, link)
|
||||
return {"needs_password": False, **linksmod.public_meta(link, job, asset, _file_url(token))}
|
||||
if not verify_password((payload.get("password") or ""), link.password_hash):
|
||||
raise HTTPException(status_code=403, detail="Wrong password.")
|
||||
asset = _ready_asset(db, link)
|
||||
job, asset = _ready_target(db, link)
|
||||
link.view_count += 1
|
||||
db.commit()
|
||||
grant = linksmod.make_grant(token)
|
||||
return {"needs_password": False, **linksmod.public_meta(link, asset, _file_url(token, grant))}
|
||||
return {"needs_password": False, **linksmod.public_meta(link, job, asset, _file_url(token, grant))}
|
||||
|
||||
|
||||
@router.get("/watch/{token}/file")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue