RO-Rtechs commited on
Commit
611e504
·
verified ·
1 Parent(s): 6eb5814

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +348 -132
app.py CHANGED
@@ -6,10 +6,23 @@ import requests
6
  from concurrent.futures import ThreadPoolExecutor
7
  import time
8
  from yt_dlp import YoutubeDL
 
9
  import subprocess
10
  import shutil
11
  from typing import List, Tuple
12
  import pandas as pd
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  def sanitize_title(title):
15
  return re.sub(r'[\\/*?:"<>|]', "", title)
@@ -17,6 +30,13 @@ def sanitize_title(title):
17
  def format_time(seconds):
18
  return time.strftime('%H:%M:%S', time.gmtime(seconds))
19
 
 
 
 
 
 
 
 
20
  def get_video_info(video_url):
21
  with YoutubeDL({'quiet': True, 'no_warnings': True}) as ydl:
22
  try:
@@ -43,62 +63,125 @@ def get_video_info(video_url):
43
  return info['title'], best_format['url'], None
44
  else:
45
  raise Exception("No suitable video formats found")
46
- except Exception as e:
47
  raise Exception(f"Error extracting video info: {str(e)}")
 
 
48
 
49
- def download_segment(url, start_time, end_time, output_path):
 
 
 
 
 
 
 
50
  command = [
51
  'ffmpeg',
52
  '-ss', format_time(start_time),
53
  '-i', url,
54
  '-t', format_time(end_time - start_time),
55
  '-c', 'copy',
 
56
  '-y',
57
- output_path
58
  ]
59
- process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
60
-
61
- while True:
62
- output = process.stderr.readline()
63
- if output == '' and process.poll() is not None:
64
- break
65
- if output:
66
- yield output.strip()
67
 
68
- rc = process.poll()
69
- return rc == 0
70
-
71
- def combine_segments(video_segments, audio_segments, output_path):
72
- temp_video = 'temp_video.mp4'
73
- temp_audio = 'temp_audio.m4a'
74
 
75
- # Concatenate video segments
76
- with open('video_list.txt', 'w') as f:
77
- for segment in video_segments:
78
- f.write(f"file '{segment}'\n")
 
79
 
80
- subprocess.run(['ffmpeg', '-f', 'concat', '-safe', '0', '-i', 'video_list.txt', '-c', 'copy', temp_video])
 
 
 
 
81
 
82
- # Concatenate audio segments if they exist
83
- if audio_segments:
84
- with open('audio_list.txt', 'w') as f:
85
- for segment in audio_segments:
86
- f.write(f"file '{segment}'\n")
87
- subprocess.run(['ffmpeg', '-f', 'concat', '-safe', '0', '-i', 'audio_list.txt', '-c', 'copy', temp_audio])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- # Combine video and audio
90
- subprocess.run(['ffmpeg', '-i', temp_video, '-i', temp_audio, '-c', 'copy', output_path])
91
- else:
92
- shutil.move(temp_video, output_path)
93
-
94
- # Clean up temporary files
95
- os.remove('video_list.txt')
96
- if os.path.exists('audio_list.txt'):
97
- os.remove('audio_list.txt')
98
- if os.path.exists(temp_video):
99
- os.remove(temp_video)
100
- if os.path.exists(temp_audio):
101
- os.remove(temp_audio)
102
 
103
  def add_segment(start_hours, start_minutes, start_seconds, end_hours, end_minutes, end_seconds, segments):
104
  start_time = f"{start_hours:02d}:{start_minutes:02d}:{start_seconds:02d}"
@@ -132,116 +215,225 @@ def parse_segments(segments: pd.DataFrame) -> List[Tuple[int, int]]:
132
  continue # Skip invalid segments
133
  return parsed_segments
134
 
135
- def process_video(video_url, segments, combine, progress=gr.Progress()):
136
  if not video_url.strip():
137
- return 0, "Error: Please provide a valid YouTube URL", None
 
138
 
139
- # Extract segments from the Dataframe
140
  parsed_segments = parse_segments(segments)
