Skip to content

Commit 5f1efa3

Browse files
feat: bypass yt-dlp rate limit using gh CLI for version lookup
yt-dlp -U hits GitHub's unauthenticated API (60 req/hr limit), causing frequent 403 failures. New strategy: 1. If gh CLI is available, use `gh api` (authenticated, 5000 req/hr) to get the latest release tag 2. Pass the tag to `yt-dlp --update-to stable@TAG` which downloads directly from GitHub releases (no API rate limit on downloads) 3. Handle permission errors with automatic sudo retry 4. Falls back to `yt-dlp -U` if gh CLI is not installed Tested: full topgrade run, 27/27 steps OK, yt-dlp updated successfully. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 08a3d80 commit 5f1efa3

1 file changed

Lines changed: 84 additions & 23 deletions

File tree

src/steps/generic.rs

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,16 +2526,84 @@ pub fn run_falconf(ctx: &ExecutionContext) -> Result<()> {
25262526
ctx.execute(falconf).arg("sync").status_checked()
25272527
}
25282528

2529+
/// Try to get the latest yt-dlp version tag using `gh api` (authenticated, 5000 req/hr).
2530+
/// Returns None if `gh` is not installed or the API call fails.
2531+
fn ytdlp_latest_version_via_gh() -> Option<String> {
2532+
let gh = which_crate::which("gh").ok()?;
2533+
let output = std::process::Command::new(gh)
2534+
.args(["api", "repos/yt-dlp/yt-dlp/releases/latest", "--jq", ".tag_name"])
2535+
.output()
2536+
.ok()?;
2537+
if output.status.success() {
2538+
let tag = String::from_utf8_lossy(&output.stdout).trim().to_string();
2539+
if !tag.is_empty() {
2540+
return Some(tag);
2541+
}
2542+
}
2543+
None
2544+
}
2545+
25292546
pub fn run_ytdlp(ctx: &ExecutionContext) -> Result<()> {
25302547
let ytdlp = require("yt-dlp")?;
25312548

2532-
// Check if yt-dlp was installed via a package manager by inspecting the
2533-
// output of `yt-dlp -U`. If it mentions pip, brew, or another package
2534-
// manager, skip since the package manager handles updates.
2535-
let output = ctx.execute(&ytdlp).always().args(["-U"]).output()?;
2549+
// First, get the current version to check if it's managed by a package manager.
2550+
let version_output = ctx.execute(&ytdlp).always().args(["--version"]).output()?;
2551+
let current_version = match &version_output {
2552+
ExecutorOutput::Wet(output) => String::from_utf8_lossy(&output.stdout).trim().to_string(),
2553+
ExecutorOutput::Dry => return Ok(()),
2554+
};
2555+
2556+
print_separator("yt-dlp");
25362557

2558+
// Strategy: use `gh api` (authenticated) to get the latest version, then
2559+
// pass it to `yt-dlp --update-to` which downloads directly from GitHub
2560+
// releases without hitting the rate-limited API.
2561+
if let Some(latest_tag) = ytdlp_latest_version_via_gh() {
2562+
if current_version == latest_tag {
2563+
println!("yt-dlp is up to date ({current_version})");
2564+
return Ok(());
2565+
}
2566+
2567+
println!("Updating yt-dlp {current_version} -> {latest_tag}");
2568+
let update_target = format!("stable@{latest_tag}");
2569+
2570+
// Try without sudo first
2571+
let output = ctx
2572+
.execute(&ytdlp)
2573+
.always()
2574+
.args(["--update-to", &update_target])
2575+
.output()?;
2576+
let output = match output {
2577+
ExecutorOutput::Wet(o) => o,
2578+
ExecutorOutput::Dry => return Ok(()),
2579+
};
2580+
2581+
if output.status.success() {
2582+
std::io::stdout().lock().write_all(&output.stdout).unwrap();
2583+
return Ok(());
2584+
}
2585+
2586+
// If permission error, retry with sudo
2587+
let stderr = String::from_utf8_lossy(&output.stderr).to_lowercase();
2588+
if stderr.contains("unable to write") || stderr.contains("permission denied") {
2589+
let sudo = ctx.require_sudo()?;
2590+
return sudo
2591+
.execute(ctx, &ytdlp)?
2592+
.args(["--update-to", &update_target])
2593+
.status_checked();
2594+
}
2595+
2596+
// Other error — report it
2597+
std::io::stdout().lock().write_all(&output.stdout).unwrap();
2598+
std::io::stderr().lock().write_all(&output.stderr).unwrap();
2599+
return Err(eyre!("yt-dlp self-update failed"));
2600+
}
2601+
2602+
// Fallback: `gh` CLI not available — use `yt-dlp -U` directly.
2603+
// This may hit GitHub API rate limits for unauthenticated users.
2604+
let output = ctx.execute(&ytdlp).always().args(["-U"]).output()?;
25372605
let output = match output {
2538-
ExecutorOutput::Wet(output) => output,
2606+
ExecutorOutput::Wet(o) => o,
25392607
ExecutorOutput::Dry => return Ok(()),
25402608
};
25412609

@@ -2544,43 +2612,36 @@ pub fn run_ytdlp(ctx: &ExecutionContext) -> Result<()> {
25442612
String::from_utf8_lossy(&output.stdout),
25452613
String::from_utf8_lossy(&output.stderr)
25462614
);
2615+
let combined_lower = combined.to_lowercase();
25472616

2548-
// If managed by a package manager, skip the self-update
2549-
let pkg_manager_keywords = ["pip", "brew", "pacman", "apt", "choco", "scoop", "winget", "nix"];
2550-
if pkg_manager_keywords
2551-
.iter()
2552-
.any(|kw| combined.to_lowercase().contains(kw))
2553-
{
2617+
// If managed by a package manager, skip
2618+
let pkg_keywords = ["pip", "brew", "pacman", "apt", "choco", "scoop", "winget", "nix"];
2619+
if pkg_keywords.iter().any(|kw| combined_lower.contains(kw)) {
25542620
return Err(SkipStep("yt-dlp is managed by a package manager; skipping self-update".to_string()).into());
25552621
}
25562622

2557-
print_separator("yt-dlp");
2558-
2559-
// If the non-sudo attempt succeeded, report it
25602623
if output.status.success() {
25612624
std::io::stdout().lock().write_all(&output.stdout).unwrap();
25622625
std::io::stderr().lock().write_all(&output.stderr).unwrap();
25632626
return Ok(());
25642627
}
25652628

2566-
let combined_lower = combined.to_lowercase();
2567-
2568-
// If it hit a GitHub API rate limit, skip gracefully instead of failing
2629+
// Rate limit — skip gracefully
25692630
if combined_lower.contains("rate limit") {
25702631
std::io::stderr().lock().write_all(&output.stderr).unwrap();
2571-
return Err(
2572-
SkipStep("yt-dlp update skipped: GitHub API rate limit exceeded, try again later".to_string()).into(),
2573-
);
2632+
return Err(SkipStep(
2633+
"yt-dlp update skipped: GitHub API rate limit exceeded (install `gh` CLI for authenticated updates)"
2634+
.to_string(),
2635+
)
2636+
.into());
25742637
}
25752638

2576-
// Check if it failed due to a permission error (e.g., installed to /usr/local/bin)
2639+
// Permission error — retry with sudo
25772640
if combined_lower.contains("unable to write") || combined_lower.contains("permission denied") {
2578-
// Retry with sudo
25792641
let sudo = ctx.require_sudo()?;
25802642
return sudo.execute(ctx, &ytdlp)?.args(["-U"]).status_checked();
25812643
}
25822644

2583-
// Some other error
25842645
std::io::stdout().lock().write_all(&output.stdout).unwrap();
25852646
std::io::stderr().lock().write_all(&output.stderr).unwrap();
25862647
Err(eyre!("yt-dlp self-update failed"))

0 commit comments

Comments
 (0)