I'm building a YouTube video info. fetcher using Remix, Node.js, and yt-dlp. It works locally, but fails in Docker with several issues (see output below):
- Python not found when trying to use yt-dlp via Python
- YouTube API returns "UNPLAYABLE" content status
- Fallback to youtube-dl-exec is too slow
Output
GET /?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DZyF-pmNfnH0 200 - - 17100.357 msPlayer API response for ZyF-pmNfnH0 (attempt 1): {"status": 200,"hasStreamingData": false,"playabilityStatus": "UNPLAYABLE","reason": "This content isn't available."}Python yt-dlp error: Error: Command failed: python /app/yt_dlp_temp.py "https://www.youtube.com/watch?v=ZyF-pmNfnH0"/bin/sh: 1: python: not foundWhat I've tried
- Different YouTube API endpoints (ANDROID, WEB)
- Multiple fallback strategies
- Various yt-dlp configurations
Questions
- How can I fix the "python not found" error in Docker?
- Why does the YouTube API return UNPLAYABLE status when the video is available?
- How can I improve performance to match sites like y2mate?
Code
api.info.ts (main route handler):
// app/routes/api.info.tsimport { json, type LoaderFunctionArgs } from "@remix-run/node";import youtubedl from "youtube-dl-exec";import { fetchWithPythonYtDlp } from "~/lib/yt-dlp-wrapper";export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const videoUrl = url.searchParams.get("url"); try { const videoId = extractVideoId(videoUrl); let info; try { info = await fetchVideoInfo(videoId); if (info.formats.length === 0) throw new Error(`Format array is empty`); } catch (error) { console.warn(`Player API failed, falling back to yt-dlp:`, error); try { info = await fetchWithPythonYtDlp(videoUrl); } catch (error) { console.warn(`Falling back to youtube-dl-exec:`, error); info = await fetchVideoInfoFallback(videoId, videoUrl); } } return json({ success: true, data: info }); } catch (error) { return json({ error: "Failed to fetch video info" }, { status: 500 }); }}yt-dlp-wrapper.ts (Python fallback):
// ~/lib/yt-dlp-wrapper.tsimport { exec } from "child_process";import { promisify } from "util";import fs from "fs";const execPromise = promisify(exec);export async function fetchWithPythonYtDlp(videoUrl: string) { const pythonScript = `import yt_dlpimport jsonimport sysvideo_url = sys.argv[1]ydl_opts = {'quiet': True, 'skip_download': True}with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(video_url, download=False)print(json.dumps(info))`; fs.writeFileSync("yt_dlp_temp.py", pythonScript); const { stdout } = await execPromise(`python yt_dlp_temp.py "${videoUrl}"`); return JSON.parse(stdout);}Dockerfile
# ---------- Stage 1: Build ----------FROM node:18-bullseye-slim AS builder# Install yt-dlp for build phase (if needed)RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*RUN pip3 install --no-cache-dir yt-dlp# Set working directoryWORKDIR /app# Copy and install dependenciesCOPY package*.json ./RUN npm install# Copy all source filesCOPY . .# Build Remix appRUN npm run build# ---------- Stage 2: Runtime ----------FROM node:18-bullseye-slim# Install only runtime dependenciesRUN apt-get update && \ apt-get install -y ffmpeg python3 python3-pip && \ rm -rf /var/lib/apt/lists/*# Install yt-dlp (needed at runtime)RUN pip3 install --no-cache-dir yt-dlp# Set working directoryWORKDIR /app# Copy package files and install only prod depsCOPY package*.json ./RUN npm install --omit=dev# Copy only built + required app files from builder stageCOPY --from=builder /app/build ./buildCOPY --from=builder /app/public ./publicCOPY --from=builder /app/app ./appCOPY --from=builder /app/remix.config.js ./remix.config.js# Expose port and start the appEXPOSE 3000CMD ["npm", "run", "start"]