tecuts commited on
Commit
f38c41b
·
verified ·
1 Parent(s): 4798e0a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -50
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
- def run_ytdlp_download(url: str, out_path: Path):
 
 
 
119
  """
120
- Runs a single yt-dlp download, using 'aria2c' for direct files
121
- and the default (or ffmpeg) downloader for HLS/m3u8 streams.
 
122
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  ydl_opts = {
124
- 'outtmpl': str(out_path),
 
125
  'quiet': True,
126
  'noprogress': True,
127
- }
128
-
129
- # Check if the URL is an HLS playlist.
130
- # If it's NOT, we assume it's a direct file and can use the aria2c optimization.
131
- if ".m3u8" not in url.lower():
132
- logging.info(f"Using aria2c for direct file download: {out_path.name}")
133
- # --- SPEED OPTIMIZATION (FOR DIRECT FILES) ---
134
- ydl_opts['external_downloader'] = 'aria2c'
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
- else:
142
- logging.info(f"Using default HLS downloader for m3u8 stream: {out_path.name}")
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([url])
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": "Fetching video info..."}
 
161
  try:
162
- # Step 1: Get video info
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
- subprocess.run(
185
- ['ffmpeg', '-i', str(video_path), '-i', str(audio_path), '-c', 'copy', str(final_output_path)],
186
- check=True, capture_output=True, text=True
 
 
 
 
 
 
 
 
187
  )
188
 
189
- # Step 4: Finalize and set status to complete
 
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
- task_statuses[task_id] = {"status": "failed", "error": str(e)}
 
 
 
 
201
  finally:
202
- if 'video_path' in locals() and video_path.exists(): video_path.unlink()
203
- if 'audio_path' in locals() and audio_path.exists(): audio_path.unlink()
 
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 ---