fix the timeout waiting from web client

This commit is contained in:
2026-01-02 18:35:25 +01:00
parent 92f9b110ce
commit 492d336ee3
2 changed files with 433 additions and 561 deletions

123
app.py
View File

@@ -3,13 +3,15 @@
Flask server for YouTube Concert Splitter Flask server for YouTube Concert Splitter
Downloads YouTube videos and splits them into tracks based on a setlist. Downloads YouTube videos and splits them into tracks based on a setlist.
""" """
from flask import Flask, request, render_template, jsonify from flask import Flask, request, render_template, jsonify, Response
import os import os
import re import re
import subprocess import subprocess
import yt_dlp import yt_dlp
from pydub import AudioSegment from pydub import AudioSegment
from mutagen.easyid3 import EasyID3 from mutagen.easyid3 import EasyID3
import json
import sys
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', 'your_secret_key_here_change_this') app.secret_key = os.environ.get('SECRET_KEY', 'your_secret_key_here_change_this')
@@ -50,10 +52,10 @@ def download_youtube_audio(url: str, output_folder: str):
cookies_file = 'cookies.txt' cookies_file = 'cookies.txt'
has_cookies = os.path.isfile(cookies_file) has_cookies = os.path.isfile(cookies_file)
if has_cookies: if has_cookies:
print(f"✓ Using cookies from {cookies_file}") print(f"✓ Using cookies from {cookies_file}", flush=True)
else: else:
print("⚠️ No cookies.txt found - download may fail for some videos") print("⚠️ No cookies.txt found - download may fail for some videos", flush=True)
print(" See COOKIES_INSTRUCTIONS.txt for how to add cookies") print(" See COOKIES_INSTRUCTIONS.txt for how to add cookies", flush=True)
# Enhanced options to bypass YouTube restrictions # Enhanced options to bypass YouTube restrictions
ydl_opts = { ydl_opts = {
@@ -150,7 +152,7 @@ def split_audio(mp3_path: str, entries: list, album: str, artist: str, output_di
Skip tracks that already exist. Skip tracks that already exist.
Returns list of track info with status (created or skipped). Returns list of track info with status (created or skipped).
""" """
print(f"Loading audio file: {mp3_path}") print(f"Loading audio file: {mp3_path}", flush=True)
audio = AudioSegment.from_file(mp3_path) audio = AudioSegment.from_file(mp3_path)
total_ms = len(audio) total_ms = len(audio)
@@ -168,7 +170,7 @@ def split_audio(mp3_path: str, entries: list, album: str, artist: str, output_di
# Check if file already exists # Check if file already exists
if os.path.isfile(filepath): if os.path.isfile(filepath):
print(f"Skipping track {idx}/{len(entries)}: {filename} (already exists)") print(f"Skipping track {idx}/{len(entries)}: {filename} (already exists)", flush=True)
track_results.append({ track_results.append({
'filename': filename, 'filename': filename,
'status': 'skipped' 'status': 'skipped'
@@ -177,7 +179,7 @@ def split_audio(mp3_path: str, entries: list, album: str, artist: str, output_di
continue continue
# Extract and export segment # Extract and export segment
print(f"Creating track {idx}/{len(entries)}: {filename}") print(f"Creating track {idx}/{len(entries)}: {filename}", flush=True)
segment = audio[start_ms:end_ms] segment = audio[start_ms:end_ms]
# Export with 320kbps # Export with 320kbps
@@ -192,7 +194,7 @@ def split_audio(mp3_path: str, entries: list, album: str, artist: str, output_di
tags['tracknumber'] = str(idx) tags['tracknumber'] = str(idx)
tags.save() tags.save()
except Exception as e: except Exception as e:
print(f"Warning: Could not add ID3 tags to {filename}: {e}") print(f"Warning: Could not add ID3 tags to {filename}: {e}", flush=True)
track_results.append({ track_results.append({
'filename': filename, 'filename': filename,
@@ -207,11 +209,11 @@ def set_permissions(directory: str):
"""Set directory permissions to 775 recursively.""" """Set directory permissions to 775 recursively."""
try: try:
subprocess.run(['chmod', '-R', '775', directory], check=True) subprocess.run(['chmod', '-R', '775', directory], check=True)
print(f"Set permissions 775 on {directory}") print(f"Set permissions 775 on {directory}", flush=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Warning: Could not set permissions: {e}") print(f"Warning: Could not set permissions: {e}", flush=True)
except FileNotFoundError: except FileNotFoundError:
print("Warning: chmod command not found (might be on Windows)") print("Warning: chmod command not found (might be on Windows)", flush=True)
@app.route('/') @app.route('/')
@@ -223,6 +225,9 @@ def index():
@app.route('/split', methods=['POST']) @app.route('/split', methods=['POST'])
def split_concert(): def split_concert():
"""Handle the split request.""" """Handle the split request."""
# Force flush stdout immediately
sys.stdout.flush()
try: try:
# Get form data # Get form data
url = request.form.get('youtube_url', '').strip() url = request.form.get('youtube_url', '').strip()
@@ -230,7 +235,12 @@ def split_concert():
album = request.form.get('album', '').strip() album = request.form.get('album', '').strip()
setlist_text = request.form.get('setlist', '').strip() setlist_text = request.form.get('setlist', '').strip()
# Validate required inputs (setlist is OPTIONAL) print(f"=== Processing Request ===", flush=True)
print(f"URL: {url}", flush=True)
print(f"Artist: {artist}", flush=True)
print(f"Album: {album}", flush=True)
# Validate required inputs
if not url: if not url:
return jsonify({'error': 'No YouTube URL provided'}), 400 return jsonify({'error': 'No YouTube URL provided'}), 400
@@ -243,23 +253,27 @@ def split_concert():
# Sanitize album for directory name # Sanitize album for directory name
album_sanitized = sanitize(album) album_sanitized = sanitize(album)
# Create output directory directly in DOWNLOAD_FOLDER # Create output directory
output_dir = os.path.join(DOWNLOAD_FOLDER, album_sanitized) output_dir = os.path.join(DOWNLOAD_FOLDER, album_sanitized)
# Create directory only if it doesn't exist
if not os.path.exists(output_dir): if not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
print(f"✓ Created album directory: {output_dir}") print(f"✓ Created album directory: {output_dir}", flush=True)
else: else:
print(f"✓ Album directory already exists: {output_dir}") print(f"✓ Album directory already exists: {output_dir}", flush=True)
# Download audio # Download audio
print(f"Downloading audio from: {url}") print(f"Downloading audio from: {url}", flush=True)
sys.stdout.flush()
mp3_path, info = download_youtube_audio(url, DOWNLOAD_FOLDER) mp3_path, info = download_youtube_audio(url, DOWNLOAD_FOLDER)
print(f"✓ Download complete: {mp3_path}", flush=True)
sys.stdout.flush()
# DECISION POINT: Empty setlist = Single song mode # DECISION POINT: Empty setlist = Single song mode
if not setlist_text: if not setlist_text:
print("📀 Single song mode: No setlist provided") print("📀 Single song mode: No setlist provided", flush=True)
# Use video title as track name # Use video title as track name
track_title = info.get('title', 'Unknown Track') track_title = info.get('title', 'Unknown Track')
@@ -268,8 +282,8 @@ def split_concert():
# Check if file already exists # Check if file already exists
if os.path.isfile(filepath): if os.path.isfile(filepath):
print(f"⊘ Track already exists: {filename}") print(f"⊘ Track already exists: {filename}", flush=True)
os.remove(mp3_path) # Clean up downloaded file os.remove(mp3_path)
track_results = [{ track_results = [{
'filename': filename, 'filename': filename,
@@ -278,8 +292,7 @@ def split_concert():
created_count = 0 created_count = 0
skipped_count = 1 skipped_count = 1
else: else:
# Move and rename the file (no splitting needed) print(f"✓ Creating single track: {filename}", flush=True)
print(f"✓ Creating single track: {filename}")
# Load audio to re-export with proper tags # Load audio to re-export with proper tags
audio = AudioSegment.from_file(mp3_path) audio = AudioSegment.from_file(mp3_path)
@@ -293,9 +306,9 @@ def split_concert():
tags['artist'] = artist tags['artist'] = artist
tags['tracknumber'] = '1' tags['tracknumber'] = '1'
tags.save() tags.save()
print(f"✓ Added ID3 tags") print(f"✓ Added ID3 tags", flush=True)
except Exception as e: except Exception as e:
print(f"⚠️ Warning: Could not add ID3 tags: {e}") print(f"⚠️ Warning: Could not add ID3 tags: {e}", flush=True)
# Clean up original # Clean up original
os.remove(mp3_path) os.remove(mp3_path)
@@ -308,35 +321,41 @@ def split_concert():
skipped_count = 0 skipped_count = 0
else: else:
# SPLIT MODE: Parse setlist and split audio # SPLIT MODE
print(f"✂️ Split mode: Processing setlist with {len(setlist_text.splitlines())} lines") print(f"✂️ Split mode: Processing setlist with {len(setlist_text.splitlines())} lines", flush=True)
# Parse setlist for splitting # Parse setlist
try: try:
entries = parse_setlist(setlist_text) entries = parse_setlist(setlist_text)
except ValueError as e: except ValueError as e:
os.remove(mp3_path) # Clean up downloaded file os.remove(mp3_path)
return jsonify({'error': f'Setlist parsing error: {str(e)}'}), 400 return jsonify({'error': f'Setlist parsing error: {str(e)}'}), 400
if not entries: if not entries:
os.remove(mp3_path) # Clean up downloaded file os.remove(mp3_path)
return jsonify({'error': 'No valid tracks found in setlist'}), 400 return jsonify({'error': 'No valid tracks found in setlist'}), 400
# Split audio into tracks (skipping existing ones) # Split audio
print(f"Splitting into {len(entries)} tracks...") print(f"Splitting into {len(entries)} tracks...", flush=True)
sys.stdout.flush()
track_results, created_count, skipped_count = split_audio( track_results, created_count, skipped_count = split_audio(
mp3_path, entries, album, artist, output_dir mp3_path, entries, album, artist, output_dir
) )
# Clean up original MP3 # Clean up original
print(f"Removing original file: {mp3_path}") print(f"Removing original file: {mp3_path}", flush=True)
os.remove(mp3_path) os.remove(mp3_path)
# Set permissions # Set permissions
set_permissions(output_dir) set_permissions(output_dir)
# Return success response with detailed track information print(f"✓ Processing complete!", flush=True)
return jsonify({ print(f" Created: {created_count}, Skipped: {skipped_count}", flush=True)
sys.stdout.flush()
# Build response
response_data = {
'success': True, 'success': True,
'album': album, 'album': album,
'artist': artist, 'artist': artist,
@@ -345,13 +364,31 @@ def split_concert():
'skipped_count': skipped_count, 'skipped_count': skipped_count,
'tracks': track_results, 'tracks': track_results,
'output_dir': output_dir 'output_dir': output_dir
}) }
print(f"Sending response: {json.dumps(response_data, indent=2)}", flush=True)
# Return JSON response with explicit content type
return Response(
json.dumps(response_data),
status=200,
mimetype='application/json'
)
except Exception as e: except Exception as e:
print(f"❌ Error: {str(e)}") print(f"❌ Error: {str(e)}", flush=True)
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return jsonify({'error': str(e)}), 500 sys.stdout.flush()
# Return error as JSON
error_response = {'error': str(e)}
return Response(
json.dumps(error_response),
status=500,
mimetype='application/json'
)
if __name__ == '__main__': if __name__ == '__main__':
# Get configuration from environment variables # Get configuration from environment variables
@@ -359,10 +396,8 @@ if __name__ == '__main__':
port = int(os.environ.get('FLASK_PORT', 5000)) port = int(os.environ.get('FLASK_PORT', 5000))
debug = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true' debug = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
print(f"Starting YouTube Concert Splitter on {host}:{port}") print(f"Starting YouTube Concert Splitter on {host}:{port}", flush=True)
print(f"Music directory: {DOWNLOAD_FOLDER}") print(f"Music directory: {DOWNLOAD_FOLDER}", flush=True)
# Run on all interfaces, configurable port
# app.run(host=host, port=port, debug=debug)
app.run(host="0.0.0.0", port=port, debug=debug)
# Run with threaded=True for better handling of long requests
app.run(host="0.0.0.0", port=port, debug=debug, threaded=True)

View File

@@ -1,166 +1,124 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>YouTube Concert Splitter</title> <title>YouTube Concert Splitter</title>
<style> <style>
body { * {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0; margin: 0;
padding: 20px; padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 20px;
} }
.container { .container {
background: #fff; background: white;
padding: 40px; border-radius: 20px;
border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3);
box-shadow: 0 8px 20px rgba(0,0,0,0.2); max-width: 600px;
width: 100%; width: 100%;
max-width: 700px; padding: 40px;
} }
h1 { h1 {
margin-bottom: 10px;
color: #333; color: #333;
margin-bottom: 10px;
font-size: 28px; font-size: 28px;
text-align: center;
} }
.subtitle { .subtitle {
text-align: center;
color: #666; color: #666;
margin-bottom: 30px; margin-bottom: 30px;
font-size: 14px; font-size: 14px;
} }
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 20px;
}
.form-group { .form-group {
display: flex; margin-bottom: 20px;
flex-direction: column;
}
.form-group.full-width {
grid-column: 1 / -1;
} }
label { label {
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
font-weight: 600; color: #555;
color: #444; font-weight: 500;
font-size: 14px; font-size: 14px;
} }
input[type="text"] { input[type="text"], textarea {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
border: 2px solid #e0e0e0; border: 2px solid #e0e0e0;
border-radius: 6px; border-radius: 8px;
font-size: 14px; font-size: 14px;
transition: border-color 0.3s; transition: border-color 0.3s;
box-sizing: border-box;
} }
input[type="text"]:focus { input[type="text"]:focus, textarea:focus {
outline: none; outline: none;
border-color: #667eea; border-color: #667eea;
} }
textarea { textarea {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 13px;
font-family: 'Courier New', monospace;
resize: vertical; resize: vertical;
min-height: 200px; min-height: 120px;
transition: border-color 0.3s; font-family: 'Courier New', monospace;
box-sizing: border-box;
} }
textarea:focus { .helper-text {
outline: none;
border-color: #667eea;
}
.help-text {
font-size: 12px; font-size: 12px;
color: #777; color: #888;
margin-top: 5px; margin-top: 5px;
margin-bottom: 0;
font-style: italic;
} }
button { button {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border: none; border: none;
padding: 14px 30px; border-radius: 8px;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
border-radius: 6px;
cursor: pointer; cursor: pointer;
width: 100%;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
margin-top: 10px;
} }
button:hover { button:hover:not(:disabled) {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
} }
button:disabled { button:disabled {
background: #ccc; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
transform: none;
} }
.loader-overlay { .loading {
position: fixed; display: none;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loader-content {
background: white;
padding: 40px;
border-radius: 12px;
text-align: center; text-align: center;
max-width: 400px; padding: 30px;
margin: 0 20px; }
.loading.active {
display: block;
} }
.spinner { .spinner {
border: 6px solid #f3f3f3; border: 4px solid #f3f3f3;
border-top: 6px solid #667eea; border-top: 4px solid #667eea;
border-radius: 50%; border-radius: 50%;
width: 60px; width: 50px;
height: 60px; height: 50px;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto 20px; margin: 0 auto 20px;
} }
@@ -170,400 +128,279 @@
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
.loader-text { .loading-text {
font-size: 18px;
color: #333;
font-weight: 600;
margin-bottom: 10px;
}
.loader-subtext {
font-size: 14px;
color: #666; color: #666;
}
.message {
padding: 15px;
border-radius: 6px;
margin-top: 20px;
font-size: 14px; font-size: 14px;
} }
.error {
background: #fee;
color: #c33;
border: 1px solid #fcc;
}
.success {
background: #efe;
color: #3a3;
border: 1px solid #cfc;
}
.warning {
background: #fef7e0;
color: #8a6d3b;
border: 1px solid #faebcc;
}
.results { .results {
margin-top: 20px; display: none;
margin-top: 30px;
padding: 20px; padding: 20px;
background: #f9f9f9; background: #f8f9fa;
border-radius: 6px; border-radius: 10px;
border: 1px solid #e0e0e0; }
.results.active {
display: block;
} }
.results h3 { .results h3 {
margin-top: 0;
color: #333; color: #333;
margin-bottom: 15px;
font-size: 18px; font-size: 18px;
} }
.track-list { .result-info {
list-style: none;
padding: 0;
margin: 10px 0;
}
.track-list li {
padding: 8px;
background: white; background: white;
margin-bottom: 5px;
border-radius: 4px;
font-size: 13px;
color: #555;
word-break: break-word;
}
.track-list li.skipped {
background: #fff9e6;
color: #856404;
}
.track-list li.created {
background: #e8f5e9;
color: #2e7d32;
}
.download-info {
margin-top: 15px;
padding: 15px; padding: 15px;
background: #e8f4f8; border-radius: 8px;
border-radius: 6px;
border-left: 4px solid #2196F3;
word-break: break-all;
}
.download-info strong {
color: #1976D2;
}
.stats {
display: flex;
gap: 20px;
margin-top: 15px;
flex-wrap: wrap;
}
.stat-item {
flex: 1;
min-width: 120px;
padding: 10px;
background: white;
border-radius: 4px;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #667eea;
}
.stat-label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
/* Mobile Responsive Styles */
@media screen and (max-width: 768px) {
body {
padding: 10px;
align-items: flex-start;
}
.container {
padding: 20px;
margin: 10px 0;
}
h1 {
font-size: 24px;
margin-bottom: 8px;
}
.subtitle {
font-size: 13px;
margin-bottom: 20px;
}
.form-row {
grid-template-columns: 1fr;
gap: 15px;
margin-bottom: 15px; margin-bottom: 15px;
} }
label { .result-row {
font-size: 13px; display: flex;
} justify-content: space-between;
margin-bottom: 8px;
input[type="text"],
textarea {
font-size: 16px; /* Prevents zoom on iOS */
padding: 10px;
}
textarea {
min-height: 150px;
font-size: 12px;
}
.help-text {
font-size: 11px;
}
button {
padding: 12px 20px;
font-size: 15px;
}
.loader-content {
padding: 30px 20px;
max-width: 90%;
}
.loader-text {
font-size: 16px;
}
.loader-subtext {
font-size: 13px;
}
.message {
font-size: 13px;
padding: 12px;
}
.results {
padding: 15px;
}
.results h3 {
font-size: 16px;
}
.results h4 {
font-size: 14px; font-size: 14px;
} }
.stats { .result-label {
gap: 10px; color: #666;
font-weight: 500;
} }
.stat-item { .result-value {
min-width: 80px; color: #333;
padding: 8px;
} }
.stat-number { .tracks-list {
font-size: 20px; max-height: 300px;
overflow-y: auto;
} }
.stat-label { .track-item {
font-size: 11px; background: white;
}
.track-list li {
font-size: 12px;
padding: 6px;
}
.download-info {
font-size: 12px;
padding: 12px; padding: 12px;
} border-radius: 6px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
} }
@media screen and (max-width: 480px) { .track-filename {
.container { color: #333;
flex: 1;
}
.track-status {
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.track-status.created {
background: #d4edda;
color: #155724;
}
.track-status.skipped {
background: #fff3cd;
color: #856404;
}
.error {
display: none;
margin-top: 20px;
padding: 15px; padding: 15px;
background: #f8d7da;
color: #721c24;
border-radius: 8px;
font-size: 14px;
} }
h1 { .error.active {
font-size: 20px; display: block;
}
.subtitle {
font-size: 12px;
}
.stat-item {
min-width: 70px;
}
.stat-number {
font-size: 18px;
}
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>🎵 YouTube Concert Splitter</h1> <h1>🎵 YouTube Concert Splitter</h1>
<p class="subtitle">Download and split concert videos into individual tracks</p> <p class="subtitle">Download and split concerts into individual tracks</p>
<form id="splitterForm"> <form id="splitForm">
<div class="form-group full-width"> <div class="form-group">
<label for="youtube_url">YouTube URL:</label> <label for="youtube_url">YouTube URL *</label>
<input type="text" <input
type="text"
id="youtube_url" id="youtube_url"
name="youtube_url" name="youtube_url"
placeholder="https://www.youtube.com/watch?v=..." placeholder="https://www.youtube.com/watch?v=..."
required> required
>
</div> </div>
<div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="artist">Artist Name:</label> <label for="artist">Artist Name *</label>
<input type="text" <input
type="text"
id="artist" id="artist"
name="artist" name="artist"
placeholder="e.g., Bob Dylan" placeholder="Artist Name"
required> required
>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="album">Album Name:</label> <label for="album">Album/Concert Name *</label>
<input type="text" <input
type="text"
id="album" id="album"
name="album" name="album"
placeholder="e.g., Live at Madison Square Garden" placeholder="Live at Madison Square Garden 2024"
required> required
</div> >
</div> </div>
<div class="form-group full-width"> <div class="form-group">
<label for="setlist">Setlist (timestamps and track titles) - Optional:</label> <label for="setlist">Setlist (optional - leave empty for single track)</label>
<textarea id="setlist" <textarea
id="setlist"
name="setlist" name="setlist"
placeholder="0:00 Opening Song&#10;3:45 Second Track&#10;7:30 Third Song&#10;&#10;(Leave empty to download as single track - no splitting)"></textarea> placeholder="0:00 Opening Song&#10;3:45 Second Track&#10;7:20 Third Song&#10;..."
<div class="help-text"> ></textarea>
<strong>Split Mode:</strong> Enter "TIMESTAMP TRACK_TITLE" per line (e.g., "0:00 Song Name")<br> <div class="helper-text">Format: TIMESTAMP TITLE (one per line). Leave empty to save the entire video as a single track.</div>
<strong>Single Mode:</strong> Leave this field completely empty to download entire video as one track
</div>
</div> </div>
<button type="submit" id="submitBtn">Split Concert</button> <button type="submit">Split Concert</button>
</form> </form>
<div id="message"></div> <div class="loading" id="loading">
<div id="results"></div> <div class="spinner"></div>
<div class="loading-text">Processing... This may take a few minutes.</div>
<div class="loading-text" style="margin-top: 10px; font-size: 12px;">Please be patient, downloading and converting can take time.</div>
</div> </div>
<div id="loader" class="loader-overlay" style="display: none;"> <div class="error" id="error"></div>
<div class="loader-content">
<div class="spinner"></div> <div class="results" id="results">
<div class="loader-text">Processing...</div> <h3>✅ Success!</h3>
<div class="loader-subtext">Downloading and splitting tracks. This may take several minutes.</div> <div class="result-info">
<div class="result-row">
<span class="result-label">Album:</span>
<span class="result-value" id="resultAlbum"></span>
</div>
<div class="result-row">
<span class="result-label">Artist:</span>
<span class="result-value" id="resultArtist"></span>
</div>
<div class="result-row">
<span class="result-label">Total Tracks:</span>
<span class="result-value" id="resultTotal"></span>
</div>
<div class="result-row">
<span class="result-label">Created:</span>
<span class="result-value" id="resultCreated"></span>
</div>
<div class="result-row">
<span class="result-label">Skipped:</span>
<span class="result-value" id="resultSkipped"></span>
</div>
<div class="result-row">
<span class="result-label">Output Directory:</span>
<span class="result-value" id="resultDir"></span>
</div>
</div>
<h3 style="margin-top: 20px;">Tracks</h3>
<div class="tracks-list" id="tracksList"></div>
</div> </div>
</div> </div>
<script> <script>
document.getElementById('splitterForm').addEventListener('submit', function(e) { document.getElementById('splitForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
// Clear previous messages const form = e.target;
document.getElementById('message').innerHTML = ''; const formData = new FormData(form);
document.getElementById('results').innerHTML = '';
// Show loader // Show loading, hide results and errors
document.getElementById('loader').style.display = 'flex'; document.getElementById('loading').classList.add('active');
document.getElementById('submitBtn').disabled = true; document.getElementById('results').classList.remove('active');
document.getElementById('error').classList.remove('active');
form.querySelector('button').disabled = true;
const url = document.getElementById('youtube_url').value; try {
const artist = document.getElementById('artist').value; // Increase timeout to 10 minutes (600000ms)
const album = document.getElementById('album').value; const controller = new AbortController();
const setlist = document.getElementById('setlist').value; const timeoutId = setTimeout(() => controller.abort(), 600000);
const formData = new FormData(); const response = await fetch('/split', {
formData.append('youtube_url', url);
formData.append('artist', artist);
formData.append('album', album);
formData.append('setlist', setlist);
fetch('/split', {
method: 'POST', method: 'POST',
body: formData body: formData,
}) signal: controller.signal
.then(response => response.json()) });
.then(data => {
document.getElementById('loader').style.display = 'none';
document.getElementById('submitBtn').disabled = false;
if (data.error) { clearTimeout(timeoutId);
document.getElementById('message').innerHTML =
`<div class="message error"><strong>Error:</strong> ${data.error}</div>`;
} else {
// Display success message with stats
let html = '';
if (data.skipped_count > 0) { // Check if response is JSON
html += `<div class="message warning"><strong>Note:</strong> ${data.skipped_count} track(s) already existed and were skipped.</div>`; const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error('Server returned non-JSON response. Check server logs.');
} }
html += `<div class="message success"><strong>Success!</strong> Processing complete.</div>`; const data = await response.json();
html += `<div class="results">`;
html += `<h3>🎸 ${data.artist} - ${data.album}</h3>`;
html += `<div class="stats">`; if (!response.ok) {
html += `<div class="stat-item">`; throw new Error(data.error || 'Unknown error occurred');
html += `<div class="stat-number">${data.created_count}</div>`; }
html += `<div class="stat-label">Created</div>`;
html += `</div>`;
html += `<div class="stat-item">`;
html += `<div class="stat-number">${data.skipped_count}</div>`;
html += `<div class="stat-label">Skipped</div>`;
html += `</div>`;
html += `<div class="stat-item">`;
html += `<div class="stat-number">${data.total_tracks}</div>`;
html += `<div class="stat-label">Total Tracks</div>`;
html += `</div>`;
html += `</div>`;
html += `<div class="download-info"><strong>📁 Output Directory:</strong> ${data.output_dir}</div>`; if (data.success) {
html += `<h4>Track Details:</h4>`; // Display results
html += `<ul class="track-list">`; document.getElementById('resultAlbum').textContent = data.album;
document.getElementById('resultArtist').textContent = data.artist;
document.getElementById('resultTotal').textContent = data.total_tracks;
document.getElementById('resultCreated').textContent = data.created_count;
document.getElementById('resultSkipped').textContent = data.skipped_count;
document.getElementById('resultDir').textContent = data.output_dir;
// Display tracks
const tracksList = document.getElementById('tracksList');
tracksList.innerHTML = '';
data.tracks.forEach(track => { data.tracks.forEach(track => {
const className = track.status === 'created' ? 'created' : 'skipped'; const trackItem = document.createElement('div');
const icon = track.status === 'created' ? '✓' : '⊘'; trackItem.className = 'track-item';
const statusText = track.status === 'created' ? 'Created' : 'Already exists'; trackItem.innerHTML = `
html += `<li class="${className}">${icon} ${track.filename} <em>(${statusText})</em></li>`; <span class="track-filename">${track.filename}</span>
<span class="track-status ${track.status}">${track.status}</span>
`;
tracksList.appendChild(trackItem);
}); });
html += `</ul>`;
html += `</div>`; document.getElementById('results').classList.add('active');
document.getElementById('results').innerHTML = html; } else {
throw new Error(data.error || 'Processing failed');
}
} catch (error) {
console.error('Error:', error);
const errorDiv = document.getElementById('error');
if (error.name === 'AbortError') {
errorDiv.textContent = 'Request timed out. The download is taking too long. Please try with a shorter video or check your internet connection.';
} else {
errorDiv.textContent = error.message || 'An error occurred. Check the console for details.';
}
errorDiv.classList.add('active');
} finally {
document.getElementById('loading').classList.remove('active');
form.querySelector('button').disabled = false;
} }
})
.catch(error => {
document.getElementById('loader').style.display = 'none';
document.getElementById('submitBtn').disabled = false;
document.getElementById('message').innerHTML =
`<div class="message error"><strong>Error:</strong> ${error.message}</div>`;
});
}); });
</script> </script>
</body> </body>