- CLI
- 开发工具
【免费下载链接】gdown
Google Drive public file downloader when curl/wget fails.
导读
Google Drive 会对访问量过大的公共文件触发“Too many users have viewed or downloaded this file recently”限流,但对已登录账户却照常放行。gdown 通过一项名为「符号级 Vendor(symbol-level vendoring)」的架构决策,把 yt-dlp 的浏览器 Cookie 提取模块以最小代价引入项目:既没有新增运行时依赖,也让gdown --cookies-from-browser firefox URL开箱即用。本文基于仓库中的架构决策记录 docs/adr/0002-vendor-ytdlp-cookie-extraction.md,结合gdown/_vendor/下的生成代码与手写 shim、CLI 入口和测试用例,讲清这条决策从“为什么”到“怎么用”再到“如何维护”的完整链路。
背景:Drive 限流与“登录账户可下载”的矛盾
Google Drive 对热门的公共文件执行限流:当短时间内访问人数过多时,服务端会拒绝匿名下载。ADR 记录于 2026-09-04 实测验证:一个被限流的 Newer College rosbag 文件,不带 Cookie 时下载失败,而换上已登录浏览器的 Google Cookie 后即可正常下载。
在此之前,gdown 其实已经支持 Cookie,但使用门槛很高:用户必须先用浏览器扩展把 Cookie 导出成 Netscape 格式的cookies.txt,再手动放进缓存目录。yt-dlp 与 gallery-dl 早已确立了更顺手的 CLI 约定——--cookies FILE指定文件、--cookies-from-browser BROWSER直接从浏览器读取——而 gdown 的诉求是uvx gdown必须无需任何额外步骤就能工作。
从技术实现看,读取浏览器 Cookie 库并不轻松:要碰 SQLite 数据库、按平台取密钥(Linux 的 GNOME Keyring / KWallet、macOS 的 Keychain、Windows 的 DPAPI)、再走 AES 解密。浏览器提取是 gdown 中唯一一个纯 CLI 场景的功能,而 gdown 的大部分安装形态是把 gdown 当库使用,因此为它引入新的运行时依赖很难有正当理由。
备选方案评估:为什么最终选择 Vendor
ADR 逐一推演了四条替代路线并给出否决理由:
| 方案 | 评估结论 | 否决原因 |
|---|---|---|
| 依赖 browser-cookie3 | 否决 | LGPL 许可证;自带 3~6 个传递依赖(其中 2 个是 C 扩展);上游自 2024 年 12 月起基本停更。每个库的使用者都要为一个 CLI 功能买单,且 LGPL 代码无法在日后想卸掉依赖时复制进 MIT 项目 |
| 直接依赖 yt-dlp | 否决 | 为了一个函数引入约 3 MB 的依赖,代价不成比例 |
| 从 gallery-dl 复制代码 | 否决 | gallery-dl 是 GPL-2 许可证,不能并入 MIT 项目 |
| 从 yt-dlp Vendor | 采用 | cookies.py与纯 Python 的aes.py以 Unlicense(公有领域)发布,仅需标准库加系统工具,且上游持续维护 |
| 作为可选 extra 发布 | 否决 | extras 只会“加依赖”,无法表达精简的gdown[core],uvx gdown也拿不到该功能 |
另有一个所有方案都无法绕开的硬约束:Google 的 App-Bound Encryption(应用绑定加密)使得没有任何方案能在 Windows 上无管理员权限读取 Chrome 127+ 的 Cookie;而 Firefox 在所有平台上都可以读取。这个约束直接决定了后面的平台策略。
决策:符号级 Vendor 与一键再生成脚本
最终决策是:对 yt-dlp 的 Cookie 模块做符号级提取(symbol-level extraction)。仓库中的 scripts/vendor_ytdlp_cookies.py 是这个流程的执行者,其工作机制清晰可见:
- 锁定一个 yt-dlp 发布 tag(脚本默认值
TAG = "2026.08.19"); - 从 yt-dlp 仓库在线下载
yt_dlp/cookies.py与yt_dlp/aes.py两个模块; - 以
extract_cookies_from_browser和SUPPORTED_BROWSERS为根(ROOTS),用ast遍历其顶层定义引用的闭包,只保留可达的符号; - 把保留的源码连同必要的标准库 import 写进唯一一个生成文件 gdown/_vendor/_ytdlp_cookies.py。
该生成文件以头注释明示来源与纪律:# Generated by scripts/vendor_ytdlp_cookies.py from yt-dlp 2026.08.19.、# Do not edit; rerun the script to upgrade.、# yt-dlp is public domain (Unlicense).,并带有# ruff: noqa、# fmt: off,从工具链上杜绝手工改动。升级流程就是换一个更新的 tag 重跑脚本,例如:
uv run python scripts/vendor_ytdlp_cookies.py 2026.09.01脚本在写入后会做一次自检:比对 shim 中是否缺少提取代码引用的名字,若缺则报错并提示需要扩展 shim(见下文)。
生成文件的头部可以看到完整的浏览器清单与入口函数:
CHROMIUM_BASED_BROWSERS = {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi', 'whale'} SUPPORTED_BROWSERS = CHROMIUM_BASED_BROWSERS | {'firefox', 'safari'} def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None, container=None): if browser_name == 'firefox': return _extract_firefox_cookies(profile, container, logger) elif browser_name == 'safari': return _extract_safari_cookies(profile, logger) elif browser_name in CHROMIUM_BASED_BROWSERS: return _extract_chrome_cookies(browser_name, profile, keyring, logger) else: raise ValueError(f'unknown browser: {browser_name}')手写 shim:把“纯复制”与“手工维护”分开
提取出的 yt-dlp 代码会 import 十几个 yt-dlp 内部符号(它的 logger、进度条打印机、subprocess 包装器,以及若干小的字符串辅助函数)。这些符号放在紧邻生成文件的手写 shimgdown/_vendor/_ytdlp_shim.py 中,实现两个目标:
- 生成文件保持纯复制:它本身绝不掺入手写代码,升级时可整文件替换;
- 手写面保持最小:shim 全文约一百余行,且文件头部明确写着“每个名字的调用形态尽量贴近上游,仅供提取代码使用,gdown 自身不直接使用”。
shim 提供的替代实现包括:_YDLLogger(debug/info/warning/error 四种日志通道)、MultilinePrinter与QuietMultilinePrinter(进度输出)、Popen(基于subprocess.Popen的run类方法,统一 stdout/stderr 与超时语义)、Cryptodome(AES = None,让提取的 AES 代码在没有 pycryptodomex 时走纯 Python 路径)、以及compat_ord、error_to_str、is_path_like、str_or_none、try_call、write_string、normalize_url、sanitize_url等辅助函数。
值得注意的一个设计细节:shim 中有一个模块级探测函数_probe_secretstorage(),它在 import 时尝试加载secretstorage模块,失败时记录原因字符串并置secretstorage = None、_SECRETSTORAGE_UNAVAILABLE_REASON。这使提取的 Chromium 解密代码在 GNOME Keyring 缺失时能给出明确诊断,而不是神秘报错。
CLI 集成:一次复制、之后免 flag
两个命令行参数沿用了 yt-dlp 的命名,定义于 gdown/main.py:
--cookies FILE:读写 Netscape 格式 Cookie 文件,默认~/.cache/gdown/cookies.txt;--cookies-from-browser BROWSER:从指定浏览器复制 Google Cookie 到 Cookie 文件,choices直接取自生成文件的SUPPORTED_BROWSERS(在BROWSERS常量中排序导出);--no-cookies:完全不读不写 Cookie,且与上述两个参数互斥(解析期即报错)。
CLI 的核心流程是:下载开始前只做一次浏览器 Cookie 导入,之后库层始终保持单一的 Cookie 来源(同一个 Cookie 文件),后续运行无需再带任何参数。调用链位于 gdown/download.py 的_import_cookies_from_browser():
- 函数内部延迟导入vendor 模块——只有真的用到浏览器提取时才加载,保证库用户零成本;
- 调用
extract_cookies_from_browser(browser, logger=logger)提取全部 Cookie,然后只保留google.com及.google.com域的条目; - 通过专门的
_CookieExtractionLogger收集 keyring 与解密告警;一旦记录到 error,即使有“看起来解密成功”的 Cookie 也整体放弃(防止空口令回退把坏数据写进文件),抛DownloadError; - 对 Chromium 家族做过期时间换算:Chromium 以 1601 年起算的微秒存储到期时间,需要
expires // 1_000_000 - 11_644_473_600转成 Unix 时间戳,避免把已过期 Cookie 存成“远未来”; - 合并写入 Cookie 文件并返回导入数量。
__main__.py中的处理逻辑(gdown/main.py)随后给出反馈:成功时打印Saved N Google cookies from <browser> to <file>;找不到任何 Google Cookie 时提示“先在浏览器中登录 Google,或改用其它浏览器”并退出码 1;导入失败时打印错误详情,若在 Windows 且浏览器不是 Firefox,还会追加提示:
Chrome 127+ on Windows blocks other programs from reading its cookies. Try --cookies-from-browser firefox.一条完整可用的命令如下(README 中的示例见 README.md):
# 以已登录账号身份下载,直接复用浏览器的 Cookie gdown https://drive.google.com/uc?id=FILE_ID --cookies-from-browser firefox # 用独立文件保存/读取 Cookie,覆盖默认路径 gdown https://drive.google.com/uc?id=FILE_ID --cookies ./cookies.txt # 完全禁用 Cookie gdown https://drive.google.com/uc?id=FILE_ID --no-cookiesCookie 文件的生命周期:会话 Cookie 与权限安全
“Cookie 文件里存着整个 google.com 会话(含 Gmail 与账户信息)”是必须严肃对待的事实。代码层面有两处对应的硬措施(gdown/download.py):
_load_cookies()以ignore_discard=True, ignore_expires=True加载,因为Google 的登录状态有一部分活在会话 Cookie 里,而 Netscape 格式默认会丢弃它们;加载后把expires == 0的会话 Cookie 改为不过期(标准库否则会视为已过期),再统一清理真正过期的条目;_save_cookies()在写入任何字节之前就用mode=0o600创建文件并os.chmod(..., 0o600),确保只有当前用户可读。README 明确要求把这个文件当成密码对待,需要吊销时就删除该文件并在浏览器中退出 Google 登录。
测试用例印证了这两点:tests/test_download.py 中覆盖了「多次导入不重复、合并保留已有 Cookie」「导入后文件权限为 0o600」「keyring 失败时保留原有的好会话、绝不写入坏数据」「Chromium 过期时间换算」「扩展导出 Cookie 的会话保留」等场景。
GNOME Keyring:用 optional extra 补最后一公里
Linux 上 Chromium 系浏览器的 Cookie 密钥存放在系统钥匙串中,yt-dlp 的惯例是通过secretstorage这个 Python 包访问 GNOME Keyring。gdown 遵循同样的 optional-extra 约定(pyproject.toml):
[project.optional-dependencies] secretstorage = ["secretstorage"]主依赖列表保持原样(beautifulsoup4、filelock、requests[socks]、tqdm等,见 pyproject.toml),没有为 CLI 功能新增任何运行时依赖。安装方式:
pip install 'gdown[secretstorage]' # 或在不污染环境的情况下临时使用 uvx --from 'gdown[secretstorage]' gdown --cookies-from-browser chrome URLFirefox 在 Linux 上不需要任何额外依赖(其 Cookie 数据库本身未加密),这也是建议优先使用 Firefox 的原因之一。若secretstorage缺失,shim 中的探测会生成类似“as thesecretstoragemodule is not installed. Install withpip install 'gdown[secretstorage]'”的提示,直接告诉用户该装什么。
平台限制速查
| 平台 | 可用性 |
|---|---|
| 所有平台 | Firefox 均可读取,无需额外依赖 |
| macOS | Chromium 系首次会请求一次登录密码以解锁 Keychain;Safari 需要为终端授予“完全磁盘访问权限”(Full Disk Access) |
| Linux | Chromium 系若使用 GNOME Keyring 需gdown[secretstorage];KWallet 走kwallet-query(经 dbus) |
| Windows | Chrome 127+ 因 App-Bound Encryption 无法被其他程序读取,官方提示改用 Firefox |
后果与维护契约
这条 ADR 决策写下了一组可以验证的后果:
uvx gdown --cookies-from-browser firefox URL无额外依赖、无额外配置即可工作;- 约 1600 行生成代码常驻仓库(当前生成文件实际约 1650 行),被
ruff: noqa/fmt: off排除在 lint 与类型检查之外,且只在重跑脚本时变化; - 上游一旦重命名内部符号,两处会同时“大声失败”:一是脚本结束时的 shim 缺失名报告,二是生成模块的 import 错误——这正是扩展 shim 的信号;
- 支持浏览器清单与锁定的 yt-dlp 发布版保持一致(升级 tag 即随上游更新);
- 浏览器清单由
SUPPORTED_BROWSERS一处定义,CLI 的choices与测试中的断言(如 tests/test___main__.py 的test_browser_choices_match_vendored_extractor)都指向同一个来源,防止两者漂移; - Windows Chrome 用户得到的是指向 Firefox 的明确错误提示,而不是神秘的解密失败。
整体来看,这一设计把“功能完整”与“依赖克制”同时做到了:提取代码是上游公有领域源码的机械副本,可审计、可整体升级;手写 shim 是唯一需要人工维护的薄接口;CLI 与库层通过 Cookie 文件解耦,库用户与uvx用户各取所需。如果你想深入了解实现细节,可以从 scripts/vendor_ytdlp_cookies.py(生成器)、gdown/_vendor/_ytdlp_shim.py(shim)、gdown/download.py(导入与存取逻辑)和 tests/test_download.py(行为契约)四条线索继续深入。
- CLI
- 开发工具
【免费下载链接】gdown
Google Drive public file downloader when curl/wget fails.
相关推荐
深入解析js-cookie:轻量级浏览器Cookie处理库
深入解析js cookie:轻量级浏览器Cookie处理库 js cookie是一个轻量级、功能强大的JavaScript Cookie处理库,以其简洁的API
前端Clank高级技巧:创建复杂移动应用导航和页面转场效果
Clank高级技巧:创建复杂移动应用导航和页面转场效果 Clank是一款强大的开源移动应用原型开发框架,能够帮助开发者快速构建具有专业级用户体验的移动应用界面。
前端UI库/组件Super Productivity Cookie:浏览器Cookie管理
Super Productivity Cookie:浏览器Cookie管理 痛点:Jira集成认证的复杂性 你是否曾经遇到过这样的困境:在使用Super Pro
前端桌面应用移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考