|
|
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""" |
|
|
|
|
|
temp_dir = tempfile.mkdtemp() |
|
|
|
|
|
try: |
|
|
|
|
|
if start_sec >= end_sec: |
|
|
raise Exception("Start time must be before end time") |
|
|
|
|
|
duration = end_sec - start_sec |
|
|
if duration > 600: |
|
|
raise Exception("Maximum duration is 600 seconds (10 minutes)") |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
info = ydl.extract_info(url, download=False) |
|
|
title = info.get('title', 'soundcloud_track') |
|
|
duration_full = info.get('duration', 0) |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
ydl.download([url]) |
|
|
|
|
|
|
|
|
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]) |
|
|
|
|
|
|
|
|
if not os.path.exists(input_file) or os.path.getsize(input_file) == 0: |
|
|
raise Exception("Downloaded file is empty") |
|
|
|
|
|
|
|
|
output_file = os.path.join(temp_dir, f"{safe_title}_{start_sec}-{end_sec}s.mp3") |
|
|
|
|
|
|
|
|
cmd = [ |
|
|
'ffmpeg', |
|
|
'-i', input_file, |
|
|
'-ss', str(start_sec), |
|
|
'-to', str(end_sec), |
|
|
'-acodec', 'libmp3lame', |
|
|
'-q:a', '2', |
|
|
'-y', |
|
|
output_file |
|
|
] |
|
|
|
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True) |
|
|
|
|
|
if result.returncode != 0: |
|
|
raise Exception(f"FFmpeg error: {result.stderr}") |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
if os.path.exists(temp_dir): |
|
|
import shutil |
|
|
shutil.rmtree(temp_dir, ignore_errors=True) |
|
|
raise e |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
file_path = gr.State() |
|
|
track_duration = gr.State(0) |
|
|
|
|
|
|
|
|
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] |
|
|
) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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 |
|
|
) |
|
|
|
|
|
|
|
|
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) |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch() |
|
|
|