Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -37,7 +37,6 @@ def is_http_url(s: str) -> bool:
|
|
| 37 |
return s.startswith("http://") or s.startswith("https://")
|
| 38 |
|
| 39 |
def ensure_parent_dir(path: str) -> None:
|
| 40 |
-
"""Create parent directory for a file path if missing."""
|
| 41 |
parent = os.path.dirname(path)
|
| 42 |
if parent:
|
| 43 |
os.makedirs(parent, exist_ok=True)
|
|
@@ -86,7 +85,6 @@ def fetch_bytes(url: str, timeout: int = 45) -> bytes:
|
|
| 86 |
return r.content
|
| 87 |
|
| 88 |
def parse_torrent(raw: bytes) -> Dict[str, Any]:
|
| 89 |
-
# Use top-level decode; current bencodepy has no Bencode class
|
| 90 |
data = bencodepy.decode(raw)
|
| 91 |
if not isinstance(data, dict) or b"info" not in data:
|
| 92 |
raise ValueError("Invalid .torrent: missing 'info' dictionary.")
|
|
@@ -278,10 +276,8 @@ def _sha256_file(path: pathlib.Path, bufsize: int = 1024 * 1024) -> str:
|
|
| 278 |
return h.hexdigest()
|
| 279 |
|
| 280 |
def _supports_range(url: str, timeout: int = 30) -> Tuple[bool, Optional[int]]:
|
| 281 |
-
# HEAD to learn size and Accept-Ranges
|
| 282 |
r = requests.head(url, timeout=timeout, allow_redirects=True)
|
| 283 |
if r.status_code >= 400:
|
| 284 |
-
# Some servers don't allow HEAD; try GET without download
|
| 285 |
r = requests.get(url, stream=True, timeout=timeout, allow_redirects=True)
|
| 286 |
r.raise_for_status()
|
| 287 |
size = int(r.headers.get("Content-Length", "0") or 0)
|
|
@@ -296,10 +292,6 @@ def _supports_range(url: str, timeout: int = 30) -> Tuple[bool, Optional[int]]:
|
|
| 296 |
return ("bytes" in accept_ranges.lower() or size > 0, size if size > 0 else None)
|
| 297 |
|
| 298 |
def _download_with_resume(url: str, dest_path: pathlib.Path, timeout: int = 120) -> Tuple[int, Optional[int]]:
|
| 299 |
-
"""
|
| 300 |
-
Download URL to dest_path with simple resume (HTTP Range).
|
| 301 |
-
Returns (bytes_written, total_expected or None).
|
| 302 |
-
"""
|
| 303 |
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
| 304 |
tmp_path = dest_path.with_suffix(dest_path.suffix + ".part")
|
| 305 |
|
|
@@ -320,22 +312,12 @@ def _download_with_resume(url: str, dest_path: pathlib.Path, timeout: int = 120)
|
|
| 320 |
f.write(chunk)
|
| 321 |
bytes_written += len(chunk)
|
| 322 |
|
| 323 |
-
# finalize
|
| 324 |
final_size = (existing + bytes_written)
|
| 325 |
-
# If we know total_size and match or server returned 200 with complete payload, rename
|
| 326 |
if total_size is None or final_size >= (total_size or 0):
|
| 327 |
tmp_path.rename(dest_path)
|
| 328 |
-
else:
|
| 329 |
-
# Keep .part if incomplete
|
| 330 |
-
pass
|
| 331 |
return bytes_written, total_size
|
| 332 |
|
| 333 |
def prepare_download(parsed_json: str, base_url_override: str) -> Tuple[str, List[str], List[str], str]:
|
| 334 |
-
"""
|
| 335 |
-
Build seed list and file choices.
|
| 336 |
-
- If torrent has web seeds → use them.
|
| 337 |
-
- Else if base_url_override provided → use that as a 'seed'.
|
| 338 |
-
"""
|
| 339 |
if not parsed_json:
|
| 340 |
return "Parse something in Inspect first.", [], [], ""
|
| 341 |
parsed = json.loads(parsed_json)
|
|
@@ -353,10 +335,6 @@ def prepare_download(parsed_json: str, base_url_override: str) -> Tuple[str, Lis
|
|
| 353 |
return msg, seeds, file_list, root
|
| 354 |
|
| 355 |
def download_selected(parsed_json: str, seed_url: str, root_dir: str, selected_files: List[str]) -> Tuple[str, List[str]]:
|
| 356 |
-
"""
|
| 357 |
-
Download selected files into /mnt/data/downloads/<infohash>/...
|
| 358 |
-
Create .sha256 sidecar after successful completion.
|
| 359 |
-
"""
|
| 360 |
if not parsed_json:
|
| 361 |
raise gr.Error("Parse a torrent first.")
|
| 362 |
if not seed_url:
|
|
@@ -378,10 +356,8 @@ def download_selected(parsed_json: str, seed_url: str, root_dir: str, selected_f
|
|
| 378 |
url = _join_url(seed_url, root_dir, rel)
|
| 379 |
dest_path = out_root / rel
|
| 380 |
bytes_written, total_expected = _download_with_resume(url, dest_path)
|
| 381 |
-
# Verify size if server told us
|
| 382 |
if total_expected is not None and dest_path.exists() and dest_path.stat().st_size != total_expected:
|
| 383 |
logs.append(f"⚠️ Size mismatch for {rel} (got {dest_path.stat().st_size}, expected {total_expected}). Kept .part if incomplete.")
|
| 384 |
-
# SHA256
|
| 385 |
if dest_path.exists():
|
| 386 |
sha = _sha256_file(dest_path)
|
| 387 |
with open(str(dest_path) + ".sha256", "w") as f:
|
|
@@ -526,4 +502,8 @@ with gr.Blocks(css=CSS, title="Torrent Inspector + Full HTTP Downloader") as dem
|
|
| 526 |
)
|
| 527 |
|
| 528 |
if __name__ == "__main__":
|
| 529 |
-
demo.launch(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
return s.startswith("http://") or s.startswith("https://")
|
| 38 |
|
| 39 |
def ensure_parent_dir(path: str) -> None:
|
|
|
|
| 40 |
parent = os.path.dirname(path)
|
| 41 |
if parent:
|
| 42 |
os.makedirs(parent, exist_ok=True)
|
|
|
|
| 85 |
return r.content
|
| 86 |
|
| 87 |
def parse_torrent(raw: bytes) -> Dict[str, Any]:
|
|
|
|
| 88 |
data = bencodepy.decode(raw)
|
| 89 |
if not isinstance(data, dict) or b"info" not in data:
|
| 90 |
raise ValueError("Invalid .torrent: missing 'info' dictionary.")
|
|
|
|
| 276 |
return h.hexdigest()
|
| 277 |
|
| 278 |
def _supports_range(url: str, timeout: int = 30) -> Tuple[bool, Optional[int]]:
|
|
|
|
| 279 |
r = requests.head(url, timeout=timeout, allow_redirects=True)
|
| 280 |
if r.status_code >= 400:
|
|
|
|
| 281 |
r = requests.get(url, stream=True, timeout=timeout, allow_redirects=True)
|
| 282 |
r.raise_for_status()
|
| 283 |
size = int(r.headers.get("Content-Length", "0") or 0)
|
|
|
|
| 292 |
return ("bytes" in accept_ranges.lower() or size > 0, size if size > 0 else None)
|
| 293 |
|
| 294 |
def _download_with_resume(url: str, dest_path: pathlib.Path, timeout: int = 120) -> Tuple[int, Optional[int]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
| 296 |
tmp_path = dest_path.with_suffix(dest_path.suffix + ".part")
|
| 297 |
|
|
|
|
| 312 |
f.write(chunk)
|
| 313 |
bytes_written += len(chunk)
|
| 314 |
|
|
|
|
| 315 |
final_size = (existing + bytes_written)
|
|
|
|
| 316 |
if total_size is None or final_size >= (total_size or 0):
|
| 317 |
tmp_path.rename(dest_path)
|
|
|
|
|
|
|
|
|
|
| 318 |
return bytes_written, total_size
|
| 319 |
|
| 320 |
def prepare_download(parsed_json: str, base_url_override: str) -> Tuple[str, List[str], List[str], str]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
if not parsed_json:
|
| 322 |
return "Parse something in Inspect first.", [], [], ""
|
| 323 |
parsed = json.loads(parsed_json)
|
|
|
|
| 335 |
return msg, seeds, file_list, root
|
| 336 |
|
| 337 |
def download_selected(parsed_json: str, seed_url: str, root_dir: str, selected_files: List[str]) -> Tuple[str, List[str]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
if not parsed_json:
|
| 339 |
raise gr.Error("Parse a torrent first.")
|
| 340 |
if not seed_url:
|
|
|
|
| 356 |
url = _join_url(seed_url, root_dir, rel)
|
| 357 |
dest_path = out_root / rel
|
| 358 |
bytes_written, total_expected = _download_with_resume(url, dest_path)
|
|
|
|
| 359 |
if total_expected is not None and dest_path.exists() and dest_path.stat().st_size != total_expected:
|
| 360 |
logs.append(f"⚠️ Size mismatch for {rel} (got {dest_path.stat().st_size}, expected {total_expected}). Kept .part if incomplete.")
|
|
|
|
| 361 |
if dest_path.exists():
|
| 362 |
sha = _sha256_file(dest_path)
|
| 363 |
with open(str(dest_path) + ".sha256", "w") as f:
|
|
|
|
| 502 |
)
|
| 503 |
|
| 504 |
if __name__ == "__main__":
|
| 505 |
+
demo.launch(
|
| 506 |
+
server_name="0.0.0.0",
|
| 507 |
+
server_port=int(os.environ.get("PORT", 7860)),
|
| 508 |
+
allowed_paths=["/mnt/data"] # <-- allow returning files from /mnt/data
|
| 509 |
+
)
|