Add YouTube POT token provider (bgutil-pot, opt-in)
YouTube increasingly requires a per-video Proof-of-Origin token bound to each video ID before handing back format URLs. Without one, yt-dlp sees empty formats and downloads fail. The upstream solution is the Brainicism bgutil-ytdlp-pot-provider Python plugin paired with a long-running server that mints tokens via BotGuard. We integrate the Rust port (jim60105/bgutil-ytdlp-pot-provider-rs) so the server is a single static binary, no Node.js required. ## Architecture - bgutil-pot binary lives alongside the bundled deno + yt-dlp at ~/.local/share/yt-offline/bin/bgutil-pot. - The matching Python plugin is pip-installed into the bundled venv: python -m pip install bgutil-ytdlp-pot-provider. - Downloader spawns the server lazily on first job (port 4416, loopback only) and kills it on Drop. - Each yt-dlp invocation gets --extractor-args "youtubepot-bgutilhttp:base_url=http://127.0.0.1:4416" appended in spawn_job when use_pot_provider is on and the server child is alive. ## UX - Settings → "POT token provider" row with toggle + Install/Update button (mirrors the bundled yt-dlp row). - Disabled unless use_bundled_ytdlp is also on (plugin lives in that venv). - Refuses install if the bundled yt-dlp venv isn't there yet, with a helpful 428 error. ## Lifecycle - Lazy spawn: ensure_pot_server() runs at the top of start() so the server is up before yt-dlp queries the plugin. - Drop: kill_server() on Downloader drop so we don't leave an orphaned process bound to 4416. - start_pot_provider_update() kills the running child first so the install can overwrite the binary in place (no ETXTBSY). 3 new unit tests cover URL/extractor-args formatting; 77 total pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
4f15ab873f
commit
2696a51cc8
7 changed files with 439 additions and 4 deletions
41
src/app.rs
41
src/app.rs
|
|
@ -283,6 +283,7 @@ impl App {
|
|||
|
||||
let max_concurrent = config.backup.max_concurrent;
|
||||
let use_bundled_ytdlp = config.backup.use_bundled_ytdlp;
|
||||
let use_pot_provider = config.backup.use_pot_provider;
|
||||
let browser = config.player.browser.clone();
|
||||
let config_bind = config.web.bind.clone();
|
||||
let password_set = db.get_setting("password_hash").ok().flatten().is_some();
|
||||
|
|
@ -351,7 +352,7 @@ impl App {
|
|||
sidebar_view: SidebarView::All,
|
||||
selected_video: None,
|
||||
search: String::new(),
|
||||
downloader: Downloader::new(channels_root, browser, max_concurrent, use_bundled_ytdlp),
|
||||
downloader: Downloader::new(channels_root, browser, max_concurrent, use_bundled_ytdlp, use_pot_provider),
|
||||
show_downloads: false,
|
||||
current_screen: Screen::Library,
|
||||
dl_url: String::new(),
|
||||
|
|
@ -2237,6 +2238,43 @@ impl App {
|
|||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("POT token provider:");
|
||||
ui.horizontal(|ui| {
|
||||
// Disabled when the bundled yt-dlp isn't in use — the
|
||||
// Python plugin lives in that venv. System-yt-dlp users
|
||||
// who want POT install the plugin themselves.
|
||||
let pot_available = self.config.backup.use_bundled_ytdlp;
|
||||
ui.add_enabled_ui(pot_available, |ui| {
|
||||
ui.checkbox(&mut self.config.backup.use_pot_provider, "enable")
|
||||
.on_hover_text(
|
||||
"Spawn the bgutil-pot HTTP server and pass its \
|
||||
extractor-args to yt-dlp. YouTube increasingly \
|
||||
requires a Proof-of-Origin token for each video — \
|
||||
without one, format URLs come back empty.\n\n\
|
||||
Requires the bundled yt-dlp (Python plugin lives \
|
||||
in that venv).",
|
||||
);
|
||||
});
|
||||
let pot_installed = crate::pot_provider::installed();
|
||||
let pot_btn = if pot_installed { "Update" } else { "Install" };
|
||||
if ui.add_enabled(pot_available, egui::Button::new(pot_btn))
|
||||
.on_hover_text(
|
||||
"Download (or update) the bgutil-pot binary from GitHub \
|
||||
and pip-install the matching Python plugin into the \
|
||||
bundled venv. Streams output as a job.",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.downloader.start_pot_provider_update();
|
||||
}
|
||||
if pot_installed {
|
||||
ui.label(egui::RichText::new("✓ installed").weak().small());
|
||||
} else {
|
||||
ui.label(egui::RichText::new("not installed").weak().small());
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Web UI port:");
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.config.web.port)
|
||||
|
|
@ -2552,6 +2590,7 @@ impl App {
|
|||
}
|
||||
self.downloader.max_concurrent = self.config.backup.max_concurrent;
|
||||
self.downloader.use_bundled_ytdlp = self.config.backup.use_bundled_ytdlp;
|
||||
self.downloader.use_pot_provider = self.config.backup.use_pot_provider;
|
||||
if dir_changed {
|
||||
self.channels_root = new_dir.clone();
|
||||
self.library_root = new_dir
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue