RO-Rtechs commited on
Commit
6586801
·
verified ·
1 Parent(s): a2fa9e8

Upload 3 files

Browse files

Added segment downloader

Files changed (3) hide show
  1. app.py +239 -0
  2. packages.txt +1 -0
  3. requirements.txt +3 -0
app.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from gradio import utils
3
+ import os
4
+ import re
5
+ 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
+
13
+ def sanitize_title(title):
14
+ return re.sub(r'[\\/*?:"<>|]', "", title)
15
+
16
+ def format_time(seconds):
17
+ return time.strftime('%H:%M:%S', time.gmtime(seconds))
18
+
19
+ def get_video_info(video_url):
20
+ with YoutubeDL({'quiet': True, 'no_warnings': True}) as ydl:
21
+ try:
22
+ info = ydl.extract_info(video_url, download=False)
23
+ formats = info.get('formats', [])
24
+
25
+ # Function to safely get bitrate
26
+ def get_bitrate(format_dict, key):
27
+ return format_dict.get(key, 0) or 0
28
+
29
+ # Prefer adaptive formats (separate video and audio)
30
+ video_formats = [f for f in formats if f.get('vcodec') != 'none' and f.get('acodec') == 'none']
31
+ audio_formats = [f for f in formats if f.get('acodec') != 'none' and f.get('vcodec') == 'none']
32
+
33
+ if video_formats and audio_formats:
34
+ video_format = max(video_formats, key=lambda f: get_bitrate(f, 'vbr'))
35
+ audio_format = max(audio_formats, key=lambda f: get_bitrate(f, 'abr'))
36
+ return info['title'], video_format['url'], audio_format['url']
37
+ else:
38
+ # Fallback to best combined format
39
+ combined_formats = [f for f in formats if f.get('vcodec') != 'none' and f.get('acodec') != 'none']
40
+ if combined_formats:
41
+ best_format = max(combined_formats, key=lambda f: get_bitrate(f, 'tbr'))
42
+ return info['title'], best_format['url'], None
43
+ else:
44
+ raise Exception("No suitable video formats found")
45
+ except Exception as e:
46
+ raise Exception(f"Error extracting video info: {str(e)}")
47
+
48
+ def download_segment(url, start_time, end_time, output_path):
49
+ command = [
50
+ 'ffmpeg',
51
+ '-ss', format_time(start_time),
52
+ '-i', url,
53
+ '-t', format_time(end_time - start_time),
54
+ '-c', 'copy',
55
+ '-y',
56
+ output_path
57
+ ]
58
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
59
+
60
+ while True:
61
+ output = process.stderr.readline()
62
+ if output == '' and process.poll() is not None:
63
+ break
64
+ if output:
65
+ yield output.strip()
66
+
67
+ rc = process.poll()
68
+ return rc == 0
69
+
70
+ def combine_segments(video_segments, audio_segments, output_path):
71
+ temp_video = 'temp_video.mp4'
72
+ temp_audio = 'temp_audio.m4a'
73
+
74
+ # Concatenate video segments
75
+ with open('video_list.txt', 'w') as f:
76
+ for segment in video_segments:
77
+ f.write(f"file '{segment}'\n")
78
+
79
+ subprocess.run(['ffmpeg', '-f', 'concat', '-safe', '0', '-i', 'video_list.txt', '-c', 'copy', temp_video])
80
+
81
+ # Concatenate audio segments if they exist
82
+ if audio_segments:
83
+ with open('audio_list.txt', 'w') as f:
84
+ for segment in audio_segments:
85
+ f.write(f"file '{segment}'\n")
86
+ subprocess.run(['ffmpeg', '-f', 'concat', '-safe', '0', '-i', 'audio_list.txt', '-c', 'copy', temp_audio])
87
+
88
+ # Combine video and audio
89
+ subprocess.run(['ffmpeg', '-i', temp_video, '-i', temp_audio, '-c', 'copy', output_path])
90
+ else:
91
+ shutil.move(temp_video, output_path)
92
+
93
+ # Clean up temporary files
94
+ os.remove('video_list.txt')
95
+ if os.path.exists('audio_list.txt'):
96
+ os.remove('audio_list.txt')
97
+ if os.path.exists(temp_video):
98
+ os.remove(temp_video)
99
+ if os.path.exists(temp_audio):
100
+ os.remove(temp_audio)
101
+
102
+ def add_segment(start_hours, start_minutes, start_seconds, end_hours, end_minutes, end_seconds, segments):
103
+ start_time = f"{start_hours:02d}:{start_minutes:02d}:{start_seconds:02d}"
104
+ end_time = f"{end_hours:02d}:{end_minutes:02d}:{end_seconds:02d}"
105
+ new_segment = f"{start_time}-{end_time}"
106
+ new_row = [new_segment]
107
+ return segments + [new_row]
108
+
109
+ def remove_segment(segments, index):
110
+ return segments[:index] + segments[index+1:]
111
+
112
+ def move_segment(segments, old_index, new_index):
113
+ segments = segments.tolist() # Convert Dataframe to list
114
+ segment = segments.pop(old_index)
115
+ segments.insert(new_index, segment)
116
+ return segments
117
+
118
+ def parse_segments(segments: List[str]) -> List[Tuple[int, int]]:
119
+ parsed_segments = []
120
+ for segment in segments:
121
+ start, end = map(lambda x: sum(int(i) * 60 ** j for j, i in enumerate(reversed(x.split(':')))), segment.split('-'))
122
+ if start < end:
123
+ parsed_segments.append((start, end))
124
+ return parsed_segments
125
+
126
+ def process_video(video_url, segments, combine, progress=gr.Progress()):
127
+ if not video_url.strip():
128
+ return 0, "Error: Please provide a valid YouTube URL", None
129
+
130
+ # Extract segments from the Dataframe
131
+ segment_list = [segment[0] for segment in segments if segment[0].strip()]
132
+ parsed_segments = parse_segments(segment_list)
133
+ if not parsed_segments:
134
+ return 0, "Error: No valid segments provided", None
135
+
136
+ output_dir = 'output'
137
+ os.makedirs(output_dir, exist_ok=True)
138
+
139
+ try:
140
+ video_title, video_url, audio_url = get_video_info(video_url)
141
+ except Exception as e:
142
+ return 0, f"Error: {str(e)}", None
143
+
144
+ video_segments = []
145
+ audio_segments = []
146
+ total_segments = len(parsed_segments)
147
+
148
+ for i, (start_time, end_time) in enumerate(parsed_segments):
149
+ video_output = os.path.join(output_dir, f"{sanitize_title(video_title)}_video_segment_{i+1}.mp4")
150
+ for output in download_segment(video_url, start_time, end_time, video_output):
151
+ progress((i / total_segments) + (1 / total_segments) * 0.5)
152
+ yield i * 100 // total_segments, f"Downloading video segment {i+1}/{total_segments}: {output}", None
153
+ video_segments.append(video_output)
154
+
155
+ if audio_url:
156
+ audio_output = os.path.join(output_dir, f"{sanitize_title(video_title)}_audio_segment_{i+1}.m4a")
157
+ for output in download_segment(audio_url, start_time, end_time, audio_output):
158
+ progress((i / total_segments) + (1 / total_segments) * 0.75)
159
+ yield i * 100 // total_segments + 50, f"Downloading audio segment {i+1}/{total_segments}: {output}", None
160
+ audio_segments.append(audio_output)
161
+
162
+ if combine:
163
+ output_path = os.path.join(output_dir, f"{sanitize_title(video_title)}_combined.mp4")
164
+ combine_segments(video_segments, audio_segments, output_path)
165
+ yield 100, f"Segments combined and saved as {output_path}", output_path
166
+ else:
167
+ # If not combining, return the first video segment (you might want to modify this behavior)
168
+ output_path = video_segments[0] if video_segments else None
169
+ yield 100, "All segments downloaded successfully", output_path
170
+
171
+ # Clean up individual segments if combined
172
+ if combine:
173
+ for segment in video_segments + audio_segments:
174
+ os.remove(segment)
175
+
176
+ # Disable Gradio analytics
177
+ utils.colab_check = lambda: True
178
+
179
+ with gr.Blocks(title="Advanced YouTube Segment Downloader", theme=gr.themes.Soft()) as iface:
180
+ gr.Markdown("## Advanced YouTube Segment Downloader")
181
+ gr.Markdown("Download segments of YouTube videos using adaptive streaming and ffmpeg, with optional combining.")
182
+
183
+ with gr.Row():
184
+ video_url = gr.Textbox(label="YouTube URL", placeholder="Enter YouTube URL here")
185
+
186
+ with gr.Row():
187
+ with gr.Column(scale=1):
188
+ with gr.Row():
189
+ start_hours = gr.Number(label="Start Hours", minimum=0, maximum=23, step=1, value=0)
190
+ start_minutes = gr.Number(label="Start Minutes", minimum=0, maximum=59, step=1, value=0)
191
+ start_seconds = gr.Number(label="Start Seconds", minimum=0, maximum=59, step=1, value=0)
192
+ with gr.Row():
193
+ end_hours = gr.Number(label="End Hours", minimum=0, maximum=23, step=1, value=0)
194
+ end_minutes = gr.Number(label="End Minutes", minimum=0, maximum=59, step=1, value=0)
195
+ end_seconds = gr.Number(label="End Seconds", minimum=0, maximum=59, step=1, value=0)
196
+ add_btn = gr.Button("Add Segment")
197
+
198
+ with gr.Column(scale=2):
199
+ segments = gr.Dataframe(
200
+ headers=["Segment"],
201
+ row_count=5,
202
+ col_count=1,
203
+ interactive=True,
204
+ label="Segments"
205
+ )
206
+
207
+ combine = gr.Checkbox(label="Combine Segments")
208
+ submit_btn = gr.Button("Download Segments", variant="primary")
209
+
210
+ progress = gr.Slider(label="Progress", minimum=0, maximum=100, step=1)
211
+ status = gr.Textbox(label="Status", lines=10)
212
+ output_file = gr.File(label="Download Video")
213
+
214
+ add_btn.click(
215
+ add_segment,
216
+ inputs=[start_hours, start_minutes, start_seconds, end_hours, end_minutes, end_seconds, segments],
217
+ outputs=[segments]
218
+ )
219
+
220
+ submit_btn.click(
221
+ process_video,
222
+ inputs=[video_url, segments, combine],
223
+ outputs=[progress, status, output_file]
224
+ )
225
+
226
+ segments.change(
227
+ move_segment,
228
+ inputs=[segments, gr.Slider(0, 100, step=1, label="Old Index"), gr.Slider(0, 100, step=1, label="New Index")],
229
+ outputs=[segments]
230
+ )
231
+
232
+ remove_btn = gr.Button("Remove Selected Segment")
233
+ remove_btn.click(
234
+ remove_segment,
235
+ inputs=[segments, gr.Slider(0, 100, step=1, label="Index to Remove")],
236
+ outputs=[segments]
237
+ )
238
+
239
+ iface.launch()
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio
2
+ yt-dlp
3
+ ffmpeg-python