openvibers/openvibe.tools Commits
YT: periodic upstream self-check, status in /api/health and a banner while YouTube refuses the server; README explains the two ways out
Changes
@@@ -58,6 +58,18 @@ Public, cacheable outputs on the apex: `/` (every tool by family, planned toolsDeploy with `deploy/scripts/deploy.sh` (it refreshes the copied shared package inside each app).++## YouTube downloader: when YouTube refuses the server++++YouTube blocks some datacentre addresses outright ("Sign in to confirm you're not a bot"). The app++checks every 30 minutes (`GET /api/health` → `youtube: ok | blocked | unknown`) and shows a banner++while blocked. Player-client switches, IPv6, proof-of-origin tokens, the nightly yt-dlp and Cloudflare++WARP were all tried on 2026-09-18 and all refused; what works is a different network identity, set in++`/etc/openvibe/tools.env` and followed by `sudo systemctl restart openvibe-tools-yt`:++++- `YT_PROXY=socks5://user:pass@host:port` — a residential or mobile proxy (http, https, socks4/5).++- `YT_COOKIES_FILE=/etc/openvibe/yt-cookies.txt` — a Netscape cookies.txt exported from a throwaway++ YouTube account, readable by the service user. Accounts used this way can be suspended.++## Dev quickstart```bash@@@ -202,6 +202,23 @@return d.innerHTML;}++ // --- Upstream status ---++ // The server checks YouTube on a timer. When YouTube is refusing it, say so before anyone pastes a link.++ (function upstreamBanner() {++ fetch('/api/health', { cache: 'no-store' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (h) {++ if (!h || h.youtube !== 'blocked' || document.getElementById('yt-upstream')) return;++ var b = document.createElement('div'); b.id = 'yt-upstream'; b.setAttribute('role', 'status');++ b.style.cssText = 'max-width:680px;margin:0 auto 16px;padding:12px 16px;border-radius:12px;border:1px solid color-mix(in srgb,var(--warning,#f59e0b) 45%,transparent);background:color-mix(in srgb,var(--warning,#f59e0b) 10%,transparent);color:var(--text-primary,#e6edf7);font-size:14px;line-height:1.5';++ var t = document.createElement('b'); t.textContent = 'YouTube is refusing our server right now. ';++ var m = document.createElement('span'); m.textContent = 'Downloads will fail until that clears. It is on YouTube\'s side and we re-check every 30 minutes. ';++ var a = document.createElement('a'); a.href = 'https://openvibe.tools/audio-tools'; a.textContent = 'Audio tools'; a.style.color = 'var(--accent-light,#60a5fa)';++ var a2 = document.createElement('a'); a2.href = 'https://openvibe.tools/'; a2.textContent = 'every other tool'; a2.style.color = 'var(--accent-light,#60a5fa)';++ b.appendChild(t); b.appendChild(m); b.appendChild(a); b.appendChild(document.createTextNode(' and ')); b.appendChild(a2); b.appendChild(document.createTextNode(' work as usual.'));++ var anchor = (typeof urlInput !== 'undefined' && urlInput && urlInput.closest('form, .input-row, .url-row, .search-box')) || document.querySelector('main') || document.body;++ anchor.parentNode.insertBefore(b, anchor);++ }).catch(function () {});++ })();++// --- Start Download ---// One reducer (download-state.js) turns SSE frames and polls into view state; render() paints it// on the page and mirrors it into the navbar activity island when that module is present.@@@ -382,6 +382,7 @@ function startDownload(url, quality = 'best', opts = {}) {console.error(`[Download] ${id} (${quality}) failed: ${last}`);entry.status = 'error';entry.error = friendlyDownloadError(last);++ if (/Sign in to confirm|not a bot/i.test(String(last))) noteUpstream('blocked');removePartials(id);return;}@@@ -573,8 +574,42 @@ function getStats() {};}++// ── Upstream self-check ──────────────────────────────────────++// YouTube sometimes refuses a whole server address ("confirm you're not a bot"). Rather than let++// every visitor find out after a click, probe a known public video on a timer and say so up front.++// state: 'ok' | 'blocked' | 'unknown' (GET /api/health → youtube, shown as a banner on the page)++const PROBE_URL = 'https://www.youtube.com/watch?v=jNQXAC9IVRw';++const PROBE_EVERY_MS = 30 * 60 * 1000;++let upstream = { state: 'unknown', checkedAt: 0, detail: '' };++let _probeTimer = null, _probing = false;++++function probeUpstream() {++ if (_probing) return Promise.resolve(upstream);++ _probing = true;++ return new Promise((resolve) => {++ const args = ['--no-playlist', ...identityArgs(), '--simulate', '--no-warnings', '--socket-timeout', '20', '-f', 'ba/b', '--print', 'id', PROBE_URL];++ let err = '', out = '';++ let child;++ const done = (state, detail) => { _probing = false; upstream = { state, checkedAt: Date.now(), detail: String(detail || '').slice(0, 200) }; resolve(upstream); };++ try { child = spawn(config.ytdlpPath, args, { stdio: ['ignore', 'pipe', 'pipe'] }); } catch (e) { return done('unknown', e.message); }++ const killer = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* */ } }, 60000);++ child.stdout.on('data', d => { out += d; }); child.stderr.on('data', d => { err += d; });++ child.on('error', e => { clearTimeout(killer); done('unknown', e.message); });++ child.on('close', (code) => {++ clearTimeout(killer);++ if (code === 0 && out.trim()) return done('ok', '');++ if (/Sign in to confirm|not a bot|HTTP Error 429|HTTP Error 403/i.test(err)) return done('blocked', 'YouTube is refusing requests from this server address');++ done('unknown', err.split('\n').filter(Boolean).pop() || 'probe failed');++ });++ });++}++function startUpstreamProbe() { if (_probeTimer) return; setTimeout(() => probeUpstream().catch(() => {}), 5000).unref(); _probeTimer = setInterval(() => probeUpstream().catch(() => {}), PROBE_EVERY_MS); _probeTimer.unref(); }++function getUpstream() { return upstream; }++/** A real request just told us more than the timer knows. */++function noteUpstream(state) { if (state === 'ok' || state === 'blocked') upstream = { state, checkedAt: Date.now(), detail: state === 'blocked' ? 'YouTube is refusing requests from this server address' : '' }; }++module.exports = {getInfo, startDownload, cancelDownload, getStatus, getFile, removeFile,sanitizeTitle, downloadFilename, contentDisposition, applyLine,-- cleanup, startCleanup, stopCleanup, getStats, isValidUrl,++ cleanup, startCleanup, stopCleanup, getStats, isValidUrl, probeUpstream, startUpstreamProbe, getUpstream,};@@@ -98,7 +98,8 @@ app.use(hostGuard({ knows: seo.knowsHost, aliasOf: seo.aliasOf }));// Health checkapp.get('/api/health', (_req, res) => {const stats = downloader.getStats();-- res.json({ status: 'ok', service: 'openvibe-yt', version: '1.0.0', stats });++ const up = downloader.getUpstream();++ res.set('Cache-Control', 'no-store').json({ status: 'ok', service: 'openvibe-yt', version: '1.0.0', stats, youtube: up.state, youtubeCheckedAt: up.checkedAt ? new Date(up.checkedAt).toISOString() : null });});// Get video info@@@ -256,6 +257,7 @@ app.use((req, res) => {// ── Start ────────────────────────────────────────────────────const server = app.listen(config.port, config.host, () => {downloader.startCleanup();++ downloader.startUpstreamProbe();console.log(`\n╔═══════════════════════════════════════╗`);console.log(`║ 📺 YT.OpenVibe — YouTube Downloader ║`);console.log(`╠═══════════════════════════════════════╣`);
@@ -58,6 +58,18 @@ Public, cacheable outputs on the apex: `/` (every tool by family, planned tools
@@ -58,6 +58,18 @@ Public, cacheable outputs on the apex: `/` (every tool by family, planned tools
Deploy with `deploy/scripts/deploy.sh` (it refreshes the copied shared package inside each app).
Deploy with `deploy/scripts/deploy.sh` (it refreshes the copied shared package inside each app).
+## YouTube downloader: when YouTube refuses the server
+
+YouTube blocks some datacentre addresses outright ("Sign in to confirm you're not a bot"). The app
+checks every 30 minutes (`GET /api/health` → `youtube: ok | blocked | unknown`) and shows a banner
+while blocked. Player-client switches, IPv6, proof-of-origin tokens, the nightly yt-dlp and Cloudflare
+WARP were all tried on 2026-09-18 and all refused; what works is a different network identity, set in
+`/etc/openvibe/tools.env` and followed by `sudo systemctl restart openvibe-tools-yt`:
+
+- `YT_PROXY=socks5://user:pass@host:port` — a residential or mobile proxy (http, https, socks4/5).
+- `YT_COOKIES_FILE=/etc/openvibe/yt-cookies.txt` — a Netscape cookies.txt exported from a throwaway
+ YouTube account, readable by the service user. Accounts used this way can be suspended.
+
## Dev quickstart
## Dev quickstart
```bash
```bash
@@ -202,6 +202,23 @@
@@ -202,6 +202,23 @@
return d.innerHTML;
return d.innerHTML;
}
}
+ // --- Upstream status ---
+ // The server checks YouTube on a timer. When YouTube is refusing it, say so before anyone pastes a link.
+ (function upstreamBanner() {
+ fetch('/api/health', { cache: 'no-store' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (h) {
+ if (!h || h.youtube !== 'blocked' || document.getElementById('yt-upstream')) return;
+ var b = document.createElement('div'); b.id = 'yt-upstream'; b.setAttribute('role', 'status');
+ b.style.cssText = 'max-width:680px;margin:0 auto 16px;padding:12px 16px;border-radius:12px;border:1px solid color-mix(in srgb,var(--warning,#f59e0b) 45%,transparent);background:color-mix(in srgb,var(--warning,#f59e0b) 10%,transparent);color:var(--text-primary,#e6edf7);font-size:14px;line-height:1.5';
+ var t = document.createElement('b'); t.textContent = 'YouTube is refusing our server right now. ';
+ var m = document.createElement('span'); m.textContent = 'Downloads will fail until that clears. It is on YouTube\'s side and we re-check every 30 minutes. ';
+ var a = document.createElement('a'); a.href = 'https://openvibe.tools/audio-tools'; a.textContent = 'Audio tools'; a.style.color = 'var(--accent-light,#60a5fa)';
+ var a2 = document.createElement('a'); a2.href = 'https://openvibe.tools/'; a2.textContent = 'every other tool'; a2.style.color = 'var(--accent-light,#60a5fa)';
+ b.appendChild(t); b.appendChild(m); b.appendChild(a); b.appendChild(document.createTextNode(' and ')); b.appendChild(a2); b.appendChild(document.createTextNode(' work as usual.'));
+ var anchor = (typeof urlInput !== 'undefined' && urlInput && urlInput.closest('form, .input-row, .url-row, .search-box')) || document.querySelector('main') || document.body;
+ anchor.parentNode.insertBefore(b, anchor);
+ }).catch(function () {});
+ })();
+
// --- Start Download ---
// --- Start Download ---
// One reducer (download-state.js) turns SSE frames and polls into view state; render() paints it
// One reducer (download-state.js) turns SSE frames and polls into view state; render() paints it
// on the page and mirrors it into the navbar activity island when that module is present.
// on the page and mirrors it into the navbar activity island when that module is present.
@@ -382,6 +382,7 @@ function startDownload(url, quality = 'best', opts = {}) {
@@ -382,6 +382,7 @@ function startDownload(url, quality = 'best', opts = {}) {
console.error(`[Download] ${id} (${quality}) failed: ${last}`);
console.error(`[Download] ${id} (${quality}) failed: ${last}`);
entry.status = 'error';
entry.status = 'error';
entry.error = friendlyDownloadError(last);
entry.error = friendlyDownloadError(last);
+ if (/Sign in to confirm|not a bot/i.test(String(last))) noteUpstream('blocked');
removePartials(id);
removePartials(id);
return;
return;
}
}
@@ -573,8 +574,42 @@ function getStats() {
@@ -573,8 +574,42 @@ function getStats() {
};
};
}
}
+// ── Upstream self-check ──────────────────────────────────────
+// YouTube sometimes refuses a whole server address ("confirm you're not a bot"). Rather than let
+// every visitor find out after a click, probe a known public video on a timer and say so up front.
+// state: 'ok' | 'blocked' | 'unknown' (GET /api/health → youtube, shown as a banner on the page)
+const PROBE_URL = 'https://www.youtube.com/watch?v=jNQXAC9IVRw';
+const PROBE_EVERY_MS = 30 * 60 * 1000;
+let upstream = { state: 'unknown', checkedAt: 0, detail: '' };
+let _probeTimer = null, _probing = false;
+
+function probeUpstream() {
+ if (_probing) return Promise.resolve(upstream);
+ _probing = true;
+ return new Promise((resolve) => {
+ const args = ['--no-playlist', ...identityArgs(), '--simulate', '--no-warnings', '--socket-timeout', '20', '-f', 'ba/b', '--print', 'id', PROBE_URL];
+ let err = '', out = '';
+ let child;
+ const done = (state, detail) => { _probing = false; upstream = { state, checkedAt: Date.now(), detail: String(detail || '').slice(0, 200) }; resolve(upstream); };
+ try { child = spawn(config.ytdlpPath, args, { stdio: ['ignore', 'pipe', 'pipe'] }); } catch (e) { return done('unknown', e.message); }
+ const killer = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* */ } }, 60000);
+ child.stdout.on('data', d => { out += d; }); child.stderr.on('data', d => { err += d; });
+ child.on('error', e => { clearTimeout(killer); done('unknown', e.message); });
+ child.on('close', (code) => {
+ clearTimeout(killer);
+ if (code === 0 && out.trim()) return done('ok', '');
+ if (/Sign in to confirm|not a bot|HTTP Error 429|HTTP Error 403/i.test(err)) return done('blocked', 'YouTube is refusing requests from this server address');
+ done('unknown', err.split('\n').filter(Boolean).pop() || 'probe failed');
+ });
+ });
+}
+function startUpstreamProbe() { if (_probeTimer) return; setTimeout(() => probeUpstream().catch(() => {}), 5000).unref(); _probeTimer = setInterval(() => probeUpstream().catch(() => {}), PROBE_EVERY_MS); _probeTimer.unref(); }
+function getUpstream() { return upstream; }
+/** A real request just told us more than the timer knows. */
+function noteUpstream(state) { if (state === 'ok' || state === 'blocked') upstream = { state, checkedAt: Date.now(), detail: state === 'blocked' ? 'YouTube is refusing requests from this server address' : '' }; }
+
module.exports = {
module.exports = {
getInfo, startDownload, cancelDownload, getStatus, getFile, removeFile,
getInfo, startDownload, cancelDownload, getStatus, getFile, removeFile,
sanitizeTitle, downloadFilename, contentDisposition, applyLine,
sanitizeTitle, downloadFilename, contentDisposition, applyLine,
- cleanup, startCleanup, stopCleanup, getStats, isValidUrl,
+ cleanup, startCleanup, stopCleanup, getStats, isValidUrl, probeUpstream, startUpstreamProbe, getUpstream,
};
};
@@ -98,7 +98,8 @@ app.use(hostGuard({ knows: seo.knowsHost, aliasOf: seo.aliasOf }));
@@ -98,7 +98,8 @@ app.use(hostGuard({ knows: seo.knowsHost, aliasOf: seo.aliasOf }));
// Health check
// Health check
app.get('/api/health', (_req, res) => {
app.get('/api/health', (_req, res) => {
const stats = downloader.getStats();
const stats = downloader.getStats();
- res.json({ status: 'ok', service: 'openvibe-yt', version: '1.0.0', stats });
+ const up = downloader.getUpstream();
+ res.set('Cache-Control', 'no-store').json({ status: 'ok', service: 'openvibe-yt', version: '1.0.0', stats, youtube: up.state, youtubeCheckedAt: up.checkedAt ? new Date(up.checkedAt).toISOString() : null });
});
});
// Get video info
// Get video info
@@ -256,6 +257,7 @@ app.use((req, res) => {
@@ -256,6 +257,7 @@ app.use((req, res) => {
// ── Start ────────────────────────────────────────────────────
// ── Start ────────────────────────────────────────────────────
const server = app.listen(config.port, config.host, () => {
const server = app.listen(config.port, config.host, () => {
downloader.startCleanup();
downloader.startCleanup();
+ downloader.startUpstreamProbe();
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`║ 📺 YT.OpenVibe — YouTube Downloader ║`);
console.log(`║ 📺 YT.OpenVibe — YouTube Downloader ║`);
console.log(`╠═══════════════════════════════════════╣`);
console.log(`╠═══════════════════════════════════════╣`);