Spaces:
Running
Running
| import os | |
| import re | |
| import uuid | |
| import asyncio | |
| import logging | |
| from fastapi import FastAPI, Query, HTTPException | |
| from fastapi.responses import FileResponse, JSONResponse | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = FastAPI() | |
| TIKTOK_URL_RE = re.compile( | |
| r'https?://(?:www\.|vm\.|vt\.)?tiktok\.com/\S+', re.IGNORECASE | |
| ) | |
| INSTAGRAM_URL_RE = re.compile( | |
| r'https?://(?:www\.)?instagram\.com/(?:reel|p)/\S+', re.IGNORECASE | |
| ) | |
| TMP_DIR = "/tmp/tiktok" | |
| os.makedirs(TMP_DIR, exist_ok=True) | |
| async def download(url: str = Query(...)): | |
| if not TIKTOK_URL_RE.match(url): | |
| raise HTTPException(status_code=400, detail="Invalid TikTok URL") | |
| job_id = uuid.uuid4().hex | |
| out_tmpl = f"{TMP_DIR}/{job_id}.%(ext)s" | |
| cmd = [ | |
| "yt-dlp", | |
| "--no-warnings", | |
| "-f", "bestvideo+bestaudio/best", | |
| "--merge-output-format", "mp4", | |
| "-o", out_tmpl, | |
| url, | |
| ] | |
| logger.info(f"Running: {' '.join(cmd)}") | |
| proc = await asyncio.create_subprocess_exec( | |
| *cmd, | |
| stdout=asyncio.subprocess.PIPE, | |
| stderr=asyncio.subprocess.PIPE, | |
| ) | |
| try: | |
| stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=60) | |
| except asyncio.TimeoutError: | |
| proc.kill() | |
| raise HTTPException(status_code=504, detail="Timeout") | |
| stdout_str = stdout.decode(errors="ignore").strip() | |
| stderr_str = stderr.decode(errors="ignore").strip() | |
| logger.info(f"yt-dlp stdout: {stdout_str}") | |
| logger.error(f"yt-dlp stderr: {stderr_str}") | |
| logger.info(f"yt-dlp returncode: {proc.returncode}") | |
| if proc.returncode != 0: | |
| if "Unsupported URL" in stderr_str and "/photo/" in stdout_str: | |
| raise HTTPException(status_code=415, detail="Photo post") | |
| raise HTTPException(status_code=502, detail=stderr_str) | |
| files = [f for f in os.listdir(TMP_DIR) if f.startswith(job_id)] | |
| if not files: | |
| raise HTTPException(status_code=502, detail="File not found after download") | |
| filepath = os.path.join(TMP_DIR, files[0]) | |
| async def cleanup(): | |
| try: | |
| os.remove(filepath) | |
| except Exception: | |
| pass | |
| return FileResponse( | |
| filepath, | |
| media_type="video/mp4", | |
| filename="video.mp4", | |
| background=cleanup, | |
| ) | |
| async def download_insta(url: str = Query(...)): | |
| if not INSTAGRAM_URL_RE.match(url): | |
| raise HTTPException(status_code=400, detail="Invalid Instagram URL") | |
| job_id = uuid.uuid4().hex | |
| out_dir = f"{TMP_DIR}/{job_id}" | |
| os.makedirs(out_dir, exist_ok=True) | |
| cmd = [ | |
| "gallery-dl", | |
| "--no-mtime", | |
| "-D", out_dir, | |
| url, | |
| ] | |
| logger.info(f"Running: {' '.join(cmd)}") | |
| proc = await asyncio.create_subprocess_exec( | |
| *cmd, | |
| stdout=asyncio.subprocess.PIPE, | |
| stderr=asyncio.subprocess.PIPE, | |
| ) | |
| try: | |
| stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=60) | |
| except asyncio.TimeoutError: | |
| proc.kill() | |
| import os | |
| import re | |
| import uuid | |
| import shutil | |
| import asyncio | |
| import logging | |
| from fastapi import FastAPI, Query, HTTPException | |
| from fastapi.responses import FileResponse, JSONResponse | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = FastAPI() | |
| TIKTOK_URL_RE = re.compile( | |
| r'https?://(?:www\.|vm\.|vt\.)?tiktok\.com/\S+', re.IGNORECASE | |
| ) | |
| INSTAGRAM_URL_RE = re.compile( | |
| r'https?://(?:www\.)?instagram\.com/\S+', re.IGNORECASE | |
| ) | |
| TMP_DIR = "/tmp/tiktok" | |
| os.makedirs(TMP_DIR, exist_ok=True) | |
| async def download(url: str = Query(...)): | |
| # Если инста — редиректим на логику gallery-dl | |
| if INSTAGRAM_URL_RE.match(url): | |
| return await _download_insta(url) | |
| if not TIKTOK_URL_RE.match(url): | |
| raise HTTPException(status_code=400, detail="Invalid URL") | |
| job_id = uuid.uuid4().hex | |
| out_tmpl = f"{TMP_DIR}/{job_id}.%(ext)s" | |
| cmd = [ | |
| "yt-dlp", | |
| "--no-warnings", | |
| "-f", "bestvideo+bestaudio/best", | |
| "--merge-output-format", "mp4", | |
| "-o", out_tmpl, | |
| url, | |
| ] | |
| logger.info(f"Running: {' '.join(cmd)}") | |
| proc = await asyncio.create_subprocess_exec( | |
| *cmd, | |
| stdout=asyncio.subprocess.PIPE, | |
| stderr=asyncio.subprocess.PIPE, | |
| ) | |
| try: | |
| stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=60) | |
| except asyncio.TimeoutError: | |
| proc.kill() | |
| raise HTTPException(status_code=504, detail="Timeout") | |
| stdout_str = stdout.decode(errors="ignore").strip() | |
| stderr_str = stderr.decode(errors="ignore").strip() | |
| logger.info(f"yt-dlp stdout: {stdout_str}") | |
| logger.error(f"yt-dlp stderr: {stderr_str}") | |
| logger.info(f"yt-dlp returncode: {proc.returncode}") | |
| if proc.returncode != 0: | |
| if "Unsupported URL" in stderr_str and "/photo/" in stdout_str: | |
| raise HTTPException(status_code=415, detail="Photo post") | |
| raise HTTPException(status_code=502, detail=stderr_str) | |
| files = [f for f in os.listdir(TMP_DIR) if f.startswith(job_id)] | |
| if not files: | |
| raise HTTPException(status_code=502, detail="File not found after download") | |
| filepath = os.path.join(TMP_DIR, files[0]) | |
| async def cleanup(): | |
| try: | |
| os.remove(filepath) | |
| except Exception: | |
| pass | |
| return FileResponse( | |
| filepath, | |
| media_type="video/mp4", | |
| filename="video.mp4", | |
| background=cleanup, | |
| ) | |
| async def download_insta(url: str = Query(...)): | |
| if not INSTAGRAM_URL_RE.match(url): | |
| raise HTTPException(status_code=400, detail="Invalid Instagram URL") | |
| return await _download_insta(url) | |
| async def _download_insta(url: str): | |
| job_id = uuid.uuid4().hex | |
| out_dir = f"{TMP_DIR}/{job_id}" | |
| os.makedirs(out_dir, exist_ok=True) | |
| cmd = [ | |
| "gallery-dl", | |
| "--no-mtime", | |
| "-D", out_dir, | |
| url, | |
| ] | |
| logger.info(f"Running: {' '.join(cmd)}") | |
| proc = await asyncio.create_subprocess_exec( | |
| *cmd, | |
| stdout=asyncio.subprocess.PIPE, | |
| stderr=asyncio.subprocess.PIPE, | |
| ) | |
| import os | |
| import re | |
| import uuid | |
| import shutil | |
| import asyncio | |
| import logging | |
| from fastapi import FastAPI, Query, HTTPException | |
| from fastapi.responses import FileResponse, JSONResponse | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = FastAPI() | |
| TIKTOK_URL_RE = re.compile( | |
| r'https?://(?:www\.|vm\.|vt\.)?tiktok\.com/\S+', re.IGNORECASE | |
| ) | |
| INSTAGRAM_URL_RE = re.compile( | |
| r'https?://(?:www\.)?instagram\.com/\S+', re.IGNORECASE | |
| ) | |
| TMP_DIR = "/tmp/tiktok" | |
| os.makedirs(TMP_DIR, exist_ok=True) | |
| async def download(url: str = Query(...)): | |
| if INSTAGRAM_URL_RE.match(url): | |
| return await _download_insta(url) | |
| if not TIKTOK_URL_RE.match(url): | |
| raise HTTPException(status_code=400, detail="Invalid URL") | |
| job_id = uuid.uuid4().hex | |
| out_tmpl = f"{TMP_DIR}/{job_id}.%(ext)s" | |
| cmd = [ | |
| "yt-dlp", | |
| "--no-warnings", | |
| "-f", "bestvideo+bestaudio/best", | |
| "--merge-output-format", "mp4", | |
| "-o", out_tmpl, | |
| url, | |
| ] | |
| logger.info(f"Running: {' '.join(cmd)}") | |
| proc = await asyncio.create_subprocess_exec( | |
| *cmd, | |
| stdout=asyncio.subprocess.PIPE, | |
| stderr=asyncio.subprocess.PIPE, | |
| ) | |
| try: | |
| stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=60) | |
| except asyncio.TimeoutError: | |
| proc.kill() | |
| raise HTTPException(status_code=504, detail="Timeout") | |
| stdout_str = stdout.decode(errors="ignore").strip() | |
| stderr_str = stderr.decode(errors="ignore").strip() | |
| logger.info(f"yt-dlp stdout: {stdout_str}") | |
| logger.error(f"yt-dlp stderr: {stderr_str}") | |
| logger.info(f"yt-dlp returncode: {proc.returncode}") | |
| if proc.returncode != 0: | |
| if "Unsupported URL" in stderr_str and "/photo/" in stdout_str: | |
| raise HTTPException(status_code=415, detail="Photo post") | |
| raise HTTPException(status_code=502, detail=stderr_str) | |
| files = [f for f in os.listdir(TMP_DIR) if f.startswith(job_id)] | |
| if not files: | |
| raise HTTPException(status_code=502, detail="File not found after download") | |
| filepath = os.path.join(TMP_DIR, files[0]) | |
| async def cleanup(): | |
| try: | |
| os.remove(filepath) | |
| except Exception: | |
| pass | |
| return FileResponse( | |
| filepath, | |
| media_type="video/mp4", | |
| filename="video.mp4", | |
| background=cleanup, | |
| ) | |
| async def download_insta(url: str = Query(...)): | |
| if not INSTAGRAM_URL_RE.match(url): | |
| raise HTTPException(status_code=400, detail="Invalid Instagram URL") | |
| return await _download_insta(url) | |
| async def _download_insta(url: str): | |
| job_id = uuid.uuid4().hex | |
| out_tmpl = f"{TMP_DIR}/{job_id}.%(ext)s" | |
| cmd = [ | |
| "yt-dlp", | |
| "--no-warnings", | |
| "--no-check-certificate", | |
| "-f", "bestvideo+bestaudio/best", | |
| "--merge-output-format", "mp4", | |
| "--add-header", "User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", | |
| "--add-header", "Accept-Language:en-US,en;q=0.9", | |
| "-o", out_tmpl, | |
| url, | |
| ] | |
| logger.info(f"Running insta: {' '.join(cmd)}") | |
| proc = await asyncio.create_subprocess_exec( | |
| *cmd, | |
| stdout=asyncio.subprocess.PIPE, | |
| stderr=asyncio.subprocess.PIPE, | |
| ) | |
| try: | |
| stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=60) | |
| except asyncio.TimeoutError: | |
| proc.kill() | |
| raise HTTPException(status_code=504, detail="Timeout") | |
| stdout_str = stdout.decode(errors="ignore").strip() | |
| stderr_str = stderr.decode(errors="ignore").strip() | |
| logger.info(f"yt-dlp insta stdout: {stdout_str}") | |
| logger.error(f"yt-dlp insta stderr: {stderr_str}") | |
| logger.info(f"yt-dlp insta returncode: {proc.returncode}") | |
| if proc.returncode != 0: | |
| raise HTTPException(status_code=502, detail=stderr_str or "yt-dlp failed") | |
| files = [f for f in os.listdir(TMP_DIR) if f.startswith(job_id)] | |
| if not files: | |
| raise HTTPException(status_code=502, detail="File not found after download") | |
| filepath = os.path.join(TMP_DIR, files[0]) | |
| async def cleanup(): | |
| try: | |
| os.remove(filepath) | |
| except Exception: | |
| pass | |
| return FileResponse( | |
| filepath, | |
| media_type="video/mp4", | |
| filename="video.mp4", | |
| background=cleanup, | |
| ) | |
| async def root(): | |
| return JSONResponse({"status": "ok", "usage": "GET /download?url=<tiktok_or_instagram_url>"}) | |