141
  if not parsed_segments:
142
- return 0, "Error: No valid segments provided", None
 
143
 
144
- output_dir = 'output'
145
- os.makedirs(output_dir, exist_ok=True)
146
 
147
  try:
 
148
  video_title, video_url, audio_url = get_video_info(video_url)
149
  except Exception as e:
150
- return 0, f"Error: {str(e)}", None
 
151
 
152
  video_segments = []
153
  audio_segments = []
154
  total_segments = len(parsed_segments)
155
 
156
  for i, (start_time, end_time) in enumerate(parsed_segments):
157
- video_output = os.path.join(output_dir, f"{sanitize_title(video_title)}_video_segment_{i+1}.mp4")
158
- for output in download_segment(video_url, start_time, end_time, video_output):
159
- progress((i / total_segments) + (1 / total_segments) * 0.5)
160
- yield i * 100 // total_segments, f"Downloading video segment {i+1}/{total_segments}: {output}", None
161
- video_segments.append(video_output)
162
-
163
- if audio_url:
164
- audio_output = os.path.join(output_dir, f"{sanitize_title(video_title)}_audio_segment_{i+1}.m4a")
165
- for output in download_segment(audio_url, start_time, end_time, audio_output):
166
- progress((i / total_segments) + (1 / total_segments) * 0.75)
167
- yield i * 100 // total_segments + 50, f"Downloading audio segment {i+1}/{total_segments}: {output}", None
168
- audio_segments.append(audio_output)
169
-
170
- if combine:
171
- output_path = os.path.join(output_dir, f"{sanitize_title(video_title)}_combined.mp4")
172
- combine_segments(video_segments, audio_segments, output_path)
173
- yield 100, f"Segments combined and saved as {output_path}", output_path
174
- else:
175
- # If not combining, return the first video segment (you might want to modify this behavior)
176
- output_path = video_segments[0] if video_segments else None
177
- yield 100, "All segments downloaded successfully", output_path
178
-
179
- # Clean up individual segments if combined
180
- if combine:
181
- for segment in video_segments + audio_segments:
182
- os.remove(segment)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
  # Disable Gradio analytics
185
  utils.colab_check = lambda: True
186
 
