@@ -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+
25292546pub 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