import os import uuid import asyncio import subprocess import shutil from fastapi import FastAPI, Query, HTTPException from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="M3U8 to MP4 Converter", version="1.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) TEMP_DIR = "/tmp/m3u8_conversions" os.makedirs(TEMP_DIR, exist_ok=True) active_conversions = {} def cleanup_files(file_paths): for path in file_paths: try: if os.path.exists(path): if os.path.isdir(path): shutil.rmtree(path) else: os.remove(path) logger.info(f"Cleaned up: {path}") except Exception as e: logger.error(f"Cleanup error for {path}: {e}") def get_ffmpeg_path(): ffmpeg_path = shutil.which("ffmpeg") if ffmpeg_path: return ffmpeg_path for path in ["/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg", "/app/ffmpeg", "./ffmpeg"]: if os.path.exists(path): return path return "ffmpeg" async def convert_m3u8_to_mp4(m3u8_url: str, output_path: str, task_id: str): ffmpeg = get_ffmpeg_path() cmd = [ ffmpeg, "-i", m3u8_url, "-c", "copy", "-bsf:a", "aac_adtstoasc", "-movflags", "+faststart", "-y", output_path ] cmd_fallback = [ ffmpeg, "-i", m3u8_url, "-c:v", "libx264", "-preset", "fast", "-crf", "23", "-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart", "-y", output_path ] logger.info(f"Starting conversion: {task_id}") logger.info(f"Command: {' '.join(cmd)}") active_conversions[task_id] = {"status": "converting", "progress": 0} try: process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() if process.returncode != 0: logger.warning(f"Copy mode failed, trying re-encode mode: {stderr.decode()[:500]}") active_conversions[task_id]["status"] = "re-encoding" process = await asyncio.create_subprocess_exec( *cmd_fallback, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() if process.returncode != 0: raise Exception(f"Conversion failed: {stderr.decode()[:1000]}") active_conversions[task_id]["status"] = "completed" logger.info(f"Conversion completed: {task_id}") except Exception as e: active_conversions[task_id]["status"] = "failed" active_conversions[task_id]["error"] = str(e) logger.error(f"Conversion error: {e}") raise @app.get("/") def root(): return { "message": "M3U8 to MP4 Converter API", "usage": "/video?url=&format=mp4", "endpoints": { "convert": "/video?url=&format=mp4", "status": "/status/" } } @app.get("/video") async def convert_video( url: str = Query(..., description="M3U8 URL to convert"), format: str = Query("mp4", description="Output format (mp4 only)") ): if not url: raise HTTPException(status_code=400, detail="URL parameter is required") if not url.startswith(("http://", "https://")): raise HTTPException(status_code=400, detail="Invalid URL format") task_id = str(uuid.uuid4()) output_filename = f"video_{task_id}.mp4" output_path = os.path.join(TEMP_DIR, output_filename) try: await convert_m3u8_to_mp4(url, output_path, task_id) if not os.path.exists(output_path): raise HTTPException(status_code=500, detail="Conversion failed - output file not created") file_size = os.path.getsize(output_path) logger.info(f"File size: {file_size} bytes") response = FileResponse( path=output_path, media_type="video/mp4", filename=f"converted_video_{task_id}.mp4", headers={ "Content-Disposition": f'attachment; filename="converted_video_{task_id}.mp4"' } ) @response.background def cleanup(): cleanup_files([output_path]) return response except HTTPException: cleanup_files([output_path]) raise except Exception as e: cleanup_files([output_path]) logger.error(f"Error: {e}") raise HTTPException(status_code=500, detail=f"Conversion error: {str(e)}") @app.get("/status/{task_id}") def get_status(task_id: str): if task_id not in active_conversions: return {"status": "not_found"} return active_conversions[task_id] @app.get("/health") def health_check(): ffmpeg = get_ffmpeg_path() ffmpeg_available = os.path.exists(ffmpeg) or shutil.which("ffmpeg") is not None return { "status": "healthy", "ffmpeg_available": ffmpeg_available, "ffmpeg_path": ffmpeg } if __name__ == "__main__": import uvicorn port = int(os.environ.get("PORT", 7860)) uvicorn.run(app, host="0.0.0.0", port=port)