MySafeCode commited on
Commit
0ffadf4
·
verified ·
1 Parent(s): 5eb395b

Create app2.py

Browse files
Files changed (1) hide show
  1. app2.py +405 -0
app2.py ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from yt_dlp import YoutubeDL
3
+ import tempfile
4
+ import os
5
+ import subprocess
6
+ import traceback
7
+ import shutil
8
+
9
+ # Constants
10
+ MAX_DURATION = 300 # 5 minutes max
11
+ DEFAULT_URL = "https://soundcloud.com/emma-eline-pihlstr-m/have-yourself-a-merry-little-christmas"
12
+ DEFAULT_DURATION = 30
13
+
14
+ def download_and_trim(url, duration_sec):
15
+ """Two-step process: download full audio then trim"""
16
+ # Create temporary directory
17
+ temp_dir = tempfile.mkdtemp()
18
+
19
+ try:
20
+ # Step 1: Download with yt-dlp
21
+ print(f"Downloading from: {url}")
22
+
23
+ # First get info for title
24
+ with YoutubeDL({'quiet': True, 'no_warnings': True}) as ydl:
25
+ info = ydl.extract_info(url, download=False)
26
+ if not info:
27
+ raise Exception("Could not fetch track information")
28
+
29
+ title = info.get('title', 'soundcloud_track')
30
+ # Create safe filename
31
+ safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).rstrip()
32
+ safe_title = safe_title[:100] # Limit length
33
+
34
+ # Download options
35
+ download_path = os.path.join(temp_dir, "original")
36
+ ydl_opts = {
37
+ 'format': 'bestaudio/best',
38
+ 'outtmpl': download_path + '.%(ext)s',
39
+ 'quiet': True,
40
+ 'no_warnings': True,
41
+ 'noplaylist': True,
42
+ 'extractaudio': True,
43
+ 'audioformat': 'mp3',
44
+ 'postprocessors': [{
45
+ 'key': 'FFmpegExtractAudio',
46
+ 'preferredcodec': 'mp3',
47
+ 'preferredquality': '192',
48
+ }],
49
+ 'socket_timeout': 30,
50
+ 'retries': 3,
51
+ }
52
+
53
+ # Download
54
+ with YoutubeDL(ydl_opts) as ydl:
55
+ ydl.download([url])
56
+
57
+ # Find the downloaded file
58
+ downloaded_files = [f for f in os.listdir(temp_dir)
59
+ if f.startswith('original') and f.endswith('.mp3')]
60
+
61
+ if not downloaded_files:
62
+ # Try to find any audio file
63
+ downloaded_files = [f for f in os.listdir(temp_dir)
64
+ if f.endswith(('.mp3', '.webm', '.m4a'))]
65
+
66
+ if not downloaded_files:
67
+ raise Exception("No audio file was downloaded")
68
+
69
+ input_file = os.path.join(temp_dir, downloaded_files[0])
70
+
71
+ # Verify file
72
+ if not os.path.exists(input_file):
73
+ raise Exception("Downloaded file not found")
74
+
75
+ file_size = os.path.getsize(input_file)
76
+ if file_size == 0:
77
+ raise Exception("Downloaded file is empty")
78
+
79
+ print(f"Downloaded: {input_file} ({file_size} bytes)")
80
+
81
+ # Step 2: Trim with ffmpeg
82
+ output_filename = f"{safe_title}_{duration_sec}s.mp3"
83
+ output_file = os.path.join(temp_dir, output_filename)
84
+
85
+ # Use ffmpeg to trim
86
+ cmd = [
87
+ 'ffmpeg',
88
+ '-i', input_file, # Input file
89
+ '-t', str(duration_sec), # Duration to keep
90
+ '-acodec', 'libmp3lame', # MP3 codec
91
+ '-q:a', '2', # Good quality (0-9, 2=good)
92
+ '-metadata', f'title={safe_title} [{duration_sec}s snippet]',
93
+ '-metadata', f'comment=Snippet from {url}',
94
+ '-y', # Overwrite output
95
+ output_file
96
+ ]
97
+
98
+ print(f"Trimming with command: {' '.join(cmd)}")
99
+ result = subprocess.run(cmd, capture_output=True, text=True)
100
+
101
+ if result.returncode != 0:
102
+ print(f"FFmpeg stderr: {result.stderr}")
103
+ raise Exception(f"Trimming failed: {result.stderr[:200]}")
104
+
105
+ # Verify trimmed file
106
+ if not os.path.exists(output_file):
107
+ raise Exception("Trimmed file was not created")
108
+
109
+ trimmed_size = os.path.getsize(output_file)
110
+ if trimmed_size == 0:
111
+ raise Exception("Trimmed file is empty")
112
+
113
+ print(f"Created: {output_file} ({trimmed_size} bytes)")
114
+
115
+ # Clean up original file
116
+ try:
117
+ os.unlink(input_file)
118
+ except:
119
+ pass
120
+
121
+ return output_file, output_filename
122
+
123
+ except Exception as e:
124
+ # Clean up on error
125
+ if os.path.exists(temp_dir):
126
+ shutil.rmtree(temp_dir, ignore_errors=True)
127
+ print(f"Error: {str(e)}")
128
+ traceback.print_exc()
129
+ raise e
130
+
131
+ def validate_url(url):
132
+ """Validate SoundCloud URL"""
133
+ if not url or not url.strip():
134
+ return False, "Please enter a URL"
135
+
136
+ url_lower = url.lower()
137
+ if 'soundcloud.com' not in url_lower:
138
+ return False, "Please enter a SoundCloud URL"
139
+
140
+ # Check if it's a track, not a playlist or user page
141
+ if '/sets/' in url_lower:
142
+ return False, "Playlists not supported. Please use a track URL."
143
+
144
+ # Count slashes to guess if it's a track
145
+ parts = url_lower.replace('https://', '').replace('http://', '').split('/')
146
+ if len(parts) < 3 or not parts[2]:
147
+ return False, "Please enter a specific track URL"
148
+
149
+ return True, ""
150
+
151
+ # Create the Gradio app
152
+ with gr.Blocks(
153
+ title="SoundCloud Snippet Generator",
154
+ theme=gr.themes.Soft(),
155
+ css="""
156
+ .gradio-container { max-width: 1000px !important; margin: auto; }
157
+ .header { text-align: center; margin-bottom: 20px; }
158
+ .success { color: #28a745; font-weight: bold; }
159
+ .error { color: #dc3545; font-weight: bold; }
160
+ .warning { background-color: #fff3cd; padding: 10px; border-radius: 5px; }
161
+ .example-url {
162
+ background: #f8f9fa;
163
+ padding: 5px 10px;
164
+ border-radius: 5px;
165
+ margin: 5px 0;
166
+ cursor: pointer;
167
+ }
168
+ .example-url:hover { background: #e9ecef; }
169
+ """
170
+ ) as demo:
171
+
172
+ # Header
173
+ gr.Markdown("""
174
+ <div class="header">
175
+ <h1>🎵 SoundCloud Snippet Generator</h1>
176
+ <p>Download the first <i>N</i> seconds of any public SoundCloud track</p>
177
+ </div>
178
+ """)
179
+
180
+ # Main content
181
+ with gr.Row():
182
+ # Left column - Inputs
183
+ with gr.Column(scale=2):
184
+ # URL input with default
185
+ url_input = gr.Textbox(
186
+ label="SoundCloud Track URL",
187
+ placeholder="https://soundcloud.com/artist/track-name",
188
+ value=DEFAULT_URL,
189
+ elem_id="url_input"
190
+ )
191
+
192
+ # URL validation status
193
+ url_status = gr.Markdown("", elem_id="url_status")
194
+
195
+ # Duration controls
196
+ with gr.Row():
197
+ duration_slider = gr.Slider(
198
+ minimum=5,
199
+ maximum=MAX_DURATION,
200
+ value=DEFAULT_DURATION,
201
+ step=5,
202
+ label=f"Duration (5-{MAX_DURATION} seconds)"
203
+ )
204
+ duration_number = gr.Number(
205
+ value=DEFAULT_DURATION,
206
+ label="Seconds",
207
+ precision=0,
208
+ minimum=5,
209
+ maximum=MAX_DURATION
210
+ )
211
+
212
+ # Link slider and number
213
+ duration_slider.change(
214
+ lambda x: x,
215
+ inputs=[duration_slider],
216
+ outputs=[duration_number]
217
+ )
218
+ duration_number.change(
219
+ lambda x: x,
220
+ inputs=[duration_number],
221
+ outputs=[duration_slider]
222
+ )
223
+
224
+ # Example tracks
225
+ with gr.Accordion("🎯 Try these example tracks", open=False):
226
+ gr.Markdown("""
227
+ <div style="font-size: 0.9em;">
228
+ <div class="example-url" onclick="document.getElementById('url_input').value='https://soundcloud.com/emma-eline-pihlstr-m/have-yourself-a-merry-little-christmas'">🎄 Have Yourself a Merry Little Christmas</div>
229
+ <div class="example-url" onclick="document.getElementById('url_input').value='https://soundcloud.com/nocopyrightsounds/that-new-new'">🎵 That New New - NCS</div>
230
+ <div class="example-url" onclick="document.getElementById('url_input').value='https://soundcloud.com/lofi_girl/coffee-jazz-music'">☕ Coffee Jazz - Lofi Girl</div>
231
+ <div class="example-url" onclick="document.getElementById('url_input').value='https://soundcloud.com/chillhopdotcom/blue-window'">🔵 Blue Window - Chillhop</div>
232
+ </div>
233
+ """)
234
+
235
+ # Generate button
236
+ generate_btn = gr.Button(
237
+ "🎬 Generate Snippet",
238
+ variant="primary",
239
+ size="lg",
240
+ elem_id="generate_btn"
241
+ )
242
+
243
+ # Status display
244
+ status_display = gr.Markdown("", elem_id="status")
245
+
246
+ # Download section
247
+ with gr.Group():
248
+ gr.Markdown("### 💾 Download")
249
+ filename_display = gr.Textbox(
250
+ label="File will be saved as:",
251
+ interactive=False,
252
+ elem_id="filename"
253
+ )
254
+ download_btn = gr.DownloadButton(
255
+ "⬇️ Download MP3",
256
+ visible=False,
257
+ elem_id="download_btn"
258
+ )
259
+
260
+ # Right column - Output and info
261
+ with gr.Column(scale=1):
262
+ # Audio preview
263
+ audio_preview = gr.Audio(
264
+ label="🎧 Preview",
265
+ type="filepath",
266
+ interactive=False,
267
+ visible=False,
268
+ elem_id="audio_preview"
269
+ )
270
+
271
+ # Info box
272
+ gr.Markdown("""
273
+ <div class="warning">
274
+ <h4>⚠️ Important Notes</h4>
275
+ <ul>
276
+ <li>Works with <b>public tracks only</b></li>
277
+ <li>Some tracks have download restrictions</li>
278
+ <li>Maximum duration: 5 minutes</li>
279
+ <li>Processing takes 10-30 seconds</li>
280
+ <li>Files are automatically deleted after download</li>
281
+ </ul>
282
+ </div>
283
+
284
+ <h4>✅ How to use:</h4>
285
+ <ol>
286
+ <li>Paste a SoundCloud URL</li>
287
+ <li>Set the duration</li>
288
+ <li>Click "Generate Snippet"</li>
289
+ <li>Preview the audio</li>
290
+ <li>Click "Download MP3"</li>
291
+ </ol>
292
+ """)
293
+
294
+ # Store file path
295
+ file_path = gr.State()
296
+
297
+ # URL validation function
298
+ def on_url_change(url):
299
+ is_valid, msg = validate_url(url)
300
+ if not url:
301
+ return gr.Markdown("", visible=False)
302
+ elif is_valid:
303
+ return gr.Markdown("✅ Valid SoundCloud track URL", visible=True)
304
+ else:
305
+ return gr.Markdown(f"⚠️ {msg}", visible=True)
306
+
307
+ # Main processing function
308
+ def process_snippet(url, duration):
309
+ # Clear previous outputs
310
+ yield {
311
+ status_display: "⏳ Starting download...",
312
+ audio_preview: gr.Audio(visible=False),
313
+ download_btn: gr.DownloadButton(visible=False),
314
+ filename_display: "",
315
+ }
316
+
317
+ # Validate
318
+ is_valid, msg = validate_url(url)
319
+ if not is_valid:
320
+ yield {
321
+ status_display: f"❌ {msg}",
322
+ audio_preview: gr.Audio(visible=False),
323
+ download_btn: gr.DownloadButton(visible=False),
324
+ }
325
+ return
326
+
327
+ try:
328
+ # Step 1: Download
329
+ yield {
330
+ status_display: "⏳ Downloading track...",
331
+ audio_preview: gr.Audio(visible=False),
332
+ download_btn: gr.DownloadButton(visible=False),
333
+ }
334
+
335
+ filepath, filename = download_and_trim(url, duration)
336
+
337
+ # Step 2: Success
338
+ yield {
339
+ status_display: "✅ Snippet created successfully!",
340
+ audio_preview: gr.Audio(value=filepath, visible=True),
341
+ download_btn: gr.DownloadButton(visible=True),
342
+ filename_display: filename,
343
+ file_path: filepath,
344
+ }
345
+
346
+ except Exception as e:
347
+ error_msg = str(e)
348
+ # Clean up error message
349
+ if "Private" in error_msg or "not accessible" in error_msg:
350
+ error_msg = "Track is private or not accessible"
351
+ elif "unavailable" in error_msg or "not found" in error_msg:
352
+ error_msg = "Track not found or unavailable"
353
+ elif "Copyright" in error_msg or "restricted" in error_msg:
354
+ error_msg = "Track has download restrictions"
355
+
356
+ yield {
357
+ status_display: f"❌ {error_msg}",
358
+ audio_preview: gr.Audio(visible=False),
359
+ download_btn: gr.DownloadButton(visible=False),
360
+ filename_display: "",
361
+ }
362
+
363
+ # Set up interactions
364
+ url_input.change(
365
+ fn=on_url_change,
366
+ inputs=[url_input],
367
+ outputs=[url_status]
368
+ )
369
+
370
+ generate_btn.click(
371
+ fn=process_snippet,
372
+ inputs=[url_input, duration_slider],
373
+ outputs=[
374
+ status_display,
375
+ audio_preview,
376
+ download_btn,
377
+ filename_display,
378
+ file_path
379
+ ]
380
+ )
381
+
382
+ # Download button
383
+ download_btn.click(
384
+ fn=lambda fp: fp if fp and os.path.exists(fp) else None,
385
+ inputs=[file_path],
386
+ outputs=None
387
+ )
388
+
389
+ # Footer
390
+ gr.Markdown("---")
391
+ gr.Markdown("""
392
+ <div style="text-align: center; color: #666; font-size: 0.9em;">
393
+ <p>Built with ❤️ using Gradio & yt-dlp</p>
394
+ <p>Respect artists' rights. For personal use only.</p>
395
+ </div>
396
+ """)
397
+
398
+ # Launch the app
399
+ if __name__ == "__main__":
400
+ demo.launch(
401
+ server_name="0.0.0.0",
402
+ share=False,
403
+ debug=False,
404
+ show_error=True
405
+ )