cdnV2

git clone https://github.com/notamitgamer/cdnV2.git
· rebuilt hourlyOpen on GitHub
commit 8e6ab1c9229e52cf8fb2f25e182513c2b8fb4edf
parent 7a55b9af09b95b79e86397b37bce997e3fed673a
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Thu, 17 Sep 2026 14:32:26 +0530

Revert "Restore YouTube/Cobalt mp3 download (fixed startup check)"

Diffstat:
MDockerfile | 5-----
Mapp/main.py | 326-------------------------------------------------------------------------------
Mapp/templates/_navbar.html | 5-----
Dapp/templates/ytmusic.html | 462-------------------------------------------------------------------------------
Mrender.yaml | 13++-----------
Mstart.sh | 119+++----------------------------------------------------------------------------
6 files changed, 6 insertions(+), 924 deletions(-)

diff --git a/Dockerfile b/Dockerfile @@ -1,5 +1,3 @@ -FROM ghcr.io/imputnet/cobalt:11 AS cobalt - FROM node:24-bookworm-slim WORKDIR /app @@ -11,9 +9,6 @@ RUN apt-get update \ python3-pip \ && rm -rf /var/lib/apt/lists/* -# Copy the already-built Cobalt API from the official Cobalt image -COPY --from=cobalt /app /opt/cobalt-api - # Install FastAPI dependencies COPY requirements.txt . diff --git a/app/main.py b/app/main.py @@ -368,332 +368,6 @@ async def gh_sync( } -@app.get("/ytmusic") -async def url_ytmusic_page(request: Request): - ctx = await render_context({"page": "ytmusic"}) - return templates.TemplateResponse(request, "ytmusic.html", ctx) - - -# --------------------------------------------------------------------------------- -# YTMusic routes: Cobalt API Proxy -# --------------------------------------------------------------------------------- - -class YtRequest(BaseModel): - url: str - - -COBALT_INTERNAL_URL = os.getenv( - "COBALT_INTERNAL_URL", - "http://127.0.0.1:9000", -).rstrip("/") - -COBALT_PUBLIC_PREFIX = "/cobalt" - - -@app.api_route( - "/cobalt/{path:path}", - methods=["GET", "HEAD"], -) -async def cobalt_tunnel_proxy(path: str, request: Request): - cobalt_url = f"{COBALT_INTERNAL_URL}/{path}" - - query_string = request.url.query - if query_string: - cobalt_url += f"?{query_string}" - - headers = {} - - for header in [ - "Range", - "If-Range", - "If-None-Match", - "If-Modified-Since", - ]: - value = request.headers.get(header) - if value: - headers[header] = value - - client = httpx.AsyncClient( - follow_redirects=False, - timeout=60.0, - ) - - try: - req = client.build_request( - request.method, - cobalt_url, - headers=headers, - ) - - res = await client.send(req, stream=True) - - response_headers = {} - - for header in [ - "Content-Type", - "Content-Length", - "Content-Range", - "Accept-Ranges", - "Content-Disposition", - "Cache-Control", - "ETag", - "Last-Modified", - ]: - value = res.headers.get(header) - if value: - response_headers[header] = value - - return StreamingResponse( - _proxy_stream(client, res), - status_code=res.status_code, - headers=response_headers, - ) - - except httpx.RequestError as exc: - await client.aclose() - raise HTTPException( - status_code=502, - detail=f"Error connecting to Cobalt tunnel: {exc}", - ) - - -@app.post("/api/yt/download") -async def yt_download_cobalt(request: Request, body: YtRequest): - url = body.url.strip() - - if not url: - raise HTTPException( - status_code=400, - detail="Missing YouTube URL.", - ) - - cobalt_api = f"{COBALT_INTERNAL_URL}/" - - headers = { - "Accept": "application/json", - "Content-Type": "application/json", - "User-Agent": ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/122.0.0.0 Safari/537.36" - ), - } - - cobalt_api_token = os.getenv("COBALT_API_TOKEN") - - if cobalt_api_token: - if " " not in cobalt_api_token: - headers["Authorization"] = f"Api-Key {cobalt_api_token}" - else: - headers["Authorization"] = cobalt_api_token - - payload = { - "url": url, - "downloadMode": "audio", - "audioFormat": "mp3", - "audioBitrate": "128", - "filenameStyle": "basic", - "youtubeHLS": True, - } - - async with httpx.AsyncClient( - timeout=60.0, - follow_redirects=False, - ) as client: - try: - res = await client.post( - cobalt_api, - json=payload, - headers=headers, - ) - - if res.status_code != 200: - err_text = res.text - - try: - data = res.json() - - if "error" in data and isinstance(data["error"], dict): - err_text = data["error"].get( - "code", - err_text, - ) - elif data.get("text"): - err_text = data["text"] - - except Exception: - pass - - if len(err_text) > 300: - err_text = err_text[:300] + "..." - - raise HTTPException( - status_code=400, - detail=f"Cobalt API failed: {err_text}", - ) - - data = res.json() - status = data.get("status") - - if status == "error": - error = data.get("error", {}) - - print("========== COBALT ERROR ==========") - print(f"Full response: {data}") - - if isinstance(error, dict): - print(f"Error code: {error.get('code')}") - print(f"Error context: {error.get('context')}") - else: - print(f"Error: {error}") - - print("==================================") - - if isinstance(error, dict): - raise HTTPException( - status_code=400, - detail={ - "message": "Cobalt API failed", - "code": error.get("code"), - "context": error.get("context"), - "raw": data, - }, - ) - else: - raise HTTPException( - status_code=400, - detail={ - "message": "Cobalt API failed", - "error": str(error), - "raw": data, - }, - ) - - dl_url = data.get("url") - - if not dl_url: - raise HTTPException( - status_code=500, - detail=( - "Cobalt succeeded but returned no " - f"download link. (Status: {status})" - ), - ) - - if status == "tunnel": - parsed = urlparse(dl_url) - - if parsed.path.startswith("/tunnel"): - public_url = ( - f"{CDN_BASE_URL}{COBALT_PUBLIC_PREFIX}" - f"{parsed.path}" - ) - - if parsed.query: - public_url += f"?{parsed.query}" - - dl_url = public_url - - return { - "url": dl_url, - "title": data.get("filename", "Audio"), - } - - except httpx.RequestError as exc: - raise HTTPException( - status_code=502, - detail=f"Error connecting to Cobalt: {exc}", - ) - - -# --------------------------------------------------------------------------------- -# TEMPORARY COBALT DIAGNOSTIC ENDPOINT -# --------------------------------------------------------------------------------- - -@app.get("/api/yt/debug") -async def yt_debug(url: str): - url = url.strip() - - if not url: - raise HTTPException( - status_code=400, - detail="Missing YouTube URL.", - ) - - cobalt_api = f"{COBALT_INTERNAL_URL}/" - - headers = { - "Accept": "application/json", - "Content-Type": "application/json", - "User-Agent": ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/122.0.0.0 Safari/537.36" - ), - } - - cobalt_api_token = os.getenv("COBALT_API_TOKEN") - - if cobalt_api_token: - if " " not in cobalt_api_token: - headers["Authorization"] = f"Api-Key {cobalt_api_token}" - else: - headers["Authorization"] = cobalt_api_token - - payload = { - "url": url, - "downloadMode": "audio", - "audioFormat": "mp3", - "audioBitrate": "128", - "filenameStyle": "basic", - } - - async with httpx.AsyncClient( - timeout=60.0, - follow_redirects=False, - ) as client: - try: - res = await client.post( - cobalt_api, - json=payload, - headers=headers, - ) - - raw_text = res.text - - try: - response_body = res.json() - except Exception: - response_body = raw_text - - print("========== COBALT DEBUG ==========") - print(f"HTTP status: {res.status_code}") - print(f"Response headers: {dict(res.headers)}") - print(f"Response body: {response_body}") - print("===================================") - - return { - "cobalt_url": cobalt_api, - "http_status": res.status_code, - "response_headers": dict(res.headers), - "response": response_body, - } - - except httpx.RequestError as exc: - print("========== COBALT DEBUG ERROR ==========") - print(f"Request error: {repr(exc)}") - print("========================================") - - raise HTTPException( - status_code=502, - detail={ - "message": "Error connecting to Cobalt", - "error": repr(exc), - }, - ) - - - @app.get("/shorten") async def url_shortener_page(request: Request): ctx = await render_context({"page": "shorten"}) diff --git a/app/templates/_navbar.html b/app/templates/_navbar.html @@ -179,9 +179,6 @@ <a href="/shorten" class="{{ 'active' if page == 'shorten' }}" title="URL Shortener" aria-label="URL Shortener"{{ ' aria-current="page"'|safe if page == 'shorten' }}> <svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg> </a> - <a href="/ytmusic" class="{{ 'active' if page == 'ytmusic' }}" title="Youtube Music Download" aria-label="Youtube Music Download"{{ ' aria-current="page"'|safe if page == 'ytmusic' }}> - <svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 10v3"/><path d="M6 6v11"/><path d="M10 3v18"/><path d="M14 8v7"/><path d="M18 5v13"/><path d="M22 10v3"/></svg> - </a> </nav> </header> {% if not (page == 'listing' and not path) %} @@ -209,8 +206,6 @@ {% set ns.items = ns.items + [{'name': 'gh-sync', 'href': '/gh-sync'}] %} {% elif page == 'shorten' %} {% set ns.items = ns.items + [{'name': 'shorten', 'href': '/shorten'}] %} - {% elif page == 'ytmusic' %} - {% set ns.items = ns.items + [{'name': 'ytmusic', 'href': '/ytmusic'}] %} {% elif page == 'file' %} {% set ns.items = ns.items + [{'name': filename, 'href': none}] %} {% endif %} diff --git a/app/templates/ytmusic.html b/app/templates/ytmusic.html @@ -1,461 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>YouTube Music Downloader &mdash; amit/cdn</title> - <meta name="description" content="Get a direct download link for YouTube music."> - <link rel="icon" href="/favicon.ico" sizes="any"> - <link rel="icon" type="image/png" sizes="32x32" href="/static/icons/favicon-32x32.png"> - <link rel="icon" type="image/png" sizes="16x16" href="/static/icons/favicon-16x16.png"> - <link rel="icon" type="image/svg+xml" href="/static/icons/logo.svg"> - <link rel="apple-touch-icon" sizes="180x180" href="/static/icons/apple-touch-icon.png"> - <link rel="manifest" href="/static/manifest.json"> - <meta name="theme-color" content="#09090b"> - <meta name="color-scheme" content="dark"> - <script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.4/dist/turbo.es2017-esm.js"></script> - <link rel="preconnect" href="https://fonts.googleapis.com"> - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> - <link href="https://fonts.googleapis.com/css2?family=Zilla+Slab:wght@500;600;700&family=Work+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> - <style> - :root { - --bg: #09090b; - --surface: #121214; - --surface-2: #1e1e22; - --surface-3: #27272a; - --text-main: #f4f4f5; - --text-dim: #b8b8c0; - --accent-primary: #818cf8; - --accent-primary-hover: #a5b4fc; - --success: #10b981; - --danger: #ef4444; - --border: rgba(255, 255, 255, 0.08); - --border-hover: rgba(255, 255, 255, 0.15); - --font-heading: 'Zilla Slab', Georgia, 'Times New Roman', serif; - --font-body: 'Work Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; - --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - --radius-md: 8px; - --radius-lg: 12px; - --radius-xl: 16px; - } - * { box-sizing: border-box; } - body { - font-family: var(--font-body); - background: var(--bg); - color: var(--text-main); - margin: 0; - padding: 0 0 4rem 0; - line-height: 1.6; - font-size: 1rem; - -webkit-font-smoothing: antialiased; - } - @media (max-width: 480px) { - body { font-size: 0.95rem; } - } - .container { - width: 90%; - max-width: 1000px; - margin: 0 auto; - } - h1 { - font-family: var(--font-heading); - font-size: clamp(2.2rem, 5vw, 3rem); - letter-spacing: -0.02em; - color: #ffffff; - font-weight: 600; - margin: 0 0 1.25rem 0; - line-height: 1.1; - max-width: 20ch; - } - blockquote { - background: var(--surface); - border: 1px solid var(--border); - border-left: 3px solid var(--accent-primary); - border-radius: var(--radius-md); - margin: 0 0 2.25rem 0; - padding: 1rem 1.25rem; - color: var(--text-dim); - max-width: 68ch; - font-size: 0.95rem; - } - a { color: var(--accent-primary); text-decoration: none; transition: color 0.15s; } - a:hover { color: var(--accent-primary-hover); text-decoration: underline; } - .input-group { - position: relative; - display: flex; - align-items: center; - width: 100%; - } - .url-input { - width: 100%; - padding: 0.85rem 2.5rem 0.85rem 1.25rem; - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--text-main); - font-family: var(--font-mono); - font-size: 0.9rem; - transition: all 0.2s ease; - box-shadow: 0 2px 10px rgba(0,0,0,0.1) inset; - } - .url-input:focus { - outline: none; - background: var(--surface-2); - border-color: var(--accent-primary); - box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.15); - } - .url-input::placeholder { color: var(--text-dim); } - .input-clear-btn { - position: absolute; - right: 0.75rem; - top: 50%; - transform: translateY(-50%); - background: transparent; - border: none; - color: var(--text-dim); - cursor: pointer; - padding: 0.25rem; - display: none; - align-items: center; - justify-content: center; - border-radius: 50%; - transition: color 0.2s, background 0.2s; - } - .input-group[data-has-text="true"] .input-clear-btn { display: flex; } - .input-clear-btn:hover { color: var(--text-main); background: var(--surface-3); } - .btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - background: var(--surface-2); - color: var(--text-main); - padding: 0.65rem 1.25rem; - border: 1px solid var(--border); - border-radius: var(--radius-md); - cursor: pointer; - font-family: var(--font-body); - font-size: 0.9rem; - font-weight: 500; - transition: all 0.2s ease, transform 0.1s ease; - text-decoration: none; - } - .btn:hover:not(:disabled) { background: var(--surface-3); border-color: var(--border-hover); text-decoration: none; color: var(--text-main); } - .btn:active:not(:disabled) { transform: scale(0.97); } - .btn:disabled { opacity: 0.6; cursor: not-allowed; } - .btn-primary { background: var(--accent-primary) !important; color: #ffffff !important; border-color: var(--accent-primary) !important; font-weight: 600; transition: none !important; } - .btn-primary:hover:not(:disabled), .btn-primary:focus:not(:disabled), .btn-primary:active:not(:disabled) { background: var(--accent-primary) !important; border-color: var(--accent-primary) !important; color: #ffffff !important; } - .btn-primary svg { stroke: #ffffff !important; } - .result-box { - margin-top: 2rem; - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 1.5rem; - } - .result-box h3 { - font-family: var(--font-heading); - font-size: 1.2rem; - margin: 0 0 1rem 0; - color: #fff; - } - .result-url { - font-family: var(--font-mono); - font-size: 0.95rem; - word-break: break-all; - color: var(--accent-primary); - background: var(--surface-2); - padding: 0.75rem 1rem; - border-radius: var(--radius-md); - border: 1px solid var(--border); - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - flex-wrap: wrap; - } - .result-url a { color: var(--accent-primary); text-decoration: none; } - .result-url a:hover { text-decoration: underline; } - .error-box { - display: none; - color: var(--danger); - margin-top: 1rem; - padding: 1rem 1.25rem; - border: 1px solid rgba(239, 68, 68, 0.5); - background: rgba(239, 68, 68, 0.1); - border-radius: var(--radius-md); - } - .info-text { - color: var(--text-dim); - font-size: 0.9rem; - margin-top: 1rem; - } - .spin-anim { animation: spin 0.7s linear infinite; display: inline-flex; } - @keyframes spin { 100% { transform: rotate(360deg); } } - .btn-primary .btn-icon { display: inline-flex; } - .btn-primary.is-loading .btn-icon.btn-icon-default { display: none; } - .btn-primary .btn-icon-spinner { display: none; } - .btn-primary.is-loading .btn-icon-spinner { display: inline-flex; } - footer { - width: 90%; - max-width: 1000px; - margin: 4rem auto 0; - padding-top: 1.5rem; - border-top: 1px solid var(--border); - font-size: .85rem; - color: var(--muted); - } - @media (max-width: 480px) { - .container { width: 100%; padding: 0 1.25rem; } - .result-url { flex-direction: column; align-items: flex-start; } - } - </style> -</head> -<body> - {% include "_navbar.html" %} - <main class="container"> - <h1>YouTube Music Downloader</h1> - <blockquote> - Paste a YouTube or YouTube Music URL and get a 128 KBPS audio download link. - </blockquote> - - <div class="input-group" id="youtube-group"> - <input - type="url" - id="youtube-input" - class="url-input" - placeholder="Paste a YouTube URL (e.g. https://music.youtube.com/...)" - autocomplete="off" - spellcheck="false" - required - > - <button type="button" class="input-clear-btn" id="youtube-clear" aria-label="Clear URL"> - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> - <line x1="18" y1="6" x2="6" y2="18"/> - <line x1="6" y1="6" x2="18" y2="18"/> - </svg> - </button> - </div> - - <div style="display:flex; gap:0.75rem; margin-top:1rem; align-items:center;"> - <button type="button" class="btn btn-primary" id="download-btn"> - <span class="btn-icon btn-icon-default"> - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> - <path d="M12 3v12"/> - <path d="m7 10 5 5 5-5"/> - <path d="M5 21h14"/> - </svg> - </span> - <span class="btn-icon btn-icon-spinner spin-anim"> - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> - <path d="M21 12a9 9 0 1 1-6.219-8.56"/> - </svg> - </span> - <span id="download-btn-label">Get Download Link</span> - </button> - </div> - - <div id="status-box" class="info-text" style="display:none;" aria-live="polite"></div> - <div id="error-box" class="error-box" role="alert" aria-live="polite"></div> - - <div id="result" style="display:none;" class="result-box"> - <h3>Your download link</h3> - <div class="result-url"> - <a href="#" id="masked-link" target="_blank" rel="noreferrer" referrerpolicy="no-referrer"></a> - <button type="button" class="btn" id="copy-btn">Copy</button> - </div> - <p class="info-text">This link redirects to the official VidsSave download stream.</p> - </div> - </main> - - <footer> - <p>&copy; 2025 - 2026 notamitgamer. All rights reserved.</p> - </footer> - - <script> - (function() { - const AUTH = '20250901majwlqo'; - const DOMAIN = 'api-ak.vidssave.com'; - - function isYouTubeUrl(rawUrl) { - try { - const parsed = new URL(rawUrl); - if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return false; - const host = parsed.hostname.toLowerCase().replace(/^www\./, ''); - return ( - host === 'youtube.com' || - host === 'm.youtube.com' || - host === 'music.youtube.com' || - host === 'youtu.be' - ); - } catch { - return false; - } - } - - function setupYtMusic() { - const youtubeInput = document.getElementById('youtube-input'); - const youtubeClear = document.getElementById('youtube-clear'); - const downloadBtn = document.getElementById('download-btn'); - const downloadBtnLabel = document.getElementById('download-btn-label'); - const statusBox = document.getElementById('status-box'); - const errorBox = document.getElementById('error-box'); - const resultDiv = document.getElementById('result'); - const maskedLink = document.getElementById('masked-link'); - const copyBtn = document.getElementById('copy-btn'); - - if (!youtubeInput || !downloadBtn || downloadBtn.dataset.initialized) return; - downloadBtn.dataset.initialized = 'true'; - - function setStatus(message) { - statusBox.textContent = message; - statusBox.style.display = message ? 'block' : 'none'; - } - - function showError(message) { - errorBox.textContent = message; - errorBox.style.display = 'block'; - } - - function clearMessages() { - errorBox.style.display = 'none'; - resultDiv.style.display = 'none'; - setStatus(''); - } - - function setLoading(loading) { - downloadBtn.disabled = loading; - downloadBtn.classList.toggle('is-loading', loading); - downloadBtnLabel.textContent = loading ? 'Working...' : 'Get Download Link'; - } - - async function getDownloadLink(url) { - const response = await fetch('/api/yt/download', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url }) - }); - - const data = await response.json(); - if (!response.ok || !data.url) { - throw new Error(data.detail || 'Failed to extract download link.'); - } - return data.url; - } - - async function shortenUrl(url) { - const response = await fetch('/api/shorten', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url }) - }); - - const data = await response.json().catch(() => ({})); - if (!response.ok || !data.id || !data.masked_url) { - throw new Error(data.detail || 'The download link could not be shortened.'); - } - return data.masked_url; - } - - downloadBtn.addEventListener('click', async () => { - const url = youtubeInput.value.trim(); - clearMessages(); - - if (!url) { - showError('Please enter a YouTube URL.'); - return; - } - if (!isYouTubeUrl(url)) { - showError('Please enter a valid YouTube or YouTube Music URL.'); - return; - } - - setLoading(true); - - try { - setStatus('Extracting direct audio link natively...'); - const downloadUrl = await getDownloadLink(url); - - setStatus('Shortening link...'); - const maskedUrl = await shortenUrl(downloadUrl); - - maskedLink.href = maskedUrl; - maskedLink.textContent = maskedUrl; - copyBtn.setAttribute('data-url', maskedUrl); - - resultDiv.style.display = 'block'; - setStatus('Ready.'); - } catch (error) { - console.error(error); - showError(error.message || 'Something went wrong.'); - } finally { - setLoading(false); - } - }); - - youtubeClear.addEventListener('click', () => { - youtubeInput.value = ''; - youtubeInput.focus(); - youtubeClear.parentElement.removeAttribute('data-has-text'); - clearMessages(); - }); - - youtubeInput.addEventListener('input', () => { - if (youtubeInput.value.length > 0) { - youtubeClear.parentElement.setAttribute('data-has-text', 'true'); - } else { - youtubeClear.parentElement.removeAttribute('data-has-text'); - } - }); - - youtubeInput.addEventListener('keydown', event => { - if (event.key === 'Enter') { - event.preventDefault(); - downloadBtn.click(); - } - }); - - copyBtn.addEventListener('click', async () => { - const url = copyBtn.getAttribute('data-url'); - if (!url) return; - - try { - await navigator.clipboard.writeText(url); - const originalText = copyBtn.textContent; - const originalStyle = copyBtn.style.cssText; - - copyBtn.textContent = 'Copied!'; - copyBtn.style.borderColor = 'var(--success)'; - copyBtn.style.color = 'var(--success)'; - - setTimeout(() => { - copyBtn.style.cssText = originalStyle; - copyBtn.textContent = originalText; - }, 1000); - } catch (error) { - console.error('Copy failed:', error); - } - }); - } - - document.addEventListener('turbo:load', setupYtMusic); - document.addEventListener('DOMContentLoaded', setupYtMusic); - - if (!window.__ytmusicKbdBound) { - window.__ytmusicKbdBound = true; - document.addEventListener('keydown', event => { - if (event.key !== '/') return; - const tag = document.activeElement?.tagName || ''; - if (tag === 'INPUT' || tag === 'TEXTAREA') return; - - const currentYtInput = document.getElementById('youtube-input'); - if (currentYtInput) { - event.preventDefault(); - currentYtInput.focus(); - } - }); - } - })(); - </script> -</body> -</html> -\ No newline at end of file diff --git a/render.yaml b/render.yaml @@ -14,13 +14,4 @@ services: value: raw.cdn.amit.is-a.dev - key: RAW_BASE_URL - value: https://raw.cdn.amit.is-a.dev - - - key: COBALT_API_URL - value: https://cdn.amit.is-a.dev/cobalt/ - - - key: COBALT_PORT - value: "9000" - - - key: YOUTUBE_COOKIE_HEADER - sync: false -\ No newline at end of file + value: https://raw.cdn.amit.is-a.dev +\ No newline at end of file diff --git a/start.sh b/start.sh @@ -1,122 +1,11 @@ -#!/bin/bash +#!/bin/sh set -e echo "========================================" -echo "Starting CDN + Cobalt" +echo "Starting CDN" echo "========================================" -echo "" -echo "========== COBALT ENV ==========" -echo "API_URL=${API_URL:-https://cdn.amit.is-a.dev/cobalt/}" -echo "API_PORT=9000" -echo "================================" -echo "" - -COBALT_COOKIE_PATH="" - -if [ -n "$YOUTUBE_COOKIE_HEADER" ]; then - echo "YOUTUBE_COOKIE_HEADER is set - writing cookies.json..." - - COBALT_COOKIE_PATH="/tmp/cobalt-cookies.json" - - YOUTUBE_COOKIE_HEADER="$YOUTUBE_COOKIE_HEADER" python3 -c " -import json -import os - -cookie_header = os.environ['YOUTUBE_COOKIE_HEADER'].strip() - -with open('$COBALT_COOKIE_PATH', 'w') as f: - json.dump({'youtube': [cookie_header]}, f) -" - - echo "Wrote $COBALT_COOKIE_PATH" -else - echo "YOUTUBE_COOKIE_HEADER not set - continuing without YouTube cookies" - echo "(YouTube downloads will likely fail with error.api.youtube.login)" -fi - -echo "" -echo "Starting Cobalt..." - -cd /opt/cobalt-api - -# NOTE: we intentionally do NOT gate startup on the presence of a specific -# file (e.g. "src/cobalt"). Cobalt's own image runs it as -# `node src/cobalt`, which Node resolves via its module resolution -# algorithm (src/cobalt.js or src/cobalt/index.js) - there is no literal -# file named "cobalt" with no extension. A hard existence check on that -# exact path previously killed the whole container on every boot even -# when the Cobalt build itself was fine. Instead we start the process and -# let the readiness probe below be the real success/failure signal. -if [ -n "$COBALT_COOKIE_PATH" ]; then - API_URL="https://cdn.amit.is-a.dev/cobalt/" \ - API_PORT=9000 \ - COOKIE_PATH="$COBALT_COOKIE_PATH" \ - node src/cobalt & -else - API_URL="https://cdn.amit.is-a.dev/cobalt/" \ - API_PORT=9000 \ - node src/cobalt & -fi - -COBALT_PID=$! - -echo "Cobalt PID: $COBALT_PID" - -echo "" -echo "Waiting for Cobalt to start..." - -COBALT_READY=0 - -for i in $(seq 1 30); do - if python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9000/', timeout=1)" >/dev/null 2>&1; then - COBALT_READY=1 - echo "Cobalt is ready." - break - fi - - if ! kill -0 "$COBALT_PID" 2>/dev/null; then - echo "ERROR: Cobalt process exited unexpectedly during startup." - wait "$COBALT_PID" - exit 1 - fi - - sleep 1 -done - -if [ "$COBALT_READY" -ne 1 ]; then - echo "ERROR: Cobalt did not become ready within 30 seconds." - kill "$COBALT_PID" 2>/dev/null || true - exit 1 -fi - -echo "" -echo "Starting FastAPI..." - -cd /app - -uvicorn app.main:app \ +exec uvicorn app.main:app \ --host 0.0.0.0 \ - --port "${PORT:-8000}" & - -FASTAPI_PID=$! - -echo "FastAPI PID: $FASTAPI_PID" - -echo "" -echo "========================================" -echo "Both services started" -echo "========================================" -echo "Cobalt: http://127.0.0.1:9000" -echo "FastAPI: http://0.0.0.0:${PORT:-8000}" -echo "========================================" - -wait -n "$COBALT_PID" "$FASTAPI_PID" - -echo "" -echo "ERROR: One of the services stopped." -echo "Cobalt PID: $COBALT_PID" -echo "FastAPI PID: $FASTAPI_PID" - -exit 1 + --port "${PORT:-8000}"