import gradio as gr from yt_dlp import YoutubeDL import tempfile import os import subprocess def download_snippet(url, start_sec, end_sec): """Download and trim audio snippet with custom start/end times""" # Create temp directory temp_dir = tempfile.mkdtemp() try: # Validate times if start_sec >= end_sec: raise Exception("Start time must be before end time") duration = end_sec - start_sec if duration > 600: # Limit to 10 minutes max raise Exception("Maximum duration is 600 seconds (10 minutes)") # First, download the full track (or best we can get) ydl_opts = { 'format': 'bestaudio/best', 'outtmpl': os.path.join(temp_dir, 'full_audio.%(ext)s'), 'quiet': True, 'no_warnings': True, 'noplaylist': True, } with YoutubeDL(ydl_opts) as ydl: # Get info for filename info = ydl.extract_info(url, download=False) title = info.get('title', 'soundcloud_track') duration_full = info.get('duration', 0) # Validate against full duration if duration_full and end_sec > duration_full: raise Exception(f"End time ({end_sec}s) exceeds track duration ({duration_full}s)") safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).rstrip() # Download ydl.download([url]) # Find the downloaded file downloaded_files = [f for f in os.listdir(temp_dir) if f.startswith('full_audio')] if not downloaded_files: raise Exception("No file was downloaded") input_file = os.path.join(temp_dir, downloaded_files[0]) # Check if file exists and has content if not os.path.exists(input_file) or os.path.getsize(input_file) == 0: raise Exception("Downloaded file is empty") # Create output filename output_file = os.path.join(temp_dir, f"{safe_title}_{start_sec}-{end_sec}s.mp3") # Use ffmpeg to trim with start and end times cmd = [ 'ffmpeg', '-i', input_file, # Input file '-ss', str(start_sec), # Start time '-to', str(end_sec), # End time '-acodec', 'libmp3lame', # MP3 codec '-q:a', '2', # Good quality '-y', # Overwrite output output_file ] # Run ffmpeg result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise Exception(f"FFmpeg error: {result.stderr}") # Check if output was created if not os.path.exists(output_file) or os.path.getsize(output_file) == 0: raise Exception("Trimmed file is empty") return output_file, f"{safe_title}_{start_sec}-{end_sec}s.mp3", duration_full except Exception as e: # Clean up on error if os.path.exists(temp_dir): import shutil shutil.rmtree(temp_dir, ignore_errors=True) raise e # Simple Gradio interface with gr.Blocks(title="SoundCloud Snippet", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # 🎵 SoundCloud Snippet Downloader Download any segment of a public SoundCloud track """) with gr.Row(): url = gr.Textbox( label="SoundCloud URL", placeholder="https://soundcloud.com/artist/track-name", value="https://soundcloud.com/emma-eline-pihlstr-m/have-yourself-a-merry-little-christmas", lines=2 ) with gr.Row(): with gr.Column(scale=1): start_slider = gr.Slider( minimum=0, maximum=600, value=0, step=1, label="Start Time (seconds)" ) start_number = gr.Number( value=0, label="Start (seconds)", precision=0, minimum=0, maximum=600 ) with gr.Column(scale=1): end_slider = gr.Slider( minimum=1, maximum=600, value=30, step=1, label="End Time (seconds)" ) end_number = gr.Number( value=30, label="End (seconds)", precision=0, minimum=1, maximum=600 ) with gr.Column(scale=1): duration_display = gr.Textbox( label="Segment Duration", value="30 seconds", interactive=False ) max_duration = gr.Textbox( label="Track Duration", value="Unknown", interactive=False, visible=False ) with gr.Row(): download_btn = gr.Button("Download Snippet", variant="primary") with gr.Row(): audio_player = gr.Audio(label="Preview", type="filepath") download_file = gr.DownloadButton("Save MP3", visible=False) # Store file path and track duration file_path = gr.State() track_duration = gr.State(0) # Sync sliders and number inputs def sync_start(start_val): return start_val, start_val def sync_end(end_val): return end_val, end_val start_slider.change( sync_start, inputs=[start_slider], outputs=[start_number, start_slider] ) start_number.change( sync_start, inputs=[start_number], outputs=[start_slider, start_number] ) end_slider.change( sync_end, inputs=[end_slider], outputs=[end_number, end_slider] ) end_number.change( sync_end, inputs=[end_number], outputs=[end_slider, end_number] ) # Update duration display def update_duration(start, end): duration = end - start if duration <= 0: return "Invalid (start must be before end)", gr.update(visible=False) return f"{duration} seconds", gr.update(visible=True) start_slider.change( update_duration, inputs=[start_slider, end_slider], outputs=[duration_display, download_btn] ) end_slider.change( update_duration, inputs=[start_slider, end_slider], outputs=[duration_display, download_btn] ) def process_download(url, start, end): if not url or 'soundcloud.com' not in url.lower(): raise gr.Error("Please enter a valid SoundCloud URL") if start >= end: raise gr.Error("Start time must be before end time") try: filepath, filename, full_duration = download_snippet(url, start, end) # Update max duration display if we have it max_dur_update = gr.update( value=f"{full_duration} seconds" if full_duration > 0 else "Unknown", visible=True ) return { audio_player: filepath, download_file: gr.DownloadButton(visible=True), file_path: filepath, max_duration: max_dur_update, track_duration: full_duration } except Exception as e: raise gr.Error(f"Download failed: {str(e)}") download_btn.click( process_download, inputs=[url, start_slider, end_slider], outputs=[audio_player, download_file, file_path, max_duration, track_duration] ) download_file.click( lambda x: x if x and os.path.exists(x) else None, inputs=[file_path], outputs=None ) # Auto-adjust end slider when track duration is known def adjust_sliders(full_duration): if full_duration and full_duration > 0: return ( gr.update(maximum=min(600, full_duration)), gr.update(maximum=min(600, full_duration), value=min(30, full_duration)), gr.update(maximum=min(600, full_duration)), gr.update(maximum=min(600, full_duration), value=min(30, full_duration)) ) return ( gr.update(maximum=600), gr.update(maximum=600), gr.update(maximum=600), gr.update(maximum=600) ) # This would need to be triggered after we get track duration # For simplicity, we'll update when the download completes if __name__ == "__main__": demo.launch()