cdnV2

git clone https://github.com/notamitgamer/cdnV2.git
· rebuilt hourlyOpen on GitHub
commit 74e77775110fb97eee9399aa72dcc515f4523209
parent 3bb18910c8762ae5263fc23bd2f9678228e6a76b
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Wed, 16 Sep 2026 22:35:06 +0530

fix#11

Diffstat:
MDockerfile | 50++++++++++++++++++++++++++++++++++++++++++++++----
Mapp/main.py | 257++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mrender.yaml | 10++++++++++
Astart.sh | 26++++++++++++++++++++++++++
4 files changed, 278 insertions(+), 65 deletions(-)

diff --git a/Dockerfile b/Dockerfile @@ -1,13 +1,54 @@ -FROM python:3.11-slim +FROM node:24-bookworm-slim WORKDIR /app +# Install Python and build dependencies required by Cobalt +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + python3-dev \ + git \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Enable pnpm +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN corepack enable + +# Clone the current Cobalt source +RUN git clone --depth 1 https://github.com/imputnet/cobalt.git /opt/cobalt + +WORKDIR /opt/cobalt + +# Install Cobalt production dependencies +RUN pnpm install --prod --frozen-lockfile + +# Deploy only the Cobalt API package +RUN pnpm deploy --filter=@imput/cobalt-api --prod /opt/cobalt-api + +# Install Python dependencies +WORKDIR /app + COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -# Copy the app structure +RUN pip3 install \ + --no-cache-dir \ + --break-system-packages \ + -r requirements.txt + +# Copy the FastAPI application COPY app/ ./app/ +# Copy startup script +COPY start.sh /start.sh + +RUN chmod +x /start.sh + +# Render exposes the FastAPI process. +# Cobalt itself remains internal on port 9000. EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["/start.sh"] +\ No newline at end of file diff --git a/app/main.py b/app/main.py @@ -314,31 +314,120 @@ async def url_ytmusic_page(request: Request): class YtRequest(BaseModel): url: str -COBALT_FALLBACK_URLS = [ - "https://api.cobalt.tools/", - "https://co.wukko.me/" -] + +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): + """ + Proxy Cobalt tunnel requests through the same Render service. + + Cobalt itself listens only on 127.0.0.1:9000, while this FastAPI + application is the only publicly exposed process. + """ + 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=None, + ) + + try: + upstream = await client.send( + client.build_request( + request.method, + cobalt_url, + headers=headers, + ), + stream=True, + ) + except httpx.RequestError as exc: + await client.aclose() + raise HTTPException( + status_code=502, + detail=f"Error connecting to Cobalt tunnel: {exc}", + ) + + response_headers = {} + + for header in ( + "content-type", + "content-length", + "content-range", + "accept-ranges", + "content-disposition", + "cache-control", + "etag", + "last-modified", + "estimated-content-length", + ): + value = upstream.headers.get(header) + if value: + response_headers[header] = value + + async def stream_cobalt(): + try: + async for chunk in upstream.aiter_raw(): + yield chunk + finally: + await client.aclose() + + return StreamingResponse( + stream_cobalt(), + status_code=upstream.status_code, + headers=response_headers, + ) + @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.") + raise HTTPException( + status_code=400, + detail="Missing YouTube URL.", + ) - # Determine which instance URL to use - user_defined_api = os.getenv("COBALT_API_URL") - - # If the user defines an API URL, strictly use it. Otherwise, attempt fallbacks - # in case community instances go offline (DNS/NXDOMAIN errors). - instances_to_try = [user_defined_api] if user_defined_api else COBALT_FALLBACK_URLS + 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" + "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}" @@ -349,57 +438,103 @@ async def yt_download_cobalt(request: Request, body: YtRequest): "url": url, "downloadMode": "audio", "audioFormat": "mp3", - "filenameStyle": "basic" + "audioBitrate": "128", + "filenameStyle": "basic", } - last_error = "Could not reach any Cobalt API instances." - - for instance in instances_to_try: - if not instance.endswith("/"): - instance += "/" - - async with httpx.AsyncClient(timeout=30.0) as client: - try: - res = await client.post(instance, json=payload, headers=headers) - - # Non-200 could be bot protection, ratelimits, or API validation errors. - if res.status_code != 200: - err_text = res.text - try: - data = res.json() - if "error" in data and "code" in data["error"]: - err_text = data["error"]["code"] - else: - err_text = data.get("text", err_text) - except Exception: - if len(err_text) > 200: - err_text = err_text[:200] + "..." - - last_error = f"API error ({res.status_code}): {err_text}" - # Do not fallback if the instance actually responded with an auth/format error - if res.status_code in (400, 401, 403): - raise HTTPException(status_code=400, detail=f"Cobalt returned an error: {err_text}") - continue - - data = res.json() - status = data.get("status") - - if status == "error": - err_code = data.get("error", {}).get("code", "Unknown Cobalt error") - raise HTTPException(status_code=400, detail=f"Cobalt error: {err_code}") - - dl_url = data.get("url") - if not dl_url: - raise HTTPException(status_code=500, detail=f"Cobalt succeeded but returned no download link. (Status: {status})") - - return {"url": dl_url, "title": "Audio"} - - except (httpx.RequestError, socket.gaierror) as exc: - last_error = f"Failed to connect to {instance}: {exc}" - continue # Try the next instance in the list - - # If the loop finishes without returning, all instances failed - raise HTTPException(status_code=502, detail=last_error) + 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", {}) + + if isinstance(error, dict): + error_code = error.get( + "code", + "Unknown Cobalt error", + ) + else: + error_code = str(error) + + raise HTTPException( + status_code=400, + detail=f"Cobalt error: {error_code}", + ) + + 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})" + ), + ) + + # Cobalt may return its own tunnel URL. + # Convert it to our public FastAPI proxy URL. + 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}", + ) + # --------------------------------------------------------------------------------- diff --git a/render.yaml b/render.yaml @@ -6,9 +6,18 @@ services: envVars: - key: HF_REPO_ID value: notamitgamer/cdn + - key: HF_TOKEN sync: false + - key: RAW_DOMAIN 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" +\ No newline at end of file diff --git a/start.sh b/start.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e + +echo "Starting Cobalt..." + +cd /opt/cobalt/api + +API_URL="${COBALT_API_URL:-https://cdn.amit.is-a.dev/cobalt/}" +API_PORT="${COBALT_PORT:-9000}" + +export API_URL +export API_PORT +export API_LISTEN_ADDRESS="127.0.0.1" + +pnpm start & +COBALT_PID=$! + +echo "Cobalt started with PID ${COBALT_PID}" + +cd /app + +echo "Starting FastAPI..." + +exec uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8000}" +\ No newline at end of file