feat(downloads): store + show a canonical "downloaded from" source URL
Every download now records the clean source page URL and shows it on the Downloads page (open in a new tab, or copy to clipboard). The worker stores yt-dlp's canonical webpage_url on the asset (migration 0043 adds media_assets.source_webpage_url); the serializer prefers it and falls back to a URL derived from source_kind+source_ref, so YouTube, external YouTube links and external URLs (e.g. Facebook reels) all get a correct reference, and queued/older rows work before the worker fills it. Edit clips return null (a clip's source is the user's own earlier download, not a web page). i18n en/hu/de.
This commit is contained in:
parent
0e1b24a93c
commit
42d465d760
9 changed files with 115 additions and 0 deletions
27
backend/alembic/versions/0043_asset_source_url.py
Normal file
27
backend/alembic/versions/0043_asset_source_url.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"""canonical source webpage URL on media assets
|
||||
|
||||
Revision ID: 0043_asset_source_url
|
||||
Revises: 0042_download_links
|
||||
Create Date: 2026-07-04
|
||||
|
||||
Stores yt-dlp's canonical `webpage_url` for a download's source, so the Downloads page can show a
|
||||
clean "downloaded from" reference (copyable + openable). Nullable — filled by the worker on
|
||||
extraction; existing/queued rows fall back to a URL derived from source_kind+source_ref.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0043_asset_source_url"
|
||||
down_revision: Union[str, None] = "0042_download_links"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("media_assets", sa.Column("source_webpage_url", sa.Text(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("media_assets", "source_webpage_url")
|
||||
|
|
@ -741,6 +741,10 @@ class MediaAsset(Base, TimestampMixin):
|
|||
uploader: Mapped[str | None] = mapped_column(String(255))
|
||||
upload_date: Mapped[date | None] = mapped_column(Date)
|
||||
thumbnail_url: Mapped[str | None] = mapped_column(Text)
|
||||
# yt-dlp's canonical page URL for the source (webpage_url) — the clean "downloaded from"
|
||||
# reference shown on the Downloads page. Null until the worker extracts it (older/queued rows
|
||||
# fall back to a URL derived from source_kind+source_ref).
|
||||
source_webpage_url: Mapped[str | None] = mapped_column(Text)
|
||||
error: Mapped[str | None] = mapped_column(Text)
|
||||
ref_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
|
||||
last_access_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
|
|
|||
|
|
@ -68,6 +68,20 @@ def resolve_source(raw: str) -> tuple[str, str]:
|
|||
raise HTTPException(status_code=400, detail="That doesn't look like a valid video link.")
|
||||
|
||||
|
||||
def _reference_url(job: DownloadJob, asset: MediaAsset | None) -> str | None:
|
||||
"""The clean "downloaded from" URL for the Downloads page: the canonical page URL yt-dlp
|
||||
recorded, else one derived from source_kind+source_ref (covers queued/older rows before the
|
||||
worker fills it). Returns None for edit derivatives — a clip's source is the user's own earlier
|
||||
download, not a web page, so there's no meaningful external URL to show."""
|
||||
if job.job_kind == "edit":
|
||||
return None
|
||||
if asset is not None and asset.source_webpage_url:
|
||||
return asset.source_webpage_url
|
||||
if job.source_kind in ("youtube", "url"):
|
||||
return service.source_url(job.source_kind, job.source_ref)
|
||||
return None
|
||||
|
||||
|
||||
def _serialize(job: DownloadJob, asset: MediaAsset | None) -> dict:
|
||||
ready = asset is not None and asset.status == "ready" and bool(asset.rel_path)
|
||||
return {
|
||||
|
|
@ -82,6 +96,7 @@ def _serialize(job: DownloadJob, asset: MediaAsset | None) -> dict:
|
|||
"created_at": job.created_at.isoformat() if job.created_at else None,
|
||||
"source_kind": job.source_kind,
|
||||
"source_ref": job.source_ref,
|
||||
"source_url": _reference_url(job, asset),
|
||||
"profile_id": job.profile_id,
|
||||
"spec": job.profile_snapshot,
|
||||
"display_name": job.display_name,
|
||||
|
|
|
|||
|
|
@ -221,6 +221,7 @@ def _fill_asset_meta_early(asset_id: int, info: dict) -> None:
|
|||
asset.thumbnail_url = info.get("thumbnail")
|
||||
asset.duration_s = int(info["duration"]) if info.get("duration") else None
|
||||
asset.upload_date = _parse_upload_date(info.get("upload_date"))
|
||||
asset.source_webpage_url = info.get("webpage_url")
|
||||
db.commit()
|
||||
|
||||
|
||||
|
|
@ -409,6 +410,7 @@ def _download(job_id: int, asset_id: int, source_kind: str, source_ref: str, spe
|
|||
asset.uploader = meta.uploader
|
||||
asset.upload_date = meta.upload_date
|
||||
asset.thumbnail_url = meta.thumbnail_url
|
||||
asset.source_webpage_url = info.get("webpage_url")
|
||||
asset.nfo_written = nfo_ok
|
||||
asset.error = None
|
||||
asset.last_access_at = datetime.now(timezone.utc)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue