problem with youtube in docker
This commit is contained in:
46
Dockerfile
Normal file
46
Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Use Python 3.11 slim image as base
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1 \
|
||||||
|
DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies including FFmpeg
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ffmpeg \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --upgrade pip && \
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY app.py .
|
||||||
|
COPY templates/ templates/
|
||||||
|
|
||||||
|
# Create music directory with proper permissions
|
||||||
|
RUN mkdir -p /app/youtube && \
|
||||||
|
chmod -R 775 /app/youtube
|
||||||
|
|
||||||
|
# Create a non-root user and switch to it
|
||||||
|
#RUN useradd -m -u 1000 appuser && \
|
||||||
|
# chown -R appuser:appuser /app
|
||||||
|
#USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000')" || exit 1
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["python", "app.py"]
|
||||||
366
app.py
Normal file
366
app.py
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Flask server for YouTube Concert Splitter
|
||||||
|
Downloads YouTube videos and splits them into tracks based on a setlist.
|
||||||
|
"""
|
||||||
|
from flask import Flask, request, render_template, jsonify
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import yt_dlp
|
||||||
|
from pydub import AudioSegment
|
||||||
|
from mutagen.easyid3 import EasyID3
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.environ.get('SECRET_KEY', 'your_secret_key_here_change_this')
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DOWNLOAD_FOLDER = os.environ.get('DOWNLOAD_FOLDER', 'youtube')
|
||||||
|
os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(s: str) -> str:
|
||||||
|
"""Sanitize string for use as filename."""
|
||||||
|
s = s.strip()
|
||||||
|
s = re.sub(r'[\\/:"*?<>|]+', '', s)
|
||||||
|
s = re.sub(r"[^A-Za-z0-9 _\-]", '_', s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def parse_timestamp(ts: str) -> int:
|
||||||
|
"""Parse timestamp string (HH:MM:SS or MM:SS) to milliseconds."""
|
||||||
|
parts = ts.strip().split(':')
|
||||||
|
try:
|
||||||
|
if len(parts) == 3:
|
||||||
|
h, m, s = parts
|
||||||
|
elif len(parts) == 2:
|
||||||
|
h = 0
|
||||||
|
m, s = parts
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid timestamp format: '{ts}'")
|
||||||
|
return (int(h) * 3600 + int(m) * 60 + int(s)) * 1000
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(f"Cannot parse timestamp '{ts}': {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def download_youtube_audio(url: str, output_folder: str):
|
||||||
|
"""Download YouTube video and convert to MP3 at 320kbps with enhanced anti-blocking measures."""
|
||||||
|
|
||||||
|
# Check if cookies file exists
|
||||||
|
cookies_file = 'cookies.txt'
|
||||||
|
has_cookies = os.path.isfile(cookies_file)
|
||||||
|
if has_cookies:
|
||||||
|
print(f"✓ Using cookies from {cookies_file}")
|
||||||
|
else:
|
||||||
|
print("⚠️ No cookies.txt found - download may fail for some videos")
|
||||||
|
print(" See COOKIES_INSTRUCTIONS.txt for how to add cookies")
|
||||||
|
|
||||||
|
# Enhanced options to bypass YouTube restrictions
|
||||||
|
ydl_opts = {
|
||||||
|
'format': 'bestaudio/best',
|
||||||
|
'outtmpl': f'{output_folder}/%(title)s.%(ext)s',
|
||||||
|
'noplaylist': True,
|
||||||
|
'quiet': False,
|
||||||
|
'no_warnings': False,
|
||||||
|
'extract_flat': False,
|
||||||
|
'ignoreerrors': False,
|
||||||
|
|
||||||
|
# Use cookies if available
|
||||||
|
'cookiefile': cookies_file if has_cookies else None,
|
||||||
|
|
||||||
|
# Anti-blocking measures
|
||||||
|
'nocheckcertificate': True,
|
||||||
|
'geo_bypass': True,
|
||||||
|
'age_limit': None,
|
||||||
|
|
||||||
|
# Better headers to mimic a real browser
|
||||||
|
'http_headers': {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-us,en;q=0.5',
|
||||||
|
'Accept-Encoding': 'gzip,deflate',
|
||||||
|
'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Extractor specific arguments for YouTube
|
||||||
|
'extractor_args': {
|
||||||
|
'youtube': {
|
||||||
|
'player_client': ['android', 'web'],
|
||||||
|
'player_skip': ['webpage', 'configs'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
# Post-processing
|
||||||
|
'postprocessors': [{
|
||||||
|
'key': 'FFmpegExtractAudio',
|
||||||
|
'preferredcodec': 'mp3',
|
||||||
|
'preferredquality': '320',
|
||||||
|
}],
|
||||||
|
|
||||||
|
# Retry options
|
||||||
|
'retries': 10,
|
||||||
|
'fragment_retries': 10,
|
||||||
|
'skip_unavailable_fragments': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
info = ydl.extract_info(url, download=True)
|
||||||
|
base_name = ydl.prepare_filename(info)
|
||||||
|
|
||||||
|
# Determine the actual MP3 filename
|
||||||
|
mp3_path = os.path.splitext(base_name)[0] + '.mp3'
|
||||||
|
|
||||||
|
if not os.path.isfile(mp3_path):
|
||||||
|
raise FileNotFoundError(f"Downloaded MP3 not found: {mp3_path}")
|
||||||
|
|
||||||
|
return mp3_path, info
|
||||||
|
|
||||||
|
|
||||||
|
def parse_setlist(setlist_text: str):
|
||||||
|
"""Parse setlist text into list of (timestamp_ms, title) tuples."""
|
||||||
|
entries = []
|
||||||
|
lines = setlist_text.strip().split('\n')
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Match pattern: "TIMESTAMP TITLE"
|
||||||
|
m = re.match(r"(\d+:\d+(?::\d+)?)\s+(.+)", line)
|
||||||
|
if not m:
|
||||||
|
raise ValueError(f"Invalid setlist line format: '{line}'")
|
||||||
|
|
||||||
|
ts, title = m.groups()
|
||||||
|
try:
|
||||||
|
timestamp_ms = parse_timestamp(ts)
|
||||||
|
entries.append((timestamp_ms, title.strip()))
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(f"Error parsing line '{line}': {e}")
|
||||||
|
|
||||||
|
# Sort by timestamp
|
||||||
|
entries.sort(key=lambda x: x[0])
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def split_audio(mp3_path: str, entries: list, album: str, artist: str, output_dir: str):
|
||||||
|
"""
|
||||||
|
Split audio file into tracks based on setlist entries.
|
||||||
|
Skip tracks that already exist.
|
||||||
|
Returns list of track info with status (created or skipped).
|
||||||
|
"""
|
||||||
|
print(f"Loading audio file: {mp3_path}")
|
||||||
|
audio = AudioSegment.from_file(mp3_path)
|
||||||
|
total_ms = len(audio)
|
||||||
|
|
||||||
|
track_results = []
|
||||||
|
created_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
|
for idx, (start_ms, title) in enumerate(entries, start=1):
|
||||||
|
# Determine end time
|
||||||
|
end_ms = entries[idx][0] if idx < len(entries) else total_ms
|
||||||
|
|
||||||
|
# Create filename
|
||||||
|
filename = f"{idx:02d} - {sanitize(title)}.mp3"
|
||||||
|
filepath = os.path.join(output_dir, filename)
|
||||||
|
|
||||||
|
# Check if file already exists
|
||||||
|
if os.path.isfile(filepath):
|
||||||
|
print(f"Skipping track {idx}/{len(entries)}: {filename} (already exists)")
|
||||||
|
track_results.append({
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'skipped'
|
||||||
|
})
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract and export segment
|
||||||
|
print(f"Creating track {idx}/{len(entries)}: {filename}")
|
||||||
|
segment = audio[start_ms:end_ms]
|
||||||
|
|
||||||
|
# Export with 320kbps
|
||||||
|
segment.export(filepath, format='mp3', bitrate='320k')
|
||||||
|
|
||||||
|
# Add ID3 tags
|
||||||
|
try:
|
||||||
|
tags = EasyID3(filepath)
|
||||||
|
tags['title'] = title
|
||||||
|
tags['album'] = album
|
||||||
|
tags['artist'] = artist
|
||||||
|
tags['tracknumber'] = str(idx)
|
||||||
|
tags.save()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not add ID3 tags to {filename}: {e}")
|
||||||
|
|
||||||
|
track_results.append({
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'created'
|
||||||
|
})
|
||||||
|
created_count += 1
|
||||||
|
|
||||||
|
return track_results, created_count, skipped_count
|
||||||
|
|
||||||
|
|
||||||
|
def set_permissions(directory: str):
|
||||||
|
"""Set directory permissions to 775 recursively."""
|
||||||
|
try:
|
||||||
|
subprocess.run(['chmod', '-R', '775', directory], check=True)
|
||||||
|
print(f"Set permissions 775 on {directory}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Warning: Could not set permissions: {e}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Warning: chmod command not found (might be on Windows)")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""Render the main page."""
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/split', methods=['POST'])
|
||||||
|
def split_concert():
|
||||||
|
"""Handle the split request."""
|
||||||
|
try:
|
||||||
|
# Get form data
|
||||||
|
url = request.form.get('youtube_url', '').strip()
|
||||||
|
artist = request.form.get('artist', '').strip()
|
||||||
|
album = request.form.get('album', '').strip()
|
||||||
|
setlist_text = request.form.get('setlist', '').strip()
|
||||||
|
|
||||||
|
# Validate required inputs (setlist is OPTIONAL)
|
||||||
|
if not url:
|
||||||
|
return jsonify({'error': 'No YouTube URL provided'}), 400
|
||||||
|
|
||||||
|
if not artist:
|
||||||
|
return jsonify({'error': 'No artist name provided'}), 400
|
||||||
|
|
||||||
|
if not album:
|
||||||
|
return jsonify({'error': 'No album name provided'}), 400
|
||||||
|
|
||||||
|
# Sanitize album for directory name
|
||||||
|
album_sanitized = sanitize(album)
|
||||||
|
|
||||||
|
# Create output directory directly in DOWNLOAD_FOLDER
|
||||||
|
output_dir = os.path.join(DOWNLOAD_FOLDER, album_sanitized)
|
||||||
|
|
||||||
|
# Create directory only if it doesn't exist
|
||||||
|
if not os.path.exists(output_dir):
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
print(f"✓ Created album directory: {output_dir}")
|
||||||
|
else:
|
||||||
|
print(f"✓ Album directory already exists: {output_dir}")
|
||||||
|
|
||||||
|
# Download audio
|
||||||
|
print(f"Downloading audio from: {url}")
|
||||||
|
mp3_path, info = download_youtube_audio(url, DOWNLOAD_FOLDER)
|
||||||
|
|
||||||
|
# DECISION POINT: Empty setlist = Single song mode
|
||||||
|
if not setlist_text:
|
||||||
|
print("📀 Single song mode: No setlist provided")
|
||||||
|
|
||||||
|
# Use video title as track name
|
||||||
|
track_title = info.get('title', 'Unknown Track')
|
||||||
|
filename = f"01 - {sanitize(track_title)}.mp3"
|
||||||
|
filepath = os.path.join(output_dir, filename)
|
||||||
|
|
||||||
|
# Check if file already exists
|
||||||
|
if os.path.isfile(filepath):
|
||||||
|
print(f"⊘ Track already exists: {filename}")
|
||||||
|
os.remove(mp3_path) # Clean up downloaded file
|
||||||
|
|
||||||
|
track_results = [{
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'skipped'
|
||||||
|
}]
|
||||||
|
created_count = 0
|
||||||
|
skipped_count = 1
|
||||||
|
else:
|
||||||
|
# Move and rename the file (no splitting needed)
|
||||||
|
print(f"✓ Creating single track: {filename}")
|
||||||
|
|
||||||
|
# Load audio to re-export with proper tags
|
||||||
|
audio = AudioSegment.from_file(mp3_path)
|
||||||
|
audio.export(filepath, format='mp3', bitrate='320k')
|
||||||
|
|
||||||
|
# Add ID3 tags
|
||||||
|
try:
|
||||||
|
tags = EasyID3(filepath)
|
||||||
|
tags['title'] = track_title
|
||||||
|
tags['album'] = album
|
||||||
|
tags['artist'] = artist
|
||||||
|
tags['tracknumber'] = '1'
|
||||||
|
tags.save()
|
||||||
|
print(f"✓ Added ID3 tags")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Warning: Could not add ID3 tags: {e}")
|
||||||
|
|
||||||
|
# Clean up original
|
||||||
|
os.remove(mp3_path)
|
||||||
|
|
||||||
|
track_results = [{
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'created'
|
||||||
|
}]
|
||||||
|
created_count = 1
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
|
else:
|
||||||
|
# SPLIT MODE: Parse setlist and split audio
|
||||||
|
print(f"✂️ Split mode: Processing setlist with {len(setlist_text.splitlines())} lines")
|
||||||
|
|
||||||
|
# Parse setlist for splitting
|
||||||
|
try:
|
||||||
|
entries = parse_setlist(setlist_text)
|
||||||
|
except ValueError as e:
|
||||||
|
os.remove(mp3_path) # Clean up downloaded file
|
||||||
|
return jsonify({'error': f'Setlist parsing error: {str(e)}'}), 400
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
os.remove(mp3_path) # Clean up downloaded file
|
||||||
|
return jsonify({'error': 'No valid tracks found in setlist'}), 400
|
||||||
|
|
||||||
|
# Split audio into tracks (skipping existing ones)
|
||||||
|
print(f"Splitting into {len(entries)} tracks...")
|
||||||
|
track_results, created_count, skipped_count = split_audio(
|
||||||
|
mp3_path, entries, album, artist, output_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up original MP3
|
||||||
|
print(f"Removing original file: {mp3_path}")
|
||||||
|
os.remove(mp3_path)
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
set_permissions(output_dir)
|
||||||
|
|
||||||
|
# Return success response with detailed track information
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'album': album,
|
||||||
|
'artist': artist,
|
||||||
|
'total_tracks': len(track_results),
|
||||||
|
'created_count': created_count,
|
||||||
|
'skipped_count': skipped_count,
|
||||||
|
'tracks': track_results,
|
||||||
|
'output_dir': output_dir
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Get configuration from environment variables
|
||||||
|
host = os.environ.get('FLASK_HOST', '0.0.0.0')
|
||||||
|
port = int(os.environ.get('FLASK_PORT', 5000))
|
||||||
|
debug = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
|
||||||
|
|
||||||
|
print(f"Starting YouTube Concert Splitter on {host}:{port}")
|
||||||
|
print(f"Music directory: {DOWNLOAD_FOLDER}")
|
||||||
|
|
||||||
|
# Run on all interfaces, configurable port
|
||||||
|
app.run(host="0.0.0.0", port=port, debug=debug)
|
||||||
263
cookies.txt
Normal file
263
cookies.txt
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
# Netscape HTTP Cookie File
|
||||||
|
# This file is generated by yt-dlp. Do not edit.
|
||||||
|
|
||||||
|
accounts.google.com FALSE / TRUE 13412705143000000 OTZ
|
||||||
|
accounts.google.com FALSE / TRUE 13444936467584108 SMSV
|
||||||
|
accounts.google.com FALSE / TRUE 13446034542734482 __Host-GAPS
|
||||||
|
ogs.google.com FALSE / TRUE 13412705172000000 OTZ
|
||||||
|
.standsapp.org TRUE / FALSE 13417889183000000 _fbp
|
||||||
|
.standsapp.org TRUE / FALSE 13444673186249540 _ga
|
||||||
|
.standsapp.org TRUE / FALSE 13444673186259278 _ga_YKB0534KDL
|
||||||
|
.standsapp.org TRUE / FALSE 13417889186000000 _gcl_au
|
||||||
|
.region1.google-analytics.com TRUE / TRUE 13417889187217774 ar_debug
|
||||||
|
www.standsapp.org FALSE / TRUE 13441649179130482 pll_language
|
||||||
|
.youtube.com TRUE / TRUE 1782604482 VISITOR_INFO1_LIVE ObDp_56FMXo
|
||||||
|
.youtube.com TRUE / TRUE 1782604482 VISITOR_PRIVACY_METADATA CgJJVBIhEh0SGwsMDg8QERITFBUWFxgZGhscHR4fICEiIyQlJiAS
|
||||||
|
.youtube.com TRUE / TRUE 1782604481 __Secure-ROLLOUT_TOKEN GMyewrv_45ED
|
||||||
|
.youtube.com TRUE / TRUE 13444936467727630 __Secure-3PAPISID
|
||||||
|
.youtube.com TRUE / TRUE 13444936467727524 __Secure-3PSID
|
||||||
|
.youtube.com TRUE / FALSE 0 PREF hl=en&tz=UTC
|
||||||
|
.youtube.com TRUE / TRUE 13443061673959104 __Secure-1PSIDTS
|
||||||
|
.youtube.com TRUE / TRUE 13443061673959260 __Secure-3PSIDTS
|
||||||
|
.youtube.com TRUE / TRUE 13443061731393390 __Secure-3PSIDCC
|
||||||
|
.youtube.com TRUE / TRUE 0 SOCS CAI
|
||||||
|
.youtube.com TRUE / TRUE 1767054281 GPS 1
|
||||||
|
.youtube.com TRUE / TRUE 1830124482 __Secure-YT_TVFAS t=490847&s=2
|
||||||
|
.youtube.com TRUE / TRUE 1782604482 DEVICE_INFO ChxOelU0T1RRek1qWXlNRGczTWpnME5qTTJNdz09EMKpzMoGGMKpzMoG
|
||||||
|
.youtube.com TRUE / TRUE 0 YSC ir2O5VGnpYY
|
||||||
|
.youtube.com TRUE /tv TRUE 1799884482 __Secure-YT_DERP CIjqrJfaAg%3D%3D
|
||||||
|
.google.com TRUE / FALSE 13425665336925712 SEARCH_SAMESITE
|
||||||
|
.google.com TRUE / TRUE 13425665336925768 __Secure-BUCKET
|
||||||
|
.google.com TRUE / FALSE 13444936467583980 APISID
|
||||||
|
.google.com TRUE / FALSE 13444936467583932 HSID
|
||||||
|
.google.com TRUE / TRUE 13444936467584004 SAPISID
|
||||||
|
.google.com TRUE / TRUE 13444936467583956 SSID
|
||||||
|
.google.com TRUE / TRUE 13444936467584030 __Secure-1PAPISID
|
||||||
|
.google.com TRUE / TRUE 13444936467583818 __Secure-1PSID
|
||||||
|
.google.com TRUE / TRUE 13444936467584052 __Secure-3PAPISID
|
||||||
|
.google.com TRUE / TRUE 13444936467583844 __Secure-3PSID
|
||||||
|
.google.com TRUE / TRUE 13425665336130490 AEC
|
||||||
|
.google.com TRUE / TRUE 13443061586344152 __Secure-1PSIDTS
|
||||||
|
.google.com TRUE / TRUE 13443061586344312 __Secure-3PSIDTS
|
||||||
|
.google.com TRUE / TRUE 13411525885680368 __Secure-STRP
|
||||||
|
.google.com TRUE / FALSE 13443061648654366 SIDCC
|
||||||
|
.google.com TRUE / TRUE 13443061648654472 __Secure-1PSIDCC
|
||||||
|
.google.com TRUE / TRUE 13443061648654508 __Secure-3PSIDCC
|
||||||
|
extensions.gnome.org FALSE / TRUE 13441564334641682 csrftoken
|
||||||
|
.gnome-look.org TRUE / FALSE 13425883544000000 _pk_ref.20.5702
|
||||||
|
.gnome-look.org TRUE / FALSE 13444070744000000 _pk_id.20.5702
|
||||||
|
.gnome-look.org TRUE / FALSE 13410117508000000 _pk_ses.20.5702
|
||||||
|
www.gnome-look.org FALSE / FALSE 13410119251490280 verified
|
||||||
|
mail.google.com FALSE / TRUE 13412709741379250 __Host-GMAIL_SCH_GML
|
||||||
|
mail.google.com FALSE / TRUE 13412709741379140 __Host-GMAIL_SCH_GMN
|
||||||
|
mail.google.com FALSE / TRUE 13412709741379232 __Host-GMAIL_SCH_GMS
|
||||||
|
mail.google.com FALSE / TRUE 13444819212601578 __Secure-OSID
|
||||||
|
mail.google.com FALSE /mail/u/1 TRUE 13441795214000000 GMAIL_LF
|
||||||
|
mail.google.com FALSE /mail/u/1 TRUE 13412293260979888 COMPASS
|
||||||
|
mail.google.com FALSE /chat/u/0 TRUE 13412338995152670 COMPASS
|
||||||
|
contacts.google.com FALSE / TRUE 13412709746000000 OTZ
|
||||||
|
.github.com TRUE / TRUE 13441654551289152 _octo
|
||||||
|
.github.com TRUE / TRUE 13441654551289172 logged_in
|
||||||
|
.jetbrains.com TRUE / TRUE 13441224316000000 first_utm_parameters
|
||||||
|
.jetbrains.com TRUE / FALSE 13425740930000000 optimizelySession
|
||||||
|
.jetbrains.com TRUE / TRUE 13412781128762452 JBA
|
||||||
|
.jetbrains.com TRUE / FALSE 13442513677000000 _clck
|
||||||
|
.jetbrains.com TRUE / TRUE 13442081676000000 last_utm_parameters
|
||||||
|
.jetbrains.com TRUE / TRUE 13417896316524766 FPAU
|
||||||
|
.jetbrains.com TRUE / FALSE 13411064113000000 _clsk
|
||||||
|
.jetbrains.com TRUE / TRUE 13445537716281512 _dcid
|
||||||
|
.jetbrains.com TRUE / TRUE 13445537712344148 _ga
|
||||||
|
.jetbrains.com TRUE / TRUE 13418753712000000 _rdt_uuid
|
||||||
|
.jetbrains.com TRUE / FALSE 13426529711000000 optimizelyEndUserId
|
||||||
|
.jetbrains.com TRUE / TRUE 13445537839448894 _ga_9J976DJZ68
|
||||||
|
.jetbrains.com TRUE / TRUE 13445537839440488 _ga_M8TDRLXFQH
|
||||||
|
www.jetbrains.com FALSE / FALSE 13417896316000000 userToken
|
||||||
|
.docker.com TRUE / FALSE 13444332791000000 _cs_c
|
||||||
|
.docker.com TRUE / FALSE 13444728790466464 _ga_XJWPQMJYHQ
|
||||||
|
.docker.com TRUE / TRUE 13444246390000000 _hp5_event_props.4204607514
|
||||||
|
.docker.com TRUE / FALSE 13441704793000000 ajs_anonymous_id
|
||||||
|
.docker.com TRUE / FALSE 13410170590000000 signals-sdk-session-id
|
||||||
|
.docker.com TRUE / FALSE 13441704790000000 signals-sdk-user-id
|
||||||
|
.docker.com TRUE / TRUE 13444246416000000 _hp5_let.4204607514
|
||||||
|
.docker.com TRUE / FALSE 13410170664000000 _cs_s
|
||||||
|
docs.docker.com FALSE / TRUE 13410183190000000 _gd_session
|
||||||
|
docs.docker.com FALSE / TRUE 13444728790479132 _gd_visitor
|
||||||
|
.docs.docker.com TRUE / TRUE 13441704791000000 _zitok
|
||||||
|
www.virtualbox.org FALSE / TRUE 13417946132189528 trac_session
|
||||||
|
.microsoft.com TRUE / TRUE 13441708111888220 MC1
|
||||||
|
.microsoft.com TRUE / TRUE 13410373910819606 MS0
|
||||||
|
.microsoft.com TRUE / TRUE 13444068110768244 MUID
|
||||||
|
code.visualstudio.com FALSE / TRUE 13441708106488964 MSFPC
|
||||||
|
code.visualstudio.com FALSE / TRUE 13441708113109592 MicrosoftApplicationsTelemetryDeviceId
|
||||||
|
code.visualstudio.com FALSE / TRUE 13410173913000000 ai_session
|
||||||
|
support.lenovo.com FALSE / FALSE 13444387305000000 _evidon_consent_cookie
|
||||||
|
support.lenovo.com FALSE / FALSE 13444300905000000 _evidon_suppress_notification_cookie
|
||||||
|
.lenovo.com TRUE / FALSE 13444732905363200 s_ecid
|
||||||
|
.lenovo.com TRUE / TRUE 13441708906000000 QuantumMetricUserID
|
||||||
|
.ebay.it TRUE / FALSE 13425724918004140 __uzma
|
||||||
|
.ebay.it TRUE / FALSE 13425724918004160 __uzmb
|
||||||
|
.ebay.it TRUE / FALSE 13425724918004176 __uzmc
|
||||||
|
.ebay.it TRUE / FALSE 13425724918004192 __uzmd
|
||||||
|
.ebay.it TRUE / FALSE 13425724918004208 __uzme
|
||||||
|
.ebay.it TRUE / TRUE 13444732936840336 dp1
|
||||||
|
.ebay.it TRUE / TRUE 13444732936840352 nonsession
|
||||||
|
.support.lenovo.com TRUE / FALSE 13411468906097544 esupport#lang1
|
||||||
|
src.ebay-us.com FALSE / TRUE 13444732921099446 thx_guid
|
||||||
|
src.ebay-us.com FALSE / TRUE 13444732921099488 tmx_guid
|
||||||
|
.gemini.google.com TRUE / FALSE 13444733417184702 _ga
|
||||||
|
.gemini.google.com TRUE / FALSE 13417894934000000 _gcl_au
|
||||||
|
.anydesk.com TRUE / FALSE 13417964415000000 _gcl_au
|
||||||
|
.anydesk.com TRUE / TRUE 13410190330763684 __cf_bm
|
||||||
|
.teamviewer.com TRUE / TRUE 13443884532000000 AMCV_4DBF233B617961000A495FE3%40AdobeOrg
|
||||||
|
.teamviewer.com TRUE / FALSE 13441724535000000 OptanonAlertBoxClosed
|
||||||
|
.teamviewer.com TRUE / TRUE 13441724549693132 __bs
|
||||||
|
.teamviewer.com TRUE / TRUE 13441724531375784 aep_fpid
|
||||||
|
.teamviewer.com TRUE / TRUE 13444316532605844 kndctr_4DBF233B617961000A495FE3_AdobeOrg_identity
|
||||||
|
.teamviewer.com TRUE / FALSE 13410274951000000 _clsk
|
||||||
|
.teamviewer.com TRUE / FALSE 13417964535000000 _gcl_au
|
||||||
|
.teamviewer.com TRUE / TRUE 13412784154000000 geo-preference
|
||||||
|
.teamviewer.com TRUE / TRUE 13410190358634336 kndctr_4DBF233B617961000A495FE3_AdobeOrg_cluster
|
||||||
|
engage.teamviewer.com FALSE /api/in FALSE 13410189536282488 cv-sid
|
||||||
|
engage.teamviewer.com FALSE /api/in/wg/conf FALSE 13410189535379236 cv-sid
|
||||||
|
engage.teamviewer.com FALSE /api/in/wg/conf/kbybqvXk3q FALSE 13410189535346756 cv-sid
|
||||||
|
anydesk.com FALSE / FALSE 13441724530000000 nQ_cookieId
|
||||||
|
.company-target.com TRUE / TRUE 13444402950598434 tuuid
|
||||||
|
.company-target.com TRUE / TRUE 13444402950598496 tuuid_lu
|
||||||
|
myaccount.google.com FALSE / TRUE 13412851140000000 OTZ
|
||||||
|
www.google.com FALSE / TRUE 13412851140000000 OTZ
|
||||||
|
chat.google.com FALSE / TRUE 13412851215000000 OTZ
|
||||||
|
meet.google.com FALSE / TRUE 13444819228917146 OSID
|
||||||
|
meet.google.com FALSE / TRUE 13444819228917248 __Secure-OSID
|
||||||
|
chat.deepseek.com FALSE / FALSE 13415444163000000 _gc_usr_id_cs0_d0_sec0_part0
|
||||||
|
chat.deepseek.com FALSE / FALSE 13444820164541440 smidV2
|
||||||
|
chat.deepseek.com FALSE / FALSE 13410265209000000 _gc_s_cs0_d0_sec0_part0
|
||||||
|
.deepseek.com TRUE / FALSE 13441796177000000 ds_cookie_preference
|
||||||
|
.deepseek.com TRUE / TRUE 13433591100000000 intercom-device-id-guh50jw4
|
||||||
|
ubuntu.com FALSE / FALSE 13441798110000000 _cookies_accepted
|
||||||
|
.ubuntu.com TRUE / FALSE 13441798147000000 _CEFT
|
||||||
|
.ubuntu.com TRUE / FALSE 13410348537000000 _ce.clock_data
|
||||||
|
.ubuntu.com TRUE / FALSE 13444822146978580 _ga
|
||||||
|
.ubuntu.com TRUE / FALSE 13418038110000000 _gcl_au
|
||||||
|
.ubuntu.com TRUE / FALSE 13444822147120776 _mkto_trk
|
||||||
|
.ubuntu.com TRUE / TRUE 13418038146000000 _rdt_uuid
|
||||||
|
.ubuntu.com TRUE / TRUE 13441798147000000 _zitok
|
||||||
|
.ubuntu.com TRUE / FALSE 13444822698453376 _ga_5LTL1CNEJM
|
||||||
|
.ubuntu.com TRUE / FALSE 13444822698457298 _ga_PGQQ61N4N6
|
||||||
|
centroricerche.sanmarcoweb.com FALSE /csv FALSE 13412876936442220 webUser
|
||||||
|
drive.google.com FALSE / TRUE 13444906256578716 OSID
|
||||||
|
drive.google.com FALSE / TRUE 13412938303000000 OTZ
|
||||||
|
copilot.microsoft.com FALSE / TRUE 13444042968457108 MUID
|
||||||
|
copilot.microsoft.com FALSE / FALSE 13444042968457168 MUIDB
|
||||||
|
copilot.microsoft.com FALSE / FALSE 13444042968457236 _EDGE_V
|
||||||
|
copilot.microsoft.com FALSE / TRUE 13444907053878120 userSidebarOpen
|
||||||
|
copilot.microsoft.com FALSE / TRUE 13441908111000000 BCP
|
||||||
|
copilot.microsoft.com FALSE / TRUE 13441908111000000 CMCCP
|
||||||
|
copilot.microsoft.com FALSE / TRUE 13441908144519756 MicrosoftApplicationsTelemetryDeviceId
|
||||||
|
copilot.microsoft.com FALSE / TRUE 13410374011000000 ai_session
|
||||||
|
copilot.microsoft.com FALSE / TRUE 13418149014000000 hasBeenMsalAuthenticated
|
||||||
|
.chatgpt.com TRUE / FALSE 13441451098894820 oai-did
|
||||||
|
.chatgpt.com TRUE / FALSE 13425901599933620 oai-allow-ne
|
||||||
|
.chatgpt.com TRUE / FALSE 13425901599933554 oai_consent_analytics
|
||||||
|
.chatgpt.com TRUE / FALSE 13425901599933600 oai_consent_marketing
|
||||||
|
servizi2.inps.it FALSE / FALSE 13441888258739948 cookiesession1
|
||||||
|
servizi2.inps.it FALSE / FALSE 13410694722000000 aem_dict_flag
|
||||||
|
servizi2.inps.it FALSE / FALSE 13413200323000000 s_nr30
|
||||||
|
servizi2.inps.it FALSE / FALSE 13445168323020904 s_tslv
|
||||||
|
servizi2.inps.it FALSE / FALSE 13444563523000000 _pk_id.2.e578
|
||||||
|
servizi2.inps.it FALSE / FALSE 13410610125000000 _pk_ses.2.e578
|
||||||
|
.demdex.net TRUE / TRUE 13425904258198638 demdex
|
||||||
|
www.inps.it FALSE / TRUE 13441888258000000 kampyle_userid
|
||||||
|
www.inps.it FALSE / FALSE 13426376292000000 _pk_ref.2.ff96
|
||||||
|
www.inps.it FALSE / FALSE 13410694692000000 aem_dict_flag
|
||||||
|
www.inps.it FALSE / FALSE 13444307458000000 _pk_id.2.ff96
|
||||||
|
www.inps.it FALSE / FALSE 13410610131000000 _pk_ses.2.ff96
|
||||||
|
www.inps.it FALSE / TRUE 13442144332000000 kampyleSessionPageCounter
|
||||||
|
www.inps.it FALSE / TRUE 13442144326000000 kampyleUserSession
|
||||||
|
www.inps.it FALSE / TRUE 13442144326000000 kampyleUserSessionsCount
|
||||||
|
www.inps.it FALSE / FALSE 13413200332000000 s_nr30
|
||||||
|
www.inps.it FALSE / FALSE 13445168332065730 s_tslv
|
||||||
|
serviziweb2.inps.it FALSE / FALSE 13441888269938272 cookiesession1
|
||||||
|
serviziweb2.inps.it FALSE / FALSE 13426376294000000 _pk_ref.2.7483
|
||||||
|
serviziweb2.inps.it FALSE / FALSE 13444307470000000 _pk_id.2.7483
|
||||||
|
serviziweb2.inps.it FALSE / FALSE 13413200358000000 s_nr30
|
||||||
|
serviziweb2.inps.it FALSE / FALSE 13445168358593804 s_tslv
|
||||||
|
chatgpt.com FALSE / FALSE 13441891666000000 _dd_s
|
||||||
|
.c1.microsoft.com TRUE / TRUE 13410976910768284 MR
|
||||||
|
.c.bing.com TRUE / TRUE 13444068111709472 SRM_B
|
||||||
|
.c.bing.com TRUE / TRUE 13444068111709486 SRM_I
|
||||||
|
.login.microsoftonline.com TRUE / TRUE 13444068125000000 brcap
|
||||||
|
.live.com TRUE / TRUE 13427677343408184 ANON
|
||||||
|
.live.com TRUE / TRUE 13444068143408148 MSPAuth
|
||||||
|
.live.com TRUE / TRUE 13444068143408160 MSPProf
|
||||||
|
.live.com TRUE / TRUE 13419037343408172 NAP
|
||||||
|
.live.com TRUE / TRUE 13444068143408080 PPLState
|
||||||
|
.login.live.com TRUE / TRUE 13444068143408258 JSHP
|
||||||
|
.login.live.com TRUE / TRUE 13444068136160892 MSCC
|
||||||
|
.login.live.com TRUE / TRUE 13444068143408128 MSPCID
|
||||||
|
.login.live.com TRUE / TRUE 13444068143408228 SDIDC
|
||||||
|
login.microsoftonline.com FALSE / TRUE 13412964132162732 fpc
|
||||||
|
.copilot.microsoft.com TRUE / TRUE 13410374814381838 __cf_bm
|
||||||
|
.claude.ai TRUE / FALSE 13444936438175780 __ssid
|
||||||
|
.claude.ai TRUE / TRUE 13444936439527718 _fbp
|
||||||
|
.claude.ai TRUE / TRUE 13441912439527496 anthropic-consent-preferences
|
||||||
|
.claude.ai TRUE / TRUE 13443010568393446 CH-prefers-color-scheme
|
||||||
|
.claude.ai TRUE / FALSE 13443010570000000 ajs_anonymous_id
|
||||||
|
.claude.ai TRUE / FALSE 13443010570000000 ajs_user_id
|
||||||
|
.claude.ai TRUE / TRUE 13434804571000000 intercom-device-id-lupk8zyo
|
||||||
|
.claude.ai TRUE / TRUE 13443010568392894 lastActiveOrg
|
||||||
|
.claude.ai TRUE / TRUE 13443025659831380 user-sidebar-visible-on-load
|
||||||
|
.claude.ai TRUE /fc TRUE 13412968441398182 ARID
|
||||||
|
claude.ai FALSE / FALSE 13425928440000000 g_state
|
||||||
|
claude.ai FALSE / TRUE 13411518465662864 activitySessionId
|
||||||
|
claude.ai FALSE / TRUE 13437395265662978 anthropic-device-id
|
||||||
|
.google.it TRUE / TRUE 13444936467832424 SAPISID
|
||||||
|
.google.it TRUE / TRUE 13444936467832344 SSID
|
||||||
|
.google.it TRUE / TRUE 13444936467832470 __Secure-1PAPISID
|
||||||
|
.google.it TRUE / TRUE 13444936467832512 __Secure-3PAPISID
|
||||||
|
.google.it TRUE / TRUE 13444936467832254 __Secure-3PSID
|
||||||
|
eu-west-1.signin.aws FALSE /platform TRUE 13441978750943354 platform-ubid
|
||||||
|
wiki.sanmarcoweb.com FALSE / FALSE 13426008228983252 wikiSynergyToken
|
||||||
|
wiki.sanmarcoweb.com FALSE / FALSE 13426008228983230 wikiSynergyUserID
|
||||||
|
wiki.sanmarcoweb.com FALSE / FALSE 13426008228983242 wikiSynergyUserName
|
||||||
|
.typeless.now TRUE / FALSE 13445081082653212 _ga
|
||||||
|
.typeless.now TRUE / FALSE 13445081082652742 _ga_SW8FCPTQ4J
|
||||||
|
.typeless.now TRUE / FALSE 13418297082000000 _gcl_au
|
||||||
|
.extensions-hub.com TRUE / TRUE 13410528281387470 _ga_FHLJ7MSZL4
|
||||||
|
.idserver.servizicie.interno.gov.it TRUE / TRUE 13410609199447872 JSESSIONID
|
||||||
|
.inps.it TRUE / TRUE 13442144331000000 lang
|
||||||
|
.inps.it TRUE / FALSE 13410610158000000 adobeLastVisitedUrl
|
||||||
|
.inps.it TRUE / TRUE 13410610155000000 kndctr_06F9BFD15A2171460A495CF8_AdobeOrg_cluster
|
||||||
|
smilearning.sanmarcoweb.com FALSE / TRUE 13426418017014876 my_wiki_wikiToken
|
||||||
|
smilearning.sanmarcoweb.com FALSE / TRUE 13426418017014768 my_wiki_wikiUserID
|
||||||
|
smilearning.sanmarcoweb.com FALSE / TRUE 13426418017014860 my_wiki_wikiUserName
|
||||||
|
securden.sanmarcoweb.com FALSE / TRUE 13442336738332264 securdenpost6549251129127743
|
||||||
|
securden.sanmarcoweb.com FALSE / TRUE 13445447138332338 securdensession5561950725146635
|
||||||
|
calendar-pa.clients6.google.com FALSE / TRUE 13411835737257050 COMPASS
|
||||||
|
calendar.google.com FALSE / TRUE 13411841563803676 COMPASS
|
||||||
|
bitbucket.sanmarcoweb.com FALSE / TRUE 13413569612442948 _atl_bitbucket_remember_me
|
||||||
|
a26669750187.cdn.optimizely.com FALSE / TRUE 13426529712172480 https://www.jetbrains.com_oeu1765646785917r0.5446460522019785$$26613100737$$session_state
|
||||||
|
.getpostman.com TRUE / TRUE 13413572325657536 _PUB_ID
|
||||||
|
.getpostman.com TRUE / FALSE 13445540321620854 _pm.store
|
||||||
|
.getpostman.com TRUE / FALSE 13418756321823388 _pmt
|
||||||
|
.getpostman.com TRUE / FALSE 13442516320000000 analytics_session_id
|
||||||
|
.getpostman.com TRUE / FALSE 13442516320000000 analytics_session_id.last_access
|
||||||
|
.getpostman.com TRUE / TRUE 13442516325657480 dashboard_beta
|
||||||
|
.getpostman.com TRUE / TRUE 13413572325657408 getpostmanlogin
|
||||||
|
.postman.com TRUE / TRUE 13413572327008484 _PUB_ID
|
||||||
|
.postman.com TRUE / TRUE 13410982127008528 __cf_bm
|
||||||
|
.postman.com TRUE / FALSE 13418756327008106 _pmt
|
||||||
|
.postman.com TRUE / TRUE 13442516327008418 dashboard_beta
|
||||||
|
.postman.com TRUE / TRUE 13413572327008350 getpostmanlogin
|
||||||
|
.identity.getpostman.com TRUE / TRUE 13410982114779800 __cf_bm
|
||||||
|
.postman.co TRUE / FALSE 13418756326340216 _pmt
|
||||||
|
.postman.co TRUE / TRUE 13442516326340450 dashboard_beta
|
||||||
|
identity.getpostman.com FALSE / TRUE 13444676315290018 dwndvc
|
||||||
|
identity.getpostman.com FALSE / TRUE 13411066714779750 legacy_sails.sid
|
||||||
|
identity.postman.co FALSE / TRUE 13411066726340048 legacy_sails.sid
|
||||||
|
chromewebstore.google.com FALSE / TRUE 13414117587000000 OTZ
|
||||||
|
.aitopia.ai TRUE / TRUE 13446085577353154 PHPSESSID
|
||||||
|
.aitopia.ai TRUE / FALSE 13421893577353120 eref
|
||||||
|
.aitopia.ai TRUE / FALSE 13421893577353136 hopekey
|
||||||
|
extensions.aitopia.ai FALSE / TRUE 13411527377353168 __cflb
|
||||||
|
.chromewebstore.google.com TRUE / FALSE 13446085645754266 _ga
|
||||||
|
.chromewebstore.google.com TRUE / FALSE 13446085669817496 _ga_KHZNC1Q6K0
|
||||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
youtube-splitter:
|
||||||
|
build: .
|
||||||
|
container_name: youtube-concert-splitter
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
# Mount server's music directory to container
|
||||||
|
- /mnt/media/music:/app/youtube
|
||||||
|
# Optional: Mount cookies.txt if you have it
|
||||||
|
- ./cookies.txt:/app/cookies.txt
|
||||||
|
environment:
|
||||||
|
- FLASK_ENV=production
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- DOWNLOAD_FOLDER=/app/youtube
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000')"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Flask==3.0.0
|
||||||
|
yt-dlp==2024.12.6
|
||||||
|
pydub==0.25.1
|
||||||
|
mutagen==1.47.0
|
||||||
|
Werkzeug==3.0.1
|
||||||
434
templates/index.html
Normal file
434
templates/index.html
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>YouTube Concert Splitter</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: #fff;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 28px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #444;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 200px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #777;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 14px 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-overlay {
|
||||||
|
position: fixed;
|
||||||
|
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;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 6px solid #f3f3f3;
|
||||||
|
border-top: 6px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-text {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-subtext {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 20px;
|
||||||
|
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 {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-list li {
|
||||||
|
padding: 8px;
|
||||||
|
background: white;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-list li.skipped {
|
||||||
|
background: #fff9e6;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-list li.created {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-info {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #e8f4f8;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🎵 YouTube Concert Splitter</h1>
|
||||||
|
<p class="subtitle">Download and split concert videos into individual tracks</p>
|
||||||
|
|
||||||
|
<form id="splitterForm">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="youtube_url">YouTube URL:</label>
|
||||||
|
<input type="text"
|
||||||
|
id="youtube_url"
|
||||||
|
name="youtube_url"
|
||||||
|
placeholder="https://www.youtube.com/watch?v=..."
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="artist">Artist Name:</label>
|
||||||
|
<input type="text"
|
||||||
|
id="artist"
|
||||||
|
name="artist"
|
||||||
|
placeholder="e.g., Bob Dylan"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="album">Album Name:</label>
|
||||||
|
<input type="text"
|
||||||
|
id="album"
|
||||||
|
name="album"
|
||||||
|
placeholder="e.g., Live at Madison Square Garden"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="setlist">Setlist (timestamps and track titles) - Optional:</label>
|
||||||
|
<textarea id="setlist"
|
||||||
|
name="setlist"
|
||||||
|
placeholder="0:00 Opening Song 3:45 Second Track 7:30 Third Song (Leave empty to download as single track - no splitting)"></textarea>
|
||||||
|
<div class="help-text">
|
||||||
|
<strong>Split Mode:</strong> Enter "TIMESTAMP TRACK_TITLE" per line (e.g., "0:00 Song Name")<br>
|
||||||
|
<strong>Single Mode:</strong> Leave this field completely empty to download entire video as one track
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" id="submitBtn">Split Concert</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="message"></div>
|
||||||
|
<div id="results"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loader" class="loader-overlay" style="display: none;">
|
||||||
|
<div class="loader-content">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<div class="loader-text">Processing...</div>
|
||||||
|
<div class="loader-subtext">Downloading and splitting tracks. This may take several minutes.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('splitterForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Clear previous messages
|
||||||
|
document.getElementById('message').innerHTML = '';
|
||||||
|
document.getElementById('results').innerHTML = '';
|
||||||
|
|
||||||
|
// Show loader
|
||||||
|
document.getElementById('loader').style.display = 'flex';
|
||||||
|
document.getElementById('submitBtn').disabled = true;
|
||||||
|
|
||||||
|
const url = document.getElementById('youtube_url').value;
|
||||||
|
const artist = document.getElementById('artist').value;
|
||||||
|
const album = document.getElementById('album').value;
|
||||||
|
const setlist = document.getElementById('setlist').value;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('youtube_url', url);
|
||||||
|
formData.append('artist', artist);
|
||||||
|
formData.append('album', album);
|
||||||
|
formData.append('setlist', setlist);
|
||||||
|
|
||||||
|
fetch('/split', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('loader').style.display = 'none';
|
||||||
|
document.getElementById('submitBtn').disabled = false;
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
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) {
|
||||||
|
html += `<div class="message warning"><strong>Note:</strong> ${data.skipped_count} track(s) already existed and were skipped.</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<div class="message success"><strong>Success!</strong> Processing complete.</div>`;
|
||||||
|
html += `<div class="results">`;
|
||||||
|
html += `<h3>🎸 ${data.artist} - ${data.album}</h3>`;
|
||||||
|
|
||||||
|
html += `<div class="stats">`;
|
||||||
|
html += `<div class="stat-item">`;
|
||||||
|
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>`;
|
||||||
|
html += `<h4>Track Details:</h4>`;
|
||||||
|
html += `<ul class="track-list">`;
|
||||||
|
data.tracks.forEach(track => {
|
||||||
|
const className = track.status === 'created' ? 'created' : 'skipped';
|
||||||
|
const icon = track.status === 'created' ? '✓' : '⊘';
|
||||||
|
const statusText = track.status === 'created' ? 'Created' : 'Already exists';
|
||||||
|
html += `<li class="${className}">${icon} ${track.filename} <em>(${statusText})</em></li>`;
|
||||||
|
});
|
||||||
|
html += `</ul>`;
|
||||||
|
html += `</div>`;
|
||||||
|
document.getElementById('results').innerHTML = html;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user