factorstudios commited on
Commit
3fe0ae5
·
verified ·
1 Parent(s): 1ca39ac

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +57 -2
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
- browser = await playwright.chromium.launch(headless=True)
 
 
 
 
 
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