|
|
import gradio as gr |
|
|
from yt_dlp import YoutubeDL |
|
|
import tempfile |
|
|
import os |
|
|
import subprocess |
|
|
import traceback |
|
|
import shutil |
|
|
|
|
|
|
|
|
MAX_DURATION = 300 |
|
|
DEFAULT_URL = "https://soundcloud.com/emma-eline-pihlstr-m/have-yourself-a-merry-little-christmas" |
|
|
DEFAULT_DURATION = 30 |
|
|
|
|
|
def download_and_trim(url, duration_sec): |
|
|
"""Two-step process: download full audio then trim""" |
|
|
|
|
|
temp_dir = tempfile.mkdtemp() |
|
|
|
|
|
try: |
|
|
|
|
|
print(f"Downloading from: {url}") |
|
|
|
|
|
|
|
|
with YoutubeDL({'quiet': True, 'no_warnings': True}) as ydl: |
|
|
info = ydl.extract_info(url, download=False) |
|
|
if not info: |
|
|
raise Exception("Could not fetch track information") |
|
|
|
|
|
title = info.get('title', 'soundcloud_track') |
|
|
|
|
|
safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).rstrip() |
|
|
safe_title = safe_title[:100] |
|
|
|
|
|
|
|
|
download_path = os.path.join(temp_dir, "original") |
|
|
ydl_opts = { |
|
|
'format': 'bestaudio/best', |
|
|
'outtmpl': download_path + '.%(ext)s', |
|
|
'quiet': True, |
|
|
'no_warnings': True, |
|
|
'noplaylist': True, |
|
|
'extractaudio': True, |
|
|
'audioformat': 'mp3', |
|
|
'postprocessors': [{ |
|
|
'key': 'FFmpegExtractAudio', |
|
|
'preferredcodec': 'mp3', |
|
|
'preferredquality': '192', |
|
|
}], |
|
|
'socket_timeout': 30, |
|
|
'retries': 3, |
|
|
} |
|
|
|
|
|
|
|
|
with YoutubeDL(ydl_opts) as ydl: |
|
|
ydl.download([url]) |
|
|
|
|
|
|
|
|
downloaded_files = [f for f in os.listdir(temp_dir) |
|
|
if f.startswith('original') and f.endswith('.mp3')] |
|
|
|
|
|
if not downloaded_files: |
|
|
|
|
|
downloaded_files = [f for f in os.listdir(temp_dir) |
|
|
if f.endswith(('.mp3', '.webm', '.m4a'))] |
|
|
|
|
|
if not downloaded_files: |
|
|
raise Exception("No audio file was downloaded") |
|
|
|
|
|
input_file = os.path.join(temp_dir, downloaded_files[0]) |
|
|
|
|
|
|
|
|
if not os.path.exists(input_file): |
|
|
raise Exception("Downloaded file not found") |
|
|
|
|
|
file_size = os.path.getsize(input_file) |
|
|
if file_size == 0: |
|
|
raise Exception("Downloaded file is empty") |
|
|
|
|
|
print(f"Downloaded: {input_file} ({file_size} bytes)") |
|
|
|
|
|
|
|
|
output_filename = f"{safe_title}_{duration_sec}s.mp3" |
|
|
output_file = os.path.join(temp_dir, output_filename) |
|
|
|
|
|
|
|
|
cmd = [ |
|
|
'ffmpeg', |
|
|
'-i', input_file, |
|
|
'-t', str(duration_sec), |
|
|
'-acodec', 'libmp3lame', |
|
|
'-q:a', '2', |
|
|
'-metadata', f'title={safe_title} [{duration_sec}s snippet]', |
|
|
'-metadata', f'comment=Snippet from {url}', |
|
|
'-y', |
|
|
output_file |
|
|
] |
|
|
|
|
|
print(f"Trimming with command: {' '.join(cmd)}") |
|
|
result = subprocess.run(cmd, capture_output=True, text=True) |
|
|
|
|
|
if result.returncode != 0: |
|
|
print(f"FFmpeg stderr: {result.stderr}") |
|
|
raise Exception(f"Trimming failed: {result.stderr[:200]}") |
|
|
|
|
|
|
|
|
if not os.path.exists(output_file): |
|
|
raise Exception("Trimmed file was not created") |
|
|
|
|
|
trimmed_size = os.path.getsize(output_file) |
|
|
if trimmed_size == 0: |
|
|
raise Exception("Trimmed file is empty") |
|
|
|
|
|
print(f"Created: {output_file} ({trimmed_size} bytes)") |
|
|
|
|
|
|
|
|
try: |
|
|
os.unlink(input_file) |
|
|
except: |
|
|
pass |
|
|
|
|
|
return output_file, output_filename |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
if os.path.exists(temp_dir): |
|
|
shutil.rmtree(temp_dir, ignore_errors=True) |
|
|
print(f"Error: {str(e)}") |
|
|
traceback.print_exc() |
|
|
raise e |
|
|
|
|
|
def validate_url(url): |
|
|
"""Validate SoundCloud URL""" |
|
|
if not url or not url.strip(): |
|
|
return False, "Please enter a URL" |
|
|
|
|
|
url_lower = url.lower() |
|
|
if 'soundcloud.com' not in url_lower: |
|
|
return False, "Please enter a SoundCloud URL" |
|
|
|
|
|
|
|
|
if '/sets/' in url_lower: |
|
|
return False, "Playlists not supported. Please use a track URL." |
|
|
|
|
|
|
|
|
parts = url_lower.replace('https://', '').replace('http://', '').split('/') |
|
|
if len(parts) < 3 or not parts[2]: |
|
|
return False, "Please enter a specific track URL" |
|
|
|
|
|
return True, "" |
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
title="SoundCloud Snippet Generator", |
|
|
theme=gr.themes.Soft(), |
|
|
css=""" |
|
|
.gradio-container { max-width: 1000px !important; margin: auto; } |
|
|
.header { text-align: center; margin-bottom: 20px; } |
|
|
.success { color: #28a745; font-weight: bold; } |
|
|
.error { color: #dc3545; font-weight: bold; } |
|
|
.warning { background-color: #fff3cd; padding: 10px; border-radius: 5px; } |
|
|
.example-url { |
|
|
background: #f8f9fa; |
|
|
padding: 5px 10px; |
|
|
border-radius: 5px; |
|
|
margin: 5px 0; |
|
|
cursor: pointer; |
|
|
} |
|
|
.example-url:hover { background: #e9ecef; } |
|
|
""" |
|
|
) as demo: |
|
|
|
|
|
|
|
|
gr.Markdown(""" |
|
|
<div class="header"> |
|
|
<h1>🎵 SoundCloud Snippet Generator</h1> |
|
|
<p>Download the first <i>N</i> seconds of any public SoundCloud track</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
|
|
|
with gr.Column(scale=2): |
|
|
|
|
|
url_input = gr.Textbox( |
|
|
label="SoundCloud Track URL", |
|
|
placeholder="https://soundcloud.com/artist/track-name", |
|
|
value=DEFAULT_URL, |
|
|
elem_id="url_input" |
|
|
) |
|
|
|
|
|
|
|
|
url_status = gr.Markdown("", elem_id="url_status") |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
duration_slider = gr.Slider( |
|
|
minimum=5, |
|
|
maximum=MAX_DURATION, |
|
|
value=DEFAULT_DURATION, |
|
|
step=5, |
|
|
label=f"Duration (5-{MAX_DURATION} seconds)" |
|
|
) |
|
|
duration_number = gr.Number( |
|
|
value=DEFAULT_DURATION, |
|
|
label="Seconds", |
|
|
precision=0, |
|
|
minimum=5, |
|
|
maximum=MAX_DURATION |
|
|
) |
|
|
|
|
|
|
|
|
duration_slider.change( |
|
|
lambda x: x, |
|
|
inputs=[duration_slider], |
|
|
outputs=[duration_number] |
|
|
) |
|
|
duration_number.change( |
|
|
lambda x: x, |
|
|
inputs=[duration_number], |
|
|
outputs=[duration_slider] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Accordion("🎯 Try these example tracks", open=False): |
|
|
gr.Markdown(""" |
|
|
<div style="font-size: 0.9em;"> |
|
|
<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> |
|
|
<div class="example-url" onclick="document.getElementById('url_input').value='https://soundcloud.com/nocopyrightsounds/that-new-new'">🎵 That New New - NCS</div> |
|
|
<div class="example-url" onclick="document.getElementById('url_input').value='https://soundcloud.com/lofi_girl/coffee-jazz-music'">☕ Coffee Jazz - Lofi Girl</div> |
|
|
<div class="example-url" onclick="document.getElementById('url_input').value='https://soundcloud.com/chillhopdotcom/blue-window'">🔵 Blue Window - Chillhop</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
generate_btn = gr.Button( |
|
|
"🎬 Generate Snippet", |
|
|
variant="primary", |
|
|
size="lg", |
|
|
elem_id="generate_btn" |
|
|
) |
|
|
|
|
|
|
|
|
status_display = gr.Markdown("", elem_id="status") |
|
|
|
|
|
|
|
|
with gr.Group(): |
|
|
gr.Markdown("### 💾 Download") |
|
|
filename_display = gr.Textbox( |
|
|
label="File will be saved as:", |
|
|
interactive=False, |
|
|
elem_id="filename" |
|
|
) |
|
|
download_btn = gr.DownloadButton( |
|
|
"⬇️ Download MP3", |
|
|
visible=False, |
|
|
elem_id="download_btn" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Column(scale=1): |
|
|
|
|
|
audio_preview = gr.Audio( |
|
|
label="🎧 Preview", |
|
|
type="filepath", |
|
|
interactive=False, |
|
|
visible=False, |
|
|
elem_id="audio_preview" |
|
|
) |
|
|
|
|
|
|
|
|
gr.Markdown(""" |
|
|
<div class="warning"> |
|
|
<h4>⚠️ Important Notes</h4> |
|
|
<ul> |
|
|
<li>Works with <b>public tracks only</b></li> |
|
|
<li>Some tracks have download restrictions</li> |
|
|
<li>Maximum duration: 5 minutes</li> |
|
|
<li>Processing takes 10-30 seconds</li> |
|
|
<li>Files are automatically deleted after download</li> |
|
|
</ul> |
|
|
</div> |
|
|
|
|
|
<h4>✅ How to use:</h4> |
|
|
<ol> |
|
|
<li>Paste a SoundCloud URL</li> |
|
|
<li>Set the duration</li> |
|
|
<li>Click "Generate Snippet"</li> |
|
|
<li>Preview the audio</li> |
|
|
<li>Click "Download MP3"</li> |
|
|
</ol> |
|
|
""") |
|
|
|
|
|
|
|
|
file_path = gr.State() |
|
|
|
|
|
|
|
|
def on_url_change(url): |
|
|
is_valid, msg = validate_url(url) |
|
|
if not url: |
|
|
return gr.Markdown("", visible=False) |
|
|
elif is_valid: |
|
|
return gr.Markdown("✅ Valid SoundCloud track URL", visible=True) |
|
|
else: |
|
|
return gr.Markdown(f"⚠️ {msg}", visible=True) |
|
|
|
|
|
|
|
|
def process_snippet(url, duration): |
|
|
|
|
|
yield { |
|
|
status_display: "⏳ Starting download...", |
|
|
audio_preview: gr.Audio(visible=False), |
|
|
download_btn: gr.DownloadButton(visible=False), |
|
|
filename_display: "", |
|
|
} |
|
|
|
|
|
|
|
|
is_valid, msg = validate_url(url) |
|
|
if not is_valid: |
|
|
yield { |
|
|
status_display: f"❌ {msg}", |
|
|
audio_preview: gr.Audio(visible=False), |
|
|
download_btn: gr.DownloadButton(visible=False), |
|
|
} |
|
|
return |
|
|
|
|
|
try: |
|
|
|
|
|
yield { |
|
|
status_display: "⏳ Downloading track...", |
|
|
audio_preview: gr.Audio(visible=False), |
|
|
download_btn: gr.DownloadButton(visible=False), |
|
|
} |
|
|
|
|
|
filepath, filename = download_and_trim(url, duration) |
|
|
|
|
|
|
|
|
yield { |
|
|
status_display: "✅ Snippet created successfully!", |
|
|
audio_preview: gr.Audio(value=filepath, visible=True), |
|
|
download_btn: gr.DownloadButton(visible=True), |
|
|
filename_display: filename, |
|
|
file_path: filepath, |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
error_msg = str(e) |
|
|
|
|
|
if "Private" in error_msg or "not accessible" in error_msg: |
|
|
error_msg = "Track is private or not accessible" |
|
|
elif "unavailable" in error_msg or "not found" in error_msg: |
|
|
error_msg = "Track not found or unavailable" |
|
|
elif "Copyright" in error_msg or "restricted" in error_msg: |
|
|
error_msg = "Track has download restrictions" |
|
|
|
|
|
yield { |
|
|
status_display: f"❌ {error_msg}", |
|
|
audio_preview: gr.Audio(visible=False), |
|
|
download_btn: gr.DownloadButton(visible=False), |
|
|
filename_display: "", |
|
|
} |
|
|
|
|
|
|
|
|
url_input.change( |
|
|
fn=on_url_change, |
|
|
inputs=[url_input], |
|
|
outputs=[url_status] |
|
|
) |
|
|
|
|
|
generate_btn.click( |
|
|
fn=process_snippet, |
|
|
inputs=[url_input, duration_slider], |
|
|
outputs=[ |
|
|
status_display, |
|
|
audio_preview, |
|
|
download_btn, |
|
|
filename_display, |
|
|
file_path |
|
|
] |
|
|
) |
|
|
|
|
|
|
|
|
download_btn.click( |
|
|
fn=lambda fp: fp if fp and os.path.exists(fp) else None, |
|
|
inputs=[file_path], |
|
|
outputs=None |
|
|
) |
|
|
|
|
|
|
|
|
gr.Markdown("---") |
|
|
gr.Markdown(""" |
|
|
<div style="text-align: center; color: #666; font-size: 0.9em;"> |
|
|
<p>Built with ❤️ using Gradio & yt-dlp</p> |
|
|
<p>Respect artists' rights. For personal use only.</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch( |
|
|
server_name="0.0.0.0", |
|
|
share=False, |
|
|
debug=False, |
|
|
show_error=True |
|
|
) |