Igbalode/mp4-mp3
0
1"""2FastAPI service that:31. Accepts any public video URL (POST /convert)42. Converts it to MP3 using yt-dlp + FFmpeg53. Returns a permanent MP3 URL64. Provides a tiny HTML page (GET /download)75. Generates a customizable QR code with a ✔️ icon embedded8"""9 10import logging11import os12import uuid13import tempfile14from io import BytesIO15from pathlib import Path16from urllib.parse import quote_plus17from typing import Optional18 19import httpx20import qrcode21from qrcode.image.styledpil import StyledPilImage22from qrcode.image.styles.moduledrawers import RoundedModuleDrawer23from qrcode.image.styles.colormasks import SolidFillColorMask24from PIL import Image, ImageDraw25from fastapi import FastAPI, HTTPException, Query26from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse27from fastapi.staticfiles import StaticFiles28from pydantic import BaseModel, HttpUrl29 30logging.basicConfig(level=logging.INFO)31logger = logging.getLogger("uvicorn")32 33STATIC_DIR = Path(__file__).with_suffix("").parent / "static"34STATIC_DIR.mkdir(exist_ok=True)35 36servers = [{"url": "https://igbalode-mp4-mp3.hf.space"}]37 38app = FastAPI(39 title="Video-to-MP3 API",40 description="Convert any public video to MP3 and grab it via a download page.",41 version="1.5.0",42 servers=servers,43)44 45app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")46 47 48class ConvertRequest(BaseModel):49 url: HttpUrl50 51 52class ConvertResponse(BaseModel):53 mp3_url: str54 filename: str55 56 57# ---------- Downloader ----------58def _download_audio(url: str) -> bytes:59 """Use yt-dlp to pull the best audio stream."""60 ydl_opts = {61 "format": "bestaudio/best",62 "outtmpl": "-",63 "quiet": True,64 "noplaylist": True,65 }66 with yt_dlp.YoutubeDL(ydl_opts) as ydl:67 info = ydl.extract_info(url, download=False)68 audio_url = info["url"]69 r = httpx.get(audio_url, timeout=120, follow_redirects=True)70 r.raise_for_status()71 if len(r.content) > 2_000_000_000:72 raise HTTPException(413, "Audio stream too large (>2 GB)")73 return r.content74 75 76# ---------- Converter ----------77def _convert_to_mp3(audio_bytes: bytes) -> bytes:78 import subprocess79 with tempfile.NamedTemporaryFile(delete=False, suffix=".webm") as tmp_in, \80 tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_out:81 tmp_in.write(audio_bytes)82 tmp_in.flush()83 subprocess.run(84 ["ffmpeg", "-i", tmp_in.name, "-vn", "-acodec", "libmp3lame", "-q:a", "2", tmp_out.name],85 check=True,86 stdout=subprocess.PIPE,87 stderr=subprocess.PIPE,88 )89 mp3_bytes = Path(tmp_out.name).read_bytes()90 Path(tmp_in.name).unlink(missing_ok=True)91 Path(tmp_out.name).unlink(missing_ok=True)92 return mp3_bytes93 94 95# ---------- Main endpoint ----------96@app.post("/convert", response_model=ConvertResponse)97def convert_video(req: ConvertRequest):98 logger.info("Convert request: %s", str(req.url))99 try:100 audio_bytes = _download_audio(str(req.url))101 except Exception as e:102 logger.exception("Download error")103 raise HTTPException(400, f"Download error: {e}")104 105 try:106 mp3_bytes = _convert_to_mp3(audio_bytes)107 except Exception as e:108 logger.exception("Conversion error")109 raise HTTPException(500, f"Conversion failed: {e}")110 111 filename = f"{uuid.uuid4().hex}.mp3"112 (STATIC_DIR / filename).write_bytes(mp3_bytes)113 114 mp3_url = f"https://igbalode-mp4-mp3.hf.space/static/{filename}"115 return ConvertResponse(mp3_url=mp3_url, filename=filename)116 117 118# ---------- Download page ----------119@app.get("/download", response_class=HTMLResponse)120def get_download_page(url: str = Query(..., description="Public MP3 URL")):121 safe_url = quote_plus(url, safe="/:")122 html = f"""123 <!DOCTYPE html>124 <html lang="en">125 <head>126 <meta charset="UTF-8"/>127 <title>Download MP3</title>128 <style>129 body{{font-family:system-ui,sans-serif;background:#f5f5f5;text-align:center;padding-top:10vh}}130 h1{{margin-bottom:1rem}}131 audio{{width:90%;max-width:400px}}132 .btn{{display:inline-block;margin-top:1rem;padding:.75rem 1.5rem;background:#0d6efd;color:white;border-radius:6px;text-decoration:none;font-weight:bold}}133 .btn:hover{{background:#0b5ed7}}134 </style>135 </head>136 <body>137 <h1>Your MP3 is ready</h1>138 <audio controls>139 <source src="{safe_url}" type="audio/mpeg"/>140 </audio><br/>141 <a class="btn" href="{safe_url}" download>Download MP3</a>142 </body>143 </html>144 """145 return html146 147 148# ---------- QR code with embedded ✔️ icon ----------149@app.get("/qr")150def qr_code(151 url: str = Query(..., description="Public MP3 URL"),152 size: int = Query(10, ge=4, le=40, description="Module pixel size"),153 border: int = Query(4, ge=0, le=20, description="Border thickness"),154 fg: str = Query("black", description="Foreground color"),155 bg: str = Query("white", description="Background color"),156):157 """PNG QR code with rounded modules and a ✔️ icon in center."""158 qr = qrcode.QRCode(159 error_correction=qrcode.constants.ERROR_CORRECT_H,160 box_size=size,161 border=border,162 )163 qr.add_data(url)164 qr.make(fit=True)165 166 # Base QR167 img = qr.make_image(168 image_factory=StyledPilImage,169 module_drawer=RoundedModuleDrawer(),170 color_mask=SolidFillColorMask(front_color=fg, back_color=bg),171 )172 173 # Add tiny ✔️ icon in center174 icon_size = int(img.size[0] * 0.18)175 icon = Image.new("RGBA", (icon_size, icon_size), (0, 0, 0, 0))176 draw = ImageDraw.Draw(icon)177 # Simple green tick178 draw.line([(0, icon_size // 2), (icon_size // 3, icon_size)], fill="green", width=3)179 draw.line([(icon_size // 3, icon_size), (icon_size, 0)], fill="green", width=3)180 181 pos = ((img.size[0] - icon_size) // 2, (img.size[1] - icon_size) // 2)182 img.paste(icon, pos, icon)183 184 buf = BytesIO()185 img.save(buf, format="PNG")186 buf.seek(0)187 return StreamingResponse(buf, media_type="image/png")188 189 190@app.get("/", include_in_schema=False)191def root():192 return RedirectResponse("/docs")