diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rwxr-xr-x | bunkr_dl | 27 |
2 files changed, 29 insertions, 0 deletions
@@ -8,6 +8,8 @@ search, from the command line. - Resumes partially downloaded files when run again against the same folder. - Shows a progress bar for the album (files) and for the current file (size, speed, ETA). +- Prints a summary at the end, also on Ctrl+C: files downloaded, skipped, + failed and partial, bytes written, time and average speed. - Works with any Bunkr mirror domain, since links are resolved relative to the URL you pass. @@ -20,6 +20,7 @@ import requests from bs4 import BeautifulSoup from rich.console import Console, Group from rich.live import Live +from rich import filesize from rich.progress import (Progress, TextColumn, BarColumn, MofNCompleteColumn, TimeElapsedColumn, TaskProgressColumn, DownloadColumn, TransferSpeedColumn, TimeRemainingColumn) @@ -28,6 +29,7 @@ import os import sys import argparse import time +from datetime import timedelta from urllib.parse import urljoin, urlparse, quote DEFAULT_OUTPUT = "/data/bunkr" # Base download folder, override with -o/--output @@ -45,6 +47,8 @@ album_bars = Progress(TextColumn("{task.description:30}"), BarColumn(complete_st file_bars = Progress(TextColumn("{task.description:30}"), BarColumn(complete_style="cyan"), TaskProgressColumn(), DownloadColumn(binary_units=True), TransferSpeedColumn(), TimeRemainingColumn(), console=console) +# Run totals for the summary; bytes counts only what this run wrote +stats = dict(downloaded=0, skipped=0, failed=0, partial=0, bytes=0) def get_soup(url): """Helper function to get parsed HTML""" @@ -165,6 +169,7 @@ def download_file(file_url, file_name, save_dir, retries=5): with requests.get(file_url, headers=req_headers, stream=True, timeout=60) as response: if response.status_code == 416: # Range past end: already complete console.out(f"Already downloaded: {local_filename}") + stats['skipped'] += 1 return local_filename response.raise_for_status() # Check if it's a resumable download @@ -183,9 +188,14 @@ def download_file(file_url, file_name, save_dir, retries=5): if chunk: # Filter out keep-alive chunks f.write(chunk) file_bars.advance(task, len(chunk)) + stats['bytes'] += len(chunk) + except KeyboardInterrupt: + stats['partial'] += 1 # Kept on disk, a rerun resumes it + raise finally: file_bars.remove_task(task) console.out(f"Download complete: {local_filename}") + stats['downloaded'] += 1 return local_filename except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, requests.exceptions.Timeout) as e: attempt += 1 @@ -196,6 +206,7 @@ def download_file(file_url, file_name, save_dir, retries=5): break console.out(f"Failed to download {file_url} after {retries} attempts.") + stats['failed'] += 1 return None def process_album(album_url, base_save_directory): @@ -239,8 +250,10 @@ def process_album(album_url, base_save_directory): download_file(*final_media, album_save_directory) else: console.out(f"Failed to get final media from {download_link}") + stats['failed'] += 1 else: console.out(f"Failed to find download link on {media_link}") + stats['failed'] += 1 album_bars.advance(task) def main(): @@ -295,11 +308,25 @@ def main(): console.out(f"Processing album {url}") process_album(url, save_directory) +def print_summary(status, elapsed): + speed = filesize.decimal(int(stats['bytes'] / elapsed)) if elapsed else "0 bytes" + console.out(f"{status}: {stats['downloaded']} downloaded, {stats['skipped']} skipped, " + f"{stats['failed']} failed, {stats['partial']} partial, " + f"{filesize.decimal(stats['bytes'])} in {timedelta(seconds=int(elapsed))} " + f"(avg {speed}/s)") + if __name__ == "__main__": + start = time.monotonic() + status = "Done" try: with Live(Group(album_bars, file_bars), console=console): main() except KeyboardInterrupt: # Partial file stays on disk; rerunning the same command resumes it console.out("\nInterrupted. Rerun the same command to resume.") + status = "Stopped" sys.exit(130) + finally: + # Skip it for --help, bad arguments and runs that found nothing + if any(stats.values()): + print_summary(status, time.monotonic() - start) |