187
- with gr.Blocks(title="YouTube Segment Downloader", theme=gr.themes.Soft(primary_hue="indigo", secondary_hue="blue")) as iface:
188
- gr.Markdown(
189
- """
190
- # YouTube Segment Downloader
191
- Download specific segments of YouTube videos with ease.
192
- """
193
- )
194
-
195
- with gr.Row():
196
- video_url = gr.Textbox(label="YouTube URL", placeholder="Enter YouTube URL here")
197
-
198
- with gr.Row():
199
- with gr.Column(scale=1):
200
- with gr.Group():
201
- gr.Markdown("### Add Segment")
202
- with gr.Row():
203
- start_hours = gr.Number(label="Start", minimum=0, maximum=23, step=1, value=0)
204
- start_minutes = gr.Number(label="", minimum=0, maximum=59, step=1, value=0)
205
- start_seconds = gr.Number(label="", minimum=0, maximum=59, step=1, value=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  with gr.Row():
207
- end_hours = gr.Number(label="End", minimum=0, maximum=23, step=1, value=0)
208
- end_minutes = gr.Number(label="", minimum=0, maximum=59, step=1, value=0)
209
- end_seconds = gr.Number(label="", minimum=0, maximum=59, step=1, value=0)
210
- add_btn = gr.Button("Add Segment", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
- with gr.Column(scale=2):
213
- segments = gr.Dataframe(
214
- headers=["Segment"],
215
- row_count=5,
216
- col_count=1,
217
- datatype=["str"],
218
- interactive=True,
219
- label="Segments to Download"
220
- )
221
-
222
- with gr.Row():
223
- with gr.Column(scale=1):
224
- combine = gr.Checkbox(label="Combine Segments", value=True)
225
- with gr.Column(scale=2):
226
- submit_btn = gr.Button("Download Segments", variant="primary", size="lg")
227
-
228
- with gr.Row():
229
- progress = gr.Slider(label="Progress", minimum=0, maximum=100, step=1, interactive=False)
230
-
231
- with gr.Row():
232
- status = gr.Textbox(label="Status", lines=3)
233
-
234
- with gr.Row():
235
- output_file = gr.File(label="Downloaded Video")
236
-
237
- with gr.Accordion("Advanced Options", open=False):
238
- with gr.Row():
239
- remove_index = gr.Number(label="Remove Segment Index", minimum=0, step=1, value=0)
240
- remove_btn = gr.Button("Remove Segment", variant="secondary")
241
- with gr.Row():
242
- old_index = gr.Number(label="Move From Index", minimum=0, step=1, value=0)
243
- new_index = gr.Number(label="Move To Index", minimum=0, step=1, value=0)
244
- move_btn = gr.Button("Move Segment", variant="secondary")
245
 
246
  add_btn.click(
247
  add_segment,
@@ -251,7 +443,7 @@ with gr.Blocks(title="YouTube Segment Downloader", theme=gr.themes.Soft(primary_
251
 
252
  submit_btn.click(
253
  process_video,
254
- inputs=[video_url, segments, combine],
255
  outputs=[progress, status, output_file]
256
  )
257
 
@@ -267,4 +459,28 @@ with gr.Blocks(title="YouTube Segment Downloader", theme=gr.themes.Soft(primary_
267
  outputs=[segments]
268
  )
269
 
270
- iface.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  from concurrent.futures import ThreadPoolExecutor
7
  import time
8
  from yt_dlp import YoutubeDL
9
+ from yt_dlp.utils import DownloadError
10
  import subprocess
11
  import shutil
12
  from typing import List, Tuple
13
  import pandas as pd
14
+ import asyncio
15
+ import aiohttp
16
+ import aiofiles
17
+ import json
18
+ from functools import lru_cache
19
+ import tenacity
20
+ from tqdm import tqdm
21
+ from pathlib import Path
22
+ import logging
23
+
24
+ logging.basicConfig(level=logging.DEBUG)
25
+ logger = logging.getLogger(__name__)
26
 
27
  def sanitize_title(title):
28
  return re.sub(r'[\\/*?:"<>|]', "", title)
 
30
  def format_time(seconds):
31
  return time.strftime('%H:%M:%S', time.gmtime(seconds))
32
 
33
+ @tenacity.retry(
34
+ stop=tenacity.stop_after_attempt(3),
35
+ wait=tenacity.wait_exponential(multiplier=1, min=4, max=10),
36
+ retry=tenacity.retry_if_exception_type((requests.RequestException, aiohttp.ClientError)),
37
+ reraise=True
38
+ )
39
+ @lru_cache(maxsize=100)
40
  def get_video_info(video_url):
41
  with YoutubeDL({'quiet': True, 'no_warnings': True}) as ydl:
42
  try:
 
63
  return info['title'], best_format['url'], None
64
  else:
65
  raise Exception("No suitable video formats found")
66
+ except DownloadError as e:
67
  raise Exception(f"Error extracting video info: {str(e)}")
68
+ except Exception as e:
69
+ raise Exception(f"Unexpected error: {str(e)}")
70
 
71
+ @tenacity.retry(
72
+ stop=tenacity.stop_after_attempt(3),
73
+ wait=tenacity.wait_exponential(multiplier=1, min=4, max=10),
74
+ retry=tenacity.retry_if_exception_type((subprocess.CalledProcessError, asyncio.TimeoutError)),
75
+ reraise=True
76
+ )
77
+ async def download_segment_async(url, start_time, end_time, output_path):
78
+ output_path = Path(output_path)
79
  command = [
80
  'ffmpeg',
81
  '-ss', format_time(start_time),
82
  '-i', url,
83
  '-t', format_time(end_time - start_time),
84
  '-c', 'copy',
85
+ '-avoid_negative_ts', 'make_zero',
86
  '-y',
87
+ str(output_path)
88
  ]
89
+ logger.debug(f"Executing command: {' '.join(command)}")
90
+ process = await asyncio.create_subprocess_exec(
91
+ *command,
92
+ stdout=asyncio.subprocess.PIPE,
93
+ stderr=asyncio.subprocess.PIPE
94
+ )
 
 
95
 
96
+ stdout, stderr = await process.communicate()
 
 
 
 
 
97
 
98
+ if process.returncode != 0:
99
+ error_message = f"FFmpeg error: {stderr.decode()}"
100
+ logger.error(error_message)
101
+ yield error_message
102
+ raise Exception(error_message)
103
 
104
+ if not output_path.exists():
105
+ error_message = f"Output file not created: {output_path}"
106
+ logger.error(error_message)
107
+ yield error_message
108
+ raise FileNotFoundError(error_message)
109
 
110
+ logger.info(f"Successfully downloaded segment to {output_path}")
111
+ yield f"Successfully downloaded segment to {output_path}"
112
+
113
+ async def combine_segments_async(video_segments, audio_segments, output_path):
114
+ if not audio_segments:
115
+ raise Exception("No audio segments to combine")
116
+
117
+ video_list = None
118
+ audio_list = None
119
+ temp_video = None
120
+ temp_audio = None
121
+
122
+ try:
123
+ if video_segments:
124
+ temp_video = 'temp_video.mp4'
125
+ temp_audio = 'temp_audio.m4a'
126
+
127
+ # Concatenate video segments
128
+ video_list = 'video_list.txt'
129
+ async with aiofiles.open(video_list, 'w') as f:
130
+ await f.write('\n'.join(f"file '{segment}'" for segment in video_segments))
131
+
132
+ process = await asyncio.create_subprocess_exec(
133
+ 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', video_list, '-c', 'copy', temp_video,
134
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
135
+ )
136
+ stdout, stderr = await process.communicate()
137
+ if process.returncode != 0:
138
+ raise Exception(f"Error concatenating video segments: {stderr.decode()}")
139
+
140
+ # Concatenate audio segments
141
+ audio_list = 'audio_list.txt'
142
+ async with aiofiles.open(audio_list, 'w') as f:
143
+ await f.write('\n'.join(f"file '{segment}'" for segment in audio_segments))
144
+
145
+ process = await asyncio.create_subprocess_exec(
146
+ 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', audio_list, '-c', 'copy', temp_audio,
147
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
148
+ )
149
+ stdout, stderr = await process.communicate()
150
+ if process.returncode != 0:
151
+ raise Exception(f"Error concatenating audio segments: {stderr.decode()}")
152
+
153
+ # Combine video and audio
154
+ process = await asyncio.create_subprocess_exec(
155
+ 'ffmpeg', '-i', temp_video, '-i', temp_audio, '-c', 'copy', output_path,
156
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
157
+ )
158
+ stdout, stderr = await process.communicate()
159
+ if process.returncode != 0:
160
+ raise Exception(f"Error combining video and audio: {stderr.decode()}")
161
+ else:
162
+ # Audio only
163
+ audio_list = 'audio_list.txt'
164
+ async with aiofiles.open(audio_list, 'w') as f:
165
+ await f.write('\n'.join(f"file '{segment}'" for segment in audio_segments))
166
+
167
+ process = await asyncio.create_subprocess_exec(
168
+ 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', audio_list, '-c', 'copy', output_path,
169
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
170
+ )
171
+ stdout, stderr = await process.communicate()
172
+ if process.returncode != 0:
173
+ raise Exception(f"Error combining audio segments: {stderr.decode()}")
174
 
175
+ return output_path
176
+ except Exception as e:
177
+ raise Exception(f"Error in combine_segments_async: {str(e)}")
178
+ finally:
179
+ # Clean up temporary files
180
+ for file in [f for f in [video_list, audio_list, temp_video, temp_audio] if f]:
181
+ try:
182
+ Path(file).unlink(missing_ok=True)
183
+ except Exception as e:
184
+ print(f"Error removing temporary file {file}: {str(e)}")
 
 
 
185
 
186
  def add_segment(start_hours, start_minutes, start_seconds, end_hours, end_minutes, end_seconds, segments):
187
  start_time = f"{start_hours:02d}:{start_minutes:02d}:{start_seconds:02d}"
 
215
  continue # Skip invalid segments
216
  return parsed_segments
217
 
218
+ async def process_video(video_url, segments, combine, audio_only, progress=gr.Progress()):
219
  if not video_url.strip():
220
+ yield 0, "Error: Please provide a valid YouTube URL", None
221
+ return
222
 
 
223
  parsed_segments = parse_segments(segments)
224
  if not parsed_segments:
225
+ yield 0, "Error: No valid segments provided", None
226
+ return
227
 
228
+ output_dir = Path('output')
229
+ output_dir.mkdir(exist_ok=True)
230
 
231
  try:
232
+ progress(0, "Extracting video info...")
233
  video_title, video_url, audio_url = get_video_info(video_url)
234
  except Exception as e:
235
+ yield 0, f"Error: {str(e)}", None
236
+ return
237
 
238
  video_segments = []
239
  audio_segments = []
240
  total_segments = len(parsed_segments)
241
 
242
  for i, (start_time, end_time) in enumerate(parsed_segments):
243
+ progress((i / total_segments) * 0.8, f"Downloading segment {i+1}/{total_segments}")
244
+
245
+ try:
246
+ if not audio_only:
247
+ video_output = output_dir / f"{sanitize_title(video_title)}_video_segment_{i+1}.mp4"
248
+ async for output in download_segment_async(video_url, start_time, end_time, str(video_output)):
249
+ progress((i / total_segments) * 0.8 + (1 / total_segments) * 0.4, f"Downloading video segment {i+1}/{total_segments}: {output}")
250
+ if not video_output.exists():
251
+ raise FileNotFoundError(f"Video segment file not found: {video_output}")
252
+ video_segments.append(str(video_output))
253
+
254
+ audio_output = output_dir / f"{sanitize_title(video_title)}_audio_segment_{i+1}.m4a"
255
+ async for output in download_segment_async(audio_url or video_url, start_time, end_time, str(audio_output)):
256
+ progress((i / total_segments) * 0.8 + (1 / total_segments) * 0.8, f"Downloading audio segment {i+1}/{total_segments}: {output}")
257
+ if not audio_output.exists():
258
+ raise FileNotFoundError(f"Audio segment file not found: {audio_output}")
259
+ audio_segments.append(str(audio_output))
260
+
261
+ except Exception as e:
262
+ yield (i / total_segments) * 100, f"Error downloading segment {i+1}: {str(e)}", None
263
+ return
264
+
265
+ try:
266
+ if combine:
267
+ progress(90, "Combining segments...")
268
+ if audio_only:
269
+ output_path = output_dir / f"{sanitize_title(video_title)}_combined.m4a"
270
+ else:
271
+ output_path = output_dir / f"{sanitize_title(video_title)}_combined.mp4"
272
+
273
+ output_path = await combine_segments_async(video_segments if not audio_only else [], audio_segments, str(output_path))
274
+ if Path(output_path).exists():
275
+ yield 100, f"Segments combined and saved as {output_path}", output_path
276
+ else:
277
+ raise FileNotFoundError(f"Combined output file not found: {output_path}")
278
+ else:
279
+ output_path = audio_segments[0] if audio_only else video_segments[0]
280
+ if Path(output_path).exists():
281
+ yield 100, "All segments downloaded successfully", output_path
282
+ else:
283
+ raise FileNotFoundError(f"Output file not found: {output_path}")
284
+
285
+ except Exception as e:
286
+ yield 100, f"Error: {str(e)}", None
287
+ finally:
288
+ # Clean up individual segment files if they were combined
289
+ if combine:
290
+ for segment in video_segments + audio_segments:
291
+ try:
292
+ Path(segment).unlink(missing_ok=True)
293
+ except Exception as e:
294
+ print(f"Error removing segment file {segment}: {str(e)}")
295
+
296
+ def get_video_qualities(video_url):
297
+ if not video_url.strip():
298
+ return []
299
+
300
+ with YoutubeDL({'quiet': True, 'no_warnings': True}) as ydl:
301
+ try:
302
+ info = ydl.extract_info(video_url, download=False)
303
+ formats = info.get('formats', [])
304
+
305
+ qualities = set()
306
+ for f in formats:
307
+ if f.get('vcodec') != 'none' and f.get('acodec') != 'none':
308
+ height = f.get('height')
309
+ if height:
310
+ qualities.add(f"{height}p")
311
+
312
+ return sorted(list(qualities), key=lambda x: int(x[:-1]), reverse=True)
313
+ except DownloadError:
314
+ return []
315
+ except Exception as e:
316
+ print(f"Error in get_video_qualities: {str(e)}")
317
+ return []
318
 
319
  # Disable Gradio analytics
320
  utils.colab_check = lambda: True
321
 
322
+ custom_css = """
323
+ :root {
324
+ --primary-hue: 210;
325
+ --secondary-hue: 160;
326
+ --neutral-hue: 0;
327
+ --primary-color: hsl(var(--primary-hue), 70%, 55%);
328
+ --secondary-color: hsl(var(--secondary-hue), 70%, 55%);
329
+ --neutral-color: hsl(var(--neutral-hue), 0%, 95%);
330
+ --text-color: hsl(var(--neutral-hue), 0%, 20%);
331
+ --border-radius: 8px;
332
+ }
333
+ body { font-family: 'Inter', sans-serif; margin: 0; padding: 0; height: 100vh; overflow: hidden; }
334
+ #component-0 { height: 100vh; overflow-y: auto; padding: 1rem; box-sizing: border-box; }
335
+ .container { max-width: 100%; margin: 0 auto; display: flex; flex-direction: column; height: 100%; }
336
+ .header { text-align: center; margin-bottom: 1rem; }
337
+ .main-content { display: flex; flex-wrap: wrap; gap: 1rem; flex-grow: 1; overflow-y: auto; }
338
+ .input-section, .segment-controls, .options-section, .output-section {
339
+ background-color: var(--neutral-color);
340
+ border-radius: var(--border-radius);
341
+ padding: 1rem;
342
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
343
+ flex: 1 1 300px;
344
+ }
345
+ .segment-input, .time-input, .button-row {
346
+ display: flex;
347
+ flex-wrap: wrap;
348
+ gap: 0.5rem;
349
+ align-items: center;
350
+ }
351
+ .time-input { width: 60px !important; text-align: center; }
352
+ .time-label, .separator { font-weight: bold; color: var(--primary-color); margin-bottom: 0.5rem; }
353
+ .button-row { justify-content: space-between; }
354
+ @media (max-width: 768px) {
355
+ .main-content { flex-direction: column; }
356
+ .input-section, .segment-controls, .options-section, .output-section { flex: 1 1 100%; }
357
+ }
358
+ .url-input { flex-grow: 1; }
359
+ .quality-dropdown { width: 100px !important; }
360
+ """
361
+
362
+ with gr.Blocks(
363
+ title="YouTube Segment Downloader",
364
+ theme=gr.themes.Soft(
365
+ primary_hue="blue",
366
+ secondary_hue="green",
367
+ neutral_hue="gray"
368
+ ).set(
369
+ body_text_color="*neutral_800",
370
+ background_fill_primary="*neutral_50",
371
+ button_primary_background_fill="*primary_500",
372
+ button_primary_background_fill_hover="*primary_600",
373
+ button_secondary_background_fill="*secondary_500",
374
+ button_secondary_background_fill_hover="*secondary_600",
375
+ ),
376
+ css=custom_css
377
+ ) as iface:
378
+ with gr.Column(elem_classes="container"):
379
+ gr.Markdown("# 🎬 YouTube Segment Downloader", elem_classes="header")
380
+
381
+ with gr.Row(elem_classes="main-content"):
382
+ with gr.Column(elem_classes="input-section"):
383
  with gr.Row():
384
+ video_url = gr.Textbox(
385
+ label="YouTube URL",
386
+ placeholder="Paste URL here",
387
+ elem_classes="url-input"
388
+ )
389
+ quality = gr.Dropdown(
390
+ label="Quality",
391
+ choices=[],
392
+ interactive=True,
393
+ elem_classes="quality-dropdown",
394
+ visible=False
395
+ )
396
+ url_status = gr.Markdown(visible=False)
397
+
398
+ with gr.Column(elem_classes="segment-controls"):
399
+ gr.Markdown("### Add Segments", elem_classes="time-label")
400
+ with gr.Row(elem_classes="segment-input"):
401
+ start_hours = gr.Number(label="Start HH", minimum=0, maximum=23, step=1, value=0, elem_classes="time-input")
402
+ start_minutes = gr.Number(label="MM", minimum=0, maximum=59, step=1, value=0, elem_classes="time-input")
403
+ start_seconds = gr.Number(label="SS", minimum=0, maximum=59, step=1, value=0, elem_classes="time-input")
404
+
405
+ gr.Markdown("to", elem_classes="separator")
406
+
407
+ end_hours = gr.Number(label="End HH", minimum=0, maximum=23, step=1, value=0, elem_classes="time-input")
408
+ end_minutes = gr.Number(label="MM", minimum=0, maximum=59, step=1, value=0, elem_classes="time-input")
409
+ end_seconds = gr.Number(label="SS", minimum=0, maximum=59, step=1, value=0, elem_classes="time_input")
410
+
411
+ add_btn = gr.Button("Add Segment", variant="primary")
412
+
413
+ segments = gr.Dataframe(
414
+ headers=["Segment"],
415
+ row_count=3,
416
+ col_count=1,
417
+ datatype=["str"],
418
+ interactive=True,
419
+ label="Segments"
420
+ )
421
+
422
+ with gr.Column(elem_classes="options-section"):
423
+ combine = gr.Checkbox(label="Combine segments", value=True)
424
+ audio_only = gr.Checkbox(label="Audio only", value=False)
425
+ remove_index = gr.Number(label="Remove segment", minimum=0, step=1, value=0)
426
+ remove_btn = gr.Button("Remove", variant="secondary")
427
+ old_index = gr.Number(label="Move from", minimum=0, step=1, value=0)
428
+ new_index = gr.Number(label="to", minimum=0, step=1, value=0)
429
+ move_btn = gr.Button("Move", variant="secondary")
430
+
431
+ submit_btn = gr.Button("🚀 Download", variant="primary")
432
 
433
+ with gr.Column(elem_classes="output-section"):
434
+ progress = gr.Slider(label="Progress", minimum=0, maximum=100, step=1, interactive=False)
435
+ status = gr.Textbox(label="Status", lines=1)
436
+ output_file = gr.File(label="Downloaded File")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
  add_btn.click(
439
  add_segment,
 
443
 
444
  submit_btn.click(
445
  process_video,
446
+ inputs=[video_url, segments, combine, audio_only],
447
  outputs=[progress, status, output_file]
448
  )
449
 
 
459
  outputs=[segments]
460
  )
461
 
462
+ def update_qualities(url):
463
+ qualities = get_video_qualities(url)
464
+ if qualities:
465
+ return (
466
+ gr.Dropdown(choices=qualities, value=qualities[0], visible=True),
467
+ gr.Markdown(visible=False)
468
+ )
469
+ elif url.strip(): # Only show error if URL is not empty
470
+ return (
471
+ gr.Dropdown(choices=[], visible=False),
472
+ gr.Markdown("Unable to fetch video qualities. The URL might be invalid or the video might be unavailable.", visible=True)
473
+ )
474
+ else:
475
+ return (
476
+ gr.Dropdown(choices=[], visible=False),
477
+ gr.Markdown(visible=False)
478
+ )
479
+
480
+ video_url.change(
481
+ update_qualities,
482
+ inputs=[video_url],
483
+ outputs=[quality, url_status]
484
+ )
485
+
486
+ iface.launch(server_name="0.0.0.0", server_port=7860)