Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -7,8 +7,11 @@ Run with:
|
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
import asyncio
|
|
|
|
| 10 |
import json
|
|
|
|
| 11 |
import re
|
|
|
|
| 12 |
import tempfile
|
| 13 |
from pathlib import Path
|
| 14 |
from typing import Final
|
|
@@ -18,6 +21,7 @@ from uuid import uuid4
|
|
| 18 |
from fastapi import FastAPI, Form
|
| 19 |
from fastapi.responses import FileResponse, HTMLResponse
|
| 20 |
from starlette.background import BackgroundTask
|
|
|
|
| 21 |
from playwright.async_api import async_playwright
|
| 22 |
|
| 23 |
SAVE_TT_HOME: Final = "https://savett.cc/en1/"
|
|
@@ -31,10 +35,54 @@ app = FastAPI(title="TikTok MP4 Downloader", docs_url=None, redoc_url=None)
|
|
| 31 |
download_lock = asyncio.Lock()
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
def render_home(error: str | None = None) -> str:
|
| 35 |
error_markup = ""
|
| 36 |
if error:
|
| 37 |
-
error_markup = f'<div class="notice error" role="alert">{error}</div>'
|
| 38 |
|
| 39 |
return f"""<!doctype html>
|
| 40 |
<html lang="en">
|
|
@@ -151,7 +199,12 @@ async def fetch_mp4(tiktok_url: str) -> tuple[Path, str]:
|
|
| 151 |
output_path = TMP_DIR / f"tiktok_{video_id}_{uuid4().hex[:8]}.mp4"
|
| 152 |
|
| 153 |
async with async_playwright() as playwright:
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
context = await browser.new_context(
|
| 156 |
user_agent=(
|
| 157 |
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
|
@@ -238,6 +291,8 @@ async def download(tiktok_url: str = Form(...)):
|
|
| 238 |
try:
|
| 239 |
async with download_lock:
|
| 240 |
file_path, download_name = await fetch_mp4(validated_url)
|
|
|
|
|
|
|
| 241 |
except Exception as exc:
|
| 242 |
return HTMLResponse(render_home(f"Download failed: {exc}"), status_code=502)
|
| 243 |
|
|
|
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
import asyncio
|
| 10 |
+
from html import escape
|
| 11 |
import json
|
| 12 |
+
import os
|
| 13 |
import re
|
| 14 |
+
import shutil
|
| 15 |
import tempfile
|
| 16 |
from pathlib import Path
|
| 17 |
from typing import Final
|
|
|
|
| 21 |
from fastapi import FastAPI, Form
|
| 22 |
from fastapi.responses import FileResponse, HTMLResponse
|
| 23 |
from starlette.background import BackgroundTask
|
| 24 |
+
from playwright.async_api import Error as PlaywrightError
|
| 25 |
from playwright.async_api import async_playwright
|
| 26 |
|
| 27 |
SAVE_TT_HOME: Final = "https://savett.cc/en1/"
|
|
|
|
| 35 |
download_lock = asyncio.Lock()
|
| 36 |
|
| 37 |
|
| 38 |
+
class BrowserRuntimeError(RuntimeError):
|
| 39 |
+
"""Raised when Chromium is unavailable in the process running FastAPI."""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def chromium_launch_options() -> dict[str, object]:
|
| 43 |
+
"""Prefer an explicit Chromium path, then common system browser locations."""
|
| 44 |
+
configured_path = os.environ.get("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH")
|
| 45 |
+
if configured_path:
|
| 46 |
+
browser_path = Path(configured_path)
|
| 47 |
+
if not browser_path.is_file():
|
| 48 |
+
raise BrowserRuntimeError(
|
| 49 |
+
"PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH is set, but its file does not exist. "
|
| 50 |
+
"Set it to a valid Chromium executable or remove the variable."
|
| 51 |
+
)
|
| 52 |
+
executable_path = str(browser_path)
|
| 53 |
+
else:
|
| 54 |
+
executable_path = next(
|
| 55 |
+
(
|
| 56 |
+
path
|
| 57 |
+
for name in ("chromium", "chromium-browser", "google-chrome", "google-chrome-stable")
|
| 58 |
+
if (path := shutil.which(name))
|
| 59 |
+
),
|
| 60 |
+
None,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
options: dict[str, object] = {"headless": True}
|
| 64 |
+
if executable_path:
|
| 65 |
+
options["executable_path"] = executable_path
|
| 66 |
+
|
| 67 |
+
# System Chromium generally refuses to start as root unless this flag is used.
|
| 68 |
+
# Prefer running the service as an unprivileged user in production.
|
| 69 |
+
if os.name != "nt" and hasattr(os, "geteuid") and os.geteuid() == 0:
|
| 70 |
+
options["args"] = ["--no-sandbox", "--disable-dev-shm-usage"]
|
| 71 |
+
return options
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def browser_install_message() -> str:
|
| 75 |
+
return (
|
| 76 |
+
"Chromium is not available to the FastAPI process. Install it during deployment with "
|
| 77 |
+
"`python -m playwright install chromium`, or set "
|
| 78 |
+
"PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH to a valid system Chromium binary."
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
def render_home(error: str | None = None) -> str:
|
| 83 |
error_markup = ""
|
| 84 |
if error:
|
| 85 |
+
error_markup = f'<div class="notice error" role="alert">{escape(error)}</div>'
|
| 86 |
|
| 87 |
return f"""<!doctype html>
|
| 88 |
<html lang="en">
|
|
|
|
| 199 |
output_path = TMP_DIR / f"tiktok_{video_id}_{uuid4().hex[:8]}.mp4"
|
| 200 |
|
| 201 |
async with async_playwright() as playwright:
|
| 202 |
+
try:
|
| 203 |
+
browser = await playwright.chromium.launch(**chromium_launch_options())
|
| 204 |
+
except PlaywrightError as exc:
|
| 205 |
+
if "Executable doesn't exist" in str(exc):
|
| 206 |
+
raise BrowserRuntimeError(browser_install_message()) from exc
|
| 207 |
+
raise
|
| 208 |
context = await browser.new_context(
|
| 209 |
user_agent=(
|
| 210 |
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
|
|
|
| 291 |
try:
|
| 292 |
async with download_lock:
|
| 293 |
file_path, download_name = await fetch_mp4(validated_url)
|
| 294 |
+
except BrowserRuntimeError as exc:
|
| 295 |
+
return HTMLResponse(render_home(str(exc)), status_code=503)
|
| 296 |
except Exception as exc:
|
| 297 |
return HTMLResponse(render_home(f"Download failed: {exc}"), status_code=502)
|
| 298 |
|