Update main.py
Browse files
main.py
CHANGED
|
@@ -8,7 +8,6 @@ import os
|
|
| 8 |
import sys
|
| 9 |
import subprocess
|
| 10 |
import json
|
| 11 |
-
import tempfile
|
| 12 |
import random
|
| 13 |
import time
|
| 14 |
import asyncio
|
|
@@ -18,10 +17,9 @@ from pathlib import Path
|
|
| 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,
|
| 24 |
-
from fastapi.responses import FileResponse, HTMLResponse,
|
| 25 |
from fastapi.middleware.cors import CORSMiddleware
|
| 26 |
from pydantic import BaseModel, HttpUrl
|
| 27 |
import uvicorn
|
|
@@ -50,7 +48,7 @@ class BatchDownloadRequest(BaseModel):
|
|
| 50 |
quality: str = "best"
|
| 51 |
audio_only: bool = False
|
| 52 |
use_cookies: Optional[bool] = None
|
| 53 |
-
max_concurrent: int = 2
|
| 54 |
|
| 55 |
class VideoInfo(BaseModel):
|
| 56 |
title: str
|
|
@@ -105,11 +103,16 @@ class HealthResponse(BaseModel):
|
|
| 105 |
strategies_enabled: List[str]
|
| 106 |
batch_support: bool
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
# Initialize FastAPI app
|
| 109 |
app = FastAPI(
|
| 110 |
title="Batch YouTube Video Downloader",
|
| 111 |
description="Download YouTube videos individually or in batches with cookie support",
|
| 112 |
-
version="4.0.
|
| 113 |
docs_url="/docs",
|
| 114 |
redoc_url="/redoc"
|
| 115 |
)
|
|
@@ -550,39 +553,6 @@ class BatchYouTubeDownloader:
|
|
| 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"""
|
|
@@ -723,7 +693,7 @@ async def read_root():
|
|
| 723 |
</div>
|
| 724 |
|
| 725 |
<div class="feature new-feature">
|
| 726 |
-
<strong>🆕
|
| 727 |
</div>
|
| 728 |
|
| 729 |
<div class="feature">
|
|
@@ -752,6 +722,7 @@ async def read_root():
|
|
| 752 |
<ul>
|
| 753 |
<li><code>POST /video/info</code> - Get single video information</li>
|
| 754 |
<li><code>POST /video/download</code> - Download single video</li>
|
|
|
|
| 755 |
</ul>
|
| 756 |
|
| 757 |
<h4>Batch Operations:</h4>
|
|
@@ -759,8 +730,7 @@ async def read_root():
|
|
| 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/
|
| 763 |
-
<li><code>GET /download-all</code> - Download ALL files from server</li>
|
| 764 |
</ul>
|
| 765 |
</div>
|
| 766 |
|
|
@@ -873,7 +843,7 @@ async def health_check():
|
|
| 873 |
"Enhanced Headers",
|
| 874 |
"Concurrent Downloads",
|
| 875 |
"Progress Tracking",
|
| 876 |
-
"
|
| 877 |
]
|
| 878 |
|
| 879 |
return HealthResponse(
|
|
@@ -1060,85 +1030,35 @@ async def get_batch_status(batch_id: str):
|
|
| 1060 |
status = batch_status_store[batch_id]
|
| 1061 |
return BatchStatus(**status)
|
| 1062 |
|
| 1063 |
-
@app.get("/batch/
|
| 1064 |
-
async def
|
| 1065 |
-
"""
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 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 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1142 |
|
| 1143 |
@app.get("/video/file/{filename}")
|
| 1144 |
async def download_file(filename: str):
|
|
@@ -1180,5 +1100,4 @@ if __name__ == "__main__":
|
|
| 1180 |
port=port,
|
| 1181 |
reload=False,
|
| 1182 |
log_level="info"
|
| 1183 |
-
)
|
| 1184 |
-
|
|
|
|
| 8 |
import sys
|
| 9 |
import subprocess
|
| 10 |
import json
|
|
|
|
| 11 |
import random
|
| 12 |
import time
|
| 13 |
import asyncio
|
|
|
|
| 17 |
from typing import Optional, Dict, Any, List
|
| 18 |
from datetime import datetime
|
| 19 |
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
| 20 |
|
| 21 |
+
from fastapi import FastAPI, HTTPException, BackgroundTasks, UploadFile, File
|
| 22 |
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
| 23 |
from fastapi.middleware.cors import CORSMiddleware
|
| 24 |
from pydantic import BaseModel, HttpUrl
|
| 25 |
import uvicorn
|
|
|
|
| 48 |
quality: str = "best"
|
| 49 |
audio_only: bool = False
|
| 50 |
use_cookies: Optional[bool] = None
|
| 51 |
+
max_concurrent: int = 2
|
| 52 |
|
| 53 |
class VideoInfo(BaseModel):
|
| 54 |
title: str
|
|
|
|
| 103 |
strategies_enabled: List[str]
|
| 104 |
batch_support: bool
|
| 105 |
|
| 106 |
+
class BatchFileListResponse(BaseModel):
|
| 107 |
+
batch_id: str
|
| 108 |
+
total_files: int
|
| 109 |
+
files: List[Dict[str, str]]
|
| 110 |
+
|
| 111 |
# Initialize FastAPI app
|
| 112 |
app = FastAPI(
|
| 113 |
title="Batch YouTube Video Downloader",
|
| 114 |
description="Download YouTube videos individually or in batches with cookie support",
|
| 115 |
+
version="4.0.2",
|
| 116 |
docs_url="/docs",
|
| 117 |
redoc_url="/redoc"
|
| 118 |
)
|
|
|
|
| 553 |
# Global downloader instance
|
| 554 |
downloader = BatchYouTubeDownloader()
|
| 555 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
@app.get("/", response_class=HTMLResponse)
|
| 557 |
async def read_root():
|
| 558 |
"""Serve the main HTML interface with batch support and cookie upload"""
|
|
|
|
| 693 |
</div>
|
| 694 |
|
| 695 |
<div class="feature new-feature">
|
| 696 |
+
<strong>🆕 Individual File Downloads:</strong> Download each file separately
|
| 697 |
</div>
|
| 698 |
|
| 699 |
<div class="feature">
|
|
|
|
| 722 |
<ul>
|
| 723 |
<li><code>POST /video/info</code> - Get single video information</li>
|
| 724 |
<li><code>POST /video/download</code> - Download single video</li>
|
| 725 |
+
<li><code>GET /video/file/{filename}</code> - Download a specific file</li>
|
| 726 |
</ul>
|
| 727 |
|
| 728 |
<h4>Batch Operations:</h4>
|
|
|
|
| 730 |
<li><code>POST /batch/info</code> - Get info for multiple videos</li>
|
| 731 |
<li><code>POST /batch/download</code> - Start batch download</li>
|
| 732 |
<li><code>GET /batch/status/{batch_id}</code> - Check batch progress</li>
|
| 733 |
+
<li><code>GET /batch/files/{batch_id}</code> - Get list of downloadable files</li>
|
|
|
|
| 734 |
</ul>
|
| 735 |
</div>
|
| 736 |
|
|
|
|
| 843 |
"Enhanced Headers",
|
| 844 |
"Concurrent Downloads",
|
| 845 |
"Progress Tracking",
|
| 846 |
+
"Individual File Downloads"
|
| 847 |
]
|
| 848 |
|
| 849 |
return HealthResponse(
|
|
|
|
| 1030 |
status = batch_status_store[batch_id]
|
| 1031 |
return BatchStatus(**status)
|
| 1032 |
|
| 1033 |
+
@app.get("/batch/files/{batch_id}", response_model=BatchFileListResponse)
|
| 1034 |
+
async def get_batch_files(batch_id: str):
|
| 1035 |
+
"""Get list of downloadable files for a batch"""
|
| 1036 |
+
if batch_id not in batch_status_store:
|
| 1037 |
+
raise HTTPException(status_code=404, detail="Batch not found")
|
| 1038 |
+
|
| 1039 |
+
batch_status = batch_status_store[batch_id]
|
| 1040 |
+
|
| 1041 |
+
if batch_status["status"] != "completed":
|
| 1042 |
+
raise HTTPException(
|
| 1043 |
+
status_code=400,
|
| 1044 |
+
detail=f"Batch is not completed yet. Current status: {batch_status['status']}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1045 |
)
|
| 1046 |
+
|
| 1047 |
+
files = []
|
| 1048 |
+
for result in batch_status["results"]:
|
| 1049 |
+
if result.get("success") and result.get("download_path"):
|
| 1050 |
+
file_path = result["download_path"]
|
| 1051 |
+
if os.path.exists(file_path):
|
| 1052 |
+
files.append({
|
| 1053 |
+
"filename": os.path.basename(file_path),
|
| 1054 |
+
"url": f"/video/file/{os.path.basename(file_path)}"
|
| 1055 |
+
})
|
| 1056 |
+
|
| 1057 |
+
return BatchFileListResponse(
|
| 1058 |
+
batch_id=batch_id,
|
| 1059 |
+
total_files=len(files),
|
| 1060 |
+
files=files
|
| 1061 |
+
)
|
| 1062 |
|
| 1063 |
@app.get("/video/file/{filename}")
|
| 1064 |
async def download_file(filename: str):
|
|
|
|
| 1100 |
port=port,
|
| 1101 |
reload=False,
|
| 1102 |
log_level="info"
|
| 1103 |
+
)
|
|
|