Update main.py
Browse files
main.py
CHANGED
|
@@ -18,9 +18,10 @@ from pathlib import Path
|
|
| 18 |
from typing import Optional, Dict, Any, List
|
| 19 |
from datetime import datetime
|
| 20 |
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
| 21 |
|
| 22 |
from fastapi import FastAPI, HTTPException, BackgroundTasks, Request, UploadFile, File, Form
|
| 23 |
-
from fastapi.responses import FileResponse, HTMLResponse
|
| 24 |
from fastapi.middleware.cors import CORSMiddleware
|
| 25 |
from pydantic import BaseModel, HttpUrl
|
| 26 |
import uvicorn
|
|
@@ -549,6 +550,39 @@ class BatchYouTubeDownloader:
|
|
| 549 |
# Global downloader instance
|
| 550 |
downloader = BatchYouTubeDownloader()
|
| 551 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
@app.get("/", response_class=HTMLResponse)
|
| 553 |
async def read_root():
|
| 554 |
"""Serve the main HTML interface with batch support and cookie upload"""
|
|
@@ -688,6 +722,10 @@ async def read_root():
|
|
| 688 |
<strong>π Progress Tracking:</strong> Monitor batch download progress in real-time
|
| 689 |
</div>
|
| 690 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
<div class="feature">
|
| 692 |
<strong>πͺ Cookie Support:</strong> Upload cookies for better success rates
|
| 693 |
</div>
|
|
@@ -721,6 +759,8 @@ async def read_root():
|
|
| 721 |
<li><code>POST /batch/info</code> - Get info for multiple videos</li>
|
| 722 |
<li><code>POST /batch/download</code> - Start batch download</li>
|
| 723 |
<li><code>GET /batch/status/{batch_id}</code> - Check batch progress</li>
|
|
|
|
|
|
|
| 724 |
</ul>
|
| 725 |
</div>
|
| 726 |
|
|
@@ -832,7 +872,8 @@ async def health_check():
|
|
| 832 |
"Smart Retry Logic",
|
| 833 |
"Enhanced Headers",
|
| 834 |
"Concurrent Downloads",
|
| 835 |
-
"Progress Tracking"
|
|
|
|
| 836 |
]
|
| 837 |
|
| 838 |
return HealthResponse(
|
|
@@ -1019,6 +1060,86 @@ async def get_batch_status(batch_id: str):
|
|
| 1019 |
status = batch_status_store[batch_id]
|
| 1020 |
return BatchStatus(**status)
|
| 1021 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1022 |
@app.get("/video/file/{filename}")
|
| 1023 |
async def download_file(filename: str):
|
| 1024 |
"""Serve downloaded files"""
|
|
|
|
| 18 |
from typing import Optional, Dict, Any, List
|
| 19 |
from datetime import datetime
|
| 20 |
from concurrent.futures import ThreadPoolExecutor
|
| 21 |
+
import io
|
| 22 |
|
| 23 |
from fastapi import FastAPI, HTTPException, BackgroundTasks, Request, UploadFile, File, Form
|
| 24 |
+
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
|
| 25 |
from fastapi.middleware.cors import CORSMiddleware
|
| 26 |
from pydantic import BaseModel, HttpUrl
|
| 27 |
import uvicorn
|
|
|
|
| 550 |
# Global downloader instance
|
| 551 |
downloader = BatchYouTubeDownloader()
|
| 552 |
|
| 553 |
+
def generate_multipart_response(file_paths: List[str], boundary: str = None):
|
| 554 |
+
"""Generate a multipart response with multiple files"""
|
| 555 |
+
if boundary is None:
|
| 556 |
+
boundary = f"----formdata-{uuid.uuid4().hex}"
|
| 557 |
+
|
| 558 |
+
def file_generator():
|
| 559 |
+
for file_path in file_paths:
|
| 560 |
+
if os.path.exists(file_path):
|
| 561 |
+
filename = os.path.basename(file_path)
|
| 562 |
+
file_size = os.path.getsize(file_path)
|
| 563 |
+
|
| 564 |
+
# Write multipart boundary and headers
|
| 565 |
+
yield f"--{boundary}\r\n".encode()
|
| 566 |
+
yield f"Content-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\n".encode()
|
| 567 |
+
yield f"Content-Type: application/octet-stream\r\n".encode()
|
| 568 |
+
yield f"Content-Length: {file_size}\r\n".encode()
|
| 569 |
+
yield "\r\n".encode()
|
| 570 |
+
|
| 571 |
+
# Stream file content
|
| 572 |
+
with open(file_path, 'rb') as f:
|
| 573 |
+
while True:
|
| 574 |
+
chunk = f.read(8192)
|
| 575 |
+
if not chunk:
|
| 576 |
+
break
|
| 577 |
+
yield chunk
|
| 578 |
+
|
| 579 |
+
yield "\r\n".encode()
|
| 580 |
+
|
| 581 |
+
# Final boundary
|
| 582 |
+
yield f"--{boundary}--\r\n".encode()
|
| 583 |
+
|
| 584 |
+
return file_generator(), boundary
|
| 585 |
+
|
| 586 |
@app.get("/", response_class=HTMLResponse)
|
| 587 |
async def read_root():
|
| 588 |
"""Serve the main HTML interface with batch support and cookie upload"""
|
|
|
|
| 722 |
<strong>π Progress Tracking:</strong> Monitor batch download progress in real-time
|
| 723 |
</div>
|
| 724 |
|
| 725 |
+
<div class="feature new-feature">
|
| 726 |
+
<strong>π Bulk File Download:</strong> Download all files from a batch in one request
|
| 727 |
+
</div>
|
| 728 |
+
|
| 729 |
<div class="feature">
|
| 730 |
<strong>πͺ Cookie Support:</strong> Upload cookies for better success rates
|
| 731 |
</div>
|
|
|
|
| 759 |
<li><code>POST /batch/info</code> - Get info for multiple videos</li>
|
| 760 |
<li><code>POST /batch/download</code> - Start batch download</li>
|
| 761 |
<li><code>GET /batch/status/{batch_id}</code> - Check batch progress</li>
|
| 762 |
+
<li><code>GET /batch/download-all/{batch_id}</code> - Download all files from batch</li>
|
| 763 |
+
<li><code>GET /download-all</code> - Download ALL files from server</li>
|
| 764 |
</ul>
|
| 765 |
</div>
|
| 766 |
|
|
|
|
| 872 |
"Smart Retry Logic",
|
| 873 |
"Enhanced Headers",
|
| 874 |
"Concurrent Downloads",
|
| 875 |
+
"Progress Tracking",
|
| 876 |
+
"Bulk File Download"
|
| 877 |
]
|
| 878 |
|
| 879 |
return HealthResponse(
|
|
|
|
| 1060 |
status = batch_status_store[batch_id]
|
| 1061 |
return BatchStatus(**status)
|
| 1062 |
|
| 1063 |
+
@app.get("/batch/download-all/{batch_id}")
|
| 1064 |
+
async def download_all_batch_files(batch_id: str):
|
| 1065 |
+
"""Download all files from a completed batch as multipart response"""
|
| 1066 |
+
try:
|
| 1067 |
+
if batch_id not in batch_status_store:
|
| 1068 |
+
raise HTTPException(status_code=404, detail="Batch not found")
|
| 1069 |
+
|
| 1070 |
+
batch_status = batch_status_store[batch_id]
|
| 1071 |
+
|
| 1072 |
+
if batch_status["status"] != "completed":
|
| 1073 |
+
raise HTTPException(
|
| 1074 |
+
status_code=400,
|
| 1075 |
+
detail=f"Batch is not completed yet. Current status: {batch_status['status']}"
|
| 1076 |
+
)
|
| 1077 |
+
|
| 1078 |
+
# Get all successfully downloaded files
|
| 1079 |
+
file_paths = []
|
| 1080 |
+
for result in batch_status["results"]:
|
| 1081 |
+
if result.get("success") and result.get("download_path"):
|
| 1082 |
+
file_path = result["download_path"]
|
| 1083 |
+
if os.path.exists(file_path):
|
| 1084 |
+
file_paths.append(file_path)
|
| 1085 |
+
|
| 1086 |
+
if not file_paths:
|
| 1087 |
+
raise HTTPException(status_code=404, detail="No files found for this batch")
|
| 1088 |
+
|
| 1089 |
+
# Generate multipart response
|
| 1090 |
+
file_generator, boundary = generate_multipart_response(file_paths)
|
| 1091 |
+
|
| 1092 |
+
headers = {
|
| 1093 |
+
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
| 1094 |
+
"Content-Disposition": f"attachment; filename=\"batch_{batch_id}_files.multipart\""
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
return StreamingResponse(
|
| 1098 |
+
file_generator,
|
| 1099 |
+
headers=headers,
|
| 1100 |
+
media_type=f"multipart/form-data; boundary={boundary}"
|
| 1101 |
+
)
|
| 1102 |
+
|
| 1103 |
+
except HTTPException:
|
| 1104 |
+
raise
|
| 1105 |
+
except Exception as e:
|
| 1106 |
+
logger.error(f"Error downloading batch files: {e}")
|
| 1107 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1108 |
+
|
| 1109 |
+
@app.get("/download-all")
|
| 1110 |
+
async def download_all_files():
|
| 1111 |
+
"""Download all files from the server as multipart response"""
|
| 1112 |
+
try:
|
| 1113 |
+
# Get all files from the download directory
|
| 1114 |
+
download_dir = Path(downloader.download_dir)
|
| 1115 |
+
all_files = [f for f in download_dir.glob("*") if f.is_file()]
|
| 1116 |
+
|
| 1117 |
+
if not all_files:
|
| 1118 |
+
raise HTTPException(status_code=404, detail="No files found on server")
|
| 1119 |
+
|
| 1120 |
+
# Convert to string paths
|
| 1121 |
+
file_paths = [str(f) for f in all_files]
|
| 1122 |
+
|
| 1123 |
+
# Generate multipart response
|
| 1124 |
+
file_generator, boundary = generate_multipart_response(file_paths)
|
| 1125 |
+
|
| 1126 |
+
headers = {
|
| 1127 |
+
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
| 1128 |
+
"Content-Disposition": f"attachment; filename=\"all_server_files.multipart\""
|
| 1129 |
+
}
|
| 1130 |
+
|
| 1131 |
+
return StreamingResponse(
|
| 1132 |
+
file_generator,
|
| 1133 |
+
headers=headers,
|
| 1134 |
+
media_type=f"multipart/form-data; boundary={boundary}"
|
| 1135 |
+
)
|
| 1136 |
+
|
| 1137 |
+
except HTTPException:
|
| 1138 |
+
raise
|
| 1139 |
+
except Exception as e:
|
| 1140 |
+
logger.error(f"Error downloading all files: {e}")
|
| 1141 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1142 |
+
|
| 1143 |
@app.get("/video/file/{filename}")
|
| 1144 |
async def download_file(filename: str):
|
| 1145 |
"""Serve downloaded files"""
|