problem with youtube in docker

This commit is contained in:
2025-12-30 01:06:33 +01:00
parent 0656ca54b2
commit aa4b10c991
6 changed files with 1138 additions and 0 deletions

46
Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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&#10;3:45 Second Track&#10;7:30 Third Song&#10;&#10;(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>