Update app.py
Browse files
app.py
CHANGED
|
@@ -46,7 +46,7 @@ async def cleanup_file(filepath: Path):
|
|
| 46 |
except Exception as e:
|
| 47 |
logging.error(f"Error during cleanup of {filepath.parent}: {e}")
|
| 48 |
|
| 49 |
-
def get_best_formats_with_fallback(data: dict, requested_quality: int):
|
| 50 |
"""
|
| 51 |
Parses the Info API response to find the best matching video format
|
| 52 |
with a robust fallback, and the best audio format.
|
|
@@ -112,40 +112,55 @@ def get_best_formats_with_fallback(data: dict, requested_quality: int):
|
|
| 112 |
raise ValueError("Could not find a suitable video and/or audio stream.")
|
| 113 |
|
| 114 |
return video_url, audio_url
|
|
|
|
| 115 |
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
| 119 |
"""
|
| 120 |
-
|
| 121 |
-
|
|
|
|
| 122 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
ydl_opts = {
|
| 124 |
-
'
|
|
|
|
| 125 |
'quiet': True,
|
| 126 |
'noprogress': True,
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
ydl_opts['external_downloader_args'] = [
|
| 136 |
'--min-split-size=1M',
|
| 137 |
'--max-connection-per-server=16',
|
| 138 |
'--max-concurrent-downloads=16',
|
| 139 |
'--split=16'
|
| 140 |
-
]
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
# For M3U8 streams, yt-dlp will use its internal HLS downloader
|
| 144 |
-
# or FFmpeg. Since FFmpeg is already required for your merge step,
|
| 145 |
-
# this will work perfectly.
|
| 146 |
-
|
| 147 |
with YoutubeDL(ydl_opts) as ydl:
|
| 148 |
-
ydl.download([
|
| 149 |
|
| 150 |
async def process_in_background(
|
| 151 |
task_id: str,
|
|
@@ -156,37 +171,34 @@ async def process_in_background(
|
|
| 156 |
):
|
| 157 |
"""
|
| 158 |
This is the main worker function that runs in the background.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
"""
|
| 160 |
-
task_statuses[task_id] = {"status": "processing", "message": "
|
|
|
|
| 161 |
try:
|
| 162 |
-
# Step 1:
|
| 163 |
-
async with httpx.AsyncClient(follow_redirects=True) as client:
|
| 164 |
-
info_api_url = f"{BASE_URL.rstrip('/')}/api/info"
|
| 165 |
-
response = await client.get(info_api_url, params={"url": video_url, "playlist": "false"}, timeout=30.0)
|
| 166 |
-
response.raise_for_status()
|
| 167 |
-
video_data = response.json()
|
| 168 |
-
video_stream_url, audio_stream_url = get_best_formats_with_fallback(video_data, quality)
|
| 169 |
-
|
| 170 |
-
# Step 2: Download files
|
| 171 |
-
task_statuses[task_id]["message"] = "Downloading video and audio streams..."
|
| 172 |
final_output_dir = STATIC_DIR / task_id
|
| 173 |
final_output_dir.mkdir()
|
| 174 |
-
video_path = TEMP_DIR / f"{task_id}_video.mp4"
|
| 175 |
-
audio_path = TEMP_DIR / f"{task_id}_audio.m4a"
|
| 176 |
-
|
| 177 |
-
video_dl_task = asyncio.to_thread(run_ytdlp_download, video_stream_url, video_path)
|
| 178 |
-
audio_dl_task = asyncio.to_thread(run_ytdlp_download, audio_stream_url, audio_path)
|
| 179 |
-
await asyncio.gather(video_dl_task, audio_dl_task)
|
| 180 |
-
|
| 181 |
-
# Step 3: Merge files
|
| 182 |
-
task_statuses[task_id]["message"] = "Merging files with FFmpeg..."
|
| 183 |
final_output_path = final_output_dir / "video.mp4"
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
)
|
| 188 |
|
| 189 |
-
# Step
|
|
|
|
| 190 |
download_url = f"{base_url_for_links.rstrip('/')}/static/{task_id}/video.mp4"
|
| 191 |
task_statuses[task_id] = {
|
| 192 |
"status": "complete",
|
|
@@ -196,11 +208,17 @@ async def process_in_background(
|
|
| 196 |
background_tasks.add_task(cleanup_file, final_output_path)
|
| 197 |
|
| 198 |
except Exception as e:
|
|
|
|
| 199 |
logging.error(f"Task {task_id} failed: {e}")
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
finally:
|
| 202 |
-
|
| 203 |
-
|
|
|
|
| 204 |
|
| 205 |
|
| 206 |
# --- API Endpoints ---
|
|
|
|
| 46 |
except Exception as e:
|
| 47 |
logging.error(f"Error during cleanup of {filepath.parent}: {e}")
|
| 48 |
|
| 49 |
+
"""def get_best_formats_with_fallback(data: dict, requested_quality: int):
|
| 50 |
"""
|
| 51 |
Parses the Info API response to find the best matching video format
|
| 52 |
with a robust fallback, and the best audio format.
|
|
|
|
| 112 |
raise ValueError("Could not find a suitable video and/or audio stream.")
|
| 113 |
|
| 114 |
return video_url, audio_url
|
| 115 |
+
"""
|
| 116 |
|
| 117 |
|
| 118 |
+
def download_and_merge_with_ytdlp(
|
| 119 |
+
video_url: str,
|
| 120 |
+
quality: int,
|
| 121 |
+
output_path: Path
|
| 122 |
+
):
|
| 123 |
"""
|
| 124 |
+
Tells yt-dlp to download and merge the best formats automatically.
|
| 125 |
+
|
| 126 |
+
This is the robust, correct way to do this.
|
| 127 |
"""
|
| 128 |
+
# This format string tells yt-dlp:
|
| 129 |
+
# 1. Find the best video with a height <= [quality] (e.g., 1080)
|
| 130 |
+
# 2. ...that ALSO has video ("vcodec!=none")
|
| 131 |
+
# 3. Find the best audio ("ba")
|
| 132 |
+
# 4. If that fails (e.g., no separate streams), fall back to the
|
| 133 |
+
# best "muxed" stream (video+audio, "v+a")
|
| 134 |
+
# 5. ...with a height <= [quality]
|
| 135 |
+
# 6. As a final fallback, just get the best-ever stream <= [quality]
|
| 136 |
+
format_selector = (
|
| 137 |
+
f"bestvideo[height<={quality}][vcodec!=none]+bestaudio[acodec!=none]/best[v+a][height<={quality}]/best[height<={quality}]"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
logging.info(f"Using yt-dlp format selector: {format_selector}")
|
| 141 |
+
|
| 142 |
ydl_opts = {
|
| 143 |
+
'format': format_selector,
|
| 144 |
+
'outtmpl': str(output_path),
|
| 145 |
'quiet': True,
|
| 146 |
'noprogress': True,
|
| 147 |
+
# 'merge_output_format': 'mp4', # yt-dlp is smart enough to use mp4 from outtmpl
|
| 148 |
+
|
| 149 |
+
# --- SPEED OPTIMIZATION (FOR MUXED/HLS) ---
|
| 150 |
+
# We can keep aria2c! yt-dlp will ONLY use it if it finds a
|
| 151 |
+
# direct URL. If it finds an m3u8, it will ignore it and
|
| 152 |
+
# use its HLS downloader. This is safe.
|
| 153 |
+
'external_downloader': 'aria2c',
|
| 154 |
+
'external_downloader_args': [
|
|
|
|
| 155 |
'--min-split-size=1M',
|
| 156 |
'--max-connection-per-server=16',
|
| 157 |
'--max-concurrent-downloads=16',
|
| 158 |
'--split=16'
|
| 159 |
+
],
|
| 160 |
+
}
|
| 161 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
with YoutubeDL(ydl_opts) as ydl:
|
| 163 |
+
ydl.download([video_url])
|
| 164 |
|
| 165 |
async def process_in_background(
|
| 166 |
task_id: str,
|
|
|
|
| 171 |
):
|
| 172 |
"""
|
| 173 |
This is the main worker function that runs in the background.
|
| 174 |
+
|
| 175 |
+
--- SIMPLIFIED LOGIC ---
|
| 176 |
+
1. Tell yt-dlp to download, merge, and save the file.
|
| 177 |
+
2. Set status to complete.
|
| 178 |
+
3. Clean up.
|
| 179 |
"""
|
| 180 |
+
task_statuses[task_id] = {"status": "processing", "message": "Processing video..."}
|
| 181 |
+
|
| 182 |
try:
|
| 183 |
+
# Step 1: Define output path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
final_output_dir = STATIC_DIR / task_id
|
| 185 |
final_output_dir.mkdir()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
final_output_path = final_output_dir / "video.mp4"
|
| 187 |
+
|
| 188 |
+
# Step 2: Run the complete download and merge in one go.
|
| 189 |
+
# This one function call replaces your info-fetch, two downloads,
|
| 190 |
+
# and ffmpeg merge steps.
|
| 191 |
+
task_statuses[task_id]["message"] = "Downloading and merging video..."
|
| 192 |
+
|
| 193 |
+
await asyncio.to_thread(
|
| 194 |
+
download_and_merge_with_ytdlp,
|
| 195 |
+
video_url,
|
| 196 |
+
quality,
|
| 197 |
+
final_output_path
|
| 198 |
)
|
| 199 |
|
| 200 |
+
# Step 3: Finalize and set status to complete
|
| 201 |
+
task_statuses[task_id]["message"] = "Processing complete."
|
| 202 |
download_url = f"{base_url_for_links.rstrip('/')}/static/{task_id}/video.mp4"
|
| 203 |
task_statuses[task_id] = {
|
| 204 |
"status": "complete",
|
|
|
|
| 208 |
background_tasks.add_task(cleanup_file, final_output_path)
|
| 209 |
|
| 210 |
except Exception as e:
|
| 211 |
+
# This will now catch errors from yt-dlp directly, e.g., "video unavailable"
|
| 212 |
logging.error(f"Task {task_id} failed: {e}")
|
| 213 |
+
# Add the error output from yt-dlp if available
|
| 214 |
+
error_message = str(e)
|
| 215 |
+
if "yt-dlp error" in error_message: # You can customize this
|
| 216 |
+
error_message = f"yt-dlp failed: {error_message}"
|
| 217 |
+
task_statuses[task_id] = {"status": "failed", "error": error_message}
|
| 218 |
finally:
|
| 219 |
+
# We no longer have temp video/audio files, so cleanup is simpler.
|
| 220 |
+
# The main cleanup_file task will handle the final directory.
|
| 221 |
+
pass
|
| 222 |
|
| 223 |
|
| 224 |
# --- API Endpoints ---
|