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
|
|
@ -1301,6 +1301,16 @@ async function openSettings(){
|
|||
<span style="font-size:11px;color:var(--muted)">${cur.bundled_ytdlp_installed?'✓ installed':'not installed'}</span>
|
||||
<span id="ytdlp-status" style="font-size:11px;color:var(--muted)"></span>
|
||||
</div>
|
||||
<div class="settings-row" style="margin-top:8px">
|
||||
<label for="cf-pot-provider">Use POT token provider</label>
|
||||
<input type="checkbox" id="cf-pot-provider" ${cur.use_pot_provider?'checked':''} ${cur.use_bundled_ytdlp?'':'disabled'}>
|
||||
</div>
|
||||
<div class="settings-hint" style="margin-bottom:6px">Spawns the bgutil-pot HTTP server (loopback only) and points yt-dlp at it. YouTube increasingly requires a Proof-of-Origin token bound to each video ID — without one, format URLs come back empty. Requires the bundled yt-dlp; the matching Python plugin is pip-installed into the bundled venv on Install.</div>
|
||||
<div style="display:flex;gap:6px;align-items:center;margin-bottom:4px">
|
||||
<button onclick="updatePotProvider(this)" ${cur.use_bundled_ytdlp?'':'disabled'}>${cur.pot_provider_installed?'⟳ Update POT':'⤓ Install POT'}</button>
|
||||
<span style="font-size:11px;color:var(--muted)">${cur.pot_provider_installed?'✓ installed':'not installed'}</span>
|
||||
<span id="pot-status" style="font-size:11px;color:var(--muted)"></span>
|
||||
</div>
|
||||
<hr style="border-color:var(--border);margin:12px 0">
|
||||
<div style="font-weight:700;margin-bottom:8px">Plex</div>
|
||||
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:6px">
|
||||
|
|
@ -1429,6 +1439,18 @@ async function updateYtdlp(btn){
|
|||
}catch(e){st.textContent='Error: '+e.message}
|
||||
finally{btn.disabled=false}
|
||||
}
|
||||
async function updatePotProvider(btn){
|
||||
btn.disabled=true;
|
||||
const st=document.getElementById('pot-status');
|
||||
st.textContent='Started — click ⬇ in the header to view progress';
|
||||
try{
|
||||
const r=await fetch('/api/pot/update',{method:'POST'});
|
||||
const t=await r.text();
|
||||
if(!r.ok)throw new Error(t);
|
||||
setStatus('POT provider install job started');
|
||||
}catch(e){st.textContent='Error: '+e.message}
|
||||
finally{btn.disabled=false}
|
||||
}
|
||||
async function runScheduler(btn){
|
||||
btn.disabled=true;
|
||||
const st=document.getElementById('sched-status');
|
||||
|
|
@ -1452,8 +1474,9 @@ async function saveSettings(btn){
|
|||
const schedInterval=parseInt(document.getElementById('cf-sched-interval')?.value||'24',10);
|
||||
const maxConcurrent=parseInt(document.getElementById('cf-max-concurrent')?.value||'3',10);
|
||||
const useBundledYtdlp=document.getElementById('cf-ytdlp-bundled')?.checked||false;
|
||||
const usePotProvider=document.getElementById('cf-pot-provider')?.checked||false;
|
||||
const sourceUrl=document.getElementById('cf-source-url')?.value;
|
||||
const payload={transcode,scheduler_enabled:schedEnabled,scheduler_interval_hours:schedInterval,max_concurrent:maxConcurrent,use_bundled_ytdlp:useBundledYtdlp};
|
||||
const payload={transcode,scheduler_enabled:schedEnabled,scheduler_interval_hours:schedInterval,max_concurrent:maxConcurrent,use_bundled_ytdlp:useBundledYtdlp,use_pot_provider:usePotProvider};
|
||||
if(bindMode)payload.bind_mode=bindMode;
|
||||
if(plexPath!==undefined)payload.plex_library_path=plexPath;
|
||||
if(sourceUrl!==undefined)payload.source_url=sourceUrl;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue