1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#!/usr/bin/env python3
"""Re-download the pdfs/ files that are actually Drive interstitial pages.

A failed download is detected by content, not size: a real PDF starts with
the magic bytes ``%PDF``. Anything else (e.g. the "Google Drive can't scan
this file for viruses" interstitial HTML) is re-downloaded using the full
confirm+uuid flow with cookie persistence.

Only replaces the local file when the new bytes look like a real PDF.
Prints FAIL lines for files that could not be fetched (these go into
manual_download.html).

Usage:
    ./retry_download.py [--dry-run]
"""
import csv
import re
import sys
import urllib.request
from http.cookiejar import CookieJar
from pathlib import Path

HERE = Path(__file__).resolve().parent
PDFS = HERE / "pdfs"

UA = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"}


def drive_id(link):
    m = re.search(r"/file/d/([^/?]+)", link)
    return m.group(1) if m else None


def fetch(url, opener):
    req = urllib.request.Request(url, headers=UA)
    with opener.open(req, timeout=120) as r:
        return r.read()


def download_drive_file(file_id, dest):
    """Return True and write dest only if we got PDF bytes."""
    opener = urllib.request.build_opener(
        urllib.request.HTTPCookieProcessor(CookieJar())
    )
    page = fetch(
        f"https://drive.google.com/uc?export=download&id={file_id}", opener
    )
    if page.startswith(b"%PDF"):
        data = page
    else:
        text = page.decode("utf-8", "replace")
        # Modern interstitial: <form action="https://drive.usercontent...">
        # with hidden inputs id/export/confirm/uuid.
        action = re.search(
            r'<form[^>]*action="([^"]+)"', text
        )
        fields = dict(
            re.findall(r'name="(\w+)" value="([^"]*)"', text)
        )
        if not action or "uuid" not in fields:
            return False, "no uuid/confirm token in interstitial"
        query = "&".join(f"{k}={v}" for k, v in fields.items())
        data = fetch(f"{action.group(1)}?{query}", opener)
    if not data.startswith(b"%PDF"):
        return False, f"got {len(data)} non-PDF bytes"
    if len(data) < 50_000:
        return False, f"suspiciously small PDF ({len(data)} bytes)"
    dest.write_bytes(data)
    return True, f"{len(data) / 1e6:.1f} MB"


def main():
    dry = "--dry-run" in sys.argv
    with (HERE / "download.txt").open(encoding="utf-8-sig") as f:
        rows = list(csv.DictReader(f, delimiter="\t"))

    # Mirror download_all.sh naming for duplicate titles.
    seen: dict = {}
    targets = []
    for row in rows:
        name = (row.get("Book Name") or "").strip()
        if not name:
            continue
        n = seen.get(name, 0)
        seen[name] = n + 1
        target = name
        if n:
            p = Path(name)
            target = f"{p.stem} ({n + 1}){p.suffix}" if p.suffix else f"{name} ({n + 1})"
        targets.append((target, row.get("Link") or ""))

    failed = []
    for target, link in targets:
        path = PDFS / target
        if path.is_file() and path.read_bytes()[:4] == b"%PDF":
            continue  # genuine PDF, leave it alone
        fid = drive_id(link)
        if not fid:
            failed.append((target, link, "no file id"))
            print(f"FAIL {target}: no file id")
            continue
        if dry:
            size_info = f"{path.stat().st_size} bytes" if path.is_file() else "missing"
            print(f"WOULD-RETRY {target} ({size_info})")
            continue
        print(f"DOWNLOAD {target} ...", flush=True)
        try:
            ok, msg = download_drive_file(fid, path)
        except Exception as e:  # noqa: BLE001 - report, don't stop the batch
            ok, msg = False, f"{type(e).__name__}: {e}"
        print(f"  {'OK' if ok else 'FAIL'}: {msg}")
        if not ok:
            failed.append((target, link, msg))

    if failed and not dry:
        lines = [
            "<!DOCTYPE html><html><head><meta charset='utf-8'>"
            "<title>Manual downloads</title></head><body>",
            "<h1>Manual downloads</h1>",
            "<p>These files could not be downloaded automatically. "
            "Open each link and download it by hand into <code>pdfs/</code>.</p>",
            "<ul>",
        ]
        for target, link, msg in failed:
            import html as _h

            lines.append(
                f"<li><a href='{_h.escape(link)}'>{_h.escape(target)}</a>"
                f" — auto-download failed: {_h.escape(msg)}</li>"
            )
        lines += ["</ul></body></html>"]
        (HERE / "manual_download.html").write_text(
            "\n".join(lines), encoding="utf-8"
        )
        print(f"Wrote manual_download.html with {len(failed)} links.")
    elif not dry:
        (HERE / "manual_download.html").unlink(missing_ok=True)
        print("All suspect files re-downloaded successfully.")
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main())