diff --git a/src/yt_scraper/store.py b/src/yt_scraper/store.py index c801b99..4c28bf0 100644 --- a/src/yt_scraper/store.py +++ b/src/yt_scraper/store.py @@ -58,6 +58,12 @@ _VIDEO_COLUMNS: dict[str, str] = { # 1 cuando upload_date viene del discovery aproximado (texto relativo de # YouTube: "hace 3 semanas"), 0/NULL cuando es exacto (extraccion). "upload_date_approx": "INTEGER DEFAULT 0", + "video_download_status": "TEXT DEFAULT 'not_downloaded'", + "video_path": "TEXT", + "video_filename": "TEXT", + "video_size": "INTEGER", + "video_downloaded_at": "TEXT", + "video_error": "TEXT", } _CHANNEL_COLUMNS: dict[str, str] = { @@ -247,6 +253,18 @@ def _order_clause(sort: str | None) -> str: "likes_desc": by_likes, "title": f"videos.title COLLATE NOCASE ASC, {_NEWEST_FIRST}", "title_asc": f"videos.title COLLATE NOCASE ASC, {_NEWEST_FIRST}", + # Por nombre de canal (no por UUID): el subquery consulta channels, + # que tiene decenas de filas y PK sobre channel_id. + "channel": ( + "(SELECT c.name FROM channels c WHERE c.channel_id = videos.channel_id) " + "COLLATE NOCASE ASC, videos.channel_id, videos.channel_seq DESC, videos.video_id DESC" + ), + # Agrupa por ciclo de vida: hecho, luego pendientes, luego los que + # necesitan atencion (sin subs), errores al final. + "status": ( + "CASE videos.status WHEN 'done' THEN 0 WHEN 'pending' THEN 1 " + "WHEN 'no_subtitles' THEN 2 ELSE 3 END ASC, " + _NEWEST_FIRST + ), }.get((sort or "").strip(), _NEWEST_FIRST) @@ -296,6 +314,12 @@ class VideoRow: channel_seq: int | None = None # 1 cuando upload_date es aproximado (discovery), 0/NULL si es exacto. upload_date_approx: int | None = None + video_download_status: str | None = None + video_path: str | None = None + video_filename: str | None = None + video_size: int | None = None + video_downloaded_at: str | None = None + video_error: str | None = None # Date the row was ordered by. Equals `upload_date` when it is known; for a # video discovery has not extracted yet it is inferred from `channel_seq` # (see `query_videos`). Only populated by queries that compute it. @@ -764,6 +788,31 @@ class Store: (status, reason[:2000] if reason else None, now, video_id), ) + def update_video_download( + self, + video_id: str, + status: str, + *, + path: str | None = None, + filename: str | None = None, + size: int | None = None, + error: str | None = None, + ) -> None: + """Persist media-download state independently from transcript state.""" + now = _now_iso() if status == "done" else None + with self._cursor() as cur: + cur.execute( + """UPDATE videos SET + video_download_status = ?, + video_path = COALESCE(?, video_path), + video_filename = COALESCE(?, video_filename), + video_size = COALESCE(?, video_size), + video_downloaded_at = COALESCE(?, video_downloaded_at), + video_error = ? + WHERE video_id = ?""", + (status, path, filename, size, now, error[:2000] if error else None, video_id), + ) + def set_availability(self, video_id: str, availability: str | None) -> None: """Record what a full extraction learned; more authoritative than the flat listing, which omits the field for most entries.""" @@ -1194,6 +1243,12 @@ def _row_to_videorow(row: sqlite3.Row) -> VideoRow: availability=row["availability"] if "availability" in keys else None, channel_seq=row["channel_seq"] if "channel_seq" in keys else None, upload_date_approx=row["upload_date_approx"] if "upload_date_approx" in keys else None, + video_download_status=row["video_download_status"] if "video_download_status" in keys else None, + video_path=row["video_path"] if "video_path" in keys else None, + video_filename=row["video_filename"] if "video_filename" in keys else None, + video_size=row["video_size"] if "video_size" in keys else None, + video_downloaded_at=row["video_downloaded_at"] if "video_downloaded_at" in keys else None, + video_error=row["video_error"] if "video_error" in keys else None, sort_date=row["sort_date"] if "sort_date" in keys else None, ) diff --git a/src/yt_scraper/webapp/static/index.html b/src/yt_scraper/webapp/static/index.html index ef7f51c..c4d1203 100644 --- a/src/yt_scraper/webapp/static/index.html +++ b/src/yt_scraper/webapp/static/index.html @@ -313,6 +313,8 @@ + + DurationViewsStatusActions @@ -402,6 +404,11 @@ + + Download MKV @@ -418,6 +425,40 @@ + +
+
+ +
+
Este navegador no pudo reproducir el MKV directamente.
+
Puedes descargar el archivo y abrirlo con VLC u otro reproductor local.
+ Descargar MKV +
+
+
+
+
+
1080p · MKV ·
+
+ +
+
+ +
+
+ Downloading video… + +
+
+
+ + / + · +
+
+
diff --git a/src/yt_scraper/webapp/static/styles.css b/src/yt_scraper/webapp/static/styles.css index e5d4ee7..8be30ea 100644 --- a/src/yt_scraper/webapp/static/styles.css +++ b/src/yt_scraper/webapp/static/styles.css @@ -69,6 +69,10 @@ body::before { .nav-item .nav-ico { width: 1.1rem; height: 1.1rem; opacity: 0.9; } /* ---- form controls ---- */ +/* El popup nativo de