From 0bab021e21fcc2d83e24b12c12ee692c43c63cb0 Mon Sep 17 00:00:00 2001 From: mini Date: Wed, 18 Feb 2026 23:00:49 +0800 Subject: [PATCH] feat: dedup downloads by (url, format_id) index, reuse existing files --- backend/app/database.py | 6 ++++++ backend/app/models.py | 8 ++++++-- backend/app/routes/download.py | 13 +++++++++++++ frontend/src/views/Home.vue | 13 +++++++++++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/backend/app/database.py b/backend/app/database.py index 38e75b3..44bb099 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -21,3 +21,9 @@ async def get_db(): async def init_db(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) + # Ensure composite index exists on already-created tables (idempotent) + await conn.execute( + __import__("sqlalchemy").text( + "CREATE INDEX IF NOT EXISTS ix_video_url_format_id ON videos (url, format_id)" + ) + ) diff --git a/backend/app/models.py b/backend/app/models.py index e655bee..f096381 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,6 +1,6 @@ """SQLAlchemy models.""" from datetime import datetime -from sqlalchemy import Column, Integer, String, DateTime, BigInteger, Text +from sqlalchemy import Column, Integer, String, DateTime, BigInteger, Text, Index from app.database import Base @@ -9,7 +9,7 @@ class Video(Base): id = Column(Integer, primary_key=True, autoincrement=True) task_id = Column(String(64), unique=True, index=True, nullable=False) - url = Column(String(512), nullable=False) + url = Column(String(512), nullable=False, index=True) title = Column(String(512), default="") platform = Column(String(32), default="twitter") thumbnail = Column(String(1024), default="") @@ -24,3 +24,7 @@ class Video(Base): progress = Column(Integer, default=0) # 0-100 created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + __table_args__ = ( + Index("ix_video_url_format_id", "url", "format_id"), + ) diff --git a/backend/app/routes/download.py b/backend/app/routes/download.py index 45200d9..dcb8913 100644 --- a/backend/app/routes/download.py +++ b/backend/app/routes/download.py @@ -50,6 +50,19 @@ async def _do_download(task_id: str, url: str, format_id: str): @router.post("/download", response_model=DownloadResponse) async def start_download(req: DownloadRequest, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db)): + # Dedup: reuse existing completed download if file still on disk + existing = (await db.execute( + select(Video).where( + Video.url == req.url, + Video.format_id == req.format_id, + Video.status == "done", + ).order_by(Video.created_at.desc()).limit(1) + )).scalar_one_or_none() + + if existing and os.path.exists(existing.file_path): + logger.info(f"Reusing existing download task_id={existing.task_id} for url={req.url} format={req.format_id}") + return DownloadResponse(task_id=existing.task_id, status="done") + task_id = str(uuid.uuid4())[:8] video = Video(task_id=task_id, url=req.url, quality=req.quality, format_id=req.format_id, status="pending") db.add(video) diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue index a68ee6b..f773d76 100644 --- a/frontend/src/views/Home.vue +++ b/frontend/src/views/Home.vue @@ -94,8 +94,17 @@ async function startDownload() { url: url.value, format_id: selectedFormat.value, quality: selectedFormat.value }) taskId.value = res.data.task_id - statusText.value = 'Starting download...' - pollStatus() + if (res.data.status === 'done') { + // Already downloaded — skip polling, show save button immediately + progress.value = 100 + statusText.value = '✅ Already downloaded' + downloadReady.value = true + downloadUrl.value = `/api/file/task/${res.data.task_id}` + downloading.value = false + } else { + statusText.value = 'Starting download...' + pollStatus() + } } catch (e) { error.value = e.response?.data?.detail || 'Failed to start download' downloading.value = false