Enhance PVideoDl with finished_at tracking for downloads and UI updates. Added finished_at field to Download model, updated storage to handle legacy databases, and improved frontend to display recent downloads. Refactored scripts for better clarity and error handling.
This commit is contained in:
+24
-3
@@ -8,7 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -17,6 +17,10 @@ import aiosqlite
|
||||
from app.models import Download, DownloadStatus
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class Storage(ABC):
|
||||
@abstractmethod
|
||||
async def init(self) -> None: ...
|
||||
@@ -52,6 +56,7 @@ _COLUMNS = (
|
||||
"eta",
|
||||
"error",
|
||||
"created_at",
|
||||
"finished_at",
|
||||
)
|
||||
|
||||
|
||||
@@ -59,6 +64,8 @@ def _row_to_download(row: aiosqlite.Row) -> Download:
|
||||
data = dict(row)
|
||||
data["status"] = DownloadStatus(data["status"])
|
||||
data["created_at"] = datetime.fromisoformat(data["created_at"])
|
||||
if data.get("finished_at"):
|
||||
data["finished_at"] = datetime.fromisoformat(data["finished_at"])
|
||||
return Download.model_validate(data)
|
||||
|
||||
|
||||
@@ -98,22 +105,36 @@ class SqliteStorage(Storage):
|
||||
speed REAL,
|
||||
eta REAL,
|
||||
error TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
created_at TEXT NOT NULL,
|
||||
finished_at TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
# Миграция старых БД, созданных до появления finished_at.
|
||||
await self._migrate_add_column("finished_at", "TEXT")
|
||||
# На старте всё, что осталось "качающимся" после прошлого запуска,
|
||||
# помечаем упавшим — воркеры этого процесса о них не знают.
|
||||
await self._db.execute(
|
||||
"UPDATE downloads SET status = ?, error = ? WHERE status = ?",
|
||||
"UPDATE downloads SET status = ?, error = ?, finished_at = ? "
|
||||
"WHERE status = ?",
|
||||
(
|
||||
DownloadStatus.FAILED.value,
|
||||
"Прервано при перезапуске приложения",
|
||||
_now().isoformat(),
|
||||
DownloadStatus.DOWNLOADING.value,
|
||||
),
|
||||
)
|
||||
await self._db.commit()
|
||||
|
||||
async def _migrate_add_column(self, name: str, decl: str) -> None:
|
||||
"""Добавить колонку, если её ещё нет (idempotent-миграция старых БД)."""
|
||||
async with self._conn.execute("PRAGMA table_info(downloads)") as cursor:
|
||||
cols = {row["name"] for row in await cursor.fetchall()}
|
||||
if name not in cols:
|
||||
await self._conn.execute(
|
||||
f"ALTER TABLE downloads ADD COLUMN {name} {decl}"
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
if self._db is not None:
|
||||
await self._db.close()
|
||||
|
||||
@@ -37,6 +37,7 @@ class Download(BaseModel):
|
||||
eta: float | None = None # секунд до конца, оценка
|
||||
error: str | None = None
|
||||
created_at: datetime = Field(default_factory=_now)
|
||||
finished_at: datetime | None = None # момент перехода в done/failed
|
||||
|
||||
|
||||
class CreateDownloads(BaseModel):
|
||||
|
||||
@@ -13,7 +13,7 @@ import logging
|
||||
from app.core.events import EventBus
|
||||
from app.core.queue import DownloadQueue
|
||||
from app.core.storage import Storage
|
||||
from app.models import Download, DownloadEvent, DownloadStatus
|
||||
from app.models import Download, DownloadEvent, DownloadStatus, _now
|
||||
from app.services.downloader import Progress, pick_downloader
|
||||
|
||||
logger = logging.getLogger("pvideodl.worker")
|
||||
@@ -89,7 +89,12 @@ class WorkerPool:
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("Ошибка скачивания %s: %s", download.url, exc)
|
||||
failed = await self._storage.update(
|
||||
download_id, status=DownloadStatus.FAILED, error=str(exc), speed=None, eta=None
|
||||
download_id,
|
||||
status=DownloadStatus.FAILED,
|
||||
error=str(exc),
|
||||
speed=None,
|
||||
eta=None,
|
||||
finished_at=_now(),
|
||||
)
|
||||
await self._publish("failed", failed)
|
||||
return
|
||||
@@ -104,6 +109,7 @@ class WorkerPool:
|
||||
speed=None,
|
||||
eta=0,
|
||||
error=None,
|
||||
finished_at=_now(),
|
||||
)
|
||||
await self._publish("done", done)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user