factorstudios commited on
Commit
0443b0a
·
verified ·
1 Parent(s): e8b8a99

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +241 -478
app.py CHANGED
@@ -1,486 +1,249 @@
1
- #!/usr/bin/env python3
2
- """
3
- bootloader.py - Mac OS 15 Sequoia Bootloader
4
- Uses download URL as bootable USB and JSON as storage disk for installation.
5
  """
6
 
7
- import requests
8
- import duckdb
 
9
  import json
10
- import hashlib
11
- import os
12
- import shutil
13
- import subprocess
14
  import tempfile
15
- import tarfile
16
- import zipfile
17
- from datetime import datetime
18
  from pathlib import Path
19
- from fastapi import FastAPI, Request, Response, HTTPException, Depends, status
20
- from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse
21
- from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
22
- from fastapi.staticfiles import StaticFiles
23
- from fastapi.templating import Jinja2Templates
24
-
25
-
26
- app = FastAPI()
27
-
28
- class MacOSBootloader:
29
- def __init__(self, iso_url, storage_json_url):
30
- self.iso_url = iso_url
31
- self.storage_json_url = storage_json_url
32
- self.temp_dir = tempfile.mkdtemp(prefix="macos_sequoia_")
33
-
34
- # Bootable USB (download URL) mount point
35
- self.usb_mount_point = os.path.join(self.temp_dir, "bootable_usb")
36
- os.makedirs(self.usb_mount_point, exist_ok=True)
37
-
38
- # Storage disk (JSON) mount point
39
- self.storage_mount_point = os.path.join(self.temp_dir, "storage_disk")
40
- os.makedirs(self.storage_mount_point, exist_ok=True)
41
-
42
- # Installation target
43
- self.install_target = os.path.join(self.temp_dir, "macos_sequoia")
44
- os.makedirs(self.install_target, exist_ok=True)
45
-
46
- def download_bootable_usb(self):
47
- """Download the bootable USB image (ISO) from URL"""
48
- print(f"📥 Downloading bootable USB image: {self.iso_url}")
49
-
50
- try:
51
- # Get file info first
52
- response = requests.head(self.iso_url, allow_redirects=True, timeout=30)
53
- response.raise_for_status()
54
-
55
- file_size = int(response.headers.get('content-length', 0))
56
- print(f"📦 Bootable USB size: {file_size / (1024*1024*1024):.2f} GB")
57
-
58
- # Download the actual file
59
- usb_path = os.path.join(self.temp_dir, "bootable_usb_image.iso")
60
-
61
- print("⏬ Downloading installation media...")
62
- with requests.get(self.iso_url, stream=True, timeout=60) as r:
63
- r.raise_for_status()
64
- with open(usb_path, 'wb') as f:
65
- for chunk in r.iter_content(chunk_size=8192):
66
- f.write(chunk)
67
- downloaded = os.path.getsize(usb_path)
68
- progress = (downloaded / file_size) * 100
69
- print(f" 📊 Progress: {progress:.1f}%", end='\r')
70
-
71
- print(f"\n✅ Bootable USB downloaded: {usb_path}")
72
- return usb_path
73
-
74
- except Exception as e:
75
- print(f"❌ Bootable USB download error: {e}")
76
- return None
77
-
78
- def mount_bootable_usb(self, usb_path):
79
- """Mount the bootable USB image"""
80
- print("🔗 Mounting bootable USB...")
81
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  try:
83
- # For ISO files, we need to extract or mount them
84
- if usb_path.endswith('.iso'):
85
- # Try to mount using system commands (Linux/Mac)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  try:
87
- subprocess.run([
88
- 'hdiutil', 'attach', usb_path,
89
- '-mountpoint', self.usb_mount_point,
90
- '-noverify', '-nobrowse'
91
- ], check=True, capture_output=True)
92
- print("✅ Bootable USB mounted using hdiutil")
93
- except:
94
- # Fallback: extract using Python
95
- print("⚠️ Using fallback extraction method")
96
- with open(usb_path, 'rb') as iso_file:
97
- # Simple extraction - actual ISO parsing would be more complex
98
- # This is a simplified version for demonstration
99
- shutil.copyfileobj(iso_file, open(os.path.join(self.usb_mount_point, 'boot.image'), 'wb'))
100
-
101
- elif usb_path.endswith(('.zip', '.tar.gz', '.tgz')):
102
- # Extract compressed files
103
- if usb_path.endswith('.zip'):
104
- with zipfile.ZipFile(usb_path, 'r') as zip_ref:
105
- zip_ref.extractall(self.usb_mount_point)
106
- else:
107
- with tarfile.open(usb_path, 'r:*') as tar_ref:
108
- tar_ref.extractall(self.usb_mount_point)
109
-
110
- print("✅ Bootable USB extracted")
111
-
112
- # Verify USB contents
113
- usb_contents = os.listdir(self.usb_mount_point)
114
- print(f"📁 USB contents: {len(usb_contents)} items")
115
-
116
- return True
117
-
118
- except Exception as e:
119
- print(f"❌ Bootable USB mount error: {e}")
120
- return False
121
-
122
- def connect_to_storage_disk(self):
123
- """Connect to JSON storage disk"""
124
- print(f"💾 Connecting to storage disk: {self.storage_json_url}")
125
-
126
- try:
127
- # Download storage JSON
128
- response = requests.get(self.storage_json_url, timeout=30)
129
- response.raise_for_status()
130
- self.storage_data = response.json()
131
-
132
- # Create storage disk structure
133
- storage_structure = self.storage_data.get('storage_structure', {})
134
- self._create_storage_structure(self.storage_mount_point, storage_structure)
135
-
136
- print(f"✅ Storage disk connected: {len(storage_structure)} directories")
137
- return True
138
-
139
- except Exception as e:
140
- print(f"❌ Storage disk connection error: {e}")
141
- return False
142
-
143
- def _create_storage_structure(self, base_path, structure):
144
- """Create storage disk directory structure"""
145
- for dir_name, dir_content in structure.items():
146
- dir_path = os.path.join(base_path, dir_name)
147
- os.makedirs(dir_path, exist_ok=True)
148
-
149
- if isinstance(dir_content, dict):
150
- self._create_storage_structure(dir_path, dir_content)
151
-
152
- def verify_installation_media(self):
153
- """Verify the bootable USB contents"""
154
- print("🔍 Verifying installation media...")
155
-
156
- try:
157
- required_files = [
158
- 'boot.efi', 'mach_kernel', 'BaseSystem.dmg',
159
- 'InstallESD.dmg', 'InstallInfo.plist'
160
- ]
161
-
162
- usb_files = os.listdir(self.usb_mount_point)
163
- found_files = [f for f in required_files if any(f in usb_file for usb_file in usb_files)]
164
-
165
- print(f"��� Found {len(found_files)}/{len(required_files)} required files")
166
- return len(found_files) >= 3 # At least 3 critical files
167
-
168
- except Exception as e:
169
- print(f"❌ Media verification error: {e}")
170
- return False
171
-
172
- def prepare_installation(self):
173
- """Prepare the installation environment"""
174
- print("🛠️ Preparing installation environment...")
175
-
176
- try:
177
- # Create installation directories
178
- directories = [
179
- 'System', 'Library', 'Applications', 'Users',
180
- 'private', 'var', 'tmp', 'Volumes'
181
- ]
182
-
183
- for directory in directories:
184
- os.makedirs(os.path.join(self.install_target, directory), exist_ok=True)
185
-
186
- # Copy essential files from USB to installation target
187
- essential_files = ['mach_kernel', 'boot.efi']
188
- for file in essential_files:
189
- src = self._find_file_in_usb(file)
190
- if src:
191
- dst = os.path.join(self.install_target, file)
192
- shutil.copy2(src, dst)
193
- print(f" ✅ Copied: {file}")
194
-
195
- return True
196
-
197
- except Exception as e:
198
- print(f"❌ Preparation error: {e}")
199
- return False
200
-
201
- def _find_file_in_usb(self, filename):
202
- """Find a file in the USB mount point"""
203
- for root, dirs, files in os.walk(self.usb_mount_point):
204
- if filename in files:
205
- return os.path.join(root, filename)
206
- return None
207
-
208
- def install_operating_system(self):
209
- """Main installation process"""
210
- print("🚀 Installing Mac OS 15 Sequoia...")
211
-
212
- try:
213
- # Step 1: Install base system
214
- print("📦 Installing base system...")
215
- base_system = self._find_file_in_usb('BaseSystem.dmg')
216
- if base_system:
217
- self._install_dmg(base_system, self.install_target)
218
-
219
- # Step 2: Install additional components
220
- print("⚙️ Installing system components...")
221
- install_esd = self._find_file_in_usb('InstallESD.dmg')
222
- if install_esd:
223
- self._install_dmg(install_esd, os.path.join(self.install_target, 'System', 'Installation'))
224
-
225
- # Step 3: Copy installation packages
226
- print("📦 Copying installation packages...")
227
- pkg_dir = os.path.join(self.usb_mount_point, 'Packages')
228
- if os.path.exists(pkg_dir):
229
- target_pkg = os.path.join(self.install_target, 'System', 'Installation', 'Packages')
230
- shutil.copytree(pkg_dir, target_pkg)
231
- print(f" ✅ Copied {len(os.listdir(pkg_dir))} packages")
232
-
233
- # Step 4: Create system configuration
234
- print("⚙️ Creating system configuration...")
235
- self._create_system_configuration()
236
-
237
- # Step 5: Set up boot files
238
- print("👢 Setting up boot configuration...")
239
- self._setup_boot_configuration()
240
-
241
- print("✅ Operating system installation complete")
242
- return True
243
-
244
- except Exception as e:
245
- print(f"❌ Installation error: {e}")
246
- return False
247
-
248
- def _install_dmg(self, dmg_path, target_dir):
249
- """Simulate DMG installation"""
250
- print(f" 🖥️ Installing {os.path.basename(dmg_path)}...")
251
-
252
- # In a real scenario, this would use hdiutil or similar
253
- # For simulation, we'll create the expected directory structure
254
- os.makedirs(target_dir, exist_ok=True)
255
-
256
- # Create essential files that would be in the DMG
257
- essential_files = [
258
- 'System/Library/CoreServices/SystemVersion.plist',
259
- 'System/Library/CoreServices/boot.efi',
260
- 'usr/lib/dyld',
261
- 'bin/bash',
262
- 'sbin/launchd'
263
- ]
264
-
265
- for file_path in essential_files:
266
- full_path = os.path.join(target_dir, file_path)
267
- os.makedirs(os.path.dirname(full_path), exist_ok=True)
268
-
269
- if file_path.endswith('.plist'):
270
- # Create plist file
271
- plist_content = {
272
- 'ProductName': 'Mac OS X',
273
- 'ProductVersion': '15.0',
274
- 'ProductBuildVersion': '24A344',
275
- 'ProductCopyright': '1983-2024 Apple Inc.'
276
- }
277
- with open(full_path, 'w') as f:
278
- json.dump(plist_content, f, indent=2)
279
- else:
280
- # Create placeholder file
281
- with open(full_path, 'w') as f:
282
- f.write(f"# Placeholder for {os.path.basename(file_path)}\n")
283
-
284
- print(f" ✅ Installed {os.path.basename(dmg_path)}")
285
-
286
- def _create_system_configuration(self):
287
- """Create system configuration files"""
288
- config_files = {
289
- 'System/Library/CoreServices/SystemVersion.plist': {
290
- 'ProductName': 'Mac OS X',
291
- 'ProductVersion': '15.0',
292
- 'ProductBuildVersion': '24A344',
293
- 'ProductCopyright': '1983-2024 Apple Inc.'
294
- },
295
- 'etc/hosts': '127.0.0.1 localhost\n::1 localhost\n',
296
- 'etc/fstab': '' # Empty fstab for now
297
- }
298
-
299
- for file_path, content in config_files.items():
300
- full_path = os.path.join(self.install_target, file_path)
301
- os.makedirs(os.path.dirname(full_path), exist_ok=True)
302
-
303
- if isinstance(content, dict):
304
- with open(full_path, 'w') as f:
305
- json.dump(content, f, indent=2)
306
- else:
307
- with open(full_path, 'w') as f:
308
- f.write(content)
309
-
310
- def _setup_boot_configuration(self):
311
- """Set up boot configuration"""
312
- boot_files = {
313
- 'boot.efi': '# UEFI bootloader\n',
314
- 'System/Library/CoreServices/boot.efi': '# macOS bootloader\n',
315
- 'Library/Preferences/SystemConfiguration/com.apple.Boot.plist': {
316
- 'Kernel Flags': '',
317
- 'Kernel Cache': 'prelinkedkernel',
318
- 'Timeout': '0'
319
- }
320
- }
321
-
322
- for file_path, content in boot_files.items():
323
- full_path = os.path.join(self.install_target, file_path)
324
- os.makedirs(os.path.dirname(full_path), exist_ok=True)
325
-
326
- if isinstance(content, dict):
327
- with open(full_path, 'w') as f:
328
- json.dump(content, f, indent=2)
329
- else:
330
- with open(full_path, 'w') as f:
331
- f.write(content)
332
-
333
- def finalize_installation(self):
334
- """Finalize the installation"""
335
- print("🎯 Finalizing installation...")
336
-
337
- try:
338
- # Update storage disk with installation info
339
- installation_info = {
340
- 'installation_date': datetime.utcnow().isoformat(),
341
- 'version': '15.0',
342
- 'build': '24A344',
343
- 'install_path': self.install_target,
344
- 'status': 'completed'
345
- }
346
-
347
- info_path = os.path.join(self.storage_mount_point, 'installation_info.json')
348
- with open(info_path, 'w') as f:
349
- json.dump(installation_info, f, indent=2)
350
-
351
- # Create success marker
352
- success_path = os.path.join(self.install_target, '.installation_complete')
353
- with open(success_path, 'w') as f:
354
- f.write('Installation completed successfully\n')
355
-
356
- print("✅ Installation finalized")
357
- return True
358
-
359
- except Exception as e:
360
- print(f"❌ Finalization error: {e}")
361
- return False
362
-
363
- def verify_installation(self):
364
- """Verify the installation was successful"""
365
- print("🔍 Verifying installation...")
366
-
367
- try:
368
- required_files = [
369
- 'mach_kernel',
370
- 'System/Library/CoreServices/SystemVersion.plist',
371
- 'System/Library/CoreServices/boot.efi',
372
- 'usr/lib/dyld'
373
- ]
374
-
375
- missing_files = []
376
- for file_path in required_files:
377
- full_path = os.path.join(self.install_target, file_path)
378
- if not os.path.exists(full_path):
379
- missing_files.append(file_path)
380
-
381
- if missing_files:
382
- print(f"❌ Missing files: {missing_files}")
383
- return False
384
-
385
- print("✅ Installation verification passed")
386
- return True
387
-
388
- except Exception as e:
389
- print(f"❌ Verification error: {e}")
390
- return False
391
-
392
- def run_installation(self):
393
- """Main installation process"""
394
- print("🚀 Starting Mac OS 15 Sequoia Installation")
395
- print("=" * 60)
396
- print(f"📀 Bootable USB: {self.iso_url}")
397
- print(f"💾 Storage Disk: {self.storage_json_url}")
398
- print("=" * 60)
399
-
400
- # Step 1: Download bootable USB
401
- usb_path = self.download_bootable_usb()
402
- if not usb_path:
403
- return False
404
-
405
- # Step 2: Mount bootable USB
406
- if not self.mount_bootable_usb(usb_path):
407
- return False
408
-
409
- # Step 3: Connect to storage disk
410
- if not self.connect_to_storage_disk():
411
- return False
412
-
413
- # Step 4: Verify installation media
414
- if not self.verify_installation_media():
415
- return False
416
-
417
- # Step 5: Prepare installation
418
- if not self.prepare_installation():
419
- return False
420
-
421
- # Step 6: Install OS
422
- if not self.install_operating_system():
423
- return False
424
-
425
- # Step 7: Finalize installation
426
- if not self.finalize_installation():
427
- return False
428
-
429
- # Step 8: Verify installation
430
- if not self.verify_installation():
431
- return False
432
-
433
- print("=" * 60)
434
- print("🎉 Mac OS 15 Sequoia Installation Complete!")
435
- print(f"📁 Installed at: {self.install_target}")
436
- print(f"💾 Storage updated: {self.storage_mount_point}")
437
-
438
- return True
439
-
440
- def cleanup(self):
441
- """Clean up temporary files"""
442
- try:
443
- shutil.rmtree(self.temp_dir)
444
- print(f"🧹 Cleaned up temporary files: {self.temp_dir}")
445
- except:
446
- print(f"⚠️ Could not clean up temporary files: {self.temp_dir}")
447
-
448
- def main():
449
- # Configuration
450
- ISO_URL = "https://archive.org/download/mac-os-15-sequoia-by-metaperso/Mac%20Os%2015%20Sequoia%20by%20Metaperso.iso"
451
- STORAGE_JSON_URL = "https://huggingface.co/datasets/Fred808/helium/raw/main/storage.json"
452
-
453
- # Create bootloader instance
454
- bootloader = MacOSBootloader(ISO_URL, STORAGE_JSON_URL)
455
-
456
  try:
457
- # Run installation
458
- success = bootloader.run_installation()
459
-
460
- if success:
461
- print("\n✅ Mac OS 15 Sequoia installed successfully!")
462
- print(f"📂 OS Location: {bootloader.install_target}")
463
- else:
464
- print("\n❌ Installation failed!")
465
- return 1
466
-
467
- except KeyboardInterrupt:
468
- print("\n⏹️ Installation interrupted by user")
469
- return 1
470
- except Exception as e:
471
- print(f"\n💥 Unexpected error: {e}")
472
- return 1
473
- finally:
474
- # Ask user if they want to clean up
475
- cleanup = input("\n🧹 Clean up installation files? (y/N): ").lower().strip()
476
- if cleanup == 'y':
477
- bootloader.cleanup()
478
- else:
479
- print(f"📁 Installation files preserved at: {bootloader.temp_dir}")
480
-
481
- return 0
482
-
483
- if __name__ == "__main__":
484
- exit(main())
485
- import uvicorn
486
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ """A local FastAPI UI for downloading authorized TikTok videos through SaveTT.
2
+
3
+ Run with:
4
+ uvicorn app:app --host 127.0.0.1 --port 8000
5
  """
6
 
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
15
+ from urllib.parse import urlparse
16
+ from uuid import uuid4
17
+
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/"
24
+ NAVIGATION_TIMEOUT_MS: Final = 60_000
25
+ MEDIA_TIMEOUT_MS: Final = 90_000
26
+ TMP_DIR: Final = Path(tempfile.gettempdir()) / "savett_fastapi_downloads"
27
+
28
+ app = FastAPI(title="TikTok MP4 Downloader", docs_url=None, redoc_url=None)
29
+ # The downloader uses a full Chromium browser for each request. Serializing jobs
30
+ # keeps the lightweight local server predictable and avoids concurrent browser load.
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">
41
+ <head>
42
+ <meta charset="utf-8">
43
+ <meta name="viewport" content="width=device-width, initial-scale=1">
44
+ <title>TikTok MP4 Downloader</title>
45
+ <style>
46
+ :root {{ color-scheme: light; }}
47
+ * {{ box-sizing: border-box; }}
48
+ body {{
49
+ margin: 0;
50
+ min-height: 100vh;
51
+ display: grid;
52
+ place-items: center;
53
+ background: radial-gradient(circle at top left, #203e72, #0b1020 58%);
54
+ color: #192132;
55
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
56
+ }}
57
+ main {{ width: min(680px, calc(100% - 32px)); padding: 24px 0; }}
58
+ .card {{
59
+ overflow: hidden;
60
+ background: rgba(255,255,255,.98);
61
+ border: 1px solid rgba(255,255,255,.36);
62
+ border-radius: 22px;
63
+ box-shadow: 0 24px 80px rgba(0,0,0,.30);
64
+ }}
65
+ .header {{ padding: 36px 38px 28px; background: linear-gradient(135deg, #edf4ff, #f8fbff); }}
66
+ .eyebrow {{ margin: 0 0 10px; color: #2b5da8; font-size: .78rem; font-weight: 750; letter-spacing: .11em; text-transform: uppercase; }}
67
+ h1 {{ margin: 0; font-size: clamp(1.8rem, 4.6vw, 2.55rem); letter-spacing: -.04em; }}
68
+ .subhead {{ max-width: 560px; margin: 12px 0 0; color: #5a6678; font-size: 1rem; line-height: 1.55; }}
69
+ form {{ padding: 30px 38px 18px; }}
70
+ label {{ display: block; margin-bottom: 9px; font-size: .9rem; font-weight: 700; }}
71
+ input {{
72
+ width: 100%; padding: 15px 16px; border: 1px solid #cfd8e8; border-radius: 12px;
73
+ outline: none; color: #172033; font: inherit; transition: box-shadow .18s, border-color .18s;
74
+ }}
75
+ input:focus {{ border-color: #3e75c6; box-shadow: 0 0 0 4px rgba(62,117,198,.16); }}
76
+ button {{
77
+ width: 100%; margin-top: 14px; padding: 15px 18px; border: 0; border-radius: 12px;
78
+ background: #235fb4; color: white; cursor: pointer; font: 700 1rem/1.2 inherit;
79
+ transition: background .18s, transform .18s;
80
+ }}
81
+ button:hover {{ background: #174d97; }}
82
+ button:active {{ transform: translateY(1px); }}
83
+ .notice {{ margin: 0 38px; padding: 12px 14px; border-radius: 10px; font-size: .92rem; line-height: 1.4; }}
84
+ .error {{ background: #fff0f0; border: 1px solid #f3c2c2; color: #9a2424; }}
85
+ .footer {{ margin: 0; padding: 6px 38px 34px; color: #697488; font-size: .82rem; line-height: 1.55; }}
86
+ .loading {{ display: none; margin-top: 12px; color: #526379; font-size: .9rem; text-align: center; }}
87
+ form.is-submitting button {{ opacity: .75; cursor: wait; }}
88
+ form.is-submitting .loading {{ display: block; }}
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <main>
93
+ <section class="card" aria-labelledby="page-title">
94
+ <header class="header">
95
+ <p class="eyebrow">Local FastAPI utility</p>
96
+ <h1 id="page-title">Download a TikTok MP4</h1>
97
+ <p class="subhead">Paste a public TikTok video link. The server fetches the MP4 and your browser saves the completed file.</p>
98
+ </header>
99
+ {error_markup}
100
+ <form id="download-form" action="/download" method="post">
101
+ <label for="tiktok-url">TikTok video URL</label>
102
+ <input id="tiktok-url" name="tiktok_url" type="url" required autocomplete="off" placeholder="https://www.tiktok.com/@creator/video/...">
103
+ <button type="submit">Download MP4</button>
104
+ <p class="loading" aria-live="polite">Processing the link and preparing your download. This can take up to a minute.</p>
105
+ </form>
106
+ <p class="footer">Use this utility only to download videos that you own or are otherwise authorized to save. Download links returned by the source service are temporary.</p>
107
+ </section>
108
+ </main>
109
+ <script>
110
+ document.getElementById('download-form').addEventListener('submit', (event) => {{
111
+ event.currentTarget.classList.add('is-submitting');
112
+ }});
113
+ </script>
114
+ </body>
115
+ </html>"""
116
+
117
+
118
+ def validate_tiktok_url(raw_url: str) -> str:
119
+ """Return a normalized TikTok URL or raise ValueError for an invalid input."""
120
+ try:
121
+ parsed = urlparse(raw_url.strip())
122
+ except ValueError as exc:
123
+ raise ValueError("Enter a valid TikTok video URL.") from exc
124
+
125
+ host = (parsed.hostname or "").lower()
126
+ if parsed.scheme not in {"http", "https"} or not (host == "tiktok.com" or host.endswith(".tiktok.com")):
127
+ raise ValueError("The URL must be from tiktok.com.")
128
+ if not re.search(r"/video/\d+", parsed.path):
129
+ raise ValueError("The URL must point to a TikTok video (it should include /video/<id>).")
130
+ return parsed.geturl()
131
+
132
+
133
+ def video_id_from_url(tiktok_url: str) -> str:
134
+ match = re.search(r"/video/(\d+)", tiktok_url)
135
+ return match.group(1) if match else uuid4().hex
136
+
137
+
138
+ def looks_like_video(body: bytes, content_type: str) -> bool:
139
+ """Reject HTML/JSON error pages returned in place of media."""
140
+ if len(body) < 32:
141
+ return False
142
+ if "text/html" in content_type or "application/json" in content_type:
143
+ return False
144
+ return body[4:8] == b"ftyp" or content_type.startswith("video/")
145
+
146
+
147
+ async def fetch_mp4(tiktok_url: str) -> tuple[Path, str]:
148
+ """Use Playwright to get SaveTT's MP4 URLs and store the first usable file."""
149
+ TMP_DIR.mkdir(parents=True, exist_ok=True)
150
+ video_id = video_id_from_url(tiktok_url)
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 "
158
+ "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
159
+ ),
160
+ viewport={"width": 1365, "height": 900},
161
+ )
162
+ page = await context.new_page()
163
+ page.set_default_timeout(NAVIGATION_TIMEOUT_MS)
164
+
165
  try:
166
+ await page.goto(SAVE_TT_HOME, wait_until="domcontentloaded")
167
+ await page.locator('input[name="url"]').fill(tiktok_url)
168
+ await asyncio.gather(
169
+ page.wait_for_url(re.compile(r"/en1/download(?:$|\?)"), timeout=NAVIGATION_TIMEOUT_MS),
170
+ page.locator('form#go button[type="submit"]').click(),
171
+ )
172
+
173
+ # SaveTT places temporary media URLs as JSON in the MP4 option value.
174
+ mp4_payload = await page.locator("#formatselect option").evaluate_all(
175
+ """options => {
176
+ const mp4 = options.find(option => option.textContent.trim() === 'MP4');
177
+ return mp4 ? mp4.value : null;
178
+ }"""
179
+ )
180
+ if not mp4_payload:
181
+ raise RuntimeError("The downloader did not return an MP4 option for that URL.")
182
+
183
+ try:
184
+ media_urls = json.loads(mp4_payload).get("URL", [])
185
+ except json.JSONDecodeError as exc:
186
+ raise RuntimeError("The downloader returned an invalid MP4 response.") from exc
187
+
188
+ if not isinstance(media_urls, list) or not media_urls:
189
+ raise RuntimeError("The downloader did not supply a media URL.")
190
+
191
+ last_error: Exception | None = None
192
+ for media_url in media_urls:
193
  try:
194
+ response = await context.request.get(
195
+ media_url,
196
+ headers={"Referer": "https://www.tiktok.com/", "Accept": "*/*"},
197
+ timeout=MEDIA_TIMEOUT_MS,
198
+ max_redirects=10,
199
+ )
200
+ if not response.ok:
201
+ raise RuntimeError(f"The media server returned HTTP {response.status}.")
202
+
203
+ body = await response.body()
204
+ content_type = response.headers.get("content-type", "").lower()
205
+ if not looks_like_video(body, content_type):
206
+ raise RuntimeError("The media server did not return an MP4 file.")
207
+
208
+ await asyncio.to_thread(output_path.write_bytes, body)
209
+ return output_path, f"tiktok_{video_id}.mp4"
210
+ except Exception as exc: # Try each backup media URL returned by SaveTT.
211
+ last_error = exc
212
+
213
+ raise RuntimeError(f"Every available media URL failed. Last error: {last_error}")
214
+ finally:
215
+ await context.close()
216
+ await browser.close()
217
+
218
+
219
+ def remove_file(path: Path) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  try:
221
+ path.unlink(missing_ok=True)
222
+ except OSError:
223
+ pass
224
+
225
+
226
+ @app.get("/", response_class=HTMLResponse)
227
+ async def home() -> HTMLResponse:
228
+ return HTMLResponse(render_home())
229
+
230
+
231
+ @app.post("/download")
232
+ async def download(tiktok_url: str = Form(...)):
233
+ try:
234
+ validated_url = validate_tiktok_url(tiktok_url)
235
+ except ValueError as exc:
236
+ return HTMLResponse(render_home(str(exc)), status_code=400)
237
+
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
+
244
+ return FileResponse(
245
+ file_path,
246
+ media_type="video/mp4",
247
+ filename=download_name,
248
+ background=BackgroundTask(remove_file, file_path),
249
+ )